# Affected e2e (/docs/guides/affected-e2e)



A full e2e suite is slow; most changes touch a few screens. `warden affected` works out which flows a branch needs, and `warden e2e` runs just those on leased devices and fails when a required one fails.

```bash
warden affected mobile --base origin/main --explain   # what would run, and why
warden e2e mobile --base origin/main --count 2        # run it: exit 0 = every required flow passed
```

Flows stay in your flow runner's own files — YAML flows, or `*.e2e.ts` tests (e.g. [tester.army e2e](https://e2e.tester.army/docs/mobile)) whose relative imports count as fragments; warden only picks them, leases devices, runs your `runner` command once per flow, and reports.

## How flows are picked [#how-flows-are-picked]

1. **Changed files.** Everything that differs from `merge-base(<base>, HEAD)`: commits on the branch, staged and unstaged edits, untracked files, both sides of a rename. `--files a,b` skips git.
2. **`runAll`.** A change to a lockfile, native project or bundler config selects every flow.
3. **`paths`.** Globs on a flow select it when a changed file matches.
4. **Import graph.** Each flow lists its `entries` — the screens or routes it drives. Warden follows their imports, resolved the way the TypeScript compiler does (each file's nearest `tsconfig.json`: `paths`, `baseUrl`, `extends`, `customConditions`; workspace packages followed to their source). A changed file anywhere in that graph selects the flow.
   * **Per platform.** React Native platform files resolve per platform (`x.ios.tsx` → `x.native.tsx` → `x.tsx`), so an `.android.tsx` change only selects Android flows.
   * **Runtime imports only.** `import type` / `export type` are dropped, as the bundler drops them.
   * **Router layouts.** With `routerRoot`, every `_layout` above an entry counts as an entry too (file-based routers render them around the route).
   * **`maxDepth`** limits how many import hops from an entry still count. Apps with hub modules (a session context, a shared API client) otherwise reach almost everything from every screen; `3` is a good start.
5. **Flow files.** A changed flow selects itself; a changed fragment selects every flow that `run:`s it.
6. **`always`** flows run every time, and so do flows with no `entries` / `paths` unless `unmapped` is `"skip"`.

A flow runs on the platforms named in its config, else the `ios` / `android` token in its id (`store-ios-02-chats`), else its per-platform `launch` map, else both.

`--explain` prints the reason for each flow, including the shortest import chain:

```text
ios  2 of 11 flow(s)  (base origin/main, 3 changed file(s))
  ● qa-chat-attachment  required
      import   src/modules/chats/composer.tsx
               src/app/(app)/chats/[chat_id].tsx → src/modules/chats/thread.tsx → src/modules/chats/composer.tsx
  ● store-ios-03-chat-thread  optional
      import   src/modules/chats/composer.tsx
  skipped: qa-offline-banner, store-ios-01-my-day, …
  reached no flow: src/modules/voice/voice-auth.ts
```

"Reached no flow" lists changed files no flow covers — gaps in the map. `--strict` exits 3 when there are any.

## Configuring a suite [#configuring-a-suite]

```ts title="warden.config.ts"
import { defineConfig } from "@delacour/warden/config";

export default defineConfig({
  e2e: {
    mobile: {
      flowsDir: "flows",
      runner: ["<flow-runner>", "run", "{flowPath}", "--device", "{udid}"],
      platform: "ios",
      tsconfig: "apps/mobile/tsconfig.json",
      routerRoot: "apps/mobile/src/app",
      maxDepth: 3,
      runAll: ["bun.lock", "patches/**", "apps/mobile/app.config.ts", "apps/mobile/ios/**", "apps/mobile/android/**"],
      ignore: ["**/*.md", "**/*.test.ts"],
      passes: 2,
      count: 2,
      flows: {
        "qa-chat-attachment": { entries: ["apps/mobile/src/app/(app)/chats/[chat_id].tsx"] },
        "qa-offline-*": { entries: ["apps/mobile/src/app/+native-intent.ts"], paths: ["apps/mobile/src/modules/linking/**"] },
        "store-*": { required: false }
      }
    }
  }
});
```

A flow's id is its path under `flowsDir` without the extension. Keys in `flows` are ids or globs; a flow's exact key and every matching glob merge (`entries` / `paths` add up, the exact key's `required` / `platforms` / `maxDepth` win). All paths are relative to the config file. See the [field reference](/docs/reference/configuration#e2e).

## Running and gating [#running-and-gating]

`warden e2e` is [`warden batch`](/docs/guides/e2e#batches-one-job-per-device) with the job list coming from the selection. Every batch and claim flag works (`--count`, `--serve`, `--serve-ready`, `--app`, `--record` …); the suite's `count`, `profile`, `retry`, `passes`, `app`, `ports`, `env`, `serve`, `serveReady` and `serveTimeout` fill in anything you don't pass, and `project` (a `projects[].name`) sets where the runner and serve run and which app is installed — the same semantics as a [batch preset](/docs/guides/e2e#saving-a-batch-as-a-preset).

* Each flow runs as `runner` with `{flow}` (id), `{flowPath}` (absolute file), `{udid}`, `{worker}` and `{seq}` substituted, in the config file's directory.
* `passes: 2` makes a flow pass only after two consecutive green runs on the same device (`WARDEN_PASS` = 0, 1), which catches flows that only pass from a lucky start state.
* `required: false` flows are reported but never fail the gate. `--required-only` runs just the required ones.
* `e2e-report.json` (in the batch dir, and at `--report <file>`) lists every flow with its reasons, attempts and verdict, plus a `screenshot` (png of the device) for a flow whose last attempt failed. `--json` prints it.
* Leases are released when the run ends. Once the gate passes, the sims / emulators warden created (or booted for the run) are shut down too; a failed run leaves them booted so you can inspect them (`warden release --mine --shutdown` when done). `--no-shutdown` keeps them up after a pass.
* Nothing affected → exit 0 without claiming a device. `--dry-run` selects and explains, then stops.

### In CI [#in-ci]

```bash
git fetch origin main
warden e2e mobile --base origin/main --count 2 --report e2e-report.json
```

The same command runs locally or in a pre-push hook. Parsed imports are cached under `$WARDEN_HOME/affected`, so repeat runs only re-read changed files.

### Feeding a batch preset [#feeding-a-batch-preset]

`warden affected` prints one flow id per line, so it also works as a preset's job source:

```ts
jobsFrom: { command: "warden affected mobile --platform ios --base origin/main" }
```

## Per-device setup [#per-device-setup]

`setup` runs a shell command once on every leased device, after the app install and before that device's first flow. Typical uses: point the installed app at the suite's local API, or answer a first-launch system alert.

```ts title="warden.config.ts"
import { defineConfig } from "@delacour/warden/config";

export default defineConfig({
  e2e: {
    online: {
      ports: ["8091:5"],
      serve: "bun api",
      setup: "xcrun simctl spawn {udid} defaults write com.example.app ApiUrl http://localhost:$WARDEN_PORT_0"
    }
  }
});
```

The command gets `WARDEN_UDID`, `WARDEN_WORKER`, `WARDEN_PORT_<i>` / `WARDEN_PORTS`, `WARDEN_APP_PATH` / `WARDEN_APP_HASH` and the suite `env`, and runs in the suite's `project` root. Each device's output is `logs/setup-<worker>.log`, next to the job logs. A device whose setup exits non-zero is dropped from the pool (its flows go to the remaining devices); if none is left the run fails and names the setup logs. `e2e-report.json` records every device under `setup` (`worker`, `udid`, `ok`, `exitCode`, `log`).

`slim: true` (iOS) runs [`warden sim slim`](/docs/reference/cli#warden-sim-slim-udid) on each device first, to save RAM and CPU when several simulators run together. A slim failure only warns; the device still runs flows. The result is recorded under `setup[].slim` in `e2e-report.json`.
