warden

How warden e2e works

A spec file, a suite in warden.config.ts and one command, warden e2e, that leases devices, installs the build and runs the specs in parallel.

An e2e test in warden is a *.e2e.ts file; warden e2e <suite> runs a suite of them across leased simulators or emulators and writes a report. Read this page first, then follow the links for each piece.

warden e2e example-dev --all

The pieces

PieceWhat it isWhere it lives
SpecOne file of tests for a user flow: test("…", async ({ app, device, screen }) => { … }), written with @delacour/warden-e2ee2e/*.e2e.ts (Write specs)
SuiteA named set of specs plus how to run them: devices, build, Metro, timeoutse2e.<name> in warden.config.ts (Suites)
Runwarden e2e <suite>: leases devices, installs, starts Metro, runs, reportsone command
Evidenceresults.jsonl per worker, e2e-report.json per run, artifacts per failed testthe batch dir (Debugging)

A spec's id is its path under specsDir without the extension (e2e/todos.e2e.ts is todos).

What one run does

Pick the specs

--all runs every spec, --specs a,b exactly those, otherwise warden selects the specs the git diff needs (Affected). Nothing selected exits 0 without claiming a device.

Lease N devices

--count N (or the suite's count) claims N simulators or emulators from the machine-wide lease registry, queueing up to 30 minutes when none is free. warden never touches a device it did not create or lease (Leases).

Install the build

With app: true (or --app), each device gets the project's app: already installed, else cached, else EAS, else a local build, keyed on the Expo fingerprint (App builds).

Start Metro (dev-client suites)

With metro: {} warden leases a port, runs expo start --dev-client on it, and calls it ready only once that port is proven to serve this checkout. Every app.open() then launches against that Metro.

Run one bun test per device

Each device gets one bun test --preload @delacour/warden-e2e/preload process over its share of the specs (sharded round-robin by spec id). Start-up, preload and driver connect are paid once per device. isolate: "spec" runs one process per spec instead.

Report and release

Every test appends a line to results.jsonl; warden folds them into e2e-report.json, prints the failed specs, and releases the leases (Ctrl-C included). Exit 0 if every required spec passed, 1 if one failed, 130 if interrupted.

warden e2e example-dev --all
  │
  ├─ select specs ─────────────► 7 specs
  ├─ lease devices ────────────► sim A, sim B
  ├─ install build, start Metro (one port, leased)
  │
  ├─ sim A: bun test  specs 1, 3, 5, 7 ──► results.jsonl
  ├─ sim B: bun test  specs 2, 4, 6    ──► results.jsonl
  │
  └─ e2e-report.json, release leases

Wall time is the slowest device's share, not the sum. A parallel run of 37 offline specs across 5 leased simulators took 3m25s against 13m55s serially on one (4.1x).

Where agents fit

Your agent drives the app by hand with warden ui on a simulator it claimed. Then it writes what it checked as a spec under e2e/. warden ui and the spec runner share one driver and one selector grammar, so a selector that worked by hand works in a test. After that, warden e2e is the same command for you, your agent and CI (CI).

When a spec fails

The failing test writes failure.png, screen-at-failure.txt (the full accessibility tree) and trace.jsonl next to its results. A retry (--retry N, or a test's own retry) reruns only the specs that did not pass. A spec fails when any test failed or timed out, or its process exited non-zero; a process that hangs past jobTimeoutMs per spec is killed (exit 124). On a failed run the devices stay booted so you can inspect them. See Debugging.

Limits

  • iOS needs macOS and Xcode; Android needs the SDK. There is no Windows support.
  • Specs read the accessibility tree, so a view must be accessible to be found by testID.
  • @delacour/warden-e2e and warden must be the same version; the preload exits 2 otherwise.

On this page