e2e 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/.build.json +1 -1
  2. package/dist/agent/act.js +7 -4
  3. package/dist/agent/action-dispatcher.js +4 -5
  4. package/dist/agent/actions.d.ts +32 -23
  5. package/dist/agent/actions.js +38 -24
  6. package/dist/agent/model/sdk.js +2 -0
  7. package/dist/agent/observation-feed.d.ts +5 -6
  8. package/dist/agent/observation-feed.js +5 -7
  9. package/dist/agent/observation.d.ts +40 -11
  10. package/dist/agent/observation.js +109 -59
  11. package/dist/agent/point-tap.d.ts +8 -8
  12. package/dist/agent/point-tap.js +1 -1
  13. package/dist/agent/replay.d.ts +19 -8
  14. package/dist/agent/replay.js +11 -14
  15. package/dist/agent/scroll-target.d.ts +2 -3
  16. package/dist/agent/step-cache.d.ts +41 -14
  17. package/dist/agent/step-cache.js +147 -62
  18. package/dist/agent/tool-loop.js +2 -0
  19. package/dist/cache/anchors.d.ts +72 -23
  20. package/dist/cache/anchors.js +233 -60
  21. package/dist/cache/context.d.ts +25 -14
  22. package/dist/cache/context.js +50 -8
  23. package/dist/cache/decide.d.ts +2 -2
  24. package/dist/cache/decide.js +4 -6
  25. package/dist/cache/identity.d.ts +18 -4
  26. package/dist/cache/identity.js +9 -4
  27. package/dist/cache/recorder.d.ts +15 -1
  28. package/dist/cache/recorder.js +36 -23
  29. package/dist/cache/rekeyed.d.ts +46 -0
  30. package/dist/cache/rekeyed.js +89 -0
  31. package/dist/cache/relocate.d.ts +14 -14
  32. package/dist/cache/relocate.js +33 -25
  33. package/dist/cache/route.d.ts +32 -33
  34. package/dist/cache/route.js +94 -50
  35. package/dist/cache/store.d.ts +6 -0
  36. package/dist/cache/store.js +21 -1
  37. package/dist/cache/trace.d.ts +53 -14
  38. package/dist/cache/trace.js +70 -28
  39. package/dist/cli/index.js +8 -8
  40. package/dist/cli/init/engines.d.ts +1 -3
  41. package/dist/cli/init/engines.js +2 -2
  42. package/dist/cli/init/gateways.d.ts +1 -1
  43. package/dist/cli/init/gateways.js +11 -1
  44. package/dist/cli/init/sibling-versions.json +1 -1
  45. package/dist/cli/init/versions.d.ts +0 -8
  46. package/dist/cli/init/versions.js +1 -10
  47. package/dist/config/resolve.d.ts +1 -0
  48. package/dist/config/resolve.js +33 -1
  49. package/dist/engine/index.d.ts +2 -2
  50. package/dist/expect/async.js +2 -1
  51. package/dist/expect/poll-scope.d.ts +52 -0
  52. package/dist/expect/poll-scope.js +113 -0
  53. package/dist/expect/poll.js +31 -4
  54. package/dist/expect/values.js +11 -4
  55. package/dist/internal/abandoned.d.ts +11 -0
  56. package/dist/internal/abandoned.js +23 -0
  57. package/dist/internal/brands.d.ts +2 -0
  58. package/dist/internal/brands.js +2 -0
  59. package/dist/internal/client-identity.d.ts +9 -0
  60. package/dist/internal/client-identity.js +14 -0
  61. package/dist/internal/redact.d.ts +7 -4
  62. package/dist/internal/redact.js +116 -23
  63. package/dist/internal/time.d.ts +6 -0
  64. package/dist/internal/time.js +25 -13
  65. package/dist/internal/urls.d.ts +9 -5
  66. package/dist/internal/urls.js +19 -10
  67. package/dist/locator/engine.d.ts +12 -2
  68. package/dist/locator/engine.js +8 -3
  69. package/dist/locator/screen.js +28 -4
  70. package/dist/mcp/catalog.d.ts +3 -2
  71. package/dist/mcp/catalog.js +6 -6
  72. package/dist/mcp/session.js +1 -1
  73. package/dist/oauth/chatgpt.js +3 -23
  74. package/dist/oauth/cli.js +1 -0
  75. package/dist/oauth/copilot.d.ts +5 -4
  76. package/dist/oauth/copilot.js +80 -7
  77. package/dist/oauth/grok.js +2 -1
  78. package/dist/oauth/models.js +2 -1
  79. package/dist/oauth/opencode-console.d.ts +10 -0
  80. package/dist/oauth/opencode-console.js +133 -0
  81. package/dist/oauth/providers/github-copilot.d.ts +12 -0
  82. package/dist/oauth/providers/github-copilot.js +77 -23
  83. package/dist/oauth/providers/opencode-console.d.ts +31 -0
  84. package/dist/oauth/providers/opencode-console.js +148 -0
  85. package/dist/oauth/providers.d.ts +2 -6
  86. package/dist/oauth/providers.js +3 -7
  87. package/dist/oauth/responses.d.ts +9 -0
  88. package/dist/oauth/responses.js +23 -0
  89. package/dist/report/build.d.ts +26 -0
  90. package/dist/report/build.js +16 -8
  91. package/dist/report/failure-text.d.ts +6 -1
  92. package/dist/report/failure-text.js +14 -4
  93. package/dist/report/format.d.ts +10 -5
  94. package/dist/report/format.js +12 -5
  95. package/dist/report/junit.js +11 -12
  96. package/dist/report/list.d.ts +1 -0
  97. package/dist/report/list.js +38 -18
  98. package/dist/report/markdown.d.ts +1 -1
  99. package/dist/report/markdown.js +37 -14
  100. package/dist/report/repeats.d.ts +6 -1
  101. package/dist/report/repeats.js +12 -3
  102. package/dist/run/artifacts.d.ts +18 -0
  103. package/dist/run/artifacts.js +68 -1
  104. package/dist/run/execute.d.ts +9 -0
  105. package/dist/run/execute.js +49 -24
  106. package/dist/run/extended-fixtures.js +6 -2
  107. package/dist/run/failure-evidence.js +4 -2
  108. package/dist/run/fixtures.js +6 -8
  109. package/dist/run/in-process.d.ts +2 -0
  110. package/dist/run/in-process.js +1 -0
  111. package/dist/run/last-run.d.ts +42 -8
  112. package/dist/run/last-run.js +121 -14
  113. package/dist/run/realm.d.ts +4 -1
  114. package/dist/run/realm.js +20 -6
  115. package/dist/run/records.d.ts +16 -0
  116. package/dist/run/records.js +26 -0
  117. package/dist/run/retry.d.ts +3 -1
  118. package/dist/run/retry.js +4 -2
  119. package/dist/run/runner.d.ts +1 -1
  120. package/dist/run/runner.js +69 -30
  121. package/dist/run/serial.d.ts +2 -0
  122. package/dist/run/serial.js +8 -5
  123. package/dist/run/steps.d.ts +0 -6
  124. package/dist/run/steps.js +3 -20
  125. package/dist/run/worker/entry.js +5 -4
  126. package/dist/run/worker/protocol.d.ts +2 -0
  127. package/dist/run/worker/session.d.ts +2 -0
  128. package/dist/run/worker/session.js +1 -0
  129. package/dist/telemetry/events.js +1 -0
  130. package/dist/types.d.ts +9 -3
  131. package/docs/agents.mdx +6 -3
  132. package/docs/assertions.mdx +94 -0
  133. package/docs/authentication.mdx +3 -1
  134. package/docs/browser.mdx +42 -6
  135. package/docs/cache.mdx +64 -18
  136. package/docs/ci.mdx +11 -7
  137. package/docs/debugging.mdx +6 -3
  138. package/docs/executors.mdx +4 -2
  139. package/docs/github.mdx +12 -4
  140. package/docs/{writing-tests.mdx → goals.mdx} +21 -101
  141. package/docs/index.mdx +4 -4
  142. package/docs/integrations/kernel.mdx +1 -1
  143. package/docs/locators.mdx +64 -0
  144. package/docs/migrate/cypress.mdx +2 -2
  145. package/docs/migrate/maestro.mdx +2 -2
  146. package/docs/migrate/playwright.mdx +19 -16
  147. package/docs/migrate/selenium.mdx +2 -2
  148. package/docs/mobile.mdx +21 -4
  149. package/docs/models.mdx +53 -7
  150. package/docs/physical-devices.mdx +76 -0
  151. package/docs/quickstart.mdx +4 -3
  152. package/docs/reference/app.mdx +1 -1
  153. package/docs/reference/cli.mdx +30 -13
  154. package/docs/reference/config.mdx +25 -7
  155. package/docs/reference/engine.mdx +3 -3
  156. package/docs/reference/environment.mdx +8 -1
  157. package/docs/reference/errors.mdx +5 -5
  158. package/docs/reference/expect.mdx +34 -12
  159. package/docs/reference/mobile.mdx +23 -13
  160. package/docs/reference/reporters.mdx +8 -2
  161. package/docs/reference/screen.mdx +30 -7
  162. package/docs/reference/test.mdx +11 -4
  163. package/docs/reference/web.mdx +125 -21
  164. package/docs/security.mdx +26 -16
  165. package/docs/subscriptions.mdx +91 -12
  166. package/docs/telemetry.mdx +1 -1
  167. package/package.json +16 -3
  168. package/schema/report-v1.schema.json +14 -1
  169. package/skills/e2e/SKILL.md +2 -2
  170. package/skills/e2e/references/agent.md +22 -11
  171. package/skills/e2e/references/debugging.md +4 -4
  172. package/skills/e2e/references/running.md +13 -8
  173. package/skills/e2e/references/setup.md +36 -15
  174. package/skills/e2e/references/writing-tests.md +34 -17
@@ -17,7 +17,7 @@ import { StreamRedactor } from '../../internal/redact.js';
17
17
  import { processSecrets, registerStaticSecrets, staticSecretLedger } from '../secrecy.js';
18
18
  import { SessionStore } from '../sessions.js';
19
19
  import { TargetWorker } from './session.js';
20
- import { isAbandonedStepRejection } from '../steps.js';
20
+ import { isAbandonedRejection } from '../../internal/abandoned.js';
21
21
  /**
22
22
  * Outbound messages in flight. `process.send` is asynchronous and a
23
23
  * `process.exit` right behind it can drop the message, so exiting waits for
@@ -152,6 +152,7 @@ async function bootstrap(message, debug, aiTrace) {
152
152
  sessionStore: SessionStore.forWorker(message.runId, message.sessionsRoot, Buffer.from(message.sessionKeyBase64, 'base64')),
153
153
  runId: message.runId,
154
154
  artifactsRoot: message.artifactsRoot,
155
+ rerunDir: message.rerunDir,
155
156
  headed: message.headed,
156
157
  workerSlot: message.workerSlot,
157
158
  env: process.env,
@@ -169,13 +170,13 @@ function main() {
169
170
  process.on('SIGINT', () => undefined);
170
171
  process.on('SIGTERM', () => undefined);
171
172
  process.on('uncaughtException', (cause) => fatal(cause));
172
- // A step the body did not await rejects on a promise nobody holds once it
173
- // is cancelled; the attempt has already recorded it as STEP_NOT_AWAITED.
173
+ // A step or poll the test did not await rejects on a promise nobody holds
174
+ // once it is cancelled; the attempt has already recorded it as STEP_NOT_AWAITED.
174
175
  // Any other rejection nobody caught is charged to the attempt in flight,
175
176
  // or recorded against the last test that finished; only before any test
176
177
  // has finished is it the worker's.
177
178
  process.on('unhandledRejection', (cause) => {
178
- if (isAbandonedStepRejection(cause))
179
+ if (isAbandonedRejection(cause))
179
180
  return;
180
181
  if (worker?.strayRejection(cause) !== true)
181
182
  fatal(cause);
@@ -56,6 +56,8 @@ export interface WorkerBootstrap {
56
56
  readonly workerSlot: number;
57
57
  readonly runId: string;
58
58
  readonly artifactsRoot: string;
59
+ /** See `TargetExecutorOptions.rerunDir`. */
60
+ readonly rerunDir: string | undefined;
59
61
  readonly headed: boolean;
60
62
  readonly sessionsRoot: string;
61
63
  /** Per-run AES key; transferred only over this channel, never disk or env. */
@@ -35,6 +35,8 @@ export interface TargetWorkerDeps {
35
35
  readonly sessionStore: SessionStore;
36
36
  readonly runId: string;
37
37
  readonly artifactsRoot: string;
38
+ /** See `TargetExecutorOptions.rerunDir`. */
39
+ readonly rerunDir: string | undefined;
38
40
  readonly headed: boolean;
39
41
  /** See `TargetExecutorOptions.workerSlot`. */
40
42
  readonly workerSlot: number;
@@ -46,6 +46,7 @@ export class TargetWorker {
46
46
  target: deps.target,
47
47
  runId: deps.runId,
48
48
  artifactsRoot: deps.artifactsRoot,
49
+ rerunDir: deps.rerunDir,
49
50
  sessionStore: deps.sessionStore,
50
51
  headed: deps.headed,
51
52
  workerSlot: deps.workerSlot,
@@ -205,6 +205,7 @@ export function runCompletedEvent(report, context) {
205
205
  tests_executed: run.summary.executed,
206
206
  tests_passed: run.summary.passed,
207
207
  tests_failed: run.summary.failed,
208
+ tests_interrupted: run.summary.interrupted,
208
209
  tests_flaky: run.summary.flaky,
209
210
  tests_skipped: run.summary.skipped,
210
211
  attempts_total: attempts.reduce((total, count) => total + count, 0),
package/dist/types.d.ts CHANGED
@@ -198,7 +198,7 @@ export interface TextMatchOptions {
198
198
  /**
199
199
  * Role query options. A role query never matches a node hidden from the
200
200
  * accessibility tree, on every engine; `visible` (inherited) is the one knob
201
- * that narrows the other query kinds the same way.
201
+ * that narrows the other query kinds, to the nodes `toBeVisible()` accepts.
202
202
  */
203
203
  export interface RoleOptions extends TextMatchOptions {
204
204
  /** Accessible name filter. */
@@ -377,9 +377,13 @@ export interface Locator extends Screen {
377
377
  all(): Promise<Locator[]>;
378
378
  /** Reads the normalized text of every current match, without auto-waiting; empty when nothing matches. */
379
379
  allTextContents(): Promise<string[]>;
380
- /** Waits for the requested locator state. */
380
+ /**
381
+ * Waits for the requested locator state, `visible` by default: `attached`
382
+ * for one match, visible or not; `detached` for none; `hidden` for none or
383
+ * a hidden one.
384
+ */
381
385
  waitFor(options?: {
382
- state?: 'visible' | 'hidden';
386
+ state?: 'attached' | 'detached' | 'visible' | 'hidden';
383
387
  timeout?: number;
384
388
  }): Promise<void>;
385
389
  /** Adds deterministic locator filters. */
@@ -1250,6 +1254,8 @@ export interface E2EConfig {
1250
1254
  cleanupTimeout?: number;
1251
1255
  /** Retries per test, 0 through 10; default 1 in CI, else 0. */
1252
1256
  retries?: number;
1257
+ /** Fail the run when a test skips itself after a soft failure or an earlier failed attempt; default false. */
1258
+ failOnSkippedFailure?: boolean;
1253
1259
  /** Parallel workers, 1 through 1024; default 1 in CI, else half the cores. An engine may cap it lower. */
1254
1260
  workers?: number;
1255
1261
  /** `{ store }` hands every artifact to a host store as it is produced. */
package/docs/agents.mdx CHANGED
@@ -109,9 +109,12 @@ agent if the problem affects several tests:
109
109
  5. Compare models or `providerOptions` by running the same tests with
110
110
  `--agent <name>`.
111
111
 
112
- Named agents can use the [replay cache](/cache). Custom tools and executors
113
- have their own replay constraints, described under [project tools](/tools)
114
- and [custom executors](/executors#replay-cache).
112
+ Named agents can use the [replay cache](/cache). Each agent records its
113
+ own entries, keyed by its name and its context, so a test that runs as
114
+ `buyer` and as `admin` never replays one persona's steps for the other, and
115
+ changing `agentContext` records again. Custom tools and executors have their
116
+ own replay constraints, described under [project tools](/tools) and
117
+ [custom executors](/executors#replay-cache).
115
118
 
116
119
  ## Pick an agent per run
117
120
 
@@ -0,0 +1,94 @@
1
+ ---
2
+ title: Assertions
3
+ description: Check the screen with agent.assert, wait for a condition, and extract data.
4
+ ---
5
+
6
+ An assertion checks what is on screen. The agent has three judgment calls:
7
+ `assert` checks the screen now, `waitFor` waits until a condition holds, and
8
+ `extract` reads structured data. When you know the exact value, use an
9
+ `expect` [locator assertion](#locator-assertions) instead, with no model call.
10
+
11
+ ```ts
12
+ await agent.assert('the dashboard shows a trial badge');
13
+ ```
14
+
15
+ ## `agent.assert`
16
+
17
+ `assert` checks a statement against the current screen. A false statement
18
+ fails with `ASSERTION_FAILED`. If the screen does not provide enough evidence,
19
+ it fails with `ASSERTION_INCONCLUSIVE`.
20
+
21
+ The judging model sees the statement and the current screen. It does not see
22
+ earlier steps or the acting agent's summaries.
23
+
24
+ ## `agent.waitFor`
25
+
26
+ `assert` checks the screen now. Use `waitFor` when the condition may become true
27
+ later. It polls the screen and only calls the model when the observation changes.
28
+
29
+ ```ts
30
+ await agent.waitFor('the export finishes and a download link appears', {
31
+ interval: 500,
32
+ timeout: 120_000,
33
+ });
34
+ ```
35
+
36
+ ## `agent.extract`
37
+
38
+ `extract` reads structured data from the screen. Provide a
39
+ [Standard Schema](https://standardschema.dev) validator, such as Zod, to define
40
+ the result.
41
+
42
+ ```ts
43
+ import { expect } from 'e2e';
44
+ import { z } from 'zod';
45
+
46
+ const data = await agent.extract('every todo title and how many remain', {
47
+ schema: z.object({ titles: z.array(z.string()), remaining: z.number().int() }),
48
+ });
49
+ expect(data.titles).toContain('Buy milk');
50
+ ```
51
+
52
+ <Tip>
53
+ For model-generated descriptions, check the relevant meaning rather than the
54
+ entire sentence. Use a [locator assertion](/locators) when the exact text matters.
55
+ </Tip>
56
+
57
+ ## Judging pixels
58
+
59
+ By default, judgments read a redacted text snapshot of the screen. Add
60
+ `vision: true` for visual checks, such as charts or layout. Use
61
+ `vision: 'only'` to judge the screenshot alone.
62
+
63
+ ```ts
64
+ await agent.assert('the chart trends upward', { vision: true });
65
+ await agent.assert('the search form is not covered by a banner', { vision: 'only' });
66
+ ```
67
+
68
+ Images add input tokens. Screenshots are withheld for the rest of an attempt
69
+ after a secret is filled. The [agent reference](/reference/agent#vision)
70
+ lists the modes and restrictions.
71
+
72
+ ## Locator assertions
73
+
74
+ `expect` on a locator checks an exact value and calls no model. It retries
75
+ until the check passes or the assertion timeout runs out.
76
+
77
+ ```ts
78
+ await expect(screen.getByRole('status')).toHaveText('2 remaining');
79
+ await expect(screen.getByRole('heading', 'Dashboard')).toBeVisible();
80
+ ```
81
+
82
+ [Locators](/locators) covers queries and actions. The
83
+ [expect reference](/reference/expect#locator-matchers) lists every matcher.
84
+
85
+ ## Next
86
+
87
+ <CardGroup cols={2}>
88
+ <Card title="Locators" icon="crosshairs" href="/locators">
89
+ Exact actions and checks with `screen` and `expect`.
90
+ </Card>
91
+ <Card title="How agent steps work" icon="list-check" href="/agent-steps">
92
+ Verdicts, exit codes, and what the model sees.
93
+ </Card>
94
+ </CardGroup>
@@ -211,7 +211,9 @@ export default {
211
211
  ```
212
212
 
213
213
  `E2E_SECRET_<NAME>` overrides an entry per run, with the same uppercasing rule
214
- as credentials, so a CI job can supply the value without touching the config:
214
+ as credentials, so a CI job can supply the value without touching the config.
215
+ Two entries that map to one variable, like `stripe-key` and `stripe_key`, fail
216
+ the config load with `INVALID_CONFIG`, as two such credentials do:
215
217
 
216
218
  <CodeGroup>
217
219
  ```bash npm
package/docs/browser.mdx CHANGED
@@ -39,7 +39,9 @@ await app.open('/checkout');
39
39
  await expect(screen.getByText('$42.00')).toBeVisible();
40
40
  ```
41
41
 
42
- Each handler must call exactly one of `fulfill`, `continue`, or `abort`. An
42
+ Each handler must call exactly one of `fulfill`, `continue`, `fallback`, or
43
+ `abort`. The newest route runs first; `continue` sends the request straight to
44
+ the network, while `fallback` passes it to the route registered before. An
43
45
  `expect` that fails inside the handler fails the test, at the next step or at
44
46
  the end of the test when nothing follows. To inspect a real response, start
45
47
  waiting before the action that sends it:
@@ -114,14 +116,33 @@ const flag = await browser.evaluate((name: string) => window.localStorage.getIte
114
116
  Use `evaluate` for state that `screen` cannot read. Prefer locators and
115
117
  assertions for visible UI so tests depend less on the DOM structure.
116
118
 
119
+ ## Run a script before the page
120
+
121
+ An init script runs in every document before the page's own scripts: mock a
122
+ browser API, seed `Math.random`, or set a flag the app reads at boot. For every
123
+ test:
124
+
125
+ ```ts
126
+ web({ initScripts: [{ path: 'tests/init.js' }] })
127
+ ```
128
+
129
+ For one test, before the page opens:
130
+
131
+ ```ts
132
+ await browser.addInitScript((value) => { Math.random = () => value; }, 0.5);
133
+ await app.open('/raffle');
134
+ ```
135
+
136
+ See [init scripts](/reference/web#init-scripts).
137
+
117
138
  ## Cookies
118
139
 
119
140
  ```ts
120
141
  await browser.setCookies([{ url: 'http://localhost:3000', name: 'tz', value: 'UTC' }]);
121
142
  ```
122
143
 
123
- A cookie URL that does not parse, or uses `file:`, `data:`, or
124
- `javascript:`, is `POLICY_DENIED`; a domain cookie is checked as the origin
144
+ A cookie URL that does not parse, or uses a scheme other than `http:` or
145
+ `https:` (`about:blank` included), is `POLICY_DENIED`; a domain cookie is checked as the origin
125
146
  it would be sent to. If any entry is refused, no cookies are set. To resize
126
147
  the page, see [Testing viewports](/viewports).
127
148
 
@@ -144,8 +165,23 @@ export default {
144
165
  } satisfies E2EConfig;
145
166
  ```
146
167
 
147
- `npx playwright install webkit --with-deps` provisions the extra browser in
148
- CI. `connect` attaches to a remote Chromium over CDP instead of launching
168
+ In CI, provision the extra browser with the [install command](/reference/web#installing-browsers):
169
+
170
+ <CodeGroup>
171
+ ```bash npm
172
+ npx @e2e-dev/web install webkit --with-deps
173
+ ```
174
+
175
+ ```bash pnpm
176
+ pnpm exec e2e-web install webkit --with-deps
177
+ ```
178
+
179
+ ```bash bun
180
+ bunx @e2e-dev/web install webkit --with-deps
181
+ ```
182
+ </CodeGroup>
183
+
184
+ `connect` attaches to a remote Chromium over CDP instead of launching
149
185
  one; a [browser provider](#hosted-browsers) does the same and also leases
150
186
  and releases the browser. [Testing viewports](/viewports) covers sizes.
151
187
 
@@ -237,7 +273,7 @@ The attempt runs in the browser's persistent default context, on the
237
273
  browser's own first tab, so the remote must contain only that context, and
238
274
  the limits of
239
275
  [`connect.reconnectEndpoint`](/reference/web#recovering-a-cdp-transport)
240
- apply: no `headers`, `basicAuth`, or `userAgent` (each is `INVALID_CONFIG`), no
276
+ apply: no `headers`, `basicAuth`, `userAgent`, `locale`, or `timezoneId` (each is `INVALID_CONFIG`), no
241
277
  `app.clearState()` (`UNSUPPORTED_CAPABILITY` naming the scope), and no session
242
278
  state capture or restore, so a run whose tests consume a
243
279
  [session](/authentication) on the target fails with `COLLECTION_ERROR`
package/docs/cache.mdx CHANGED
@@ -45,27 +45,60 @@ bunx e2e run --no-cache
45
45
 
46
46
  ## How a replay works
47
47
 
48
- 1. Check the starting screen: the same route, whatever the record id, the
49
- query, or the fragment. Retries always run live.
48
+ 1. Check the starting screen: the same route, whatever the record id or the
49
+ fragment. Retries always run live.
50
50
  2. Find each recorded control and repeat its action. The runner matches
51
51
  controls by their role, name, test id, and surrounding context. It waits
52
52
  for missing controls and stops if a match is ambiguous. A control with no
53
- name is found by its place among the unnamed controls of its kind.
54
- 3. Check the final path and controls that appeared during the original step.
55
- Text that changes with the data, a count, a date, an id, is not used.
53
+ name is found by its place among the unnamed controls of its kind inside
54
+ the same named row, list item, or group; with no such container to tell
55
+ it from its twins, the agent takes the step.
56
+ 3. Check the final route and what the original step changed: the controls
57
+ that appeared, with their checked, selected, expanded, or pressed state,
58
+ and the ones that went away. At least one of those changes must happen
59
+ during the replay, counted from the first screen the replay saw on the
60
+ route it ended on, so an outcome already on screen before the actions
61
+ that should produce it proves nothing; a step whose last action moved to
62
+ another route has that move as its evidence. An alert the replay raised
63
+ that the recording never saw stops it too. Text that reads differently on
64
+ every run, a date, a time, a duration, an id, is not used, except in an
65
+ alert, where it is read as a placeholder.
56
66
  4. Finish without a model call, or let the agent continue with a record of
57
67
  what ran and why replay stopped.
58
68
 
69
+ A route is the origin, the path, and the query. Ids, tokens, timestamps,
70
+ and cache busters in either the path or the query are read as an id
71
+ placeholder, so `/orders/42?t=1727780000` and `/orders/7?t=1727780999` are
72
+ one route. Every other query value is part of it: `?mode=safe` and
73
+ `?mode=unsafe`, or `/companies` and `/companies?tab=notes`, are different
74
+ screens, and so is a tracking parameter that was not there when the step
75
+ was recorded. Two routes that differ in one word, `/products/summer-sneaker`
76
+ and `/products/winter-boot`, are different screens too, even when they look
77
+ alike. A location on the app's own origin is compared by its path, so a
78
+ recording follows the app to a preview deployment that shares its
79
+ `app.identity`; a page on any other origin is another route.
80
+
81
+ A count that names what it counts, `3 records imported` or `Showing 1 to 9
82
+ of 9 results`, is part of the effect when the step made it appear on the
83
+ screen it acted on, and must read the same on replay. A count that was
84
+ already on screen and only moved, `41 items` to `42 items`, or one on a
85
+ screen the step navigated to, is not used, since leftover data from earlier
86
+ runs moves it again. A bare number such as a badge is never used.
87
+
59
88
  Typed values replay as recorded, even when an earlier run left the same
60
89
  value on screen. Secret fills resolve the current value by name and run
61
90
  through authorization again.
62
91
 
63
92
  ## What must match
64
93
 
65
- A cache entry belongs to a specific test, target, instruction, and set of
66
- params. Renaming the test or target, changing the instruction or an ordinary
67
- param, or upgrading the engine's major or minor version causes a miss.
68
- Changing the model does not.
94
+ A cache entry belongs to a specific test, target, instruction, set of
95
+ params, and agent. Renaming the test or target, changing the instruction or
96
+ an ordinary param, running with another configured agent, changing the
97
+ agent's `context` or the test's `agentContext`, or upgrading the engine's
98
+ major or minor version causes a miss. A test that runs once per agent
99
+ (`agent: ['buyer', 'admin']`) records once per agent. Changing the model
100
+ does not. The agent context enters the key as a hash of its redacted text,
101
+ so a secret in it contributes only its name.
69
102
 
70
103
  A timestamp or a fresh email address would cause a miss on every run. Wrap
71
104
  values like these in `unique()` so the runner substitutes the current value
@@ -96,7 +129,9 @@ identity. On devices, replay uses the
96
129
 
97
130
  In `read-write` mode, the runner saves a recording at the end of the
98
131
  attempt when a later verification step passed. A step that took no action
99
- records nothing.
132
+ records nothing, and neither does one that changed nothing the replay could
133
+ check: no control appeared, went away, or changed state, and the route
134
+ stayed the same.
100
135
 
101
136
  | Counts as verification | Does not count |
102
137
  | --- | --- |
@@ -145,14 +180,14 @@ such as an `afterEach` assertion.
145
180
  | `retry` | Retries never replay. | Nothing. |
146
181
  | `invalid-entry` | The file is unreadable, over 1 MiB, or from another schema version. | `npx e2e cache clear`. |
147
182
  | `truncated` | The recording hit a limit, see [below](#limits-and-gaps). | Split the step, or pass the secret as a `Secret`. |
148
- | `wrong-context` | The app was on another screen than when the step was recorded: a different route, not a different record, query, or fragment. | Open the same screen before the step. |
149
- | `target-not-found` | A recorded control did not appear within 15 seconds. | The UI changed. The next passing run re-records. |
183
+ | `wrong-context` | The app was on another screen than when the step was recorded: a different origin, path, or query, not a different record id or fragment. | Open the same screen before the step. |
184
+ | `target-not-found` | A recorded control did not appear within 15 seconds, or it has no name and no named container to tell it from its twins. | The UI changed: the next passing run re-records. For an unnamed control, give it a label or put it in a named row or group. |
150
185
  | `target-ambiguous` | The number of matching controls changed. | Give repeated controls distinct names or use unique test data. |
151
186
  | `gap` | An action cannot be replayed, see [below](#limits-and-gaps). | The agent continues this part live. Put stable values in `params`. |
152
187
  | `action-failed` | The app rejected a replayed action. | Check the app. |
153
188
  | `action-uncertain` | A replayed action failed with an unknown commit state. The agent is told to check before acting again. | Check the app. |
154
189
  | `viewport-changed` | A bare-point tap or hover was recorded on a viewport of another size. | Keep the viewport fixed. |
155
- | `end-mismatch` | Every action ran, but the recorded path or a control that should have appeared did not come back. | Leftover data from an earlier run, a banner that comes and goes, or a regression. |
190
+ | `end-mismatch` | Every action ran, but the recorded route, a control that should have appeared or gone away, or a recorded state did not come back; the outcome was already on screen before the replay; or the replay raised an alert the recording never saw. | Leftover data from an earlier run, a banner that comes and goes, or a regression. |
156
191
 
157
192
  A stop before any action ran is a miss. After replay starts, it is a
158
193
  hand-off. Unique test data and cleanup in `afterEach` help avoid stale
@@ -176,8 +211,9 @@ the instruction or `params`, or ones the agent composed itself, replay.
176
211
 
177
212
  Each entry is a JSON file under `.e2e/cache/`, named by a hash of its key.
178
213
  It contains actions, target descriptions, typed text, a step summary, and
179
- checks for the final state. Those checks include the path and up to eight
180
- controls that appeared during the step.
214
+ checks for the final state. Those checks include the route, up to eight
215
+ controls that appeared during the step, alerts and status messages first,
216
+ and up to eight that went away.
181
217
 
182
218
  Entries contain no prompts, conversations, screenshots, observed trees, or
183
219
  secret values. A secret fill stores only the secret's name. Other typed
@@ -220,12 +256,16 @@ Local runs leave a committed directory clean unless something changed. A
220
256
  replayed step never rewrites its entry, and a live re-recording with the same
221
257
  actions, paths, and anchors is not written: the summary, the measured end
222
258
  wait, and the rule that flagged a typed run-time value differ between two
223
- live runs of the same flow and do not count. A file changes when:
259
+ live runs of the same flow and do not count. Upgrading `e2e` to a version
260
+ that changes how entries are matched re-keys every entry, so the first run
261
+ after it records them again. A file changes when:
224
262
 
225
263
  - a step records for the first time,
226
264
  - the agent finishes a step after a hand-off, re-recording what it saw,
227
265
  - a step fails after replaying, or ends in an `end-mismatch` the agent had
228
266
  to repair, which deletes the entry,
267
+ - a step with an entry passes without leaving anything to record, which
268
+ deletes the entry,
229
269
  - no verification step confirmed the entry by the end of the attempt, which
230
270
  deletes it.
231
271
 
@@ -257,8 +297,14 @@ CI=1 bunx e2e run --strict-cache
257
297
  The reasons that fail are the ones that mean the app or the entry changed:
258
298
  `invalid-entry`, `wrong-context`, `target-not-found`, `target-ambiguous`,
259
299
  `viewport-changed`, `action-failed`, `action-uncertain`, and `end-mismatch`.
260
- A step with no recording (`no-entry`), a retry, a `gap`, and a `truncated`
261
- recording still run live. `REPLAY_STALE` exits 2 and is not retried, since a
300
+ A `no-entry` miss fails too when the cache directory holds a recording of the
301
+ same step under another key, from before an `e2e` or engine upgrade or a
302
+ change to the project id, the app's identity, or the agent's context. The
303
+ message names that file; delete it once nothing replays it. A custom
304
+ `cache.store` is not checked this way. Entries recorded before this check
305
+ existed are covered once a `read-write` run replays or re-records them. A
306
+ step never recorded (a new or edited instruction or params), a retry, a
307
+ `gap`, and a `truncated` recording still run live. `REPLAY_STALE` exits 2 and is not retried, since a
262
308
  retry never replays and would pass live. The entry stays where it is, even
263
309
  in `read-write` mode, so every strict run fails on it until it is re-recorded:
264
310
  run the test in `read-write` mode without the flag, and with `cache.strict`
package/docs/ci.mdx CHANGED
@@ -35,7 +35,7 @@ jobs:
35
35
  - run: pnpm install --frozen-lockfile
36
36
 
37
37
  - name: Install browsers
38
- run: npx playwright install chromium --with-deps
38
+ run: pnpm exec e2e-web install chromium --with-deps
39
39
 
40
40
  - name: Run e2e
41
41
  run: npx e2e run --reporter list,junit
@@ -66,7 +66,9 @@ jobs:
66
66
  ```
67
67
 
68
68
  The workflow pins actions to commit SHAs and installs dependencies from the
69
- lockfile. `npx e2e` runs the installed version.
69
+ lockfile. `npx e2e` runs the installed version. The
70
+ [install command](/reference/web#installing-browsers) downloads the browsers
71
+ for the Playwright version the engine pins, with their system libraries.
70
72
 
71
73
  Artifacts upload on every run that is not cancelled, so a test that failed
72
74
  and then passed on retry still has its evidence.
@@ -139,10 +141,12 @@ implemented yet.
139
141
 
140
142
  `--last-failed` runs only the tests the previous run did not pass, read from
141
143
  `report.json` in the [`output`](/reference/config#output) directory (`.e2e`
142
- by default). The rerun empties `artifacts/` there when it starts, so
143
- upload the first run's artifacts before rerunning in the same job if you want
144
- both. The [CLI reference](/reference/cli#flags) lists what counts as not
145
- passed. Locally it is the loop after a red run:
144
+ by default). The rerun keeps the artifacts the first run's report names and
145
+ writes its own under `artifacts/rerun-1/`, so one upload after both steps
146
+ holds the evidence of each. A test another filter keeps out of the rerun
147
+ stays owed: the next `--last-failed` runs it. The
148
+ [CLI reference](/reference/cli#flags) lists what counts as not passed.
149
+ Locally it is the loop after a red run:
146
150
 
147
151
  <CodeGroup>
148
152
  ```bash npm
@@ -193,7 +197,7 @@ flaky, so you can identify flows that need investigation.
193
197
  `--max-failures <n>` stops the run once that many tests have failed, so a
194
198
  broken deploy does not spend a model call on every remaining agent step. The
195
199
  tests not started are skipped with cause `failure-limit`, the ones running
196
- end as `interrupted`, and the job exits 1.
200
+ end as `interrupted`, counted apart from the failures, and the job exits 1.
197
201
 
198
202
  ## Exit codes
199
203
 
@@ -91,7 +91,9 @@ failed result:
91
91
 
92
92
  Artifacts land under `.e2e/artifacts/` (`<output>/artifacts/` when the
93
93
  [`output`](/reference/config#output) is moved), one directory per attempt.
94
- Each run empties it first, so it holds the latest run's evidence:
94
+ Each run empties it first, so it holds the latest run's evidence. A
95
+ `--last-failed` rerun instead empties it down to the files the report it
96
+ reruns names, and writes its own under `rerun-<n>/`:
95
97
 
96
98
  - **Screenshots.** Failed attempts capture one whenever the engine can and
97
99
  pixels are allowed. The built-in `agent.assert` also captures one after
@@ -99,8 +101,9 @@ Each run empties it first, so it holds the latest run's evidence:
99
101
  be withheld. Use `app.screenshot('label')` to capture one explicitly.
100
102
  - **Playwright traces** on the attempts the [`trace`](/reference/config#trace)
101
103
  mode records: every attempt locally, the first retry in CI. Open one with
102
- `npx playwright show-trace <file>`; `--trace off` skips the cost for one
103
- run, `--trace` records every attempt of a CI run.
104
+ `npx playwright-core@1.63.0 show-trace <file>` (`pnpm dlx` under pnpm);
105
+ `--trace off` skips the cost for one run, `--trace` records every attempt
106
+ of a CI run.
104
107
  - **Downloads** saved through `browser.waitForDownload`, recorded with kind
105
108
  `download`. They are the app's bytes: `redaction: "incomplete"` unless a
106
109
  secret reached the session (filled, or held by the engine for basic auth)
@@ -215,8 +215,10 @@ default, follows the configured cache mode. A recorded step replays without
215
215
  calling `runStep`. A replay that stops partway calls `runStep` with
216
216
  `ctx.replayedPrefix`: continue from the current screen and do not redo the
217
217
  replayed actions. What a step does through `ctx.actions` is recorded; a
218
- mutating tool call leaves a gap the executor fills on replay. Entries are not
219
- keyed by executor, so an entry one agent recorded replays for another.
218
+ mutating tool call leaves a gap the executor fills on replay. Entries are
219
+ keyed by the configured agent and its context, so an entry one agent
220
+ recorded never replays for another; the executor itself is not part of the
221
+ key, so changing an agent's executor keeps its entries.
220
222
 
221
223
  `'off'` never replays or records the executor's steps, so every step reaches
222
224
  `runStep`. Use it for a brain whose own history or memory is the source of
package/docs/github.mdx CHANGED
@@ -63,7 +63,10 @@ Upload artifacts even when a retry passes. Each failure's evidence links open
63
63
  the run's Artifacts section; inside the download, every test has a directory
64
64
  named after it and the agent that ran it
65
65
  (`web/tests_billing.e2e.ts__upgrades/default/attempt-0/`), so keep
66
- `path: .e2e/artifacts` and the download lines up with the report. A project
66
+ `path: .e2e/artifacts` and the download lines up with the report. A
67
+ `--last-failed` rerun keeps the first run's directories and puts its own
68
+ under `rerun-1/`, so a flaky test's failed attempt still links to its own
69
+ files. A project
67
70
  below the checkout root, such as `packages/e2e-tests`, gets source links under
68
71
  that path. The checkout root is the nearest directory holding `.git` inside
69
72
  `GITHUB_WORKSPACE`, so `actions/checkout` with a `path` input works too.
@@ -143,9 +146,14 @@ passes:
143
146
 
144
147
  The comment covers the whole suite, not only the tests the rerun ran again.
145
148
  A test that failed and then passed shows as flaky, a test that failed twice
146
- stays failed, and the rest keep their first-pass results. The comment
147
- matches the job's status: it stays red when another filter kept a failed
148
- test out of the rerun.
149
+ stays failed, one that was interrupted and then passed shows as passed, and
150
+ the rest keep their first-pass results. The comment stays red while the
151
+ rerun left out something it owed: a failed test, one `--max-failures` stopped
152
+ before it ran, or a failed `beforeAll` or `afterAll` whose tests another
153
+ filter kept out of the rerun. It shows ⏹️ when the rerun left out a test the
154
+ first pass never finished. The rerun's own exit code counts only what it ran.
155
+ A failed attempt's evidence links to the first run's files, which the rerun
156
+ keeps.
149
157
 
150
158
  ## Matrix jobs
151
159