Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
53dc694
test(expo): add comprehensive test coverage for native components
chriscanin Apr 15, 2026
fe9e3fe
test(integration-mobile): fix Maestro flows after real-device validation
chriscanin Apr 16, 2026
532dcdf
test(integration-mobile): iOS UserProfile selectors + skip unready flows
chriscanin Apr 16, 2026
034c3f3
test(integration-mobile): fix cold-launch flow for post-signin state
chriscanin Apr 16, 2026
6250faf
test(integration-mobile): Android fixes + dark-mode-applied skip
chriscanin Apr 16, 2026
4b7c966
ci(mobile-e2e): wire INTEGRATION_INSTANCE_KEYS + per-run BAPI test user
chriscanin Apr 30, 2026
3848bcd
Merge branch 'main' into chris/native-component-tests
chriscanin May 6, 2026
808601a
Merge remote-tracking branch 'origin/main' into chris/native-componen…
chriscanin May 7, 2026
7887add
test(expo): fix unit tests and lint after merge with main
chriscanin May 7, 2026
989630d
Merge branch 'main' into chris/native-component-tests
chriscanin May 7, 2026
e4ec3ef
ci(e2e): list available keys when resolve-instance-keys can't find th…
chriscanin May 7, 2026
0af5318
ci(e2e): use staging BAPI for mobile-e2e BAPI calls
chriscanin May 8, 2026
555daa6
ci(e2e): print BAPI response body when user creation fails
chriscanin May 8, 2026
b2f4fcc
ci(e2e): include username in BAPI user creation payload
chriscanin May 8, 2026
e562c92
ci(e2e): override quickstart's verdaccio .npmrc with public registry
chriscanin May 8, 2026
7531501
ci(e2e): swap @clerk/expo to locally-built package before install
chriscanin May 8, 2026
b9d7360
ci(e2e): pnpm install --ignore-workspace so quickstart actually installs
chriscanin May 8, 2026
230b55b
ci(e2e): pnpm pack @clerk/expo so workspace deps resolve outside work…
chriscanin May 8, 2026
73fe7f6
ci(e2e): stub missing splash/adaptive-icon assets before prebuild
chriscanin May 8, 2026
4c086e2
ci(e2e): target an iOS simulator for expo run:ios so CI doesn't need …
chriscanin May 8, 2026
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
6 changes: 6 additions & 0 deletions .changeset/expo-native-component-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/expo': patch
---

- Export `NativeSessionSync` and `app.plugin.js` sub-plugins to enable unit testing (internal, no public API change).
- Add JUnit/Robolectric/MockK test dependencies to the Android module for native unit tests.
107 changes: 98 additions & 9 deletions .github/workflows/mobile-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ on:

env:
EXPO_INSTANCE_NAME: clerkstage-with-native-components
# Override the quickstart's checked-in .npmrc, which points pnpm/npm/npx at a
# local verdaccio registry (http://localhost:4873) that doesn't exist on CI.
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/

concurrency:
group: mobile-e2e-${{ github.ref }}
Expand Down Expand Up @@ -62,9 +65,39 @@ jobs:
- name: Build @clerk/expo
run: pnpm turbo build --filter=@clerk/expo...

- name: Pack @clerk/expo
# `pnpm pack` resolves workspace:^ deps to real versions in the
# packed tarball, which is what we need so the quickstart (outside
# the workspace) can install it.
run: pnpm --filter @clerk/expo pack --pack-destination /tmp/clerk-expo-pkg

- name: Point quickstart at packed @clerk/expo
working-directory: clerk-expo-quickstart/NativeComponentQuickstart
run: |
# The quickstart's main pins @clerk/expo to a local verdaccio
# snapshot version which doesn't exist on public npm. Swap it for
# the tarball we just packed.
tarball=$(ls /tmp/clerk-expo-pkg/clerk-expo-*.tgz | head -1)
jq --arg t "file:$tarball" '.dependencies["@clerk/expo"] = $t' package.json > package.json.tmp
mv package.json.tmp package.json

- name: Install quickstart deps
working-directory: clerk-expo-quickstart/NativeComponentQuickstart
run: pnpm install
# --ignore-workspace because the quickstart dir is nested inside the
# javascript checkout; without this, pnpm walks up and treats the
# outer monorepo as the workspace and skips the quickstart entirely.
run: pnpm install --ignore-workspace --no-frozen-lockfile

- name: Stub missing image assets
working-directory: clerk-expo-quickstart/NativeComponentQuickstart
# The quickstart's app.json references splash-icon.png and android
# adaptive-icon variants that aren't actually committed. Fill them in
# from icon.png so prebuild's image-asset mods don't fail with ENOENT.
run: |
cd assets/images
for f in splash-icon.png android-icon-foreground.png android-icon-background.png android-icon-monochrome.png; do
[ -f "$f" ] || cp icon.png "$f"
done

- name: Resolve Clerk instance keys
id: keys
Expand All @@ -83,11 +116,18 @@ jobs:
CLERK_SECRET_KEY: ${{ steps.keys.outputs.sk }}
run: |
email="ci-${GITHUB_RUN_ID}-${RANDOM}+clerk_test@clerkcookie.com"
username="ci_${GITHUB_RUN_ID}_${RANDOM}"
password="ClerkCI!$(openssl rand -hex 8)Aa1"
response=$(curl -fsS -X POST https://api.clerk.com/v1/users \
http_code=$(curl -sS -o /tmp/bapi_response.json -w "%{http_code}" -X POST https://api.clerkstage.dev/v1/users \
-H "Authorization: Bearer $CLERK_SECRET_KEY" \
-H "Content-Type: application/json" \
-d "{\"email_address\":[\"$email\"],\"password\":\"$password\"}")
-d "{\"email_address\":[\"$email\"],\"username\":\"$username\",\"password\":\"$password\"}")
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "::error::BAPI user creation failed (HTTP $http_code)"
jq . /tmp/bapi_response.json 2>/dev/null || cat /tmp/bapi_response.json
exit 1
fi
response=$(cat /tmp/bapi_response.json)
user_id=$(echo "$response" | jq -er '.id')
echo "::add-mask::$password"
echo "email=$email" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -137,7 +177,7 @@ jobs:
CLERK_SECRET_KEY: ${{ steps.keys.outputs.sk }}
USER_ID: ${{ steps.user.outputs.user_id }}
run: |
curl -fsS -X DELETE "https://api.clerk.com/v1/users/$USER_ID" \
curl -fsS -X DELETE "https://api.clerkstage.dev/v1/users/$USER_ID" \
-H "Authorization: Bearer $CLERK_SECRET_KEY" || true

ios:
Expand Down Expand Up @@ -167,9 +207,39 @@ jobs:
- name: Build @clerk/expo
run: pnpm turbo build --filter=@clerk/expo...

- name: Pack @clerk/expo
# `pnpm pack` resolves workspace:^ deps to real versions in the
# packed tarball, which is what we need so the quickstart (outside
# the workspace) can install it.
run: pnpm --filter @clerk/expo pack --pack-destination /tmp/clerk-expo-pkg

- name: Point quickstart at packed @clerk/expo
working-directory: clerk-expo-quickstart/NativeComponentQuickstart
run: |
# The quickstart's main pins @clerk/expo to a local verdaccio
# snapshot version which doesn't exist on public npm. Swap it for
# the tarball we just packed.
tarball=$(ls /tmp/clerk-expo-pkg/clerk-expo-*.tgz | head -1)
jq --arg t "file:$tarball" '.dependencies["@clerk/expo"] = $t' package.json > package.json.tmp
mv package.json.tmp package.json

- name: Install quickstart deps
working-directory: clerk-expo-quickstart/NativeComponentQuickstart
run: pnpm install
# --ignore-workspace because the quickstart dir is nested inside the
# javascript checkout; without this, pnpm walks up and treats the
# outer monorepo as the workspace and skips the quickstart entirely.
run: pnpm install --ignore-workspace --no-frozen-lockfile

- name: Stub missing image assets
working-directory: clerk-expo-quickstart/NativeComponentQuickstart
# The quickstart's app.json references splash-icon.png and android
# adaptive-icon variants that aren't actually committed. Fill them in
# from icon.png so prebuild's image-asset mods don't fail with ENOENT.
run: |
cd assets/images
for f in splash-icon.png android-icon-foreground.png android-icon-background.png android-icon-monochrome.png; do
[ -f "$f" ] || cp icon.png "$f"
done

- name: Resolve Clerk instance keys
id: keys
Expand All @@ -188,11 +258,18 @@ jobs:
CLERK_SECRET_KEY: ${{ steps.keys.outputs.sk }}
run: |
email="ci-${GITHUB_RUN_ID}-${RANDOM}+clerk_test@clerkcookie.com"
username="ci_${GITHUB_RUN_ID}_${RANDOM}"
password="ClerkCI!$(openssl rand -hex 8)Aa1"
response=$(curl -fsS -X POST https://api.clerk.com/v1/users \
http_code=$(curl -sS -o /tmp/bapi_response.json -w "%{http_code}" -X POST https://api.clerkstage.dev/v1/users \
-H "Authorization: Bearer $CLERK_SECRET_KEY" \
-H "Content-Type: application/json" \
-d "{\"email_address\":[\"$email\"],\"password\":\"$password\"}")
-d "{\"email_address\":[\"$email\"],\"username\":\"$username\",\"password\":\"$password\"}")
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "::error::BAPI user creation failed (HTTP $http_code)"
jq . /tmp/bapi_response.json 2>/dev/null || cat /tmp/bapi_response.json
exit 1
fi
response=$(cat /tmp/bapi_response.json)
user_id=$(echo "$response" | jq -er '.id')
echo "::add-mask::$password"
echo "email=$email" >> "$GITHUB_OUTPUT"
Expand All @@ -218,7 +295,19 @@ jobs:
run: |
cd clerk-expo-quickstart/NativeComponentQuickstart
npx expo prebuild --clean
npx expo run:ios --configuration Release --no-bundler
# Pick the first available iOS simulator and boot it so expo
# run:ios builds for the simulator (no code-signing certs on CI)
# rather than defaulting to a physical device.
SIM_UDID=$(xcrun simctl list devices --json | jq -r '[.devices | to_entries[] | select(.key | startswith("com.apple.CoreSimulator.SimRuntime.iOS")) | .value[] | select(.isAvailable == true)] | first | .udid')
if [ -z "$SIM_UDID" ] || [ "$SIM_UDID" = "null" ]; then
echo "::error::No available iOS simulator found"
xcrun simctl list devices
exit 1
fi
echo "Using simulator $SIM_UDID"
xcrun simctl boot "$SIM_UDID" 2>/dev/null || true
xcrun simctl bootstatus "$SIM_UDID" -b
npx expo run:ios --device "$SIM_UDID" --configuration Release --no-bundler
cd ../../integration-mobile
# Maestro doesn't auto-recurse into subdirectories; pass each flow explicitly.
find flows -type f -name "*.yaml" ! -path "*/common/*" -print0 | \
Expand All @@ -237,5 +326,5 @@ jobs:
CLERK_SECRET_KEY: ${{ steps.keys.outputs.sk }}
USER_ID: ${{ steps.user.outputs.user_id }}
run: |
curl -fsS -X DELETE "https://api.clerk.com/v1/users/$USER_ID" \
curl -fsS -X DELETE "https://api.clerkstage.dev/v1/users/$USER_ID" \
-H "Authorization: Bearer $CLERK_SECRET_KEY" || true
5 changes: 4 additions & 1 deletion .typedoc/custom-plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ const LINK_REPLACEMENTS = [
['enterprise-account-connection', '/docs/reference/backend/types/backend-enterprise-account-connection'],
['enterprise-connection', '/docs/reference/backend/types/backend-enterprise-connection'],
['enterprise-connection-oauth-config', '/docs/reference/backend/types/backend-enterprise-connection-oauth-config'],
['enterprise-connection-saml-connection', '/docs/reference/backend/types/backend-enterprise-connection-saml-connection'],
[
'enterprise-connection-saml-connection',
'/docs/reference/backend/types/backend-enterprise-connection-saml-connection',
],
['external-account', '/docs/reference/backend/types/backend-external-account'],
['phone-number', '/docs/reference/backend/types/backend-phone-number'],
['saml-account', '/docs/reference/backend/types/backend-saml-account'],
Expand Down
7 changes: 7 additions & 0 deletions integration-mobile/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Local env file — never commit. Use config/.env.example as the template.
config/.env

# Maestro artifacts
*.png
*.mp4
maestro-output/
18 changes: 18 additions & 0 deletions integration-mobile/config/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copy to .env and fill in values from your Clerk dev instance.
# .env is gitignored.

# Clerk publishable key for the test app (development instance)
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here

# Google Sign-In (iOS): the reversed-client-id URL scheme from GoogleService-Info.plist
EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME=com.googleusercontent.apps.your-ios-client-id

# Google Sign-In (Android + iOS): the web client ID
EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID=your-web-client-id.apps.googleusercontent.com

# Test user (must use Clerk's testmode +clerk_test pattern for high-rate-limit access)
CLERK_TEST_EMAIL=tester+clerk_test@example.com
CLERK_TEST_PASSWORD=ClerkTest!2024

# Optional: which simulator/emulator to target by default (Maestro will auto-pick if unset)
# MAESTRO_DEVICE=iPhone 16 Pro
27 changes: 27 additions & 0 deletions integration-mobile/fixtures/test-users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "Test user metadata for the Maestro flows. Real credentials live in config/.env, never in this file.",
"users": [
{
"id": "primary",
"description": "Primary test user. Pre-existing in the Clerk dev instance.",
"emailEnv": "CLERK_TEST_EMAIL",
"passwordEnv": "CLERK_TEST_PASSWORD"
},
{
"id": "secondary",
"description": "Used by sign-out-then-sign-in-different-user flow. Provision separately.",
"emailEnv": "CLERK_TEST_EMAIL_SECONDARY",
"passwordEnv": "CLERK_TEST_PASSWORD_SECONDARY"
},
{
"id": "signup",
"description": "Generated per run with the +clerk_test pattern so verification codes auto-resolve.",
"emailTemplate": "tester+clerk_test_{timestamp}@example.com",
"passwordEnv": "CLERK_TEST_PASSWORD"
}
],
"notes": [
"Use +clerk_test addresses to bypass captcha and get higher rate limits.",
"Document any new test users you add here so future devs know what they're for."
]
}
9 changes: 9 additions & 0 deletions integration-mobile/flows/common/assert-signed-in.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Subflow: assert the user is on the signed-in home screen.
appId: com.clerk.clerkexpoquickstart
---
- assertVisible:
text: "Welcome"
- assertVisible:
text: "Manage Profile"
- assertVisible:
text: "Sign Out"
5 changes: 5 additions & 0 deletions integration-mobile/flows/common/assert-signed-out.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Subflow: assert the user is on the signed-out screen with the AuthView visible.
appId: com.clerk.clerkexpoquickstart
---
- assertVisible:
text: 'Welcome! Sign in to continue\.?'
66 changes: 66 additions & 0 deletions integration-mobile/flows/common/open-app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Subflow: launch the NativeComponentQuickstart app from a clean state.
# This is a dev build, so we must handle the Expo dev launcher (iOS uses
# http://localhost:8081; Android uses http://10.0.2.2:8081) and the
# Expo developer menu overlay that appears on first launch.
appId: com.clerk.clerkexpoquickstart
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# Android Google Password Manager may linger from a previous run.
# Dismiss it before anything else.
- runFlow:
when:
visible: ".*Google Password Manager.*"
commands:
- tapOn:
text: "Not now|Never"
- waitForAnimationToEnd:
timeout: 2000
# Dev launcher: tap whichever dev-server URL is shown (port 8081).
# Maestro's text field is regex-matched, so ".*:8081" matches both
# "http://10.0.2.2:8081" (Android) and "http://localhost:8081" (iOS).
- runFlow:
when:
visible: "Development Build"
commands:
- tapOn:
text: ".*:8081"
- waitForAnimationToEnd:
timeout: 10000
# Dismiss the Expo developer menu if it pops up. Tap the "Close" (X)
# accessibility element at the top-right of the sheet. On iOS the
# accessibility text is "Close" (not the resource-id "xmark"); on Android
# it's "Close" on the view's accessibilityText.
- runFlow:
when:
visible: ".*developer menu.*"
commands:
- tapOn:
text: "Close"
optional: true
- runFlow:
when:
visible: ".*developer menu.*"
commands:
- tapOn:
point: "50%,20%"
- waitForAnimationToEnd:
timeout: 2000
- waitForAnimationToEnd:
timeout: 3000
# If a previous flow left the user signed in (session persists in
# Keychain/SecureStore across clearState), sign out so subsequent flows
# start from the AuthView.
- runFlow:
when:
visible: "Sign Out"
commands:
- tapOn:
text: "Sign Out"
- waitForAnimationToEnd:
timeout: 3000
# Assert the AuthView is visible (signed-out state)
- assertVisible:
text: 'Welcome! Sign in to continue\.?'
34 changes: 34 additions & 0 deletions integration-mobile/flows/common/sign-in-email-password.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Subflow: enter email + password into the native AuthView and submit.
# Requires CLERK_TEST_EMAIL and CLERK_TEST_PASSWORD env vars.
appId: com.clerk.clerkexpoquickstart
---
- assertVisible:
text: 'Welcome! Sign in to continue\.?'
- tapOn:
text: "Enter your email or username"
- eraseText: 50
- inputText: ${CLERK_TEST_EMAIL}
- tapOn:
text: "Continue"
index: 0
- waitForAnimationToEnd:
timeout: 3000
- tapOn:
text: "Enter your password"
- eraseText: 50
- inputText: ${CLERK_TEST_PASSWORD}
- tapOn:
text: "Continue"
index: 0
- waitForAnimationToEnd:
timeout: 5000
# Android Google Password Manager may prompt to save the password after
# sign-in. Dismiss it so assertions on the home screen work.
- runFlow:
when:
visible: ".*Google Password Manager.*"
commands:
- tapOn:
text: "Not now|Never"
- waitForAnimationToEnd:
timeout: 2000
9 changes: 9 additions & 0 deletions integration-mobile/flows/common/sign-out-via-button.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Subflow: tap the Sign Out button on the home screen and wait for AuthView.
appId: com.clerk.clerkexpoquickstart
---
- tapOn:
text: "Sign Out"
- waitForAnimationToEnd:
timeout: 3000
- assertVisible:
text: 'Welcome! Sign in to continue\.?'
16 changes: 16 additions & 0 deletions integration-mobile/flows/common/sign-out-via-profile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Subflow: open the UserProfile via Manage Profile, tap Log out, assert signed out.
appId: com.clerk.clerkexpoquickstart
---
- tapOn:
text: "Manage Profile"
- waitForAnimationToEnd:
timeout: 3000
- assertVisible:
text: "Account"
# iOS renders "Sign out", Android renders "Log out"
- tapOn:
text: "Log out|Sign out"
- waitForAnimationToEnd:
timeout: 3000
- assertVisible:
text: 'Welcome! Sign in to continue\.?'
Loading
Loading