e2e 0.15.1 → 0.15.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 (87) hide show
  1. package/README.md +51 -24
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act.js +3 -3
  4. package/dist/agent/act.js.map +1 -1
  5. package/dist/agent/error.d.ts +16 -3
  6. package/dist/agent/error.d.ts.map +1 -1
  7. package/dist/agent/error.js +25 -6
  8. package/dist/agent/error.js.map +1 -1
  9. package/dist/agent/model/tool-choice.d.ts +24 -1
  10. package/dist/agent/model/tool-choice.d.ts.map +1 -1
  11. package/dist/agent/model/tool-choice.js +27 -1
  12. package/dist/agent/model/tool-choice.js.map +1 -1
  13. package/dist/agent/step-cache.d.ts +14 -3
  14. package/dist/agent/step-cache.d.ts.map +1 -1
  15. package/dist/agent/step-cache.js +13 -3
  16. package/dist/agent/step-cache.js.map +1 -1
  17. package/dist/agent/tool-loop.d.ts.map +1 -1
  18. package/dist/agent/tool-loop.js +62 -9
  19. package/dist/agent/tool-loop.js.map +1 -1
  20. package/dist/cache/context.d.ts +19 -6
  21. package/dist/cache/context.d.ts.map +1 -1
  22. package/dist/cache/context.js +15 -21
  23. package/dist/cache/context.js.map +1 -1
  24. package/dist/cli/init/scaffold.d.ts +1 -0
  25. package/dist/cli/init/scaffold.d.ts.map +1 -1
  26. package/dist/cli/init/scaffold.js +2 -1
  27. package/dist/cli/init/scaffold.js.map +1 -1
  28. package/dist/cli/init/sibling-versions.json +1 -1
  29. package/dist/expect/values.js +1 -1
  30. package/dist/expect/values.js.map +1 -1
  31. package/dist/run/execute.d.ts.map +1 -1
  32. package/dist/run/execute.js +21 -8
  33. package/dist/run/execute.js.map +1 -1
  34. package/dist/types.d.ts +1 -1
  35. package/dist/types.d.ts.map +1 -1
  36. package/docs/agent-steps.mdx +126 -0
  37. package/docs/agents.mdx +217 -0
  38. package/docs/api-testing.mdx +65 -0
  39. package/docs/authentication.mdx +345 -0
  40. package/docs/browser.mdx +440 -0
  41. package/docs/bug-bash.mdx +90 -0
  42. package/docs/cache.mdx +306 -0
  43. package/docs/ci.mdx +210 -0
  44. package/docs/coding-agents.mdx +183 -0
  45. package/docs/debugging.mdx +291 -0
  46. package/docs/executors.mdx +243 -0
  47. package/docs/explore.mdx +174 -0
  48. package/docs/github.mdx +184 -0
  49. package/docs/index.mdx +82 -0
  50. package/docs/integrations/eas.mdx +216 -0
  51. package/docs/integrations/index.mdx +24 -0
  52. package/docs/integrations/kernel.mdx +163 -0
  53. package/docs/migrate/cypress.mdx +124 -0
  54. package/docs/migrate/detox.mdx +140 -0
  55. package/docs/migrate/maestro.mdx +229 -0
  56. package/docs/migrate/playwright.mdx +427 -0
  57. package/docs/migrate/selenium.mdx +110 -0
  58. package/docs/mobile-ci.mdx +567 -0
  59. package/docs/mobile.mdx +555 -0
  60. package/docs/models.mdx +241 -0
  61. package/docs/quickstart.mdx +260 -0
  62. package/docs/reference/agent.mdx +524 -0
  63. package/docs/reference/app.mdx +72 -0
  64. package/docs/reference/cli.mdx +817 -0
  65. package/docs/reference/config.mdx +986 -0
  66. package/docs/reference/engine.mdx +1362 -0
  67. package/docs/reference/environment.mdx +113 -0
  68. package/docs/reference/errors.mdx +143 -0
  69. package/docs/reference/expect.mdx +513 -0
  70. package/docs/reference/mcp.mdx +202 -0
  71. package/docs/reference/mobile.mdx +522 -0
  72. package/docs/reference/reporters.mdx +216 -0
  73. package/docs/reference/screen.mdx +688 -0
  74. package/docs/reference/test.mdx +449 -0
  75. package/docs/reference/web.mdx +611 -0
  76. package/docs/security.mdx +292 -0
  77. package/docs/starting-your-app.mdx +231 -0
  78. package/docs/subscriptions.mdx +157 -0
  79. package/docs/telemetry.mdx +185 -0
  80. package/docs/tools.mdx +150 -0
  81. package/docs/viewports.mdx +146 -0
  82. package/docs/writing-an-engine.mdx +316 -0
  83. package/docs/writing-tests.mdx +256 -0
  84. package/package.json +2 -1
  85. package/skills/e2e/SKILL.md +4 -1
  86. package/skills/e2e/references/agent.md +3 -1
  87. package/skills/e2e/references/setup.md +13 -4
@@ -0,0 +1,611 @@
1
+ ---
2
+ title: Browser
3
+ description: The browser engine, its options, and the browser fixture it contributes.
4
+ ---
5
+
6
+ `@e2e-dev/web` drives Chromium, Firefox, and WebKit through Playwright.
7
+ A web target names it as its engine, and it contributes the `browser` fixture for
8
+ browser-only work. Import `test` from the package to have `browser` typed.
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.
13
+
14
+ ```ts title="e2e.config.ts"
15
+ import type { E2EConfig } from 'e2e';
16
+ import { web } from '@e2e-dev/web';
17
+
18
+ export default {
19
+ targets: [{ engine: web(), app: { url: 'http://localhost:3000' } }],
20
+ } satisfies E2EConfig;
21
+ ```
22
+
23
+ ## Options
24
+
25
+ ```ts
26
+ /**
27
+ * Options of the browser engine: how it drives the app. The app itself
28
+ * (its `url`, `environment`, `identity`, and the command that starts it) is
29
+ * the target's `app`.
30
+ */
31
+ export interface WebOptions {
32
+ /**
33
+ * Browser to launch, `chromium` by default. A `BrowserProvider` leases
34
+ * hosted browsers instead: one per worker slot at `prepare`, or one per
35
+ * attempt, each attached to over CDP and released when its scope ends. A
36
+ * provider implies chromium and excludes `connect`.
37
+ */
38
+ readonly browser?: BrowserName | BrowserProvider;
39
+ /**
40
+ * Initial viewport of every attempt's page; default 1280 by 720. `null`
41
+ * emulates no size: the page fills the browser window, whatever size the
42
+ * window has (a hosted browser's live view, a headed run), and
43
+ * `browser.setViewport` still fixes one for the rest of the attempt.
44
+ */
45
+ readonly viewport?: ViewportSize | null;
46
+ /**
47
+ * How the engine's own screencast records an attempt that records video.
48
+ * A browser provider that records (`BrowserProvider.record`) records in
49
+ * its place, and ignores these. Which attempts record is the config's
50
+ * `video`, not an engine option.
51
+ */
52
+ readonly screencast?: WebScreencastOptions;
53
+ /**
54
+ * Attach to a remote browser over CDP instead of launching locally. Requires
55
+ * the chromium browser (the default). Wired by a hosted-browser engine.
56
+ */
57
+ readonly connect?: WebConnectOptions;
58
+ /**
59
+ * Headers added to requests within the app's site, including subdomains.
60
+ * Useful for preview-protection tokens. Other sites never receive them.
61
+ * Names are case-insensitive and replace existing headers of the same name.
62
+ * This enables request routing, disables the browser's HTTP cache, and
63
+ * blocks service workers. Playwright traces record request headers.
64
+ */
65
+ readonly headers?: Readonly<Record<string, string>>;
66
+ /**
67
+ * HTTP basic authentication. The browser answers a `401` challenge with
68
+ * these credentials on any origin, matching Playwright's `httpCredentials`.
69
+ */
70
+ readonly basicAuth?: WebBasicAuth;
71
+ /**
72
+ * The attribute that carries an element's test id: what the `testId` query
73
+ * (`screen.getByTestId`) resolves and what `SemanticNode.testId` reports.
74
+ * Defaults to `data-testid`.
75
+ */
76
+ readonly testIdAttribute?: string;
77
+ /**
78
+ * The `User-Agent` every attempt's context sends and `navigator.userAgent`
79
+ * reports, for an app that switches into a test mode on a marker in the
80
+ * agent string. Defaults to the browser's own. A `user-agent` entry in
81
+ * `headers` beside it is `INVALID_CONFIG`, and so is a persistent context
82
+ * (`connect.reconnectEndpoint`, or a provider with `scope: 'attempt'`).
83
+ */
84
+ readonly userAgent?: string;
85
+ }
86
+
87
+ /** The engine's screencast of the page: the frame size and the JPEG quality of the frames it encodes. */
88
+ export interface WebScreencastOptions {
89
+ /** Frame size in pixels; default the viewport's, or the window's under `viewport: null`. A smaller size makes a smaller file. */
90
+ readonly size?: ViewportSize;
91
+ /** JPEG quality of each captured frame, 0 through 100; Playwright's default when unset. */
92
+ readonly quality?: number;
93
+ }
94
+ ```
95
+
96
+ The app a web target opens is the target's `app`, not an engine option:
97
+ `targets: [{ engine: web(), app: { url } }]`, documented under
98
+ [the app under test](/reference/config#the-app-under-test). A `web()` target
99
+ needs `app.url` and refuses the device keys (`bundleId`, `appPath`,
100
+ `launchArguments`, `permissions`); `web({ url })` or any other app key on
101
+ `web()` is an unknown key, `INVALID_CONFIG`.
102
+ A target with `app.url` is a `browser` target, so `requires: ['browser']`
103
+ selects web targets.
104
+
105
+ `connect` needs chromium; `cdpEndpoint(signal)` is called once per worker and
106
+ again at an attempt start after a disconnect. A `browser` provider object leases hosted
107
+ browsers and releases them when the run or the attempt ends; see
108
+ [Hosted browsers](/browser#hosted-browsers) for the `BrowserProvider`
109
+ contract and an example provider. `browser: provider` together with
110
+ `connect` is `INVALID_CONFIG`, and so is an option `web()` does not take, or
111
+ an unknown key inside `connect` or `basicAuth`, naming the nearest known key
112
+ when it is a likely typo (`viewprt` is `viewport`) and every key otherwise. `:` is not allowed in the `basicAuth` user name.
113
+ The `basicAuth` password is a string or `secrets.get(name)`: a secret the
114
+ engine resolves when each attempt starts, redacted like any configured one,
115
+ and so is the base64 credential of the `Authorization` header it becomes.
116
+ That protection is text only: unlike a fill, it withholds no screenshot or
117
+ model pixels, so a page that renders the password on screen is not masked in
118
+ them ([Security](/security#secrets)).
119
+
120
+ `headers` and `basicAuth` are how a test reaches a [protected preview](/starting-your-app#protected-previews).
121
+
122
+ `screencast` shapes the recording, not which attempts record: that is the
123
+ config's [`video`](/reference/config#video) mode. `size` takes whole pixels,
124
+ each side at least 1, and `quality` an integer from 0 through 100; anything
125
+ else, or another key, is `INVALID_CONFIG` at config load. The old
126
+ `web({ video })` spelling is `INVALID_CONFIG` naming `screencast`.
127
+
128
+ ### Recovering a CDP transport
129
+
130
+ `connect.reconnectEndpoint(signal)` opts into recovery of the same remote
131
+ browser after its CDP transport disconnects. This mode uses the browser's
132
+ persistent default context because Chrome deletes ordinary Playwright contexts
133
+ when their transport detaches.
134
+
135
+ ```ts
136
+ interface WebConnectOptions {
137
+ cdpEndpoint(signal: AbortSignal): string | Promise<string>;
138
+ reconnectEndpoint?(signal: AbortSignal): string | Promise<string>;
139
+ }
140
+ ```
141
+
142
+ In this mode, `cdpEndpoint` provisions a fresh, dedicated browser at every
143
+ attempt start, including retries. The engine rejects a browser reused by a
144
+ previous attempt. The remote must have only its default context, and the host
145
+ must delete the browser when the attempt ends. Engine cleanup detaches its
146
+ connection; it does not terminate the host's browser.
147
+
148
+ After a disconnect, the next operation calls `reconnectEndpoint` once, bounded
149
+ by that operation's timeout and abort signal. This resolver must return the
150
+ existing browser's endpoint. The engine verifies the persistent default context
151
+ ID and the original page's target ID. It refuses browsers that do not expose
152
+ the default context ID. A missing page or a different context fails the attempt.
153
+ A URL match is insufficient to select a replacement tab.
154
+ The engine installs secure-field masking before publishing each CDP binding;
155
+ an incompatible Playwright version fails the connection instead of exposing pixels.
156
+ Recovery also requires the original page's frames to retain their closed shadow
157
+ DOM tracking hooks. If a document loaded without that hook while disconnected,
158
+ recovery fails with `ENGINE_FAILURE`: existing closed roots cannot be recovered
159
+ without reloading the application. Mask queries reject any untracked document.
160
+
161
+ Endpoint resolution, CDP attachment, setup, and dispatch share one timeout.
162
+ Exhausting it raises `OPERATION_TIMEOUT`; caller or attempt cancellation raises
163
+ `CANCELLED`. Cancelling a concurrent waiter does not cancel the operation that
164
+ started recovery. Cancelling that recovery owner fails the shared recovery.
165
+
166
+ Recovery clears old node references. Observation-derived `tapAt` calls,
167
+ observation-root swipes, and focused engine keyboard input require a fresh
168
+ engine observation before they resume.
169
+ An observation or locator read started before recovery cannot publish references
170
+ after the connection changes. Each attempt owns its binding and recordings, so
171
+ late cleanup cannot change a later attempt.
172
+ The deterministic `browser.mouse` and `browser.keyboard` fixtures remain available
173
+ without an agent observation. Test code owns its input and can read current
174
+ geometry and focus with `browser.evaluate` before acting.
175
+
176
+ Recovery preserves the page's unsaved state, storage, the attempt's secret
177
+ taint, and the agent ledger. Operations already dispatched
178
+ are never retried. Dialog and route handlers are reattached. Video and trace
179
+ recording resume, but a segment lost during the disconnect cannot be recovered.
180
+ Code using `surfaceOf(engine)` must read its accessors again after recovery;
181
+ previously retained Playwright objects belong to the disconnected transport.
182
+
183
+ Persistent recovery does not support `headers`, `basicAuth`, `userAgent`, `app.clearState`,
184
+ or session state capture and restore. `app.restart` remains available. Omit
185
+ `reconnectEndpoint` to keep ordinary isolated contexts and the existing behavior.
186
+ A `browser` provider with `scope: 'attempt'` rides the same persistent
187
+ context, with the same limits: the lease's `cdpEndpoint` provisions the
188
+ attempt's browser and its `reconnectEndpoint` (else `cdpEndpoint` again)
189
+ reconnects to it. A lease is fresh by the provider's contract, so the engine
190
+ does not compare its context with earlier attempts': a hosted service may
191
+ restore browsers from one snapshot, which gives new browsers the same
192
+ context id.
193
+
194
+ The attempt's first page is the browser's own first tab, navigated to
195
+ `about:blank`, rather than a second tab beside it; a page opened after that,
196
+ such as the one `app.restart` opens, is a new tab.
197
+
198
+ ## The active tab
199
+
200
+ `browser` is the whole browser, showing one active tab. What reads or acts on
201
+ a page acts on that tab: `goto`, `reload`, `back`, `forward`, `url`, `title`,
202
+ `waitForURL`, `locator`, `frameLocator`, `evaluate`, `waitForResponse`,
203
+ `waitForDownload`, `keyboard`, `mouse`, the `expect(browser)` matchers, and
204
+ every `screen` query. What belongs to the browser covers every tab:
205
+ `cookies`, `setCookies`, `route`, `unroute`, and `onDialog`. `setViewport`
206
+ sizes the active tab and every tab opened after it.
207
+
208
+ A tab the app opens itself, from a `target="_blank"` link or `window.open`,
209
+ is not followed: the active tab stays the one the attempt opened, so assert
210
+ on what that tab shows, or open the link's URL with `browser.goto`.
211
+
212
+ ## The browser fixture
213
+
214
+ ```ts
215
+ /**
216
+ * The browser the attempt drives, showing one active tab. Navigation, reads,
217
+ * `locator`, `frameLocator`, `evaluate`, `waitForResponse`, `waitForDownload`,
218
+ * the keyboard and mouse, `expect(browser)`, and `screen` act on the active
219
+ * tab; cookies, routes, and dialog handlers cover the whole browser, every
220
+ * tab included. A tab the app opens itself (a `target="_blank"` link,
221
+ * `window.open`) is not followed: the active tab stays the one the attempt
222
+ * opened.
223
+ */
224
+ export interface Browser extends Expectable<BrowserExpectation> {
225
+ /** Navigates to an allowed URL. */
226
+ goto(
227
+ url: string,
228
+ options?: {
229
+ waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
230
+ timeout?: number;
231
+ },
232
+ ): Promise<void>;
233
+ /** Reloads the current document. */
234
+ reload(options?: ActionOptions): Promise<void>;
235
+ /** Navigates browser history back once. */
236
+ back(options?: ActionOptions): Promise<void>;
237
+ /** Navigates browser history forward once. */
238
+ forward(options?: ActionOptions): Promise<void>;
239
+ /** Returns the current URL. */
240
+ url(): Promise<string>;
241
+ /** Returns the current title. */
242
+ title(): Promise<string>;
243
+ /** Waits for the current URL to match. */
244
+ waitForURL(url: string | RegExp, options?: { timeout?: number }): Promise<void>;
245
+ /** Creates a web-only CSS or XPath locator. */
246
+ locator(selector: string): Locator;
247
+ /** Creates a screen query scope inside one iframe. */
248
+ frameLocator(selector: string): FrameScreen;
249
+ /** Evaluates trusted test code in the page. */
250
+ evaluate<T extends JsonValue>(fn: string | (() => T | Promise<T>)): Promise<T>;
251
+ /** Evaluates trusted test code with one required JSON-safe argument. */
252
+ evaluate<T extends JsonValue, Arg extends JsonValue>(
253
+ fn: string | ((arg: Arg) => T | Promise<T>),
254
+ arg: Arg,
255
+ ): Promise<T>;
256
+ /** Adds an attempt-scoped network route. */
257
+ route(
258
+ pattern: string | RegExp,
259
+ handler: (route: WebRoute) => void | Promise<void>,
260
+ ): Promise<void>;
261
+ /** Removes matching attempt-scoped routes. */
262
+ unroute(pattern: string | RegExp): Promise<void>;
263
+ /** Waits for a matching response. */
264
+ waitForResponse(
265
+ pattern: string | RegExp,
266
+ options?: { timeout?: number },
267
+ ): Promise<WebResponse>;
268
+ /** Returns cookies visible to the current context. */
269
+ cookies(): Promise<Cookie[]>;
270
+ /** Sets cookies after URL validation. */
271
+ setCookies(cookies: readonly Cookie[]): Promise<void>;
272
+ /** Sets the viewport size. */
273
+ setViewport(size: { width: number; height: number }): Promise<void>;
274
+ /** Registers an attempt-scoped dialog handler and returns an unsubscribe function. */
275
+ onDialog(handler: DialogHandler): Promise<() => Promise<void>>;
276
+ /** Runs a trigger and waits for its download. */
277
+ waitForDownload(
278
+ trigger: () => Promise<void>,
279
+ options?: { timeout?: number },
280
+ ): Promise<{ path: string; suggestedFilename: string }>;
281
+ /** Viewport-level keyboard, for whatever has focus. */
282
+ readonly keyboard: {
283
+ /** Sends one key. */
284
+ press(key: string): Promise<void>;
285
+ /** Types plain text. */
286
+ type(text: string): Promise<void>;
287
+ };
288
+ /**
289
+ * Direct pointer input from test code, independent of agent observations.
290
+ * After CDP recovery, test code can read current geometry with `evaluate`
291
+ * before starting a pointer sequence.
292
+ */
293
+ readonly mouse: {
294
+ /** Moves the pointer. */
295
+ move(x: number, y: number): Promise<void>;
296
+ /** Scrolls the pointer wheel. */
297
+ wheel(deltaX: number, deltaY: number): Promise<void>;
298
+ /** Presses the primary pointer button. */
299
+ down(): Promise<void>;
300
+ /** Releases the primary pointer button. */
301
+ up(): Promise<void>;
302
+ };
303
+ }
304
+ ```
305
+
306
+ | Member | Default timeout |
307
+ | --- | --- |
308
+ | `goto`, `reload`, `back`, `forward` | `config.timeout`, 120000 ms, the same budget `app.open()` uses; `{ timeout }` overrides, capped by the test deadline |
309
+ | `waitForResponse`, `waitForDownload` | `config.actionTimeout`, 30000 ms; `{ timeout }` overrides, capped by the test deadline |
310
+ | `waitForURL` | `config.assertionTimeout`, 5000 ms |
311
+ | `url`, `title`, `evaluate`, `route`, `unroute`, `cookies`, `setCookies`, `setViewport`, `onDialog`, `keyboard.*`, `mouse.*` | `config.actionTimeout`, 30000 ms, not overridable |
312
+
313
+ Every `browser` call is a recorded `browser.<method>` step, except `locator` and
314
+ `frameLocator`: they build a locator without touching the page, and the
315
+ action or assertion on it is the step. A relative URL resolves against the
316
+ target's `app.url`, else `APP_URL_REQUIRED`. Declare `requires: ['browser']`
317
+ in a suite that also runs on other platforms.
318
+
319
+ ### Navigation
320
+
321
+ `goto` defaults `waitUntil` to `'load'` and throws `POLICY_DENIED` for a
322
+ `file:`, `data:`, or `javascript:` URL. `waitForURL` polls every 100 ms; a
323
+ relative string resolves against the base URL and matches exactly after
324
+ normalization, a `RegExp` tests the full URL, anything else (a predicate
325
+ function, say) is `INVALID_ARGUMENT`, and a timeout is `ASSERTION_FAILED`. The other navigations throw `ACTION_FAILED`.
326
+
327
+ ```ts
328
+ await browser.goto('/dashboard', { waitUntil: 'networkidle' });
329
+ await browser.waitForURL(/\/orders\/\d+$/, { timeout: 15_000 });
330
+ ```
331
+
332
+ ### Locators and frames
333
+
334
+ `locator` and `frameLocator`, at the page or inside a frame, throw
335
+ `INVALID_LOCATOR` for an empty selector. `frameLocator` throws
336
+ `LOCATOR_NOT_FOUND` for a missing frame and `LOCATOR_AMBIGUOUS` for several.
337
+ The selector is a Playwright selector: CSS by default, XPath with `xpath=` or
338
+ a leading `//`, and `>>` chains, so
339
+ `browser.locator('[data-testid="row"] >> nth=2 >> [data-testid="conditions"] > div')`
340
+ scopes CSS to one row.
341
+
342
+ ```ts
343
+ const payment = browser.frameLocator('#payment-frame');
344
+ await payment.getByLabel('Card number').fill('4242424242424242');
345
+ ```
346
+
347
+ `frameLocator` returns a `FrameScreen`: a `Screen` whose queries answer from
348
+ inside the frame, plus the two web-only escape hatches scoped to it.
349
+
350
+ ```ts
351
+ export interface FrameScreen extends Screen {
352
+ /** Creates a web-only CSS or XPath locator inside this frame. */
353
+ locator(selector: string): Locator;
354
+ /** Creates a screen query scope inside an iframe nested in this frame. */
355
+ frameLocator(selector: string): FrameScreen;
356
+ }
357
+ ```
358
+
359
+ Reach for the frame's `locator` only when the control has no accessible
360
+ name, label, placeholder, or test id; the [browser guide](/browser#reach-into-an-iframe)
361
+ shows when that applies.
362
+
363
+ Every `screen` query searches its scope and each closed shadow root the page
364
+ attached under it, with Playwright's own role, name, text, label, and
365
+ attribute matching and each node found once, so a control the observed tree
366
+ lists inside a closed root resolves and takes actions and reads. `locator`,
367
+ `frameLocator`, and `filter({ hasText })` keep Playwright's reach and stop at
368
+ a closed root; the [browser guide](/browser#shadow-dom) has the details.
369
+
370
+ ### evaluate
371
+
372
+ The function runs in the page and cannot access variables from the test.
373
+ It is serialized by its source and inlined into the page, so it must not
374
+ close over anything outside its own body; helpers declared inside the body
375
+ work. Pass outside values through `arg`. Both `arg` and the return value
376
+ must be `JsonValue`, otherwise the call fails with `INVALID_ARGUMENT`.
377
+
378
+ An exception in the evaluated code is `EVALUATE_FAILED` with its message. A
379
+ Playwright timeout is `ACTION_FAILED`; a closed page or lost context is
380
+ `ENGINE_FAILURE`.
381
+
382
+ ```ts
383
+ const count = await browser.evaluate(() => document.querySelectorAll('li').length);
384
+ const flag = await browser.evaluate(
385
+ (name: string) => window.localStorage.getItem(name),
386
+ 'featureFlags',
387
+ );
388
+ ```
389
+
390
+ ### Routing
391
+
392
+ `route`, `unroute`, and `waitForResponse` take a string pattern (`*` within a
393
+ path segment, `**` across `/`) or a `RegExp`, both tested against the full
394
+ URL; a predicate function is `INVALID_ARGUMENT`. A route survives
395
+ `app.restart()`, `app.clearState()`, and session restore.
396
+ The handler must decide exactly once with `fulfill`, `continue`, or `abort`;
397
+ deciding twice or not at all is `ACTION_FAILED`, as is a `waitForResponse`
398
+ that nothing matches inside the deadline. The response resolves once its body
399
+ is known: `url`, `status`, and `headers` are always readable, and a body the
400
+ browser could not read (a connection reset short of the declared
401
+ `Content-Length`, a request the page aborted, or a redirect, whose body the
402
+ browser never keeps) rejects `text()` and `json()` with `ACTION_FAILED`
403
+ instead of an empty string. A genuinely empty body reads as `''`. A handler
404
+ that throws (a failed `expect` on the request, say) fails the next step with
405
+ that error, or the attempt at its end when no step follows; a handler still
406
+ running then gets the cleanup budget to finish. One that outlives the budget
407
+ fails the attempt with `CLEANUP_TIMEOUT`, and whatever it throws afterwards is
408
+ dropped.
409
+
410
+ ```ts
411
+ await browser.route('**/api/quote', async (route) => {
412
+ await route.fulfill({ json: { cents: 4200 } });
413
+ });
414
+
415
+ const response = await browser.waitForResponse('**/api/orders');
416
+ expect(response.status).toBe(201);
417
+ ```
418
+
419
+ ```ts
420
+ /** `json` or `body`, never both; neither fulfills with an empty body. */
421
+ export type RouteFulfillResponse = {
422
+ status?: number;
423
+ headers?: Record<string, string>;
424
+ } & (
425
+ | { json: JsonValue; body?: never }
426
+ | { body: string; json?: never }
427
+ | { body?: never; json?: never }
428
+ );
429
+
430
+ export interface WebRoute {
431
+ /** The intercepted request. */
432
+ readonly request: {
433
+ readonly url: string;
434
+ readonly method: string;
435
+ readonly headers: Readonly<Record<string, string>>;
436
+ readonly postData?: string;
437
+ };
438
+ /** Fulfills the intercepted request once. */
439
+ fulfill(response: RouteFulfillResponse): Promise<void>;
440
+ /** Continues the intercepted request once. */
441
+ continue(): Promise<void>;
442
+ /** Aborts the intercepted request once. */
443
+ abort(): Promise<void>;
444
+ }
445
+
446
+ export interface WebResponse {
447
+ /** Response URL. */
448
+ readonly url: string;
449
+ /** HTTP status. */
450
+ readonly status: number;
451
+ /** Response headers, lower-cased names. */
452
+ readonly headers: Readonly<Record<string, string>>;
453
+ /** Parses the response body as JSON. */
454
+ json<T = unknown>(): Promise<T>;
455
+ /** Reads the response body as text. */
456
+ text(): Promise<string>;
457
+ }
458
+ ```
459
+
460
+ ### Cookies and viewport
461
+
462
+ `setCookies` validates every cookie before setting any; an unparseable
463
+ target or a forbidden scheme is `POLICY_DENIED` and nothing is set. For the
464
+ `domain` form the scheme follows the base URL and a leading `.` is stripped.
465
+ `setViewport` records the new size on the report. It works before the first
466
+ navigation too, as Playwright's `setViewportSize` does: the next `app.open`
467
+ loads at that size, so a phone-sized test never renders the desktop layout first.
468
+ The size holds for the rest of the attempt, through `app.restart()` and
469
+ `app.clearState()`.
470
+
471
+ ```ts
472
+ export interface CookieFields {
473
+ /** Cookie name. */
474
+ name: string;
475
+ /** Cookie value. */
476
+ value: string;
477
+ /** Unix timestamp in whole seconds. */
478
+ expires?: number;
479
+ /** HttpOnly flag. */
480
+ httpOnly?: boolean;
481
+ /** Secure flag. */
482
+ secure?: boolean;
483
+ /** SameSite attribute. */
484
+ sameSite?: 'Strict' | 'Lax' | 'None';
485
+ }
486
+
487
+ /** `url`, or `domain` with an optional `path`, never both. */
488
+ export type Cookie = CookieFields &
489
+ (
490
+ | { url: string; domain?: never; path?: never }
491
+ | { url?: never; domain: string; path?: string }
492
+ );
493
+ ```
494
+
495
+ ```ts
496
+ await browser.setCookies([{ url: 'http://localhost:3000', name: 'tz', value: 'UTC' }]);
497
+ ```
498
+
499
+ ### Dialogs and downloads
500
+
501
+ A function handler must call `accept` or `dismiss`; one that does neither
502
+ has the dialog dismissed and fails the next step with `APP_NOT_OPEN`, as an
503
+ unhandled dialog does. One that throws fails the next step, or the attempt at
504
+ its end when no step follows, with its error: a failed `expect` stays
505
+ `ASSERTION_FAILED`, an unclassified throw is `ENGINE_FAILURE`.
506
+ `waitForDownload` registers the file as a `download`
507
+ artifact and throws `ACTION_FAILED` when it does not complete in time. A
508
+ trigger that resolves in time but starts no download fails with
509
+ `no download started within <n>ms; the trigger resolved after <m>ms`, where
510
+ the wait includes the trigger's own time. A trigger still running at the
511
+ timeout fails with the step timeout, and one that throws with its own error.
512
+ On a hosted browser the file comes back through the provider's
513
+ [`downloads`](/browser#downloads-on-the-provider).
514
+
515
+ ```ts
516
+ /** A native dialog as a test's handler sees it. */
517
+ export interface Dialog {
518
+ /** The dialog's text. */
519
+ readonly message: string;
520
+ /** Accepts the dialog once. */
521
+ accept(text?: string): Promise<void>;
522
+ /** Dismisses the dialog once. */
523
+ dismiss(): Promise<void>;
524
+ }
525
+ ```
526
+
527
+ ```ts
528
+ const off = await browser.onDialog('accept');
529
+ await screen.getByRole('button', 'Delete').tap();
530
+ await off();
531
+
532
+ const file = await browser.waitForDownload(() =>
533
+ screen.getByRole('link', 'Export CSV').tap(),
534
+ );
535
+ ```
536
+
537
+ ### keyboard and mouse
538
+
539
+ Both namespaces are viewport-level. Prefer `locator.press` and
540
+ `locator.fill` when a target exists. `keyboard.type` takes plain text only,
541
+ never a `Secret`.
542
+
543
+ ### JsonValue
544
+
545
+ ```ts
546
+ /** A JSON scalar. */
547
+ export type JsonPrimitive = string | number | boolean | null;
548
+
549
+ /** Data that survives JSON serialization. */
550
+ export type JsonValue =
551
+ | JsonPrimitive
552
+ | { readonly [key: string]: JsonValue }
553
+ | readonly JsonValue[];
554
+ ```
555
+
556
+ ## Semantic capture recovery
557
+
558
+ When the semantic reader times out, agent observations can recover using a
559
+ fresh viewport screenshot. Playwright runs a new secure-field mask sweep
560
+ across every reachable frame, including open and closed shadow roots. Every
561
+ mask probe must succeed. The observation explicitly reports unavailable
562
+ semantics and clears earlier node references.
563
+
564
+ Recovery requires time left in the operation budget and permission from the
565
+ runner. `act` permits it before a secret fill; judgments also require
566
+ `vision: true` or `'only'`. Failed masking, cancellation, and hard engine
567
+ errors still stop the observation.
568
+
569
+ When fallback is permitted, the reader reserves one quarter of the remaining
570
+ observation budget, capped at two seconds, for the screenshot. The deadline
571
+ covers the page walk, metadata and element readback, and nested frames.
572
+ Semantic metadata crosses the Playwright connection as one JSON string to
573
+ reduce serialization overhead. Actions still use the live element handles
574
+ captured with that metadata. Pages that replace `JSON.stringify` or add
575
+ `toJSON` methods to object or array prototypes use structured transfer.
576
+ Cleanup does not wait for stalled protocol replies. An abandoned reader
577
+ cannot reuse node IDs or publish handles over a newer observation.
578
+
579
+ ## Exported types
580
+
581
+ `WebOptions`, `WebScreencastOptions`, `WebConnectOptions`, `WebBasicAuth`, `BrowserName`,
582
+ `BrowserProvider`, `BrowserProviderScope`, `BrowserRequest`, `BrowserLease`,
583
+ `BrowserReleaseContext`, `BrowserProviderDownloads`, `BrowserDownloadContext`,
584
+ `PlaywrightLiveSurface`, `Browser`, `FrameScreen`,
585
+ `BrowserExpectation`, `WebRoute`, `RouteFulfillResponse`, `WebResponse`,
586
+ `Cookie`, `CookieFields`, `Dialog`, and `DialogHandler` are exported from the
587
+ package for typing config helpers and fixtures. The runtime exports are
588
+ `web`, [`surfaceOf`](#surfaceof), and `test`, `describe`, `beforeEach`,
589
+ `afterEach`, `beforeAll`, and `afterAll`, typed with the `browser` fixture.
590
+
591
+ ## expect(browser)
592
+
593
+ `expect(browser)` returns the `BrowserExpectation` the engine attaches: `toHaveURL`,
594
+ `toHaveTitle`, and `toHaveClass`. See [browser matchers](/reference/expect#browser-matchers).
595
+
596
+ ## surfaceOf
597
+
598
+ ```ts
599
+ import { web, surfaceOf } from '@e2e-dev/web';
600
+
601
+ const engine = web();
602
+ const live = surfaceOf(engine); // PlaywrightLiveSurface | undefined
603
+ live!.page(); // the Page of the current attempt
604
+ live!.context(); // its BrowserContext
605
+ ```
606
+
607
+ Use `surfaceOf(handle)` in a [custom executor](/executors#driving-the-page-yourself)
608
+ to access the current Playwright page and context directly. It returns
609
+ `undefined` for a handle from another package. The accessors throw
610
+ `INVALID_STATE` outside an active attempt. Direct Playwright calls are not
611
+ recorded as steps.