warden

Debugging

Where a failed e2e run leaves its evidence, how to read it, and the fix for each common failure.

When a spec fails, read the evidence in this order: the run's stdout, screen-at-failure.txt, failure.png, trace.jsonl, then the logs. A missing element is fixed in the component, not with retries.

warden e2e example-dev --specs sign-in --json | jq '.specs[] | select(.verdict != "passed")'

Batch directory

Every run writes under a batch directory, $WARDEN_HOME/batches/<id> (the --record directory when you pass one; WARDEN_HOME defaults to ~/.warden; spec processes get it as WARDEN_BATCH_DIR).

e2e-report.json
metro.log
<worker>-<seq>-<job>.log
warm-<worker>.log
setup-<worker>.log
results.jsonl
failure.png
screen-at-failure.txt
trace.jsonl
console.jsonl
network.jsonl
WhatWhere
verdict per spec, and per test (tests[])stdout, and e2e-report.json (--json prints it, --report <file> copies it)
every test's status, duration and errore2e/<worker>-<seq>/results.jsonl (WARDEN_E2E_OUT)
a failed test's evidencee2e/<worker>-<seq>/<spec>/<test>-<hash>/
a spec process's outputlogs/<worker>-<seq>-<job>.log (<job> is shard-<k> under device isolation, the spec id with isolate: "spec"), screenshot next to it
Metro's outputmetro.log
a device's driver warm-uplogs/warm-<worker>.log
a device's setup commandlogs/setup-<worker>.log

The evidence files

FileContents
failure.pngthe device at the moment of failure
screen-at-failure.txtthe full accessibility tree, with testIDs, labels and values. Read this first: a query that found nothing is almost always an element that is missing or not accessible there
trace.jsonlevery step and every device / driver / locator-read call (span lines), with timings. Written for every test attempt, passed or not
console.jsonl, network.jsonldev client only, when devtools were attached: the JS console and network requests
screenshots/device.screenshot(name) images as NNN-name.png, from any test
results.jsonlone line per finished test, skipped ones included: file, title[], status (passed, failed, skipped, timed-out), durationMs, startedAt, error (message, code, stack), artifacts, device (udid, platform, name). With retries only the final attempt is written
e2e-report.jsoneach spec's verdict, attempts and tests[] (title, status, durationMs, error, artifacts), a device, log and screenshot for a spec whose last attempt failed; top-level suite, platform, base, isolate, headless, batchDir, only and each device's setup

<spec> is the spec's path under the suite's specsDir without .e2e.ts; <test-title> is a slug of the describe and test titles. On a later pass the worker directory is <worker>-<seq>-p<pass>. A by-hand run writes under WARDEN_E2E_OUT (default .warden-e2e/<timestamp> in the current directory).

A spec fails when any of its tests failed or timed out, or its process exited non-zero. When a process wrote no results.jsonl at all (a crash before the first test), the verdict falls back to the exit code with a warning. Under CI, a test.only left in a spec fails the run and is listed under only.

A spec's error is an E2eError with a code: LOCATOR_NOT_FOUND, LOCATOR_AMBIGUOUS, INVALID_LOCATOR, ACTION_FAILED, NOT_ACTIONABLE, TIMEOUT, INVALID_ARGUMENT, ASSERTION, SESSION, DRIVER. A locator failure carries the screen as text.

Fix a missing testID in the component

"Locator not found" means the view is not in the accessibility tree under that id. Specs read that tree, and a testID is only exposed when the native view is accessible. In a dev client, a plain getByTestId that finds nothing there falls back to the React tree, but only tap() and boundingBox() work on such a component; anything else fails with NOT_ACTIONABLE. Open screen-at-failure.txt, confirm the id is absent, and add accessible and testID to the component. Retries and longer timeouts only hide it. By hand, warden ui snapshot -i and warden ui tree show the same tree.

Common failures

SymptomCauseFix
First tap after launch or navigation ignoredThe view is in the accessibility tree before it takes touchestap({ until: <what the tap opens> }) re-sends it
testIDs vanish 1-6 s after sign-in; keys go nowhereiOS "Save Password?" sheetIn the app under the e2e flag: autoComplete="off" and textContentType="oneTimeCode". In the spec: test.use({ systemAlerts: "dismiss" }) or device.alert("dismiss"). Never defaults write
Dropped charactersKeys sent while focus was still moving to the fieldNever lower WARDEN_SIMD_PACE_MS / WARDEN_SIMD_HOLD_MS. fill and spec type / press wait for the field to report focused, and fill verifies and retries
App back at Welcome mid-testNative crashRead ~/Library/Logs/DiagnosticReports/<app>-*.ips
Deep link ignoredOpened while the app was settlingdevice.openLink(url, { until })
port <p> is served by <dir>, not this worktreeAnother checkout's Metro owns the portStop that Metro or let warden pick another port. Never start Metro by hand
device_mismatchTwo devices on one MetroThe runner pins by WARDEN_DEVICE_NAME; set it by hand when running manually
Tests run stale JS on a Release buildNative-only fingerprintUse native+js with jsInputs in the project's fingerprint
Preload exits 2 with a version messagewarden and @delacour/warden-e2e are lock-stepbun add -d @delacour/warden-e2e@<warden version>
no driver factory configuredbun test run without the preloadAdd --preload @delacour/warden-e2e/preload, or preload under [test] in bunfig.toml
Form submitted by hideKeyboardiOS presses Enterdevice.settle() or tap elsewhere
A press right after fill on Android is lostThe app was still taking the typed text indevice.settle() before the press
E_AX_UNAVAILABLE right after launchTransientBuilt-in 5x300 ms retry. simctl launch can take over 30 s under load
E_AX_UNAVAILABLE: "another UiAutomation client holds <serial>"Android allows one UiAutomation connection per device and another tool (Maestro, am instrument, uiautomator) has itStop the process named in the message, or release the device. warden kills only its own stale warden-uiad
Exit 3 engine_unavailableNo warden-simd helperwarden doctor --fetch-helper (Intel Mac: no helper)
pinch / rotate / path fails E_UNSUPPORTED, reason: helper_outdated (iOS)warden-simd predates the gesture opwarden ui close --udid <udid>, or rebuild: bun run --cwd packages/e2e native:build
Android slow or unsupporteduiautomator dump in use (the warden-uiad server is off or could not start); no launch argsDeep link instead of --metro; ASCII text only without the server. WARDEN_E2E_DEBUG=1 logs uiad: lines; WARDEN_ANDROID_UIAD=0 forces the dump
Settle takes about 2.5 sTwo helpers on one simulatorwarden ui close --udid <udid>, or kill the stale warden-simd --udid <udid>
Exit 124A job exceeded jobTimeoutMs per spec and was killedFind the hang in trace.jsonl; raise jobTimeoutMs only if the spec is genuinely long

WARDEN_E2E_DEBUG=1 logs every helper call with its duration. warden doctor shows helper health; degraded means the accessibility guest is present but its last read failed, and the next good read clears it.

Keep a device out of the pool

warden release <leaseId> --bad "<why>"

A device that keeps failing is quarantined; warden sim unquarantine <udid|serial> clears it. A failed run leaves its devices booted so you can inspect them; warden release --mine --shutdown when done.

On this page