Community examples aren't actively verified by the e2e team so exercise extra caution when running.
The tests in specs/ are an ordinary e2e project. e2e.config.ts and the files under specs/ run with npx e2e run when the environment names three things: a device, a build of E2EHost, and a Clerk development instance.
The CLI in bin/ and src/ sits on top of that project. It makes those three things for a run, and it adds what a pull request needs. The tests never import the CLI. test/seam.test.ts fails when a file under specs/ or e2e.config.ts names a path outside specs/.
To prove a change, use the CLI. .claude/skills/verify-clerk-ios/SKILL.md has the steps. Run a test by hand when you want e2e alone, for example to work on a test with e2e's own flags.
Run a test by hand
You need Node 24.8 or newer on 24, Xcode and a booted iOS simulator, and the publishable key and secret key of a Clerk development instance.
cd e2e-tests
npm ci
xcodebuild build -quiet -workspace ../Clerk.xcworkspace -scheme E2EHost -configuration Debug \
-destination 'generic/platform=iOS Simulator' -derivedDataPath /tmp/e2ehost
xcrun simctl list devices booted
export CLERK_E2E_DEVICE=<the id of a booted simulator, from the list above>
export CLERK_E2E_APP_PATH=/tmp/e2ehost/Build/Products/Debug-iphonesimulator/E2EHost.app
export CLERK_PUBLISHABLE_KEY=pk_test_...
export CLERK_SECRET_KEY=sk_test_...
npx e2e run specs/features/sign-in-email-code/complete.e2e.ts--video on, --retries, --grep, and --output are e2e's own flags. e2e writes its report and its screenshots to .e2e/, which git ignores.
| Setting | Needed | What it is |
|---|---|---|
CLERK_E2E_DEVICE | yes | The id of a booted simulator, as xcrun simctl list devices booted prints it. A name is not accepted. |
CLERK_E2E_APP_PATH | no | A build of E2EHost for the simulator. The first launch of a run installs it. Leave it unset when the app is already on the simulator. |
CLERK_PUBLISHABLE_KEY | yes | The pk_test_ key of a development instance. |
CLERK_SECRET_KEY | yes | The sk_test_ key of the same instance. The tests create users and sign-in tickets with it. A pk_live_ or sk_live_ key is refused. |
The instance
The tests expect the settings in src/core/instances/base.json of @clerk/internal-mobile-e2e, which the CLI puts on the application it creates. On an instance with those settings, every test file that has no settings file beside it passes by hand.
An instance left at Clerk's defaults passes only part of the suite. The tests that sign in with a ticket, or sign up with an email address and a password, pass. The tests that need a phone number, an email code at sign-in, organizations, or a second account on the device fail, because a new instance has none of those turned on.
Every run creates users with addresses like maya.7c1e+clerk_test@example.com and never deletes them. The address is a first name and the last four characters of the run id, here r20261002-141210-7c1e. No two tests of a run on one machine get the same name. A development instance holds 100 users. Use an instance that you can empty.
Test files that need the CLI
These test files have a .settings.json file beside them, and pass only on an instance with those settings:
specs/features/session-tasks/choose-organization.e2e.tsspecs/features/session-tasks/complete-setup-mfa.e2e.tsspecs/features/session-tasks/setup-mfa.e2e.tsspecs/features/session-tasks/sign-up-setup-mfa.e2e.ts
The CLI's run puts the instance on the declared settings before the file starts, and e2e alone does not. Run them through the CLI, for example e2e-tests/bin/control-clerk-ios run session-tasks from the repo root.
What a run by hand does not do
- It creates no Clerk application, changes no instance settings, and replaces no instance that is filling up.
- It leases and locks no device. Run one test process per device at a time.
- It does not rebuild the app when the sources change.
- It records no video of the whole run.
--video onrecords one per attempt. - It keeps no sealed run directory, scans nothing for secrets, redacts no log, and posts nothing to a pull request.
- It leaves the
agent-devicedaemon that e2e started running, and with it the iOS runner process that the daemon keeps.npx agent-device daemon stop --cleanstops both. Without--cleanthe runner stays.
Layout
e2e.config.tsandspecs/are the e2e project.specs/features/has the tests,specs/fixtures.tsconfigures the sharedhostfixture they use, andspecs/app.tsnames the test app. The fixture and its support code come from@clerk/internal-mobile-e2e, whosespecs/support/inputs.tsis the only file that reads the settings above.bin/andsrc/are the iOS side of the CLI. The shared CLI comes from@clerk/internal-mobile-e2e, pinned to a commit inpackage.json. Under the CLI the test process gets the same settings, with one difference: in place of the secret key it gets the address and token of a stand-in that the CLI runs on this machine. The stand-in forwards the three Backend API calls the tests make, for this run's test users only, so the test process never holds the key.test/has the unit tests of the iOS side.npm testruns them, with no device, key, or network.- To change the shared core, follow the README of
@clerk/internal-mobile-e2e, then bump the commit pin here.