| type | Reference | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| title | Running e2e tests | ||||||||
| description | The canonical, minimal command set for running React Native Firebase e2e tests on every platform. | ||||||||
| tags |
|
||||||||
| timestamp | 2026-07-26 00:00:00 UTC |
Canonical local e2e commands. Use only these commands. -ci variants are CI-only. Avoid :test-cover-reuse, :test-cover-and-process, :test-reuse (stale native risk). If another doc disagrees, this wins.
All e2e how-to lives here; other docs link here — they do not define alternate entrypoints or commands.
Never invoke the test runner (Jet), Detox, Metro, or emulators directly. Use only the repo-root yarn tests:* commands defined in this document (for example yarn tests:packager:jet, yarn tests:emulator:start, yarn tests:<platform>:test-cover). Do not run jet, npx jet, yarn jet, detox test, cd tests && …, or ad-hoc Metro/emulator start commands. When another doc mentions e2e, Jet, Detox, or pre-flight, follow the link to this runbook — do not infer commands from log output or implementation details.
Install, prepare, and validation commands are not in this doc — they live in agent command policy (read before any non-e2e shell command). Before any native :build: install / patch / fmt gate (root yarn exit 0 + fmt ≥ 12.1.0).
yarn # repo root — exit 0 required. Applies .yarn/patches (jet, mocha-remote-*, detox) and patch-package (incl. tests/patches/react-native+0.78.3.patch → fmt 12.1.0); installs tests devDeps incl. babel-plugin-istanbulBefore yarn tests:ios:build / yarn tests:android:build: install / patch / fmt gate (root yarn exit 0 + fmt ≥ 12.1.0).
- Packager (background):
yarn tests:packager:jet- Emulators (background, always):
yarn tests:emulator:start-
Rebuild when needed
- Before any native
:build: install / patch / fmt gate — rootyarnexit 0 + fmt podspec ≥ 12.1.0. Missing this gate → Apple Clang 21 consteval failures on unpatched fmt 11.0.2. - Native changed →
yarn tests:ios:build/yarn tests:android:buildbefore e2e. macOS uses firebase-js-sdk only — no native rebuild. packages/*/lib/**changed →yarn lerna:preparemust run to completion (exit 0) before anything else — Metro servesdist/module/**, notlib/**. See prepare completion gate and agent command policy § prepare must finish first. After prepare finishes, restart the packager withyarn tests:packager:jet-reset-cachewhen Metro was already running (Rules §1).- TurboModule codegen / spec / podspec / native shell changed → same as native changed, plus regen codegen (workflow § Running codegen) when specs changed; if app loads with Metro redbox
Requiring unknown module "undefined", see TurboModule stale toolchain. - JS bundle (debug): all platforms (iOS, Android, macOS) load JS from Metro; only release builds pre-bundle/embed JS.
lib/**edits alone do not require:build— use the prepare completion gate and Metro restart above. - TS coverage: run
:buildbefore:test-coveron iOS/Android so Istanbul + patched test-runner coverage instrumentation is in the debug native app (bundle still from Metro). After test-runner patch changes, restart the packager withyarn tests:packager:jet-reset-cache(Rules §1).
- Before any native
-
Always run with coverage:
yarn tests:ios:test-cover
yarn tests:android:test-cover
yarn tests:macos:test-coverClean :build + :test-cover each time — not reuse variants.
-
Report locations — Coverage design. Android CI also runs
yarn tests:android:unit(JVM) before Detox; post-e2e produces mergedjacocoTestReport(unit + e2e) — details there, not duplicated here. -
One e2e at a time — never overlap
:test-coverruns on one host. All platforms share Metro:8081and the test-runner WebSocket port (default 8090); parallel runs race on coverage/device/emulator state. Every run starts after clean pre-flight. Log triage for port/orchestration markers: test-runner host orchestration. -
No source edits during e2e — wait/cancel cleanly before editing
packages/**,tests/**, or bundle-affecting OKF docs. Saves can hot reload/rebundle and invalidate tests/coverage.
Use validation tiers: unit-focused, area-focused, full. Match tier to work type. Runs are serial from clean pre-flight. Log long output; upstream gets exit code + short summary.
Policy: OKF documentation and commit policy. Terms: iteration vocabulary.
Internal only — do not invoke sub-commands. Wait on the single repo-root :test-cover command; Detox/Jest and the test runner start automatically.
yarn tests:android:test-cover # only command you run
└─ (internal) detox → jest → firebase.test.js → test runner on :8090 → app
macOS: yarn tests:macos:test-cover only — same :8090 transport, no Detox.
Do not poll pgrep, detox, process names, or :8090 for completion. They match stale wrappers, orphans, zombies, and contention.
No commands to run from this section — for interpreting :test-cover logs and CI artifacts only. Patch workflow: detox-patches.md. CI triage: iOS orchestration.
| Port | Protocol | Role |
|---|---|---|
8090 (default JET_REMOTE_PORT) |
WebSocket (mocha-remote-*) |
App ↔ host test transport; drives Mocha in the app |
8091 (default JET_REMOTE_PORT + 1, override RNFB_JET_CONTROL_PORT) |
HTTP POST only | Host ↔ test-runner control plane — not used by the app |
Why two ports — Port 8090 is a WebSocket server (ws library). Plain HTTP POST to that socket (e.g. /launch-ready) gets 426 Upgrade Required and can crash the runner with ERR_HTTP_HEADERS_SENT if a control handler shares the same HTTP stack. Control endpoints therefore live on a separate small HTTP server (startControlHttpServer in the test-runner patch).
Launch gate (orchestration race fix) — firebase.test.js starts the test runner with RNFB_JET_DEFER_RUN=1. It listens on 8090 and defers server.run() until the host signals launch success:
- On Android, host force-stops both test packages and clears any stray 8090 listener before spawning Jet.
- Host waits for TCP 8090, then Metro (debug) if needed, then
launchAppWithRetry. - Host
POSTs/orchestrate-state({ "phase": "launch-pending" | "launch-ok" | … }) to the control port (best-effort diagnostics). - After
launchAppsucceeds, hostPOSTs/launch-ready→ test runner callsserver.run()and the app may receive the mocha-remoterunaction. - Mocha tests must not start during a stuck or retried
launchApp; on inner launch retry the host may kill and respawn the test runner beforeterminateApp/simulator reboot.
Log markers — [rnfb-e2e] orchestrate-state=…, [jet-control] deferring server.run until POST /launch-ready, [jet-control] launch-ready received, [jet-control] listening on http://…:8091, [jet-coverage] …, Jet client connected.
Pre-flight — Host-clear probes check 8090 only (stray test-runner WS listener). 8091 may be open during a run; do not treat it as a stale-process signal by itself.
GitHub Actions Testing E2E iOS adds CI-only steps local :test-cover does not run: pre-boot (boot-simulator.sh), one filtered sim-app.log stream, wait-for-load-settle.sh (threshold 20) immediately before Detox, and optional video when record_screens: true. Host syslog and unfiltered simulator logs are disabled to reduce runner baseload.
Canonical owner: iOS CI baseload policy. Artifact names and triage: simulator logging and video.
- Pre-flight; if host-clear probes fail, pre-flight recovery first.
- One foreground Shell command; set
block_until_mslarge enough (~15m macOS, ~45–60m iOS/Android). Do not background/poll. - From repo root, tee canonical command:
yarn tests:android:test-cover 2>&1 | tee /tmp/rnfb-e2e-android.log
yarn tests:ios:test-cover 2>&1 | tee /tmp/rnfb-e2e-ios.log
yarn tests:macos:test-cover 2>&1 | tee /tmp/rnfb-e2e-macos.logUse /tmp/rnfb-e2e-<platform>.log (overwrite each iteration). Do not substitute other entrypoints — see agent rule.
- Completion = shell exit code.
0finished; non-zero failed/aborted. Read log for counts. - Parse log tail; do not infer from processes:
rg 'passing|failing' /tmp/rnfb-e2e-<platform>.log | tail -1
rg '^\s+\d+\)' /tmp/rnfb-e2e-<platform>.log # failure blocks, if any
rg 'Tests Complete|jet-coverage.*merged' /tmp/rnfb-e2e-<platform>.log | tail -3Markers: ✨ Tests Complete ✨, Jest N passing / N failing, [jet-coverage] merged … before NYC shutdown, [rnfb-e2e] orchestrate-state=, [jet-control] launch-ready received.
- Return only platform, exit code, pass/fail line, failing tests, log path, optional coverage-gap line. No full log upstream.
Canonical owner for host-clear probes, recovery after abort, and service checks. Other OKF docs link here by reference — do not duplicate commands or probes elsewhere.
Run all four steps before every :test-cover. After an interrupted run, run pre-flight recovery and re-run the probes.
If product code under packages/*/lib/** was edited in this session, yarn lerna:prepare (or scoped yarn lerna run prepare --scope …) must have fully finished with exit code 0 before pre-flight steps 1–3 or any :test-cover / :build.
- Wait for the prepare shell to return — do not batch prepare in parallel with Metro restart, pre-flight probes, or e2e in the same agent turn.
- Then restart Metro when it was already running:
yarn tests:packager:jet-reset-cache(Rules §3). - Then continue with host-clear probes and service checks below.
Skipping this gate causes missing or half-written dist/module/** while Metro /status still returns 200 — a common source of bundle-load and module-not-found failures that look like product bugs.
Owner for install/prepare serialization: agent command policy § prepare must finish first.
No in-flight test run on the target platform:
| Platform | Clear when |
|---|---|
| Android | Android app reset + host-clear probes pass |
| iOS | Host-clear probes pass — zero booted simulators and no stray listener on :8090. Detox boots iPhone 17 from tests/.detoxrc.js; do not pre-boot or leave simulators running. |
| macOS | Host-clear probes pass (no io.invertase.testing process) |
Also wait for any visible unfinished yarn tests:*:test-cover.
Android app reset (blocking) — run before every Android :test-cover, not only after a failed run:
adb -s emulator-5554 shell am force-stop com.invertase.testing
adb -s emulator-5554 shell am force-stop com.invertase.testing.testThe main app (com.invertase.testing) can sit on “waiting for jet to start tests…” while the host-clear probe still passes (it only checks com.invertase.testing.test). A stale main app then connects as a second Jet client after Detox launches a fresh run → Received a message from the client, but server wasn't running and no Mocha summary. Force-stop both packages, then re-run the probe.
On darwin hosts, also clear the macOS test app before Android :test-cover — macOS and Android share Jet :8090. Alternating macOS and Android runs without killing io.invertase.testing leaves a stale macOS Jet client that connects when Android starts → duplicate clients and server wasn't running.
! pgrep -x io.invertase.testing >/dev/null 2>&1(firebase.test.js runs this automatically via ensureAndroidJetHostClear before spawning Android Jet; use the probe manually when prepping from the runbook.)
Host-clear probes — run after Android app reset (Android) or directly (iOS/macOS); exit 0 = clear (chain with &&):
# iOS — booted-device count must be 0
test "$(xcrun simctl list devices booted | grep -c '(Booted)' || true)" -eq 0
test -z "$(lsof -nP -iTCP:8090 -sTCP:LISTEN -t 2>/dev/null || true)"
# Android
! adb -s emulator-5554 shell pidof com.invertase.testing.test >/dev/null 2>&1
# macOS
! pgrep -x io.invertase.testing >/dev/null 2>&1Pre-flight recovery — when probes fail after Android app reset, abort, kill, or EADDRINUSE on :8090. Before any new :test-cover, kill the stray 8090 listener, re-run force-stop (Android) or the iOS/macOS steps below, then host-clear probes.
# Android — force-stop both apps, then clear the Jet WS listener
adb -s emulator-5554 shell am force-stop com.invertase.testing
adb -s emulator-5554 shell am force-stop com.invertase.testing.test
lsof -nP -iTCP:8090 -sTCP:LISTEN -t | xargs kill 2>/dev/null || true
# iOS — Detox re-boots iPhone 17 after shutdown booted
lsof -nP -iTCP:8090 -sTCP:LISTEN -t | xargs kill 2>/dev/null || true
pkill -f 'detox test --configuration ios' 2>/dev/null || true
pkill -f 'jet.js --target=ios' 2>/dev/null || true
xcrun simctl shutdown bootedAfter Android recovery, verify pidof com.invertase.testing.test is empty and :8090 is closed before rerunning :test-cover.
Do not use boot-simulator.sh or simctl shutdown all as routine prep (what not to do).
Metro and emulators must be running and responsive — do not assume from a prior session or background start.
curl -sf http://127.0.0.1:8081/status >/dev/null # Metro (127.0.0.1 matches test app bundle URL)
curl -sf http://127.0.0.1:8080 >/dev/null # Firestore emulator
test -n "$(lsof -nP -iTCP:5001 -sTCP:LISTEN -t 2>/dev/null || true)" # Functions emulator — listener onlyIf Metro or Firestore checks fail: start yarn tests:packager:jet and yarn tests:emulator:start (background); re-check until both pass. After yarn lerna:prepare has finished (step 0) or test-runner patch edits, restart the packager with yarn tests:packager:jet-reset-cache (Rules §1) — never restart Metro while prepare is still running.
A listener on :8081, :8080, or :5001 alone is not sufficient for Metro/Firestore — their HTTP checks must succeed. Functions (:5001): verify the listener is up; curl -sf http://127.0.0.1:5001/ exits non-zero because the root path returns 404 — that is expected and not a service failure (do not treat it like the Metro/Firestore gates).
Confirm local harness overrides (or committed files for full tier only) match the item's validation_tier (iteration vocabulary), not the branch's committed harness alone.
| Tier | Harness before :test-cover |
|---|---|
Unit-focused (implementation) |
Area narrowing required — tests/harness.overrides.js with modules + RNFBDebug: true; .only OK locally. |
Area-focused (independent-review, baseline-capture) |
Area narrowing required — same overrides file; load full spec file(s) for the package area; no .only; RNFBDebug: true. |
Full (pre-merge-validation) |
No harness.overrides.js (delete or {}); full app in committed tests/app.js; RNFBDebug: false. |
Committed full harness on the branch does not override unit-focused or area-focused tier for local runs. Package workflows define which module/spec (e.g. pipelines § area harness). How: local harness overrides. Never commit harness.overrides.js.
See Harness narrowing gate (blocking) — a run that skips step 3 does not close implementation_gate or review_gate.
Completion = shell exit code + log markers — not open-ended log tailing.
| Platform | Early markers (≈2–3 min) | Done |
|---|---|---|
| macOS | Jet client connected |
✨ Tests Complete ✨, Jest N passing |
| iOS/Android | Detox launch done, Jet client connected |
Same |
If stalled — no new markers for 5 minutes, or past tier budget (~15m macOS, ~45–60m iOS/Android) without Tests Complete: treat as interrupted run. Run pre-flight recovery, confirm host-clear probes and services ready, retry. Do not keep watching flat tee output.
- macOS bundle/Metro hangs → ci-workflows/other.md § bundle load hang
- iOS Metro at launch → ci-workflows/ios.md § Metro unresponsive
Do not poll pgrep, process names, or :8090 for completion (above). Stall detection uses missing progress markers, not exit polling.
Both unit-focused and area-focused tiers require area narrowing before the first :test-cover. The only difference between those tiers is whether .only is allowed and whether the full package-area spec loads — not whether the harness stays at full app load.
Primary mechanism: create a local tests/harness.overrides.js (gitignored) from tests/harness.overrides.example.js. Committed tests/app.js and tests/globals.js stay at full harness — do not edit them for module narrowing or RNFBDebug. See local harness overrides.
| Mistake | Symptom | Gate impact |
|---|---|---|
Run :test-cover with no overrides (full harness) during implementation or independent-review |
macOS/iOS/Android pass counts in the hundreds or thousands (all modules via require.context) |
Run is invalid — does not close implementation_gate or review_gate |
Edit only one platform block in tests/app.js (legacy pattern) while the other still pushes full list |
macOS ~700 firestore tests pass; iOS/Android logs show database, crashlytics, etc. |
Run is invalid on iOS/Android — use overrides file instead |
| Correct area harness via overrides | Pass counts match loaded module/spec scope (sanity table) | Expected |
Apply locally before every :test-cover at unit-focused or area-focused tier — even when git shows the full push harness. Remove tests/harness.overrides.js (or export {}) after the run when the branch keeps full harness (typical until phase R). Never commit harness.overrides.js.
Validation report must state: harness narrowed (yes/no), override file used (yes/no), which module/spec loads, whether pass counts match area scope, and which platforms ran with exit codes. A green full-app run is not a substitute.
Both unit-focused (implementation) and area-focused (baseline-capture, independent-review) require e2e on every platform where the changed module loads in the committed harness — not a subset for convenience.
Determine required platforms from committed tests/app.js platform blocks (use committed lists when deciding macOS vs native requirement, not a narrowed local harness):
| Platform class | When required |
|---|---|
macOS (Platform.other) |
Module appears in the committed if (Platform.other) list (or overrides modules includes it) |
| iOS and Android | Module appears in the committed if (!Platform.other) list (or overrides modules includes it) |
Area-focused (baseline-capture, independent-review) — closes review_gate / baseline only when:
- Full loaded package spec(s) with area narrowing (no
.only). - Serial
:test-coveron each required platform above — pre-flight before every run. - Native platforms:
yarn tests:<platform>:buildbefore first:test-coverwhen native changed (Rules §3). - Validation handoff includes a platform matrix: platform, exit code, pass/fail/pending counts, log path.
Invalid shortcuts (do not close gates):
- “macOS + iOS minimum”; skipping Android when the module loads on Android.
- “Skip Android if time tight” or “Android fallback only if iOS failures look env-related” without a fresh Android run.
- Substituting a prior implementer log for
independent-reviewon the frozen tree.
Module-specific skip: only when the module is absent from that platform’s harness list (e.g. messaging is not on macOS). Record in the work-queue Notes — not an oral exception.
Unit-focused (implementation) — native touched: macOS first when the path is TS/web-only; when the module loads on iOS and Android, run both before closing implementation_gate (same narrowing; .only OK locally; never commit).
See also: coverage design § platform parity, validation checklist § handoff.
Checklist (copy before first run):
tests/harness.overrides.jsexists with correctmodules(almost always include'app') andRNFBDebug: true.- Overrides
moduleslists only the package under change (e.g.['app', 'firestore']). - Spec load uses direct
requireof the area spec — notrequire.contextfor all packages — when sub-suite narrowing applies; otherwise full packagerequire.contextis OK when the module list is narrowed. - No
.onlywhen tier is area-focused;.onlyoptional when tier is unit-focused. - Grep log: pass count consistent with area scope (~100 for pipeline-only, ~700 for full firestore package on macOS), not full app (~141+ macOS baseline with full load per work queue).
For implementation work type — validation tier unit-focused (change authoring workflow):
- Pre-flight — prepare completion gate when
lib/**changed, host-clear probes, services ready, harness overrides in place (step 3),RNFBDebug: truevia overrides; if probes fail, pre-flight recovery first. - Edit e2e/spec; add
.onlyif needed; never commit overrides or.only. - macOS first when TS-only:
yarn tests:macos:test-cover 2>&1 | tee /tmp/rnfb-e2e-macos.log— wait for exit code (stalled run if markers stop). - If macOS green and native touched:
yarn tests:<platform>:build && yarn tests:<platform>:test-cover 2>&1 | tee /tmp/rnfb-e2e-<platform>.log; one platform at a time. - Grep log tail → fix → repeat from step 1.
- When
implementation_gatecloses, next work type isindependent-reviewat area-focused tier — frozen tree; no.only; area narrowing per package workflow.
Never overlap runs that use :test-cover. See host rule.
| Rule | Requirement |
|---|---|
| One e2e run at a time | Wait for prior shell exit code + short log summary |
| No overlapping tiers | Never run unit-focused-tier and area-focused-tier :test-cover concurrently on one host |
| Clean pre-flight every run | Pre-flight — host-clear probes, services, harness tier |
| Phase J loop | implementation (Jest + unit-focused) → independent-review (area-focused, frozen tree) → commit — work queue protocol |
Tier scope table: E2e validation tiers.
Each run owns its blocking :test-cover and returns summaries only.
Run pre-flight recovery, confirm host-clear probes pass, then rerun from repo root: yarn tests:<platform>:build && yarn tests:<platform>:test-cover (foreground; tee if logging). Keep one :test-cover active at a time on a host.
- Do not invoke the test runner (Jet), Detox, Metro, or emulators except through repo-root
yarn tests:*commands in this doc — see agent rule. - Do not run
:test-cover,:build, Metro restart, or pre-flight whileyarn/yarn lerna:prepareis still in progress — wait for exit 0 first (prepare completion gate). - Do not run native
:builduntil install / patch / fmt gate is satisfied (fmt ≥ 12.1.0). - Do not background
:test-coverand pollpgrep,detox, or process names for completion. - Do not use
:test-cover-reuse,:test-cover-and-process, or:test-reusewhen measuring coverage or closing review gates. - Do not use
:8090listening as “e2e still running” without the platform active signal above. - Do not start iOS/Android/macOS
:test-coverconcurrently on one host. - Do not edit source while a tee'd run is still in progress.
- Do not passively tail tee output when progress markers stop — follow stalled run detection.
- Do not run full harness (
require.context, all modules) for unit-focused/area-focused tier — match harness to tier. - Do not run
.github/workflows/scripts/boot-simulator.sh,simctl shutdown all, orkill -9on:8090as prep.boot-simulator.shis CI-only or internal to iOS test-runner retry.
# Background (once):
yarn tests:emulator:start
yarn tests:packager:jet
# Per platform (rebuild when native changed):
yarn tests:ios:build && yarn tests:ios:test-cover
yarn tests:android:build && yarn tests:android:test-cover
yarn tests:macos:test-coverFull e2e loads every package. Narrow locally; never commit narrowing.
| Kind | Mechanism | Scope |
|---|---|---|
| Area narrowing | tests/harness.overrides.js |
Which modules load (filter platformSupportedModules on all platforms) |
| Sub-suite narrowing | Temporary edit to tests/app.js (require one spec instead of require.context) |
Which spec files load within a module — unit-focused diagnosis only |
| Single-test narrowing | it.only(...) |
One case in a loaded file |
| Single-suite narrowing | describe.only(...) |
One block in a loaded file |
Area narrowing = overrides file for modules + RNFBDebug; not test-runner --grep or packager --target.
Canonical owner for module narrowing and fail-fast debug. Package workflows name which module/spec; this section defines how to focus the harness without editing committed files.
cp tests/harness.overrides.example.js tests/harness.overrides.jsEdit tests/harness.overrides.js (gitignored — never commit). Shape:
| Field | Purpose |
|---|---|
modules?: string[] |
After each platform block builds its list, filter to only these module names (works on macOS and iOS/Android — no dual-block edits) |
RNFBDebug?: boolean |
true = verbose RNFB logging + disabled test retries (§ fail-fast) |
Example — app package only, debug on:
module.exports = {
RNFBDebug: true,
modules: ['app'],
};Omit a field or export {} to keep committed defaults for that field.
Committed tests/app.js still builds full platformSupportedModules per platform block. If harness.overrides.js exists, tests/globals.js reads RNFBDebug and tests/app.js filters the module list after the platform block runs — so one overrides file applies on every platform.
Module specs load in loadTests() via platformSupportedModules.includes('<module>') — usually require.context('../packages/<module>/e2e', …).
| Goal | Change |
|---|---|
| Full package area | Overrides modules only — leave require.context as-is |
| Single spec file | Temporarily replace require.context for that module with require('../packages/<module>/e2e/<Spec>.e2e.js') in tests/app.js — never commit (sub-suite; unit-focused diagnosis only unless package workflow says otherwise) |
- Delete
tests/harness.overrides.jsor setmodule.exports = {}. - Revert any temporary
require.context→ singlerequireedits intests/app.js. - Remove all
.onlyand native instrumentation.
Do not revert durable product wiring in committed tests/globals.js (e.g. NativeRNFBTurbo* proxy routing — NewArch-AD-13).
| Platform | Narrowed firestore-only (full packages/firestore/e2e) |
Pipeline-only (Pipeline.e2e.js) |
|---|---|---|
| macOS | ~700 passing | ~100 passing |
| iOS / Android | Same order of magnitude as macOS for the same spec scope | ~100 passing |
Pass counts in the thousands or unrelated suites (database, crashlytics, …) in the log → confirm overrides file exists with correct modules and re-run.
Area example: modules: ['app', 'firestore'] + full firestore specs via existing require.context.
Package-specific spec names: Firestore pipeline harness, namespace removal § module area harness.
RNFBDebug: for unit-focused and area-focused tiers, set RNFBDebug: true in tests/harness.overrides.js before the first :test-cover — not optional. It prints per-case start/finish and disables Mocha retry/backoff, so failures surface immediately instead of burning time on retries. Committed tests/globals.js default stays false. Remove overrides (or set RNFBDebug: false) before full tier or commit (§ before merge).
| Kind | Mechanism | When |
|---|---|---|
| Sub-suite narrowing | describe.only / require one e2e file (e.g. Aggregate/count.e2e.js only) |
Unit-focused diagnosis only — after pre-flight is clean and the same failure repeats on back-to-back runs without assertion progress. Never for area-focused gate closure (no .only). Never commit. |
| Single-test narrowing | it.only(...) |
Same as sub-suite — unit-focused diagnosis only |
Package workflows may name default area specs (e.g. Firestore pipeline harness); sub-suite narrowing is tighter than area narrowing for iteration speed.
E2e diagnosis escalation (cross-package): change authoring § implementation inner loop.
Package workflows may further restrict narrowing per validation tier.
All tiers use canonical commands, host rule, and clean pre-flight. Tier names describe scope, not who runs the commands — see iteration vocabulary.
| Validation tier | E2e scope | Narrowing allowed | Typical work type |
|---|---|---|---|
| Unit-focused | Fast loop while product code is changing | harness.overrides.js + optional .only / sub-suite require in tests/app.js — never commit |
implementation |
| Area-focused | Full loaded spec(s) for the package/area under change | harness.overrides.js required; no .only |
baseline-capture, independent-review |
| Full | Unfocused — all modules, all platforms | Delete overrides file — committed full harness only | pre-merge-validation |
Universal rules:
- E2e is always serial — one
:test-coverat a time on the host. - Every run starts from verified pre-flight; if probes fail, pre-flight recovery before another run.
- Use only canonical commands from this doc.
- Never overlap unit-focused-tier and area-focused-tier
:test-coveron one host.
See also: unit-focused-tier loop, dispatch, pre-merge.
- Devices — Detox boots simulator/emulator (
iPhone 17on iOS,TestingAVDon Android); host-clear probes require zero booted iOS simulators before:test-cover. macOS auto-starts app. - adb empty —
adb kill-server && adb start-server && adb devices - Stale processes — one Metro (
:8081), one emulator set (:8080,:9099,:9000,:4400,:5001, …). Stray listener on:8090after a run → pre-flight recovery, then restart background services with Rules §1–2 (yarn tests:packager:jet,yarn tests:emulator:start). - Android Gradle home — when Android
:buildor:test-coverfails with missing/wrong Gradle cache on a host that does not default to~/.gradle, exportGRADLE_USER_HOME=$HOME/.gradlebefore the run.
Detox's default emulator launch restores the AVD Quick Boot snapshot unless told otherwise. On TestingAVD that can leave the device offline on a gray screen — adb devices shows emulator-XXXX offline and Detox hangs on wait-for-device.
Root cause: warm boot paths — Quick Boot snapshot restore on Detox launch, and (pre-fix) adb reboot on Jet retry — skip a full cold boot.
Fix (committed): tests/.detoxrc.js sets bootArgs: '-no-snapshot-load -no-snapshot-save' on the TestingAVD device. Jet retry in tests/e2e/firebase.test.js cold-restarts the same emulator (kill + relaunch with the same args) instead of adb reboot.
Detect:
adb devices -l # emulator-XXXX offline
pgrep -fl 'qemu-system.*TestingAVD'
rg 'SPAWN_CMD.*@TestingAVD' /tmp/rnfb-e2e-android.log # no -no-snapshot-load → stale runbook / configRecovery before :test-cover:
adb -s emulator-5554 emu kill 2>/dev/null || true
pkill -f 'qemu-system.*TestingAVD' 2>/dev/null || true
adb kill-server && adb start-server && adb devices # must be empty
# If gray screen persists after cold-boot config, wipe Quick Boot snapshots:
# rm -rf ~/.android/avd/TestingAVD.avd/snapshotsThen rerun pre-flight and yarn tests:android:test-cover. Cold boot adds ~30–60s to the first Android launch vs Quick Boot — expected.
Detox injects a prebuilt Detox.framework and XCUITest runner from a versioned cache under ~/Library/Detox/ios/ (hashed by Xcode version). iOS :test-cover / :build fail before any test runs if that cache is missing or stale (common after Xcode upgrade, first checkout, or a failed Detox postinstall).
Detect (from a failed iOS run log):
rg 'Detox\.framework could not be found' /tmp/rnfb-e2e-ios.logTypical error (Detox prints the fix inline):
DetoxRuntimeError: .../Library/Detox/ios/framework/<hash>/Detox.framework could not be found,
this means either you changed a version of Xcode or Detox postinstall script was unsuccessful.
To attempt a fix try running 'detox clean-framework-cache && detox build-framework-cache'
Detect (proactive, before :test-cover):
test -d ~/Library/Detox/ios/framework/*/Detox.framework && echo "Detox framework cache: OK" || echo "Detox framework cache: MISSING"Fix (canonical — repo root):
yarn tests:ios:detox-framework-cache:rebuildThis runs Detox's rebuild-framework-cache (clean + build of both the injected Detox library and the XCUITest runner) from the tests/ workspace. Expect ~10–30s on a warm machine; first build after Xcode change may take longer.
Verify cache present after rebuild:
ls ~/Library/Detox/ios/framework/*/Detox.framework
ls ~/Library/Detox/ios/xcuitest-runner/*/Then resume the normal iOS loop: pre-flight → yarn tests:ios:build (if native changed) → yarn tests:ios:test-cover.
CI restores the same tree from ~/Library/Detox/ios keyed by Xcode version (iOS workflow § Detox Framework Cache Restore). Local developers must rebuild when the cache is missing — it is not committed to git.
During TurboModule work, three different undefined / load failures are easy to confuse — only the third row below is fixed by the native registration checklist:
| Symptom | Likely cause | Fix (escalate in order) |
|---|---|---|
this.native.isFoo is undefined in JS |
Spec/native constants chain incomplete (getConstants, getTypedExportedConstants, iOS typed constants) |
Workflow checklist rows 1, 6, 8 |
Android Proxy target must be an Object |
JNI not linked / stale autolinking cache | Delete tests/android/build/generated/autolinking/autolinking.json + *.sha, :build |
Metro redbox Requiring unknown module "undefined" at app load |
Stale Metro and/or partial native refresh after codegen / lib/** / podspec changes |
Steps below; then full refresh if needed |
Detect: app redbox before Jet client connected; curl of index.bundle may still return 200 — that does not rule out staleness.
Routine fix (try first after codegen, podspec, or packages/*/lib/** edits):
- Prepare completion gate —
yarn lerna:prepareexit 0. - Regenerate codegen if specs changed — workflow § Running codegen (
cd tests,npx @react-native-community/cli codegen …). yarn tests:<platform>:build(includespod installon iOS when needed).yarn tests:packager:jet-reset-cache(Metro was running during the edits).- Pre-flight →
yarn tests:<platform>:test-cover.
Full toolchain refresh — when the routine fix still shows Requiring unknown module "undefined" or you wiped node_modules mid-migration:
- Pre-flight recovery — stop Metro, Jet, Detox; shutdown booted simulators.
- Remove all
node_modules(repo root,tests/, and underpackages/*if present). yarnat repo root (wait for exit 0 — includeslerna:prepareand patches).- Confirm install / patch / fmt gate (fmt ≥ 12.1.0) before any native
:build. - Regenerate all touched packages' codegen from
tests/(workflow § Running codegen). yarn tests:ios:pod:installwhen iOS native/codegen changed.yarn tests:<platform>:build.yarn tests:packager:jet-reset-cache→ pre-flight →:test-cover.
Do not treat this redbox as a missing TurboModule registration until the refresh sequence has been run once on a clean tree.
Local stalls — see stalled run detection first (Metro /status, Jet client connected markers).
Native / device logs (remove instrumentation before merge):
- macOS —
log show --predicate 'process == "io.invertase.testing"' --last 10m --style compact; filtercom.facebook.react.log:javascriptfor bundle errors. Blank window / Jet never connects: oftenNative module NativeRNFBTurboApp is not registered— see TurboModule workflow § gotchas — macOS web registration. Other bundle errors → other.md - iOS —
xcrun simctl spawn booted log stream --level debug --style compact --predicate 'process == "testing"'; silent hangs:sample <pid>ontesting. Metro redboxRequiring unknown module "undefined"→ TurboModule stale toolchain, not registration checklist alone. - Android —
adb logcat(filter your tags)
Benign noise: iOS Detox EXEC_FAIL "xcrun simctl terminate … com.invertase.testing" … found nothing to terminate — app wasn't running; ignore.
Cloud API pressure — Installations / Remote Config failures with FIS 503 or “Too many server requests” are live-project quota on any platform, not emulator issues. See Firebase testing project — CI triage.
Pre-merge applies once to the branch commit stream before merge/push intended for merge, not after every commit.
- Remove all narrowing (full tier): delete
tests/harness.overrides.js(or{}), revert any temporaryrequire.context→ singlerequireedits intests/app.js, remove all.only, remove native instrumentation. Committedtests/globals.js/tests/app.jsstay at full harness defaults — do not revert durable product wiring (e.g.NativeRNFBTurbo*proxy). - Pre-flight — host-clear probes pass before each platform run.
- Rebuild when needed (
tests:<platform>:build;yarn lerna:prepareforlib/**). After TurboModule codegen or native bridge edits, see TurboModule stale toolchain. - Full unfocused suite with coverage on iOS, Android, macOS — one platform at a time, all green.
- Record validation evidence package and coverage evidence package when lib/native touched — required before merge or push (change authoring § validation evidence).
- Stale native build after native edits → rebuild first.
- All three platforms required; macOS exercises the JS SDK path.