Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/expo-native-e2e-agent-device.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/expo': patch
---

Fix the Android `<AuthView>` rendering no sign-in form when it is opened within a couple of seconds of `isLoaded` turning true. The native view now recreates itself once the Clerk Android SDK finishes loading instead of staying empty until it is dismissed and reopened.
96 changes: 56 additions & 40 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_VERSION: '2.8.0'
AGENT_DEVICE_VERSION: '0.20.10'

jobs:
native-build:
Expand Down Expand Up @@ -137,7 +137,7 @@ jobs:
GOOGLE_SIGNIN_TARBALL="$(ls "$SDK_PACK_DIR"/clerk-expo-google-signin-*.tgz)"
pnpm add "$SDK_TARBALL" "$GOOGLE_SIGNIN_TARBALL" -w
# expo-dev-client makes even release builds boot into the dev
# launcher (unreachable Metro in CI), which stalls every Maestro
# launcher (unreachable Metro in CI), which stalls every e2e
# flow on a blank screen. Skip it on e2e jobs only.
DEV_CLIENT="expo-dev-client"
if [ "$RUN_E2E" = "true" ]; then DEV_CLIENT=""; fi
Expand Down Expand Up @@ -187,6 +187,15 @@ jobs:
distribution: temurin
java-version: 17

- name: Set up Gradle cache
if: matrix.platform == 'android' && steps.native-build-cache.outputs.cache-hit != 'true'
uses: gradle/actions/setup-gradle@4733eaac7c1b0da527e4206b7671e0061de1ce37 # v6.3.0
with:
# This workflow never runs on main, so the default (write from main
# only) would never seed the cache.
cache-read-only: false
add-job-summary: on-failure

- name: Prebuild Android fixture
if: matrix.platform == 'android' && steps.native-build-cache.outputs.cache-hit != 'true'
working-directory: ${{ env.FIXTURE_DIR }}
Expand Down Expand Up @@ -214,32 +223,32 @@ jobs:
path: ${{ steps.native-build-key.outputs.artifact }}
key: ${{ steps.native-build-cache.outputs.cache-primary-key }}

- name: Cache maestro CLI
# setup-node above is skipped on a native-build cache hit.
- name: Set up Node for agent-device
if: steps.keys.outputs.pk != ''
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.15.0

- name: Resolve Xcode version
id: xcode
if: matrix.platform == 'ios' && steps.keys.outputs.pk != ''
run: echo "version=$(xcodebuild -version | head -1 | tr ' ' '-')" >> "$GITHUB_OUTPUT"

# Exact key on purpose: agent-device advises against restore-key
# fallbacks for runner artifacts; prepare ios-runner recovers a stale one.
- name: Cache agent-device Apple runner
if: matrix.platform == 'ios' && steps.keys.outputs.pk != ''
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.maestro
key: maestro-${{ runner.os }}-${{ env.MAESTRO_VERSION }}
path: ~/.agent-device/apple-runner/derived
key: agent-device-runner-${{ runner.os }}-${{ env.AGENT_DEVICE_VERSION }}-${{ steps.xcode.outputs.version }}

- name: Install maestro CLI
- name: Install agent-device CLI
if: steps.keys.outputs.pk != ''
run: |
set -o pipefail
if [ -x "$HOME/.maestro/bin/maestro" ]; then
echo "Using cached Maestro"
else
installed=0
for i in 1 2 3; do
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 install attempt $i failed (or binary missing); retrying"
sleep 5
done
[ "$installed" = 1 ] || { echo "::error::Maestro install failed after 3 attempts"; exit 1; }
fi
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
"$HOME/.maestro/bin/maestro" --version
npm install -g "agent-device@$AGENT_DEVICE_VERSION"
agent-device --version

- name: Boot iOS simulator
id: sim
Expand Down Expand Up @@ -277,7 +286,7 @@ jobs:
echo "user_id=$user_id"
} >> "$GITHUB_OUTPUT"

- name: Run iOS e2e (Maestro)
- name: Run iOS e2e (agent-device)
id: run_e2e_ios
if: matrix.platform == 'ios' && steps.user.outputs.user_id != ''
working-directory: ${{ env.FIXTURE_DIR }}
Expand All @@ -295,18 +304,25 @@ jobs:
xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.AutoCapitalization -bool NO || true
xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.AutoCorrection -bool NO || true
xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.Prediction -bool NO || true
# The one-time keyboard tutorial sheets carry their own Continue
# button, which makes the AuthView's Continue ambiguous.
for key in DidShowContinuousPathIntroduction DidShowGestureKeyboardIntroduction KeyboardDidShowProductivityTutorial UIKeyboardDidShowInternationalInfoIntroduction; do
xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.preferences "$key" -bool YES || true
done
xcrun simctl install "$SIM_UDID" ios/build/Build/Products/Release-iphonesimulator/ClerkExpoNativeBuildFixture.app
# Stream the app's console output into the debug artifact so a hang has
# actionable evidence (keychain/network errors) instead of just screenshots.
mkdir -p "$RUNNER_TEMP/maestro-debug"
mkdir -p "$RUNNER_TEMP/agent-device-debug"
xcrun simctl spawn "$SIM_UDID" log stream --style compact \
--predicate 'processImagePath CONTAINS "ClerkExpoNativeBuildFixture"' \
> "$RUNNER_TEMP/maestro-debug/sim-console.log" 2>&1 &
> "$RUNNER_TEMP/agent-device-debug/sim-console.log" 2>&1 &
LOG_PID=$!
cd "$GITHUB_WORKSPACE/$E2E_DIR"
# On a cached build no xcodebuild has run, so without this the first
# snapshot pays the cold XCTest runner build and times out.
agent-device prepare ios-runner --platform ios --udid "$SIM_UDID" --timeout 600000
rc=0
MAESTRO_DEBUG_OUTPUT="$RUNNER_TEMP/maestro-debug" ./run-flows.sh \
xcrun simctl terminate "$SIM_UDID" com.clerk.exponativebuildfixture || rc=$?
E2E_DEBUG_OUTPUT="$RUNNER_TEMP/agent-device-debug" ./run-flows.sh ios || rc=$?
kill "$LOG_PID" 2>/dev/null || true
exit "$rc"

Expand Down Expand Up @@ -339,7 +355,7 @@ jobs:
disable-animations: false
script: echo "Generated AVD snapshot for caching."

- name: Run Android e2e (Maestro)
- name: Run Android e2e (agent-device)
id: run_e2e_android
if: matrix.platform == 'android' && steps.user.outputs.user_id != ''
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2
Expand All @@ -357,7 +373,7 @@ jobs:
# scalar (>-) plus && keeps everything in one shell invocation.
script: >-
cd integration/tests/expo-native &&
MAESTRO_DEBUG_OUTPUT="$RUNNER_TEMP/maestro-debug" ./run-android-flows.sh
E2E_DEBUG_OUTPUT="$RUNNER_TEMP/agent-device-debug" ./run-android-flows.sh
"$GITHUB_WORKSPACE/$FIXTURE_DIR/android/app/build/outputs/apk/release/app-release.apk"

# Runs before the artifact upload so the credentials are already dead
Expand All @@ -371,26 +387,26 @@ jobs:
curl -fsS -X DELETE "$BAPI_URL/v1/users/$USER_ID" \
-H "Authorization: Bearer $CLERK_SECRET_KEY" || true

# 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 debug output
# Session diagnostics record typed input in plaintext; add-mask only
# covers step logs, not artifact contents.
- name: Scrub test credentials from agent-device debug output
if: always() && (steps.run_e2e_ios.outcome == 'failure' || steps.run_e2e_android.outcome == 'failure')
env:
CLERK_TEST_PASSWORD: ${{ steps.user.outputs.password }}
run: |
[ -d "$RUNNER_TEMP/maestro-debug" ] || exit 0
[ -d "$RUNNER_TEMP/agent-device-debug" ] || exit 0
[ -n "$CLERK_TEST_PASSWORD" ] || exit 0
# shellcheck disable=SC2016
find "$RUNNER_TEMP/maestro-debug" -type f \
\( -name '*.html' -o -name '*.json' -o -name '*.log' -o -name '*.txt' -o -name '*.xml' -o -name '*.yaml' \) \
find "$RUNNER_TEMP/agent-device-debug" -type f \
\( -name '*.html' -o -name '*.json' -o -name '*.ndjson' -o -name '*.log' -o -name '*.txt' -o -name '*.xml' -o -name '*.yaml' -o -name '*.ad' \) \
-exec perl -pi -e 's/\Q$ENV{CLERK_TEST_PASSWORD}\E/[REDACTED]/g' {} +

- name: Upload Maestro artifacts on e2e failure
- name: Upload agent-device 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:
name: maestro-${{ matrix.platform }}
path: ${{ runner.temp }}/maestro-debug
name: agent-device-${{ matrix.platform }}
path: ${{ runner.temp }}/agent-device-debug
retention-days: 7

- name: Report e2e outcome
Expand All @@ -401,7 +417,7 @@ jobs:
run: |
outcome="$IOS_OUTCOME"
[ "$outcome" = "skipped" ] && outcome="$ANDROID_OUTCOME"
echo "## Maestro e2e (${{ matrix.platform }}): $outcome" >> "$GITHUB_STEP_SUMMARY"
echo "## agent-device e2e (${{ matrix.platform }}): $outcome" >> "$GITHUB_STEP_SUMMARY"
if [ "$outcome" = "failure" ]; then
echo "::error::Maestro e2e failed. See the maestro-${{ matrix.platform }} artifact."
echo "::error::agent-device e2e failed. See the agent-device-${{ matrix.platform }} artifact."
fi
40 changes: 40 additions & 0 deletions integration/tests/expo-native/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Expo native E2E

Drives the `integration/templates/expo-native` fixture on an iOS simulator and
an Android emulator with [agent-device](https://github.com/callstack/agent-device).

- `flows/*.sh` are the cross-platform tests, one per scenario.
- `flows/subflows/*.sh` are `run_flow`-only pieces shared by the tests.
- `lib.sh` is the verb table. Each function maps one step (`tap_on_id`,
`wait_visible_text`, `input_text`, ...) to one agent-device command, so a
flow reads as a list of steps and platform differences live in
`is_platform` branches.
- `run-flows.sh` runs a warmup launch, then every flow in its own session with
one clean-state retry, and writes a timing table to the job summary.
`run-android-flows.sh` installs the APK and captures logcat first.

## Selectors

Selectors use English text and labels because clerk-android ships no test
tags and both native SDKs localize every string, so devices must run the `en`
locale. clerk-ios accessibility identifiers such as `clerk.dismissButton` only
appear inside iOS-only branches. `text=` and `label=` match exactly, so
tapping `Continue` never hits the `Continue to <app>` title. Fixture testIDs
(`auth-state`, `open-auth-view-button`, ...) are matched with `id=`.

Clean state is `settings clear-app-state` plus, on iOS, a simulator keychain
reset, because clerk-ios keeps device state in the keychain and agent-device
only clears the data container.

## Run locally

Build the fixture the way the workflow does and install it on a booted
simulator or emulator, then:

export CLERK_TEST_EMAIL=... CLERK_TEST_PASSWORD=...
SIM_UDID=<udid> ./run-flows.sh ios
./run-android-flows.sh <path-to-app-release.apk>

`AGENT_DEVICE=<path>` points at a specific binary. `E2E_DEBUG_OUTPUT=<dir>`
keeps a step log per attempt plus a failure screenshot and the agent-device
session state for attempts that failed.
15 changes: 0 additions & 15 deletions integration/tests/expo-native/config.yaml

This file was deleted.

26 changes: 26 additions & 0 deletions integration/tests/expo-native/flows/authview-detach-reattach.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Dismiss the native AuthView mid-flow and reopen it: the native view must
# survive detach/reattach and still complete a sign-in afterwards. Dismissal
# is the platform-native gesture: Android's system back button (doubling as
# the back-dismisses-AuthView regression), iOS's sheet Close control
# (clerk-ios exposes the stable clerk.dismissButton identifier).
# Also asserts the custom `logo` React element is rehosted into the native
# logo slot with a nonzero size, both on first mount and after reattach.
run_flow subflows/open-app.sh
tap_on_id open-auth-view-button
wait_visible_text 25000 "Welcome! Sign in to continue" "Welcome! Sign in to continue."
# Retrying wait (not assert_visible): the rehosted logo is sized on the first
# RN layout pass, so it can lag the welcome copy by a frame or two.
wait_visible_text 10000 "E2E Custom Logo"
if is_platform android; then
back_system
fi
if is_platform ios; then
tap_on_id clerk.dismissButton
fi
wait_visible_id open-auth-view-button 15000
tap_on_id open-auth-view-button
wait_visible_text 15000 "E2E Custom Logo"
run_flow subflows/sign-in-email-password.sh
run_flow subflows/assert-signed-in.sh
tap_on_id sign-out-button
run_flow subflows/assert-signed-out.sh
46 changes: 0 additions & 46 deletions integration/tests/expo-native/flows/authview-detach-reattach.yaml

This file was deleted.

31 changes: 31 additions & 0 deletions integration/tests/expo-native/flows/embedded-profile-host-back.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Embedded UserProfileView (onHostBack): internal navigation stays native,
# and the host-supplied root back button closes the screen from JS.
run_flow subflows/open-app.sh
tap_on_id open-auth-view-button
run_flow subflows/sign-in-email-password.sh
run_flow subflows/assert-signed-in.sh
tap_on_id open-embedded-profile-button
# 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.
if is_platform ios; then
wait_visible_text 20000 "Security"
tap_on_text "Security"
wait_visible_text 15000 "Password" "Passkeys" "Two-step verification" "Active devices"
tap_on_button_text "Back"
wait_visible_text 15000 "Security"
fi
if is_platform android; then
wait_visible_text 20000 "Edit profile"
tap_on_text "Manage account"
wait_visible_text 15000 "EMAIL ADDRESSES" "Add email address"
tap_on_button_text "Back"
wait_visible_text 15000 "Edit profile"
fi
tap_on_button_text "Back"
wait_visible_id open-embedded-profile-button 15000
tap_on_id sign-out-button
run_flow subflows/assert-signed-out.sh
Loading
Loading