Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/expo-native-e2e-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
42 changes: 16 additions & 26 deletions .github/workflows/expo-native-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand All @@ -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:
Expand All @@ -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
2 changes: 1 addition & 1 deletion integration/templates/expo-native/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}

@wobsoriano wobsoriano Jul 31, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clerk-android is built against OkHttp 5, while React Native pins apps to OkHttp 4. That mismatch causes the native instance config fetch to fail at runtime, so sign-in never loads.

This CI fixture forces OkHttp 5, along with the last okhttp-urlconnection alpha that still ships JavaNetCookieJar, to match clerk-android. This is a known ecosystem issue. Expo hit the same problem (expo/expo#44848), and the same workaround is documented on StackOverflow: https://stackoverflow.com/questions/72885577/how-to-implement-okhttp-5-0-0-in-react-native-module

This only affects the CI fixture. Consumer apps are unchanged.

Original file line number Diff line number Diff line change
@@ -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;
});
};
6 changes: 4 additions & 2 deletions integration/tests/expo-native/config.yaml
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
53 changes: 38 additions & 15 deletions integration/tests/expo-native/flows/embedded-profile-host-back.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,49 @@ 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:
- tapOn:
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
Comment on lines 32 to +55

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target workflow ---'
cat -n integration/tests/expo-native/flows/subflows/sign-in-email-password.yaml | sed -n '1,110p'
printf '%s\n' '--- email generation and related selectors ---'
rg -n -C 3 'CLERK_TEST_EMAIL|sign-in-email-password|Select All|extendedWaitUntil' integration/tests/expo-native

Repository: clerk/javascript

Length of output: 29379


🌐 Web query:

Maestro selectors text regular expressions exact text plus sign escape

💡 Result:

In the Maestro automation framework, all text and id selectors are treated as regular expressions by default [1][2]. Because the plus sign (+) is a reserved control character in regular expressions—signifying "one or more" of the preceding element—it must be escaped when you intend to match it as a literal character [1][2]. To match a literal plus sign, you should escape it using a backslash (\) within your regex string [3][4]. For example, to match the literal text "+33", use: - tapOn: "\+33.*" [3] When working with these selectors, keep the following in mind: 1. Full String Matching: Because Maestro treats these selectors as regex, the pattern must match the entire text of the element [2]. To perform a partial match, ensure you include wildcards like .* at the beginning and end of your string as needed (e.g., .*\\+33.*) [2]. 2. YAML and Escape Sequences: If you encounter issues with YAML parsing while using backslashes, you may need to use double backslashes (\\) in your string or use block scalars (e.g., >-) to handle special characters more reliably [3][4]. 3. Alternative: If a specific symbol or character sequence is causing consistent issues with regex matching, a common workaround is to use the wildcard .* to bypass the problematic character, provided the surrounding text is unique enough to identify the element [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- all CLERK_TEST_EMAIL references ---'
rg -n -C 2 'CLERK_TEST_EMAIL|clerk_test' . --glob '!node_modules' --glob '!dist' --glob '!build'
printf '%s\n' '--- Maestro configuration and environment handling ---'
rg -n -C 3 'maestro|--env|test email|TEST_EMAIL' integration/tests/expo-native .github package.json pnpm-workspace.yaml 2>/dev/null || true

Repository: clerk/javascript

Length of output: 38062


🏁 Script executed (no clone):

#!/bin/bash
set -e
python3 - <<'PY'
import re

for email in ("user+clerk_test@example.com", "user+clerk_test@sub.example.com"):
    pattern = re.compile(email)
    print(email, "fullmatch=", bool(pattern.fullmatch(email)), "search=", bool(pattern.search(email)))
PY

Length of output: 247


Escape CLERK_TEST_EMAIL before using it in Maestro selectors.

The +clerk_test suffix and other regex metacharacters in the email make the raw selector fail. Use an escaped selector value for notVisible and extendedWaitUntil, while retaining CLERK_TEST_EMAIL for inputText.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integration/tests/expo-native/flows/subflows/sign-in-email-password.yaml`
around lines 32 - 55, Escape CLERK_TEST_EMAIL when assigning it to the Maestro
selector expressions used by notVisible and extendedWaitUntil, so regex
metacharacters in the email are matched literally. Keep the original
CLERK_TEST_EMAIL value unchanged for inputText.

- waitForAnimationToEnd:
timeout: 2000
- tapOn:
text: 'Continue'
index: 0
Expand All @@ -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
Expand Down
18 changes: 10 additions & 8 deletions integration/tests/expo-native/run-flows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <app>' title instead of the button.
command -v maestro >/dev/null 2>&1 || {
echo "maestro is required: https://docs.maestro.dev"
exit 1
}

Expand All @@ -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 \
"$@"
}

Expand Down Expand Up @@ -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
Expand Down
Loading