diff --git a/.github/workflows/android-espresso.yml b/.github/workflows/android-espresso.yml new file mode 100644 index 0000000000..c1ecedda38 --- /dev/null +++ b/.github/workflows/android-espresso.yml @@ -0,0 +1,208 @@ +name: Android Espresso E2E + +on: + workflow_dispatch: + push: + branches: [master, develop, testlio/ui-tests] + pull_request: + branches: [master, develop, testlio/ui-tests] + +concurrency: + group: e2e-android-espresso-${{ github.ref }} + cancel-in-progress: true + +jobs: + android-espresso-tests: + name: Android Espresso UI Tests + runs-on: ubuntu-latest + timeout-minutes: 120 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Free disk space + run: | + echo "=== Disk before cleanup ===" + df -h + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android/sdk/ndk/2[0-6]* + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + echo "=== Disk after cleanup ===" + df -h + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: '.node-version' + cache: 'yarn' + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Create env file + env: + ENV_FILE: ${{ secrets.ENV_DEVELOPMENT }} + run: echo "$ENV_FILE" > .env.development + + - name: Create Sentry properties + run: | + echo "${{ secrets.SENTRY_PROPERTIES }}" > sentry.properties + echo "${{ secrets.SENTRY_PROPERTIES }}" > android/sentry.properties + + - name: Set native config values + run: yarn set:dev + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: gradle- + + - name: Enable KVM for emulator + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Start Metro bundler + run: | + npx react-native start & + echo $! > /tmp/metro.pid + npx wait-on http://localhost:8081/status --timeout 60000 + + - name: Build debug APK and test APK + run: | + cd android + ./gradlew assembleDebug assembleDebugAndroidTest \ + -PreactNativeArchitectures=x86_64 \ + --no-daemon + + - name: Start Android emulator and run Espresso tests + uses: reactivecircus/android-emulator-runner@v2 + timeout-minutes: 75 + with: + api-level: 34 + arch: x86_64 + profile: pixel_6 + avd-name: e2e-emulator + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -memory 4096 + disable-animations: true + script: | + adb logcat -c + adb logcat > /home/runner/work/_temp/device-logcat.txt & + echo $! > /tmp/logcat.pid + adb reverse tcp:8081 tcp:8081 + bash scripts/run-tests.sh + kill $(cat /tmp/logcat.pid) || true + + - name: Stop Metro bundler + if: always() + run: kill $(cat /tmp/metro.pid) || true + + - name: Copy Allure results to workspace + if: always() + run: | + mkdir -p allure-results + cp -r /home/runner/work/_temp/allure-results/. allure-results/ || true + find allure-results -type f || echo "nothing" + + - name: Check Allure results were produced + if: always() + run: | + if [ -z "$(find allure-results -name '*.json' 2>/dev/null)" ]; then + echo "::warning::No Allure result JSON files found under allure-results/" + else + echo "Allure results found:" + find allure-results -name '*.json' + fi + + - name: Upload device logcat + if: always() + uses: actions/upload-artifact@v4 + with: + name: device-logcat + path: /home/runner/work/_temp/device-logcat.txt + retention-days: 7 + + - name: Upload raw Espresso test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: espresso-android-results + path: android/app/build/outputs/androidTest-results/connected/ + retention-days: 7 + + - name: Install Allure CLI + if: always() + run: | + sudo apt-get update -q + sudo apt-get install -y -q default-jre-headless + curl -fLo allure.tgz https://github.com/allure-framework/allure2/releases/download/2.29.0/allure-2.29.0.tgz + tar -xzf allure.tgz + sudo mv allure-2.29.0 /opt/allure + sudo ln -sf /opt/allure/bin/allure /usr/local/bin/allure + + - name: Zip Allure Results + if: always() + run: | + rm -f allure-results.zip + zip -r allure-results.zip allure-results/ + echo "=== Zip contents ===" + unzip -l allure-results.zip | head -30 + + - name: Upload Allure Results Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: allure-results + path: allure-results.zip + retention-days: 7 + + - name: Generate Allure HTML Report + if: always() + run: allure generate allure-results --clean -o allure-report + + - name: Upload Allure HTML Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: allure-html-report + path: allure-report + retention-days: 7 + + - name: Install Testlio CLI + if: always() + run: npm install -g @testlio/cli + + - name: Upload Results to Testlio + if: always() + env: + RUN_API_TOKEN: ${{ secrets.TESTLIO_RUN_TOKEN }} + TESTLIO_DEVICE_ID: 363a5ce7-6cef-436c-8531-6c07195c8fe8 + run: | + testlio create-run \ + --testConfig testlio-cli/test-config.json \ + --projectConfig testlio-cli/project-config.json \ + --externalResults true \ + --resultProvider local \ + --automatedDeviceIds "$TESTLIO_DEVICE_ID" + + testlio parse-run-results \ + --projectConfig testlio-cli/project-config.json \ + --path allure-results.zip \ + --automatedDeviceId "$TESTLIO_DEVICE_ID" + + testlio finalize-results \ + --projectConfig testlio-cli/project-config.json \ No newline at end of file diff --git a/.github/workflows/ios-xcuitest.yml b/.github/workflows/ios-xcuitest.yml new file mode 100644 index 0000000000..9a3075a42f --- /dev/null +++ b/.github/workflows/ios-xcuitest.yml @@ -0,0 +1,215 @@ +name: iOS XCUITest + +on: + workflow_dispatch: + push: + branches: [master, develop, testlio/ui-tests] + pull_request: + branches: [master, develop, testlio/ui-tests] + +jobs: + ios-ui-tests: + runs-on: macos-latest + timeout-minutes: 180 + + steps: + # Checkout Repo + - name: Checkout Repo + uses: actions/checkout@v4 + + # Select Xcode + - name: Select Xcode 26.0.1 + run: sudo xcode-select -switch /Applications/Xcode_26.0.1.app + + # Setup Node + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: yarn + + # Install JS dependencies + - name: Install JS dependencies + run: yarn install --frozen-lockfile + + # Setup Ruby + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: false + + # Install Gems + - name: Install Gems + run: | + bundle config set deployment false + bundle install + + # Disable Sentry upload + - name: Disable Sentry upload + run: | + echo "SENTRY_DISABLE_AUTO_UPLOAD=true" >> $GITHUB_ENV + + # Install Pods + - name: Install Pods + run: | + cd ios + bundle exec pod install + + # Generate React Native Bundle + - name: Generate React Native Bundle + run: | + npx react-native bundle \ + --entry-file index.js \ + --platform ios \ + --dev false \ + --bundle-output ios/main.jsbundle \ + --assets-dest ios + + # Boot Simulator + - name: Boot Simulator + run: | + xcrun simctl boot "iPhone 17" || true + xcrun simctl bootstatus "iPhone 17" -b + + # Apply production configuration + - name: Apply production configuration + run: | + export NODE_ENV=production + yarn set:prod + + # Build for testing + - name: Build for testing + run: | + set -o pipefail + xcodebuild \ + -workspace ios/BitPayApp.xcworkspace \ + -scheme BitPayApp \ + -configuration Release \ + -destination 'platform=iOS Simulator,name=iPhone 17' \ + -derivedDataPath Build \ + -only-testing:BitPayAppUITests \ + build-for-testing + + # Run XCUITest + # - name: Run XCUITest + # run: | + # set -o pipefail + # xcodebuild \ + # -workspace ios/BitPayApp.xcworkspace \ + # -scheme BitPayApp \ + # -configuration Release \ + # -destination 'platform=iOS Simulator,name=iPhone 17' \ + # -derivedDataPath Build \ + # -resultBundlePath TestResults.xcresult \ + # -parallel-testing-enabled NO \ + # -only-testing:BitPayAppUITests \ + # test + + - name: Run XCUITest in order + run: | + set -e + set -o pipefail + + rm -rf TestResults.xcresult + + xcodebuild \ + -workspace ios/BitPayApp.xcworkspace \ + -scheme BitPayApp \ + -configuration Release \ + -destination 'platform=iOS Simulator,name=iPhone 17' \ + -derivedDataPath Build \ + -resultBundlePath TestResults.xcresult \ + -parallel-testing-enabled NO \ + -only-testing:BitPayAppUITests/Test591_OnboardingCreateWallet \ + -only-testing:BitPayAppUITests/Test592_ImportWalletRecoveryPhrase \ + -only-testing:BitPayAppUITests/Test593_BTCConfirmPaymentScreen \ + -only-testing:BitPayAppUITests/Test594_BasicSwapBTC \ + -only-testing:BitPayAppUITests/Test595_BasicSellBTC \ + -only-testing:BitPayAppUITests/Test596_BasicBuyBTC \ + test + + # Upload XCResult + - name: Upload XCUITest Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: xcuitest-results + path: TestResults.xcresult + + # Install Allure CLI + - name: Install Allure CLI + if: always() + run: brew install allure || true + + # Install allure-xcresult + - name: Install allure-xcresult + if: always() + run: | + if ! command -v allure-xcresult &> /dev/null; then + curl -L -o allure-xcresult https://github.com/kvld/allure-xcresult/releases/download/v1.5.0/allure-xcresult + chmod +x allure-xcresult + sudo mv allure-xcresult /usr/local/bin/ + fi + + # Convert to Allure + - name: Convert XCResult to Allure Results + if: always() + run: | + allure-xcresult \ + --input TestResults.xcresult \ + --output allure-results + + # Zip Allure + - name: Zip Allure Results + if: always() + run: | + zip -r allure-results.zip allure-results + + # Upload Allure Results + - name: Upload Allure Results Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: allure-results + path: allure-results.zip + + # Generate HTML + - name: Generate Allure HTML Report + if: always() + run: | + allure generate allure-results --clean -o allure-report + + # Upload HTML + - name: Upload Allure HTML Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: allure-html-report + path: allure-report + + # Install Testlio CLI + - name: Install Testlio CLI + if: always() + run: npm install -g @testlio/cli + + # Upload to Testlio + - name: Upload Results to Testlio + if: always() + run: | + export RUN_API_TOKEN="${{ secrets.TESTLIO_RUN_TOKEN }}" + + testlio create-run \ + --testConfig testlio-cli/test-config.json \ + --projectConfig testlio-cli/project-config.json \ + --externalResults true \ + --resultProvider local \ + --automatedDeviceIds 363a5ce7-6cef-436c-8531-6c07195c8fe8 + + testlio parse-run-results \ + --projectConfig testlio-cli/project-config.json \ + --path allure-results.zip \ + --automatedDeviceId 363a5ce7-6cef-436c-8531-6c07195c8fe8 + + testlio finalize-results \ + --projectConfig testlio-cli/project-config.json diff --git a/README.md b/README.md index 1aca45d55c..5934ad4a03 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# tests + # BitPay App v2 Welcome to BitPay App v2! diff --git a/android/app/build.gradle b/android/app/build.gradle index a483cc4ee5..b0cbb346d0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,6 +24,7 @@ react { // skip the bundling of the JS bundle and the assets. By default is just 'debug'. // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. // debuggableVariants = ["liteDebug", "prodDebug"] + // debuggableVariants = [] /* Bundling */ // A list containing the node command and its flags. Default is just 'node'. @@ -54,6 +55,7 @@ react { /* Autolinking */ autolinkLibrariesWithApp() + } /** @@ -110,6 +112,7 @@ android { ndk { abiFilters "armeabi-v7a", "x86", "x86_64", "arm64-v8a" } + testInstrumentationRunner "io.qameta.allure.android.runners.AllureAndroidJUnitRunner" } splits { abi { @@ -163,9 +166,30 @@ android { } } + packagingOptions { + pickFirst 'lib/*/libfbjni.so' + } } dependencies { + //Espresso libraries + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1' + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test:rules:1.5.0' + androidTestImplementation 'androidx.test:core:1.5.0' + + androidTestImplementation 'org.hamcrest:hamcrest:2.2' + + androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0' + + // Allure reporting + androidTestImplementation "io.qameta.allure:allure-kotlin-model:2.4.0" + androidTestImplementation "io.qameta.allure:allure-kotlin-commons:2.4.0" + androidTestImplementation "io.qameta.allure:allure-kotlin-junit4:2.4.0" + androidTestImplementation "io.qameta.allure:allure-kotlin-android:2.4.0" + // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") implementation platform('com.google.firebase:firebase-bom:29.1.0') @@ -175,6 +199,10 @@ dependencies { implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") implementation("androidx.core:core-splashscreen:1.0.0") + // Test Orchestrator + TestStorage + androidTestUtil "androidx.test:orchestrator:1.5.1" + androidTestImplementation "androidx.test.services:test-services:1.5.0" + if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") } else { @@ -248,3 +276,4 @@ internal object BundleHash { // v.sources.java.addGeneratedSourceDirectory(generateHashTask, { it.outputs.files.singleFile }) // } } + diff --git a/android/app/src/androidTest/AndroidManifest.xml b/android/app/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..343a7b66c4 --- /dev/null +++ b/android/app/src/androidTest/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/base/BaseTest.kt b/android/app/src/androidTest/java/com/bitpay/wallet/base/BaseTest.kt new file mode 100644 index 0000000000..110afd3c3d --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/base/BaseTest.kt @@ -0,0 +1,135 @@ +package com.bitpay.wallet.base + +import android.content.Intent +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.bitpay.wallet.pages.OnboardingPage +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import com.bitpay.wallet.utils.allureScreenshot +import org.junit.Rule +import androidx.test.uiautomator.Until +import androidx.test.uiautomator.By +import org.junit.After +import org.junit.rules.TestName + +open class BaseTest { + + @get:Rule + val testName = TestName() + + companion object { + var skipRelaunch: Boolean = false + var skipOnboardingHandling: Boolean = false + } + + @get:Rule + val screenshotOnFailureRule = object : TestWatcher() { + override fun failed(e: Throwable?, description: Description?) { + allureScreenshot("Failure - ${description?.methodName}") + } + } + + private val onboardingPage = OnboardingPage() + +// private fun clearAppData() { +// val instrumentation = InstrumentationRegistry.getInstrumentation() +// val packageName = instrumentation.targetContext.packageName +// instrumentation.uiAutomation.executeShellCommand( +// "pm clear $packageName" +// ).close() +// Thread.sleep(2000) // wait for clear to complete before relaunch +// } + + + @Before + fun setup() { + if (!skipRelaunch) launchApp() + if (!skipOnboardingHandling) handleOnboardingIfPresent() + } + + @After + fun stopRecording() { + // val video = ScreenRecorder.stop() + // allureVideo("Screen Recording - ${testName.methodName}", video) + } + + private fun launchApp() { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val context = instrumentation.targetContext + + val device: UiDevice? = try { + UiDevice.getInstance(instrumentation) + } catch (e: IllegalStateException) { + android.util.Log.w("BaseTest", "UiAutomation already registered: ${e.message}") + null + } + + device?.let { if (!it.isScreenOn) it.wakeUp() } + + val intent = context.packageManager.getLaunchIntentForPackage("com.bitpay.wallet") + intent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + context.startActivity(intent) + + // CI emulators are typically slower than local hardware to load and + // render the JS bundle. Give RN's startup a generous window before any + // test attempts its first onView() lookup, rather than racing ahead. + device?.wait(Until.hasObject(By.pkg("com.bitpay.wallet").depth(0)), 20000) + Thread.sleep(2000) + } + +// private fun launchApp() { +// val instrumentation = InstrumentationRegistry.getInstrumentation() +// val context = instrumentation.targetContext +// +// val device: UiDevice? = try { +// UiDevice.getInstance(instrumentation) +// } catch (e: IllegalStateException) { +// android.util.Log.w( +// "BaseTest", +// "UiAutomation already registered, skipping UiDevice setup: ${e.message}" +// ) +// null +// } +// +// device?.let { +// if (!it.isScreenOn) { +// it.wakeUp() +// } +// } +// +// val intent = +// context.packageManager.getLaunchIntentForPackage("com.bitpay.wallet") +// +// intent!!.addFlags( +// Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK +// ) +// +// context.startActivity(intent) +// } + + /** + * If the onboarding "Continue without an account" screen is currently + * displayed, walk through the standard onboarding flow once so every + * test starts from a consistent post-onboarding state. + * If that screen isn't present (e.g. app is already past onboarding, + * or skipRelaunch left it on a different screen), this is a no-op. + */ + private fun handleOnboardingIfPresent() { + if (!onboardingPage.isContinueWithoutAccountDisplayed()) { + return + } + + onboardingPage.waitForPageToLoad() + onboardingPage.clickContinueWithoutAccount() + onboardingPage.clickSkip() // Skip turn on notifications + + assertTrue( + "Protect Your Wallet was not displayed", + onboardingPage.verifyProtectYourWalletIsDisplayed() + ) + onboardingPage.clickSkip() + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/AddYourCryptoOptionPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/AddYourCryptoOptionPage.kt new file mode 100644 index 0000000000..3c0e0d1f9f --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/AddYourCryptoOptionPage.kt @@ -0,0 +1,27 @@ +package com.bitpay.wallet.pages + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import com.bitpay.wallet.utils.WaitUtils +import com.bitpay.wallet.utils.WaitUtils.withTestId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import org.hamcrest.Matchers.allOf + +class AddYourCryptoOptionPage { + + // ---- Locators ---- + private val importKeyButton = withTestId("creation-options-import-button") + + + // ---- Actions ---- + + + fun clickImportKey() { + WaitUtils.waitForView(importKeyButton) + onView(importKeyButton).perform(click()) + } + +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/ConfirmPaymentPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/ConfirmPaymentPage.kt new file mode 100644 index 0000000000..236d07b380 --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/ConfirmPaymentPage.kt @@ -0,0 +1,43 @@ +package com.bitpay.wallet.pages + +import androidx.test.espresso.Espresso.onView +import com.bitpay.wallet.utils.WaitUtils +import com.bitpay.wallet.utils.WaitUtils.withTestId +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.pressImeActionButton +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText + +class ConfirmPaymentPage { + + // ---- Locators ---- + private val confirmPaymentText = withText("Confirm Payment") + + private val summaryText = withText("SUMMARY") + + + // ---- Actions ---- + + fun verifyConfirmPaymentTitleDisplayed(): Boolean { + return try { + WaitUtils.waitForView(confirmPaymentText) + onView(confirmPaymentText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + fun verifySummaryTextDisplayed(): Boolean { + return try { + WaitUtils.waitForView(summaryText) + onView(summaryText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/HomePage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/HomePage.kt new file mode 100644 index 0000000000..45058bd125 --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/HomePage.kt @@ -0,0 +1,74 @@ +package com.bitpay.wallet.pages + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import com.bitpay.wallet.utils.WaitUtils +import com.bitpay.wallet.utils.WaitUtils.withTestId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription + +class HomePage { + + // ---- Locators ---- + private val portfolioBalanceText = withTestId("portfolio-balance-info-button") + private val myCryptoAddButton = withTestId("my-crypto-add-button") + private val buyButton = withTestId("buy-button") + private val sellButton = withTestId("sell-button") + private val swapButton = withTestId("swap-button") + private val receiveButton = withTestId("receive-button") + private val sendButton = withTestId("send-button") + + + // ---- Actions ---- + + fun waitForPageToLoad() { + WaitUtils.waitForView( + portfolioBalanceText, + timeoutMs = 120000 + ) + } + + fun verifyPortfolioBalanceTextDisplayed(): Boolean { + return try { + WaitUtils.waitForView(portfolioBalanceText) + onView(portfolioBalanceText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + fun clickAddYourCrypto() { + WaitUtils.waitForView(myCryptoAddButton) + onView(myCryptoAddButton).perform(click()) + } + + fun clickBuy() { + WaitUtils.waitForView(buyButton) + onView(buyButton).perform(click()) + } + + fun clickSell() { + WaitUtils.waitForView(sellButton) + onView(sellButton).perform(click()) + } + + fun clickSwap() { + WaitUtils.waitForView(swapButton) + onView(swapButton).perform(click()) + } + + fun clickReceive() { + WaitUtils.waitForView(receiveButton) + onView(receiveButton).perform(click()) + } + + fun clickSend() { + // WaitUtils.waitForView(sendButton) + WaitUtils.waitForViewEnabled(sendButton) + onView(sendButton).perform(click()) + } + +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/ImportWalletPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/ImportWalletPage.kt new file mode 100644 index 0000000000..bfdf2ad0e7 --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/ImportWalletPage.kt @@ -0,0 +1,37 @@ +package com.bitpay.wallet.pages + +import androidx.test.espresso.Espresso.onView +import com.bitpay.wallet.utils.WaitUtils +import com.bitpay.wallet.utils.WaitUtils.withTestId +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.pressImeActionButton + +class ImportWalletPage { + + // ---- Locators ---- + private val recoveryPhraseInputField = withTestId("import-text-input") + private val importWalletButton = withTestId("import-wallet-button") + + + // ---- Actions ---- + + fun pressKeyboardTick() { + onView(recoveryPhraseInputField).perform(pressImeActionButton()) + } + + fun enterRecoveryPhrase(recoveryPhrase: String) { + WaitUtils.waitForView(recoveryPhraseInputField) + onView(recoveryPhraseInputField).perform( + ViewActions.click(), + ViewActions.typeText(recoveryPhrase), + ViewActions.closeSoftKeyboard() + ) + } + + fun clickImportWallet() { + WaitUtils.waitForView(importWalletButton) + onView(importWalletButton).perform(click()) + } + +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/KeyboardPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/KeyboardPage.kt new file mode 100644 index 0000000000..49c344dd11 --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/KeyboardPage.kt @@ -0,0 +1,114 @@ +package com.bitpay.wallet.pages + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import com.bitpay.wallet.utils.WaitUtils +import com.bitpay.wallet.utils.WaitUtils.withTestId +import org.hamcrest.Matchers.not +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import org.hamcrest.Matchers.allOf + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.CoordinatesProvider +import androidx.test.espresso.action.GeneralClickAction +import androidx.test.espresso.action.Press +import androidx.test.espresso.action.Tap +import androidx.test.espresso.matcher.ViewMatchers.withText +import android.view.View + +class KeyboardPage { + + // ---- Locators ---- + + private val continueButton = withText("Continue") + + + // ---- Actions ---- + fun enterAmount(amount: String = "0") { + for (char in amount) { + val key = allOf( + withText(char.toString()), + not(hasSibling(withText("BTC"))) + ) + WaitUtils.waitForView(key) + onView(key).perform(click()) + } + } + + fun clickContinue() { + WaitUtils.waitForViewEnabled(continueButton) + onView(continueButton).perform(click()) + } + + + /** + * Backspace key does not expose a unique accessibility identifier. + * Calculate its position dynamically using the relative spacing + * between the keypad keys "6" and "9". + */ + fun clickBackspace(count: Int = 1) { + + val sixKey = withText("6") + val nineKey = withText("9") + + WaitUtils.waitForView(sixKey) + WaitUtils.waitForView(nineKey) + + var sixView: View? = null + var nineView: View? = null + + onView(sixKey).perform(object : androidx.test.espresso.ViewAction { + override fun getConstraints() = org.hamcrest.Matchers.any(View::class.java) + override fun getDescription() = "capture six key view" + override fun perform(uiController: androidx.test.espresso.UiController, view: View) { + sixView = view + } + }) + + onView(nineKey).perform(object : androidx.test.espresso.ViewAction { + override fun getConstraints() = org.hamcrest.Matchers.any(View::class.java) + override fun getDescription() = "capture nine key view" + override fun perform(uiController: androidx.test.espresso.UiController, view: View) { + nineView = view + } + }) + + val sixLocation = IntArray(2) + val nineLocation = IntArray(2) + sixView!!.getLocationOnScreen(sixLocation) + nineView!!.getLocationOnScreen(nineLocation) + + val sixMidY = sixLocation[1] + (sixView!!.height / 2) + val nineMidY = nineLocation[1] + (nineView!!.height / 2) + val nineMidX = nineLocation[0] + (nineView!!.width / 2) + + val rowHeight = Math.abs(nineMidY - sixMidY) + + val backspaceX = nineMidX.toFloat() + val backspaceY = (nineMidY + rowHeight).toFloat() + + val backspaceCoordinatesProvider = CoordinatesProvider { _ -> + floatArrayOf(backspaceX, backspaceY) + } + + repeat(count) { + onView(withText("9")).perform( + GeneralClickAction( + Tap.SINGLE, + backspaceCoordinatesProvider, + Press.FINGER, + android.view.InputDevice.SOURCE_UNKNOWN, + android.view.MotionEvent.BUTTON_PRIMARY + ) + ) + } + } + + +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/MyKeyPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/MyKeyPage.kt new file mode 100644 index 0000000000..f50418dfb8 --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/MyKeyPage.kt @@ -0,0 +1,50 @@ +package com.bitpay.wallet.pages + +import androidx.test.espresso.Espresso.onView +import com.bitpay.wallet.utils.WaitUtils +import com.bitpay.wallet.utils.WaitUtils.withTestId +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.pressImeActionButton +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText + +class MyKeyPage { + + // ---- Locators ---- + private val myKeyText = withText("My Key") + private val bitcoinText = withText("Bitcoin") + + + // ---- Actions ---- + + fun waitForPageToLoad() { + WaitUtils.waitForView( + myKeyText, + timeoutMs = 900000 + ) + } + + fun verifyMyKeyDisplayed(): Boolean { + return try { + WaitUtils.waitForView(myKeyText, 900000) + onView(myKeyText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + fun verifyBitcoinTextDisplayed(): Boolean { + return try { + WaitUtils.waitForView(bitcoinText) + onView(bitcoinText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/OnboardingPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/OnboardingPage.kt new file mode 100644 index 0000000000..b4b3cf1147 --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/OnboardingPage.kt @@ -0,0 +1,166 @@ +package com.bitpay.wallet.pages + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import com.bitpay.wallet.utils.WaitUtils +import com.bitpay.wallet.utils.WaitUtils.withTestId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withResourceName +import org.hamcrest.Matchers.allOf + +class OnboardingPage { + + // ---- Locators ---- + private val getStartedButton = withTestId("get-started-button") + private val continueWithoutAccountButton = withTestId("continue-without-an-account-button") + private val skipButton = withTestId("skip-button") + private val createKeyButton = withTestId("create-a-key-button") + private val alreadyHaveKeyButton = withTestId("i-already-have-a-key-button") + private val protectYourWalletText = withText("Protect your wallet") + private val backupKeyPromptText = withText("Would you like to backup your key?") + private val bottomSheetLaterButton = withTestId("bottom-notification-secondary-action-button") + private val bottomSheetBackupYourKeyButton = + withTestId("bottom-notification-primary-action-button") + private val backupRecoveryPhraseElement = withContentDescription("Backup your recovery phrase") + + // private val iUnderstandCheckBox1 = withTestId("first-term-checkbox") + + private val iUnderstandCheckBox1 = allOf( + withTestId("first-term-checkbox"), + withContentDescription("Checkbox") + ) + + private val iUnderstandCheckBox2 = allOf( + withTestId("second-term-checkbox"), + withContentDescription("Checkbox") + ) + + private val iUnderstandCheckBox3 = allOf( + withTestId("third-term-checkbox"), + withContentDescription("Checkbox") + ) + +// private val iUnderstandCheckBox2 = withTestId("second-term-checkbox") +// private val iUnderstandCheckBox3 = withTestId("third-term-checkbox") + + // private val iUnderstandCheckBox2 = withTestId("second-term-checkbox") +// private val iUnderstandCheckBox3 = withTestId("third-term-checkbox") + private val agreeAndContinueButton = withTestId("agree-and-continue-button") + + + // ---- Actions ---- + + fun waitForPageToLoad() { + WaitUtils.waitForView( + continueWithoutAccountButton, + timeoutMs = 600000 + ) + } + + fun clickGetStarted() { + WaitUtils.waitForView(getStartedButton) + onView(getStartedButton).perform(click()) + } + + fun isContinueWithoutAccountDisplayed(): Boolean { + return try { + WaitUtils.waitForView(continueWithoutAccountButton, timeoutMs = 120000) + true + } catch (e: Throwable) { + android.util.Log.e("OnboardingPage", "isContinueWithoutAccountDisplayed failed", e) + false + } + } + + fun clickContinueWithoutAccount() { + WaitUtils.waitForView(continueWithoutAccountButton) + onView(continueWithoutAccountButton).perform(click()) + } + + fun clickSkip() { + WaitUtils.waitForView(skipButton) + onView(skipButton).perform(click()) + } + + fun clickCreateKey() { + WaitUtils.waitForView(createKeyButton) + onView(createKeyButton).perform(click()) + } + + fun clickAlreadyHaveKey() { + WaitUtils.waitForView(alreadyHaveKeyButton) + onView(alreadyHaveKeyButton).perform(click()) + } + + fun verifyProtectYourWalletIsDisplayed(): Boolean { + return try { + WaitUtils.waitForView(protectYourWalletText) + onView(protectYourWalletText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + fun verifyBackupKeyPromptIsDisplayed(): Boolean { + return try { + WaitUtils.waitForView(backupKeyPromptText, timeoutMs = 600000) + onView(backupKeyPromptText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + fun clickBottomSheetBackUpYourKey() { + WaitUtils.waitForView(bottomSheetBackupYourKeyButton) + onView(bottomSheetBackupYourKeyButton).perform(click()) + } + + fun clickBottomSheetLater() { + WaitUtils.waitForView(bottomSheetLaterButton) + onView(bottomSheetLaterButton).perform(click()) + } + + fun clickBackupRecoveryPhrase() { + WaitUtils.waitForView(backupRecoveryPhraseElement) + onView(backupRecoveryPhraseElement).perform(click()) + } + + fun verifyIUnderstandCheckbox1Displayed(): Boolean { + return try { + WaitUtils.waitForView(iUnderstandCheckBox1, 900000) + onView(iUnderstandCheckBox1).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + fun clickIUnderstandCheckbox1() { + WaitUtils.waitForView(iUnderstandCheckBox1) + onView(iUnderstandCheckBox1).perform(click()) + Thread.sleep(300) + } + + fun clickIUnderstandCheckbox2() { + WaitUtils.waitForView(iUnderstandCheckBox2) + onView(iUnderstandCheckBox2).perform(click()) + Thread.sleep(300) + } + + fun clickIUnderstandCheckbox3() { + WaitUtils.waitForView(iUnderstandCheckBox3) + onView(iUnderstandCheckBox3).perform(click()) + Thread.sleep(300) + } + + fun clickAgreeAndContinue() { + WaitUtils.waitForView(agreeAndContinueButton) + onView(agreeAndContinueButton).perform(click()) + } + +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/SelectCurrencyPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/SelectCurrencyPage.kt new file mode 100644 index 0000000000..e38fb9db8f --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/SelectCurrencyPage.kt @@ -0,0 +1,186 @@ +package com.bitpay.wallet.pages + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import com.bitpay.wallet.utils.WaitUtils +import com.bitpay.wallet.utils.WaitUtils.withTestId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.withSubstring +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import com.bitpay.wallet.utils.WaitUtils.withIndex +import org.hamcrest.Matchers.allOf + +class SelectCurrencyPage { + + // ---- Locators ---- + private val selectCurrencyText = withText("Select a Currency") + private val bitcoinText = withSubstring("Bitcoin") + private val ethereumText = withSubstring("Ethereum") + private val sendToText = withText("Send To") + private val sendToAddressInput = withTestId("send-to-address-input") + private val swapText = withText("Swap") + private val swapFrom = withTestId("swap-crypto-from-wallet-selector") + private val swapTo = withTestId("swap-crypto-to-wallet-selector") + private val selectKeyToDepositToText = withText("Select Key to Deposit to") + private val myKeyWallet = withIndex(withTestId("wallet-card-My Key"), 1) + private val evmAccount = withText("EVM Account") + private val enterSwapAmountButton =withTestId("swap-crypto-enter-amount-button") + private val minButton = withContentDescription("MIN") + private val swapChangellyTermsCheckbox =withTestId("swap-crypto-changelly-terms-checkbox") + private val slideToSwipeButton = withContentDescription("Slide to swap") + + private val buyText = withText("Buy") + private val sellText = withText("Sell") + private val buyContinueButton = withTestId("button") + private val chooseCrypto = withText("Choose Crypto") + + + + + + + + + + // ---- Actions ---- + fun verifySelectCurrencyDisplayed(): Boolean { + return try { + WaitUtils.waitForView(selectCurrencyText) + onView(selectCurrencyText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + fun clickBitcoin() { + WaitUtils.waitForView(bitcoinText) + onView(bitcoinText).perform(click()) + } + + fun clickEthereum() { + WaitUtils.waitForView(ethereumText) + onView(ethereumText).perform(click()) + } + + fun verifySendToDisplayed(): Boolean { + return try { + WaitUtils.waitForView(sendToText) + onView(sendToText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + fun enterSendToAddress(sendToAddress: String) { + WaitUtils.waitForView(sendToAddressInput) + onView(sendToAddressInput).perform( + ViewActions.click(), + ViewActions.typeText(sendToAddress), + ViewActions.closeSoftKeyboard() + ) + } + + fun verifySwapTitleDisplayed(): Boolean { + return try { + WaitUtils.waitForView(swapText) + onView(swapText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + fun verifyBuyTitleDisplayed(): Boolean { + return try { + WaitUtils.waitForView(buyText) + onView(buyText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + fun verifySellTitleDisplayed(): Boolean { + return try { + WaitUtils.waitForView(sellText) + onView(sellText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + fun clickSwapFromOption() { + WaitUtils.waitForView(swapFrom) + onView(swapFrom).perform(click()) + } + + fun clickSwapToOption() { + WaitUtils.waitForView(swapTo) + onView(swapTo).perform(click()) + } + + fun verifySelectKeyToDepositToDisplayed(): Boolean { + return try { + WaitUtils.waitForView(selectKeyToDepositToText) + onView(selectKeyToDepositToText).check(matches(isDisplayed())) + true + } catch (e: Throwable) { + false + } + } + + fun clickEnterSwapAmount() { + WaitUtils.waitForView(enterSwapAmountButton) + onView(enterSwapAmountButton).perform(click()) + } + + fun clickMinSwapAmount() { + WaitUtils.waitForView(minButton) + onView(minButton).perform(click()) + } + + fun clickSecondMyKeyWallet() { + WaitUtils.waitForView(myKeyWallet) + onView(myKeyWallet).perform(click()) + } + + fun clickEVMAccount() { + WaitUtils.waitForView(evmAccount) + onView(evmAccount).perform(click()) + } + + fun clickChangellyTermsCheckbox() { + WaitUtils.waitForView(swapChangellyTermsCheckbox) + onView(swapChangellyTermsCheckbox).perform(click()) + } + + fun verifySlideToSwapButtonEnabled(): Boolean { + return try { + WaitUtils.waitForViewEnabled(slideToSwipeButton) + true + } catch (e: Throwable) { + false + } + } + + fun verifyBuyContinueButtonEnabled(): Boolean { + return try { + WaitUtils.waitForViewEnabled(buyContinueButton) + true + } catch (e: Throwable) { + false + } + } + + fun clickChooseCrypto() { + WaitUtils.waitForView(chooseCrypto) + onView(chooseCrypto).perform(click()) + } + +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test591OnboardingCreateWallet.kt b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test591OnboardingCreateWallet.kt new file mode 100644 index 0000000000..201ea1ab6a --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test591OnboardingCreateWallet.kt @@ -0,0 +1,65 @@ +package com.bitpay.wallet.tests + +import com.bitpay.wallet.base.BaseTest +import com.bitpay.wallet.pages.HomePage +import com.bitpay.wallet.pages.OnboardingPage +import com.bitpay.wallet.utils.allureStep +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bitpay.wallet.utils.allureScreenshot +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +class Test591OnboardingCreateWallet : BaseTest() { + private val onboardingPage = OnboardingPage() + private val homePage = HomePage() + + @Test + fun testOnboardingCreateWallet() { + allureStep("Click Create Key on onboarding screen") { + onboardingPage.clickCreateKey() + allureScreenshot("After Create Key clicked") + } + + allureStep("Verify backup key prompt is displayed") { + assertTrue( + "Backup key prompt was not displayed", + onboardingPage.verifyBackupKeyPromptIsDisplayed() + ) + allureScreenshot("Backup key prompt displayed") + } + + allureStep("Skip backup prompt") { + onboardingPage.clickSkip() // Skip "Would you like to backup" + } + + allureStep("Dismiss bottom sheet (Later)") { + onboardingPage.clickBottomSheetLater() + } + + allureStep("Check all three 'I understand' checkboxes") { + onboardingPage.clickIUnderstandCheckbox1() + onboardingPage.clickIUnderstandCheckbox2() + onboardingPage.clickIUnderstandCheckbox3() + } + + allureStep("Click Agree and Continue") { + onboardingPage.clickAgreeAndContinue() + } + + allureStep("Wait for Home page to load") { + homePage.waitForPageToLoad() + } + + allureStep("Verify portfolio balance is displayed on Home page") { + assertTrue( + "Home Page - Portfolio balance text not displayed", + homePage.verifyPortfolioBalanceTextDisplayed() + ) + allureScreenshot("Final - Portfolio balance verified") + } + + } +} diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test592ImportWalletRecoveryPhrase.kt b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test592ImportWalletRecoveryPhrase.kt new file mode 100644 index 0000000000..7b724f8f2e --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test592ImportWalletRecoveryPhrase.kt @@ -0,0 +1,102 @@ +package com.bitpay.wallet.tests + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bitpay.wallet.base.BaseTest +import com.bitpay.wallet.pages.AddYourCryptoOptionPage +import com.bitpay.wallet.pages.HomePage +import com.bitpay.wallet.pages.ImportWalletPage +import com.bitpay.wallet.pages.MyKeyPage +import com.bitpay.wallet.pages.OnboardingPage +import com.bitpay.wallet.utils.allureScreenshot +import com.bitpay.wallet.utils.allureStep +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class Test592ImportWalletRecoveryPhrase : BaseTest() { + + private val onboardingPage = OnboardingPage() + private val homePage = HomePage() + private val addYourCryptoOptionPage = AddYourCryptoOptionPage() + private val importWalletPage = ImportWalletPage() + private val myKeyPage = MyKeyPage() + + + @Test + fun testImportWalletRecoveryPhrase() { + + allureStep("Click Create Key on onboarding screen") { + onboardingPage.clickCreateKey() + } + + allureStep("Verify backup key prompt is displayed") { + assertTrue( + "Backup key prompt was not displayed", + onboardingPage.verifyBackupKeyPromptIsDisplayed() + ) + allureScreenshot("Backup key prompt displayed") + } + + allureStep("Skip backup prompt") { + onboardingPage.clickSkip() // Skip "Would you like to backup" + } + + allureStep("Dismiss bottom sheet (Later)") { + onboardingPage.clickBottomSheetLater() + } + + allureStep("Check all three 'I understand' checkboxes") { + onboardingPage.clickIUnderstandCheckbox1() + onboardingPage.clickIUnderstandCheckbox2() + onboardingPage.clickIUnderstandCheckbox3() + } + + allureStep("Click Agree and Continue") { + onboardingPage.clickAgreeAndContinue() + } + + allureStep("Wait for Home page to load") { + homePage.waitForPageToLoad() + allureScreenshot("Home page loaded") + } + + allureStep("Verify portfolio balance is displayed on Home page") { + assertTrue( + "Home Page - Portfolio balance text not displayed", + homePage.verifyPortfolioBalanceTextDisplayed() + ) + } + + allureStep("Click Add Your Crypto on Home page") { + homePage.clickAddYourCrypto() + } + + allureStep("Click Import Key option") { + addYourCryptoOptionPage.clickImportKey() + } + + allureStep("Enter recovery phrase and import wallet") { + importWalletPage.enterRecoveryPhrase("hobby short divert lady spare quit act settle body town license alone") + allureScreenshot("Recovery phrase entered") + importWalletPage.clickImportWallet() + } + + allureStep("Verify wallet import succeeded - My Key page displayed") { + assertTrue( + "Wallet Import Failed - My Key not displayed", + myKeyPage.verifyMyKeyDisplayed() + ) + allureScreenshot("My Key page displayed after import") + } + + allureStep("Verify Bitcoin is displayed in My Key page") { + assertTrue( + "Bitcoin not displayed in My Key page", + myKeyPage.verifyBitcoinTextDisplayed() + ) + allureScreenshot("Final - Bitcoin shown in My Key page") + } + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test593SendBTC.kt b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test593SendBTC.kt new file mode 100644 index 0000000000..103921a811 --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test593SendBTC.kt @@ -0,0 +1,122 @@ +package com.bitpay.wallet.tests + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bitpay.wallet.base.BaseTest +import com.bitpay.wallet.pages.AddYourCryptoOptionPage +import com.bitpay.wallet.pages.ConfirmPaymentPage +import com.bitpay.wallet.pages.HomePage +import com.bitpay.wallet.pages.ImportWalletPage +import com.bitpay.wallet.pages.KeyboardPage +import com.bitpay.wallet.pages.MyKeyPage +import com.bitpay.wallet.pages.OnboardingPage +import com.bitpay.wallet.pages.SelectCurrencyPage +import com.bitpay.wallet.utils.allureScreenshot +import com.bitpay.wallet.utils.allureStep +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class Test593SendBTC : BaseTest() { + + private val onboardingPage = OnboardingPage() + private val homePage = HomePage() + private val addYourCryptoOptionPage = AddYourCryptoOptionPage() + private val importWalletPage = ImportWalletPage() + private val myKeyPage = MyKeyPage() + private val selectCurrencyPage = SelectCurrencyPage() + private val keyboardPage = KeyboardPage() + private val confirmPaymentPage = ConfirmPaymentPage() + + @Test + fun testSendBTC() { + allureStep("Click 'Already have a key' on onboarding screen") { + onboardingPage.clickAlreadyHaveKey() + } + + allureStep("Enter recovery phrase and import wallet") { + importWalletPage.enterRecoveryPhrase("hobby short divert lady spare quit act settle body town license alone") + allureScreenshot("Recovery phrase entered") + importWalletPage.clickImportWallet() + } + + allureStep("Verify 'I understand' checkbox 1 is displayed") { + onboardingPage.verifyIUnderstandCheckbox1Displayed() + } + + allureStep("Check all three 'I understand' checkboxes") { + onboardingPage.clickIUnderstandCheckbox1() + onboardingPage.clickIUnderstandCheckbox2() + onboardingPage.clickIUnderstandCheckbox3() + } + + allureStep("Click Agree and Continue") { + onboardingPage.clickAgreeAndContinue() + } + + allureStep("Wait for Home page to load") { + homePage.waitForPageToLoad() + allureScreenshot("Home page loaded") + } + + allureStep("Verify portfolio balance is displayed on Home page") { + assertTrue( + "Home Page - Portfolio balance text not displayed", + homePage.verifyPortfolioBalanceTextDisplayed() + ) + } + + allureStep("Click Send on Home page") { + homePage.clickSend() + } + + allureStep("Verify Select Currency page is displayed") { + assertTrue( + "Select Currency page not displayed", + selectCurrencyPage.verifySelectCurrencyDisplayed() + ) + assertTrue( + "Select Currency page not displayed", + selectCurrencyPage.verifySelectCurrencyDisplayed() + ) + allureScreenshot("Select Currency page displayed") + } + + allureStep("Click Bitcoin currency option") { + selectCurrencyPage.clickBitcoin() + } + + allureStep("Verify Send To page is displayed") { + assertTrue( + "Send To page not displayed", + selectCurrencyPage.verifySendToDisplayed() + ) + } + + allureStep("Enter Send To address") { + selectCurrencyPage.enterSendToAddress("bc1q0effzahtsn685tyjppgukpvfhv37hrlm4g67ws") + } + + allureStep("Enter amount and click Continue") { + keyboardPage.enterAmount("0.00005") + keyboardPage.clickContinue() + } + + allureStep("Verify Confirm Payment page is displayed") { + assertTrue( + "Confirm Payment page not displayed", + confirmPaymentPage.verifyConfirmPaymentTitleDisplayed(), + ) + allureScreenshot("Confirm Payment page displayed") + } + + allureStep("Verify Summary text is displayed on Confirm Payment page") { + assertTrue( + "Confirm Payment - Summary page not displayed", + confirmPaymentPage.verifySummaryTextDisplayed() + ) + allureScreenshot("Final - Summary displayed before sending") + } + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test594SwapBTC.kt b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test594SwapBTC.kt new file mode 100644 index 0000000000..421af00aa4 --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test594SwapBTC.kt @@ -0,0 +1,118 @@ +package com.bitpay.wallet.tests + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bitpay.wallet.base.BaseTest +import com.bitpay.wallet.pages.AddYourCryptoOptionPage +import com.bitpay.wallet.pages.HomePage +import com.bitpay.wallet.pages.ImportWalletPage +import com.bitpay.wallet.pages.MyKeyPage +import com.bitpay.wallet.pages.OnboardingPage +import com.bitpay.wallet.pages.SelectCurrencyPage +import com.bitpay.wallet.utils.allureScreenshot +import com.bitpay.wallet.utils.allureStep +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class Test594SwapBTC : BaseTest() { + + private val onboardingPage = OnboardingPage() + private val homePage = HomePage() + private val addYourCryptoOptionPage = AddYourCryptoOptionPage() + private val importWalletPage = ImportWalletPage() + private val myKeyPage = MyKeyPage() + private val selectCurrencyPage = SelectCurrencyPage() + + @Test + fun testBTCSwap() { + allureStep("Click 'Already have a key' on onboarding screen") { + onboardingPage.clickAlreadyHaveKey() + } + + allureStep("Enter recovery phrase and import wallet") { + importWalletPage.enterRecoveryPhrase("hobby short divert lady spare quit act settle body town license alone") + allureScreenshot("Recovery phrase entered") + importWalletPage.clickImportWallet() + } + + allureStep("Verify 'I understand' checkbox 1 is displayed") { + onboardingPage.verifyIUnderstandCheckbox1Displayed() + } + + allureStep("Check all three 'I understand' checkboxes") { + onboardingPage.clickIUnderstandCheckbox1() + onboardingPage.clickIUnderstandCheckbox2() + onboardingPage.clickIUnderstandCheckbox3() + } + + allureStep("Click Agree and Continue") { + onboardingPage.clickAgreeAndContinue() + } + + allureStep("Wait for Home page to load") { + homePage.waitForPageToLoad() + allureScreenshot("Home page loaded") + } + + allureStep("Verify portfolio balance is displayed on Home page") { + assertTrue( + "Home Page - Portfolio balance text not displayed", + homePage.verifyPortfolioBalanceTextDisplayed() + ) + } + + allureStep("Click Swap on Home page") { + homePage.clickSwap() + } + + allureStep("Verify Swap page is displayed") { + assertTrue( + "Swap page not displayed", + selectCurrencyPage.verifySwapTitleDisplayed() + ) + allureScreenshot("Swap page displayed") + } + + allureStep("Select Bitcoin as Swap From currency") { + selectCurrencyPage.clickSwapFromOption() + selectCurrencyPage.clickBitcoin() + } + + allureStep("Select Ethereum as Swap To currency") { + selectCurrencyPage.clickSwapToOption() + selectCurrencyPage.clickEthereum() + } + + allureStep("Verify Select Key To Deposit To option is displayed") { + assertTrue( + "Select Key To Deposit To option not displayed", + selectCurrencyPage.verifySelectKeyToDepositToDisplayed() + ) + } + + allureStep("Select second My Key wallet and EVM account") { + selectCurrencyPage.clickSecondMyKeyWallet() + selectCurrencyPage.clickEVMAccount() + } + + allureStep("Enter minimum swap amount") { + selectCurrencyPage.clickEnterSwapAmount() + selectCurrencyPage.clickMinSwapAmount() + allureScreenshot("Minimum swap amount entered") + } + + allureStep("Accept Changelly terms checkbox") { + selectCurrencyPage.clickChangellyTermsCheckbox() + } + + allureStep("Verify Slide to Swap button is enabled") { + assertTrue( + "Slide to Swap button not enabled", + selectCurrencyPage.verifySlideToSwapButtonEnabled() + ) + allureScreenshot("Final - Slide to Swap button enabled") + } + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test595SellBTC.kt b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test595SellBTC.kt new file mode 100644 index 0000000000..74a9deb690 --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test595SellBTC.kt @@ -0,0 +1,98 @@ +package com.bitpay.wallet.tests + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bitpay.wallet.base.BaseTest +import com.bitpay.wallet.pages.AddYourCryptoOptionPage +import com.bitpay.wallet.pages.HomePage +import com.bitpay.wallet.pages.ImportWalletPage +import com.bitpay.wallet.pages.KeyboardPage +import com.bitpay.wallet.pages.MyKeyPage +import com.bitpay.wallet.pages.OnboardingPage +import com.bitpay.wallet.pages.SelectCurrencyPage +import com.bitpay.wallet.utils.allureScreenshot +import com.bitpay.wallet.utils.allureStep +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class Test595SellBTC : BaseTest() { + + private val onboardingPage = OnboardingPage() + private val homePage = HomePage() + private val addYourCryptoOptionPage = AddYourCryptoOptionPage() + private val importWalletPage = ImportWalletPage() + private val myKeyPage = MyKeyPage() + private val selectCurrencyPage = SelectCurrencyPage() + private val keyboardPage = KeyboardPage() + + @Test + fun testBTCSell() { + allureStep("Click 'Already have a key' on onboarding screen") { + onboardingPage.clickAlreadyHaveKey() + } + + allureStep("Enter recovery phrase and import wallet") { + importWalletPage.enterRecoveryPhrase("hobby short divert lady spare quit act settle body town license alone") + allureScreenshot("Recovery phrase entered") + importWalletPage.clickImportWallet() + } + + allureStep("Verify 'I understand' checkbox 1 is displayed") { + onboardingPage.verifyIUnderstandCheckbox1Displayed() + } + + allureStep("Check all three 'I understand' checkboxes") { + onboardingPage.clickIUnderstandCheckbox1() + onboardingPage.clickIUnderstandCheckbox2() + onboardingPage.clickIUnderstandCheckbox3() + } + + allureStep("Click Agree and Continue") { + onboardingPage.clickAgreeAndContinue() + } + + allureStep("Wait for Home page to load") { + homePage.waitForPageToLoad() + allureScreenshot("Home page loaded") + } + + allureStep("Verify portfolio balance is displayed on Home page") { + assertTrue( + "Home Page - Portfolio balance text not displayed", + homePage.verifyPortfolioBalanceTextDisplayed() + ) + } + + allureStep("Click Sell on Home page") { + homePage.clickSell() + } + + allureStep("Verify Sell page is displayed") { + assertTrue( + "Sell page not displayed", + selectCurrencyPage.verifySellTitleDisplayed() + ) + allureScreenshot("Sell page displayed") + } + + allureStep("Enter sell amount") { + keyboardPage.enterAmount("0.0007") + } + + allureStep("Choose Bitcoin as the crypto to sell") { + selectCurrencyPage.clickChooseCrypto() + selectCurrencyPage.clickBitcoin() + allureScreenshot("Bitcoin selected for sell") + } + + allureStep("Verify Continue button is enabled") { + assertTrue( + "Buy - Continue button not enabled", + selectCurrencyPage.verifyBuyContinueButtonEnabled() + ) + allureScreenshot("Final - Continue button enabled") + } + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test596BuyBTC.kt b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test596BuyBTC.kt new file mode 100644 index 0000000000..1f34bffbfb --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test596BuyBTC.kt @@ -0,0 +1,110 @@ +package com.bitpay.wallet.tests + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bitpay.wallet.base.BaseTest +import com.bitpay.wallet.pages.AddYourCryptoOptionPage +import com.bitpay.wallet.pages.HomePage +import com.bitpay.wallet.pages.ImportWalletPage +import com.bitpay.wallet.pages.KeyboardPage +import com.bitpay.wallet.pages.MyKeyPage +import com.bitpay.wallet.pages.OnboardingPage +import com.bitpay.wallet.pages.SelectCurrencyPage +import com.bitpay.wallet.utils.allureScreenshot +import com.bitpay.wallet.utils.allureStep +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class Test596BuyBTC : BaseTest() { + + private val onboardingPage = OnboardingPage() + private val homePage = HomePage() + private val addYourCryptoOptionPage = AddYourCryptoOptionPage() + private val importWalletPage = ImportWalletPage() + private val myKeyPage = MyKeyPage() + private val selectCurrencyPage = SelectCurrencyPage() + private val keyboardPage = KeyboardPage() + + @Test + fun testBTCBuy() { + allureStep("Click 'Already have a key' on onboarding screen") { + onboardingPage.clickAlreadyHaveKey() + } + + allureStep("Enter recovery phrase and import wallet") { + importWalletPage.enterRecoveryPhrase("hobby short divert lady spare quit act settle body town license alone") + allureScreenshot("Recovery phrase entered") + importWalletPage.clickImportWallet() + } + + allureStep("Verify 'I understand' checkbox 1 is displayed") { + onboardingPage.verifyIUnderstandCheckbox1Displayed() + } + + allureStep("Check all three 'I understand' checkboxes") { + onboardingPage.clickIUnderstandCheckbox1() + onboardingPage.clickIUnderstandCheckbox2() + onboardingPage.clickIUnderstandCheckbox3() + } + + allureStep("Click Agree and Continue") { + onboardingPage.clickAgreeAndContinue() + } + + allureStep("Wait for Home page to load") { + homePage.waitForPageToLoad() + allureScreenshot("Home page loaded") + } + + allureStep("Verify portfolio balance is displayed on Home page") { + assertTrue( + "Home Page - Portfolio balance text not displayed", + homePage.verifyPortfolioBalanceTextDisplayed() + ) + } + + allureStep("Click Buy on Home page") { + homePage.clickBuy() + } + + allureStep("Verify Buy page is displayed") { + assertTrue( + "Buy page not displayed", + selectCurrencyPage.verifyBuyTitleDisplayed() + ) + allureScreenshot("Buy page displayed") + } + + allureStep("Clear default amount and enter buy amount") { + keyboardPage.clickBackspace(3) + keyboardPage.enterAmount("30") + } + + allureStep("Select Bitcoin as the crypto to buy") { + selectCurrencyPage.clickBitcoin() + selectCurrencyPage.clickBitcoin() // Select Crypto page + allureScreenshot("Bitcoin selected for buy") + } + + allureStep("Verify Select Key To Deposit To option is displayed") { + assertTrue( + "Select Key To Deposit To option not displayed", + selectCurrencyPage.verifySelectKeyToDepositToDisplayed() + ) + } + + allureStep("Select second My Key wallet") { + selectCurrencyPage.clickSecondMyKeyWallet() + } + + allureStep("Verify Continue button is enabled") { + assertTrue( + "Buy - Continue button not enabled", + selectCurrencyPage.verifyBuyContinueButtonEnabled() + ) + allureScreenshot("Final - Continue button enabled") + } + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/utils/AllureUtils.kt b/android/app/src/androidTest/java/com/bitpay/wallet/utils/AllureUtils.kt new file mode 100644 index 0000000000..a02dadb971 --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/utils/AllureUtils.kt @@ -0,0 +1,27 @@ +package com.bitpay.wallet.utils + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import io.qameta.allure.kotlin.Allure +import java.io.File + +fun allureScreenshot(name: String = "Screenshot") { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val context = InstrumentationRegistry.getInstrumentation().targetContext + val file = File(context.cacheDir, "screenshot_${System.currentTimeMillis()}.png") + + val saved = device.takeScreenshot(file) + if (saved && file.exists()) { + Allure.attachment(name, file.inputStream()) + } +} + +inline fun allureStep(name: String, crossinline block: () -> Unit) { + Allure.step(name) { block() } +} + +fun allureVideo(name: String, file: File?) { + if (file == null || !file.exists()) return + Allure.attachment(name, file.inputStream(), "video/mp4") + file.delete() +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/utils/WaitUtils.kt b/android/app/src/androidTest/java/com/bitpay/wallet/utils/WaitUtils.kt new file mode 100644 index 0000000000..7b09aeec89 --- /dev/null +++ b/android/app/src/androidTest/java/com/bitpay/wallet/utils/WaitUtils.kt @@ -0,0 +1,96 @@ +package com.bitpay.wallet.utils + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import org.hamcrest.Matcher +import android.view.View +import androidx.test.espresso.matcher.BoundedMatcher +import org.hamcrest.Description +import org.hamcrest.TypeSafeMatcher + + +object WaitUtils { + + fun waitForView( + matcher: Matcher, + timeoutMs: Long = 30000, + intervalMs: Long = 500 + ) { + val endTime = System.currentTimeMillis() + timeoutMs + var lastError: Throwable? = null + + while (System.currentTimeMillis() < endTime) { + try { + onView(matcher).check(matches(isDisplayed())) + return // found it, exit + } catch (e: Throwable) { + // catch EVERYTHING - NoMatchingViewException, AssertionError, + // RootViewWithoutFocusException, anything else Espresso throws + lastError = e + Thread.sleep(intervalMs) + } + } + + // timed out - throw the last real error so the failure message is useful + throw lastError ?: RuntimeException("View not found within ${timeoutMs}ms: $matcher") + } + + fun waitForViewEnabled( + matcher: Matcher, + timeoutMs: Long = 120000, + intervalMs: Long = 500 + ) { + val endTime = System.currentTimeMillis() + timeoutMs + var lastError: Throwable? = null + + while (System.currentTimeMillis() < endTime) { + try { + onView(matcher).check(matches(isDisplayed())).check(matches(isEnabled())) + return + } catch (e: Throwable) { + lastError = e + Thread.sleep(intervalMs) + } + } + + throw lastError ?: RuntimeException("View not enabled within ${timeoutMs}ms: $matcher") + } + + /** + * React Native testIDs are surfaced to native Espresso as View.getTag(), + * NOT as an Android resource id. withResourceName()/withId() will never + * match them - this matcher checks the tag directly instead. + */ + fun withTestId(testId: String): BoundedMatcher { + return object : BoundedMatcher(View::class.java) { + override fun describeTo(description: Description) { + description.appendText("with RN testID: $testId") + } + + override fun matchesSafely(view: View): Boolean { + return testId == view.tag + } + } + } + + fun withIndex(matcher: Matcher, index: Int): Matcher { + return object : TypeSafeMatcher() { + var currentIndex = 0 + + override fun describeTo(description: Description) { + description.appendText("with index: $index ") + matcher.describeTo(description) + } + + override fun matchesSafely(item: T): Boolean { + val matched = matcher.matches(item) + if (matched && currentIndex++ == index) { + return true + } + return false + } + } + } +} diff --git a/android/app/src/androidTest/resources/allure.properties b/android/app/src/androidTest/resources/allure.properties new file mode 100644 index 0000000000..890384fac0 --- /dev/null +++ b/android/app/src/androidTest/resources/allure.properties @@ -0,0 +1 @@ +allure.results.useTestStorage=true \ No newline at end of file diff --git a/android/app/src/main/java/com/bitpay/wallet/MainActivity.kt b/android/app/src/main/java/com/bitpay/wallet/MainActivity.kt index ca753330d0..0ff11a675b 100644 --- a/android/app/src/main/java/com/bitpay/wallet/MainActivity.kt +++ b/android/app/src/main/java/com/bitpay/wallet/MainActivity.kt @@ -48,10 +48,15 @@ class MainActivity : ReactActivity() { (application as MainApplication).addActivityToStack(this.javaClass) window.apply { - setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE - ) + if (!BuildConfig.DEBUG) { + // Disable screenshots/screen recording only in production. + // Allow automation tools (Espresso/Appium/Layout Inspector) + // to capture screenshots in debug builds. + setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) + } if (Build.VERSION.SDK_INT in 19..20) { setWindowFlag(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, true) diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml index 8420bcbfa9..776e6d0846 100644 --- a/android/app/src/main/res/xml/network_security_config.xml +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -1,7 +1,7 @@ - - + + localhost @@ -11,4 +11,4 @@ - \ No newline at end of file + diff --git a/android/build.gradle b/android/build.gradle index f5086ca47e..c6b79f7e01 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -52,3 +52,46 @@ allprojects { maven { url "https://dl.appsflyer.com" } } } + +gradle.afterProject { proj -> + if (proj != rootProject && proj.hasProperty('android')) { + proj.android { + if (defaultConfig.minSdkVersion == null || defaultConfig.minSdkVersion.apiLevel < 24) { + defaultConfig { + minSdkVersion 24 + } + } + + if (compileSdkVersion == null || (compileSdkVersion as String).replace("android-", "").toInteger() < 34) { + compileSdkVersion 36 + } + + packagingOptions { + // Native shared libraries that multiple RN modules bundle a + // copy of, which also exist inside react-android itself. + // Confirmed needed by: react-native-gesture-handler + // (libfbjni.so), react-native-webassembly (libc++_shared.so). + pickFirst 'lib/*/libfbjni.so' + pickFirst 'lib/*/libc++_shared.so' + pickFirst 'lib/*/libjsi.so' + pickFirst 'lib/*/libreactnative.so' + pickFirst 'lib/*/libhermes.so' + + resources { + pickFirsts += 'META-INF/**/coroutines.pro' + excludes += [ + 'META-INF/MANIFEST.MF', + 'META-INF/LICENSE', + 'META-INF/LICENSE.txt', + 'META-INF/LICENSE.md', + 'META-INF/LICENSE-notice.md', + 'META-INF/NOTICE', + 'META-INF/NOTICE.txt', + 'META-INF/DEPENDENCIES', + 'META-INF/*.kotlin_module' + ] + } + } + } + } +} \ No newline at end of file diff --git a/index.js b/index.js index 5e576ec3ea..939c866b9b 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ import '@walletconnect/react-native-compat'; import {AppRegistry, Alert, StatusBar, Appearance, LogBox} from 'react-native'; import {IS_MAESTRO} from '@env'; -if (IS_MAESTRO === 'true') { +if (IS_MAESTRO === 'true' || __DEV__) { LogBox.ignoreAllLogs(); } import Root from './src/Root'; diff --git a/ios/BitPayApp.xcodeproj/project.pbxproj b/ios/BitPayApp.xcodeproj/project.pbxproj index 8665f7982f..f2402f174d 100644 --- a/ios/BitPayApp.xcodeproj/project.pbxproj +++ b/ios/BitPayApp.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ 256B92C82AC0ED0D00C1DF97 /* bitpay-recovery-phrase-template.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 256B92C72AC0ED0D00C1DF97 /* bitpay-recovery-phrase-template.pdf */; }; 4054F0E729AEFAF0009BBC9C /* AllowedUrlPrefixProtocol.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4054F0E529AEFAEF009BBC9C /* AllowedUrlPrefixProtocol.mm */; }; 523EFC4FD62CB0CD2FBD02FC /* libPods-BitPayApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B1AA7DD4226DBC3478258808 /* libPods-BitPayApp.a */; }; - 52C9714B9004BCE190EA469D /* BundleHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30DDDCF29E5E048D75CCF1A /* BundleHash.swift */; }; + 52C9714B9004BCE190EA469D /* BitPayApp/BundleHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30DDDCF29E5E048D75CCF1A /* BitPayApp/BundleHash.swift */; }; 5B31AAFE2786387B002765E1 /* RCTDosh.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B31AAFD2786387B002765E1 /* RCTDosh.mm */; }; 5B31AB5F2786515A002765E1 /* Dosh.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B31AB5E2786515A002765E1 /* Dosh.swift */; }; 5B5631782838854000C395A8 /* PassKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B5631772838854000C395A8 /* PassKit.framework */; }; @@ -24,6 +24,30 @@ 6D7C95E72E9007E200F0D481 /* RCTPushPermissionManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6D7C95E62E9007E200F0D481 /* RCTPushPermissionManager.mm */; }; 71BABDA4524A43E98A190C87 /* Archivo-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6D649751C2644FDCA18A5547 /* Archivo-Light.ttf */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; + CD1957ED2FB37AD40072A9AE /* Test595_BasicSellBTC_.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E92FB37AD40072A9AE /* Test595_BasicSellBTC_.swift */; }; + CD1957EE2FB37AD40072A9AE /* Test592_ImportWalletRecoveryPhrase.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E62FB37AD40072A9AE /* Test592_ImportWalletRecoveryPhrase.swift */; }; + CD1957EF2FB37AD40072A9AE /* MykeyPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957DB2FB37AD40072A9AE /* MykeyPage.swift */; }; + CD1957F02FB37AD40072A9AE /* BuyPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957D62FB37AD40072A9AE /* BuyPage.swift */; }; + CD1957F12FB37AD40072A9AE /* SellPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E12FB37AD40072A9AE /* SellPage.swift */; }; + CD1957F22FB37AD40072A9AE /* Test591_OnboardingCreateWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E52FB37AD40072A9AE /* Test591_OnboardingCreateWallet.swift */; }; + CD1957F32FB37AD40072A9AE /* Test596_BasicBuyBTC_.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957EA2FB37AD40072A9AE /* Test596_BasicBuyBTC_.swift */; }; + CD1957F42FB37AD40072A9AE /* NewRecoveryPhrasePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957DC2FB37AD40072A9AE /* NewRecoveryPhrasePage.swift */; }; + CD1957F52FB37AD40072A9AE /* VerifyYourPhrasePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E32FB37AD40072A9AE /* VerifyYourPhrasePage.swift */; }; + CD1957F62FB37AD40072A9AE /* ConfirmPaymentPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957D72FB37AD40072A9AE /* ConfirmPaymentPage.swift */; }; + CD1957F72FB37AD40072A9AE /* AllureXCTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957D32FB37AD40072A9AE /* AllureXCTestSupport.swift */; }; + CD1957F82FB37AD40072A9AE /* EnterAmountPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957D82FB37AD40072A9AE /* EnterAmountPage.swift */; }; + CD1957F92FB37AD40072A9AE /* Test594_BasicSwapBTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E82FB37AD40072A9AE /* Test594_BasicSwapBTC.swift */; }; + CD1957FA2FB37AD40072A9AE /* AddCryptoOptionPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957D52FB37AD40072A9AE /* AddCryptoOptionPage.swift */; }; + CD1957FB2FB37AD40072A9AE /* OnboardingPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957DD2FB37AD40072A9AE /* OnboardingPage.swift */; }; + CD1957FC2FB37AD40072A9AE /* ImportRecoveryPhrasePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957DA2FB37AD40072A9AE /* ImportRecoveryPhrasePage.swift */; }; + CD1957FD2FB37AD40072A9AE /* PortfolioBalancePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957DE2FB37AD40072A9AE /* PortfolioBalancePage.swift */; }; + CD1957FE2FB37AD40072A9AE /* SwapPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E22FB37AD40072A9AE /* SwapPage.swift */; }; + CD1957FF2FB37AD40072A9AE /* Test593_BTCConfirmPaymentScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E72FB37AD40072A9AE /* Test593_BTCConfirmPaymentScreen.swift */; }; + CD1958002FB37AD40072A9AE /* SelectKeyToDepositPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E02FB37AD40072A9AE /* SelectKeyToDepositPage.swift */; }; + CD1958012FB37AD40072A9AE /* SelectCurrencyPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957DF2FB37AD40072A9AE /* SelectCurrencyPage.swift */; }; + CD1958022FB37AD40072A9AE /* HomePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957D92FB37AD40072A9AE /* HomePage.swift */; }; + CD1958032FB37AD40072A9AE /* TestPlan.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = CD1957EB2FB37AD40072A9AE /* TestPlan.xctestplan */; }; + CD9683A82FB70CCC00192CFF /* AppFlows.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9683A72FB70CC700192CFF /* AppFlows.swift */; }; D34892F44CAB44409EC335A9 /* Archivo-Black.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F4435EB64D2448EC885B3036 /* Archivo-Black.ttf */; }; D9739196BA804EA29143DA96 /* Archivo-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 715670CB346B45A7A9EC2044 /* Archivo-Bold.ttf */; }; E6807D9059A643129784A5C3 /* Archivo-Thin.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4DCF32F2D2364E70AD2F1E68 /* Archivo-Thin.ttf */; }; @@ -32,6 +56,16 @@ F64C0BF216CE4402A51A5ACC /* Archivo-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BCDAB70057C84109A9201936 /* Archivo-Regular.ttf */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + CDF0850D2FACD8A500CA4C1B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = BitPayApp; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 13B07F961A680F5B00A75B9A /* BitPayApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BitPayApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = BitPayApp/Images.xcassets; sourceTree = ""; }; @@ -52,7 +86,7 @@ 5BCF1C342740B8B2006D872E /* BootSplash.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = BootSplash.storyboard; path = BitPayApp/BootSplash.storyboard; sourceTree = ""; }; 5BEA4BEE2838400700AB2121 /* RCTPaymentPass.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTPaymentPass.h; sourceTree = ""; }; 5BEA4BEF2838402700AB2121 /* RCTPaymentPass.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTPaymentPass.mm; sourceTree = ""; }; - 5D5D40A7E7B5265D652187F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = BitPayApp/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 5D5D40A7E7B5265D652187F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = BitPayApp/PrivacyInfo.xcprivacy; sourceTree = ""; }; 6D615C4729725FD70063DB8E /* RCTTimer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTimer.mm; sourceTree = ""; }; 6D615C4829725FD70063DB8E /* RCTTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTimer.h; sourceTree = ""; }; 6D649751C2644FDCA18A5547 /* Archivo-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Archivo-Light.ttf"; path = "../assets/fonts/Archivo-Light.ttf"; sourceTree = ""; }; @@ -61,7 +95,32 @@ 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = BitPayApp/AppDelegate.swift; sourceTree = ""; }; B1AA7DD4226DBC3478258808 /* libPods-BitPayApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-BitPayApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; BCDAB70057C84109A9201936 /* Archivo-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Archivo-Regular.ttf"; path = "../assets/fonts/Archivo-Regular.ttf"; sourceTree = ""; }; - E30DDDCF29E5E048D75CCF1A /* BundleHash.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BitPayApp/BundleHash.swift; sourceTree = ""; }; + CD1957D32FB37AD40072A9AE /* AllureXCTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllureXCTestSupport.swift; sourceTree = ""; }; + CD1957D52FB37AD40072A9AE /* AddCryptoOptionPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCryptoOptionPage.swift; sourceTree = ""; }; + CD1957D62FB37AD40072A9AE /* BuyPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuyPage.swift; sourceTree = ""; }; + CD1957D72FB37AD40072A9AE /* ConfirmPaymentPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmPaymentPage.swift; sourceTree = ""; }; + CD1957D82FB37AD40072A9AE /* EnterAmountPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterAmountPage.swift; sourceTree = ""; }; + CD1957D92FB37AD40072A9AE /* HomePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePage.swift; sourceTree = ""; }; + CD1957DA2FB37AD40072A9AE /* ImportRecoveryPhrasePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportRecoveryPhrasePage.swift; sourceTree = ""; }; + CD1957DB2FB37AD40072A9AE /* MykeyPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MykeyPage.swift; sourceTree = ""; }; + CD1957DC2FB37AD40072A9AE /* NewRecoveryPhrasePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewRecoveryPhrasePage.swift; sourceTree = ""; }; + CD1957DD2FB37AD40072A9AE /* OnboardingPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPage.swift; sourceTree = ""; }; + CD1957DE2FB37AD40072A9AE /* PortfolioBalancePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortfolioBalancePage.swift; sourceTree = ""; }; + CD1957DF2FB37AD40072A9AE /* SelectCurrencyPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrencyPage.swift; sourceTree = ""; }; + CD1957E02FB37AD40072A9AE /* SelectKeyToDepositPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectKeyToDepositPage.swift; sourceTree = ""; }; + CD1957E12FB37AD40072A9AE /* SellPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SellPage.swift; sourceTree = ""; }; + CD1957E22FB37AD40072A9AE /* SwapPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPage.swift; sourceTree = ""; }; + CD1957E32FB37AD40072A9AE /* VerifyYourPhrasePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyYourPhrasePage.swift; sourceTree = ""; }; + CD1957E52FB37AD40072A9AE /* Test591_OnboardingCreateWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test591_OnboardingCreateWallet.swift; sourceTree = ""; }; + CD1957E62FB37AD40072A9AE /* Test592_ImportWalletRecoveryPhrase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test592_ImportWalletRecoveryPhrase.swift; sourceTree = ""; }; + CD1957E72FB37AD40072A9AE /* Test593_BTCConfirmPaymentScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test593_BTCConfirmPaymentScreen.swift; sourceTree = ""; }; + CD1957E82FB37AD40072A9AE /* Test594_BasicSwapBTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test594_BasicSwapBTC.swift; sourceTree = ""; }; + CD1957E92FB37AD40072A9AE /* Test595_BasicSellBTC_.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test595_BasicSellBTC_.swift; sourceTree = ""; }; + CD1957EA2FB37AD40072A9AE /* Test596_BasicBuyBTC_.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test596_BasicBuyBTC_.swift; sourceTree = ""; }; + CD1957EB2FB37AD40072A9AE /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = ""; }; + CD9683A72FB70CC700192CFF /* AppFlows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFlows.swift; sourceTree = ""; }; + CDF085072FACD8A500CA4C1B /* BitPayAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BitPayAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + E30DDDCF29E5E048D75CCF1A /* BitPayApp/BundleHash.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BitPayApp/BundleHash.swift; sourceTree = ""; }; E639E0380D05420CD0D64119 /* Pods-BitPayApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BitPayApp.release.xcconfig"; path = "Target Support Files/Pods-BitPayApp/Pods-BitPayApp.release.xcconfig"; sourceTree = ""; }; EC0FDD692CC92E90009E1B35 /* InAppMessageModule.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InAppMessageModule.mm; sourceTree = ""; }; ECCB92762860B25D00F9CF8B /* SilentPushEvent.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SilentPushEvent.mm; sourceTree = ""; }; @@ -81,6 +140,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CDF085042FACD8A500CA4C1B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -107,7 +173,7 @@ 5BEA4BEE2838400700AB2121 /* RCTPaymentPass.h */, 5BEA4BEF2838402700AB2121 /* RCTPaymentPass.mm */, 5D5D40A7E7B5265D652187F9 /* PrivacyInfo.xcprivacy */, - E30DDDCF29E5E048D75CCF1A /* BundleHash.swift */, + E30DDDCF29E5E048D75CCF1A /* BitPayApp/BundleHash.swift */, ); name = BitPayApp; sourceTree = ""; @@ -158,10 +224,12 @@ children = ( 13B07FAE1A68108700A75B9A /* BitPayApp */, 832341AE1AAA6A7D00B99B32 /* Libraries */, + CDF085082FACD8A500CA4C1B /* BitPayAppUITests */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, 7950FCCDC90E6306EDD77BCD /* Pods */, 3A6F88D6BF7C4F7A8D93264A /* Resources */, + CDF085022FACD87900CA4C1B /* Recovered References */, ); indentWidth = 2; sourceTree = ""; @@ -172,10 +240,82 @@ isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* BitPayApp.app */, + CDF085072FACD8A500CA4C1B /* BitPayAppUITests.xctest */, ); name = Products; sourceTree = ""; }; + CD1957D42FB37AD40072A9AE /* AllureReport */ = { + isa = PBXGroup; + children = ( + CD1957D32FB37AD40072A9AE /* AllureXCTestSupport.swift */, + ); + path = AllureReport; + sourceTree = ""; + }; + CD1957E42FB37AD40072A9AE /* pages */ = { + isa = PBXGroup; + children = ( + CD1957D52FB37AD40072A9AE /* AddCryptoOptionPage.swift */, + CD1957D62FB37AD40072A9AE /* BuyPage.swift */, + CD1957D72FB37AD40072A9AE /* ConfirmPaymentPage.swift */, + CD1957D82FB37AD40072A9AE /* EnterAmountPage.swift */, + CD1957D92FB37AD40072A9AE /* HomePage.swift */, + CD1957DA2FB37AD40072A9AE /* ImportRecoveryPhrasePage.swift */, + CD1957DB2FB37AD40072A9AE /* MykeyPage.swift */, + CD1957DC2FB37AD40072A9AE /* NewRecoveryPhrasePage.swift */, + CD1957DD2FB37AD40072A9AE /* OnboardingPage.swift */, + CD1957DE2FB37AD40072A9AE /* PortfolioBalancePage.swift */, + CD1957DF2FB37AD40072A9AE /* SelectCurrencyPage.swift */, + CD1957E02FB37AD40072A9AE /* SelectKeyToDepositPage.swift */, + CD1957E12FB37AD40072A9AE /* SellPage.swift */, + CD1957E22FB37AD40072A9AE /* SwapPage.swift */, + CD1957E32FB37AD40072A9AE /* VerifyYourPhrasePage.swift */, + ); + path = pages; + sourceTree = ""; + }; + CD1957EC2FB37AD40072A9AE /* tests */ = { + isa = PBXGroup; + children = ( + CD1957E52FB37AD40072A9AE /* Test591_OnboardingCreateWallet.swift */, + CD1957E62FB37AD40072A9AE /* Test592_ImportWalletRecoveryPhrase.swift */, + CD1957E72FB37AD40072A9AE /* Test593_BTCConfirmPaymentScreen.swift */, + CD1957E82FB37AD40072A9AE /* Test594_BasicSwapBTC.swift */, + CD1957E92FB37AD40072A9AE /* Test595_BasicSellBTC_.swift */, + CD1957EA2FB37AD40072A9AE /* Test596_BasicBuyBTC_.swift */, + CD1957EB2FB37AD40072A9AE /* TestPlan.xctestplan */, + ); + path = tests; + sourceTree = ""; + }; + CD96839F2FB70CAF00192CFF /* flows */ = { + isa = PBXGroup; + children = ( + CD9683A72FB70CC700192CFF /* AppFlows.swift */, + ); + path = flows; + sourceTree = ""; + }; + CDF085022FACD87900CA4C1B /* Recovered References */ = { + isa = PBXGroup; + children = ( + 6D7C95E62E9007E200F0D481 /* RCTPushPermissionManager.mm */, + ); + name = "Recovered References"; + sourceTree = ""; + }; + CDF085082FACD8A500CA4C1B /* BitPayAppUITests */ = { + isa = PBXGroup; + children = ( + CD1957D42FB37AD40072A9AE /* AllureReport */, + CD1957E42FB37AD40072A9AE /* pages */, + CD1957EC2FB37AD40072A9AE /* tests */, + CD96839F2FB70CAF00192CFF /* flows */, + ); + path = BitPayAppUITests; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -202,17 +342,40 @@ productReference = 13B07F961A680F5B00A75B9A /* BitPayApp.app */; productType = "com.apple.product-type.application"; }; + CDF085062FACD8A500CA4C1B /* BitPayAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CDF085112FACD8A500CA4C1B /* Build configuration list for PBXNativeTarget "BitPayAppUITests" */; + buildPhases = ( + CDF085032FACD8A500CA4C1B /* Sources */, + CDF085042FACD8A500CA4C1B /* Frameworks */, + CDF085052FACD8A500CA4C1B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + CDF0850E2FACD8A500CA4C1B /* PBXTargetDependency */, + ); + name = BitPayAppUITests; + productName = BitPayAppUITests; + productReference = CDF085072FACD8A500CA4C1B /* BitPayAppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 2600; LastUpgradeCheck = 1210; TargetAttributes = { 13B07F861A680F5B00A75B9A = { LastSwiftMigration = 1240; }; + CDF085062FACD8A500CA4C1B = { + CreatedOnToolsVersion = 26.0.1; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "BitPayApp" */; @@ -229,6 +392,7 @@ projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* BitPayApp */, + CDF085062FACD8A500CA4C1B /* BitPayAppUITests */, ); }; /* End PBXProject section */ @@ -252,6 +416,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CDF085052FACD8A500CA4C1B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CD1958032FB37AD40072A9AE /* TestPlan.xctestplan in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -267,7 +439,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\n\nif [ -z \"$NODE_BINARY\" ]; then\n export NODE_BINARY=$(command -v node || true)\nfi\nif [ -z \"$NODE_BINARY\" ] || [ ! -x \"$NODE_BINARY\" ]; then\n echo \"error: NODE_BINARY not set and 'node' not found on PATH.\"\n exit 1\nfi\necho \"Using NODE_BINARY=$NODE_BINARY\"\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nSENTRY_XCODE=\"../node_modules/@sentry/react-native/scripts/sentry-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $SENTRY_XCODE\"\n"; + shellScript = "export SENTRY_DISABLE_AUTO_UPLOAD=true\n\nset -e\n\nif [ -z \"$NODE_BINARY\" ]; then\n export NODE_BINARY=$(command -v node || true)\nfi\nif [ -z \"$NODE_BINARY\" ] || [ ! -x \"$NODE_BINARY\" ]; then\n echo \"error: NODE_BINARY not set and 'node' not found on PATH.\"\n exit 1\nfi\necho \"Using NODE_BINARY=$NODE_BINARY\"\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nSENTRY_XCODE=\"../node_modules/@sentry/react-native/scripts/sentry-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $SENTRY_XCODE\"\n"; }; 3C1070649A3322DC1391852F /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -299,10 +471,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-resources.sh\"\n"; @@ -335,10 +511,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-frameworks.sh\"\n"; @@ -374,19 +554,56 @@ 6D615C4929725FD70063DB8E /* RCTTimer.mm in Sources */, ECCB92782860B25D00F9CF8B /* SilentPushEvent.mm in Sources */, 5B31AB5F2786515A002765E1 /* Dosh.swift in Sources */, - 52C9714B9004BCE190EA469D /* BundleHash.swift in Sources */, + 52C9714B9004BCE190EA469D /* BitPayApp/BundleHash.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CDF085032FACD8A500CA4C1B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CD1957ED2FB37AD40072A9AE /* Test595_BasicSellBTC_.swift in Sources */, + CD1957EE2FB37AD40072A9AE /* Test592_ImportWalletRecoveryPhrase.swift in Sources */, + CD1957EF2FB37AD40072A9AE /* MykeyPage.swift in Sources */, + CD1957F02FB37AD40072A9AE /* BuyPage.swift in Sources */, + CD1957F12FB37AD40072A9AE /* SellPage.swift in Sources */, + CD1957F22FB37AD40072A9AE /* Test591_OnboardingCreateWallet.swift in Sources */, + CD1957F32FB37AD40072A9AE /* Test596_BasicBuyBTC_.swift in Sources */, + CD9683A82FB70CCC00192CFF /* AppFlows.swift in Sources */, + CD1957F42FB37AD40072A9AE /* NewRecoveryPhrasePage.swift in Sources */, + CD1957F52FB37AD40072A9AE /* VerifyYourPhrasePage.swift in Sources */, + CD1957F62FB37AD40072A9AE /* ConfirmPaymentPage.swift in Sources */, + CD1957F72FB37AD40072A9AE /* AllureXCTestSupport.swift in Sources */, + CD1957F82FB37AD40072A9AE /* EnterAmountPage.swift in Sources */, + CD1957F92FB37AD40072A9AE /* Test594_BasicSwapBTC.swift in Sources */, + CD1957FA2FB37AD40072A9AE /* AddCryptoOptionPage.swift in Sources */, + CD1957FB2FB37AD40072A9AE /* OnboardingPage.swift in Sources */, + CD1957FC2FB37AD40072A9AE /* ImportRecoveryPhrasePage.swift in Sources */, + CD1957FD2FB37AD40072A9AE /* PortfolioBalancePage.swift in Sources */, + CD1957FE2FB37AD40072A9AE /* SwapPage.swift in Sources */, + CD1957FF2FB37AD40072A9AE /* Test593_BTCConfirmPaymentScreen.swift in Sources */, + CD1958002FB37AD40072A9AE /* SelectKeyToDepositPage.swift in Sources */, + CD1958012FB37AD40072A9AE /* SelectCurrencyPage.swift in Sources */, + CD1958022FB37AD40072A9AE /* HomePage.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + CDF0850E2FACD8A500CA4C1B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* BitPayApp */; + targetProxy = CDF0850D2FACD8A500CA4C1B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4221E54904CA4996330CEB54 /* Pods-BitPayApp.debug.xcconfig */; buildSettings = { - ALLOWED_URL_PREFIXES = "ALLOWED-URL-PREFIXES-REPLACE-ME"; - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; + ALLOWED_URL_PREFIXES = "https://bitpay.com/,https://test.bitpay.com/,https://staging.bitpay.com/,https://bws.bitpay.com/,https://www.coinbase.com/,https://api.coinbase.com/,https://api.1inch.dev/swap/v5.2/,https://api.coingecko.com/api/v3/simple/token_price/,https://checkout.simplexcc.com/,https://sandbox.test-simplexcc.com,https://api.moonpay.com/,https://api.moonpay.dev/,https://api.zenledger.io/bitpay/wallets/,https://rest.iad-05.braze.com/,https://sdk.iad-05.braze.com/,https://cloudflare-eth.com/,https://sepolia.infura.io/v3/,https://polygon-rpc.com/,https://polygon-amoy.core.chainstack.com/,https://static.methodfi.com/,https://api.zenledger.io/,https://api.blockcypher.com/v1/,https://api.fullstack.cash/v5/,https://deep-index.moralis.io/api/v2.2/,https://api.bitcore.io/api,https://api-eth.bitcore.io/api,https://api-matic.bitcore.io/api,https://api-arb.bitcore.io/api,https://api-base.bitcore.io/api,https://api-op.bitcore.io/api,https://api-xrp.bitcore.io/api,https://api-sol.bitcore.io/api,https://portal.polygon.technology/bridge,https://bridge.arbitrum.io/,https://bridge.base.org/,https://app.optimism.io/bridge/,https://mainnet.optimism.io/,https://arb1.arbitrum.io/rpc,https://mainnet.base.org/,https://goerli.optimism.io,https://goerli-rollup.arbitrum.io/rpc,https://goerli.base.org,https://verify.walletconnect.com/,https://verify.walletconnect.org/,https://api.etherscan.io/v2/api,https://solana-gateway.moralis.io/,https://tokens-data.1inch.io/images/501/"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; @@ -431,8 +648,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = E639E0380D05420CD0D64119 /* Pods-BitPayApp.release.xcconfig */; buildSettings = { - ALLOWED_URL_PREFIXES = "ALLOWED-URL-PREFIXES-REPLACE-ME"; - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; + ALLOWED_URL_PREFIXES = "https://bitpay.com/,https://test.bitpay.com/,https://staging.bitpay.com/,https://bws.bitpay.com/,https://www.coinbase.com/,https://api.coinbase.com/,https://api.1inch.dev/swap/v5.2/,https://api.coingecko.com/api/v3/simple/token_price/,https://checkout.simplexcc.com/,https://sandbox.test-simplexcc.com,https://api.moonpay.com/,https://api.moonpay.dev/,https://api.zenledger.io/bitpay/wallets/,https://rest.iad-05.braze.com/,https://sdk.iad-05.braze.com/,https://cloudflare-eth.com/,https://sepolia.infura.io/v3/,https://polygon-rpc.com/,https://polygon-amoy.core.chainstack.com/,https://static.methodfi.com/,https://api.zenledger.io/,https://api.blockcypher.com/v1/,https://api.fullstack.cash/v5/,https://deep-index.moralis.io/api/v2.2/,https://api.bitcore.io/api,https://api-eth.bitcore.io/api,https://api-matic.bitcore.io/api,https://api-arb.bitcore.io/api,https://api-base.bitcore.io/api,https://api-op.bitcore.io/api,https://api-xrp.bitcore.io/api,https://api-sol.bitcore.io/api,https://portal.polygon.technology/bridge,https://bridge.arbitrum.io/,https://bridge.base.org/,https://app.optimism.io/bridge/,https://mainnet.optimism.io/,https://arb1.arbitrum.io/rpc,https://mainnet.base.org/,https://goerli.optimism.io,https://goerli-rollup.arbitrum.io/rpc,https://goerli.base.org,https://verify.walletconnect.com/,https://verify.walletconnect.org/,https://api.etherscan.io/v2/api,https://solana-gateway.moralis.io/,https://tokens-data.1inch.io/images/501/"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; @@ -649,6 +865,81 @@ }; name = Release; }; + CDF0850F2FACD8A500CA4C1B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.test.appium.BitPayAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = NO; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = BitPayApp; + }; + name = Debug; + }; + CDF085102FACD8A500CA4C1B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.test.appium.BitPayAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = NO; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = BitPayApp; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -670,6 +961,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CDF085112FACD8A500CA4C1B /* Build configuration list for PBXNativeTarget "BitPayAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CDF0850F2FACD8A500CA4C1B /* Debug */, + CDF085102FACD8A500CA4C1B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; diff --git a/ios/BitPayApp.xcodeproj/xcshareddata/xcschemes/BitPayApp.xcscheme b/ios/BitPayApp.xcodeproj/xcshareddata/xcschemes/BitPayApp.xcscheme index 3874813c99..92d73acdf4 100644 --- a/ios/BitPayApp.xcodeproj/xcshareddata/xcschemes/BitPayApp.xcscheme +++ b/ios/BitPayApp.xcodeproj/xcshareddata/xcschemes/BitPayApp.xcscheme @@ -26,7 +26,9 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + systemAttachmentLifetime = "keepAlways" + userAttachmentLifetime = "keepAlways"> @@ -38,6 +40,17 @@ ReferencedContainer = "container:BitPayApp.xcodeproj"> + + + + :` convention. + static func addLabel(_ name: String, value: String) { + XCTContext.runActivity(named: "allure.label.\(name):\(value)") { _ in } + } + + /// Sets a custom test name visible in the report. + static func setDisplayName(_ name: String) { + XCTContext.runActivity(named: "allure.name:\(name)") { _ in } + } + + /// Adds a link to an issue tracker entry. + static func addIssue(_ title: String, url: String) { + XCTContext.runActivity(named: "allure.issue.\(title):\(url)") { _ in } + } + + /// Adds a generic hyperlink label. + static func addLink(_ title: String, url: String) { + XCTContext.runActivity(named: "allure.link.\(title):\(url)") { _ in } + } + + /// Adds a plain description to the test. + static func addDescription(_ description: String) { + XCTContext.runActivity(named: "allure.description:\(description)") { _ in } + } + + /// Wraps a code block in an activity that appears as a step in Allure. + @discardableResult + static func step(_ name: String, block: () throws -> T) rethrows -> T { + try XCTContext.runActivity(named: name) { _ in try block() } + } +} + +extension XCTestCase { + /// Attaches a full-screen capture to the current test. Lifetime is `.keepAlways` + /// so it remains available in the `.xcresult` bundle for Allure to render. + func allureAttachScreenshot(name: String = "Screenshot") { + let screenshot = XCUIScreen.main.screenshot() + let attachment = XCTAttachment(screenshot: screenshot) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } + + /// Attaches a screenshot of the supplied application. + func allureAttachAppScreenshot(app: XCUIApplication, name: String = "App Screenshot") { + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } + + /// Captures failure-time artifacts if the current test did not succeed. + func allureCaptureFailureArtifacts(app: XCUIApplication?, name: String = "Failure Screenshot") { + guard let app else { return } + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } +} + diff --git a/ios/BitPayAppUITests/flows/AppFlows.swift b/ios/BitPayAppUITests/flows/AppFlows.swift new file mode 100644 index 0000000000..c13c28ac1b --- /dev/null +++ b/ios/BitPayAppUITests/flows/AppFlows.swift @@ -0,0 +1,39 @@ +// +// AppFlows.swift +// BitPayApp +// +// Created by vinoth vasu on 15/05/26. +// + +import XCTest + +final class AppFlows { + + static func completeOnboardingIfRequired(app: XCUIApplication) { + + let onboardingPage = OnboardingPage(app: app) + + if onboardingPage.isContinueWithoutAccountButtonDisplayed() { + + onboardingPage.tapContinuewithoutAnAccount() + onboardingPage.skipOnboarding() + onboardingPage.skipOnboarding() + onboardingPage.createWallet() + + guard onboardingPage.isBackupKeyLabelDisplayed() else { + XCTFail("Backup key page did not appear") + return + } + + onboardingPage.skipBackup() + onboardingPage.tapLater() + onboardingPage.acceptTerms() + onboardingPage.tapAgreeAndContinueButton() + + XCTAssertTrue( + onboardingPage.isYourPortfolioBalanceTextDisplayed(), + "Home screen did not load" + ) + } + } +} diff --git a/ios/BitPayAppUITests/pages/AddCryptoOptionPage.swift b/ios/BitPayAppUITests/pages/AddCryptoOptionPage.swift new file mode 100644 index 0000000000..4b26128fee --- /dev/null +++ b/ios/BitPayAppUITests/pages/AddCryptoOptionPage.swift @@ -0,0 +1,34 @@ +import XCTest + +class AddCryptoOptionPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + var selectAnOptionText: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Select an Option'") + ).firstMatch + } + + var importKey: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Import Key'") + ).firstMatch + } + + // MARK: - Actions + + func isSelectAnOptionTitleDisplayed(timeout: TimeInterval = 30) -> Bool { + return selectAnOptionText.waitForExistence(timeout: timeout) + } + + func tapImportKey() { + importKey.tap() + } + +} diff --git a/ios/BitPayAppUITests/pages/BuyPage.swift b/ios/BitPayAppUITests/pages/BuyPage.swift new file mode 100644 index 0000000000..a83276284b --- /dev/null +++ b/ios/BitPayAppUITests/pages/BuyPage.swift @@ -0,0 +1,34 @@ +import XCTest + +class BuyPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + var buyTitle: XCUIElement { + app.staticTexts["Buy"].firstMatch + } + + var bitcoin: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Bitcoin'") + ).firstMatch + } + + + // MARK: - Validations + + func isBuyPageTitleDisplayed(timeout: TimeInterval = 180) -> Bool { + return buyTitle.waitForExistence(timeout: timeout) + } + + func tapBitcoin() { + bitcoin.tap() + } + + +} diff --git a/ios/BitPayAppUITests/pages/ConfirmPaymentPage.swift b/ios/BitPayAppUITests/pages/ConfirmPaymentPage.swift new file mode 100644 index 0000000000..fd82fc0aac --- /dev/null +++ b/ios/BitPayAppUITests/pages/ConfirmPaymentPage.swift @@ -0,0 +1,45 @@ + +// +// OnboardingPage.swift +// BitPayApp +// +// Created by vinoth vasu on 16/03/26. +// + +import XCTest + +class ConfirmPaymentPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var confirmPaymentText: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Confirm Payment'") + ).firstMatch + } + + var summaryText: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'SUMMARY'") + ).firstMatch + } + + + // MARK: - Actions + + func isConfirmPaymentTitleDisplayed(timeout: TimeInterval = 180) -> Bool { + return confirmPaymentText.waitForExistence(timeout: timeout) + } + + func isSummaryTextDisplayed(timeout: TimeInterval = 5) -> Bool { + return summaryText.waitForExistence(timeout: timeout) + } + + +} diff --git a/ios/BitPayAppUITests/pages/EnterAmountPage.swift b/ios/BitPayAppUITests/pages/EnterAmountPage.swift new file mode 100644 index 0000000000..ed597762ad --- /dev/null +++ b/ios/BitPayAppUITests/pages/EnterAmountPage.swift @@ -0,0 +1,86 @@ + +// +// OnboardingPage.swift +// BitPayApp +// +// Created by vinoth vasu on 16/03/26. +// + +import XCTest + +class EnterAmountPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + var amountField: XCUIElement { + app.staticTexts.matching(identifier: "0").firstMatch + } + + var continueButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label CONTAINS 'Continue'") + ).firstMatch + } + + // MARK: - Actions + func enterAmount(amount: String = "0") { + for char in amount { + let key = app.staticTexts[String(char)].firstMatch + XCTAssertTrue(key.waitForExistence(timeout: 5)) + key.tap() + } + } + + func tapContinue() { + continueButton.tap() + } + + /** + Backspace key does not expose a unique accessibility identifier. + Calculate its position dynamically using the relative spacing + between the keypad keys "6" and "9". + */ + func tapBackspace(count: Int = 1) { + + let sixKey = app.staticTexts["6"].firstMatch + let nineKey = app.staticTexts["9"].firstMatch + + XCTAssertTrue( + sixKey.waitForExistence(timeout: 5), + "6 key not found" + ) + + XCTAssertTrue( + nineKey.waitForExistence(timeout: 5), + "9 key not found" + ) + + let sixFrame = sixKey.frame + let nineFrame = nineKey.frame + + let rowHeight = abs(nineFrame.midY - sixFrame.midY) + + let backspaceX = nineFrame.midX + let backspaceY = nineFrame.midY + rowHeight + + let backspaceCoordinate = app.coordinate( + withNormalizedOffset: CGVector(dx: 0, dy: 0) + ).withOffset( + CGVector( + dx: backspaceX, + dy: backspaceY + ) + ) + + for _ in 0.. Bool { + return backupKeyLabel.waitForExistence(timeout: timeout) + } + + func tapLater() { + laterButton.tap() + } + + func acceptTerms() { + checkbox1.tap() + checkbox2.tap() + checkbox3.tap() + } + + func tapAgreeAndContinueButton() { + agreeAndContinueButton.tap() + } + + func isYourPortfolioBalanceTextDisplayed(timeout: TimeInterval = 25) -> Bool { + return yourPortfolioBalanceText.waitForExistence(timeout: timeout) + } + + func swipeOnboarding() { + onboardingScrollView.swipeRight() + } + + func tapGotIt() { + gotItButton.tap() + } + + func tapBuyOption() { + buyButton.tap() + } + + func tapBackUpKey() { + backUpKey.tap() + } + + func tapAddCryptoButton() { + addCryptoButton.tap() + } + +} diff --git a/ios/BitPayAppUITests/pages/ImportRecoveryPhrasePage.swift b/ios/BitPayAppUITests/pages/ImportRecoveryPhrasePage.swift new file mode 100644 index 0000000000..1ea89287a5 --- /dev/null +++ b/ios/BitPayAppUITests/pages/ImportRecoveryPhrasePage.swift @@ -0,0 +1,39 @@ +import XCTest + +class ImportRecoveryPhrasePage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var inputRecoveryPhrase: XCUIElement { + app.descendants(matching: .any) + .matching(NSPredicate(format: "label == 'Enter recovery phrase'")) + .firstMatch + } + + var importWalletButton: XCUIElement { + app.descendants(matching: .any) + .matching(NSPredicate(format: "label == 'Import wallet'")) + .firstMatch + } + + // MARK: - Actions + + func isImportScreenDisplayed(timeout: TimeInterval = 5) -> Bool { + return inputRecoveryPhrase.waitForExistence(timeout: timeout) + } + + func enterRecoveryPhrase(_ phrase: String) { + inputRecoveryPhrase.tap() + inputRecoveryPhrase.typeText(phrase) + } + + func tapImportWallet() { + importWalletButton.tap() + } +} diff --git a/ios/BitPayAppUITests/pages/MykeyPage.swift b/ios/BitPayAppUITests/pages/MykeyPage.swift new file mode 100644 index 0000000000..1fb5546839 --- /dev/null +++ b/ios/BitPayAppUITests/pages/MykeyPage.swift @@ -0,0 +1,45 @@ +import XCTest + +class MyKeyPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var myKeyTab: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'My Key'") + ).firstMatch + } + + var myWalletsText: XCUIElement { + app.staticTexts.matching( + NSPredicate(format: "label == 'My Wallets'") + ).firstMatch + } + + var bitcoinWallet: XCUIElement { + app.descendants(matching: .any).matching( + NSPredicate(format: "label CONTAINS[c] %@", "Bitcoin") + ).firstMatch + } + + // MARK: - Validations + + func isMyKeyDisplayed(timeout: TimeInterval = 900) -> Bool { + return myKeyTab.waitForExistence(timeout: timeout) + } + + func isMyWalletsDisplayed(timeout: TimeInterval = 5) -> Bool { + return myWalletsText.waitForExistence(timeout: timeout) + } + + func isBitcoinBTCWalletDisplayed(timeout: TimeInterval = 300) -> Bool { + return bitcoinWallet.waitForExistence(timeout: timeout) + } + +} diff --git a/ios/BitPayAppUITests/pages/NewRecoveryPhrasePage.swift b/ios/BitPayAppUITests/pages/NewRecoveryPhrasePage.swift new file mode 100644 index 0000000000..37ded1f5ba --- /dev/null +++ b/ios/BitPayAppUITests/pages/NewRecoveryPhrasePage.swift @@ -0,0 +1,89 @@ +import XCTest + +class NewRecoveryPhrasePage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var instructionText: XCUIElement { + app.staticTexts.matching( + NSPredicate(format: "label CONTAINS 'recovery phrase'") + ).firstMatch + } + + var textFields: [XCUIElement] { + app.textFields.allElementsBoundByIndex + } + + var confirmButton: XCUIElement { + app.otherElements["Confirm"].firstMatch + } + + var okButton: XCUIElement { + app.staticTexts["OK"].firstMatch + } + + // MARK: - Actions + + func isVerifyScreenDisplayed(timeout: TimeInterval = 10) -> Bool { + return instructionText.waitForExistence(timeout: timeout) + } + + func tapConfirm() { + XCTAssertTrue(confirmButton.waitForExistence(timeout: 5)) + confirmButton.tap() + } + + func tapOK() { + XCTAssertTrue(okButton.waitForExistence(timeout: 5)) + okButton.tap() + } + + // MARK: - Business Logic + + /// Extract indexes dynamically (e.g. 11, 9, 12) + func getRequiredIndexes() -> [Int] { + + XCTAssertTrue(instructionText.waitForExistence(timeout: 5)) + + let instruction = instructionText.label + + return extractNumbers(from: instruction).compactMap { Int($0) } + } + + /// Enter words dynamically + func enterRecoveryPhrase(words: [String]) { + + let indexes = getRequiredIndexes() + + for (i, index) in indexes.enumerated() { + let word = words[index - 1] + + let field = textFields[i] + XCTAssertTrue(field.waitForExistence(timeout: 5)) + + field.tap() + field.typeText(word) + } + + NSLog("Entered words for indexes \(indexes)") + } + + func extractNumbers(from text: String) -> [String] { + do { + let regex = try NSRegularExpression(pattern: "\\d+") + let results = regex.matches( + in: text, + range: NSRange(text.startIndex..., in: text) + ) + return results.map { String(text[Range($0.range, in: text)!]) } + } catch { + return [] + } + } +} diff --git a/ios/BitPayAppUITests/pages/OnboardingPage.swift b/ios/BitPayAppUITests/pages/OnboardingPage.swift new file mode 100644 index 0000000000..b59d1cdabd --- /dev/null +++ b/ios/BitPayAppUITests/pages/OnboardingPage.swift @@ -0,0 +1,176 @@ +// +// OnboardingPage.swift +// BitPayApp +// +// Created by vinoth vasu on 10/03/26. +// + +import XCTest + +class OnboardingPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var continueWithoutAnAccountButton: XCUIElement { + app.descendants(matching: .any) + .matching(NSPredicate(format: "label == 'Continue without an account'")) + .firstMatch + } + + var skipButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Skip'") + ).firstMatch + } + + var skipBackupButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Skip backup'") + ).firstMatch + } + + var createKeyButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Create a key'") + ).firstMatch + } + + var alreadyHaveWalletKeyButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'I already have a key'") + ).firstMatch + } + + var backupKeyLabel: XCUIElement { + app.staticTexts["Would you like to backup your key?"] + } + + var laterButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'LATER'") + ).firstMatch + } + + var gotItButton: XCUIElement { + app.staticTexts["GOT IT"] + } + + var checkbox1: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate( + format: + "label CONTAINS 'Checkbox, My funds are held and controlled on this device'" + ) + ).firstMatch + } + + var checkbox2: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate( + format: + "label CONTAINS 'Checkbox, BitPay can never recover my funds for me'" + ) + ).firstMatch + } + + var checkbox3: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate( + format: + "label CONTAINS 'Checkbox, I have read, understood and accepted the Wallet Terms of Use'" + ) + ).firstMatch + } + + var agreeAndContinueButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Agree and continue'") + ).firstMatch + } + + var yourPortfolioBalanceText: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Portfolio balance info'") + ).firstMatch + } + + var onboardingScrollView: XCUIElement { + app.scrollViews.firstMatch + } + + // MARK: - Actions + + func handleTrackingPermissionIfDisplayed(timeout: TimeInterval = 30) { + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + let askAppNotToTrackButton = springboard.buttons["Ask App Not to Track"] + + if askAppNotToTrackButton.waitForExistence(timeout: timeout) { + askAppNotToTrackButton.tap() + } + } + + func isContinueWithoutAccountButtonDisplayed(timeout: TimeInterval = 15) -> Bool { + return continueWithoutAnAccountButton.waitForExistence(timeout: timeout) + } + + func tapContinuewithoutAnAccount() { + continueWithoutAnAccountButton.tap() + } + + func skipOnboarding() { + skipButton.tap() + } + + func skipBackup() { + skipBackupButton.tap() + } + + func createWallet() { + createKeyButton.tap() + } + + func alreadyHaveWalletKey() { + alreadyHaveWalletKeyButton.tap() + } + + func isBackupKeyLabelDisplayed(timeout: TimeInterval = 120) -> Bool { + return backupKeyLabel.waitForExistence(timeout: timeout) + } + + func tapLater() { + laterButton.tap() + } + + func acceptTerms() { + XCTAssertTrue(checkbox1.waitForExistence(timeout: 10)) + checkbox1.tap() + checkbox2.tap() + + let coordinate = checkbox3.coordinate( + withNormalizedOffset: CGVector(dx: 0.1, dy: 0.5) + ) + coordinate.tap() + } + + func tapAgreeAndContinueButton() { + agreeAndContinueButton.tap() + } + + func isYourPortfolioBalanceTextDisplayed(timeout: TimeInterval = 25) -> Bool { + return yourPortfolioBalanceText.waitForExistence(timeout: timeout) + } + + func swipeOnboarding() { + onboardingScrollView.swipeRight() + } + + func tapGotIt() { + gotItButton.tap() + } +} diff --git a/ios/BitPayAppUITests/pages/PortfolioBalancePage.swift b/ios/BitPayAppUITests/pages/PortfolioBalancePage.swift new file mode 100644 index 0000000000..36e4df842e --- /dev/null +++ b/ios/BitPayAppUITests/pages/PortfolioBalancePage.swift @@ -0,0 +1,157 @@ + +// +// OnboardingPage.swift +// BitPayApp +// +// Created by vinoth vasu on 16/03/26. +// + +import XCTest + +class PortfolioBalancePage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var yourPortfolioBalanceText: XCUIElement { + app.otherElements["Portfolio Balance"].firstMatch + } + + var addCryptoButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Add crypto wallet'") + ).firstMatch + } + + var buyButton: XCUIElement { + app.descendants(matching: .any) + .matching(identifier: "buy-button") + .firstMatch + } + + var sellButton: XCUIElement { + app.descendants(matching: .any) + .matching(identifier: "sell-button") + .firstMatch + } + + var sendButton: XCUIElement { + app.descendants(matching: .any) + .matching(identifier: "send-button") + .firstMatch + } + + var swapButton: XCUIElement { + app.descendants(matching: .any) + .matching(identifier: "swap-button") + .firstMatch + } + + // MARK: - Actions + + func isYourPortfolioBalanceTextDisplayed(timeout: TimeInterval = 25) -> Bool { + return yourPortfolioBalanceText.waitForExistence(timeout: timeout) + } + +// func tapBuyButton() { +// buyButton.tap() +// } + + func tapBuyButton(timeout: TimeInterval = 900) { + XCTAssertTrue( + buyButton.waitForExistence(timeout: timeout), + "Buy button did not appear within \(timeout) seconds" + ) + buyButton.tap() + } + +// func tapSellButton() { +// sellButton.tap() +// } + + func tapSellButton(timeout: TimeInterval = 900) { + XCTAssertTrue( + sellButton.waitForExistence(timeout: timeout), + "Sell button did not appear within \(timeout) seconds" + ) + sellButton.tap() + } + +// func tapAddCryptoButton() { +// addCryptoButton.tap() +// } + + func tapAddCryptoButton() { + +// let selectAnOptionPage = AddCryptoOptionPage(app: app) +// +// for attempt in 1...5 { +// + addCryptoButton.tap() + +// if selectAnOptionPage.isSelectAnOptionTitleDisplayed() { +// return +// } +// +// if attempt < 5 { +// sleep(60) +// } +// } +// +// XCTFail("Failed to open 'Select an Option' screen after 5 attempts") + } + + func tapSendButton(timeout: TimeInterval = 900) { + XCTAssertTrue( + sendButton.waitForExistence(timeout: timeout), + "Send button did not appear within \(timeout) seconds" + ) + sendButton.tap() + } + +// func tapSendButton(timeout: TimeInterval = 900) { +// +// let isDisplayed = sendButton.waitForExistence(timeout: timeout) +// +// if !isDisplayed { +// +// // Attach full app hierarchy/page source into xcresult +// let pageSourceAttachment = XCTAttachment( +// string: app.debugDescription +// ) +// +// pageSourceAttachment.name = "App Debug Description XML" +// pageSourceAttachment.lifetime = .keepAlways +// +// XCTContext.runActivity( +// named: "Attach App Debug Description" +// ) { activity in +// activity.add(pageSourceAttachment) +// } +// +// XCTFail( +// "Send button did not appear within \(timeout) seconds" +// ) +// } +// +// sendButton.tap() +// } + +// func tapSwapButton() { +// swapButton.tap() +// } + + func tapSwapButton(timeout: TimeInterval = 900) { + XCTAssertTrue( + swapButton.waitForExistence(timeout: timeout), + "Swap button did not appear within \(timeout) seconds" + ) + swapButton.tap() + } + +} diff --git a/ios/BitPayAppUITests/pages/SelectCurrencyPage.swift b/ios/BitPayAppUITests/pages/SelectCurrencyPage.swift new file mode 100644 index 0000000000..9ddbdf8d80 --- /dev/null +++ b/ios/BitPayAppUITests/pages/SelectCurrencyPage.swift @@ -0,0 +1,68 @@ + +// +// OnboardingPage.swift +// BitPayApp +// +// Created by vinoth vasu on 16/03/26. +// + +import XCTest + +class SelectCurrencyPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var selectCurrencyTitle: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Select a Currency'") + ).firstMatch + } + + var bitcoinCurrency: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label CONTAINS 'Bitcoin, BTC'") + ).firstMatch + } + + var sendToTitle: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Send To'") + ).firstMatch + } + + var recipientAddress: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Recipient address'") + ).firstMatch + } + + + // MARK: - Actions + + func isSelectCurrencyTitleDisplayed(timeout: TimeInterval = 180) -> Bool { + return selectCurrencyTitle.waitForExistence(timeout: timeout) + } + + func tapBitcoinCurrency() { + bitcoinCurrency.tap() + } + + func isSendToTitleDisplayed(timeout: TimeInterval = 180) -> Bool { + return sendToTitle.waitForExistence(timeout: timeout) + } + + func enterRecipientAddress(address: String) { + recipientAddress.tap() + for character in address { + recipientAddress.typeText(String(character)) + // usleep(100000) + } + } + +} diff --git a/ios/BitPayAppUITests/pages/SelectKeyToDepositPage.swift b/ios/BitPayAppUITests/pages/SelectKeyToDepositPage.swift new file mode 100644 index 0000000000..89ed5846f4 --- /dev/null +++ b/ios/BitPayAppUITests/pages/SelectKeyToDepositPage.swift @@ -0,0 +1,57 @@ +import XCTest + +class SelectKeyToDepositPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var selectKeyToDeposit: XCUIElement { + app.staticTexts.matching( + NSPredicate(format: "label CONTAINS 'Select Key to Deposit to'") + ).firstMatch + } + + var myKeyWallet: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'My Key wallet'") + ).firstMatch + } + + var secondMyKeyWallet: XCUIElement { + app.descendants(matching: .any) + .matching(NSPredicate(format: "label CONTAINS 'My Key'")) + .element(boundBy: 1) + } + + var evmAccount: XCUIElement { + app.descendants(matching: .any).matching( + NSPredicate(format: "label == 'EVM Account'") + ).firstMatch + } + + + // MARK: - Validations + + func isSelectKeyToDepositToDisplayed(timeout: TimeInterval = 5) -> Bool { + return selectKeyToDeposit.waitForExistence(timeout: timeout) + } + + func tapMyKeyWallet() { + myKeyWallet.tap() + } + + func tapSecondMyKeyWallet() { + secondMyKeyWallet.tap() + } + + func tapEVMAccount() { + evmAccount.tap() + } + + +} diff --git a/ios/BitPayAppUITests/pages/SellPage.swift b/ios/BitPayAppUITests/pages/SellPage.swift new file mode 100644 index 0000000000..1fb3b1e0d6 --- /dev/null +++ b/ios/BitPayAppUITests/pages/SellPage.swift @@ -0,0 +1,35 @@ +import XCTest + +class SellPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var sellTitle: XCUIElement { + app.staticTexts["Sell"].firstMatch + } + + var chooseCrypto: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Choose Crypto'") + ).firstMatch + } + + + // MARK: - Validations + + func isSellPageTitleDisplayed(timeout: TimeInterval = 180) -> Bool { + return sellTitle.waitForExistence(timeout: timeout) + } + + func tapChooseCrypto() { + chooseCrypto.tap() + } + + +} diff --git a/ios/BitPayAppUITests/pages/SwapPage.swift b/ios/BitPayAppUITests/pages/SwapPage.swift new file mode 100644 index 0000000000..19a337d664 --- /dev/null +++ b/ios/BitPayAppUITests/pages/SwapPage.swift @@ -0,0 +1,189 @@ +import XCTest + +class SwapPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var swapTitle: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Swap'") + ).firstMatch + } + + var selectWalletFrom: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Select wallet to swap from'") + ).firstMatch + } + + var cryptoToSwapPage: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Crypto to Swap'") + ).firstMatch + } + + var bitcoin: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label CONTAINS 'Bitcoin, BTC'") + ).firstMatch + } + + var selectWalletTo: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Select wallet to swap to'") + ).firstMatch + } + + var swapTo: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Swap To'") + ).firstMatch + } + + var ethereum: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Ethereum, ETH'") + ).firstMatch + } + + var selectKeyToDeposit: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Select Key to Deposit to'") + ).firstMatch + } + + var myKeyWallet: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'My Key wallet'") + ).firstMatch + } + + var evmAccount: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'EVM Account'") + ).firstMatch + } + + var swapCrypoButton: XCUIElement { + app.descendants(matching: .any).matching( + NSPredicate( + format: "label == 'Swap crypto toggle fiat display button'" + ) + ).firstMatch + } + + var enterAmount: XCUIElement { + app.descendants(matching: .any).matching( + NSPredicate( + format: "label == 'Swap crypto enter amount button'" + ) + ).firstMatch + } + + var minAmount: XCUIElement { + app.descendants(matching: .any).matching( + NSPredicate( + format: "label == 'MIN'" + ) + ).firstMatch + } + + var changellyTermsCheckbox: XCUIElement { + app.descendants(matching: .any).matching( + NSPredicate( + format: "identifier == 'swap-crypto-changelly-terms-checkbox'" + ) + ).firstMatch + } + + var slideToSwapButton: XCUIElement { + app.descendants(matching: .any).matching( + NSPredicate( + format: "label == 'Swap crypto slide to swap button'" + ) + ).firstMatch + } + + + // MARK: - Validations + + func isSwapPageTitleDisplayed(timeout: TimeInterval = 5) -> Bool { + return swapTitle.waitForExistence(timeout: timeout) + } + + func tapSelectWalletFrom() { + selectWalletFrom.tap() + } + + func isCryptoToSwapPageDisplayed(timeout: TimeInterval = 5) -> Bool { + return cryptoToSwapPage.waitForExistence(timeout: timeout) + } + + func isBitcoinOptionDisplayed(timeout: TimeInterval = 10) -> Bool { + return bitcoin.waitForExistence(timeout: timeout) + } + + func tapBitcoin() { + bitcoin.tap() + } + + func tapSelectWalletTo() { + selectWalletTo.tap() + } + + func isSwapToPageDisplayed(timeout: TimeInterval = 5) -> Bool { + return swapTo.waitForExistence(timeout: timeout) + } + + func tapEthereum() { + ethereum.tap() + } + + func isSelectKeyToDepositToDisplayed(timeout: TimeInterval = 5) -> Bool { + return selectKeyToDeposit.waitForExistence(timeout: timeout) + } + + func tapMyKeyWallet() { + myKeyWallet.tap() + } + + func tapSwapCrypoButton() { + swapCrypoButton.tap() + } + + func tapEnterAmount() { + enterAmount.tap() + } + + func isChangellyTermsCheckboxDisplayed(timeout: TimeInterval = 5) -> Bool { + return changellyTermsCheckbox.waitForExistence(timeout: timeout) + } + + func tapChangellyTermsCheckbox() { + app.swipeUp() + sleep(1) + changellyTermsCheckbox.tap() + } + + func tapMinAmount() { + minAmount.tap() + } + + func tapCenterOfScreen() { + let center = app.coordinate( + withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5) + ) + center.tap() + } + + func isSlideToSwapButtonDisplayed(timeout: TimeInterval = 10) -> Bool { + return slideToSwapButton.waitForExistence(timeout: timeout) + } + +} diff --git a/ios/BitPayAppUITests/pages/VerifyYourPhrasePage.swift b/ios/BitPayAppUITests/pages/VerifyYourPhrasePage.swift new file mode 100644 index 0000000000..dde1166147 --- /dev/null +++ b/ios/BitPayAppUITests/pages/VerifyYourPhrasePage.swift @@ -0,0 +1,100 @@ +import XCTest + +class RecoveryPhrasePage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var recoveryPhraseTitle: XCUIElement { + app.staticTexts["Recovery Phrase"] + } + + var nextButton: XCUIElement { + app.otherElements["next-button"].firstMatch + } + + var allStaticTexts: XCUIElementQuery { + app.staticTexts + } + + var textFields: [XCUIElement] { + app.textFields.allElementsBoundByIndex + } + + // MARK: - Actions + + func isRecoveryPhraseDisplayed(timeout: TimeInterval = 10) -> Bool { + return recoveryPhraseTitle.waitForExistence(timeout: timeout) + } + + func tapNext() { + XCTAssertTrue(nextButton.waitForExistence(timeout: 5)) + nextButton.tap() + } + + // MARK: - Business Logic + + /// Extract 12 recovery words dynamically + func getRecoveryWords() -> [String] { + return allStaticTexts.allElementsBoundByIndex.compactMap { element in + let text = element.label.trimmingCharacters(in: .whitespaces) + + // Only lowercase words (filters headers, numbers) + let isWord = + text.range(of: "^[a-z]+$", options: .regularExpression) != nil + return isWord ? text : nil + } + .prefix(12) + .map { $0 } + } + + func extractNumbers(from text: String) -> [String] { + do { + let regex = try NSRegularExpression(pattern: "\\d+") + let results = regex.matches( + in: text, + range: NSRange(text.startIndex..., in: text) + ) + return results.map { String(text[Range($0.range, in: text)!]) } + } catch { + return [] + } + } + + /// Extract indexes like 2nd, 11th, 9th dynamically + func getRequiredIndexes() -> [Int] { + + let instructionElement = app.staticTexts.matching( + NSPredicate(format: "label CONTAINS 'recovery phrase correctly'") + ).firstMatch + + XCTAssertTrue(instructionElement.waitForExistence(timeout: 5)) + + let instruction = instructionElement.label + + return extractNumbers(from: instruction).compactMap { Int($0) } + } + + /// Enter recovery phrase based on required indexes + func enterRecoveryPhrase(words: [String]) { + + let indexes = getRequiredIndexes() + + for (i, index) in indexes.enumerated() { + let word = words[index - 1] + + let field = textFields[i] + XCTAssertTrue(field.waitForExistence(timeout: 5)) + + field.tap() + field.typeText(word) + } + + NSLog("Entered words for indexes \(indexes)") + } +} diff --git a/ios/BitPayAppUITests/tests/Test591_OnboardingCreateWallet.swift b/ios/BitPayAppUITests/tests/Test591_OnboardingCreateWallet.swift new file mode 100644 index 0000000000..ccb1f61567 --- /dev/null +++ b/ios/BitPayAppUITests/tests/Test591_OnboardingCreateWallet.swift @@ -0,0 +1,96 @@ +import XCTest + +final class Test591_OnboardingCreateWallet: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + + let onboardingPage = OnboardingPage(app: app) + onboardingPage.handleTrackingPermissionIfDisplayed() + } + + @MainActor + func test591_OnboardingCreateWallet() throws { + + AllureXCTestSupport.setDisplayName( + "Onboarding Create Wallet (Continue Without Account)" + ) + AllureXCTestSupport.addLabel( + "testlioManualTestID", + value: "003e6686-1128-4e66-b020-aa03f3b6ec67" + ) + AllureXCTestSupport.addDescription( + "Onboarding Create Wallet without creating a new" + ) + + let onboardingPage = OnboardingPage(app: app) + + AllureXCTestSupport.step( + "(4) On the onboarding screen “Seamlessly buy & swap”, tap Continue without an account." + ) { + onboardingPage.tapContinuewithoutAnAccount() + } + + AllureXCTestSupport.step( + "(5) On the “Turn on notifications” screen, tap Skip." + ) { + onboardingPage.skipOnboarding() + } + + AllureXCTestSupport.step( + "(6) On the “Protect your wallet” screen, tap Skip" + ) { + onboardingPage.skipOnboarding() //To skip protect wallet + } + + AllureXCTestSupport.step( + "(7) On the “Create a key or import an existing key” screen, tap Create a Key." + ) { + onboardingPage.createWallet() + } + + AllureXCTestSupport.step( + "(8) Wait until wallet creation finishes and the “Would you like to backup your key?” screen is displayed." + ) { + XCTAssertTrue( + onboardingPage.isBackupKeyLabelDisplayed(), + "Backup key page did not appear after tapping 'Create a Key'" + ) + } + + AllureXCTestSupport.step("(9) Tap Skip on the backup prompt screen.") { + onboardingPage.skipBackup() //To skip backup key + } + + AllureXCTestSupport.step( + "(10) On the “Are you sure?” confirmation modal, tap Later. " + ) { + onboardingPage.tapLater() + } + + AllureXCTestSupport.step( + "(11, 12 & 13) On the “Important” screen, select all the three agreements checkbox. " + ) { + onboardingPage.acceptTerms() + } + + AllureXCTestSupport.step("(14) Tap Agree and Continue. ") { + onboardingPage.tapAgreeAndContinueButton() + } + + AllureXCTestSupport.step( + "(15) Wait until the Home screen finishes loading. " + ) { + XCTAssertTrue( + onboardingPage.isYourPortfolioBalanceTextDisplayed(), + "Home Page - Your Portfolio Balance text is not displayed" + ) + } + } + +} diff --git a/ios/BitPayAppUITests/tests/Test592_ImportWalletRecoveryPhrase.swift b/ios/BitPayAppUITests/tests/Test592_ImportWalletRecoveryPhrase.swift new file mode 100644 index 0000000000..85e2c34033 --- /dev/null +++ b/ios/BitPayAppUITests/tests/Test592_ImportWalletRecoveryPhrase.swift @@ -0,0 +1,86 @@ +import XCTest + +final class Test592_ImportWalletRecoveryPhrase: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + + let onboardingPage = OnboardingPage(app: app) + onboardingPage.handleTrackingPermissionIfDisplayed() + + AppFlows.completeOnboardingIfRequired(app: app) + } + + @MainActor + func test592_ImportWalletRecoveryPhrase() throws { + + AllureXCTestSupport.setDisplayName( + "Import Wallet via Recovery Phrase (BTC)" + ) + AllureXCTestSupport.addLabel( + "testlioManualTestID", + value: "1ca663da-1909-451f-9908-25ba9cdc0396" + ) + AllureXCTestSupport.addDescription( + "Import Wallet via Recovery Phrase (BTC)" + ) + + let portfolioBalancePage = PortfolioBalancePage(app: app) + let selectAnOptionPage = AddCryptoOptionPage(app: app) + let importRecoveryPhasePage = ImportRecoveryPhrasePage(app: app) + let myKeyPage = MyKeyPage(app: app) + + AllureXCTestSupport.step( + "(1 to 3) On the Home screen, tap the Plus (+) control near the “Your Crypto” section to open wallet/key options." + ) { + portfolioBalancePage.tapAddCryptoButton() + } + + AllureXCTestSupport.step( + "(4) Wait until the “Select an Option” screen is displayed." + ) { + XCTAssertTrue( + selectAnOptionPage.isSelectAnOptionTitleDisplayed(), + "Select an option page is not displayed" + ) + } + + AllureXCTestSupport.step("(5) Tap Import Key") { + selectAnOptionPage.tapImportKey() + } + + AllureXCTestSupport.step( + "(6 to 7) On the “Import” screen, tap the Recovery Phrase input field & Enter a valid Recovery Phrase. " + ) { + importRecoveryPhasePage.enterRecoveryPhrase( + "hobby short divert lady spare quit act settle body town license alone" + ) + } + + AllureXCTestSupport.step("(8) Tap Import Wallet") { + importRecoveryPhasePage.tapImportWallet() + } + + AllureXCTestSupport.step( + "(9) Wait until the import finishes and the My Key screen is displayed (use an extended wait to account for slower imports). " + ) { + XCTAssertTrue(myKeyPage.isMyKeyDisplayed(), "Is My Key not displayed") + XCTAssertTrue( + myKeyPage.isMyWalletsDisplayed(), + "My Wallets not displayed" + ) + + XCTAssertTrue( + myKeyPage.isBitcoinBTCWalletDisplayed(), + "Bitcoin BTC Wallet not displayed" + ) + } + + } + +} diff --git a/ios/BitPayAppUITests/tests/Test593_BTCConfirmPaymentScreen.swift b/ios/BitPayAppUITests/tests/Test593_BTCConfirmPaymentScreen.swift new file mode 100644 index 0000000000..0153b1db6e --- /dev/null +++ b/ios/BitPayAppUITests/tests/Test593_BTCConfirmPaymentScreen.swift @@ -0,0 +1,80 @@ +import XCTest + +final class Test593_BTCConfirmPaymentScreen: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + + let onboardingPage = OnboardingPage(app: app) + onboardingPage.handleTrackingPermissionIfDisplayed() + + AppFlows.completeOnboardingIfRequired(app: app) + } + + @MainActor + func test593_BTCConfirmPaymentScreen() throws { + + AllureXCTestSupport.setDisplayName( + "Basic Send (BTC) - Confirm Payment Screen" + ) + AllureXCTestSupport.addLabel( + "testlioManualTestID", + value: "be4a044a-2c3a-4cad-aa5c-9398f415b483" + ) + AllureXCTestSupport.addDescription( + "Basic Send (BTC) - Confirm Payment Screen" + ) + + let portfolioBalancePage = PortfolioBalancePage(app: app) + let selectCurrencyPage = SelectCurrencyPage(app: app) + let enterAmountPage = EnterAmountPage(app: app) + let confirmPaymentPage = ConfirmPaymentPage(app: app) + + AllureXCTestSupport.step("(1 to 4) On the Home screen, tap Send in the top menu.") { + portfolioBalancePage.tapSendButton() + } + + AllureXCTestSupport.step("(5) Wait until the Select a Currency screen is displayed. ") { + XCTAssertTrue( + selectCurrencyPage.isSelectCurrencyTitleDisplayed(), + "Select Currency page not displayed" + ) + } + + AllureXCTestSupport.step("(6) Tap Bitcoin.") { + selectCurrencyPage.tapBitcoinCurrency() + } + + AllureXCTestSupport.step("(7) Wait until the Send To screen is displayed.") { + selectCurrencyPage.isSendToTitleDisplayed() + } + + AllureXCTestSupport.step("(8 & 9) Tap the Search contact or enter address field & Paste the receiver address.") { + selectCurrencyPage.enterRecipientAddress(address: "bc1q0effzahtsn685tyjppgukpvfhv37hrlm4g67ws") + } + + AllureXCTestSupport.step("(10 & 11) Wait until the Amount screen is displayed & Enter 0.00005 BTC.") { + enterAmountPage.enterAmount(amount: "0.00005") + } + + AllureXCTestSupport.step("(12) Tap Continue and Verify") { + enterAmountPage.tapContinue() + + XCTAssertTrue( + confirmPaymentPage.isConfirmPaymentTitleDisplayed(), + "Confirm payment page not displayed" + ) + + XCTAssertTrue( + confirmPaymentPage.isSummaryTextDisplayed(), + "Confirm payment - Summary text not displayed" + ) + + } + } +} diff --git a/ios/BitPayAppUITests/tests/Test594_BasicSwapBTC.swift b/ios/BitPayAppUITests/tests/Test594_BasicSwapBTC.swift new file mode 100644 index 0000000000..0c5c922712 --- /dev/null +++ b/ios/BitPayAppUITests/tests/Test594_BasicSwapBTC.swift @@ -0,0 +1,94 @@ +import XCTest + +final class Test594_BasicSwapBTC: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + + let onboardingPage = OnboardingPage(app: app) + onboardingPage.handleTrackingPermissionIfDisplayed() + + AppFlows.completeOnboardingIfRequired(app: app) + } + + @MainActor + func test594_BasicSwapBTC() throws { + + AllureXCTestSupport.setDisplayName( + "Basic Swap BTC to ETH" + ) + AllureXCTestSupport.addLabel( + "testlioManualTestID", + value: "947f685f-e106-4708-922f-43419f9f8f50" + ) + AllureXCTestSupport.addDescription( + "Basic Swap BTC to ETH (View Offers)" + ) + + let portfolioBalancePage = PortfolioBalancePage(app: app) + let swapPage = SwapPage(app: app) + let selectKeyToDepositPage = SelectKeyToDepositPage(app: app) + let enterAmountPage = EnterAmountPage(app: app) + let selectCurrencyPage = SelectCurrencyPage(app: app) + + AllureXCTestSupport.step("(1 to 4) From the Home screen, tap Swap in the top menu. ") { + portfolioBalancePage.tapSwapButton() + } + + AllureXCTestSupport.step("(5) Wait until the Swap Crypto screen is displayed.") { + XCTAssertTrue(swapPage.isSwapPageTitleDisplayed(), "Swap page not displayed") + } + + AllureXCTestSupport.step("(6) In the Swap From section, tap Select Wallet") { + swapPage.tapSelectWalletFrom() + } + + AllureXCTestSupport.step("(7) On the Crypto to Swap screen, tap Bitcoin (BTC)") { + XCTAssertTrue(swapPage.isBitcoinOptionDisplayed(), "Bitcoin option not displayed") + swapPage.tapBitcoin() + } + + AllureXCTestSupport.step("(8) Back on the Swap Crypto screen, in the Swap To section, tap Select Crypto.") { + swapPage.tapSelectWalletTo() + } + + AllureXCTestSupport.step("(9) On the Swap To list screen, tap Ethereum (ETH).") { + swapPage.tapEthereum() + } + + AllureXCTestSupport.step("(10) On the Select Key to Deposit to screen, tap My Key (choose the entry that has funds available if multiple are shown).") { + XCTAssertTrue(selectKeyToDepositPage.isSelectKeyToDepositToDisplayed(), "Select Key to Deposit to dialog not displayed") + selectKeyToDepositPage.tapSecondMyKeyWallet() + } + + AllureXCTestSupport.step("(11) On the Swap Crypto screen, tap Enter Amount.") { + selectKeyToDepositPage.tapEVMAccount() + swapPage.tapSwapCrypoButton() + } + + AllureXCTestSupport.step("(12) On the Swap Amount bottom sheet, enter 0.0007 BTC.") { + swapPage.tapEnterAmount() + enterAmountPage.enterAmount(amount: "0.0007") + } + + AllureXCTestSupport.step("(13) Tap Changelly checkbox.") { + swapPage.tapCenterOfScreen() + XCTAssertTrue(swapPage.isChangellyTermsCheckboxDisplayed(), "Changelly Terms and Conditions checkbox not displayed") + swapPage.tapChangellyTermsCheckbox() + } + + AllureXCTestSupport.step( + "(14) Verify Slide to Swap button is displayed." + ) { + XCTAssertTrue( + swapPage.isSlideToSwapButtonDisplayed(), + "Slide to Swap button not displayed" + ) + } + } +} diff --git a/ios/BitPayAppUITests/tests/Test595_BasicSellBTC_.swift b/ios/BitPayAppUITests/tests/Test595_BasicSellBTC_.swift new file mode 100644 index 0000000000..f22bc3716a --- /dev/null +++ b/ios/BitPayAppUITests/tests/Test595_BasicSellBTC_.swift @@ -0,0 +1,64 @@ +import XCTest + +final class Test595_BasicSellBTC: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + + let onboardingPage = OnboardingPage(app: app) + onboardingPage.handleTrackingPermissionIfDisplayed() + + AppFlows.completeOnboardingIfRequired(app: app) + } + + @MainActor + func test595_BasicSellBTC() throws { + + AllureXCTestSupport.setDisplayName( + "Basic Sell BTC" + ) + AllureXCTestSupport.addLabel( + "testlioManualTestID", + value: "dfc4301a-fad9-4d7f-a13a-91ca1f1d54a7" + ) + AllureXCTestSupport.addDescription( + "Basic Sell BTC (Continue With Opens Browser)" + ) + + let portfolioBalancePage = PortfolioBalancePage(app: app) + let sellPage = SellPage(app: app) + let enterAmountPage = EnterAmountPage(app: app) + let selectCurrencyPage = SelectCurrencyPage(app: app) + + AllureXCTestSupport.step("(1 to 4) From the Home screen, tap Sell.") { + portfolioBalancePage.tapSellButton() + } + + AllureXCTestSupport.step("(5) Wait until the Sell screen is displayed.") { + XCTAssertTrue(sellPage.isSellPageTitleDisplayed(), "Sell page not displayed") + } + + AllureXCTestSupport.step("(6) Enter $30 as the sell amount.") { + enterAmountPage.enterAmount(amount: "0.007") + } + + AllureXCTestSupport.step("(7) Tap Choose Crypto.") { + sellPage.tapChooseCrypto() + } + + AllureXCTestSupport.step("(8) On the Select Crypto screen, tap Bitcoin (BTC). ") { + selectCurrencyPage.tapBitcoinCurrency() + //tap swap currency + } + + AllureXCTestSupport.step("(9) Tap Continue with.") { + enterAmountPage.tapContinue() + } + + } +} diff --git a/ios/BitPayAppUITests/tests/Test596_BasicBuyBTC_.swift b/ios/BitPayAppUITests/tests/Test596_BasicBuyBTC_.swift new file mode 100644 index 0000000000..895d6aeebe --- /dev/null +++ b/ios/BitPayAppUITests/tests/Test596_BasicBuyBTC_.swift @@ -0,0 +1,74 @@ +import XCTest + +final class Test596_BasicBuyBTC: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + + let onboardingPage = OnboardingPage(app: app) + onboardingPage.handleTrackingPermissionIfDisplayed() + + AppFlows.completeOnboardingIfRequired(app: app) + } + + @MainActor + func test596_BasicBuyBTC() throws { + + AllureXCTestSupport.setDisplayName( + "Basic Buy BTC" + ) + AllureXCTestSupport.addLabel( + "testlioManualTestID", + value: "3c9c0271-76ba-4da3-88ad-8a5d6ea60211" + ) + AllureXCTestSupport.addDescription( + "Basic Buy BTC (Best Offer → Continue With Opens Browser)" + ) + + let portfolioBalancePage = PortfolioBalancePage(app: app) + let buyPage = BuyPage(app: app) + let selectKeyToDepositPage = SelectKeyToDepositPage(app: app) + let enterAmountPage = EnterAmountPage(app: app) + let selectCurrencyPage = SelectCurrencyPage(app: app) + + AllureXCTestSupport.step("(1 to 4) From the Home screen, tap Buy.") { + portfolioBalancePage.tapBuyButton() + } + + AllureXCTestSupport.step("(5) Wait until the Buy screen is displayed. ") { + XCTAssertTrue(buyPage.isBuyPageTitleDisplayed(), "Buy page not displayed") + } + + AllureXCTestSupport.step("(6) Enter $30 as the buy amount.") { + enterAmountPage.tapBackspace(count: 3) + enterAmountPage.enterAmount(amount: "30") + } + + AllureXCTestSupport.step("(7) Tap Choose Crypto.") { + buyPage.tapBitcoin() + } + + AllureXCTestSupport.step("(8) On the Select Crypto screen, tap Bitcoin (BTC). ") { + selectCurrencyPage.tapBitcoinCurrency() + } + + AllureXCTestSupport.step("(9) On the Select Key to Deposit to screen, tap My Key with funds available.") { + XCTAssertTrue(selectKeyToDepositPage.isSelectKeyToDepositToDisplayed(), "Select Key to Deposit to dialog not displayed") + selectKeyToDepositPage.tapMyKeyWallet() + } + + AllureXCTestSupport.step("(10) Wait until best-offer search completes and at least one offer is displayed.") { + } + + AllureXCTestSupport.step("(11) Tap Continue with {ProviderName}.") { + } + + AllureXCTestSupport.step("(12) Wait for the browser / in-app webview to open.") { + } + } +} diff --git a/ios/BitPayAppUITests/tests/TestPlan.xctestplan b/ios/BitPayAppUITests/tests/TestPlan.xctestplan new file mode 100644 index 0000000000..e40ed87ae9 --- /dev/null +++ b/ios/BitPayAppUITests/tests/TestPlan.xctestplan @@ -0,0 +1,27 @@ +{ + "configurations" : [ + { + "id" : "0232DDA3-81F2-4B5D-9BC1-2EB32AECE399", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "testTimeoutsEnabled" : true, + "uiTestingScreenshotsLifetime" : "keepAlways", + "userAttachmentLifetime" : "keepAlways", + "systemAttachmentLifetime" : "keepAlways" + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:BitPayApp.xcodeproj", + "identifier" : "CDF085062FACD8A500CA4C1B", + "name" : "BitPayAppUITests" + } + } + ], + "version" : 1 +} diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh new file mode 100644 index 0000000000..065c21e704 --- /dev/null +++ b/scripts/run-tests.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +OVERALL_EXIT=0 + +for TEST_CLASS in \ + "Test591OnboardingCreateWallet" \ + "Test592ImportWalletRecoveryPhrase" \ + "Test593SendBTC" \ + "Test594SwapBTC" \ + "Test595SellBTC" \ + "Test596BuyBTC" +do + echo "=== Clearing state before $TEST_CLASS ===" + adb shell pm clear com.bitpay.wallet || true + sleep 2 + + echo "=== Running $TEST_CLASS ===" + cd android && ./gradlew connectedDebugAndroidTest \ + -PreactNativeArchitectures=x86_64 \ + -Pandroid.testInstrumentationRunnerArguments.class="com.bitpay.wallet.tests.$TEST_CLASS" \ + --no-daemon + EXIT=$? + cd .. + + [ $EXIT -ne 0 ] && OVERALL_EXIT=$EXIT + + # Pull allure results immediately after each test + # so a timeout or crash on a later test doesn't lose earlier results + echo "=== Pulling allure results after $TEST_CLASS ===" + mkdir -p /home/runner/work/_temp/allure-results + adb pull /sdcard/googletest/test_outputfiles/allure-results/. /home/runner/work/_temp/allure-results/ || true +done + +exit $OVERALL_EXIT \ No newline at end of file diff --git a/testlio-cli/project-config.json b/testlio-cli/project-config.json new file mode 100644 index 0000000000..1ea5da9269 --- /dev/null +++ b/testlio-cli/project-config.json @@ -0,0 +1,10 @@ +{ + "baseURI": "https://api.testlio.com/", + "platformURI": "https://app.testlio.com/tmt/project/", + "projectId": "3307", + "automatedRunCollectionGuid": "2df42fa4-225b-430e-882d-e5fc3ec31de7", + "testRunCollectionGuid": "a77e695c-5061-4451-98fe-4f93b8a26d9b", + "resultCollectionGuid": "3688212d-e132-40a3-a4bd-aae8b664a2d8", + "workspaceName": "bitpay---sandbox" +} + diff --git a/testlio-cli/test-config.json b/testlio-cli/test-config.json new file mode 100644 index 0000000000..2986f99020 --- /dev/null +++ b/testlio-cli/test-config.json @@ -0,0 +1,4 @@ +{ + "automatedTestNamePrefix": "Automation Run: Smoke Test" +} +