@e2edev/e2e 0.5.1 → 0.6.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/README.md +24 -11
- package/dist/agent/act.d.ts +1 -1
- package/dist/agent/act.js +2 -1
- package/dist/agent/act.js.map +1 -1
- package/dist/agent/ai-sdk.d.ts +1 -2
- package/dist/agent/ai-sdk.d.ts.map +1 -1
- package/dist/agent/ai-sdk.js +1 -2
- package/dist/agent/ai-sdk.js.map +1 -1
- package/dist/agent/default-agent.d.ts +7 -3
- package/dist/agent/default-agent.d.ts.map +1 -1
- package/dist/agent/default-agent.js +2 -2
- package/dist/agent/default-agent.js.map +1 -1
- package/dist/agent/derived.d.ts +1 -1
- package/dist/agent/derived.js +1 -1
- package/dist/agent/error.d.ts +2 -2
- package/dist/agent/error.d.ts.map +1 -1
- package/dist/agent/error.js +2 -2
- package/dist/agent/error.js.map +1 -1
- package/dist/agent/executor.d.ts +8 -3
- package/dist/agent/executor.d.ts.map +1 -1
- package/dist/agent/executor.js +1 -1
- package/dist/agent/executor.js.map +1 -1
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +8 -5
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/invocation.d.ts +2 -2
- package/dist/agent/invocation.d.ts.map +1 -1
- package/dist/agent/invocation.js +7 -2
- package/dist/agent/invocation.js.map +1 -1
- package/dist/agent/ledger.d.ts +1 -1
- package/dist/agent/ledger.js +1 -1
- package/dist/agent/model/adapter.d.ts +7 -4
- package/dist/agent/model/adapter.d.ts.map +1 -1
- package/dist/agent/model/adapter.js +2 -2
- package/dist/agent/model/adapter.js.map +1 -1
- package/dist/agent/model/router.d.ts +1 -1
- package/dist/agent/model/router.d.ts.map +1 -1
- package/dist/agent/model/router.js +1 -1
- package/dist/agent/model/router.js.map +1 -1
- package/dist/agent/model/sdk.d.ts +11 -1
- package/dist/agent/model/sdk.d.ts.map +1 -1
- package/dist/agent/model/sdk.js +54 -10
- package/dist/agent/model/sdk.js.map +1 -1
- package/dist/agent/observation.d.ts +1 -1
- package/dist/agent/observation.d.ts.map +1 -1
- package/dist/agent/observation.js +2 -2
- package/dist/agent/observation.js.map +1 -1
- package/dist/agent/prompts.d.ts +1 -1
- package/dist/agent/prompts.js +1 -1
- package/dist/agent/protocol.d.ts +2 -3
- package/dist/agent/protocol.d.ts.map +1 -1
- package/dist/agent/protocol.js +2 -3
- package/dist/agent/protocol.js.map +1 -1
- package/dist/agent/public.d.ts +1 -1
- package/dist/agent/public.js +1 -1
- package/dist/agent/replay.d.ts +1 -1
- package/dist/agent/replay.js +1 -1
- package/dist/agent/secrets.d.ts +1 -1
- package/dist/agent/secrets.d.ts.map +1 -1
- package/dist/agent/secrets.js +1 -1
- package/dist/agent/secrets.js.map +1 -1
- package/dist/agent/step-cache.d.ts +1 -1
- package/dist/agent/step-cache.d.ts.map +1 -1
- package/dist/agent/step-cache.js +7 -1
- package/dist/agent/step-cache.js.map +1 -1
- package/dist/agent/tool-loop.d.ts +7 -3
- package/dist/agent/tool-loop.d.ts.map +1 -1
- package/dist/agent/tool-loop.js +6 -5
- package/dist/agent/tool-loop.js.map +1 -1
- package/dist/cache/anchors.d.ts +1 -1
- package/dist/cache/anchors.js +1 -1
- package/dist/cache/context.d.ts +6 -1
- package/dist/cache/context.d.ts.map +1 -1
- package/dist/cache/context.js +2 -1
- package/dist/cache/context.js.map +1 -1
- package/dist/cache/decide.d.ts +1 -1
- package/dist/cache/decide.js +1 -1
- package/dist/cache/identity.d.ts +1 -1
- package/dist/cache/identity.js +1 -1
- package/dist/cache/recorder.d.ts +4 -2
- package/dist/cache/recorder.d.ts.map +1 -1
- package/dist/cache/recorder.js +10 -1
- package/dist/cache/recorder.js.map +1 -1
- package/dist/cache/relocate.d.ts +1 -1
- package/dist/cache/relocate.js +1 -1
- package/dist/cache/store.d.ts +1 -1
- package/dist/cache/store.js +1 -1
- package/dist/cache/trace.d.ts +16 -1
- package/dist/cache/trace.d.ts.map +1 -1
- package/dist/cache/trace.js +22 -1
- package/dist/cache/trace.js.map +1 -1
- package/dist/cli/bin.js +9 -1
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/cache.d.ts +25 -0
- package/dist/cli/cache.d.ts.map +1 -0
- package/dist/cli/cache.js +249 -0
- package/dist/cli/cache.js.map +1 -0
- package/dist/cli/docs-url.d.ts +3 -0
- package/dist/cli/docs-url.d.ts.map +1 -0
- package/dist/cli/docs-url.js +3 -0
- package/dist/cli/docs-url.js.map +1 -0
- package/dist/cli/guide.d.ts +4 -0
- package/dist/cli/guide.d.ts.map +1 -0
- package/dist/cli/guide.js +16 -0
- package/dist/cli/guide.js.map +1 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +210 -41
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/agent-skill.d.ts +34 -0
- package/dist/cli/init/agent-skill.d.ts.map +1 -0
- package/dist/cli/init/agent-skill.js +44 -0
- package/dist/cli/init/agent-skill.js.map +1 -0
- package/dist/cli/init/engines.d.ts +31 -23
- package/dist/cli/init/engines.d.ts.map +1 -1
- package/dist/cli/init/engines.js +34 -24
- package/dist/cli/init/engines.js.map +1 -1
- package/dist/cli/init/package.d.ts +19 -3
- package/dist/cli/init/package.d.ts.map +1 -1
- package/dist/cli/init/package.js +20 -20
- package/dist/cli/init/package.js.map +1 -1
- package/dist/cli/init/scaffold.d.ts.map +1 -1
- package/dist/cli/init/scaffold.js +4 -3
- package/dist/cli/init/scaffold.js.map +1 -1
- package/dist/cli/init.d.ts +14 -2
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +94 -16
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/signals.d.ts +4 -1
- package/dist/cli/signals.d.ts.map +1 -1
- package/dist/cli/signals.js +8 -2
- package/dist/cli/signals.js.map +1 -1
- package/dist/cli/skill.d.ts +16 -0
- package/dist/cli/skill.d.ts.map +1 -0
- package/dist/cli/skill.js +57 -0
- package/dist/cli/skill.js.map +1 -0
- package/dist/collect/collect.d.ts +9 -1
- package/dist/collect/collect.d.ts.map +1 -1
- package/dist/collect/collect.js +50 -5
- package/dist/collect/collect.js.map +1 -1
- package/dist/collect/registry.d.ts +1 -1
- package/dist/collect/registry.d.ts.map +1 -1
- package/dist/collect/registry.js +1 -1
- package/dist/collect/registry.js.map +1 -1
- package/dist/collect/select.d.ts +1 -1
- package/dist/collect/select.d.ts.map +1 -1
- package/dist/collect/select.js +77 -8
- package/dist/collect/select.js.map +1 -1
- package/dist/config/agent.d.ts +14 -2
- package/dist/config/agent.d.ts.map +1 -1
- package/dist/config/agent.js +36 -11
- package/dist/config/agent.js.map +1 -1
- package/dist/config/app.d.ts +2 -2
- package/dist/config/app.d.ts.map +1 -1
- package/dist/config/app.js +3 -3
- package/dist/config/app.js.map +1 -1
- package/dist/config/diagnose.d.ts +15 -0
- package/dist/config/diagnose.d.ts.map +1 -0
- package/dist/config/diagnose.js +122 -0
- package/dist/config/diagnose.js.map +1 -0
- package/dist/config/load.d.ts +9 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +35 -3
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts +2 -2
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +51 -16
- package/dist/config/resolve.js.map +1 -1
- package/dist/config/validate.d.ts +6 -2
- package/dist/config/validate.d.ts.map +1 -1
- package/dist/config/validate.js +20 -3
- package/dist/config/validate.js.map +1 -1
- package/dist/credentials.d.ts +1 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +1 -1
- package/dist/credentials.js.map +1 -1
- package/dist/engine/contract.d.ts +2 -2
- package/dist/engine/contract.js +1 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +3 -3
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/session.d.ts +1 -1
- package/dist/engine/session.d.ts.map +1 -1
- package/dist/engine/session.js +5 -2
- package/dist/engine/session.js.map +1 -1
- package/dist/expect/async.d.ts +1 -1
- package/dist/expect/async.d.ts.map +1 -1
- package/dist/expect/async.js +37 -1
- package/dist/expect/async.js.map +1 -1
- package/dist/expect/values.d.ts +1 -1
- package/dist/expect/values.d.ts.map +1 -1
- package/dist/expect/values.js +1 -1
- package/dist/expect/values.js.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -4
- package/dist/index.js.map +1 -1
- package/dist/internal/errors.d.ts +21 -3
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +55 -5
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/globs.d.ts +2 -2
- package/dist/internal/globs.d.ts.map +1 -1
- package/dist/internal/globs.js +2 -2
- package/dist/internal/globs.js.map +1 -1
- package/dist/internal/ids.d.ts +3 -3
- package/dist/internal/ids.d.ts.map +1 -1
- package/dist/internal/ids.js +3 -3
- package/dist/internal/ids.js.map +1 -1
- package/dist/internal/node-version.d.ts +10 -0
- package/dist/internal/node-version.d.ts.map +1 -0
- package/dist/internal/node-version.js +28 -0
- package/dist/internal/node-version.js.map +1 -0
- package/dist/internal/package-manager.d.ts +10 -0
- package/dist/internal/package-manager.d.ts.map +1 -0
- package/dist/internal/package-manager.js +43 -0
- package/dist/internal/package-manager.js.map +1 -0
- package/dist/internal/redact.d.ts +1 -1
- package/dist/internal/redact.d.ts.map +1 -1
- package/dist/internal/redact.js +1 -1
- package/dist/internal/redact.js.map +1 -1
- package/dist/internal/suggest.d.ts +16 -0
- package/dist/internal/suggest.d.ts.map +1 -0
- package/dist/internal/suggest.js +61 -0
- package/dist/internal/suggest.js.map +1 -0
- package/dist/internal/text.d.ts +1 -1
- package/dist/internal/text.d.ts.map +1 -1
- package/dist/internal/text.js +1 -1
- package/dist/internal/text.js.map +1 -1
- package/dist/internal/urls.d.ts +2 -2
- package/dist/internal/urls.d.ts.map +1 -1
- package/dist/internal/urls.js +2 -2
- package/dist/internal/urls.js.map +1 -1
- package/dist/locator/engine.d.ts +1 -1
- package/dist/locator/engine.d.ts.map +1 -1
- package/dist/locator/engine.js +1 -1
- package/dist/locator/engine.js.map +1 -1
- package/dist/locator/expression.d.ts +1 -1
- package/dist/locator/expression.d.ts.map +1 -1
- package/dist/locator/expression.js +1 -1
- package/dist/locator/expression.js.map +1 -1
- package/dist/locator/screen.js +1 -1
- package/dist/locator/screen.js.map +1 -1
- package/dist/report/build.d.ts +1 -1
- package/dist/report/build.d.ts.map +1 -1
- package/dist/report/build.js +4 -4
- package/dist/report/build.js.map +1 -1
- package/dist/report/junit.d.ts +1 -1
- package/dist/report/junit.d.ts.map +1 -1
- package/dist/report/junit.js +1 -1
- package/dist/report/junit.js.map +1 -1
- package/dist/report/list.d.ts +1 -1
- package/dist/report/list.d.ts.map +1 -1
- package/dist/report/list.js +7 -3
- package/dist/report/list.js.map +1 -1
- package/dist/report/write.d.ts +1 -1
- package/dist/report/write.d.ts.map +1 -1
- package/dist/report/write.js +1 -1
- package/dist/report/write.js.map +1 -1
- package/dist/run/budget.d.ts +2 -2
- package/dist/run/budget.js +2 -2
- package/dist/run/declared-processes.d.ts +1 -1
- package/dist/run/declared-processes.js +1 -1
- package/dist/run/execute.d.ts +2 -3
- package/dist/run/execute.d.ts.map +1 -1
- package/dist/run/execute.js +4 -5
- package/dist/run/execute.js.map +1 -1
- package/dist/run/fixtures.d.ts +1 -1
- package/dist/run/fixtures.d.ts.map +1 -1
- package/dist/run/fixtures.js +41 -5
- package/dist/run/fixtures.js.map +1 -1
- package/dist/run/managed-process.d.ts +8 -1
- package/dist/run/managed-process.d.ts.map +1 -1
- package/dist/run/managed-process.js +35 -17
- package/dist/run/managed-process.js.map +1 -1
- package/dist/run/realm.d.ts +2 -2
- package/dist/run/realm.js +2 -2
- package/dist/run/runner.d.ts +22 -0
- package/dist/run/runner.d.ts.map +1 -1
- package/dist/run/runner.js +44 -9
- package/dist/run/runner.js.map +1 -1
- package/dist/run/scheduler.d.ts +1 -2
- package/dist/run/scheduler.d.ts.map +1 -1
- package/dist/run/scheduler.js +2 -3
- package/dist/run/scheduler.js.map +1 -1
- package/dist/run/serial.d.ts +1 -1
- package/dist/run/serial.d.ts.map +1 -1
- package/dist/run/serial.js +3 -3
- package/dist/run/serial.js.map +1 -1
- package/dist/run/sessions.d.ts +1 -1
- package/dist/run/sessions.d.ts.map +1 -1
- package/dist/run/sessions.js +1 -1
- package/dist/run/sessions.js.map +1 -1
- package/dist/run/steps.d.ts +2 -2
- package/dist/run/steps.d.ts.map +1 -1
- package/dist/run/steps.js +1 -1
- package/dist/run/steps.js.map +1 -1
- package/dist/run/units.d.ts +1 -1
- package/dist/run/units.js +1 -1
- package/dist/types.d.ts +27 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/package.json +10 -9
- package/skills/e2e/SKILL.md +110 -0
- package/skills/e2e/references/agent.md +210 -0
- package/skills/e2e/references/debugging.md +81 -0
- package/skills/e2e/references/running.md +153 -0
- package/skills/e2e/references/setup.md +238 -0
- package/skills/e2e/references/writing-tests.md +289 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Running tests
|
|
2
|
+
|
|
3
|
+
## Commands
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx --no-install e2e run [files...] [options] # run tests
|
|
7
|
+
npx --no-install e2e list [files...] [options] # print what run would select, without running
|
|
8
|
+
npx --no-install e2e init [--yes] # scaffold a project, refresh the agent skill
|
|
9
|
+
npx --no-install e2e guide [topic] # print this skill: setup, writing-tests, agent, running, debugging
|
|
10
|
+
npx --no-install e2e cache ls|clear|stats # read or empty the trace cache
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`run` flags:
|
|
14
|
+
|
|
15
|
+
| Flag | Effect |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `[files...]` | Files, directories, or quoted globs relative to the project root. They narrow the config `tests` glob, never bypass it. |
|
|
18
|
+
| `--config <path>` | Explicit config file. Default: `e2e.config.ts` or `.mts` found upward from the working directory. |
|
|
19
|
+
| `--target <ids>` | Comma-separated target names. |
|
|
20
|
+
| `--tag <tag>` | Repeatable tag filter; `--tag-mode all` requires every tag. |
|
|
21
|
+
| `--headed` | Visible browser or simulator when the engine supports it. |
|
|
22
|
+
| `--workers <n>`, `--retries <n>` | Override the resolved values. |
|
|
23
|
+
| `--reporter <ids>` | `list`, `json`, `junit`, comma-separated. `json` cannot combine with `list`. |
|
|
24
|
+
| `--artifacts <dir>` | Artifact root, default `.e2e/artifacts`. |
|
|
25
|
+
| `--no-cache` | Run with the trace cache off. |
|
|
26
|
+
| `--pass-with-no-tests` | Exit 0 when nothing matches instead of `NO_TESTS`. |
|
|
27
|
+
| `--debug` | Phase timings and an agent step table on stderr; step transcripts saved as artifacts. |
|
|
28
|
+
| `--ai-trace` | Record every model call to `.e2e/ai-trace.json`. |
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx --no-install e2e run tests/signup.e2e.ts
|
|
32
|
+
npx --no-install e2e run tests/agent --tag smoke
|
|
33
|
+
npx --no-install e2e run 'tests/**/*.smoke.e2e.ts' --target chromium --workers 1 --retries 0
|
|
34
|
+
CI=1 npx --no-install e2e run # reproduce the CI defaults locally
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`list` takes the same files and the selection flags (`--config`, `--target`,
|
|
38
|
+
`--tag`, `--tag-mode`, `--pass-with-no-tests`) and prints one line per
|
|
39
|
+
test-target pair, `file › title [target]`, then exits without starting the
|
|
40
|
+
app, an engine, or a worker. `--reporter json` prints `{ "pairs": [...] }`.
|
|
41
|
+
Use it to check a filter before a run.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx --no-install e2e list --tag smoke
|
|
45
|
+
npx --no-install e2e list tests/signup.e2e.ts --reporter json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A `package.json` script keeps it short: `"test:e2e": "e2e run"`, then
|
|
49
|
+
`pnpm test:e2e tests/signup.e2e.ts`.
|
|
50
|
+
|
|
51
|
+
## The trace cache
|
|
52
|
+
|
|
53
|
+
Entries live under `.e2e/cache/`, one file per key, named after the key
|
|
54
|
+
digest. `cache` commands read the same config as `run`, so `--config` and
|
|
55
|
+
`cache.dir` point them at the right store.
|
|
56
|
+
|
|
57
|
+
| Command | Prints |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `e2e cache ls` | One row per entry: test, target, instruction digest, age, action count. |
|
|
60
|
+
| `e2e cache stats` | Directory, entry count, total size. |
|
|
61
|
+
| `e2e cache clear` | Deletes the entries and the directory; files the runner never wrote stay. |
|
|
62
|
+
|
|
63
|
+
Use `ls` to see what a committed cache would replay, and `clear` when a
|
|
64
|
+
recorded flow is stale — `--no-cache` only skips the cache for one run.
|
|
65
|
+
|
|
66
|
+
## Output
|
|
67
|
+
|
|
68
|
+
- `list` (default): one line per file and target, a `Failed Tests` section
|
|
69
|
+
with each error, its code, the failing line and a code frame, then a
|
|
70
|
+
summary (`Test Files`, `Tests`, `AI`, `Duration`, `Report`).
|
|
71
|
+
- `.e2e/report.json` is written on every run whatever the reporters:
|
|
72
|
+
`run.status`, `run.exitCode`, `run.errors[]` (run-level failures such as
|
|
73
|
+
`APP_UNREACHABLE`), and `run.results[]`, one per test and target, with
|
|
74
|
+
`titlePath`, `file`, `source`, `status`, and `attempts[]` holding `steps[]`,
|
|
75
|
+
`artifacts[]`, and `error`.
|
|
76
|
+
- `junit`: `.e2e/junit.xml` beside the report, for CI test summaries.
|
|
77
|
+
Combine it with the terminal output: `--reporter list,junit`.
|
|
78
|
+
- `json`: the report document on stdout.
|
|
79
|
+
- Artifacts (screenshots, Playwright traces, `--debug` transcripts,
|
|
80
|
+
downloads) live under `.e2e/artifacts/`; every path is recorded in the
|
|
81
|
+
report.
|
|
82
|
+
|
|
83
|
+
## Exit codes
|
|
84
|
+
|
|
85
|
+
| Code | Meaning |
|
|
86
|
+
| ---: | --- |
|
|
87
|
+
| 0 | Every selected test passed, was flaky, or was skipped |
|
|
88
|
+
| 1 | A test or setup test failed or timed out |
|
|
89
|
+
| 2 | CLI, config, collection, credential, model-config, or policy error |
|
|
90
|
+
| 3 | Engine, app process, model provider, artifact, or cleanup failure |
|
|
91
|
+
| 4 | Internal runner error |
|
|
92
|
+
| 130 | Interrupted |
|
|
93
|
+
|
|
94
|
+
The highest code present wins for a mixed run (`130 > 4 > 3 > 2 > 1 > 0`).
|
|
95
|
+
Do not retry a job on exit 2: it is deterministic. Exit 3 is the only one
|
|
96
|
+
where a job-level retry makes sense.
|
|
97
|
+
|
|
98
|
+
Ctrl-C once interrupts and still writes the report; twice forces teardown;
|
|
99
|
+
three times exits at once.
|
|
100
|
+
|
|
101
|
+
## Continuous integration
|
|
102
|
+
|
|
103
|
+
CI mode is on when `CI` is set (and not `0` or `false`). It changes
|
|
104
|
+
`retries` to 1, `workers` to 1, rejects `test.only` with `ONLY_IN_CI`, makes
|
|
105
|
+
the trace cache `read-only`, and ignores `reuseExisting`.
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
# .github/workflows/e2e.yml
|
|
109
|
+
name: e2e
|
|
110
|
+
on:
|
|
111
|
+
pull_request:
|
|
112
|
+
push:
|
|
113
|
+
branches: [main]
|
|
114
|
+
permissions:
|
|
115
|
+
contents: read
|
|
116
|
+
jobs:
|
|
117
|
+
e2e:
|
|
118
|
+
runs-on: ubuntu-latest
|
|
119
|
+
steps:
|
|
120
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
121
|
+
- uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
|
|
122
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
123
|
+
with:
|
|
124
|
+
node-version: 26
|
|
125
|
+
cache: pnpm
|
|
126
|
+
- run: pnpm install --frozen-lockfile
|
|
127
|
+
- run: npx playwright install chromium --with-deps
|
|
128
|
+
- run: npx --no-install e2e run --reporter list,junit
|
|
129
|
+
env:
|
|
130
|
+
E2E_USER_ADMIN_USERNAME: ${{ secrets.E2E_USER_ADMIN_USERNAME }}
|
|
131
|
+
E2E_USER_ADMIN_PASSWORD: ${{ secrets.E2E_USER_ADMIN_PASSWORD }}
|
|
132
|
+
- if: ${{ !cancelled() }}
|
|
133
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
134
|
+
with:
|
|
135
|
+
name: e2e-report
|
|
136
|
+
path: |
|
|
137
|
+
.e2e/report.json
|
|
138
|
+
.e2e/junit.xml
|
|
139
|
+
- if: failure()
|
|
140
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
141
|
+
with:
|
|
142
|
+
name: e2e-artifacts
|
|
143
|
+
path: .e2e/artifacts
|
|
144
|
+
retention-days: 7
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- Install browsers as their own step so the download never counts against a
|
|
148
|
+
launch timeout.
|
|
149
|
+
- Start the app through the engine's `command`; the runner tears it down on
|
|
150
|
+
every exit path.
|
|
151
|
+
- Agentic suites: a separate config, run on `schedule` or
|
|
152
|
+
`workflow_dispatch`, `E2E_MODEL` as a CI variable and `E2E_MODEL_API_KEY`
|
|
153
|
+
as a secret, never a required check.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Setting up e2e
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
- Node.js 22.12 or newer.
|
|
6
|
+
- ES modules. The `package.json` nearest to the config and the tests must set
|
|
7
|
+
`"type": "module"`. An `e2e.config.mts` is ESM on its own, but `.ts` tests
|
|
8
|
+
still follow their package. In a CommonJS package either run
|
|
9
|
+
`npm pkg set type=module` (this also changes how its `.js` files run) or put
|
|
10
|
+
the tests in their own ESM package directory.
|
|
11
|
+
- For browser tests, `@e2edev/playwright`. Missing browsers download when the
|
|
12
|
+
engine first boots. In CI install them up front:
|
|
13
|
+
`npx playwright install chromium --with-deps`.
|
|
14
|
+
|
|
15
|
+
## Scaffold
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @e2edev/e2e@beta init # npm
|
|
19
|
+
pnpm dlx @e2edev/e2e@beta init # pnpm
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
When `@e2edev/e2e` is already installed, run `npx --no-install e2e init`
|
|
23
|
+
instead, so the installed version scaffolds.
|
|
24
|
+
|
|
25
|
+
The wizard asks for the engine (Playwright by default; None and agent-device
|
|
26
|
+
are the alternatives), whether to enable AI testing (adds `ai@^7.0.0` and
|
|
27
|
+
`createAgent` to the config), which agent directories receive this skill
|
|
28
|
+
(`.agents/skills/` and `.claude/skills/`), a confirmation of the files it
|
|
29
|
+
will write, and whether to install. `--yes` skips every prompt (use it from
|
|
30
|
+
scripts and from a shell without a TTY): Playwright, AI on, no installation,
|
|
31
|
+
skill in both directories. The closing line prints the run command,
|
|
32
|
+
`APP_URL=http://localhost:3000 npx --no-install e2e run` for Playwright, and
|
|
33
|
+
suggests a `tsconfig.json` when the project has none.
|
|
34
|
+
|
|
35
|
+
Init writes `package.json` (a private ESM package when missing; otherwise
|
|
36
|
+
only the missing dev dependencies and the `test:e2e` script are added),
|
|
37
|
+
`e2e.config.ts`, `tests/example.e2e.ts`, `.gitignore` entries for the `.e2e/`
|
|
38
|
+
output, and the skill. Existing config and test files are never touched. Re-run it after an
|
|
39
|
+
upgrade to refresh the skill; it changes nothing else in an initialized
|
|
40
|
+
project.
|
|
41
|
+
|
|
42
|
+
Without the wizard:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install --save-dev @e2edev/e2e@beta @e2edev/playwright@beta ai@^7
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`ai` (the Vercel AI SDK, v7) is only needed for `agent.*` steps.
|
|
49
|
+
|
|
50
|
+
## The config
|
|
51
|
+
|
|
52
|
+
`e2e.config.ts` sits at the project root and default-exports an object
|
|
53
|
+
literal ending in `satisfies E2EConfig`. The runner validates it at load:
|
|
54
|
+
unknown keys are `INVALID_CONFIG`.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import type { E2EConfig } from '@e2edev/e2e';
|
|
58
|
+
import { createAgent } from '@e2edev/e2e/agent';
|
|
59
|
+
import { playwright } from '@e2edev/playwright';
|
|
60
|
+
|
|
61
|
+
export default {
|
|
62
|
+
tests: 'tests/**/*.e2e.ts',
|
|
63
|
+
targets: [
|
|
64
|
+
{
|
|
65
|
+
name: 'web',
|
|
66
|
+
platform: 'web',
|
|
67
|
+
engine: playwright({
|
|
68
|
+
url: 'http://127.0.0.1:3000',
|
|
69
|
+
command: { executable: 'pnpm', args: ['dev'], log: '.e2e/logs/app.log' },
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
// Only for agent.* steps. The model comes from E2E_MODEL unless set here.
|
|
74
|
+
agent: createAgent({ system: 'You are a thorough QA agent. Verify every outcome on screen.' }),
|
|
75
|
+
credentials: {
|
|
76
|
+
admin: { username: 'admin@example.test', password: process.env.ADMIN_PASSWORD ?? '' },
|
|
77
|
+
},
|
|
78
|
+
} satisfies E2EConfig;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Key | Default | Notes |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `targets` | required | Non-empty. Each target: `name` (used by `--target` and in reports), `platform` (`web`, `ios`, `android`, or any label), `engine`. |
|
|
84
|
+
| `tests` | `'tests/**/*.e2e.ts'` | A glob or an array of globs, `/` separators. |
|
|
85
|
+
| `timeout` | `120000` | Per test attempt, in ms. Also the default `agent.act` deadline. |
|
|
86
|
+
| `actionTimeout` | `30000` | Every locator action and engine operation, including each observation inside an agent step. Raise it for slow model providers. |
|
|
87
|
+
| `assertionTimeout` | `5000` | `expect` polling window. |
|
|
88
|
+
| `retries` | `0`, `1` in CI | 0 to 10. |
|
|
89
|
+
| `workers` | half the cores, `1` in CI | Test files run in parallel across workers. Use `1` for device targets. |
|
|
90
|
+
| `reporters` | `['list']` | `list`, `json`, `junit`. `json` excludes `list`. |
|
|
91
|
+
| `cache` | `'read-write'`, `'read-only'` in CI | The trace cache for `agent.act`; `'off'` disables it. |
|
|
92
|
+
| `agent` | unset | `createAgent(...)`, an options block `{ model, context, visionModel, maxSteps, maxModelCalls, vision, providerOptions }`, or a custom `StepExecutor`. Omitted, the built-in agent runs with `E2E_MODEL`. |
|
|
93
|
+
| `credentials` | `{}` | Named `{ username, password, allowedOrigins? }` entries; `password` may be a function returning the value. |
|
|
94
|
+
| `screen.testIdAttribute` | `'data-testid'` | Attribute read by `getByTestId`. |
|
|
95
|
+
| `artifacts` | `['screenshot', 'trace']` | Kinds to keep, or `{ kinds, store }`. |
|
|
96
|
+
| `projectId` | the package name | Report and cache identity. |
|
|
97
|
+
|
|
98
|
+
## The app under test
|
|
99
|
+
|
|
100
|
+
The engine declares the app; the runner owns navigation policy, the app
|
|
101
|
+
process, and identity. `playwright()` accepts:
|
|
102
|
+
|
|
103
|
+
| Option | Meaning |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `url` | Base URL for `app.open()` and relative navigation. A missing scheme becomes `https://`, or `http://` for a loopback host. Required once a test navigates. |
|
|
106
|
+
| `command` | The process that serves `url`. See below. |
|
|
107
|
+
| `readyUrl` | Readiness probe when it differs from `url`. |
|
|
108
|
+
| `services` | Dependency processes started before `command`, in order. |
|
|
109
|
+
| `allowedOrigins` | Origins tests and the agent may navigate to. Default: the origin of `url`. |
|
|
110
|
+
| `environment` | `'test'`, `'staging'`, `'production'`. Inferred from the host; a label for the report and the cache key. |
|
|
111
|
+
| `identity` | Stable app identity for cache and session keys when the origin changes per deploy (preview URLs). |
|
|
112
|
+
| `browser` | `'chromium'` (default), `'firefox'`, `'webkit'`. |
|
|
113
|
+
| `viewport` | `{ width, height }`, default 1280x720. |
|
|
114
|
+
| `connect` | `{ cdpEndpoint }` to attach to a remote Chromium over CDP instead of launching. |
|
|
115
|
+
|
|
116
|
+
Two browsers are two targets sharing one app declaration:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const app = { url: 'http://127.0.0.1:3000' };
|
|
120
|
+
export default {
|
|
121
|
+
targets: [
|
|
122
|
+
{ name: 'chromium', platform: 'web', engine: playwright(app) },
|
|
123
|
+
{ name: 'mobile-webkit', platform: 'web', engine: playwright({ ...app, browser: 'webkit', viewport: { width: 390, height: 844 } }) },
|
|
124
|
+
],
|
|
125
|
+
} satisfies E2EConfig;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Let the runner start the app
|
|
129
|
+
|
|
130
|
+
Prefer `command` over a hand-started dev server: the run is then
|
|
131
|
+
self-contained locally and in CI.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
engine: playwright({
|
|
135
|
+
url: 'http://127.0.0.1:3000',
|
|
136
|
+
services: [
|
|
137
|
+
{
|
|
138
|
+
name: 'postgres',
|
|
139
|
+
executable: 'docker',
|
|
140
|
+
args: ['compose', 'up', '--wait', 'postgres'],
|
|
141
|
+
waitForExit: true,
|
|
142
|
+
teardown: { executable: 'docker', args: ['compose', 'down'] },
|
|
143
|
+
},
|
|
144
|
+
{ name: 'migrate', executable: 'pnpm', args: ['db:migrate'], waitForExit: true },
|
|
145
|
+
],
|
|
146
|
+
command: {
|
|
147
|
+
executable: 'pnpm',
|
|
148
|
+
args: ['dev'],
|
|
149
|
+
env: { PORT: '3000', DATABASE_URL: process.env.DATABASE_URL ?? '' },
|
|
150
|
+
startupTimeout: 120_000,
|
|
151
|
+
log: '.e2e/logs/app.log',
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
How it behaves:
|
|
157
|
+
|
|
158
|
+
- The runner spawns `command`, polls `readyUrl` (default `url`) until a
|
|
159
|
+
200 to 499 status arrives within `startupTimeout` (default 60 s), and stops
|
|
160
|
+
the process when the run ends, fails, or is interrupted. Never ready is
|
|
161
|
+
`APP_UNREACHABLE`; `.e2e/report.json` is still written.
|
|
162
|
+
- The child inherits only `PATH`, `HOME`, and the temp-directory variables,
|
|
163
|
+
plus `command.env`. Anything the app needs (database URL, API keys) must be
|
|
164
|
+
passed through `env` explicitly. Model keys and `E2E_USER_*` values are
|
|
165
|
+
never inherited.
|
|
166
|
+
- Output is discarded unless `log` names a file. Set it; a server that dies
|
|
167
|
+
on boot is otherwise invisible. Keep the file under an ignored directory
|
|
168
|
+
such as `.e2e/logs/`.
|
|
169
|
+
- If `url` already answers before the spawn, the run fails with
|
|
170
|
+
`APP_ALREADY_RUNNING`. Set `reuseExisting: true` for local development to
|
|
171
|
+
attach to a dev server that is already up; CI ignores the flag.
|
|
172
|
+
- `services` start one at a time in declaration order. Each declares
|
|
173
|
+
`readyUrl` (polled) or `waitForExit: true` (ready when it exits 0). On
|
|
174
|
+
every exit path the runner stops the app, stops the services in reverse,
|
|
175
|
+
then runs their `teardown` commands in reverse.
|
|
176
|
+
- `executable` is resolved on `PATH` and never shell-interpreted. Point it at
|
|
177
|
+
the server itself rather than at a wrapper script.
|
|
178
|
+
|
|
179
|
+
To test an app started elsewhere, point `url` at it and start it yourself,
|
|
180
|
+
or read the address from the environment:
|
|
181
|
+
`url: process.env.APP_URL ?? 'http://localhost:3000'`. The runner reads no
|
|
182
|
+
`APP_URL` itself; the config does.
|
|
183
|
+
|
|
184
|
+
## Environment variables the runner reads
|
|
185
|
+
|
|
186
|
+
| Variable | Effect |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| `E2E_MODEL` | `provider/model-id` for `agent.*` steps when the config sets no model. Routed through the Vercel AI Gateway. |
|
|
189
|
+
| `E2E_MODEL_API_KEY` | Credential for that model. `AI_GATEWAY_API_KEY` is the fallback. |
|
|
190
|
+
| `E2E_VISION_MODEL` | Model for calls with `vision`; falls back to the main model. |
|
|
191
|
+
| `E2E_USER_<NAME>_USERNAME`, `E2E_USER_<NAME>_PASSWORD` | Override `credentials.<name>`. `<NAME>` is the credential name uppercased, other characters as `_`. |
|
|
192
|
+
| `CI` | Turns on CI defaults: `retries: 1`, `workers: 1`, `test.only` rejected, cache read-only, `reuseExisting` ignored. |
|
|
193
|
+
|
|
194
|
+
## Mobile targets
|
|
195
|
+
|
|
196
|
+
`@e2edev/agent-device` drives iOS simulators and Android emulators through
|
|
197
|
+
[agent-device](https://github.com/callstack/agent-device). It needs Xcode
|
|
198
|
+
with a simulator runtime, or the Android SDK with an emulator; run
|
|
199
|
+
`npx agent-device doctor` once.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import type { E2EConfig } from '@e2edev/e2e';
|
|
203
|
+
import { createAgent } from '@e2edev/e2e/agent';
|
|
204
|
+
import { agentDevice } from '@e2edev/agent-device';
|
|
205
|
+
import { agentDeviceTools } from '@e2edev/agent-device/tools';
|
|
206
|
+
|
|
207
|
+
const iphone = agentDevice({ platform: 'ios', app: 'com.example.app' });
|
|
208
|
+
|
|
209
|
+
export default {
|
|
210
|
+
targets: [{ name: 'ios', platform: 'ios', engine: iphone }],
|
|
211
|
+
workers: 1,
|
|
212
|
+
agent: createAgent({ tools: agentDeviceTools(iphone) }),
|
|
213
|
+
} satisfies E2EConfig;
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- `app` is a bundle id, package name, or display name opened fresh per
|
|
217
|
+
attempt. `appPath` installs a `.app` or `.apk` once per worker; without
|
|
218
|
+
`app`, the installed bundle is the one opened.
|
|
219
|
+
- `workers: 1`: workers share one simulator.
|
|
220
|
+
- `screen`, `expect`, `app`, and `agent` work unchanged. Import `test` from
|
|
221
|
+
`@e2edev/agent-device` to type the `device` fixture (`setAppearance`,
|
|
222
|
+
`setNetwork`, `setPermission`, `installApp`, `locator('role=... id=...')`,
|
|
223
|
+
and more). Portable suites declare `requires: ['device']`.
|
|
224
|
+
- No `state` capability: `test.setup` and `session` are unavailable on a
|
|
225
|
+
device. Sign in with deterministic `screen` actions; `agent.act` cannot
|
|
226
|
+
fill a `Secret` on a device.
|
|
227
|
+
- A deterministic check that names a platform label runs on one platform
|
|
228
|
+
only: `test('...', { platforms: ['ios'] }, ...)`.
|
|
229
|
+
- `selectOption`, `setInputFiles`, and `scrollIntoView` are
|
|
230
|
+
`UNSUPPORTED_CAPABILITY` on a device.
|
|
231
|
+
|
|
232
|
+
## Done when
|
|
233
|
+
|
|
234
|
+
- `npx --no-install e2e run tests/example.e2e.ts` passes against the app.
|
|
235
|
+
- `package.json` has a script such as `"test:e2e": "e2e run"`.
|
|
236
|
+
- `.gitignore` lists the `.e2e/` outputs (init adds them). Committing
|
|
237
|
+
`.e2e/cache/` is opt-in: remove that line to share `agent.act` replays.
|
|
238
|
+
- CI runs the deterministic suite on pull requests; see `running`.
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Writing tests
|
|
2
|
+
|
|
3
|
+
## A complete file
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// tests/todos.e2e.ts
|
|
7
|
+
import { test } from '@e2edev/playwright';
|
|
8
|
+
import { expect } from '@e2edev/e2e';
|
|
9
|
+
|
|
10
|
+
test.describe('todos', { tags: ['todos'] }, () => {
|
|
11
|
+
test.beforeEach(async ({ app }) => {
|
|
12
|
+
await app.open('/todos');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('adds and completes a todo', async ({ screen, web }) => {
|
|
16
|
+
await screen.getByLabel('New todo').fill('Write the release notes');
|
|
17
|
+
await screen.getByRole('button', { name: 'Add' }).tap();
|
|
18
|
+
|
|
19
|
+
await expect(screen.getByRole('listitem')).toHaveCount(1);
|
|
20
|
+
await expect(screen.getByRole('status', { name: 'Remaining' })).toHaveText('1 remaining');
|
|
21
|
+
|
|
22
|
+
await screen.getByRole('checkbox', { name: 'Write the release notes' }).check();
|
|
23
|
+
await expect(screen.getByRole('status', { name: 'Remaining' })).toHaveText('0 remaining');
|
|
24
|
+
await expect(web).toHaveURL('/todos');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('ignores an empty submission', async ({ screen }) => {
|
|
28
|
+
await screen.getByRole('button', { name: 'Add' }).tap();
|
|
29
|
+
await expect(screen.getByRole('listitem')).toHaveCount(0);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Files match the config `tests` glob, default `tests/**/*.e2e.ts`. Every test
|
|
35
|
+
starts from clean state: a fresh browser context and no page open, so a test
|
|
36
|
+
calls `app.open()` first (here in `beforeEach`).
|
|
37
|
+
|
|
38
|
+
## Registration
|
|
39
|
+
|
|
40
|
+
`test` is the only registration surface; everything registers while the file
|
|
41
|
+
is imported, so a `describe` body is synchronous (an `async` body is a
|
|
42
|
+
`COLLECTION_ERROR`).
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
test('title', async ({ app, screen }) => {});
|
|
46
|
+
test('title', { tags: ['smoke'], retries: 2, timeout: 60_000 }, async ({ app }) => {});
|
|
47
|
+
test.describe('group', { tags: ['billing'] }, () => { /* tests and hooks */ });
|
|
48
|
+
test.describe('checkout flow', { serial: true }, () => { /* ordered, shared app state */ });
|
|
49
|
+
test.beforeEach(async ({ app }) => {}); // per attempt, with test fixtures
|
|
50
|
+
test.afterEach(async ({ screen }) => {}); // runs after failures too, with its own cleanup budget
|
|
51
|
+
test.beforeAll(async ({ platform }) => {}); // per suite realm, no app fixtures
|
|
52
|
+
test.afterAll(async () => {});
|
|
53
|
+
test.skip('later', async () => {});
|
|
54
|
+
test.only('focus', async () => {}); // local only: CI fails with ONLY_IN_CI
|
|
55
|
+
test.setup('sign in', { sessions: ['admin'] }, async ({ app, screen, session }) => {}); // see Sign-in sessions
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Option | Default | Notes |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `timeout` | `config.timeout`, 120 s | Covers `beforeEach` and the body. |
|
|
61
|
+
| `retries` | `config.retries` | 0 to 10. On a serial group, the group's value applies. |
|
|
62
|
+
| `tags` | `[]` | Union across layers. Select with `--tag smoke`; `--tag-mode all` requires every tag. |
|
|
63
|
+
| `skip` | unset | `true` or a reason string. |
|
|
64
|
+
| `platforms` | unset | Run only on targets with these platforms, e.g. `['ios']`. |
|
|
65
|
+
| `requires` | `[]` | Capabilities the engine must contribute, e.g. `['web']`. Otherwise the test is skipped at selection instead of failing with `UNSUPPORTED_CAPABILITY`. |
|
|
66
|
+
| `session` | unset | Restore state saved by a setup test. |
|
|
67
|
+
| `agentContext` | unset | Extra context for `agent.*` calls in this test or group. |
|
|
68
|
+
| `serial` | `false` | Groups only. Members share one app state, run in order on one worker, and retry as a whole. Inside, per-member `retries`, `session`, `platforms`, `requires`, and `skip` are errors. |
|
|
69
|
+
|
|
70
|
+
Hook order follows nesting, not position: outer `beforeEach` first, inner
|
|
71
|
+
`afterEach` first. `beforeAll` runs again for every retry and every serial
|
|
72
|
+
group, because each is a fresh module realm.
|
|
73
|
+
|
|
74
|
+
## Fixtures
|
|
75
|
+
|
|
76
|
+
Fixtures are lazy; destructure them in the callback.
|
|
77
|
+
|
|
78
|
+
| Fixture | Type | Available |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| `app` | `App` | Always. |
|
|
81
|
+
| `screen` | `Screen` | Always. |
|
|
82
|
+
| `agent` | `Agent` | Needs a configured model, else `MODEL_UNAVAILABLE`. See the `agent` topic. |
|
|
83
|
+
| `platform` | `'web' \| 'ios' \| 'android' \| string` | Always; also in `beforeAll` and `afterAll`. |
|
|
84
|
+
| `web` | `Web` | Browser targets. Import `test` from `@e2edev/playwright`. |
|
|
85
|
+
| `device` | `Device` | Device targets. Import `test` from `@e2edev/agent-device`. |
|
|
86
|
+
| `session` | `SetupSession` | Only in `test.setup`. |
|
|
87
|
+
|
|
88
|
+
### app
|
|
89
|
+
|
|
90
|
+
| Method | Does |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `open(path?)` | Opens the engine's `url`, a path relative to it, or an absolute URL inside `allowedOrigins`. |
|
|
93
|
+
| `back()` | One history step back. |
|
|
94
|
+
| `restart()` | Recreates the context and keeps persisted state, including a restored session. |
|
|
95
|
+
| `clearState()` | Clears cookies and storage, then relaunches. Not inside a serial group. |
|
|
96
|
+
| `deepLink(url)` | Opens an allowed deep or universal link. |
|
|
97
|
+
| `screenshot(label?)` | Saves a redacted screenshot as an artifact and returns its path. |
|
|
98
|
+
|
|
99
|
+
## Locators
|
|
100
|
+
|
|
101
|
+
`screen.getBy*` builds a lazy query; nothing resolves until an action, read,
|
|
102
|
+
or assertion runs. Every query also exists on a locator, scoped to its
|
|
103
|
+
subtree.
|
|
104
|
+
|
|
105
|
+
| Query | Matches |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `getByRole(role, { name?, exact?, checked?, disabled?, selected?, expanded?, hidden?, visible? })` | Semantic role, optionally by accessible name and state. First choice. |
|
|
108
|
+
| `getByLabel(text, { exact?, visible? })` | Form controls by label. |
|
|
109
|
+
| `getByPlaceholder(text)` | Inputs by placeholder. |
|
|
110
|
+
| `getByText(text, { exact?, visible? })` | Visible text. |
|
|
111
|
+
| `getByDisplayValue(value)` | Inputs by current value. |
|
|
112
|
+
| `getByTestId(id, { visible? })` | `data-testid` (or `screen.testIdAttribute`). Last resort. |
|
|
113
|
+
|
|
114
|
+
Roles: `button`, `link`, `textbox`, `searchbox`, `combobox`, `listbox`,
|
|
115
|
+
`option`, `checkbox`, `radio`, `switch`, `slider`, `image`, `heading`, `tab`,
|
|
116
|
+
`menuitem`, `list`, `listitem`, `table`, `row`, `cell`, `columnheader`,
|
|
117
|
+
`status`, `alert`, `dialog`, `alertdialog`, `main`, `navigation`, `banner`,
|
|
118
|
+
`contentinfo`, `complementary`, `region`. The union is closed; anything else
|
|
119
|
+
is a type error.
|
|
120
|
+
|
|
121
|
+
Text matching is exact by default after whitespace normalization.
|
|
122
|
+
`exact: false` is a case-insensitive substring match; a `RegExp` matches as
|
|
123
|
+
written.
|
|
124
|
+
|
|
125
|
+
Rules:
|
|
126
|
+
|
|
127
|
+
- An action, read, or assertion needs exactly one match. Two matches fail
|
|
128
|
+
immediately with `LOCATOR_AMBIGUOUS`; zero matches poll until the timeout,
|
|
129
|
+
then `LOCATOR_NOT_FOUND`. `toHaveCount` and `toBeHidden` are the
|
|
130
|
+
exceptions.
|
|
131
|
+
- Narrow with `filter({ hasText })`, `filter({ has: locator })`, `first()`,
|
|
132
|
+
`last()`, `nth(i)`, or by scoping under another locator.
|
|
133
|
+
- `visible: true` drops nodes the page hides (a closed drawer, a prerendered
|
|
134
|
+
duplicate) before the exactly-one rule. Reach for it when a query is
|
|
135
|
+
ambiguous even though one element is on screen.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const row = screen.getByRole('listitem').filter({ hasText: 'Invoice 42' });
|
|
139
|
+
await row.getByRole('button', { name: 'Void' }).tap();
|
|
140
|
+
await screen.getByText('Save', { visible: true }).first().tap();
|
|
141
|
+
await screen.scrollUntilVisible(screen.getByRole('button', { name: 'Accept' }));
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Actions
|
|
145
|
+
|
|
146
|
+
Each action resolves one node, waits for it to be actionable within
|
|
147
|
+
`config.actionTimeout` (30 s, or `{ timeout }`), and performs one operation.
|
|
148
|
+
|
|
149
|
+
`tap()` (alias `click()`), `doubleTap()`, `longPress({ durationMs? })`,
|
|
150
|
+
`fill(value | Secret)`, `clear()`, `press(key)`, `check()`, `uncheck()`,
|
|
151
|
+
`selectOption(label | { label } | { index })`, `focus()`, `hover()`,
|
|
152
|
+
`setInputFiles(paths)` (relative to the project root), `dragTo(locator)`,
|
|
153
|
+
`scrollIntoView()`, `swipe({ direction, momentum? })`.
|
|
154
|
+
|
|
155
|
+
### Reads
|
|
156
|
+
|
|
157
|
+
Reads resolve once and do not retry: `textContent()`, `inputValue()`,
|
|
158
|
+
`getAttribute(name)`, `isVisible()`, `isEnabled()`, `isChecked()`,
|
|
159
|
+
`boundingBox()`, `count()`. `waitFor({ state?: 'visible' | 'hidden', timeout? })`
|
|
160
|
+
waits for a state. When a value has to settle, use `expect` instead of a
|
|
161
|
+
read. Reading a password field's value is `POLICY_DENIED`.
|
|
162
|
+
|
|
163
|
+
## expect
|
|
164
|
+
|
|
165
|
+
`expect(locator)` polls for up to `config.assertionTimeout` (5 s) or
|
|
166
|
+
`{ timeout }`; `.not` inverts. `expect(web)` gives the browser matchers.
|
|
167
|
+
`expect(value)` is synchronous.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
await expect(screen.getByRole('status')).toHaveText('Saved');
|
|
171
|
+
await expect(screen.getByRole('dialog')).not.toBeVisible({ timeout: 10_000 });
|
|
172
|
+
await expect(screen.getByTestId('todo')).toHaveCount(3);
|
|
173
|
+
await expect(web).toHaveURL('/dashboard'); // relative to the base URL, or a RegExp
|
|
174
|
+
await expect(web).toHaveTitle(/Dashboard/);
|
|
175
|
+
expect(await screen.getByTestId('total').textContent()).toContain('$');
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
| Locator matchers | Web matchers | Value matchers |
|
|
179
|
+
| --- | --- | --- |
|
|
180
|
+
| `toBeVisible`, `toBeHidden`, `toBeEnabled`, `toBeDisabled`, `toBeChecked`, `toBeSelected`, `toBeExpanded`, `toHaveText`, `toContainText`, `toHaveValue`, `toHaveCount`, `toHaveAccessibleName` | `toHaveURL`, `toHaveTitle` | `toBe`, `toEqual`, `toBeTruthy`, `toBeFalsy`, `toBeNull`, `toBeUndefined`, `toBeDefined`, `toContain`, `toMatch`, `toBeGreaterThan`, `toBeLessThan` |
|
|
181
|
+
|
|
182
|
+
`toHaveText` compares the whole normalized text; `toContainText` a
|
|
183
|
+
substring or a RegExp. A failed matcher is `ASSERTION_FAILED`, exit code 1.
|
|
184
|
+
|
|
185
|
+
## Sign-in sessions
|
|
186
|
+
|
|
187
|
+
Sign in once in a setup test, save the state under a name, and let other
|
|
188
|
+
tests declare it. Selecting a dependent test alone still runs its setup.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// tests/auth.setup.e2e.ts
|
|
192
|
+
import { test } from '@e2edev/playwright';
|
|
193
|
+
import { expect, credentials } from '@e2edev/e2e';
|
|
194
|
+
|
|
195
|
+
test.setup('authenticate as admin', { sessions: ['admin'] }, async ({ app, screen, session, web }) => {
|
|
196
|
+
const admin = credentials.user('admin');
|
|
197
|
+
await app.open('/login');
|
|
198
|
+
await screen.getByLabel('Email').fill(admin.username);
|
|
199
|
+
await screen.getByLabel('Password').fill(admin.password);
|
|
200
|
+
await screen.getByRole('button', { name: 'Sign in' }).tap();
|
|
201
|
+
await expect(web).toHaveURL('/dashboard'); // prove the sign-in worked before saving
|
|
202
|
+
await session.save('admin');
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// tests/dashboard.e2e.ts
|
|
208
|
+
import { test, expect } from '@e2edev/e2e';
|
|
209
|
+
|
|
210
|
+
test('the dashboard opens directly', { session: 'admin' }, async ({ app, screen }) => {
|
|
211
|
+
await app.open('/dashboard');
|
|
212
|
+
await expect(screen.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- Setup tests are top-level (not inside `describe`); exactly one setup saves
|
|
217
|
+
a given name; the body saves every declared name once (`SESSION_CONTRACT`
|
|
218
|
+
otherwise).
|
|
219
|
+
- A session holds cookies, local storage, and IndexedDB, for one run only.
|
|
220
|
+
The files are encrypted and deleted at cleanup. Server state is not part of
|
|
221
|
+
it.
|
|
222
|
+
- Credentials live in the config; values come from the environment:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
credentials: {
|
|
226
|
+
admin: { username: 'admin@example.test', password: process.env.ADMIN_PASSWORD ?? '' },
|
|
227
|
+
},
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`E2E_USER_ADMIN_USERNAME` and `E2E_USER_ADMIN_PASSWORD` override either
|
|
231
|
+
field per run. `credentials.user('admin').password` is a `Secret` with no
|
|
232
|
+
plaintext accessor; only `fill()` and `agent.act` params accept it. Once a
|
|
233
|
+
secret is filled, screenshots stop being attached for the rest of that
|
|
234
|
+
attempt, so sign in inside a setup test and keep the evidence in the tests
|
|
235
|
+
that matter.
|
|
236
|
+
|
|
237
|
+
## The web fixture (browser only)
|
|
238
|
+
|
|
239
|
+
Import `test` from `@e2edev/playwright`. Prefer `app` and `screen`; use
|
|
240
|
+
`web` for what only a browser has. Portable suites declare
|
|
241
|
+
`requires: ['web']` so device targets skip the test instead of failing.
|
|
242
|
+
|
|
243
|
+
| Method | Does |
|
|
244
|
+
| --- | --- |
|
|
245
|
+
| `goto(url, { waitUntil? })`, `reload()`, `back()`, `forward()` | Navigation. `goto` accepts a path relative to the base URL. |
|
|
246
|
+
| `url()`, `title()`, `waitForURL(url \| RegExp)` | Reads and a URL wait. |
|
|
247
|
+
| `locator(css)` | Raw CSS or XPath. Not portable; a last resort. |
|
|
248
|
+
| `frameLocator(css)` | A `Screen` scoped to one iframe: `web.frameLocator('#payment').getByLabel('Card number')`. |
|
|
249
|
+
| `evaluate(fn, arg?)` | Runs serialized code in the page. JSON in and out only, no closures. |
|
|
250
|
+
| `route(pattern, handler)`, `unroute(pattern)` | Intercept requests: `route.fulfill({ json })`, `route.continue()`, `route.abort()`. |
|
|
251
|
+
| `waitForResponse(pattern)` | Resolves with `{ status, headers, json(), text() }`. |
|
|
252
|
+
| `cookies()`, `setCookies([...])` | Cookies within `allowedOrigins`. |
|
|
253
|
+
| `setViewport({ width, height })` | Resize. |
|
|
254
|
+
| `onDialog('accept' \| 'dismiss' \| handler)` | Returns an unsubscribe function. Register it before the tap that opens the dialog. |
|
|
255
|
+
| `waitForDownload(() => trigger)` | Returns `{ path, suggestedFilename }`. |
|
|
256
|
+
| `keyboard.press(key)`, `keyboard.type(text)`, `mouse.*` | Unfocused input. Prefer `locator.press` and `locator.fill`. |
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
await web.route('**/api/quote', (route) => route.fulfill({ json: { cents: 4200 } }));
|
|
260
|
+
const response = await web.waitForResponse('**/api/orders');
|
|
261
|
+
expect(response.status).toBe(201);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Patterns that keep suites honest
|
|
265
|
+
|
|
266
|
+
- Find selectors in the source, not by guessing: read the component or
|
|
267
|
+
template for labels, roles, and text. Add an `aria-label` or a heading
|
|
268
|
+
where the app has no accessible name, rather than falling back to CSS.
|
|
269
|
+
- Create the data a test needs under a name unique to the run
|
|
270
|
+
(`Invoice ${Date.now()}`) and clean up in `afterEach`. Replays and retries
|
|
271
|
+
then never trip over leftovers.
|
|
272
|
+
- One flow across several tests: `test.describe('...', { serial: true })`.
|
|
273
|
+
Otherwise tests are independent and may run on different workers.
|
|
274
|
+
- Tag by area and by cost (`smoke`, `billing`, `agent`) and run subsets with
|
|
275
|
+
`--tag`.
|
|
276
|
+
- Mix agent steps in where the path varies and pin the outcome with
|
|
277
|
+
`expect`; see the `agent` topic.
|
|
278
|
+
|
|
279
|
+
## Mistakes to avoid
|
|
280
|
+
|
|
281
|
+
- Sleeps or manual polling loops. Use a matcher with a longer `timeout`.
|
|
282
|
+
- `web.locator('.btn-primary')` when `getByRole('button', { name })` exists.
|
|
283
|
+
- `expect(await locator.textContent()).toBe(...)` for a value that is still
|
|
284
|
+
changing; use `toHaveText`.
|
|
285
|
+
- Hardcoded passwords or tokens.
|
|
286
|
+
- Sharing state between tests without `serial`.
|
|
287
|
+
- `test.only` left in a file: CI fails with `ONLY_IN_CI`.
|
|
288
|
+
- Asserting an exact sentence a model produced; assert the fact with
|
|
289
|
+
`toContain`.
|