diff --git a/.changeset/expo-native-e2e-fixes.md b/.changeset/expo-native-e2e-fixes.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/expo-native-e2e-fixes.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.github/workflows/expo-native-build.yml b/.github/workflows/expo-native-build.yml index 118c3e8613c..9702cf7d505 100644 --- a/.github/workflows/expo-native-build.yml +++ b/.github/workflows/expo-native-build.yml @@ -27,7 +27,7 @@ env: SDK_PACK_DIR: /tmp/clerk-expo-pack E2E_INSTANCE_NAME: clerkstage-with-native-components BAPI_URL: https://api.clerkstage.dev - MAESTRO_RUNNER_VERSION: '1.1.21' + MAESTRO_VERSION: '2.8.0' jobs: native-build: @@ -214,33 +214,32 @@ jobs: path: ${{ steps.native-build-key.outputs.artifact }} key: ${{ steps.native-build-cache.outputs.cache-primary-key }} - - name: Cache maestro-runner + - name: Cache maestro CLI if: steps.keys.outputs.pk != '' uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: - path: ~/.maestro-runner - key: maestro-runner-${{ runner.os }}-${{ runner.arch }}-${{ env.MAESTRO_RUNNER_VERSION }} + path: ~/.maestro + key: maestro-${{ runner.os }}-${{ env.MAESTRO_VERSION }} - - name: Install maestro-runner + - name: Install maestro CLI if: steps.keys.outputs.pk != '' run: | set -o pipefail - if [ -x "$HOME/.maestro-runner/bin/maestro-runner" ]; then - echo "Using cached maestro-runner" + if [ -x "$HOME/.maestro/bin/maestro" ]; then + echo "Using cached Maestro" else installed=0 for i in 1 2 3; do - if curl -fsSL --retry 3 --retry-delay 5 "https://open.devicelab.dev/install/maestro-runner" | - bash -s -- --version "$MAESTRO_RUNNER_VERSION"; then - if [ -x "$HOME/.maestro-runner/bin/maestro-runner" ]; then installed=1; break; fi + if curl -fLs --retry 3 --retry-delay 5 "https://get.maestro.mobile.dev" | MAESTRO_VERSION="$MAESTRO_VERSION" bash; then + if [ -x "$HOME/.maestro/bin/maestro" ]; then installed=1; break; fi fi - echo "maestro-runner install attempt $i failed (or binary missing); retrying" + echo "Maestro install attempt $i failed (or binary missing); retrying" sleep 5 done - [ "$installed" = 1 ] || { echo "::error::maestro-runner install failed after 3 attempts"; exit 1; } + [ "$installed" = 1 ] || { echo "::error::Maestro install failed after 3 attempts"; exit 1; } fi - echo "$HOME/.maestro-runner/bin" >> "$GITHUB_PATH" - "$HOME/.maestro-runner/bin/maestro-runner" --version + echo "$HOME/.maestro/bin" >> "$GITHUB_PATH" + "$HOME/.maestro/bin/maestro" --version - name: Boot iOS simulator id: sim @@ -281,16 +280,10 @@ jobs: - name: Run iOS e2e (Maestro) id: run_e2e_ios if: matrix.platform == 'ios' && steps.user.outputs.user_id != '' - # Burn-in: e2e cannot fail the check yet. Flip to hard-fail in a - # follow-up once the suite has proven quiet. - continue-on-error: true working-directory: ${{ env.FIXTURE_DIR }} env: CLERK_TEST_EMAIL: ${{ steps.user.outputs.email }} CLERK_TEST_PASSWORD: ${{ steps.user.outputs.password }} - MAESTRO_DEVICE: ${{ steps.sim.outputs.udid }} - MAESTRO_DRIVER: wda - MAESTRO_PLATFORM: ios SIM_UDID: ${{ steps.sim.outputs.udid }} run: | echo "Using simulator $SIM_UDID" @@ -349,13 +342,10 @@ jobs: - name: Run Android e2e (Maestro) id: run_e2e_android if: matrix.platform == 'android' && steps.user.outputs.user_id != '' - continue-on-error: true uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2 env: CLERK_TEST_EMAIL: ${{ steps.user.outputs.email }} CLERK_TEST_PASSWORD: ${{ steps.user.outputs.password }} - MAESTRO_DRIVER: devicelab - MAESTRO_PLATFORM: android with: api-level: 34 target: google_apis @@ -383,7 +373,7 @@ jobs: # Test reports record flow env (and typed input) in plaintext; # add-mask only covers step logs, not artifact contents. - - name: Scrub test credentials from maestro-runner reports + - name: Scrub test credentials from Maestro debug output if: always() && (steps.run_e2e_ios.outcome == 'failure' || steps.run_e2e_android.outcome == 'failure') env: CLERK_TEST_PASSWORD: ${{ steps.user.outputs.password }} @@ -395,7 +385,7 @@ jobs: \( -name '*.html' -o -name '*.json' -o -name '*.log' -o -name '*.txt' -o -name '*.xml' -o -name '*.yaml' \) \ -exec perl -pi -e 's/\Q$ENV{CLERK_TEST_PASSWORD}\E/[REDACTED]/g' {} + - - name: Upload maestro-runner artifacts on e2e failure + - name: Upload Maestro artifacts on e2e failure if: always() && (steps.run_e2e_ios.outcome == 'failure' || steps.run_e2e_android.outcome == 'failure') uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: @@ -413,5 +403,5 @@ jobs: [ "$outcome" = "skipped" ] && outcome="$ANDROID_OUTCOME" echo "## Maestro e2e (${{ matrix.platform }}): $outcome" >> "$GITHUB_STEP_SUMMARY" if [ "$outcome" = "failure" ]; then - echo "::warning::Maestro e2e failed (burn-in mode: not failing the job). See the maestro-${{ matrix.platform }} artifact." + echo "::error::Maestro e2e failed. See the maestro-${{ matrix.platform }} artifact." fi diff --git a/integration/templates/expo-native/app.json b/integration/templates/expo-native/app.json index 58e258ea523..9c378558907 100644 --- a/integration/templates/expo-native/app.json +++ b/integration/templates/expo-native/app.json @@ -12,6 +12,6 @@ "android": { "package": "com.clerk.exponativebuildfixture" }, - "plugins": ["expo-secure-store", "@clerk/expo", "expo-web-browser"] + "plugins": ["expo-secure-store", "@clerk/expo", "expo-web-browser", "./plugins/withClerkOkHttpAlignment.js"] } } diff --git a/integration/templates/expo-native/plugins/withClerkOkHttpAlignment.js b/integration/templates/expo-native/plugins/withClerkOkHttpAlignment.js new file mode 100644 index 00000000000..52337e8c960 --- /dev/null +++ b/integration/templates/expo-native/plugins/withClerkOkHttpAlignment.js @@ -0,0 +1,26 @@ +const { withAppBuildGradle } = require('expo/config-plugins'); + +// clerk-android is compiled against OkHttp 5 but React Native pins OkHttp 4, +// leaving a mixed classpath: clerk networking breaks on the 4.x core +// (NoClassDefFoundError: okhttp3.internal.UnreadableResponseBody), while RN's +// cookie jar breaks on the 5.x core (okhttp3.internal.Util). Force the 5.x +// core plus the urlconnection shim that still ships a JavaNetCookieJar class +// compatible with it, so both stacks run on one coherent OkHttp. +const GRADLE_BLOCK = ` +configurations.all { + resolutionStrategy { + force 'com.squareup.okhttp3:okhttp:5.4.0' + force 'com.squareup.okhttp3:logging-interceptor:5.4.0' + force 'com.squareup.okhttp3:okhttp-urlconnection:5.0.0-alpha.16' + } +} +`; + +module.exports = function withClerkOkHttpAlignment(config) { + return withAppBuildGradle(config, cfg => { + if (!cfg.modResults.contents.includes('com.squareup.okhttp3:okhttp:5.4.0')) { + cfg.modResults.contents += GRADLE_BLOCK; + } + return cfg; + }); +}; diff --git a/integration/tests/expo-native/config.yaml b/integration/tests/expo-native/config.yaml index e307bf2630b..94e3659b694 100644 --- a/integration/tests/expo-native/config.yaml +++ b/integration/tests/expo-native/config.yaml @@ -1,8 +1,10 @@ # Maestro workspace config: https://docs.maestro.dev/reference/workspace-configuration # Top-level files under flows/ are cross-platform tests; flows/subflows/ holds # runFlow-only pieces that discovery never runs standalone. Selectors use -# English text/labels: clerk-android ships no testTag/resource-ids, and both -# native SDKs localize all strings, so devices must run the en locale. +# English text/labels: clerk-android ships no testTag/resource-ids (clerk-ios +# does expose accessibility identifiers like clerk.auth.start.identifier, but +# shared flows can't rely on iOS-only ids), and both native SDKs localize all +# strings, so devices must run the en locale. appId: com.clerk.exponativebuildfixture flows: - 'flows/*.yaml' diff --git a/integration/tests/expo-native/flows/embedded-profile-host-back.yaml b/integration/tests/expo-native/flows/embedded-profile-host-back.yaml index 19d495bcbd3..360a99e8ed7 100644 --- a/integration/tests/expo-native/flows/embedded-profile-host-back.yaml +++ b/integration/tests/expo-native/flows/embedded-profile-host-back.yaml @@ -10,21 +10,44 @@ name: Embedded UserProfileView host back round trip - runFlow: subflows/assert-signed-in.yaml - tapOn: id: 'open-embedded-profile-button' -- extendedWaitUntil: - visible: 'Security' - timeout: 20000 -- tapOn: - text: 'Security' -- extendedWaitUntil: - visible: 'Password|Passkeys|Two-step verification|Active devices' - timeout: 15000 -# Both back buttons read 'Back' (internal id 'BackButton', host root id -# 'chevron.backward') but only one exists at a time: this tap pops Clerk's -# internal stack, the next one is the host chevron firing onHostBack. -- tapOn: 'Back' -- extendedWaitUntil: - visible: 'Security' - timeout: 15000 +# The internal push differs per platform: clerk-ios pushes a Security screen +# from the profile root, clerk-android uses tabs at the root and pushes the +# Manage account screen instead. The double-Back contract below is the same: +# both back buttons read 'Back' but only one exists at a time, so the first +# tap pops Clerk's internal stack and the second is the host chevron firing +# onHostBack. +- runFlow: + when: + platform: iOS + commands: + - extendedWaitUntil: + visible: 'Security' + timeout: 20000 + - tapOn: + text: 'Security' + - extendedWaitUntil: + visible: 'Password|Passkeys|Two-step verification|Active devices' + timeout: 15000 + - tapOn: 'Back' + - extendedWaitUntil: + visible: 'Security' + timeout: 15000 +- runFlow: + when: + platform: Android + commands: + - extendedWaitUntil: + visible: 'Edit profile' + timeout: 20000 + - tapOn: + text: 'Manage account' + - extendedWaitUntil: + visible: 'EMAIL ADDRESSES|Add email address' + timeout: 15000 + - tapOn: 'Back' + - extendedWaitUntil: + visible: 'Edit profile' + timeout: 15000 - tapOn: 'Back' - extendedWaitUntil: visible: diff --git a/integration/tests/expo-native/flows/subflows/sign-in-email-password.yaml b/integration/tests/expo-native/flows/subflows/sign-in-email-password.yaml index 469d946b65f..69df6b2991c 100644 --- a/integration/tests/expo-native/flows/subflows/sign-in-email-password.yaml +++ b/integration/tests/expo-native/flows/subflows/sign-in-email-password.yaml @@ -12,10 +12,12 @@ appId: com.clerk.exponativebuildfixture visible: 'Enter your email( or username)?' timeout: 25000 # Clear any pre-populated identifier (Clerk persists the last-used one in -# secure-store, surviving clearState) via Select-all when the edit menu -# shows, falling back to eraseText when it doesn't (the menu is unreliable -# on a remounted AuthView on iOS). The retry gates on reaching the password -# step, so a mangled identifier gets a clean second attempt. +# secure-store, surviving clearState). On iOS eraseText is cursor-dependent, +# so Select-all via the edit menu when it shows (the menu is unreliable on a +# remounted AuthView). On Android eraseText clears the whole field, and the +# long-press can open Gboard's features panel over the screen instead of the +# edit menu, so skip the menu dance there. The retry gates on reaching the +# password step, so a mangled identifier gets a clean second attempt. - retry: maxRetries: 2 commands: @@ -23,17 +25,36 @@ appId: com.clerk.exponativebuildfixture text: 'Enter your email( or username)?' - waitForAnimationToEnd: timeout: 2000 - - longPressOn: - text: 'Enter your email( or username)?' - # iOS shows "Select All", Android shows "Select all". + # A remounted AuthView prefills the last-used identifier, which is the + # same email this flow signs in with, and clearing a prefilled field is + # cursor-position dependent on Android. Only clear and retype when the + # field does not already hold the right value. - runFlow: when: - visible: 'Select [Aa]ll' + notVisible: ${CLERK_TEST_EMAIL} commands: - - tapOn: - text: 'Select [Aa]ll' - - eraseText: 100 - - inputText: ${CLERK_TEST_EMAIL} + - runFlow: + when: + platform: iOS + commands: + - longPressOn: + text: 'Enter your email( or username)?' + - runFlow: + when: + visible: 'Select All' + commands: + - tapOn: + text: 'Select All' + - eraseText: 100 + - inputText: ${CLERK_TEST_EMAIL} + # Wait for the typed value to land before tapping: the tap can race the + # recomposition that enables the button, and this also catches a mangled + # identifier inside the retry instead of at the next screen's timeout. + - extendedWaitUntil: + visible: ${CLERK_TEST_EMAIL} + timeout: 5000 + - waitForAnimationToEnd: + timeout: 2000 - tapOn: text: 'Continue' index: 0 @@ -60,6 +81,10 @@ appId: com.clerk.exponativebuildfixture text: 'Enter your password' - eraseText: 50 - inputText: ${CLERK_TEST_PASSWORD} + # The password is masked so its value can't be asserted; settle for the + # screen going stable so the tap doesn't race the enabling recomposition. + - waitForAnimationToEnd: + timeout: 2000 - tapOn: text: 'Continue' index: 0 diff --git a/integration/tests/expo-native/run-flows.sh b/integration/tests/expo-native/run-flows.sh index e5a83f4bd4a..b3eb7b7f64c 100755 --- a/integration/tests/expo-native/run-flows.sh +++ b/integration/tests/expo-native/run-flows.sh @@ -15,8 +15,11 @@ set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" -command -v maestro-runner >/dev/null 2>&1 || { - echo "maestro-runner is required: https://devicelab.dev/open-source/maestro-runner/docs/getting-started" +# Runs the official maestro CLI. maestro-runner was tried and reverted: its +# drivers mangle typed text and resolve text selectors by substring, so +# tapOn 'Continue' hits the 'Continue to ' title instead of the button. +command -v maestro >/dev/null 2>&1 || { + echo "maestro is required: https://docs.maestro.dev" exit 1 } @@ -30,10 +33,9 @@ run_flow() { shift local output_root=${MAESTRO_DEBUG_OUTPUT:-${TMPDIR:-/tmp}/clerk-expo-maestro-runner} - maestro-runner test \ - --output "$output_root/$output_name" \ - --flatten \ - --artifacts on-failure \ + maestro test \ + --debug-output "$output_root/$output_name" \ + --flatten-debug-output \ "$@" } @@ -90,8 +92,8 @@ for flow in flows/*.yaml; do flow_result=failed for attempt in 1 2; do if run_flow "${flow##*/}-attempt-$attempt" \ - --env CLERK_TEST_EMAIL="$CLERK_TEST_EMAIL" \ - --env CLERK_TEST_PASSWORD="$CLERK_TEST_PASSWORD" \ + -e CLERK_TEST_EMAIL="$CLERK_TEST_EMAIL" \ + -e CLERK_TEST_PASSWORD="$CLERK_TEST_PASSWORD" \ "$flow"; then flow_result=passed break