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
@@ -107,7 +107,14 @@ await screen.getByTestId('memory-list', { visible: true }).getByRole('listitem')
107
107
  ```
108
108
 
109
109
  `visible` uses the same visibility check as `toBeVisible()`, `isVisible()`,
110
- and `waitFor()`. It works with scopes, `filter`, `first`, `last`, and `nth`:
110
+ and `waitFor()`: what renders, as Playwright reads it. `display: none`,
111
+ `visibility: hidden`, a box with no size, a closed `<details>` body, and
112
+ content under `content-visibility: hidden` are hidden; `aria-hidden` is not,
113
+ since a decorative spinner marked with it still paints, so
114
+ `toBeHidden()` on one waits until it leaves. A child that sets
115
+ `visibility: visible` under a hidden parent is visible. On the web, text
116
+ directly under a `display: contents` element with `visibility: hidden` is
117
+ hidden, where Playwright calls it visible. It works with scopes, `filter`, `first`, `last`, and `nth`:
111
118
  `getByText('Save', { visible: true }).first()` is the first visible match.
112
119
  Prefer it over `browser.locator('p:visible')`, which ties the test to CSS.
113
120
 
@@ -292,8 +299,12 @@ export interface Locator extends Screen {
292
299
  all(): Promise<Locator[]>;
293
300
  /** Reads the normalized text of every current match, without auto-waiting; empty when nothing matches. */
294
301
  allTextContents(): Promise<string[]>;
295
- /** Waits for the requested locator state. */
296
- waitFor(options?: { state?: 'visible' | 'hidden'; timeout?: number }): Promise<void>;
302
+ /**
303
+ * Waits for the requested locator state, `visible` by default: `attached`
304
+ * for one match, visible or not; `detached` for none; `hidden` for none or
305
+ * a hidden one.
306
+ */
307
+ waitFor(options?: { state?: 'attached' | 'detached' | 'visible' | 'hidden'; timeout?: number }): Promise<void>;
297
308
  /** Adds deterministic locator filters. */
298
309
  filter(options: { hasText?: TextMatch; has?: Locator }): Locator;
299
310
  /** Selects the first current match. */
@@ -382,7 +393,11 @@ await screen.getByLabel('City').pressSequentially('War', { delay: 50 });
382
393
  ## Locator reads
383
394
 
384
395
  `waitFor` defaults `state` to `'visible'` and `timeout` to
385
- `config.actionTimeout`, and throws `LOCATOR_NOT_FOUND` on timeout.
396
+ `config.actionTimeout`, and throws `LOCATOR_NOT_FOUND` on timeout. `'attached'`
397
+ waits for one match, hidden or shown, `'detached'` for none, and `'hidden'` for
398
+ a hidden match or none. Several matches are `LOCATOR_AMBIGUOUS` in every
399
+ state. Any other state, or an option key besides `state` and `timeout`, is
400
+ `INVALID_ARGUMENT` before anything is resolved.
386
401
 
387
402
  `textContent` and `allTextContents` return the node's rendered text with
388
403
  whitespace collapsed to single spaces. On the web that is what `innerText`
@@ -399,6 +414,12 @@ a heading; it does not throw.
399
414
  `toHaveText`, `toContainText`, and `toHaveAttribute`.
400
415
  `isEnabled`, `isDisabled`, `isChecked`, and `boundingBox` are allowed there.
401
416
 
417
+ `boundingBox()` is `null` for a node that lays out no box (`display: none`
418
+ on it or an ancestor), and a box for one `visibility: hidden` keeps.
419
+ `inputValue()` and `toHaveValue` on a checkbox or radio read its value
420
+ attribute, `on` when it has none, whatever its checked state; `isChecked()`
421
+ reads the state.
422
+
402
423
  `isHidden` is `!isVisible()` and `isDisabled` is `!isEnabled()`, with the
403
424
  same strictness: `isHidden` is `true` for zero matches, `isDisabled` is
404
425
  `LOCATOR_NOT_FOUND` there, and both are `LOCATOR_AMBIGUOUS` on several.
@@ -521,7 +542,7 @@ export interface TextMatchOptions {
521
542
  /**
522
543
  * Role query options. A role query never matches a node hidden from the
523
544
  * accessibility tree, on every engine; `visible` (inherited) is the one knob
524
- * that narrows the other query kinds the same way.
545
+ * that narrows the other query kinds, to the nodes `toBeVisible()` accepts.
525
546
  */
526
547
  export interface RoleOptions extends TextMatchOptions {
527
548
  /** Accessible name filter. */
@@ -623,8 +644,10 @@ export type ScrollDirection = 'up' | 'down' | 'left' | 'right';
623
644
  export type Momentum = 'none' | 'slow' | 'fast';
624
645
  ```
625
646
 
626
- A role query never matches a node hidden from the accessibility tree.
627
- `visible` applies the same rule to the other query kinds. See
647
+ A role query never matches a node hidden from the accessibility tree: one
648
+ that does not render, or one under `aria-hidden`. `visible` narrows the
649
+ other query kinds to the nodes that render, the check `toBeVisible()` makes,
650
+ so an `aria-hidden` node that still paints counts as visible there. See
628
651
  [Hidden duplicates](#hidden-duplicates).
629
652
 
630
653
  `img` is an alias, not a role: `getByRole('img', { name })` builds the same
@@ -97,16 +97,23 @@ group titles and then the test's own, are a `COLLECTION_ERROR`.
97
97
 
98
98
  ## Members
99
99
 
100
- A skipped test never affects the exit code. With `test.only`, other tests
100
+ A skipped test does not affect the exit code by default. With `test.only`, other tests
101
101
  are reported as `filtered`, and a CI run fails with `ONLY_IN_CI`.
102
102
 
103
103
  Call `test.skip(condition, reason)` inside a test for conditions you learn at
104
104
  runtime, such as a disabled feature flag. When the condition is true, the call
105
105
  throws: the body stops at that line, as Playwright's does, and the test is
106
106
  reported `skipped` with `reason`, with no retry. Nothing after the call runs.
107
- Steps before it stay in the report as they ran, but they do not decide the
108
- status: a failed `expect.soft` before the call is dropped. Hooks and fixtures still run their teardown. When the condition is
109
- a precondition, call it first, before any step.
107
+ Steps before it stay in the report as they ran. Soft failures before the call
108
+ remain in the attempt's `secondaryErrors`, and the CLI lists the test under
109
+ `Skipped After Failure`. A failed attempt followed by a skip on retry appears
110
+ there too, with its original error. Hooks and fixtures still run their teardown.
111
+ When the condition is a precondition, call it first, before any step.
112
+
113
+ Set [`failOnSkippedFailure: true`](/reference/config) to fail the run with exit
114
+ code 1 when a runtime skip follows a failure in that test. The test remains
115
+ `skipped` with its reason, and the skip does not trigger another retry. Clean
116
+ skips and retries that pass are unaffected. The default is `false`.
110
117
 
111
118
  A setup test cannot skip because other tests depend on its sessions; the
112
119
  call fails with `INVALID_ARGUMENT`. Outside a test body, the conditional
@@ -7,9 +7,10 @@ description: The browser engine, its options, and the browser fixture it contrib
7
7
  A web target names it as its engine, and it contributes the `browser` fixture for
8
8
  browser-only work. Import `test` from the package to have `browser` typed.
9
9
 
10
- The package declares `playwright` as a peer dependency (`>=1.63.0 <2`) and
11
- does not install it. An app that already depends on Playwright keeps its
12
- version; `e2e init` adds `playwright` only to a project that has none.
10
+ The package depends on `playwright-core` pinned to an exact version, so the
11
+ engine always runs the Playwright it was tested against. An app that depends
12
+ on Playwright itself keeps its own copy; the two share a browser cache only
13
+ when their versions match.
13
14
 
14
15
  ```ts title="e2e.config.ts"
15
16
  import type { E2EConfig } from 'e2e';
@@ -20,6 +21,31 @@ export default {
20
21
  } satisfies E2EConfig;
21
22
  ```
22
23
 
24
+ ## Installing browsers
25
+
26
+ A missing browser downloads once per run, before the run's clock starts. To
27
+ install browsers as their own step, in CI or a container image, run the
28
+ package's command. It installs the browsers for the Playwright version the
29
+ engine pins:
30
+
31
+ <CodeGroup>
32
+ ```bash npm
33
+ npx @e2e-dev/web install chromium --with-deps
34
+ ```
35
+
36
+ ```bash pnpm
37
+ pnpm exec e2e-web install chromium --with-deps
38
+ ```
39
+
40
+ ```bash bun
41
+ bunx @e2e-dev/web install chromium --with-deps
42
+ ```
43
+ </CodeGroup>
44
+
45
+ Name any of `chromium`, `firefox`, and `webkit`; with none named it installs
46
+ `chromium`. `--with-deps` also installs the system libraries the browsers need
47
+ on Linux. The command exits with the installer's exit code.
48
+
23
49
  ## Options
24
50
 
25
51
  ```ts
@@ -82,8 +108,27 @@ export interface WebOptions {
82
108
  * (`connect.reconnectEndpoint`, or a provider with `scope: 'attempt'`).
83
109
  */
84
110
  readonly userAgent?: string;
111
+ /**
112
+ * The locale every attempt's context runs in, a BCP 47 tag such as
113
+ * `de-DE`: what `navigator.language`, `Intl` formatting, and the
114
+ * `Accept-Language` header report. Defaults to the browser's own. An
115
+ * `accept-language` entry in `headers` beside it is `INVALID_CONFIG`, and
116
+ * so is a persistent context.
117
+ */
118
+ readonly locale?: string;
119
+ /**
120
+ * The IANA time zone every attempt's context runs in, such as
121
+ * `Europe/Berlin`: what `Date` and `Intl` resolve local time against.
122
+ * Defaults to the machine's. A persistent context is `INVALID_CONFIG`.
123
+ */
124
+ readonly timezoneId?: string;
125
+ /** Scripts every document runs before the page's own. See [Init scripts](#init-scripts). */
126
+ readonly initScripts?: readonly WebInitScript[];
85
127
  }
86
128
 
129
+ /** JavaScript source, a file relative to the project root, or a function. */
130
+ export type WebInitScript = string | { readonly path: string } | (() => unknown);
131
+
87
132
  /** The engine's screencast of the page: the frame size and the JPEG quality of the frames it encodes. */
88
133
  export interface WebScreencastOptions {
89
134
  /** Frame size in pixels; default the viewport's, or the window's under `viewport: null`. A smaller size makes a smaller file. */
@@ -180,7 +225,7 @@ recording resume, but a segment lost during the disconnect cannot be recovered.
180
225
  Code using `surfaceOf(engine)` must read its accessors again after recovery;
181
226
  previously retained Playwright objects belong to the disconnected transport.
182
227
 
183
- Persistent recovery does not support `headers`, `basicAuth`, `userAgent`, `app.clearState`,
228
+ Persistent recovery does not support `headers`, `basicAuth`, `userAgent`, `locale`, `timezoneId`, `app.clearState`,
184
229
  or session state capture and restore. `app.restart` remains available. Omit
185
230
  `reconnectEndpoint` to keep ordinary isolated contexts and the existing behavior.
186
231
  A `browser` provider with `scope: 'attempt'` rides the same persistent
@@ -202,7 +247,7 @@ a page acts on that tab: `goto`, `reload`, `back`, `forward`, `url`, `title`,
202
247
  `waitForURL`, `locator`, `frameLocator`, `evaluate`, `waitForResponse`,
203
248
  `waitForDownload`, `keyboard`, `mouse`, the `expect(browser)` matchers, and
204
249
  every `screen` query. What belongs to the browser covers every tab:
205
- `cookies`, `setCookies`, `route`, `unroute`, and `onDialog`. `setViewport`
250
+ `cookies`, `setCookies`, `route`, `unroute`, `onDialog`, and `addInitScript`. `setViewport`
206
251
  sizes the active tab and every tab opened after it.
207
252
 
208
253
  A tab the app opens itself, from a `target="_blank"` link or `window.open`,
@@ -253,6 +298,10 @@ export interface Browser extends Expectable<BrowserExpectation> {
253
298
  fn: string | ((arg: Arg) => T | Promise<T>),
254
299
  arg: Arg,
255
300
  ): Promise<T>;
301
+ /** Adds a script every document runs before the page's own, from the next navigation on. */
302
+ addInitScript(script: string | { path: string } | (() => unknown)): Promise<void>;
303
+ /** Adds an init script function called with one JSON argument. */
304
+ addInitScript<Arg extends JsonValue>(script: (arg: Arg) => unknown, arg: Arg): Promise<void>;
256
305
  /** Adds an attempt-scoped network route. */
257
306
  route(
258
307
  pattern: string | RegExp,
@@ -308,7 +357,7 @@ export interface Browser extends Expectable<BrowserExpectation> {
308
357
  | `goto`, `reload`, `back`, `forward` | `config.timeout`, 120000 ms, the same budget `app.open()` uses; `{ timeout }` overrides, capped by the test deadline |
309
358
  | `waitForResponse`, `waitForDownload` | `config.actionTimeout`, 30000 ms; `{ timeout }` overrides, capped by the test deadline |
310
359
  | `waitForURL` | `config.assertionTimeout`, 5000 ms |
311
- | `url`, `title`, `evaluate`, `route`, `unroute`, `cookies`, `setCookies`, `setViewport`, `onDialog`, `keyboard.*`, `mouse.*` | `config.actionTimeout`, 30000 ms, not overridable |
360
+ | `url`, `title`, `evaluate`, `addInitScript`, `route`, `unroute`, `cookies`, `setCookies`, `setViewport`, `onDialog`, `keyboard.*`, `mouse.*` | `config.actionTimeout`, 30000 ms, not overridable |
312
361
 
313
362
  Every `browser` call is a recorded `browser.<method>` step, except `locator` and
314
363
  `frameLocator`: they build a locator without touching the page, and the
@@ -319,7 +368,7 @@ in a suite that also runs on other platforms.
319
368
  ### Navigation
320
369
 
321
370
  `goto` defaults `waitUntil` to `'load'` and throws `POLICY_DENIED` for a
322
- `file:`, `data:`, or `javascript:` URL. `waitForURL` polls every 100 ms; a
371
+ URL whose scheme is not `http:` or `https:` (`about:blank` is admitted). `waitForURL` polls every 100 ms; a
323
372
  relative string resolves against the base URL and matches exactly after
324
373
  normalization, a `RegExp` tests the full URL, anything else (a predicate
325
374
  function, say) is `INVALID_ARGUMENT`, and a timeout is `ASSERTION_FAILED`. It
@@ -391,16 +440,49 @@ const flag = await browser.evaluate(
391
440
  );
392
441
  ```
393
442
 
443
+ ### Init scripts
444
+
445
+ An init script runs in every document before the page's own scripts, in every
446
+ tab and frame. `web({ initScripts })` applies to every test;
447
+ `browser.addInitScript` adds one for the rest of the test.
448
+
449
+ A script is JavaScript source, a `{ path }` relative to the project root, or a
450
+ function. A function cannot close over test variables; pass a JSON `arg`
451
+ instead.
452
+
453
+ ```ts
454
+ await browser.addInitScript((value) => { Math.random = () => value; }, 0.5);
455
+ await app.open('/raffle');
456
+ ```
457
+
458
+ A script added after the page opened runs from the next navigation or reload.
459
+
394
460
  ### Routing
395
461
 
396
462
  `route`, `unroute`, and `waitForResponse` take a string pattern (`*` within a
397
463
  path segment, `**` across `/`) or a `RegExp`, both tested against the full
398
464
  URL; a predicate function is `INVALID_ARGUMENT`. A route survives
399
465
  `app.restart()`, `app.clearState()`, and session restore.
400
- The handler must decide exactly once with `fulfill`, `continue`, or `abort`;
401
- deciding twice or not at all is `ACTION_FAILED`, as is a `waitForResponse`
402
- that nothing matches inside the deadline. The response resolves once its body
403
- is known: `url`, `status`, and `headers` are always readable, and a body the
466
+ The handler must decide exactly once with `fulfill`, `continue`, `fallback`,
467
+ or `abort`; deciding twice or not at all is `ACTION_FAILED`. Routes run newest
468
+ first. `continue` sends the request to the network and no other route sees
469
+ it; its `url` resolves against the base URL and goes through the same rule as
470
+ `goto`, so a forbidden scheme is `POLICY_DENIED`, and it must keep the
471
+ request's scheme. `headers` replaces every request header. The configured
472
+ `headers` still ride a continued request bound for the app's site, and only
473
+ that site. `fallback` hands the request to the route registered before this
474
+ one, or the network when none matches. `fulfill` takes one of `json`, `body`,
475
+ or `path`, a file relative to the project root whose extension sets the
476
+ `Content-Type` unless `contentType` or a `content-type` header does. An option
477
+ a decision does not take (Playwright's `abort(errorCode)`, a misspelled key) is
478
+ `INVALID_ARGUMENT`: the request is aborted and the next step fails.
479
+
480
+ A `waitForResponse` that nothing matches inside the deadline is
481
+ `ACTION_FAILED`. The deadline bounds the match: the response resolves as soon
482
+ as its headers arrive, and `text()` and `json()` wait for the body on a budget
483
+ of their own, `config.actionTimeout` capped by the test deadline; a body still
484
+ streaming when it runs out is `ACTION_FAILED`. `url`, `status`, and `headers`
485
+ are always readable, and a body the
404
486
  browser could not read (a connection reset short of the declared
405
487
  `Content-Length`, a request the page aborted, or a redirect, whose body the
406
488
  browser never keeps) rejects `text()` and `json()` with `ACTION_FAILED`
@@ -421,16 +503,35 @@ expect(response.status).toBe(201);
421
503
  ```
422
504
 
423
505
  ```ts
424
- /** `json` or `body`, never both; neither fulfills with an empty body. */
506
+ /**
507
+ * One of `json`, `body`, or `path`, or none for an empty body. `path` names a
508
+ * file relative to the project root; its `Content-Type` follows its
509
+ * extension unless `contentType` or a `content-type` header sets one.
510
+ */
425
511
  export type RouteFulfillResponse = {
426
512
  status?: number;
427
513
  headers?: Record<string, string>;
514
+ /** Sets the `Content-Type` response header. */
515
+ contentType?: string;
428
516
  } & (
429
- | { json: JsonValue; body?: never }
430
- | { body: string; json?: never }
431
- | { body?: never; json?: never }
517
+ | { json: JsonValue; body?: never; path?: never }
518
+ | { body: string; json?: never; path?: never }
519
+ | { path: string; json?: never; body?: never }
520
+ | { body?: never; json?: never; path?: never }
432
521
  );
433
522
 
523
+ /** What `continue` changes about the request before it goes to the network. */
524
+ export interface RouteContinueOverrides {
525
+ /** Resolved against the base URL like `goto`; must keep the request's scheme. */
526
+ url?: string;
527
+ /** Request method. */
528
+ method?: string;
529
+ /** Replaces every request header; the configured `headers` for the app's site are added on top. */
530
+ headers?: Record<string, string>;
531
+ /** Request body. */
532
+ postData?: string;
533
+ }
534
+
434
535
  export interface WebRoute {
435
536
  /** The intercepted request. */
436
537
  readonly request: {
@@ -441,8 +542,10 @@ export interface WebRoute {
441
542
  };
442
543
  /** Fulfills the intercepted request once. */
443
544
  fulfill(response: RouteFulfillResponse): Promise<void>;
444
- /** Continues the intercepted request once. */
445
- continue(): Promise<void>;
545
+ /** Sends the intercepted request to the network once, skipping every other route. */
546
+ continue(overrides?: RouteContinueOverrides): Promise<void>;
547
+ /** Hands the intercepted request to the route registered before this one, or the network when none matches. */
548
+ fallback(): Promise<void>;
446
549
  /** Aborts the intercepted request once. */
447
550
  abort(): Promise<void>;
448
551
  }
@@ -454,9 +557,9 @@ export interface WebResponse {
454
557
  readonly status: number;
455
558
  /** Response headers, lower-cased names. */
456
559
  readonly headers: Readonly<Record<string, string>>;
457
- /** Parses the response body as JSON. */
560
+ /** Waits for the body and parses it as JSON. */
458
561
  json<T = unknown>(): Promise<T>;
459
- /** Reads the response body as text. */
562
+ /** Waits for the body and reads it as text. */
460
563
  text(): Promise<string>;
461
564
  }
462
565
  ```
@@ -464,7 +567,8 @@ export interface WebResponse {
464
567
  ### Cookies and viewport
465
568
 
466
569
  `setCookies` validates every cookie before setting any; an unparseable
467
- target or a forbidden scheme is `POLICY_DENIED` and nothing is set. For the
570
+ target or a forbidden scheme is `POLICY_DENIED` and nothing is set. A
571
+ relative `url` resolves against the base URL, as `goto` resolves it. For the
468
572
  `domain` form the scheme follows the base URL and a leading `.` is stripped.
469
573
  `setViewport` records the new size on the report. It works before the first
470
574
  navigation too, as Playwright's `setViewportSize` does: the next `app.open`
@@ -586,7 +690,7 @@ cannot reuse node IDs or publish handles over a newer observation.
586
690
  `BrowserProvider`, `BrowserProviderScope`, `BrowserRequest`, `BrowserLease`,
587
691
  `BrowserReleaseContext`, `BrowserProviderDownloads`, `BrowserDownloadContext`,
588
692
  `PlaywrightLiveSurface`, `Browser`, `FrameScreen`,
589
- `BrowserExpectation`, `WebRoute`, `RouteFulfillResponse`, `WebResponse`,
693
+ `BrowserExpectation`, `WebRoute`, `RouteFulfillResponse`, `RouteContinueOverrides`, `WebResponse`,
590
694
  `Cookie`, `CookieFields`, `Dialog`, and `DialogHandler` are exported from the
591
695
  package for typing config helpers and fixtures. The runtime exports are
592
696
  `web`, [`surfaceOf`](#surfaceof), and `test`, `describe`, `beforeEach`,
package/docs/security.mdx CHANGED
@@ -48,7 +48,7 @@ A tool call that breaks a rule fails with `POLICY_DENIED`:
48
48
 
49
49
  | Denied | Why |
50
50
  | --- | --- |
51
- | Navigation to `file:`, `data:`, or `javascript:` | These URL schemes are forbidden. |
51
+ | Navigation to any scheme but `http:` and `https:`, other than `about:blank` | `file:`, `data:`, `javascript:`, and wrappers such as `view-source:file:` would load local or inline content. |
52
52
  | A malformed URL | Nothing is repaired. |
53
53
  | A `Secret` reaching a JSON sink | Handles carry no value, and no value may be serialized. |
54
54
  | Reading a value, text, or attribute from a secure field, with a getter or a `toHaveValue`, `toHaveText`, `toContainText`, or `toHaveAttribute` matcher | Assert the outcome, not the secret. A withheld value is redacted, not empty or absent. |
@@ -77,10 +77,12 @@ list. Shared hosting domains such as `vercel.app` count as one site, so
77
77
  headers can reach other hosts under that domain. `basicAuth` is broader:
78
78
  it answers a `401` challenge from any origin.
79
79
 
80
- The runner checks forbidden schemes on explicit navigation: `app.open`, the
81
- agent's `navigate` verb, and `device.openLink`. `device.openApp` and the
82
- `open_app` tool take an app id and refuse a link, since the device would
83
- open one as a URL. The runner does not re-check redirects or attach to popup
80
+ The runner checks the scheme on explicit navigation. `app.open`,
81
+ `browser.goto`, and the agent's `navigate` verb admit `http:`, `https:`,
82
+ and the exact `about:blank`. A device link may use an app's custom scheme, so `device.openLink`
83
+ refuses a list instead: `file:`, `data:`, `javascript:`, `view-source:`,
84
+ `blob:`, and `filesystem:`. `device.openApp` and the `open_app` tool take an
85
+ app id and refuse a link, since the device would open one as a URL. The runner does not re-check redirects or attach to popup
84
86
  pages. Register `browser.onDialog` to handle browser dialogs.
85
87
 
86
88
  ## Secrets
@@ -145,9 +147,13 @@ Matching covers the value as written, JSON-escaped, HTML-escaped, and
145
147
  percent-encoded, in any letter case (so a CSS `text-transform` does not hide
146
148
  it), and with its whitespace collapsed. Text cut at a length limit is masked
147
149
  too when it ends with the first 8 or more characters of a value (half of a
148
- value shorter than 16). Redaction rewrites only what the runner reports;
149
- assertions still compare the text the page shows. Secret values never enter
150
- cache entries.
150
+ value shorter than 16), as written or with its whitespace collapsed, the way
151
+ an engine collapses text before it cuts it. Every string of the observed
152
+ screen, names, text, values, test ids, attributes, selectors, and frame
153
+ paths alike, is redacted once, before the model, an executor's tree, a
154
+ replay cache descriptor, or an end anchor reads it. Redaction rewrites only
155
+ what the runner reports; assertions still compare the text the page shows.
156
+ Secret values and their cut parts never enter cache entries.
151
157
 
152
158
  A static value shorter than 6 characters is `INVALID_CONFIG`, and a provider
153
159
  returning one fails the fill: every occurrence of so short a value would take
@@ -161,13 +167,13 @@ ordinary text with it.
161
167
  a hash, is a different string and passes through.
162
168
  - A value the app renders in another Unicode normalization form, decomposed
163
169
  accents where the secret has composed ones, passes through.
164
- - Text that widens the whitespace inside the value, or drops it, passes
165
- through.
170
+ - Text that drops the whitespace inside the value passes through. Text that
171
+ widens it passes through too, except on the observed screen: a field whose
172
+ collapsed form holds the value is kept collapsed and masked.
166
173
  - A value spread over several nodes, one character per cell as a PIN pad
167
174
  shows it, is not one string in the tree and passes through.
168
- - Text cut at a length limit is matched by its prefix only in the value's
169
- own form: a case mapping that changes length (`ß` shown as `SS`) or
170
- whitespace collapsed to one space escapes the prefix rule.
175
+ - A case mapping that changes length (`ß` shown as `SS`) escapes the prefix
176
+ rule for text cut at a length limit.
171
177
  - A title is redacted when its file is collected, before any provider
172
178
  runs, so a value a secret provider returns is not redacted from titles.
173
179
  - A Playwright trace from a session no secret reached is kept as recorded,
@@ -178,7 +184,7 @@ ordinary text with it.
178
184
  redacted.
179
185
  - A Playwright trace from a session a secret reached, filled or held by the
180
186
  engine, is rewritten before it is kept: any run of 8 or more characters of
181
- a value becomes `<secret:name>`, so unrelated text that shares 8 characters
187
+ a value, its whitespace collapsed or not, becomes `<secret:name>`, so unrelated text that shares 8 characters
182
188
  with a value is masked too. A base64 or base64url run that decodes to a
183
189
  value or such a fragment (a basic-auth header, a cookie, a token segment)
184
190
  is replaced whole. After a fill, screencast frames are dropped; with only
@@ -230,8 +236,12 @@ actions without consulting a model and are not signed.
230
236
 
231
237
  The runner validates entries before use. Invalid entries become misses.
232
238
  Files are named by a hash of the expected key and refused if larger than
233
- 1 MiB. Entries store actions and a redacted summary; replay executes only
234
- the actions. Secret fills store a name and authorize the fill again.
239
+ 1 MiB. The key holds the agent's context only as a hash of its redacted
240
+ text, so no secret value enters it. Entries store actions and a redacted
241
+ summary; replay executes only the actions. Secret fills store a name and
242
+ authorize the fill again. An entry recorded by one agent never replays for
243
+ another, and one recorded on the app's origin never replays on a page from
244
+ another origin.
235
245
 
236
246
  `e2e init` ignores `.e2e/cache/` in Git. If you choose to
237
247
  [commit recordings](/cache#commit-the-replay-cache), review their actions and typed
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Subscriptions
3
- description: Use your ChatGPT, GitHub Copilot, or SuperGrok subscription for agent steps.
3
+ description: Use your ChatGPT, GitHub Copilot, OpenCode Console, or SuperGrok subscription for agent steps.
4
4
  ---
5
5
 
6
6
  Use an existing subscription without a separate API key. Choose your plan
@@ -10,6 +10,7 @@ in `e2e init`, install dependencies, then sign in:
10
10
  | --- | --- |
11
11
  | ChatGPT Plus or Pro | `npx e2e login openai` |
12
12
  | GitHub Copilot | `npx e2e login github-copilot` |
13
+ | OpenCode Console (OpenCode Zen and OpenCode Go) | `npx e2e login opencode-console` |
13
14
  | SuperGrok or X Premium+ | `npx e2e login spacexai` |
14
15
 
15
16
  Follow the terminal prompts, then run `npx e2e run`. Agent calls use your
@@ -52,17 +53,17 @@ const agent = { model: chatgpt('gpt-6-luna') };
52
53
  <Tab title="GitHub Copilot">
53
54
  <CodeGroup>
54
55
  ```bash npm
55
- npm install -D ai @ai-sdk/openai-compatible
56
+ npm install -D ai @ai-sdk/openai-compatible @ai-sdk/openai
56
57
  npx e2e login github-copilot
57
58
  ```
58
59
 
59
60
  ```bash pnpm
60
- pnpm add -D ai @ai-sdk/openai-compatible
61
+ pnpm add -D ai @ai-sdk/openai-compatible @ai-sdk/openai
61
62
  pnpm exec e2e login github-copilot
62
63
  ```
63
64
 
64
65
  ```bash bun
65
- bun add -d ai @ai-sdk/openai-compatible
66
+ bun add -d ai @ai-sdk/openai-compatible @ai-sdk/openai
66
67
  bunx e2e login github-copilot
67
68
  ```
68
69
  </CodeGroup>
@@ -71,6 +72,33 @@ bunx e2e login github-copilot
71
72
  import { copilot } from 'e2e/oauth/copilot';
72
73
 
73
74
  const agent = { model: copilot('claude-sonnet-5') };
75
+ ```
76
+ </Tab>
77
+ <Tab title="OpenCode Console">
78
+ <CodeGroup>
79
+ ```bash npm
80
+ npm install -D ai @ai-sdk/openai-compatible @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google
81
+ npx e2e login opencode-console
82
+ ```
83
+
84
+ ```bash pnpm
85
+ pnpm add -D ai @ai-sdk/openai-compatible @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google
86
+ pnpm exec e2e login opencode-console
87
+ ```
88
+
89
+ ```bash bun
90
+ bun add -d ai @ai-sdk/openai-compatible @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google
91
+ bunx e2e login opencode-console
92
+ ```
93
+ </CodeGroup>
94
+
95
+ ```ts title="Model setup"
96
+ import { opencodeConsole } from 'e2e/oauth/opencode-console';
97
+
98
+ // OpenCode Zen, billed to the workspace's credits
99
+ const agent = { model: opencodeConsole('deepseek-v4.1-flash') };
100
+ // OpenCode Go, billed to your Go subscription
101
+ const goAgent = { model: opencodeConsole('go/deepseek-v4.1-flash') };
74
102
  ```
75
103
  </Tab>
76
104
  <Tab title="SuperGrok">
@@ -124,20 +152,49 @@ bunx e2e models openai # one provider
124
152
  ```
125
153
  </CodeGroup>
126
154
 
127
- Each line is the id to pass to `chatgpt()`, `copilot()`, or `grok()`, the
128
- vendor's display name, and what it says about the model: reasoning levels,
129
- vision, preview, or hidden. ChatGPT lists what the Codex backend serves your
130
- plan; Copilot lists the chat models of your plan; SuperGrok lists the
131
- language models the token reaches.
155
+ Each line is the id to pass to `chatgpt()`, `copilot()`, `opencodeConsole()`,
156
+ or `grok()`, the vendor's display name, and what it says about the model:
157
+ reasoning levels, vision, preview, or hidden. ChatGPT lists what the Codex
158
+ backend serves your plan; Copilot lists the chat models of your plan;
159
+ OpenCode Console lists the Zen and Go models your workspace has enabled,
160
+ each tagged with the plan that pays for it, and leaves out custom providers; SuperGrok lists the language models the
161
+ token reaches.
132
162
 
133
163
  Copilot also lists models that `copilot()` cannot call, and marks them:
134
164
 
135
165
  - `not enabled`: the model is off for your plan. Turn it on in your Copilot
136
166
  settings on GitHub, or ask your organization admin if they manage your
137
167
  seat.
138
- - `no chat completions`: Copilot serves the model only over other APIs,
139
- mostly its Responses API. `copilot()` uses chat completions, so pick
140
- another model.
168
+ - `no chat or responses`: Copilot serves the model only over APIs `copilot()`
169
+ does not speak, such as Anthropic's Messages API, so pick another model.
170
+
171
+ On its first call `copilot()` picks the endpoint from the model's entry in
172
+ this same listing: chat completions for most models, Copilot's Responses API
173
+ for the ones served only there. When the listing cannot be read, that call
174
+ goes over chat completions and the next call reads it again. The Responses
175
+ call runs through `@ai-sdk/openai`, so install it beside
176
+ `@ai-sdk/openai-compatible` to use those models; `e2e init` adds both.
177
+
178
+ ### OpenCode Console
179
+
180
+ One OpenCode Console sign-in serves both plans billed through it:
181
+
182
+ - A bare id, such as `deepseek-v4.1-flash`, is an OpenCode Zen model, paid from
183
+ the workspace's credits.
184
+ - A `go/` id, such as `go/deepseek-v4.1-flash`, is an OpenCode Go model, paid
185
+ from the Go subscription. Go models are listed only when you hold the
186
+ workspace's Go subscription.
187
+
188
+ The list leaves out models your workspace has turned off, and Zen's free
189
+ models, which OpenCode keeps for its own clients.
190
+
191
+ On its first call `opencodeConsole()` reads the workspace config to pick the
192
+ model's API: chat completions, OpenAI Responses (`@ai-sdk/openai`), Anthropic
193
+ Messages (`@ai-sdk/anthropic`), or Google (`@ai-sdk/google`). Install those
194
+ beside `@ai-sdk/openai-compatible`; `e2e init` adds all four. Set
195
+ `providerOptions` under the key of that API: `opencode` for chat completions
196
+ models, on Zen and Go alike, and `openai`, `anthropic`, or `google` for the
197
+ others.
141
198
 
142
199
  ## Sign-in help
143
200
 
@@ -146,6 +203,9 @@ Copilot also lists models that `copilot()` cannot call, and marks them:
146
203
  - Copilot reuses your GitHub CLI login. If you do not use `gh`, pass
147
204
  `--client-id` for your own GitHub OAuth app with device flow enabled.
148
205
  Use `--enterprise-url` for GitHub Enterprise.
206
+ - OpenCode Console prints a device code and opens the page where you approve
207
+ it and pick the workspace; the page shows e2e as an unknown client. To
208
+ switch workspaces, run `npx e2e logout opencode-console` and sign in again.
149
209
  - SuperGrok prints a device code and the URL where you enter it.
150
210
 
151
211
  Run `npx e2e login` to choose a provider and see which accounts are signed
@@ -164,3 +224,22 @@ instead of the local file, but refreshed tokens are not saved back to that
164
224
  variable. A value containing a rotating refresh token cannot be reused
165
225
  indefinitely. See [CLI reference](/reference/cli#e2e-login) for all sign-in
166
226
  options.
227
+
228
+ For OpenCode Console in CI, create a service account API key in the Console
229
+ (Service Accounts, then API Keys) and set it as `OPENCODE_API_KEY`;
230
+ `opencodeConsole()` uses it instead of the stored login. `e2e models` still
231
+ reads the stored login.
232
+
233
+ <CodeGroup>
234
+ ```bash npm
235
+ OPENCODE_API_KEY=oc_sk_... npx e2e run
236
+ ```
237
+
238
+ ```bash pnpm
239
+ OPENCODE_API_KEY=oc_sk_... pnpm exec e2e run
240
+ ```
241
+
242
+ ```bash bun
243
+ OPENCODE_API_KEY=oc_sk_... bunx e2e run
244
+ ```
245
+ </CodeGroup>
@@ -119,7 +119,7 @@ Versions:
119
119
  | `command` | `run` or `explore` |
120
120
  | `status`, `exit_code`, `duration_ms` | The run's outcome |
121
121
  | `flags` | The names of the flags given to the run command, never their values |
122
- | `tests_discovered`, `tests_selected`, `tests_executed`, `tests_passed`, `tests_failed`, `tests_flaky`, `tests_skipped` | The summary counts |
122
+ | `tests_discovered`, `tests_selected`, `tests_executed`, `tests_passed`, `tests_failed`, `tests_interrupted`, `tests_flaky`, `tests_skipped` | The summary counts |
123
123
  | `attempts_total`, `tests_retried` | How many attempts the run made across tests and serial groups, and how many of them needed more than one |
124
124
  | `targets`, `platforms`, `engines` | How many targets, on which platforms, and with which engines and versions, as your config declares them: `web`, `ios`, `web@0.6.1`, or your own engine's name and version. A name that is not a plain token of letters, digits, dots, dashes, and underscores is reported as `other` |
125
125
  | `steps_total`, `steps_agent`, `steps_locator`, `steps_assertion`, `steps_screen`, `steps_app`, `steps_session`, `steps_resource` | Step counts by kind |