e2e 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build.json +1 -1
- package/dist/agent/act.js +7 -4
- package/dist/agent/action-dispatcher.js +4 -5
- package/dist/agent/actions.d.ts +32 -23
- package/dist/agent/actions.js +38 -24
- package/dist/agent/model/sdk.js +2 -0
- package/dist/agent/observation-feed.d.ts +5 -6
- package/dist/agent/observation-feed.js +5 -7
- package/dist/agent/observation.d.ts +40 -11
- package/dist/agent/observation.js +109 -59
- package/dist/agent/point-tap.d.ts +8 -8
- package/dist/agent/point-tap.js +1 -1
- package/dist/agent/replay.d.ts +19 -8
- package/dist/agent/replay.js +11 -14
- package/dist/agent/scroll-target.d.ts +2 -3
- package/dist/agent/step-cache.d.ts +41 -14
- package/dist/agent/step-cache.js +147 -62
- package/dist/agent/tool-loop.js +2 -0
- package/dist/cache/anchors.d.ts +72 -23
- package/dist/cache/anchors.js +233 -60
- package/dist/cache/context.d.ts +25 -14
- package/dist/cache/context.js +50 -8
- package/dist/cache/decide.d.ts +2 -2
- package/dist/cache/decide.js +4 -6
- package/dist/cache/identity.d.ts +18 -4
- package/dist/cache/identity.js +9 -4
- package/dist/cache/recorder.d.ts +15 -1
- package/dist/cache/recorder.js +36 -23
- package/dist/cache/rekeyed.d.ts +46 -0
- package/dist/cache/rekeyed.js +89 -0
- package/dist/cache/relocate.d.ts +14 -14
- package/dist/cache/relocate.js +33 -25
- package/dist/cache/route.d.ts +32 -33
- package/dist/cache/route.js +94 -50
- package/dist/cache/store.d.ts +6 -0
- package/dist/cache/store.js +21 -1
- package/dist/cache/trace.d.ts +53 -14
- package/dist/cache/trace.js +70 -28
- package/dist/cli/index.js +8 -8
- package/dist/cli/init/engines.d.ts +1 -3
- package/dist/cli/init/engines.js +2 -2
- package/dist/cli/init/gateways.d.ts +1 -1
- package/dist/cli/init/gateways.js +11 -1
- package/dist/cli/init/sibling-versions.json +1 -1
- package/dist/cli/init/versions.d.ts +0 -8
- package/dist/cli/init/versions.js +1 -10
- package/dist/config/resolve.d.ts +1 -0
- package/dist/config/resolve.js +33 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/expect/async.js +2 -1
- package/dist/expect/poll-scope.d.ts +52 -0
- package/dist/expect/poll-scope.js +113 -0
- package/dist/expect/poll.js +31 -4
- package/dist/expect/values.js +11 -4
- package/dist/internal/abandoned.d.ts +11 -0
- package/dist/internal/abandoned.js +23 -0
- package/dist/internal/brands.d.ts +2 -0
- package/dist/internal/brands.js +2 -0
- package/dist/internal/client-identity.d.ts +9 -0
- package/dist/internal/client-identity.js +14 -0
- package/dist/internal/redact.d.ts +7 -4
- package/dist/internal/redact.js +116 -23
- package/dist/internal/time.d.ts +6 -0
- package/dist/internal/time.js +25 -13
- package/dist/internal/urls.d.ts +9 -5
- package/dist/internal/urls.js +19 -10
- package/dist/locator/engine.d.ts +12 -2
- package/dist/locator/engine.js +8 -3
- package/dist/locator/screen.js +28 -4
- package/dist/mcp/catalog.d.ts +3 -2
- package/dist/mcp/catalog.js +6 -6
- package/dist/mcp/session.js +1 -1
- package/dist/oauth/chatgpt.js +3 -23
- package/dist/oauth/cli.js +1 -0
- package/dist/oauth/copilot.d.ts +5 -4
- package/dist/oauth/copilot.js +80 -7
- package/dist/oauth/grok.js +2 -1
- package/dist/oauth/models.js +2 -1
- package/dist/oauth/opencode-console.d.ts +10 -0
- package/dist/oauth/opencode-console.js +133 -0
- package/dist/oauth/providers/github-copilot.d.ts +12 -0
- package/dist/oauth/providers/github-copilot.js +77 -23
- package/dist/oauth/providers/opencode-console.d.ts +31 -0
- package/dist/oauth/providers/opencode-console.js +148 -0
- package/dist/oauth/providers.d.ts +2 -6
- package/dist/oauth/providers.js +3 -7
- package/dist/oauth/responses.d.ts +9 -0
- package/dist/oauth/responses.js +23 -0
- package/dist/report/build.d.ts +26 -0
- package/dist/report/build.js +16 -8
- package/dist/report/failure-text.d.ts +6 -1
- package/dist/report/failure-text.js +14 -4
- package/dist/report/format.d.ts +10 -5
- package/dist/report/format.js +12 -5
- package/dist/report/junit.js +11 -12
- package/dist/report/list.d.ts +1 -0
- package/dist/report/list.js +38 -18
- package/dist/report/markdown.d.ts +1 -1
- package/dist/report/markdown.js +37 -14
- package/dist/report/repeats.d.ts +6 -1
- package/dist/report/repeats.js +12 -3
- package/dist/run/artifacts.d.ts +18 -0
- package/dist/run/artifacts.js +68 -1
- package/dist/run/execute.d.ts +9 -0
- package/dist/run/execute.js +49 -24
- package/dist/run/extended-fixtures.js +6 -2
- package/dist/run/failure-evidence.js +4 -2
- package/dist/run/fixtures.js +6 -8
- package/dist/run/in-process.d.ts +2 -0
- package/dist/run/in-process.js +1 -0
- package/dist/run/last-run.d.ts +42 -8
- package/dist/run/last-run.js +121 -14
- package/dist/run/realm.d.ts +4 -1
- package/dist/run/realm.js +20 -6
- package/dist/run/records.d.ts +16 -0
- package/dist/run/records.js +26 -0
- package/dist/run/retry.d.ts +3 -1
- package/dist/run/retry.js +4 -2
- package/dist/run/runner.d.ts +1 -1
- package/dist/run/runner.js +69 -30
- package/dist/run/serial.d.ts +2 -0
- package/dist/run/serial.js +8 -5
- package/dist/run/steps.d.ts +0 -6
- package/dist/run/steps.js +3 -20
- package/dist/run/worker/entry.js +5 -4
- package/dist/run/worker/protocol.d.ts +2 -0
- package/dist/run/worker/session.d.ts +2 -0
- package/dist/run/worker/session.js +1 -0
- package/dist/telemetry/events.js +1 -0
- package/dist/types.d.ts +9 -3
- package/docs/agents.mdx +6 -3
- package/docs/assertions.mdx +94 -0
- package/docs/authentication.mdx +3 -1
- package/docs/browser.mdx +42 -6
- package/docs/cache.mdx +64 -18
- package/docs/ci.mdx +11 -7
- package/docs/debugging.mdx +6 -3
- package/docs/executors.mdx +4 -2
- package/docs/github.mdx +12 -4
- package/docs/{writing-tests.mdx → goals.mdx} +21 -101
- package/docs/index.mdx +4 -4
- package/docs/integrations/kernel.mdx +1 -1
- package/docs/locators.mdx +64 -0
- package/docs/migrate/cypress.mdx +2 -2
- package/docs/migrate/maestro.mdx +2 -2
- package/docs/migrate/playwright.mdx +19 -16
- package/docs/migrate/selenium.mdx +2 -2
- package/docs/mobile.mdx +21 -4
- package/docs/models.mdx +53 -7
- package/docs/physical-devices.mdx +76 -0
- package/docs/quickstart.mdx +4 -3
- package/docs/reference/app.mdx +1 -1
- package/docs/reference/cli.mdx +30 -13
- package/docs/reference/config.mdx +25 -7
- package/docs/reference/engine.mdx +3 -3
- package/docs/reference/environment.mdx +8 -1
- package/docs/reference/errors.mdx +5 -5
- package/docs/reference/expect.mdx +34 -12
- package/docs/reference/mobile.mdx +23 -13
- package/docs/reference/reporters.mdx +8 -2
- package/docs/reference/screen.mdx +30 -7
- package/docs/reference/test.mdx +11 -4
- package/docs/reference/web.mdx +125 -21
- package/docs/security.mdx +26 -16
- package/docs/subscriptions.mdx +91 -12
- package/docs/telemetry.mdx +1 -1
- package/package.json +16 -3
- package/schema/report-v1.schema.json +14 -1
- package/skills/e2e/SKILL.md +2 -2
- package/skills/e2e/references/agent.md +22 -11
- package/skills/e2e/references/debugging.md +4 -4
- package/skills/e2e/references/running.md +13 -8
- package/skills/e2e/references/setup.md +36 -15
- package/skills/e2e/references/writing-tests.md +34 -17
package/docs/quickstart.mdx
CHANGED
|
@@ -10,7 +10,7 @@ export const setupPrompt = `Set up e2e end-to-end tests in this project. Docs: \
|
|
|
10
10
|
1. Run \`npx e2e init --yes\` (or the pnpm or bun equivalent for this project). It writes a web config using Vercel AI Gateway, an example test, the e2e skill, and MCP config. It installs nothing yet.
|
|
11
11
|
2. Read the e2e skill (\`npx e2e guide\` prints it, \`npx e2e guide <topic>\` prints a topic) and follow it for every step below.
|
|
12
12
|
3. Point the target at this app. For a web app, set its URL and the command that starts its dev server. For an iOS or Android app, replace @e2e-dev/web and playwright with @e2e-dev/mobile and rewrite the example test for the mobile engine.
|
|
13
|
-
4. Ask me which model to use for agent steps: a ChatGPT, GitHub Copilot, or SuperGrok subscription, an API key, or a local model. Configure it with its provider package. If it needs a sign-in, give me the \`npx e2e login\` command and wait until I confirm.
|
|
13
|
+
4. Ask me which model to use for agent steps: a ChatGPT, GitHub Copilot, OpenCode Console, or SuperGrok subscription, an API key, or a local model. Configure it with its provider package. If it needs a sign-in, give me the \`npx e2e login\` command and wait until I confirm.
|
|
14
14
|
5. Install dependencies, run the example test, and fix setup errors until it passes.
|
|
15
15
|
6. Write one test for the most important user flow in this app, run it, and iterate until it passes.`;
|
|
16
16
|
|
|
@@ -159,6 +159,7 @@ Connect the provider you chose in the wizard.
|
|
|
159
159
|
| --- | --- |
|
|
160
160
|
| ChatGPT Plus or Pro | `npx e2e login openai` |
|
|
161
161
|
| GitHub Copilot | `npx e2e login github-copilot` |
|
|
162
|
+
| OpenCode Console (OpenCode Zen and OpenCode Go) | `npx e2e login opencode-console` |
|
|
162
163
|
| SuperGrok or X Premium+ | `npx e2e login spacexai` |
|
|
163
164
|
|
|
164
165
|
Copilot reuses your GitHub CLI login if available. See
|
|
@@ -245,8 +246,8 @@ to watch a web test, `--no-cache` to skip replaying cached actions.
|
|
|
245
246
|
## Next
|
|
246
247
|
|
|
247
248
|
<CardGroup cols={2}>
|
|
248
|
-
<Card title="
|
|
249
|
-
Write
|
|
249
|
+
<Card title="Goals" icon="pen-to-square" href="/goals">
|
|
250
|
+
Write test steps as goals in natural language.
|
|
250
251
|
</Card>
|
|
251
252
|
<Card title="Signing in" icon="key" href="/authentication">
|
|
252
253
|
Reuse an app login and keep passwords out of model input.
|
package/docs/reference/app.mdx
CHANGED
|
@@ -43,7 +43,7 @@ timeout. They do not accept per-call timeout options.
|
|
|
43
43
|
|
|
44
44
|
| Method | Timeout | Throws |
|
|
45
45
|
| --- | --- | --- |
|
|
46
|
-
| `open(path?)` | `config.timeout`, 120000 ms | `APP_URL_REQUIRED` for a path when the target declares no `app.url`, `POLICY_DENIED` for a
|
|
46
|
+
| `open(path?)` | `config.timeout`, 120000 ms | `APP_URL_REQUIRED` for a path when the target declares no `app.url`, `POLICY_DENIED` for a URL whose scheme is not `http:` or `https:` (`about:blank` aside), `APP_UNREACHABLE`, `ACTION_FAILED`, `UNSUPPORTED_CAPABILITY` without `session.open` on a target with a URL or without `session.restart` on one without (a device launches its pinned app) |
|
|
47
47
|
| `restart()` | `config.timeout`, 120000 ms | `ACTION_FAILED`, `ENGINE_FAILURE`, `UNSUPPORTED_CAPABILITY` without `session.restart` |
|
|
48
48
|
| `clearState()` | `config.timeout`, 120000 ms | `ACTION_FAILED`, `ENGINE_FAILURE`, `UNSUPPORTED_CAPABILITY` without `session.reset` |
|
|
49
49
|
| `back()` | `config.actionTimeout`, 30000 ms | `APP_NOT_OPEN`, `ACTION_FAILED`, `UNSUPPORTED_CAPABILITY` without `session.back` |
|
package/docs/reference/cli.mdx
CHANGED
|
@@ -96,9 +96,9 @@ For a new project, the wizard asks you to choose:
|
|
|
96
96
|
**Mobile (iOS/Android)** adds `@e2e-dev/mobile`, defaults to iOS on
|
|
97
97
|
macOS and Android elsewhere, and opens Settings. **None** adds nothing.
|
|
98
98
|
- A model provider or subscription for agent steps. Choose Vercel AI Gateway,
|
|
99
|
-
OpenRouter, an OpenAI-compatible endpoint, ChatGPT, GitHub Copilot,
|
|
100
|
-
SuperGrok. Choose None for tests that only use
|
|
101
|
-
the required packages and model config. Subscription users then sign in
|
|
99
|
+
OpenRouter, an OpenAI-compatible endpoint, ChatGPT, GitHub Copilot,
|
|
100
|
+
OpenCode Console, or SuperGrok. Choose None for tests that only use
|
|
101
|
+
locators. The wizard adds the required packages and model config. Subscription users then sign in
|
|
102
102
|
with `e2e login`. See [Models](/models) and [Subscriptions](/subscriptions).
|
|
103
103
|
- Where to install the agent skill (see [e2e guide](#e2e-guide)):
|
|
104
104
|
`.agents/skills/` for Codex, Cursor, Copilot, Gemini CLI, OpenCode, and
|
|
@@ -164,6 +164,7 @@ npx e2e login openai # ChatGPT, browser sign-in
|
|
|
164
164
|
npx e2e login openai --device # sign in with a device code
|
|
165
165
|
npx e2e login github-copilot --from-gh # use your GitHub CLI login
|
|
166
166
|
npx e2e login github-copilot --client-id <id> [--enterprise-url <host>]
|
|
167
|
+
npx e2e login opencode-console # OpenCode Console, device code
|
|
167
168
|
npx e2e login spacexai # SuperGrok, device code
|
|
168
169
|
npx e2e login # choose a provider
|
|
169
170
|
```
|
|
@@ -173,6 +174,7 @@ pnpm exec e2e login openai # ChatGPT, browser sign-in
|
|
|
173
174
|
pnpm exec e2e login openai --device # sign in with a device code
|
|
174
175
|
pnpm exec e2e login github-copilot --from-gh # use your GitHub CLI login
|
|
175
176
|
pnpm exec e2e login github-copilot --client-id <id> [--enterprise-url <host>]
|
|
177
|
+
pnpm exec e2e login opencode-console # OpenCode Console, device code
|
|
176
178
|
pnpm exec e2e login spacexai # SuperGrok, device code
|
|
177
179
|
pnpm exec e2e login # choose a provider
|
|
178
180
|
```
|
|
@@ -182,6 +184,7 @@ bunx e2e login openai # ChatGPT, browser sign-in
|
|
|
182
184
|
bunx e2e login openai --device # sign in with a device code
|
|
183
185
|
bunx e2e login github-copilot --from-gh # use your GitHub CLI login
|
|
184
186
|
bunx e2e login github-copilot --client-id <id> [--enterprise-url <host>]
|
|
187
|
+
bunx e2e login opencode-console # OpenCode Console, device code
|
|
185
188
|
bunx e2e login spacexai # SuperGrok, device code
|
|
186
189
|
bunx e2e login # choose a provider
|
|
187
190
|
```
|
|
@@ -194,7 +197,7 @@ configuration.
|
|
|
194
197
|
|
|
195
198
|
| Argument or flag | Type | Default | Behavior |
|
|
196
199
|
| --- | --- | --- | --- |
|
|
197
|
-
| `[provider]` | `openai`, `github-copilot`, or `spacexai` | a picker | The subscription to sign in to |
|
|
200
|
+
| `[provider]` | `openai`, `github-copilot`, `opencode-console`, or `spacexai` | a picker | The subscription to sign in to |
|
|
198
201
|
| `--device` | boolean | `false` | ChatGPT: show a code to enter on another device instead of opening a browser |
|
|
199
202
|
| `--from-gh` | boolean | `false` | GitHub Copilot: reuse the token of the signed-in GitHub CLI |
|
|
200
203
|
| `--client-id <id>` | string | none | GitHub Copilot: the client id of your GitHub OAuth App with the device flow enabled |
|
|
@@ -239,10 +242,11 @@ bunx e2e models openai # one provider
|
|
|
239
242
|
</CodeGroup>
|
|
240
243
|
|
|
241
244
|
Asks the vendor which models the stored login serves and prints one line per
|
|
242
|
-
model: the id `chatgpt()`, `copilot()`, or `grok()`
|
|
243
|
-
for it, and its notes (reasoning levels, vision,
|
|
244
|
-
Copilot `not enabled` or `no chat
|
|
245
|
-
|
|
245
|
+
model: the id `chatgpt()`, `copilot()`, `opencodeConsole()`, or `grok()`
|
|
246
|
+
takes, the vendor's name for it, and its notes (reasoning levels, vision,
|
|
247
|
+
preview, hidden, for Copilot `not enabled` or `no chat or responses` when
|
|
248
|
+
`copilot()` cannot call it, and for OpenCode Console the plan, Zen or Go,
|
|
249
|
+
that serves it). Without a
|
|
246
250
|
stored login, or when the named provider has none, exits 1 with the sign-in
|
|
247
251
|
command; when one of several logins fails to list, the others still print and
|
|
248
252
|
the command exits 1. An unknown provider exits 2. See
|
|
@@ -359,7 +363,7 @@ or every test was filtered out.
|
|
|
359
363
|
| `--exclude-tag <tags>` | comma-separated or repeated | none | Leaves out every test carrying any of the tags, whatever `--tag` or a positional selected. A serial group still runs whole when any member is selected; tag the serial `describe` to exclude the flow. |
|
|
360
364
|
| `--grep <pattern>` | regular expression, repeatable | none | Only tests whose title matches one of the patterns. The text matched is the describe titles and the test title joined by one space (`checkout pays`); the file and the tags are not part of it. A bare pattern is case-sensitive; `'/pattern/i'` adds flags. A pattern that does not compile is a usage error, exit 2. |
|
|
361
365
|
| `--grep-invert <pattern>` | regular expression, repeatable | none | Leaves out every test whose title matches one of the patterns. Same text and pattern forms as `--grep`; both together keep the tests `--grep` matches minus the ones `--grep-invert` matches. |
|
|
362
|
-
| `--last-failed` | boolean | `false` | Only the tests the previous run did not pass, read from the `report.json` this run overwrites, `<output>/report.json` (`--output`, else the config's [`output`](/reference/config#output)): failed, timed out, interrupted, or skipped because a setup, a serial predecessor, a hook, or the worker failed, and every test it selected in the describe or file, on that target, of a `beforeAll` or `afterAll` that failed (for a setup, the tests that consume its session). A rerun that runs the hook again reports it again if it still fails
|
|
366
|
+
| `--last-failed` | boolean | `false` | Only the tests the previous run did not pass, read from the `report.json` this run overwrites, `<output>/report.json` (`--output`, else the config's [`output`](/reference/config#output)): failed, timed out, interrupted, or skipped because a setup, a serial predecessor, a hook, or the worker failed or `--max-failures` stopped the run first, and every test it selected in the describe or file, on that target, of a `beforeAll` or `afterAll` that failed (for a setup, the tests that consume its session). A rerun that runs the hook again reports it again if it still fails, and a rerun that runs any test of its scope without it failing resolves it. A test another filter (`--grep`, `--tag`, `--target`, `--agent`, a positional) leaves out of the rerun stays owed: the rerun's report carries its last result, and the hook failure whose scope it is in, under `run.carried`, the next `--last-failed` selects it again, and the [`@e2e-dev/github`](/github) page stays red until it passes (⏹️ for a test the run before interrupted). The rerun's own exit code and summary count only what it ran. It keeps the artifacts the previous report names and writes its own under `artifacts/rerun-<n>/`, so no attempt overwrites an earlier one's files. A test is named per target and agent. No report is `NO_LAST_RUN`; a previous run with nothing failed selects nothing, `NO_TESTS` unless `--pass-with-no-tests`. |
|
|
363
367
|
| `--shard <index/total>` | `2/3` | none | Runs one contiguous slice of the selected tests, cut once every other filter applied. Slices differ in size by at most one test, a serial group counts as one and is never split, and each shard brings only the setup tests its own tests need. Every shard runs the same command with a different index; the order is the report order, so the same suite shards the same way on every machine. A shard that gets no test is `NO_TESTS` unless `--pass-with-no-tests`. See [Sharding](/ci#sharding). |
|
|
364
368
|
| `--headed` | boolean | `false` | Visible UI when the engine supports it. |
|
|
365
369
|
| `--agent <names>` | comma-separated or repeated | `default` | Runs unpinned tests once per selected agent. For a test with `{ agent }`, selects the shared names when there is overlap; otherwise keeps the test's selection. An unknown name is `INVALID_CONFIG`. |
|
|
@@ -368,7 +372,7 @@ or every test was filtered out.
|
|
|
368
372
|
| `--repeat-each <n>` | integer, at least 1 | `1` | Runs every selected test this many times. Each run is a result of its own, with `repeat` 0 through `n - 1` and its own `id`; the list reporter titles the later runs `(repeat #1)`, `(repeat #2)`, and their artifacts go under a `repeat-<n>` directory. Setup tests run once. With the replay cache on, the first run records and the later ones replay, so pair it with `--no-cache` to exercise the model each time. The `list` summary adds a `Repeats` row (`1 of 2 tests passed all 5 runs`) and one line per test that did not pass every run, naming each run that failed; the `markdown` page carries the same tally. |
|
|
369
373
|
| `--max-failures <n>` | integer, at least 1 | none | Stops the run once this many tests have failed or timed out. Nothing more is dispatched, the tests running on other workers end as `interrupted`, and the tests not started, the rest of the file whose test reached the limit included, are skipped with cause `failure-limit` and the reason `run stopped after 3 failures (--max-failures 3)`. The exit code is the failures' own, 1, not 130. |
|
|
370
374
|
| `--reporter <ids>` | comma-separated list | config | Replaces the built-in IDs in `config.reporters`: `list`, `json`, `junit`, `markdown`. `json` with `list` is `INVALID_CONFIG`. [Reporter objects](/reference/reporters) in the config keep running. |
|
|
371
|
-
| `--output <dir>` | string | config, else `.e2e` | The results directory for this run, over the config's [`output`](/reference/config#output): the report, the reporter files, the AI trace, `artifacts/` (cleared once the run's tests start), and `sessions/`. Resolved from the project root; one the run cannot own is `INVALID_CONFIG`. The removed `--artifacts <dir>` fails with the `--output` it maps to: `--artifacts out/artifacts` is `--output out`, and a directory whose parent is the project root or outside it maps to `--output .e2e`. |
|
|
375
|
+
| `--output <dir>` | string | config, else `.e2e` | The results directory for this run, over the config's [`output`](/reference/config#output): the report, the reporter files, the AI trace, `artifacts/` (cleared once the run's tests start, down to what the previous report names on a `--last-failed` rerun), and `sessions/`. Resolved from the project root; one the run cannot own is `INVALID_CONFIG`. The removed `--artifacts <dir>` fails with the `--output` it maps to: `--artifacts out/artifacts` is `--output out`, and a directory whose parent is the project root or outside it maps to `--output .e2e`. |
|
|
372
376
|
| `--no-cache` | boolean | `false` | Replay cache off for this run, whatever `config.cache` says. |
|
|
373
377
|
| `--strict-cache` | boolean | `false` | Turns on `cache.strict` for this run: a step whose recording exists but no longer replays fails with `REPLAY_STALE` instead of handing off to the agent. See [Fail on a stale recording](/cache#fail-on-a-stale-recording). |
|
|
374
378
|
| `--pass-with-no-tests` | boolean | `false` | Exit 0 instead of `NO_TESTS` when nothing is selected. |
|
|
@@ -479,8 +483,8 @@ bunx e2e run --reporter list,junit
|
|
|
479
483
|
| --- | --- |
|
|
480
484
|
| `list` | Terminal progress, failures with code frames, and a run summary. On a TTY, live progress includes the current agent step, model turns, tool calls, and reasoning excerpts. Untrusted fields are capped at 8 KiB and stripped of control characters. |
|
|
481
485
|
| `json` | The report document to stdout and nothing else there, so `e2e run --reporter json \| jq` works. Same document as `<output>/report.json`, described by `schema/report-v1.schema.json` in the package. The shape changes only with the `schemaVersion` field (`report-1` today). Cannot be combined with `list`. |
|
|
482
|
-
| `junit` | `junit.xml` beside the report. One `<testsuite>` per file, one `<testcase>` per test-target pair named `title > path [target]`. A test-class error is a `<failure>`, any other an `<error>`, a skip a `<skipped>` with its reason. Run-level errors form a `run` suite. |
|
|
483
|
-
| `markdown` | `summary.md` beside the report with counts, cost, errors, and failure details. Failed and flaky tests also get a page under `failures/` there, with steps, model turns, and the screen at failure. Exploration runs show findings. [`@e2e-dev/github`](/github) uses this summary for pull request comments. |
|
|
486
|
+
| `junit` | `junit.xml` beside the report. One `<testsuite>` per file, one `<testcase>` per test-target pair named `title > path [target]`. A test-class error is a `<failure>`, any other an `<error>`, a skip a `<skipped>` with its reason. An interrupted test reached no verdict, so it is a `<skipped>` whose message starts `interrupted:`. Run-level errors form a `run` suite. |
|
|
487
|
+
| `markdown` | `summary.md` beside the report with counts, cost, errors, and failure details. Failed and flaky tests also get a page under `failures/` there, with steps, model turns, and the screen at failure. Interrupted tests are counted on their own and get no page. Exploration runs show findings. [`@e2e-dev/github`](/github) uses this summary for pull request comments. |
|
|
484
488
|
|
|
485
489
|
Every run that gets to its tests writes `report.json` under the [output directory](/reference/config#output)
|
|
486
490
|
(`.e2e` by default) whatever the reporters are, and the reporters' files
|
|
@@ -490,6 +494,16 @@ fails to start, an interrupt) writes none of them and leaves the previous
|
|
|
490
494
|
run's files in place; its errors are on the terminal and in the exit code. A `junit.xml` or `summary.md` that could not be written is a
|
|
491
495
|
line on stderr, never a run error.
|
|
492
496
|
|
|
497
|
+
`run.summary` in the report counts the results. `discovered` is every
|
|
498
|
+
test-target pair the runner collected and `selected` the ones this run
|
|
499
|
+
chose; the selection's left-out pairs are in `results` as `selected: false`
|
|
500
|
+
but in no other count. `passed`, `failed` (timed out included),
|
|
501
|
+
`interrupted`, `flaky`, and `skipped` count the selected results and add up
|
|
502
|
+
to `selected`. `interrupted` is a test the run stopped before its verdict
|
|
503
|
+
(a signal, `--max-failures`, or a run-level error such as
|
|
504
|
+
`MODEL_UNAVAILABLE`), never a failure. `executed` is the selected
|
|
505
|
+
results that ran at least one attempt.
|
|
506
|
+
|
|
493
507
|
With `--ai-trace`, the run also writes `<output>/ai-trace.json`: one entry per
|
|
494
508
|
model round trip with the exact prompt, tool definitions, response, usage,
|
|
495
509
|
and provider metadata. Observations in it are already redacted. Inline image
|
|
@@ -510,7 +524,10 @@ Signals escalate.
|
|
|
510
524
|
any test starts (during collection, an engine's `prepare`, or app
|
|
511
525
|
startup) writes no report and leaves the previous one in place.
|
|
512
526
|
2. A second signal forces. Workers dispose their engines at once and are
|
|
513
|
-
killed when the cleanup budget is spent. The report is still written
|
|
527
|
+
killed when the cleanup budget is spent. The report is still written,
|
|
528
|
+
and so are `junit.xml` and `summary.md` when the `junit` and
|
|
529
|
+
`markdown` reporters are on; a custom reporter's
|
|
530
|
+
`onRunFinished` is abandoned.
|
|
514
531
|
3. A third signal exits on the spot, after killing the process groups of every
|
|
515
532
|
app command the run started. A process attached through
|
|
516
533
|
`reuseExisting` is left alone.
|
|
@@ -96,6 +96,8 @@ export interface E2EConfig {
|
|
|
96
96
|
cleanupTimeout?: number;
|
|
97
97
|
/** Retries per test, 0 through 10; default 1 in CI, else 0. */
|
|
98
98
|
retries?: number;
|
|
99
|
+
/** Fail the run when a test skips itself after a soft failure or an earlier failed attempt; default false. */
|
|
100
|
+
failOnSkippedFailure?: boolean;
|
|
99
101
|
/** Parallel workers, 1 through 1024; default 1 in CI, else half the cores. An engine may cap it lower. */
|
|
100
102
|
workers?: number;
|
|
101
103
|
/** `{ store }` hands every artifact to a host store as it is produced. */
|
|
@@ -158,6 +160,17 @@ Every numeric value must be a safe integer. `projectId` falls back to
|
|
|
158
160
|
`package.json`. `workers` never exceeds what a target's engine declares. A
|
|
159
161
|
targeted agent action is bounded by the smaller of `actionTimeout` and 15 s.
|
|
160
162
|
|
|
163
|
+
## failOnSkippedFailure
|
|
164
|
+
|
|
165
|
+
Set `failOnSkippedFailure: true` to fail the run with exit code 1 when a test
|
|
166
|
+
skips after a soft assertion failure or skips a retry after an earlier failed
|
|
167
|
+
attempt. The default is `false`.
|
|
168
|
+
|
|
169
|
+
The test keeps its `skipped` status and reason. Its failures remain in the
|
|
170
|
+
report, and the CLI shows them under `Skipped After Failure` with either
|
|
171
|
+
setting. Clean skips, successful retries, and engine cleanup diagnostics do
|
|
172
|
+
not trigger this policy. A skip still stops the body and does not retry.
|
|
173
|
+
|
|
161
174
|
## targets
|
|
162
175
|
|
|
163
176
|
```ts
|
|
@@ -267,8 +280,9 @@ it (no `app.command`) is `INVALID_CONFIG`. An `app.command` with neither
|
|
|
267
280
|
|
|
268
281
|
### The app's site
|
|
269
282
|
|
|
270
|
-
A test or the agent may open any http(s) URL
|
|
271
|
-
|
|
283
|
+
A test or the agent may open any http(s) URL, and `about:blank`; every
|
|
284
|
+
other scheme (`file:`, `data:`, `javascript:`, `view-source:`, `blob:`) is
|
|
285
|
+
`POLICY_DENIED`. The site of `url`, its registrable domain
|
|
272
286
|
(`app.example.com` and `auth.example.com` are one site), scopes two things:
|
|
273
287
|
the browser engine's [`headers`](/reference/web#options) reach the
|
|
274
288
|
site and no other host, and child frames off the site stay out of
|
|
@@ -369,7 +383,7 @@ run.
|
|
|
369
383
|
| `report.json` | The canonical report; `--last-failed` reads the one here |
|
|
370
384
|
| `junit.xml`, `summary.md`, `failures/` | The `junit` and `markdown` reporters' files |
|
|
371
385
|
| `ai-trace.json` | `--ai-trace` |
|
|
372
|
-
| `artifacts/` | Screenshots, traces, videos, downloads, and logs, one directory per attempt. Cleared once a run's tests start, so it holds the latest run's evidence and nothing its report no longer names |
|
|
386
|
+
| `artifacts/` | Screenshots, traces, videos, downloads, and logs, one directory per attempt. Cleared once a run's tests start, so it holds the latest run's evidence and nothing its report no longer names. A `--last-failed` rerun keeps the files the report it reruns names and writes its own attempts under `artifacts/rerun-<n>/` |
|
|
373
387
|
| `sessions/` | The run's encrypted `test.setup` sessions, deleted when it ends |
|
|
374
388
|
| `videos/<session>/` | [`e2e mcp`](/reference/mcp#recording) session recordings |
|
|
375
389
|
|
|
@@ -754,8 +768,9 @@ export interface ModelInstance {
|
|
|
754
768
|
}
|
|
755
769
|
```
|
|
756
770
|
|
|
757
|
-
Construct the model in your config with an AI SDK provider.
|
|
758
|
-
|
|
771
|
+
Construct the model in your config with an AI SDK provider. You can use all
|
|
772
|
+
providers that support tool calls, see [AI SDK providers](/models#ai-sdk-providers).
|
|
773
|
+
For subscription sign-in, use the [`e2e/oauth/*` models](/subscriptions).
|
|
759
774
|
|
|
760
775
|
| Gateway | Constructor | Package | Reads |
|
|
761
776
|
| --- | --- | --- | --- |
|
|
@@ -798,7 +813,9 @@ Credentials and secrets are separate namespaces: `secrets.get(name)` returns a
|
|
|
798
813
|
reports know that handle as `<name>.password` (`<secret:admin.password>`), so
|
|
799
814
|
`credentials.admin` and `secrets.admin` can both exist; a `secrets` entry
|
|
800
815
|
named exactly `admin.password` beside an `admin` credential is
|
|
801
|
-
`INVALID_CONFIG`.
|
|
816
|
+
`INVALID_CONFIG`. So are two credentials whose names map to one
|
|
817
|
+
`E2E_USER_<NAME>_*` override, like `svc-a` and `svc_a` (see
|
|
818
|
+
[Environment variables](/reference/environment)).
|
|
802
819
|
|
|
803
820
|
```ts
|
|
804
821
|
/** Named test identity. The password remains an opaque Secret. */
|
|
@@ -824,7 +841,8 @@ export type SecretConfig = string | SecretProvider;
|
|
|
824
841
|
|
|
825
842
|
An entry is a static value, or a provider called on every authorized fill (a
|
|
826
843
|
vault lookup, a freshly computed TOTP). It is recorded in the config digest by
|
|
827
|
-
name only, and an `E2E_SECRET_<NAME>` override wins over both.
|
|
844
|
+
name only, and an `E2E_SECRET_<NAME>` override wins over both. Two entries whose
|
|
845
|
+
names map to one variable, like `api-key` and `api_key`, are `INVALID_CONFIG`. Every entry
|
|
828
846
|
must resolve to a string of at least 6 characters (code points) or a
|
|
829
847
|
function; anything else is `INVALID_CONFIG`. The minimum exists for redaction: every occurrence
|
|
830
848
|
of a value becomes `<secret:name>`, and a shorter value would take ordinary
|
|
@@ -960,7 +960,7 @@ Use these helpers to keep behavior consistent across engines:
|
|
|
960
960
|
| --- | --- |
|
|
961
961
|
| `matchesText(actual, pattern)`, `toTextPattern(match, { exact? })`, `describePattern(pattern)` | `TextPattern` matching for `locate` and text matchers; `toTextPattern` throws `INVALID_ARGUMENT` for a `match` that is neither a string nor a `RegExp` |
|
|
962
962
|
| `resolveExpression(expression, nodes, { selector? })` | The reference `locate`. Resolves one `LocatorExpression` over a semantic tree in document order, with `text` and `label` answering the innermost match when a container echoes a descendant's text. An engine whose tree is the whole truth (a device) calls it; a browser engine reproduces it. A `selector` needs the hook, a `frame` is `FRAME_NOT_FOUND` |
|
|
963
|
-
| `urlMatches(current, expected, baseHref)` | `toHaveURL`-style comparison; an `expected` that is neither a string nor a `RegExp` throws `INVALID_ARGUMENT`, as in `toTextPattern` |
|
|
963
|
+
| `urlMatches(current, expected, baseHref, ignoreCase?)` | `toHaveURL`-style comparison, `ignoreCase` as on the locator matchers; an `expected` that is neither a string nor a `RegExp` throws `INVALID_ARGUMENT`, as in `toTextPattern` |
|
|
964
964
|
| `pollCondition({ deadline, signal, negated, evaluate, onTimeout })`, `Deadline` | Assertion polling and the negation grace window |
|
|
965
965
|
| `validateJsonValue(value, label, rules?)` | Checks a value is plain JSON: a top-level `undefined` passes; a non-finite number, a cycle, an object that is not a plain object or array, or a function, symbol, bigint, or `undefined` inside is `INVALID_ARGUMENT` naming `label`. A `Secret` is `POLICY_DENIED` unless `rules.onSecret` throws its own; `rules.maxDepth` caps the nesting |
|
|
966
966
|
| `rejectUnknownKeys(label, value, keys, code?)` | Throws `INVALID_CONFIG` for the first own key of `value` outside `keys`, naming the nearest known key when one is a plausible typo (`web() has unknown key "viewprt"; did you mean "viewport"?`) and the known keys otherwise: the check the runner runs on its own config objects, for an engine factory's options. Pass `'INVALID_ARGUMENT'` as `code` for an object a test passes to a fixture method |
|
|
@@ -1185,8 +1185,8 @@ export interface EngineFixtureContext {
|
|
|
1185
1185
|
/**
|
|
1186
1186
|
* Resolves a navigation target against the base URL. Throws
|
|
1187
1187
|
* `APP_URL_REQUIRED` when the target declares no URL and `POLICY_DENIED`
|
|
1188
|
-
* for
|
|
1189
|
-
* re-implements the rule the harness owns.
|
|
1188
|
+
* for any scheme but `http:` and `https:` (`about:blank` aside), so a
|
|
1189
|
+
* fixture never re-implements the rule the harness owns.
|
|
1190
1190
|
*/
|
|
1191
1191
|
resolveUrl(url: string): string;
|
|
1192
1192
|
};
|
|
@@ -21,6 +21,7 @@ See [Models](/models) and [Subscriptions](/subscriptions).
|
|
|
21
21
|
| Variable | Reading | Effect |
|
|
22
22
|
| --- | --- | --- |
|
|
23
23
|
| `E2E_OAUTH_CREDENTIALS` | value | The stored subscription logins as JSON, the contents of `oauth.json`, for a machine nobody can sign in on. The `e2e/oauth/*` models and `e2e models` read it in place of `$XDG_CONFIG_HOME/e2e/oauth.json`; `e2e login` and `e2e logout` refuse to change it, and refreshed tokens are not written back |
|
|
24
|
+
| `OPENCODE_API_KEY` | value | An OpenCode Console service account key; `opencodeConsole()` uses it in place of the stored `opencode-console` login; `e2e models` still reads the stored login. OpenCode reads the same variable |
|
|
24
25
|
| `XDG_CONFIG_HOME` | value | Directory of the stored logins, `$XDG_CONFIG_HOME/e2e/oauth.json`; default `~/.config/e2e/oauth.json` |
|
|
25
26
|
|
|
26
27
|
## Credentials and secrets
|
|
@@ -32,7 +33,13 @@ See [Models](/models) and [Subscriptions](/subscriptions).
|
|
|
32
33
|
| `E2E_SECRET_<NAME>` | set | Replaces the value of `secrets.<name>` |
|
|
33
34
|
|
|
34
35
|
`<NAME>` is the name uppercased with every character outside `A-Z0-9`
|
|
35
|
-
replaced by `_`, so `power-user` becomes `E2E_USER_POWER_USER_USERNAME`.
|
|
36
|
+
replaced by `_`, so `power-user` becomes `E2E_USER_POWER_USER_USERNAME`. Two
|
|
37
|
+
credentials, or two secrets, whose names map to the same `<NAME>` (`api-key`
|
|
38
|
+
and `api_key` both read `E2E_SECRET_API_KEY`; `svc-a` and `svc_a` both read
|
|
39
|
+
`E2E_USER_SVC_A_USERNAME` and `_PASSWORD`) are `INVALID_CONFIG` at config
|
|
40
|
+
load, whether or not a variable is set, so one rotation never replaces two
|
|
41
|
+
entries; the message names the entries and the variables. A credential and a
|
|
42
|
+
secret may share a name, since their prefixes differ. A
|
|
36
43
|
variable for a name absent from the config is ignored. An empty value is
|
|
37
44
|
`INVALID_CONFIG`, and so is a password or secret shorter than 6 characters
|
|
38
45
|
(code points): redaction rewrites every occurrence of a value, and one that
|
|
@@ -56,7 +56,7 @@ try {
|
|
|
56
56
|
| `TEST_SETUP_FAILED` | Test prerequisites outside the product failed to arrange (blocked: test_setup). Also a `test.extend()` fixture that broke its contract: returned without calling `use()`, called it twice, or redefined a fixture the target's engine contributes | configuration | 2 |
|
|
57
57
|
| `AUTOMATION_UNSUPPORTED` | The step needs an interaction the available toolset cannot perform, e.g. drag-and-drop (blocked: automation) | test | 1 |
|
|
58
58
|
| `MODEL_UNAVAILABLE` | No model configured: the built-in agent's `agents.<name>.model` holds no AI SDK model instance; the message names the agent that was selected. Checked once per run when the `agent` fixture is first acquired; recorded as one run-level error that stops the run. `e2e explore` checks before anything starts, a custom executor's entry included, since explore runs the built-in agent. A custom executor without a model is not checked: its `act` and `assert` steps run, and `waitFor` or `extract` fails with this code on its first call. Evicts no [recording](/cache). | configuration | 2 |
|
|
59
|
-
| `POLICY_DENIED` | A denied destination (`
|
|
59
|
+
| `POLICY_DENIED` | A denied destination (a navigation scheme other than `http:` or `https:`, `about:blank` aside, a malformed URL), a forbidden fill (an undeclared secret, a non-editable sink, a password into a field that is not a password field), or pixel evidence after a secret fill (`vision: 'only'` with pixels withheld). Also reading a value, text, or attribute from a secure field, the getters and the `toHaveValue`, `toHaveText`, `toContainText`, and `toHaveAttribute` matchers alike. An unknown tool or an undeclared field is refused to the model as the call's result and is not this code | configuration | 2 |
|
|
60
60
|
| `APP_UNREACHABLE` | The app URL could not be reached, or an app command exited early or exceeded `startupTimeout`. The startup error includes recent log output with `command.env` values redacted, and suggests adding a `log` if none is configured. Under the agent, also any engine fault with no more specific code: on a device, an iOS automation runner that is busy or wedged reports here with a message that names the runner, the session and device, and the recovery; see [Testing iOS and Android](/mobile#troubleshooting) | infrastructure | 3 |
|
|
61
61
|
| `APP_ALREADY_RUNNING` | `readyUrl` already answered before the target's `app.command` was spawned and `reuseExisting` was not in effect; stop that process or set `reuseExisting: true` locally | infrastructure | 3 |
|
|
62
62
|
| `MODEL_PROVIDER_FAILED` | Provider transport failure (network error, 5xx, rate limit, or a request with no response within 120 s) after the adapter's transport retries (up to 5 per model call, honoring `retry-after`, bounded by the remaining step timeout). A request that stalls is aborted at 120 s and sent again, and an `agent.act` step's `--debug` transcript notes it. The bound covers the whole request, so a model that needs longer than 120 s for one answer fails here too. Evicts no [recording](/cache) | infrastructure | 3 |
|
|
@@ -73,7 +73,7 @@ try {
|
|
|
73
73
|
| `ASSERTION_INCONCLUSIVE` | `agent.assert` could not judge: the screen did not show enough to decide either way (the relevant part is not on screen, still loading, or unreadable). `agent.extract` raises it when the model answers that the screen does not show the requested data, instead of returning a placeholder. A failure, never a pass; the explanation says what was missing. Judged from the tree alone with `vision` off, the message adds `pass vision: true when the answer is in pixels`, as a condition (`if the engine captures pixels, ...`) until a step of the attempt has received pixels; no hint when the engine declares no screenshot capture, a secret was filled, or an earlier step's pixel request was degraded | test | 1 |
|
|
74
74
|
| `AUTHENTICATION_FAILED` | The executor concluded that signing in failed inside the app: a product verdict. `AUTH_CREDENTIAL_INVALID` is the blocked counterpart for a configured credential the app rejects | test | 1 |
|
|
75
75
|
| `STEP_NO_CONCLUSION` | The executor used every turn without calling `complete_step`; the step fails with the agent's last summary | test | 1 |
|
|
76
|
-
| `REPLAY_STALE` | Under `cache.strict` or `--strict-cache`, the step's recording exists but no longer replays (`step.cache.reason` is one of `invalid-entry`, `wrong-context`, `target-not-found`, `target-ambiguous`, `viewport-changed`, `action-failed`, `action-uncertain`, `end-mismatch`), so the step fails with no model call instead of handing off to the agent. Not retried: a retry never replays and would pass live. Re-record with a read-write run without the flag, and with `cache.strict` off when the config set it, then commit the changed entry; the message names the knob that turned strict on and the configured cache directory (or `cache.store`) | configuration | 2 |
|
|
76
|
+
| `REPLAY_STALE` | Under `cache.strict` or `--strict-cache`, the step's recording exists but no longer replays (`step.cache.reason` is one of `invalid-entry`, `wrong-context`, `target-not-found`, `target-ambiguous`, `viewport-changed`, `action-failed`, `action-uncertain`, `end-mismatch`, or `no-entry` while the cache directory, not a custom `cache.store`, holds a recording of the step under another key, which the message names), so the step fails with no model call instead of handing off to the agent. Not retried: a retry never replays and would pass live. Re-record with a read-write run without the flag, and with `cache.strict` off when the config set it, then commit the changed entry; the message names the knob that turned strict on and the configured cache directory (or `cache.store`) | configuration | 2 |
|
|
77
77
|
|
|
78
78
|
## Runner codes
|
|
79
79
|
|
|
@@ -84,7 +84,7 @@ These appear in terminal output and in the report as plain string codes.
|
|
|
84
84
|
| `CONFIG_NOT_FOUND` | `--config <path>` names a file that does not exist, or no `e2e.config.ts` or `e2e.config.mts` exists from the working directory up to the repository root | configuration | 2 |
|
|
85
85
|
| `CONFIG_AMBIGUOUS` | Both `e2e.config.ts` and `e2e.config.mts` in one directory | configuration | 2 |
|
|
86
86
|
| `CONFIG_LOAD_FAILED` | The config module failed to import: a package that is declared but not installed, an undeclared package, a wrong subpath, a removed export, or any other throw while it evaluates. The message names the fix. A config error raised while it evaluates keeps its own code: an option a factory the config calls (`web()`, `mobile()`, `defineEngine()`) refuses, or a `secrets.get()` handle turned into a string, is `INVALID_CONFIG` | configuration | 2 |
|
|
87
|
-
| `INVALID_CONFIG` | A factory the config module calls (`web()`, `mobile()`, `defineEngine()`) refused its options, with the factory's own message; or an unknown key, out-of-range value, invalid enum, missing `targets`, a target key outside `{ name, platform, engine, app, trace, video }`, a target `app` key that is unknown or malformed, `app` on a target without an engine, a `web()` target without `app.url` or with a device field, a `mobile()` target without `app.bundleId` or `app.appPath`, with `app.url` (not supported yet), a link in `bundleId`, or an unknown permission name or state (the message names the nearest name), an unknown key in a `web()` or `mobile()` option, including `web({ connect })` and `web({ basicAuth })` (the message names the nearest one), a `trace` or `video` that is not a recording mode, the removed artifact kinds list (`artifacts: [...]` or `artifacts.kinds`) and `artifacts.trace` block (the message names the `trace` mode they meant), the removed `artifacts.video` block or `video` artifact kind (the message names the `video` option), the removed `specVersion` key, a `secrets.get()` in an engine option that names no `secrets` entry (a credential's password is not one), a `secrets.get()` reference turned into a string while the config evaluates (a template literal, `String()`) or given where a string is needed (an `app.command` `args` entry or `env` value; only an engine option that declares secrets takes one), an `app.command` `args` entry or `env` value that is not a string (an `undefined` `env` value is dropped instead), an unknown key in `app.command` (the message names the nearest one), an `engine` that is not a `defineEngine` handle, `json` reporter combined with `list`, duplicate or malformed target name, a secret or credential password shorter than 6 characters (code points), oversized `agents.<name>.context`, an `expect.poll` `timeout` or `interval` that is not a finite number of milliseconds, a `tests` value that is not a glob or a list of globs, a `tests` list of only `!` exclusions, the removed top-level `limits` key, an agents entry that is a bare `StepExecutor` (the message names `{ executor }`) or a model instance (the message names `{ model }`), an unknown agent key (the message names the nearest one), the removed agent keys `timeout` and `maxTurns` (the message names `judgmentTimeout` and `maxModelCalls`), `system` or `tools` beside `executor`, a `tools` entry `defineTool` did not build or that takes a built-in tool name (a grammar verb, `complete_step`, `locate`, `start_recording`, `stop_recording`, or `report_finding`), `createToolLoopExecutor({ maxTurns })`, `reuseExisting` on an app whose `app.url` asks for a free port, or a port-0 `app.url` with no `app.command` to serve it | configuration | 2 |
|
|
87
|
+
| `INVALID_CONFIG` | A factory the config module calls (`web()`, `mobile()`, `defineEngine()`) refused its options, with the factory's own message; or an unknown key, out-of-range value, invalid enum, missing `targets`, a target key outside `{ name, platform, engine, app, trace, video }`, a target `app` key that is unknown or malformed, `app` on a target without an engine, a `web()` target without `app.url` or with a device field, a `mobile()` target without `app.bundleId` or `app.appPath`, with `app.url` (not supported yet), a link in `bundleId`, or an unknown permission name or state (the message names the nearest name), an unknown key in a `web()` or `mobile()` option, including `web({ connect })` and `web({ basicAuth })` (the message names the nearest one), a `trace` or `video` that is not a recording mode, the removed artifact kinds list (`artifacts: [...]` or `artifacts.kinds`) and `artifacts.trace` block (the message names the `trace` mode they meant), the removed `artifacts.video` block or `video` artifact kind (the message names the `video` option), the removed `specVersion` key, a `secrets.get()` in an engine option that names no `secrets` entry (a credential's password is not one), a `secrets.get()` reference turned into a string while the config evaluates (a template literal, `String()`) or given where a string is needed (an `app.command` `args` entry or `env` value; only an engine option that declares secrets takes one), an `app.command` `args` entry or `env` value that is not a string (an `undefined` `env` value is dropped instead), an unknown key in `app.command` (the message names the nearest one), an `engine` that is not a `defineEngine` handle, `json` reporter combined with `list`, duplicate or malformed target name, a secret or credential password shorter than 6 characters (code points), two credentials or two secrets whose names map to one `E2E_USER_*` or `E2E_SECRET_*` override variable (`api-key` and `api_key`), oversized `agents.<name>.context`, an `expect.poll` `timeout` or `interval` that is not a finite number of milliseconds, a `tests` value that is not a glob or a list of globs, a `tests` list of only `!` exclusions, the removed top-level `limits` key, an agents entry that is a bare `StepExecutor` (the message names `{ executor }`) or a model instance (the message names `{ model }`), an unknown agent key (the message names the nearest one), the removed agent keys `timeout` and `maxTurns` (the message names `judgmentTimeout` and `maxModelCalls`), `system` or `tools` beside `executor`, a `tools` entry `defineTool` did not build or that takes a built-in tool name (a grammar verb, `complete_step`, `locate`, `start_recording`, `stop_recording`, or `report_finding`), `createToolLoopExecutor({ maxTurns })`, `reuseExisting` on an app whose `app.url` asks for a free port, or a port-0 `app.url` with no `app.command` to serve it | configuration | 2 |
|
|
88
88
|
| `CONFIG_NOT_DETERMINISTIC` | A worker re-resolved the config module and got a different digest than the runner | configuration | 2 |
|
|
89
89
|
| `APP_URL_REQUIRED` | A navigation or relative URL on a target that declares no `app.url`, or an `app.command` with no `app.url` or `app.readyUrl` to poll | configuration | 2 |
|
|
90
90
|
| `INVALID_APP_URL` | Base URL with userinfo, a query, a fragment, a non-HTTP scheme, plain HTTP on a non-loopback host, port 0 on a host other than a literal loopback address (`127.0.0.1`, `[::1]`), a loopback address the run could not bind a free port on, or unparseable | configuration | 2 |
|
|
@@ -104,14 +104,14 @@ These appear in terminal output and in the report as plain string codes.
|
|
|
104
104
|
| `NO_LAST_RUN` | `--last-failed` found no `report.json` where the run writes it, or a file there that is not a `report-1` document. Run once without the flag first. | configuration | 2 |
|
|
105
105
|
| `ONLY_IN_CI` | A focused test reached a CI run | configuration | 2 |
|
|
106
106
|
| `COLLECTION_ERROR` | Registration outside collection, an invalid title, a non-function body, an async `describe` body, nested serial groups, an option forbidden inside a serial group, a setup test inside `describe`, an invalid session name, a malformed test option such as `tags` that is not a list of distinct names, a test or `describe` option key the call does not take (the message names the nearest one), a test file that fails to import (in a run with no positionals, or a file the positionals selected), a session that a selected test consumes or `e2e explore --session` names but no collected setup test produces (the message lists the declared sessions), a consumed session on a target whose engine has no `state` capability to save it, or a positional path outside the project root | configuration | 2 |
|
|
107
|
-
| `POLICY_DENIED` | A denied destination (`
|
|
107
|
+
| `POLICY_DENIED` | A denied destination (a navigation scheme other than `http:` or `https:`, `about:blank` aside, a malformed URL), a forbidden fill (an undeclared secret, a non-editable sink, a mismatched purpose, a `Secret` reaching a JSON sink), or tainted pixels (`app.screenshot()` after a secret fill, `vision: 'only'` with pixels withheld); see the [security model](/security). An unknown tool or an undeclared field goes back to the model as the call's result, never this code | configuration | 2 |
|
|
108
108
|
| `SESSION_MISMATCH` | A stored session does not match the current run, target, engine, platform, or app identity | configuration | 2 |
|
|
109
109
|
| `SESSION_EXPIRED` | A stored session is past its expiry at restore time (configuration, exit 2), or a setup test saved state whose expiry is not after its creation (test, exit 1) | configuration or test | 2 or 1 |
|
|
110
110
|
| `SESSION_INVALID` | A stored session failed authentication. Tampered or corrupt. | configuration | 2 |
|
|
111
111
|
| `SESSION_UNAVAILABLE` | A test requires a session no selected setup test produced | configuration | 2 |
|
|
112
112
|
| `SESSION_CONTRACT` | A setup test saved a session twice, saved an undeclared name, or finished without saving a declared one | test | 1 |
|
|
113
113
|
| `TEST_TIMEOUT` | The attempt exceeded the resolved test `timeout`. The body is not stopped: the harness operation it is in is cancelled and its fixtures die with the attempt, so it cannot reach the next test | test | 1 |
|
|
114
|
-
| `STEP_NOT_AWAITED` | The test body settled while a step it started was still running: a step call without `await`. The step is failed at the line of the call and cancelled; the attempt fails with this code when the body returned, or keeps the body's own error and records this one under `secondaryErrors` when the body threw | test | 1 |
|
|
114
|
+
| `STEP_NOT_AWAITED` | The test body settled while a step it started was still running: a step call without `await`. The step is failed at the line of the call and cancelled; the attempt fails with this code when the body returned, or keeps the body's own error and records this one under `secondaryErrors` when the body threw. An `expect.poll` matcher left running when the body, an `afterEach` hook, or a fixture teardown returned fails that phase the same way at the line of the call, and is cancelled; one left by a `beforeAll` or `afterAll` hook fails the hook with `HOOK_FAILED` | test | 1 |
|
|
115
115
|
| `UNHANDLED_REJECTION` | A promise rejected with no test running. An unhandled rejection is charged to the test running when it surfaces, since Node cannot say which test created the promise; between tests it is this run error, naming the last test that finished in the worker as the likely source. Before any test has finished in the worker it ends the worker instead: the rejection is the run error and the test on its way fails with `WORKER_CRASH`. The body that left it keeps running like one past `TEST_TIMEOUT` | test | 1 |
|
|
116
116
|
| `HOOK_FAILED` | `beforeAll` or `afterAll` threw. Skips the suite's tests. | test | 1 |
|
|
117
117
|
| `LAUNCH_TIMEOUT` | Engine init or attempt start exceeded `config.launchTimeout` | infrastructure | 3 |
|
|
@@ -52,14 +52,17 @@ before passing its result to `expect`.
|
|
|
52
52
|
| Default timeout | `config.assertionTimeout`, 5000 ms |
|
|
53
53
|
| Override | `{ timeout }` in milliseconds, on every locator and `browser` matcher |
|
|
54
54
|
| Unknown option | `INVALID_ARGUMENT` before the first read, from JavaScript too |
|
|
55
|
-
| Negation grace window | A negated matcher passes after 1000 ms of continuous truth, or
|
|
55
|
+
| Negation grace window | A negated matcher passes after 1000 ms of continuous truth, or at the deadline when `{ timeout }` is shorter than that and the negation held for the whole budget. The window counts from the moment the first read that saw the negation started, so a slow read counts toward it |
|
|
56
56
|
| Failure code | `ASSERTION_FAILED`, exit code 1 |
|
|
57
57
|
|
|
58
58
|
Locator strictness: except for `toHaveCount` and the list form of
|
|
59
59
|
`toHaveText` and `toContainText`, a positive matcher requires exactly one
|
|
60
60
|
match; zero matches keep polling until the timeout, and multiple matches fail
|
|
61
|
-
immediately with `LOCATOR_AMBIGUOUS`. `toBeHidden`,
|
|
62
|
-
|
|
61
|
+
immediately with `LOCATOR_AMBIGUOUS`. `toBeHidden`, `toBeVisible({ visible:
|
|
62
|
+
false })`, `toBeAttached({ attached: false })`, negated visibility, and
|
|
63
|
+
negated `toBeAttached` accept zero matches; for them, and for
|
|
64
|
+
`waitFor({ state: 'detached' })` and `'hidden'`, a locator under a frame that
|
|
65
|
+
is not in the document is zero matches rather than a wait for the frame.
|
|
63
66
|
|
|
64
67
|
Text comparison is normalized: leading and trailing whitespace is trimmed and
|
|
65
68
|
each run of whitespace collapses to one space. `toHaveText` and
|
|
@@ -201,8 +204,8 @@ Every matcher call is a recorded `expect.<matcher>` assertion step.
|
|
|
201
204
|
export interface BrowserExpectation {
|
|
202
205
|
/** Inverts the matcher. */
|
|
203
206
|
readonly not: BrowserExpectation;
|
|
204
|
-
/** Waits for the current URL to match. */
|
|
205
|
-
toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise<void>;
|
|
207
|
+
/** Waits for the current URL to match; `ignoreCase` compares a string case-insensitively and adds the `i` flag to a RegExp, `false` removes it. */
|
|
208
|
+
toHaveURL(expected: string | RegExp, options?: { ignoreCase?: boolean; timeout?: number }): Promise<void>;
|
|
206
209
|
/** Waits for the current title to match. */
|
|
207
210
|
toHaveTitle(expected: TextMatch, options?: { timeout?: number }): Promise<void>;
|
|
208
211
|
/** Waits for the target's `class` attribute to match: a string is the whole normalized class list, a RegExp is tested against it. */
|
|
@@ -212,9 +215,10 @@ export interface BrowserExpectation {
|
|
|
212
215
|
|
|
213
216
|
A relative `toHaveURL` string resolves against the base URL and matches
|
|
214
217
|
exactly after normalization; a `RegExp` tests the full URL. Anything else,
|
|
215
|
-
such as a predicate function, is `INVALID_ARGUMENT`.
|
|
216
|
-
`
|
|
217
|
-
`
|
|
218
|
+
such as a predicate function, is `INVALID_ARGUMENT`. `toHaveURL` also takes
|
|
219
|
+
Playwright's `ignoreCase`, with the locator matchers' semantics. Otherwise each
|
|
220
|
+
`browser` matcher takes `timeout` only; any other key, such as `ignoreCase` on
|
|
221
|
+
`toHaveTitle`, which Playwright does not take either, is `INVALID_ARGUMENT`.
|
|
218
222
|
|
|
219
223
|
`toHaveClass` polls for the node too: a target that matches nothing yet is one
|
|
220
224
|
more poll, not a failure, until the assertion timeout runs out. An ambiguous
|
|
@@ -326,8 +330,11 @@ side is refused.
|
|
|
326
330
|
`toHaveProperty` walks a dotted path or an array of keys. Use the array form
|
|
327
331
|
when a key contains a dot, or to index an array by number
|
|
328
332
|
(`['lines', 0, 'sku']`). Inherited properties count; a property holding
|
|
329
|
-
`undefined` exists.
|
|
330
|
-
|
|
333
|
+
`undefined` exists. A string, number, or boolean on the path is read through
|
|
334
|
+
its wrapper object, as in Jest, so `toHaveProperty('label.length', 3)` and
|
|
335
|
+
`toHaveProperty(['label', 0], 'a')` reach into a string; `null` and
|
|
336
|
+
`undefined` have no properties. With a second argument the value at the path
|
|
337
|
+
must equal it by `toEqual` rules.
|
|
331
338
|
|
|
332
339
|
### Matching a schema
|
|
333
340
|
|
|
@@ -455,11 +462,15 @@ ASSERTION_FAILED: 2 soft assertions failed
|
|
|
455
462
|
A soft locator or `browser` matcher is still a recorded `expect.<matcher>` step,
|
|
456
463
|
marked failed, so the report shows where in the flow it landed. A soft value
|
|
457
464
|
matcher is not a step, the same as a hard one; its failure appears in the
|
|
458
|
-
|
|
465
|
+
attempt's error records.
|
|
459
466
|
|
|
460
467
|
When the body throws on its own, or times out, that error stays the
|
|
461
468
|
attempt's verdict and the soft failures are recorded beside it under
|
|
462
|
-
`secondaryErrors`. A body that skips itself with `test.skip(...)`
|
|
469
|
+
`secondaryErrors`. A body that skips itself with `test.skip(...)` keeps them
|
|
470
|
+
there too. The CLI shows `Skipped After Failure`; set
|
|
471
|
+
[`failOnSkippedFailure: true`](/reference/config) to make that run fail with
|
|
472
|
+
exit code 1. Without that option, the test stays skipped and does not affect
|
|
473
|
+
the exit code.
|
|
463
474
|
|
|
464
475
|
A soft `toMatchSchema` returns the schema's output when the value matches
|
|
465
476
|
and `undefined` after a kept failure, and its type says so.
|
|
@@ -544,3 +555,14 @@ await expect.poll(() => readStatus(), { message: 'the batch never finished' }).n
|
|
|
544
555
|
|
|
545
556
|
`expect.poll` is not recorded as a report step; a timeout surfaces only as
|
|
546
557
|
the test's failure.
|
|
558
|
+
|
|
559
|
+
Await every poll. A poll belongs to the phase that started it: the test body
|
|
560
|
+
(with its fixtures and `beforeEach` hooks), one `afterEach` hook or fixture
|
|
561
|
+
teardown, or one `beforeAll` or `afterAll` hook. When that phase returns while
|
|
562
|
+
the poll still runs, the poll is cancelled and the phase fails with
|
|
563
|
+
`STEP_NOT_AWAITED` at the line of the call, whether or not the poll would
|
|
564
|
+
have passed. It never fails a later test: a poll started by work its phase
|
|
565
|
+
left running, such as a body past its timeout, is cancelled before its first
|
|
566
|
+
read. A body that threw keeps its own
|
|
567
|
+
error and records this one under `secondaryErrors`, and a suite hook reports
|
|
568
|
+
it as `HOOK_FAILED`.
|
|
@@ -3,7 +3,8 @@ title: Mobile
|
|
|
3
3
|
description: Mobile engine options and the device fixture for iOS and Android.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
`@e2e-dev/mobile` drives iOS simulators
|
|
6
|
+
`@e2e-dev/mobile` drives iOS simulators, Android emulators, and connected
|
|
7
|
+
phones through
|
|
7
8
|
[agent-device](https://github.com/callstack/agent-device). `agent`, `app`,
|
|
8
9
|
`screen`, and `expect` work as they do everywhere; this page covers what is
|
|
9
10
|
specific to a device target. Import `test` from the package to have `device`
|
|
@@ -79,8 +80,9 @@ export interface MobileOptions {
|
|
|
79
80
|
/** Platform the target's device runs. */
|
|
80
81
|
readonly platform: MobilePlatform;
|
|
81
82
|
/**
|
|
82
|
-
*
|
|
83
|
-
* serial (`emulator-5554`)
|
|
83
|
+
* Device to use: a simulator or emulator by name, simulator UDID, or
|
|
84
|
+
* emulator serial (`emulator-5554`), or a connected phone by name. A list
|
|
85
|
+
* is a pool: the
|
|
84
86
|
* engine declares one worker per entry and worker slot `n` drives the
|
|
85
87
|
* `n`th, so `workers` at or above the pool size runs the target's files
|
|
86
88
|
* across every device at once. Omitted, the pool is every booted device of
|
|
@@ -278,8 +280,8 @@ export interface Device {
|
|
|
278
280
|
* `relaunch` restarts it fresh. `launchArguments` and `permissions` apply
|
|
279
281
|
* to this launch alone; the target's configured ones apply to a `relaunch`
|
|
280
282
|
* of the pinned app, and to no foreground-only open. A link is
|
|
281
|
-
* `INVALID_ARGUMENT` (`file:`, `data:`,
|
|
282
|
-
* `POLICY_DENIED`): `openLink` opens one.
|
|
283
|
+
* `INVALID_ARGUMENT` (`file:`, `data:`, `javascript:`, `view-source:`,
|
|
284
|
+
* `blob:`, and `filesystem:` are `POLICY_DENIED`): `openLink` opens one.
|
|
283
285
|
*/
|
|
284
286
|
openApp(app: string, options?: OpenAppOptions): Promise<void>;
|
|
285
287
|
/**
|
|
@@ -289,8 +291,8 @@ export interface Device {
|
|
|
289
291
|
* a web link the app does not claim goes to Safari, and the next
|
|
290
292
|
* observation brings the app back. Android starts the link on that
|
|
291
293
|
* package. Without an app, Android lets the OS route the link; iOS needs
|
|
292
|
-
* one (`INVALID_ARGUMENT` without). `file:`, `data:`,
|
|
293
|
-
* links are `POLICY_DENIED`.
|
|
294
|
+
* one (`INVALID_ARGUMENT` without). `file:`, `data:`, `javascript:`,
|
|
295
|
+
* `view-source:`, `blob:`, and `filesystem:` links are `POLICY_DENIED`.
|
|
294
296
|
*/
|
|
295
297
|
openLink(url: string, options?: { app?: string }): Promise<void>;
|
|
296
298
|
/**
|
|
@@ -426,7 +428,10 @@ session opened. Android reads the device's foreground activity.
|
|
|
426
428
|
`installApp` installs a build from inside a test or a fixture: the run's build once per device (no path installs `app.appPath`), or another build for an upgrade path. A plain
|
|
427
429
|
install keeps the app's data; `reinstall: true` removes the app named by
|
|
428
430
|
`app` (default: the pinned app) first. It resolves to the id to `openApp`
|
|
429
|
-
the build by
|
|
431
|
+
the build by: the bundle id or package agent-device read from the build, else
|
|
432
|
+
the `app` passed in. With no reported id and no `app`, the install is
|
|
433
|
+
`ENGINE_FAILURE` instead of a file path posing as an id; see
|
|
434
|
+
[Troubleshooting](/mobile#troubleshooting).
|
|
430
435
|
|
|
431
436
|
```ts
|
|
432
437
|
const { app } = await device.installApp('./build/MyApp.app', { reinstall: true });
|
|
@@ -444,9 +449,9 @@ name (`Settings`); `relaunch: true` restarts it fresh, and `launchArguments`
|
|
|
444
449
|
and `permissions` apply to that launch alone (the target's own apply to a
|
|
445
450
|
`relaunch` of the pinned app). It takes no link:
|
|
446
451
|
agent-device opens any `scheme:rest` string as a URL, so a link is refused
|
|
447
|
-
before the device sees it. A `file:`, `data:`,
|
|
448
|
-
`
|
|
449
|
-
`INVALID_ARGUMENT` pointing at `openLink`.
|
|
452
|
+
before the device sees it. A `file:`, `data:`, `javascript:`,
|
|
453
|
+
`view-source:`, `blob:`, or `filesystem:` link is `POLICY_DENIED`; any other
|
|
454
|
+
link is `INVALID_ARGUMENT` pointing at `openLink`.
|
|
450
455
|
|
|
451
456
|
`openLink` opens a deep link or a web link into `app` (default: the pinned
|
|
452
457
|
app), for magic-link sign-in and deep-link routes, and the session observes
|
|
@@ -456,8 +461,13 @@ observation brings the app back. Android starts the link on that package.
|
|
|
456
461
|
Without an app, Android lets the OS route the link, to a verified app link
|
|
457
462
|
owner or the browser, and the session follows the package that took it. iOS
|
|
458
463
|
needs an app: an open bound to none leaves no app session to observe, so it
|
|
459
|
-
is `INVALID_ARGUMENT`. `file:`, `data:`,
|
|
460
|
-
`
|
|
464
|
+
is `INVALID_ARGUMENT`. `file:`, `data:`, `javascript:`, `view-source:`,
|
|
465
|
+
`blob:`, and `filesystem:` links are `POLICY_DENIED`. A device link may use
|
|
466
|
+
any app's custom scheme, so the rule refuses these schemes rather than
|
|
467
|
+
admitting http(s) alone as the web does. A string that does not parse as an
|
|
468
|
+
absolute URL is `INVALID_ARGUMENT` too, and its message names no part of the
|
|
469
|
+
input, which may carry a magic-link token in its query, path, or userinfo.
|
|
470
|
+
The report shows the link cut before its
|
|
461
471
|
query and fragment, where a magic link carries its token. The replay cache
|
|
462
472
|
anchors on the location the link lands on: a step recorded after `openLink`
|
|
463
473
|
replays when the link lands on the same screen again.
|
|
@@ -171,7 +171,7 @@ prints. All reporters run concurrently, each with a one-minute deadline.
|
|
|
171
171
|
A timeout, exception, or invalid return value is reported on stderr without
|
|
172
172
|
changing the run's status, exit code, or report.
|
|
173
173
|
|
|
174
|
-
One Ctrl-C still lets reporters finish. A second Ctrl-C stops waiting for them.
|
|
174
|
+
One Ctrl-C still lets reporters finish. A second Ctrl-C stops waiting for them, except the built-in `junit` and `markdown`, which still write their files so none of the previous run's is left beside the new report.
|
|
175
175
|
|
|
176
176
|
The second argument is an `AbortSignal` that aborts when the budget runs out
|
|
177
177
|
or the run is forced to stop. Hand it to every request and timer, or an
|
|
@@ -180,7 +180,13 @@ abandoned reporter keeps the CLI alive after the run has ended.
|
|
|
180
180
|
With `--last-failed`, `lastRun` is the report the rerun selected from. The
|
|
181
181
|
rerun's own report lists the tests it left out as unselected, so a reporter
|
|
182
182
|
that keeps one place current, the way `@e2e-dev/github` keeps a pull request
|
|
183
|
-
comment, folds the rerun into `lastRun` to show both runs as one.
|
|
183
|
+
comment, folds the rerun into `lastRun` to show both runs as one. A test the
|
|
184
|
+
rerun owed but another filter left out is in the rerun's `run.carried`: its
|
|
185
|
+
last result, the serial group its attempts live on, and the `beforeAll` or
|
|
186
|
+
`afterAll` failure whose scope holds it, from whichever earlier run reported
|
|
187
|
+
them. A fold shows the carried result in place of the unselected one and
|
|
188
|
+
counts a carried failure or hook error against the outcome. The artifacts
|
|
189
|
+
both reports name are on disk under `artifactsRoot`.
|
|
184
190
|
|
|
185
191
|
The rows a reporter resolves with print under the `list` summary in the same
|
|
186
192
|
layout as `Report`; the `json` reporter does not carry them:
|