Community examples aren't actively verified by the e2e team so exercise extra caution when running.
End-to-end tests for the example app, written in TypeScript and run with
e2e on the @e2e-dev/mobile engine.
Tests drive iOS simulators and Android emulators through the accessibility
tree, so the same file runs on both platforms.
Setup
bun install # from the repository root
cd e2e && npx agent-device doctoragent-device doctor checks for Xcode with a simulator runtime, or the Android
SDK with an emulator.
Run
From the repository root, bundle the JS, build and install the release example app, then run the suite:
bun run e2e:ios
bun run e2e:androidThe release app of react-native-test-app loads the prebuilt bundle from
example/dist, so example/ios/Pods must have been installed after that
directory existed at least once (bun run --cwd example build:ios && bun run bootstrap).
If the example app is already installed on a booted simulator or emulator:
bun run e2e:test:ios
bun run e2e:test:androidFrom this directory, bun run test:ios -- --tag smoke runs only the smoke
test, and bun run list prints every test without running it.
Several simulators at once
With nothing pinned, the engine uses every booted simulator as the pool, so
bun run e2e:test:ios already spreads the suite across whatever is open. To
choose explicitly, list simulators by name or UDID in E2E_DEVICES and point
E2E_IOS_APP_PATH at the build so each one gets the app installed before its
first test:
APP=$(ls -d ~/Library/Developer/Xcode/DerivedData/PagerViewExample-*/Build/Products/Release-iphonesimulator/ReactTestApp.app | head -1)
E2E_DEVICES="iPhone 17,iPhone 16e,iPhone 17 Pro Max,iPhone Air" E2E_IOS_APP_PATH="$APP" bun run e2e:test:iostargets.ts turns the list into a device pool. The engine boots every device
up front, declares one worker per device, and the runner spreads the test
files across them. The suite (13 tests on iOS, 12 on Android) takes about
145 s on one simulator and 395 s on one emulator; a pool divides the files
across its devices. Remote devices fit the same shape: one pool entry per
device.
| Variable | Effect |
|---|---|
E2E_DEVICE | Simulator or emulator name or UDID. Default: every booted device of the platform. |
E2E_DEVICES | Comma-separated pool of names or UDIDs, for example iPhone 17,iPhone 17 Pro. One worker per device; the test files spread across them. Every device needs the app installed, or set E2E_IOS_APP_PATH. |
E2E_IOS_APP_PATH | .app bundle installed on every pooled device before its first test. Use the Xcode build product (~/Library/Developer/Xcode/DerivedData/PagerViewExample-*/Build/Products/Release-iphonesimulator/ReactTestApp.app), never a path inside a simulator's own container: installing replaces that container and the path disappears mid-run. |
E2E_ANDROID_APK_PATH | .apk to install before the first test instead of using the installed build. |
Layout
targets.tsdeclares one mobile engine per platform, both pinned to thecom.pagerviewexampleapp;e2e.config.tsruns the suite with them.tests/*.e2e.tshold one scenario per example screen.tests/issues/*.e2e.tshold regression scenarios, taggedregressionandissue-<number>where a GitHub issue exists.tests/support/test.tsre-exportstestandexpect(import them from there so the engine can change in one place) and exportsfreshAppBeforeEach(), which every test file calls to relaunch the app before each test (installing theE2E_*_APP_PATHbuild first when set).tests/support/app.tsopens an example from the home list and forces the layout direction.tests/support/basic-pager.tsholds the checks shared by the LTR, vertical, and RTL basic pager scenarios.
Gotchas
- Swipe directions are scroll directions:
swipe({ direction: 'right' })moves the finger left and reveals the next page in LTR. - Node swipes fling. Use
screen.scrollUntilVisiblewith a target that stays on screen after any fling (the last item) instead of a middle one. - The layout direction toggle persists across launches.
openExampleforces the direction it is given, and every RTL describe block callsrestoreLtrAfterEach(). - The engine neither installs nor launches anything on its own, so every
test file calls
freshAppBeforeEach()right after its imports: it relaunches the app withdevice.openApp(APP_ID, { relaunch: true }), so every test starts fresh at the home list. A module-level hook intests/support/test.tswould attach only to the first file evaluated in a realm, andapp.open()orapp.restart()resume a running app the surface does not know about. State shared between tests must be state the app persists (the layout direction toggle is). - With
E2E_*_APP_PATHset, the hook runsdevice.installApp()before every relaunch (~2 s each on a simulator) instead of tracking which device already has the build: an install replaces the binary and keeps its data, so repeating it is correct across device pools and realms. - A connected physical iPhone joins the default device pool and fails with
ENGINE_FAILUREwhen it is locked or lacks the app. PinE2E_DEVICEto a simulator when a phone is plugged in.
CI
.github/workflows/ios.yml and android.yml build the release example app,
install it on a simulator or emulator, and run the suite for their platform.
The github() reporter from @e2e-dev/github is in e2e.config.ts; it does
nothing locally and on Actions writes the job summary and one PR comment per
run (the workflows pass GITHUB_TOKEN and hold pull-requests: write).
.e2e/report.json and the artifacts upload on every non-cancelled run.
Debugging
A failed run prints the error code and the failing line. .e2e/report.json
lists every step with its screenshot under .e2e/artifacts/. Both are
gitignored. See .agents/skills/e2e/references/debugging.md for the error
code table.