e2e 0.21.1 → 0.21.2

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.
Files changed (133) hide show
  1. package/README.md +2 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act-validation.js +1 -0
  4. package/dist/agent/act.js +7 -3
  5. package/dist/agent/action-dispatcher.js +30 -8
  6. package/dist/agent/actions.d.ts +2 -0
  7. package/dist/agent/actions.js +5 -2
  8. package/dist/agent/error.js +1 -0
  9. package/dist/agent/executor.d.ts +14 -6
  10. package/dist/agent/index.js +5 -1
  11. package/dist/agent/invocation.d.ts +3 -0
  12. package/dist/agent/model/sdk.d.ts +5 -0
  13. package/dist/agent/model/sdk.js +18 -1
  14. package/dist/agent/observation.js +1 -0
  15. package/dist/agent/primitives.js +14 -3
  16. package/dist/agent/replay.js +9 -3
  17. package/dist/agent/step-cache.d.ts +3 -2
  18. package/dist/agent/step-cache.js +7 -4
  19. package/dist/agent/tool-loop.js +4 -1
  20. package/dist/cache/context.js +1 -1
  21. package/dist/cache/recorder.d.ts +2 -1
  22. package/dist/cache/recorder.js +11 -2
  23. package/dist/cache/trace-fields.d.ts +10 -0
  24. package/dist/cache/trace-fields.js +17 -0
  25. package/dist/cache/trace.d.ts +9 -1
  26. package/dist/cache/trace.js +26 -7
  27. package/dist/cli/feedback.js +2 -1
  28. package/dist/cli/init/sibling-versions.json +1 -1
  29. package/dist/collect/select.js +12 -2
  30. package/dist/config/agent.d.ts +1 -1
  31. package/dist/config/diagnose.js +9 -3
  32. package/dist/config/esm-hooks.js +3 -2
  33. package/dist/config/resolve.js +3 -2
  34. package/dist/config/typescript.js +52 -2
  35. package/dist/engine/contract.d.ts +3 -2
  36. package/dist/engine/manifest.js +2 -2
  37. package/dist/internal/errors.js +5 -2
  38. package/dist/internal/source.js +2 -0
  39. package/dist/internal/unserved-model.d.ts +5 -0
  40. package/dist/internal/unserved-model.js +22 -0
  41. package/dist/locator/screen.js +1 -2
  42. package/dist/oauth/copilot.js +23 -5
  43. package/dist/oauth/device-code.js +35 -19
  44. package/dist/oauth/errors.d.ts +3 -0
  45. package/dist/oauth/errors.js +26 -11
  46. package/dist/oauth/providers/github-copilot.d.ts +4 -0
  47. package/dist/oauth/providers/github-copilot.js +64 -16
  48. package/dist/oauth/providers/openai.d.ts +2 -2
  49. package/dist/oauth/providers/openai.js +47 -19
  50. package/dist/oauth/providers/opencode-console.js +36 -15
  51. package/dist/oauth/providers/xai.js +18 -9
  52. package/dist/oauth/token-endpoint.d.ts +15 -6
  53. package/dist/oauth/token-endpoint.js +18 -6
  54. package/dist/report/code-frame.js +9 -3
  55. package/dist/report/failure-text.d.ts +10 -2
  56. package/dist/report/failure-text.js +24 -8
  57. package/dist/report/list-model.d.ts +13 -0
  58. package/dist/report/list-steps.d.ts +6 -1
  59. package/dist/report/list-steps.js +29 -0
  60. package/dist/report/list.d.ts +1 -0
  61. package/dist/report/list.js +16 -7
  62. package/dist/report/live-window.js +2 -1
  63. package/dist/report/markdown.js +1 -1
  64. package/dist/run/fixtures.js +2 -0
  65. package/dist/run/host-trace.d.ts +3 -2
  66. package/dist/run/host-trace.js +2 -1
  67. package/dist/run/last-run.d.ts +9 -0
  68. package/dist/run/last-run.js +28 -0
  69. package/dist/run/open-session.d.ts +3 -1
  70. package/dist/run/open-session.js +1 -1
  71. package/dist/run/runner.js +6 -2
  72. package/dist/run/sessions.js +3 -3
  73. package/dist/run/standalone-attempt.d.ts +2 -1
  74. package/dist/run/standalone-attempt.js +3 -1
  75. package/dist/run/steps.d.ts +3 -1
  76. package/dist/run/units.d.ts +8 -0
  77. package/dist/run/units.js +14 -0
  78. package/dist/run/worker-models.d.ts +2 -0
  79. package/dist/run/worker-models.js +7 -0
  80. package/dist/types.d.ts +2 -2
  81. package/docs/authentication.mdx +26 -23
  82. package/docs/browser.mdx +7 -4
  83. package/docs/bug-bash.mdx +2 -2
  84. package/docs/cache.mdx +6 -1
  85. package/docs/ci/bitrise.mdx +1 -1
  86. package/docs/ci/codemagic.mdx +2 -2
  87. package/docs/ci/eas-workflows.mdx +1 -1
  88. package/docs/coding-agents.mdx +7 -35
  89. package/docs/debugging.mdx +24 -36
  90. package/docs/executors.mdx +1 -1
  91. package/docs/github.mdx +2 -1
  92. package/docs/integrations/eas.mdx +1 -1
  93. package/docs/integrations/index.mdx +6 -0
  94. package/docs/integrations/lightpanda.mdx +192 -0
  95. package/docs/integrations/testmu.mdx +152 -0
  96. package/docs/mcp.mdx +123 -0
  97. package/docs/migrate/detox.mdx +1 -1
  98. package/docs/migrate/maestro.mdx +101 -105
  99. package/docs/migrate/playwright.mdx +65 -70
  100. package/docs/mobile.mdx +18 -3
  101. package/docs/models.mdx +4 -2
  102. package/docs/quickstart.mdx +7 -0
  103. package/docs/reference/agent.mdx +369 -277
  104. package/docs/reference/app.mdx +141 -32
  105. package/docs/reference/cli.mdx +674 -558
  106. package/docs/reference/config.mdx +1406 -752
  107. package/docs/reference/engine.mdx +1062 -256
  108. package/docs/reference/environment.mdx +156 -84
  109. package/docs/reference/errors.mdx +106 -72
  110. package/docs/reference/expect.mdx +752 -372
  111. package/docs/reference/mcp.mdx +366 -141
  112. package/docs/reference/mobile.mdx +844 -403
  113. package/docs/reference/reporters.mdx +312 -106
  114. package/docs/reference/runner.mdx +373 -174
  115. package/docs/reference/screen.mdx +931 -398
  116. package/docs/reference/security.mdx +106 -40
  117. package/docs/reference/test.mdx +626 -273
  118. package/docs/reference/web.mdx +956 -533
  119. package/docs/security.mdx +6 -5
  120. package/docs/snippets/model-provider.jsx +763 -0
  121. package/docs/snippets/setup-prompt-card.jsx +22 -0
  122. package/docs/snippets/show-more.jsx +22 -0
  123. package/docs/snippets/video-card.jsx +34 -0
  124. package/docs/snippets/video.jsx +25 -0
  125. package/docs/subscriptions.mdx +10 -3
  126. package/docs/telemetry.mdx +39 -39
  127. package/package.json +3 -2
  128. package/schema/report-v1.schema.json +6 -0
  129. package/skills/e2e/references/agent.md +8 -3
  130. package/skills/e2e/references/debugging.md +1 -1
  131. package/skills/e2e/references/mcp.md +1 -1
  132. package/skills/e2e/references/setup.md +12 -3
  133. package/skills/e2e/references/writing-tests.md +1 -1
@@ -6,9 +6,620 @@ icon: "flask-vial"
6
6
  "twitter:image": "https://e2e.tester.army/docs/images/og/reference/test.png?v=d1ad3b01c3"
7
7
  ---
8
8
 
9
- Declare tests, groups, and hooks synchronously while the test file is
10
- imported. Use `test.extend` to add fixtures and `test.skip` to skip a test at
11
- registration or during its body.
9
+ `test` declares tests, setup tests, groups, and hooks. Declarations run
10
+ synchronously while the test file is imported. Use `test.extend` to add
11
+ fixtures and `test.skip` to skip a test at registration or during its body.
12
+
13
+ ```ts title="tests/checkout.e2e.ts"
14
+ import { beforeEach, describe, expect, test } from 'e2e';
15
+
16
+ describe('checkout', { tags: ['billing'] }, () => {
17
+ beforeEach(async ({ app }) => {
18
+ await app.open('/checkout');
19
+ });
20
+
21
+ test('pays with a saved card', { retries: 2 }, async ({ screen }) => {
22
+ await screen.getByRole('button', 'Pay').tap();
23
+ await expect(screen.getByText('Payment received')).toBeVisible();
24
+ });
25
+ });
26
+ ```
27
+
28
+ ## Reference
29
+
30
+ ### Members
31
+
32
+ | Member | Returns |
33
+ | --- | --- |
34
+ | [`test`](#test) | `TestCase` |
35
+ | [`test.skip`](#test-skip) | `TestCase \| void` |
36
+ | [`test.only`](#test-only) | `TestCase` |
37
+ | [`test.setup`](#test-setup) | `TestCase` |
38
+ | [`test.extend`](#test-extend) | `TestAPI` |
39
+ | [`describe`](#describe) | `void` |
40
+ | [Hooks](#hooks) | `void` |
41
+
42
+ `describe` and the four hooks are also top-level exports, the same functions
43
+ as `test.describe`, `test.beforeEach`, and the rest:
44
+ `import { describe, beforeEach, test } from 'e2e'`.
45
+
46
+ - A per-test hook imported from `e2e` is typed with the core fixtures.
47
+ - `@e2e-dev/web` and `@e2e-dev/mobile` export `test`, `describe`, and all
48
+ four hooks typed with their `browser` or `device` fixture, so a test file
49
+ registers from one import.
50
+ - A `test` you extended with your own fixtures registers hooks that see them
51
+ through `test.beforeEach`.
52
+
53
+ Registering a test, group, or hook outside module evaluation throws
54
+ `COLLECTION_ERROR`. The full declarations are under [Types](#types).
55
+
56
+ #### `test`
57
+
58
+ Registers one test, with or without [options](#options).
59
+
60
+ ```ts
61
+ import { describe, test } from '@e2e-dev/web';
62
+ import { expect } from 'e2e';
63
+
64
+ test('app opens', async ({ app, browser }) => {
65
+ await app.open();
66
+ await expect(browser).toHaveURL('/');
67
+ });
68
+
69
+ test('checks out', { tags: ['billing'], retries: 2, timeout: 60_000 }, async ({ app }) => {
70
+ await app.open('/checkout');
71
+ });
72
+ ```
73
+
74
+ A test or group title is 1 through 512 UTF-8 bytes after NFC normalization,
75
+ with no NUL character.
76
+
77
+ | Throws | When |
78
+ | --- | --- |
79
+ | `COLLECTION_ERROR` | Two tests in one file share a title path: the group titles, then the test's own. |
80
+
81
+ #### `test.skip`
82
+
83
+ Registers a skipped test, or skips the running test.
84
+
85
+ ```ts
86
+ test.skip('exports a PDF', async ({ app }) => {}); // at registration
87
+ test.skip(organizations.length < 2, 'a single organization'); // inside a body
88
+ test.skip('not on this tenant'); // inside a body, always
89
+ ```
90
+
91
+ - A skipped test does not affect the exit code by default.
92
+ - To skip at collection by option, use the [`skip`](#skip) option.
93
+ - At registration it returns a `TestCase`; inside a body, `void`.
94
+
95
+ Call `test.skip(condition, reason)` inside a test for a condition you learn
96
+ at runtime, such as a disabled feature flag:
97
+
98
+ ```ts
99
+ test('switches organizations', { session: 'admin' }, async ({ app, agent }) => {
100
+ const organizations = await listOrganizations();
101
+ test.skip(organizations.length < 2, 'the demo tenant has a single organization');
102
+ await app.open('/backend');
103
+ await agent.act(`switch to the organization "${organizations[1].name}"`);
104
+ });
105
+ ```
106
+
107
+ - When the condition is true, the call throws, as Playwright's does. Nothing
108
+ after the call runs.
109
+ - With no failure before it, the test is reported `skipped` with `reason`,
110
+ with no retry.
111
+ - Steps before it stay in the report as they ran.
112
+ - A failed `expect.soft` before the call still fails the attempt, as in
113
+ Playwright. The attempt is `failed`, with the soft failures as its error and
114
+ the skip reason in its `skip`. It retries like any failure.
115
+ - Hooks and fixtures still run their teardown.
116
+ - When the condition is a precondition, call it first, before any step.
117
+ - A failed attempt followed by a skip on retry leaves the test `skipped`. The
118
+ CLI lists it under `Skipped After Failure` with its original error. Set
119
+ [`failOnSkippedFailure: true`](/reference/config) to fail the run with exit
120
+ code 1 in that case. The default is `false`.
121
+
122
+ | Throws | When |
123
+ | --- | --- |
124
+ | `INVALID_ARGUMENT` | Called in a setup test. A setup test cannot skip because other tests depend on its sessions. |
125
+ | `COLLECTION_ERROR` | The conditional form, outside a test body. |
126
+
127
+ #### `test.only`
128
+
129
+ Registers one focused local test.
130
+
131
+ ```ts
132
+ test.only('pays', async ({ app }) => {
133
+ await app.open('/checkout');
134
+ });
135
+ ```
136
+
137
+ Other tests are reported as `filtered`. A CI run with a focused test fails
138
+ with `ONLY_IN_CI`.
139
+
140
+ #### `test.setup`
141
+
142
+ Registers a setup test that declares the sessions it saves, and must save
143
+ each exactly once.
144
+
145
+ ```ts
146
+ import { credentials } from 'e2e';
147
+
148
+ test.setup('authenticate', { sessions: ['member'] }, async ({ app, screen, session }) => {
149
+ await app.open('/login');
150
+ await screen.getByLabel('Email').fill(credentials.user('member').username);
151
+ await screen.getByLabel('Password').fill(credentials.user('member').password);
152
+ await screen.getByRole('button', 'Sign in').tap();
153
+ await session.save('member');
154
+ });
155
+ ```
156
+
157
+ - `test.setup` must be top-level, not inside `describe`.
158
+ - The [`sessions`](#sessions) option declares the names.
159
+ - A test consumes a saved session with the [`session`](#session) option.
160
+ - The setup body gets one extra fixture, `session`:
161
+
162
+ ```ts
163
+ export interface SetupSession {
164
+ /** Saves state under a setup-declared name. */
165
+ save(name: string): Promise<void>;
166
+ }
167
+ ```
168
+
169
+ | Throws | When |
170
+ | --- | --- |
171
+ | `SESSION_CONTRACT` | At runtime, a declared session was not saved, or was saved twice. |
172
+
173
+ #### `test.extend`
174
+
175
+ Returns a new `test` typed with more fixtures. The one it was called on is
176
+ unchanged.
177
+
178
+ ```ts
179
+ import { test as base } from 'e2e';
180
+ import type { Device } from '@e2e-dev/mobile';
181
+
182
+ export const test = base.extend<{ device: Device }>(); // types only
183
+ ```
184
+
185
+ - With no argument, `extend` refines only the types: it returns the same
186
+ runtime `test`, typed with an engine's contributed fixtures. They still
187
+ resolve from the target's engine at runtime. This types a device, desktop,
188
+ or web surface without a global `declare module` augmentation.
189
+ - With definitions, it adds fixtures of your own. See
190
+ [Your own fixtures](#your-own-fixtures).
191
+
192
+ #### `describe`
193
+
194
+ Declares a group of tests, with or without options.
195
+
196
+ ```ts
197
+ import { describe, test } from 'e2e';
198
+
199
+ describe('checkout', { tags: ['smoke'], serial: true }, () => {
200
+ test('adds an item', async ({ screen }) => {});
201
+ test('pays', async ({ screen }) => {});
202
+ });
203
+ ```
204
+
205
+ - A group takes the [options](#options) a test takes, except `only`, plus
206
+ [`serial`](#serial).
207
+ - The body is synchronous.
208
+
209
+ | Throws | When |
210
+ | --- | --- |
211
+ | `COLLECTION_ERROR` | An `async` body. It is also a type error. |
212
+ | `COLLECTION_ERROR` | `test.setup` inside the body. |
213
+
214
+ ### Hooks
215
+
216
+ | Hook | Runs |
217
+ | --- | --- |
218
+ | [`beforeEach`](#beforeeach) | Before each test attempt |
219
+ | [`afterEach`](#aftereach) | After each test attempt |
220
+ | [`beforeAll`](#beforeall) | Once per suite instance |
221
+ | [`afterAll`](#afterall) | After the scope's last test |
222
+
223
+ Hook order follows scope nesting, not position in the file, so a hook
224
+ declared after a test still applies to it.
225
+
226
+ #### `beforeEach`
227
+
228
+ Registers a test-attempt setup hook.
229
+
230
+ ```ts
231
+ test.beforeEach(async ({ app }) => {
232
+ await app.open('/');
233
+ });
234
+ ```
235
+
236
+ - Runs outermost group first, then in declaration order within a group.
237
+ - `beforeEach` and the body share the test timeout.
238
+ - Receives the test fixtures.
239
+
240
+ #### `afterEach`
241
+
242
+ Registers a test-attempt teardown hook.
243
+
244
+ ```ts
245
+ test.afterEach(async ({ app }) => {
246
+ await app.screenshot('after');
247
+ });
248
+ ```
249
+
250
+ - Runs innermost group first, in reverse declaration order, after a failure
251
+ or timeout too.
252
+ - Each `afterEach` hook gets its own `cleanupTimeout` budget with working
253
+ fixtures, so teardown can still drive the app after the body timed out.
254
+ - Receives the test fixtures.
255
+
256
+ #### `beforeAll`
257
+
258
+ Registers a suite-instance setup hook. It runs once per suite instance, on
259
+ first entry into the scope.
260
+
261
+ ```ts
262
+ beforeAll(async ({ platform }) => {
263
+ await seedDatabase(platform);
264
+ });
265
+ ```
266
+
267
+ The module is re-imported for every retry, serial group, and setup test, so
268
+ `beforeAll` runs again in each. Its only fixture is `platform`.
269
+
270
+ | Throws | When |
271
+ | --- | --- |
272
+ | `HOOK_FAILED` | The hook throws. The suite's tests are skipped with cause `hook-failed`. Inside a serial group it skips every remaining member and fails the group without a retry. |
273
+
274
+ #### `afterAll`
275
+
276
+ Registers a suite-instance teardown hook. It runs as soon as the scope's last
277
+ test in the realm is done.
278
+
279
+ ```ts
280
+ afterAll(async () => {
281
+ await resetDatabase();
282
+ });
283
+ ```
284
+
285
+ A realm that a failure discards runs `afterAll` for every scope whose
286
+ `beforeAll` started. Its only fixture is `platform`. A throw is `HOOK_FAILED`,
287
+ as for [`beforeAll`](#beforeall).
288
+
289
+ #### Blocked event loop
290
+
291
+ A test that blocks its worker's event loop (`while (true) {}` in a fixture,
292
+ `beforeEach`, the body, or `afterEach`) stops every timer in that worker, so
293
+ the worker cannot time it out.
294
+
295
+ - When the worker has not responded `cleanupTimeout` (at least 5 s) after the
296
+ test timeout, the runner kills it.
297
+ - That attempt is reported `timed-out` with `TEST_TIMEOUT` and no artifacts,
298
+ after the attempts before it, and is not retried.
299
+ - The rest of its file runs on a new worker. In a serial group, the members
300
+ after it are skipped.
301
+ - A `beforeAll` or `afterAll` that blocks still hangs the run.
302
+
303
+ <Note>
304
+ A worker with a debugger attached when the test starts (an editor's
305
+ auto-attach, or `NODE_OPTIONS=--inspect=0`) is never killed this way, so a
306
+ breakpoint can stay paused. A programmatic run with an inline config or
307
+ in-memory tests runs in the runner's own process and has no worker to kill: a
308
+ blocked event loop hangs it.
309
+ </Note>
310
+
311
+ ### Options
312
+
313
+ | Option | Type | Default |
314
+ | --- | --- | --- |
315
+ | [`timeout`](#timeout) | `number` | `config.timeout` |
316
+ | [`retries`](#retries) | `number` | `config.retries` |
317
+ | [`tags`](#tags) | `readonly string[]` | - |
318
+ | [`skip`](#skip) | `boolean \| string` | - |
319
+ | [`only`](#only) | `boolean` | - |
320
+ | [`platforms`](#platforms) | `readonly string[]` | - |
321
+ | [`requires`](#requires) | `readonly Capability[]` | - |
322
+ | [`session`](#session) | `string` | - |
323
+ | [`agentContext`](#agentcontext) | `string` | - |
324
+ | [`agent`](#agent) | `string \| readonly string[]` | the run's agents |
325
+ | [`trace`](#trace) | `RecordingMode` | the run's |
326
+ | [`video`](#video) | `RecordingMode` | the run's |
327
+ | [`serial`](#serial) | `boolean` | - |
328
+ | [`sessions`](#sessions) | `readonly string[]` | required |
329
+
330
+ `test` takes every option but `serial` and `sessions`. `describe` takes
331
+ every option but `only` and `sessions`. `test.setup` requires `sessions` and
332
+ takes no `session`, `only`, or `skip`.
333
+
334
+ Resolution order: test, nearest group, outer groups, config, built-in
335
+ default. Inside a [serial group](#serial), the group's `retries`, `trace`, and
336
+ `video` apply.
337
+
338
+ | Throws | When |
339
+ | --- | --- |
340
+ | `COLLECTION_ERROR` | An option key the call does not take (`{ timout: 1000 }`). The error names the nearest known key when it is a likely typo, and every key the call takes otherwise. |
341
+
342
+ #### `timeout`
343
+
344
+ The attempt deadline in milliseconds. Innermost wins.
345
+
346
+ ```ts
347
+ test('imports a large file', { timeout: 300_000 }, async ({ app }) => {}); // default: config.timeout
348
+ ```
349
+
350
+ `timeout` must be a positive safe integer.
351
+
352
+ #### `retries`
353
+
354
+ How many times a failed test is retried.
355
+
356
+ ```ts
357
+ test('checks out', { retries: 2 }, async ({ app }) => {}); // default: config.retries
358
+ ```
359
+
360
+ An integer from 0 through 10.
361
+
362
+ #### `tags`
363
+
364
+ Tags that `--tag` selects on. Every layer's tags are unioned.
365
+
366
+ ```ts
367
+ describe('checkout', { tags: ['billing', 'smoke'] }, () => {});
368
+ ```
369
+
370
+ Tags are distinct names, none blank, with no comma and no leading or
371
+ trailing whitespace.
372
+
373
+ #### `skip`
374
+
375
+ Skips the test at collection. A string is the reason. Any truthy layer skips.
376
+
377
+ ```ts
378
+ test('exports a PDF', { skip: 'PDF export is off on staging' }, async ({ app }) => {});
379
+ ```
380
+
381
+ #### `only`
382
+
383
+ Focuses the test locally. CI rejects it with `ONLY_IN_CI`.
384
+
385
+ ```ts
386
+ test('pays', { only: true }, async ({ app }) => {});
387
+ ```
388
+
389
+ #### `platforms`
390
+
391
+ Platforms the test runs on. Other targets skip it. Innermost wins.
392
+
393
+ ```ts
394
+ test('uses Face ID', { platforms: ['ios'] }, async ({ app }) => {});
395
+ ```
396
+
397
+ #### `requires`
398
+
399
+ Capabilities the target must have. A target that lacks one skips the test at
400
+ selection. Innermost wins.
401
+
402
+ ```ts
403
+ test('onboarding asks for the camera', { requires: ['native-app'] }, async ({ app, agent }) => {
404
+ await app.open();
405
+ await agent.assert('The app asks for camera access');
406
+ });
407
+ ```
408
+
409
+ Accepted values:
410
+
411
+ - The harness capabilities of the target's engine: `observation`, `actions`,
412
+ `location`, `pointer`, `keyboard`, `state`, `artifacts`.
413
+ - One per contributed fixture: `browser`, `device`. See
414
+ [the engine contract](/reference/engine#engine).
415
+ - The kind of app the target declares: `browser` for an `app.url`,
416
+ `native-app` for an `app.bundleId` or `app.appPath`.
417
+
418
+ #### `session`
419
+
420
+ The session a setup test saved, restored before the body runs.
421
+
422
+ ```ts
423
+ test('sees the dashboard', { session: 'member' }, async ({ app }) => {});
424
+ ```
425
+
426
+ See [`test.setup`](#test-setup).
427
+
428
+ #### `agentContext`
429
+
430
+ Extra context for agent steps. Layers are concatenated outermost first.
431
+
432
+ ```ts
433
+ describe('pricing', { agentContext: 'Prices are shown in EUR.' }, () => {});
434
+ ```
435
+
436
+ #### `agent`
437
+
438
+ The configured agent (`agents.<name>`) this test or group runs with, in
439
+ place of the run's agents.
440
+
441
+ ```ts
442
+ test('checks out', { agent: ['fast', 'smart'] }, async ({ agent }) => {}); // one result per agent
443
+ ```
444
+
445
+ - A list runs the test once per agent named, as one result each.
446
+ - Innermost wins. A call's own `agent` option wins over it.
447
+ - `--agent` narrows a list to the names both name, and never overrides a pin
448
+ the flag does not name.
449
+ - A setup test runs once per target, so it pins at most one agent.
450
+
451
+ | Throws | When |
452
+ | --- | --- |
453
+ | `COLLECTION_ERROR` | At selection, before any test runs: a name that `agents` does not define. |
454
+
455
+ #### `trace`
456
+
457
+ Whether the test keeps a trace page, in place of the run's.
458
+
459
+ ```ts
460
+ test('checkout', { trace: 'retain-on-failure' }, async ({ agent }) => {});
461
+ ```
462
+
463
+ - Takes a [recording mode](/reference/config#recording), the same as the
464
+ config's `trace`: `off`, `on`, `retain-on-failure`, `on-first-retry`, or
465
+ `on-all-retries`.
466
+ - A test's or group's own value wins over `--trace`, the target's, and the
467
+ config's. One test can record on a run that records nothing, or opt out of
468
+ one that records everything.
469
+
470
+ #### `video`
471
+
472
+ Which of the test's attempts record a video, in place of the run's.
473
+
474
+ ```ts
475
+ test('checkout', { video: 'on' }, async ({ agent }) => {
476
+ await agent.act('Buy the first product in the catalog');
477
+ });
478
+ ```
479
+
480
+ - Takes the same [recording modes](/reference/config#recording) as `trace`
481
+ and the config's `video`.
482
+ - A test's or group's own value wins over `--video`, the target's, and the
483
+ config's.
484
+
485
+ | Throws | When |
486
+ | --- | --- |
487
+ | `UNSUPPORTED_ARTIFACT` | Before any test starts, naming the test: the mode records on some attempt and the target's engine cannot record video. |
488
+
489
+ #### `serial`
490
+
491
+ `describe` only. Runs the group as one ordered retry unit with shared app
492
+ state.
493
+
494
+ ```ts
495
+ describe('checkout', { serial: true }, () => {
496
+ test('adds an item', async ({ screen }) => {});
497
+ test('pays', async ({ screen }) => {});
498
+ });
499
+ ```
500
+
501
+ - The group also shares secret state: a secret fill withholds pixels for the
502
+ rest of the group's attempt. A retry starts fresh.
503
+ - Members take the `trace` and `video` on the serial `describe` or a group
504
+ around it. The group records one video per group attempt.
505
+ - A group attempt may not start because its file fails to load or its session
506
+ fails to launch. Then the first member fails with that error, and the rest
507
+ skip with cause `serial-predecessor-failed`. When the run is interrupted
508
+ during the launch, every member skips instead.
509
+ - On a retry, the members keep the verdicts of the attempt before it.
510
+ - The exit code follows the error's category, as for any other test.
511
+
512
+ | Throws | When |
513
+ | --- | --- |
514
+ | `COLLECTION_ERROR` | A member sets `retries`, `trace`, `video`, `session`, `platforms`, `requires`, `skip`, `only`, or a nested `serial`. |
515
+
516
+ #### `sessions`
517
+
518
+ `test.setup` only. The session names the setup test must save.
519
+
520
+ ```ts
521
+ test.setup('authenticate', { sessions: ['member', 'admin'] }, async ({ session }) => {});
522
+ ```
523
+
524
+ A static, non-empty array of unique names. Each name matches
525
+ `/^[A-Za-z0-9_.-]{1,128}$/`. See [`test.setup`](#test-setup).
526
+
527
+ ### Fixtures
528
+
529
+ | Fixture | Type |
530
+ | --- | --- |
531
+ | [`agent`](/reference/agent) | `Agent` |
532
+ | [`app`](/reference/app) | `App` |
533
+ | [`screen`](/reference/screen) | `Screen` |
534
+ | `platform` | `string` |
535
+ | [`session`](#test-setup) | `SetupSession` |
536
+
537
+ `agent`, `app`, and `screen` reach test bodies, `beforeEach`, and
538
+ `afterEach`. `platform` reaches every hook too, and `session` only
539
+ `test.setup` bodies. [`TestFixtures`](#testfixtures) describes each one.
540
+
541
+ Everything else is a fixture the target's engine contributes, or one of
542
+ [your own](#your-own-fixtures). `@e2e-dev/web` contributes
543
+ [`browser`](/reference/web); a device engine contributes `device`.
544
+
545
+ | Throws | When |
546
+ | --- | --- |
547
+ | `UNSUPPORTED_CAPABILITY` | A test reaches for a fixture the engine does not contribute. Declare `requires: ['browser']` to skip such tests at selection instead. |
548
+
549
+ <Note>
550
+ The fixtures above are lazy. Destructuring in the parameter list acquires
551
+ them before the body's first statement. [Your own
552
+ fixtures](#your-own-fixtures) are not lazy.
553
+ </Note>
554
+
555
+ #### Your own fixtures
556
+
557
+ `test.extend(fixtures)` defines resources set up before each test and cleaned
558
+ up afterward, including after a failure. In each fixture function, code before
559
+ `await use(value)` sets up the resource. The test receives `value`, and code
560
+ after `use` tears it down.
561
+
562
+ ```ts
563
+ import { test as base } from '@e2e-dev/web';
564
+
565
+ export const test = base.extend<{ workspace: Workspace }>({
566
+ workspace: async ({ browser }, use) => {
567
+ const workspace = await createWorkspace();
568
+ await signIn(browser, workspace);
569
+ await use(workspace); // the hooks and the body run here
570
+ await workspace.cleanup(); // teardown, after the body even when it failed
571
+ },
572
+ });
573
+
574
+ test('renames the workspace', async ({ workspace, screen }) => {
575
+ await screen.getByLabel('Name').fill(`${workspace.name} (renamed)`);
576
+ });
577
+ ```
578
+
579
+ A definition is typed against the fixtures of the `test` it extends, not its siblings in
580
+ the same call. A fixture that needs another goes in a chained second
581
+ `extend`: `base.extend({ a }).extend({ b })` gives `b` a fixture object that
582
+ already has `a`.
583
+
584
+ Per attempt:
585
+
586
+ 1. Each definition in the `extend` chain the test was registered through
587
+ runs in declaration order, after the engine's fixtures, until it calls
588
+ `use`. It runs whether or not the test names it. A fixture that opens a
589
+ page or changes app state does so for every test of that `test`. So a
590
+ file whose tests need different setups registers them through different
591
+ `extend` chains. Setup counts against the test `timeout`. A failure is
592
+ reported in phase `beforeEach`, before any `beforeEach` hook has run.
593
+ 2. `beforeEach` hooks, the body, then `afterEach` hooks run with the same
594
+ fixture object. The fixture set is the test's own chain: a hook that reads
595
+ `workspace` around a test registered through `base` fails with
596
+ `UNSUPPORTED_CAPABILITY`.
597
+ 3. Teardowns run in reverse order after the last `afterEach`, whether or not
598
+ the body passed, each with its own `cleanupTimeout` budget. An error there
599
+ fails a passing attempt in phase `afterEach` and joins `secondaryErrors`
600
+ after a body failure.
601
+
602
+ | Mistake | Fails |
603
+ | --- | --- |
604
+ | A value that is not a plain object of functions, an empty name, a core name (`agent`, `app`, `screen`, `platform`, `session`), or a name an earlier `extend` in the chain defined | At import, `COLLECTION_ERROR` naming the fixture |
605
+ | A name the target's engine contributes (`browser` on a Playwright target) | The attempt, in `beforeEach`, `TEST_SETUP_FAILED` |
606
+ | A definition that returns without calling `use` | The attempt, in `beforeEach`, `TEST_SETUP_FAILED` |
607
+ | A definition that calls `use` twice | The attempt, at teardown, `TEST_SETUP_FAILED` |
608
+ | A definition that throws before `use` | The attempt, in `beforeEach`, with that error |
609
+ | A definition that throws after `use` | The attempt, in `afterEach`, with that error |
610
+
611
+ <Note>
612
+ A body or hook that hits the test `timeout` is abandoned, not cancelled, so
613
+ teardown starts while it may still be running. A setup abandoned this way
614
+ that reaches `use` later gets an immediate resolve. Its teardown runs
615
+ detached from the run, so what it allocated is still released. Later
616
+ fixtures, hooks, and the test body do not start after an abandoned setup or
617
+ hook finishes.
618
+ </Note>
619
+
620
+ ## Types
621
+
622
+ ### `TestAPI`
12
623
 
13
624
  ```ts
14
625
  export interface TestAPI<Fixtures = TestFixtures> {
@@ -20,9 +631,10 @@ export interface TestAPI<Fixtures = TestFixtures> {
20
631
  skip(title: string, fn: TestFn<Fixtures>): TestCase;
21
632
  /**
22
633
  * Inside a running test: skips it when `condition` is true (or always,
23
- * with no condition), ending the body there. The attempt is reported
24
- * `skipped` with `reason`; steps that already ran stay in the report, and
25
- * teardown still runs. A setup test cannot skip: its sessions are owed.
634
+ * with no condition) by throwing, so nothing after the call runs. The
635
+ * attempt is reported `skipped` with `reason`; steps that already ran stay
636
+ * in the report without deciding the status, and teardown still runs. A
637
+ * setup test cannot skip: its sessions are owed.
26
638
  * Outside a test body this form is `COLLECTION_ERROR`; use the `skip`
27
639
  * option to skip at collection.
28
640
  */
@@ -82,138 +694,7 @@ export const beforeAll: TestAPI['beforeAll'];
82
694
  export const afterAll: TestAPI['afterAll'];
83
695
  ```
84
696
 
85
- `describe` and the four hooks are also top-level exports, the same functions
86
- as `test.describe` and `test.beforeEach` and the rest:
87
- `import { describe, beforeEach, test } from 'e2e'`. A per-test hook imported
88
- from `e2e` is typed with the core fixtures. `@e2e-dev/web` and
89
- `@e2e-dev/mobile` export `test`, `describe`, and all four hooks typed with
90
- their `browser` or `device` fixture, so a test file registers from one
91
- import, and a `test` you extended with your own fixtures registers hooks
92
- that see them through `test.beforeEach`.
93
-
94
- Registering a test, group, or hook outside module evaluation throws
95
- `COLLECTION_ERROR`.
96
-
97
- A test or group title is 1 through 512 UTF-8 bytes after NFC normalization,
98
- with no NUL character. Two tests in one file with the same title path, the
99
- group titles and then the test's own, are a `COLLECTION_ERROR`.
100
-
101
- ## Members
102
-
103
- A skipped test does not affect the exit code by default. With `test.only`, other tests
104
- are reported as `filtered`, and a CI run fails with `ONLY_IN_CI`.
105
-
106
- Call `test.skip(condition, reason)` inside a test for conditions you learn at
107
- runtime, such as a disabled feature flag. When the condition is true, the call
108
- throws: the body stops at that line, as Playwright's does. Nothing after the
109
- call runs. With no failure before it, the test is reported `skipped` with
110
- `reason`, with no retry.
111
- Steps before it stay in the report as they ran. A failed `expect.soft` before
112
- the call still fails the attempt, as in Playwright: the attempt is `failed`
113
- with the soft failures as its error and the skip reason in its `skip`, and it
114
- retries like any failure. Hooks and fixtures still run their teardown. When
115
- the condition is a precondition, call it first, before any step.
116
-
117
- A failed attempt followed by a skip on retry leaves the test `skipped`, and
118
- the CLI lists it under `Skipped After Failure` with its original error. Set
119
- [`failOnSkippedFailure: true`](/reference/config) to fail the run with exit
120
- code 1 in that case. The default is `false`.
121
-
122
- A setup test cannot skip because other tests depend on its sessions; the
123
- call fails with `INVALID_ARGUMENT`. Outside a test body, the conditional
124
- form is `COLLECTION_ERROR`. Use the `skip` option when declaring the test.
125
-
126
- ```ts
127
- test('switches organizations', { session: 'admin' }, async ({ app, agent }) => {
128
- const organizations = await listOrganizations();
129
- test.skip(organizations.length < 2, 'the demo tenant has a single organization');
130
- await app.open('/backend');
131
- await agent.act(`switch to the organization "${organizations[1].name}"`);
132
- });
133
- ```
134
- `test.setup` must be top-level, not inside `describe`. An `async`
135
- `describe` body is a type error and a `COLLECTION_ERROR`.
136
-
137
- ```ts
138
- import { describe, test } from '@e2e-dev/web';
139
- import { expect } from 'e2e';
140
-
141
- test('app opens', async ({ app, browser }) => {
142
- await app.open();
143
- await expect(browser).toHaveURL('/');
144
- });
145
-
146
- test('checks out', { tags: ['billing'], retries: 2, timeout: 60_000 }, async ({ app }) => {
147
- await app.open('/checkout');
148
- });
149
-
150
- describe('checkout', { tags: ['smoke'], serial: true }, () => {
151
- test('adds an item', async ({ screen }) => {});
152
- test('pays', async ({ screen }) => {});
153
- });
154
- ```
155
-
156
- `test.setup` declares the sessions it saves, and must save each exactly once:
157
-
158
- ```ts
159
- import { credentials } from 'e2e';
160
-
161
- test.setup('authenticate', { sessions: ['member'] }, async ({ app, screen, session }) => {
162
- await app.open('/login');
163
- await screen.getByLabel('Email').fill(credentials.user('member').username);
164
- await screen.getByLabel('Password').fill(credentials.user('member').password);
165
- await screen.getByRole('button', 'Sign in').tap();
166
- await session.save('member');
167
- });
168
- ```
169
-
170
- Session names must match `/^[A-Za-z0-9_.-]{1,128}$/` and be unique.
171
- Not saving a declared session, or saving one twice, throws `SESSION_CONTRACT`
172
- at runtime.
173
-
174
- ```ts
175
- export interface SetupSession {
176
- /** Saves state under a setup-declared name. */
177
- save(name: string): Promise<void>;
178
- }
179
- ```
180
-
181
- ## Hooks
182
-
183
- `beforeEach` hooks run outermost group first, then in declaration order
184
- within a group. `afterEach` hooks run innermost first, in reverse declaration
185
- order, after a failure or timeout too. `beforeAll` runs once per suite
186
- instance on first entry into the scope; `afterAll` when the scope's last test
187
- in the realm finishes. Hook order follows scope nesting, not position in the
188
- file, so a hook declared after a test still applies to it.
189
-
190
- The module is re-imported for every retry, serial group, and setup test, so
191
- `beforeAll` runs again in each. A scope's `afterAll` runs as soon as its last
192
- test in that realm is done. A realm that a failure discards runs `afterAll`
193
- for every scope whose `beforeAll` started.
194
-
195
- `beforeEach` and the body share the test timeout. Each `afterEach` hook gets
196
- its own `cleanupTimeout` budget with working fixtures, so teardown can still
197
- drive the app after the body timed out.
198
-
199
- A test that blocks its worker's event loop (`while (true) {}` in a fixture,
200
- `beforeEach`, the body, or `afterEach`) stops every timer in that worker, so
201
- the worker cannot time it out. A `beforeAll` or `afterAll` that blocks still
202
- hangs the run. When the worker has not responded `cleanupTimeout` (at least 5 s) after
203
- the test timeout, the runner kills it. That attempt is reported `timed-out`
204
- with `TEST_TIMEOUT` and no artifacts, after the attempts before it, and is
205
- not retried. The rest of its file runs on a new worker. In a serial group,
206
- the members after it are skipped. A worker with a debugger attached when the
207
- test starts (an editor's auto-attach, or `NODE_OPTIONS=--inspect=0`) is
208
- never killed this way, so a breakpoint can stay paused. A programmatic run with an inline
209
- config or in-memory tests runs in the runner's own process and has no worker
210
- to kill: a blocked event loop hangs it.
211
-
212
- A throw inside `beforeAll` or `afterAll` is `HOOK_FAILED` and skips the
213
- suite's tests with cause `hook-failed`. Inside a serial group it skips every
214
- remaining member and fails the group without a retry.
215
-
216
- ## Options
697
+ ### `TestOptions`
217
698
 
218
699
  ```ts
219
700
  export interface TestOptions {
@@ -244,9 +725,10 @@ export interface TestOptions {
244
725
  */
245
726
  agent?: string | readonly string[];
246
727
  /**
247
- * Whether the test keeps a trace page, in place of the run's: the same
248
- * modes as the config's `trace`. Innermost wins, over `--trace` too;
249
- * inside a serial group the group's value applies.
728
+ * Which of the test's attempts keep a trace, in place of the run's: the
729
+ * same modes as the config's `trace`. Innermost wins, over `--trace` too;
730
+ * inside a serial group the group's value applies, since the group runs as
731
+ * one unit.
250
732
  */
251
733
  trace?: RecordingMode;
252
734
  /**
@@ -271,62 +753,7 @@ export interface SetupOptions extends Omit<TestOptions, 'session' | 'only' | 'sk
271
753
  }
272
754
  ```
273
755
 
274
- `timeout` must be a positive safe integer; `retries` must be an integer from
275
- 0 through 10. Groups do not accept `only`. An option key the call does not
276
- take (`{ timout: 1000 }`) is `COLLECTION_ERROR`, naming the nearest known key
277
- when it is a likely typo and every key the call takes otherwise. `test.setup` requires `sessions`
278
- to be a static, non-empty array of unique names.
279
-
280
- Resolution order: test, nearest group, outer groups, config, built-in default.
281
-
282
- `requires` names capabilities of the target's engine, the harness ones
283
- (`observation`, `actions`, `location`, `pointer`, `keyboard`, `state`,
284
- `artifacts`) and one per contributed fixture (`browser`, `device`; see
285
- [the engine contract](/reference/engine#engine)), or the kind of app the
286
- target declares: `browser` for an `app.url`, `native-app` for an
287
- `app.bundleId` or `app.appPath`. A target that lacks one skips the test at
288
- selection:
289
-
290
- ```ts
291
- test('onboarding asks for the camera', { requires: ['native-app'] }, async ({ app, agent }) => {
292
- await app.open();
293
- await agent.assert('The app asks for camera access');
294
- });
295
- ```
296
-
297
- An `agent` name that `agents` does not define is a `COLLECTION_ERROR` at
298
- selection, before any test runs.
299
-
300
- `trace` and `video` take a [recording mode](/reference/config#recording):
301
- `off`, `on`, `retain-on-failure`, `on-first-retry`, or `on-all-retries`. A
302
- test's or group's own value wins over `--trace` / `--video`, the target's,
303
- and the config's, so one test can record on a run that records nothing, or
304
- opt out of one that records everything. A `video` mode set here is required:
305
- when it records on some attempt and the target's engine cannot record video,
306
- the run fails with `UNSUPPORTED_ARTIFACT` before any test starts, naming the
307
- test:
308
-
309
- ```ts
310
- test('checkout', { video: 'on' }, async ({ agent }) => {
311
- await agent.act('Buy the first product in the catalog');
312
- });
313
- ```
314
-
315
- `serial: true` makes the group one ordered retry unit with shared app state
316
- and shared secret state: a secret fill withholds pixels for the rest of the
317
- group's attempt. A retry starts fresh. Inside it, `retries`, `trace`,
318
- `video`, `session`, `platforms`, `requires`, `skip`, `only`, and a nested
319
- `serial` on a member are collection errors. Set them on the group. Members
320
- take the `trace` and `video` on the serial `describe` or a group around it,
321
- and the group records one video per group attempt. A group attempt that
322
- cannot start, because its file fails to load or its session fails to launch,
323
- fails the first member with that error and skips the rest with cause
324
- `serial-predecessor-failed`. When the run is interrupted during the launch,
325
- every member skips instead. On a retry, the members keep the verdicts of
326
- the attempt before it. The exit code follows the error's category, as for any
327
- other test.
328
-
329
- ## Fixtures
756
+ ### `TestFixtures`
330
757
 
331
758
  ```ts
332
759
  export interface TestFixtures {
@@ -351,41 +778,7 @@ export interface SuiteFixtures {
351
778
  }
352
779
  ```
353
780
 
354
- Everything else is a fixture the target's engine contributes, or one of
355
- [your own](#your-own-fixtures). `@e2e-dev/web` contributes
356
- [`browser`](/reference/web) and exports a `test` typed with it; a device
357
- engine contributes `device`. Reaching for a fixture the engine does not
358
- contribute fails with `UNSUPPORTED_CAPABILITY`; declare `requires: ['browser']`
359
- to skip such tests at selection instead.
360
-
361
- The fixtures above are lazy. Destructuring in the parameter list acquires
362
- them before the body's first statement. Your own fixtures are not: every
363
- fixture in the `extend` chain a test was registered through sets up for that
364
- test, even when its parameters omit it.
365
-
366
- ### Your own fixtures
367
-
368
- `test.extend(fixtures)` defines resources set up before each test and cleaned
369
- up afterward, including after a failure. In each fixture function, code before
370
- `await use(value)` sets up the resource. The test receives `value`, and code
371
- after `use` tears it down.
372
-
373
- ```ts
374
- import { test as base } from '@e2e-dev/web';
375
-
376
- export const test = base.extend<{ workspace: Workspace }>({
377
- workspace: async ({ browser }, use) => {
378
- const workspace = await createWorkspace();
379
- await signIn(browser, workspace);
380
- await use(workspace); // the hooks and the body run here
381
- await workspace.cleanup(); // teardown, after the body even when it failed
382
- },
383
- });
384
-
385
- test('renames the workspace', async ({ workspace, screen }) => {
386
- await screen.getByLabel('Name').fill(`${workspace.name} (renamed)`);
387
- });
388
- ```
781
+ ### `FixtureDefinitions`
389
782
 
390
783
  ```ts
391
784
  /**
@@ -410,47 +803,7 @@ export type FixtureDefinitions<Base, Extra> = {
410
803
  };
411
804
  ```
412
805
 
413
- `extend` returns a new `test`; the one it was called on is unchanged. A
414
- definition is typed against the fixtures of the `test` it extends, not its
415
- siblings in the same call. A fixture that needs another goes in a chained
416
- second `extend`: `base.extend({ a }).extend({ b })` gives `b` a fixture
417
- object that already has `a`.
418
-
419
- Per attempt:
420
-
421
- 1. Each definition runs in declaration order until it calls `use`, whether
422
- or not the test names it: a fixture that opens a page or changes app
423
- state does so for every test of that `test`, so a file whose tests need
424
- different setups registers them through different `extend` chains. Setup
425
- counts against the test `timeout`; a failure is reported in phase
426
- `beforeEach`, before any `beforeEach` hook has run.
427
- 2. `beforeEach` hooks, the body, then `afterEach` hooks run with the same
428
- fixture object. The fixture set is the test's own chain: a hook that reads
429
- `workspace` around a test registered through `base` fails with
430
- `UNSUPPORTED_CAPABILITY`.
431
- 3. Teardowns run in reverse order after the last `afterEach`, whether or not
432
- the body passed, each with its own `cleanupTimeout` budget. An error there
433
- fails a passing attempt in phase `afterEach` and joins `secondaryErrors`
434
- after a body failure.
435
-
436
- What fails, and where:
437
-
438
- | Mistake | Fails |
439
- | --- | --- |
440
- | A value that is not a plain object of functions, an empty name, a core name (`agent`, `app`, `screen`, `platform`, `session`), or a name an earlier `extend` in the chain defined | At import, `COLLECTION_ERROR` naming the fixture |
441
- | A name the target's engine contributes (`browser` on a Playwright target) | The attempt, in `beforeEach`, `TEST_SETUP_FAILED` |
442
- | A definition that returns without calling `use` | The attempt, in `beforeEach`, `TEST_SETUP_FAILED` |
443
- | A definition that calls `use` twice | The attempt, at teardown, `TEST_SETUP_FAILED` |
444
- | A definition that throws before `use` | The attempt, in `beforeEach`, with that error |
445
- | A definition that throws after `use` | The attempt, in `afterEach`, with that error |
446
-
447
- A body or hook that hits the test `timeout` is abandoned, not cancelled, so
448
- teardown starts while it may still be running. A setup abandoned this way
449
- that reaches `use` later gets an immediate resolve, and its teardown runs
450
- detached from the run, so what it allocated is still released.
451
- Later fixtures, hooks, and the test body do not start after an abandoned setup or hook finishes.
452
-
453
- ## Function types
806
+ ### Function types
454
807
 
455
808
  ```ts
456
809
  /** A test body. */