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,440 @@
1
+ ---
2
+ title: Working with the browser
3
+ description: Mock an API, handle a dialog, download a file, reach into an iframe, and pick a browser.
4
+ ---
5
+
6
+ `@e2e-dev/web` runs tests in Chromium, Firefox, and WebKit. It is built on
7
+ [Playwright](https://github.com/microsoft/playwright).
8
+
9
+ Use `screen`, `app`, and `expect` for navigation, interactions, and checks.
10
+ For browser-specific operations such as request mocking, dialogs, and
11
+ downloads, use `browser`. Import `test` from `@e2e-dev/web` to access it:
12
+
13
+ ```ts
14
+ import { test } from '@e2e-dev/web';
15
+ import { expect } from 'e2e';
16
+
17
+ test('the dashboard loads', async ({ app, screen, browser }) => {
18
+ await app.open('/dashboard');
19
+ await expect(browser).toHaveURL('/dashboard');
20
+ await expect(browser).toHaveTitle(/Dashboard/);
21
+ await expect(screen.getByRole('heading', 'Overview')).toBeVisible();
22
+ });
23
+ ```
24
+
25
+ In a suite with device targets, add `{ requires: ['browser'] }` to tests that
26
+ use `browser` so the runner skips them on devices.
27
+
28
+ ## Mock an API
29
+
30
+ `browser.route` intercepts requests for the attempt. Register it before the page
31
+ opens; it stays in force across `app.restart()` and a session restore.
32
+
33
+ ```ts
34
+ await browser.route('**/api/quote', async (route) => {
35
+ await route.fulfill({ json: { cents: 4200 } });
36
+ });
37
+
38
+ await app.open('/checkout');
39
+ await expect(screen.getByText('$42.00')).toBeVisible();
40
+ ```
41
+
42
+ Each handler must call exactly one of `fulfill`, `continue`, or `abort`. An
43
+ `expect` that fails inside the handler fails the test, at the next step or at
44
+ the end of the test when nothing follows. To inspect a real response, start
45
+ waiting before the action that sends it:
46
+
47
+ ```ts
48
+ const [response] = await Promise.all([
49
+ browser.waitForResponse('**/api/orders'),
50
+ screen.getByRole('button', 'Place order').tap(),
51
+ ]);
52
+ expect(response.status).toBe(201);
53
+ ```
54
+
55
+ ## Handle a dialog
56
+
57
+ An unhandled `alert`, `confirm`, or `prompt` fails the next step. Register a
58
+ handler first:
59
+
60
+ ```ts
61
+ const off = await browser.onDialog('accept');
62
+ await screen.getByRole('button', 'Delete').tap();
63
+ await off();
64
+ ```
65
+
66
+ A function handler gets the dialog and must call `accept(text?)` or
67
+ `dismiss()`.
68
+
69
+ ## Download a file
70
+
71
+ ```ts
72
+ const file = await browser.waitForDownload(() =>
73
+ screen.getByRole('link', 'Export CSV').tap(),
74
+ );
75
+ ```
76
+
77
+ The download is registered as a `download` artifact. `file.path` is relative to
78
+ the attempt's artifact directory, not an absolute or project-relative file
79
+ path.
80
+
81
+ ## Reach into an iframe
82
+
83
+ `browser.frameLocator` scopes queries to a frame:
84
+
85
+ ```ts
86
+ const payment = browser.frameLocator('#payment-frame');
87
+ await payment.getByLabel('Card number').fill('4242424242424242');
88
+ ```
89
+
90
+ Use role and label queries when possible. Within a frame, `locator` accepts
91
+ CSS or XPath selectors and `frameLocator` enters a nested frame:
92
+
93
+ ```ts
94
+ const editor = browser.frameLocator('#result');
95
+ await editor.locator('#upload').setInputFiles('fixtures/photo.png');
96
+ await expect(editor.frameLocator('#preview').getByRole('heading', 'Uploaded')).toBeVisible();
97
+ ```
98
+
99
+ `browser.locator('css or xpath')` does the same on the page itself. Use it only
100
+ when role, label, and test-id queries cannot identify an element. If a hidden
101
+ duplicate causes an ambiguous match, try `{ visible: true }` on the query
102
+ first.
103
+
104
+ ## Read from the page
105
+
106
+ `browser.evaluate` runs a function in the page. It is serialized, so it cannot
107
+ close over test variables; pass data as the second argument.
108
+
109
+ ```ts
110
+ const count = await browser.evaluate(() => document.querySelectorAll('li').length);
111
+ const flag = await browser.evaluate((name: string) => window.localStorage.getItem(name), 'featureFlags');
112
+ ```
113
+
114
+ Use `evaluate` for state that `screen` cannot read. Prefer locators and
115
+ assertions for visible UI so tests depend less on the DOM structure.
116
+
117
+ ## Cookies
118
+
119
+ ```ts
120
+ await browser.setCookies([{ url: 'http://localhost:3000', name: 'tz', value: 'UTC' }]);
121
+ ```
122
+
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
125
+ it would be sent to. If any entry is refused, no cookies are set. To resize
126
+ the page, see [Testing viewports](/viewports).
127
+
128
+ ## Pick a browser
129
+
130
+ The browser and the initial viewport are options of the engine; the app is
131
+ the target's. Two browsers on one app are two targets:
132
+
133
+ ```ts title="e2e.config.ts"
134
+ import type { E2EConfig } from 'e2e';
135
+ import { web } from '@e2e-dev/web';
136
+
137
+ const app = { url: 'http://localhost:3000' };
138
+
139
+ export default {
140
+ targets: [
141
+ { name: 'chromium', engine: web(), app },
142
+ { name: 'webkit-mobile', engine: web({ browser: 'webkit', viewport: { width: 390, height: 844 } }), app },
143
+ ],
144
+ } satisfies E2EConfig;
145
+ ```
146
+
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
149
+ one; a [browser provider](#hosted-browsers) does the same and also leases
150
+ and releases the browser. [Testing viewports](/viewports) covers sizes.
151
+
152
+ ## Hosted browsers
153
+
154
+ `browser` can also be a `BrowserProvider`: an object that leases a browser
155
+ from a service and hands back the CDP endpoint to attach to. The engine
156
+ attaches over CDP the way `connect` does, and adds the lifecycle `connect`
157
+ lacks: it asks for a browser when one is needed and gives it back when its
158
+ scope ends, on every exit path, so a session billed by the minute stops
159
+ when the run no longer uses it. The engine knows no vendor: for
160
+ [Kernel](/integrations/kernel), `kernel()` from `@e2e-dev/kernel` is the
161
+ provider, and for any other service a provider is a few dozen lines in your
162
+ project. Add `viewport: null` when
163
+ the service shows the window in a live view: the page then
164
+ [fills the window](/viewports#fill-the-window) instead of a fixed 1280 by
165
+ 720 area of it.
166
+
167
+ ```ts
168
+ export interface BrowserProvider {
169
+ /** Label in progress lines and error messages. */
170
+ readonly name: string;
171
+ /** `worker` (default): one browser per worker slot for the run. `attempt`: a fresh browser per attempt. */
172
+ readonly scope?: 'worker' | 'attempt';
173
+ acquire(request: BrowserRequest): Promise<BrowserLease>;
174
+ release(lease: BrowserLease, context: BrowserReleaseContext): Promise<void>;
175
+ /** Optional: the service's own recording of the browser, in the worker, for an attempt that records video. */
176
+ record?(lease: BrowserLease, context: ProviderRecordContext): Promise<ProviderRecording>;
177
+ /** Optional: serves `browser.waitForDownload` for a browser on another machine. */
178
+ readonly downloads?: BrowserProviderDownloads;
179
+ /** Optional: releases the run's leases a dead worker left open, once the run ends; resolves to their ids. */
180
+ sweep?(context: BrowserReleaseContext): Promise<readonly string[]>;
181
+ }
182
+
183
+ export interface BrowserProviderDownloads {
184
+ /** Absolute directory on the browser's machine the browser saves downloads to. */
185
+ readonly dir: string;
186
+ /** The bytes of one finished download, `file` a path under `dir`, read off the leased browser. */
187
+ read(lease: BrowserLease, file: string, context: BrowserDownloadContext): Promise<Uint8Array>;
188
+ }
189
+
190
+ export interface BrowserLease {
191
+ /** The provider's handle (a session id), handed back to `release`. */
192
+ readonly id: string;
193
+ /** `ws://`, `wss://`, or `http://` DevTools endpoint to attach to over CDP. */
194
+ readonly cdpEndpoint: string;
195
+ /** Endpoint to reattach to the same browser after a transport drop; per-attempt leases only. */
196
+ readonly reconnectEndpoint?: string;
197
+ }
198
+ ```
199
+
200
+ `acquire` receives the run id, target, worker slot and slot count, the
201
+ attempt id for a per-attempt lease, the run's environment, an abort signal,
202
+ and a `log` that reaches the reporter. Read tokens from `request.env`, not
203
+ `process.env`. A lease is JSON data: only `id`, `cdpEndpoint`, and
204
+ `reconnectEndpoint` travel to the worker, and `release` gets back the object
205
+ `acquire` returned, so a provider can keep its own fields on it. A target
206
+ with nothing to run leases nothing. A provider implies chromium (CDP attach
207
+ is chromium-only) and cannot be combined with `connect`.
208
+
209
+ `scope: 'worker'`, the default, leases one browser per worker slot when the
210
+ run starts, all slots in parallel, and each worker attaches to its own for
211
+ every attempt it runs. Every lease granted is held by the engine: a slot
212
+ that fails to lease ends the run before any test, and the leases that
213
+ succeeded are still released. Release happens after the last worker exits,
214
+ on every exit path including failure, interrupt, and a `prepare` that
215
+ threw, within `cleanupTimeout`. When a worker's browser drops, the worker
216
+ leases a replacement itself at the next attempt start and releases it when
217
+ the worker exits; the dead lease stays with the runner, which releases it
218
+ at the end. `acquire` therefore runs in a worker process too, so a provider
219
+ must not depend on state it built in the runner process; its `log` still
220
+ reaches the reporter, prefixed with the target and worker slot. Each lease
221
+ is released exactly once, by the side that acquired it.
222
+
223
+ A worker that dies (a crash, `process.exit`, a kill) cannot release what it
224
+ acquired: an attempt's browser, or a replacement. Implement `sweep` to have
225
+ the runner release those too. It runs once per target when the run ends,
226
+ after the last worker exited and the leases the runner holds were released,
227
+ so every lease it finds with the run's id and the target at the service is
228
+ one a worker left open; it releases them and resolves to their ids, which
229
+ the reporter lists. A sweep that fails is reported as a line and fails
230
+ nothing. Without `sweep`, such a browser lives until the service ends it.
231
+
232
+ `scope: 'attempt'` leases a fresh browser in every `startAttempt`, retries
233
+ included, and releases it in `endAttempt`, also after a start that failed.
234
+ A lease that arrives once the attempt was cancelled or ended is given
235
+ straight back, never used.
236
+ The attempt runs in the browser's persistent default context, on the
237
+ browser's own first tab, so the remote must contain only that context, and
238
+ the limits of
239
+ [`connect.reconnectEndpoint`](/reference/web#recovering-a-cdp-transport)
240
+ apply: no `headers`, `basicAuth`, or `userAgent` (each is `INVALID_CONFIG`), no
241
+ `app.clearState()` (`UNSUPPORTED_CAPABILITY` naming the scope), and no session
242
+ state capture or restore, so a run whose tests consume a
243
+ [session](/authentication) on the target fails with `COLLECTION_ERROR`
244
+ before its setup test logs in. After a transport
245
+ drop the next operation reattaches through the lease's `reconnectEndpoint`,
246
+ or `cdpEndpoint` when the lease has none, and the engine checks it is the
247
+ same browser.
248
+
249
+ A provider against a typical session API has three requests: create a
250
+ session, poll it until it exposes a DevTools endpoint, stop it.
251
+
252
+ ```ts title="browser-provider.ts"
253
+ import type { BrowserProvider } from '@e2e-dev/web';
254
+
255
+ /**
256
+ * Leases browsers from a service that starts a Chromium per session and
257
+ * exposes its DevTools endpoint. Replace the three requests with your
258
+ * service's API.
259
+ */
260
+ export function hostedBrowsers(options: { serviceUrl: string; scope?: 'worker' | 'attempt' }): BrowserProvider {
261
+ return {
262
+ name: 'hosted-browsers',
263
+ scope: options.scope ?? 'worker',
264
+ async acquire(request) {
265
+ const token = request.env['BROWSER_SERVICE_TOKEN'];
266
+ if (token === undefined || token === '') throw new Error('BROWSER_SERVICE_TOKEN is not set');
267
+ const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
268
+ // 1. Ask for a browser; name the session after the run so it can be found later.
269
+ const created = await fetch(`${options.serviceUrl}/sessions`, {
270
+ method: 'POST',
271
+ headers,
272
+ body: JSON.stringify({
273
+ name: `e2e ${request.runId} ${request.targetName} ${request.attemptId ?? `slot ${request.slot + 1} of ${request.slots}`}`,
274
+ }),
275
+ signal: request.signal,
276
+ });
277
+ if (!created.ok) throw new Error(`browser service: HTTP ${created.status}`);
278
+ const { id } = (await created.json()) as { id: string };
279
+ // 2. Wait until the session exposes its DevTools endpoint.
280
+ for (;;) {
281
+ const response = await fetch(`${options.serviceUrl}/sessions/${id}`, { headers, signal: request.signal });
282
+ const session = (await response.json()) as { status: string; cdpUrl?: string; viewerUrl?: string };
283
+ if (session.status === 'ready' && session.cdpUrl !== undefined) {
284
+ if (session.viewerUrl !== undefined) request.log(`watch at ${session.viewerUrl}`);
285
+ return { id, cdpEndpoint: session.cdpUrl };
286
+ }
287
+ if (session.status !== 'starting') throw new Error(`browser service: session ${id} is ${session.status}`);
288
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
289
+ }
290
+ },
291
+ async release(lease, context) {
292
+ // 3. Stop the session; the run waits for this within cleanupTimeout.
293
+ const token = context.env['BROWSER_SERVICE_TOKEN'] ?? '';
294
+ await fetch(`${options.serviceUrl}/sessions/${lease.id}/stop`, {
295
+ method: 'POST',
296
+ headers: { Authorization: `Bearer ${token}` },
297
+ signal: context.signal,
298
+ });
299
+ },
300
+ };
301
+ }
302
+ ```
303
+
304
+ ```ts title="e2e.config.ts"
305
+ import type { E2EConfig } from 'e2e';
306
+ import { web } from '@e2e-dev/web';
307
+ import { hostedBrowsers } from './browser-provider.ts';
308
+
309
+ export default {
310
+ targets: [
311
+ {
312
+ name: 'hosted',
313
+ engine: web({
314
+ browser: hostedBrowsers({ serviceUrl: 'https://browsers.example.com' }),
315
+ }),
316
+ app: { url: 'https://staging.example.com' },
317
+ },
318
+ ],
319
+ workers: 2,
320
+ } satisfies E2EConfig;
321
+ ```
322
+
323
+ Two workers lease two sessions in parallel and keep them for the run. Hosted
324
+ sessions are billed while they run, so keep `workers` at what the suite can
325
+ fill; `scope: 'attempt'` trades a session start per test for a browser no
326
+ earlier test has touched.
327
+
328
+ ### Downloads on the provider
329
+
330
+ A hosted browser saves a download to its own disk, where the runner cannot
331
+ open it. Implement `downloads` and `browser.waitForDownload` works as it does
332
+ locally: the engine has the browser save the file under `dir` on its machine
333
+ and hands the finished file's path to `read`, which fetches the bytes through
334
+ the service's file API, in the worker, bounded by the call's timeout. The
335
+ engine saves them as the attempt's `download` artifact. Without `downloads`, a
336
+ download on a browser that runs on another machine fails with `ENGINE_FAILURE`
337
+ naming the provider; one whose browsers run on the runner's machine needs
338
+ nothing. [Kernel](/integrations/kernel#downloads) serves downloads this way.
339
+
340
+ ### Recording on the provider
341
+
342
+ A service that records its browsers can record the attempt's video too.
343
+ Implement `record` and an attempt that records [video](/reference/config#video)
344
+ calls it from the worker, with the lease that attempt rides, instead of
345
+ starting the engine's screencast of the page. It starts once the attempt's
346
+ page is open and covers the whole browser window, tabs and address bar
347
+ included. `record` returns a `ProviderRecording` (import the type from
348
+ `e2e/engine`), stopped when the attempt collects its video: `stop` either
349
+ writes a file into the directory it is given and returns `{ file }`, kept as
350
+ `video/<file>` beside the attempt's other artifacts, or returns
351
+ `{ url, mediaType }` for a recording the service keeps, which the report
352
+ links to.
353
+
354
+ ```ts title="browser-provider.ts"
355
+ import type { ProviderRecording } from 'e2e/engine';
356
+
357
+ // In hostedBrowsers(), beside acquire and release.
358
+ async record(lease, context): Promise<ProviderRecording> {
359
+ const headers = { Authorization: `Bearer ${context.env['BROWSER_SERVICE_TOKEN'] ?? ''}` };
360
+ const startedAt = new Date().toISOString();
361
+ const started = await fetch(`${options.serviceUrl}/sessions/${lease.id}/recordings`, {
362
+ method: 'POST',
363
+ headers,
364
+ signal: context.signal,
365
+ });
366
+ if (!started.ok) throw new Error(`browser service: HTTP ${started.status}`);
367
+ const { id } = (await started.json()) as { id: string };
368
+ return {
369
+ startedAt,
370
+ async stop({ signal }) {
371
+ const stopped = await fetch(`${options.serviceUrl}/recordings/${id}/stop`, { method: 'POST', headers, signal });
372
+ if (!stopped.ok) throw new Error(`browser service: HTTP ${stopped.status}`);
373
+ // The service keeps the recording; the report links to its player page.
374
+ return { url: `${options.serviceUrl}/recordings/${id}`, mediaType: 'text/html' };
375
+ },
376
+ };
377
+ },
378
+ ```
379
+
380
+ In `worker` scope the lease `record` gets is usually the copy that traveled
381
+ to the worker, which carries only `id`, `cdpEndpoint`, and
382
+ `reconnectEndpoint`, so key the recording on `lease.id`.
383
+
384
+ A `record` that fails fails the attempt's launch, named after the provider
385
+ and the lease; a `stop` that fails is tried once more when the attempt ends,
386
+ so a recording never keeps running on a browser later attempts share. A
387
+ provider that cannot record, or a browser the service cannot record (a
388
+ headless one), leaves `record` out, and the engine records the screencast.
389
+ [Kernel](/integrations/kernel#recordings) records this way.
390
+
391
+ ## Icon fonts and generated content
392
+
393
+ Text a stylesheet adds with `::before` or `::after` is part of a control's
394
+ name, as browsers and screen readers read it: a button whose icon is
395
+ `.next::before { content: "→" }` is `button "→ Next"` in the tree and to
396
+ `getByRole`. Icon fonts are the exception. Font Awesome and its kin draw
397
+ each icon from a private-use character (`\f090`) that means nothing as text,
398
+ so the engine drops those characters from every name: the-internet's
399
+ `<button><i class="fa fa-sign-in"> Login</i></button>` is `button "Login"`,
400
+ and `getByRole('button', 'Login')` finds it; a glyph between two
401
+ words reads as a space. A control whose only content is such an icon reads
402
+ as its `title` when it has one, but the browser names it by the glyph, so
403
+ `getByRole` by that title finds nothing: give it an `aria-label`, or reach
404
+ it with `browser.locator('[title="Sign out"]')`. A `RegExp` name is matched against the
405
+ name as the browser computes it, glyph included, so leave it unanchored
406
+ (`/Login/`) on such a control. Label text never includes generated content,
407
+ as `getByLabel` reads it.
408
+
409
+ ## Inert content
410
+
411
+ The agent does not see an element with the `inert` attribute or anything
412
+ under it, so it never aims at a control behind an open drawer. Locators
413
+ still find inert content and `toBeVisible` still passes, as in Playwright.
414
+
415
+ ## Shadow DOM
416
+
417
+ `screen` queries, scopes, and `has` filters reach into open and closed shadow
418
+ roots, and the agent sees their content. Actions and assertions work there
419
+ as anywhere else. `first`, `last`, and `nth` count matches outside a closed
420
+ root first, then each root's matches in order.
421
+
422
+ ```ts
423
+ // The input and the button live in a closed shadow root a widget attached.
424
+ await screen.getByPlaceholder('Access code').fill('SHADOW-42');
425
+ await screen.getByRole('button', 'Submit').tap();
426
+ await expect(screen.getByText('Access granted')).toBeVisible();
427
+ ```
428
+
429
+ `browser.locator`, `frameLocator`, and `filter({ hasText })` stop at a closed
430
+ root: query the text inside the root instead, or filter with `has`. A closed
431
+ root declared in HTML (`<template shadowrootmode="closed">`) is out of reach.
432
+
433
+ <CardGroup cols={2}>
434
+ <Card title="Web engine reference" icon="masks-theater" href="/reference/web">
435
+ Every `browser` method, its timeout, and what it throws.
436
+ </Card>
437
+ <Card title="Starting your app" icon="play" href="/starting-your-app">
438
+ Let the runner start the dev server and reach protected previews.
439
+ </Card>
440
+ </CardGroup>
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: Bug bashes
3
+ description: Run many explorations at once, then prove each bug with a failing test before anyone reads about it.
4
+ ---
5
+
6
+ A bug bash runs several `e2e explore` sessions at once, each on one area
7
+ of the app. Every finding they report is then reproduced with a test that
8
+ fails for the reported reason. You get back only bugs that reproduced, each
9
+ with a repro test and the steps to reach it, plus the explorer's screenshot
10
+ and video where the engine captures them.
11
+
12
+ A coding agent runs the whole thing from the `bug-bash` topic of the
13
+ [skill](/coding-agents):
14
+
15
+ ```text
16
+ Bug bash the checkout and account settings on this branch.
17
+ ```
18
+
19
+ The agent plans the charters, fans them out, merges the results, and
20
+ verifies each finding. Nothing here needs a service beyond the model your
21
+ config already uses.
22
+
23
+ ## How it runs
24
+
25
+ The agent reads the app's routes and, for a branch, the diff, and writes
26
+ five to ten charters: one-sentence goals, each on one area with one
27
+ posture, such as a first-time user, numbers and copy, edge input, state
28
+ across reloads, or error paths. Each posture runs as its own persona, a
29
+ named agent whose system prompt sets the stance, picked per charter with
30
+ `--agent`. It runs one `e2e explore` per charter, four
31
+ at a time, each with its own `--output` directory so reports never collide,
32
+ and merges the findings. It sets aside findings that the local
33
+ environment, the design, or the seed data explain, and the explorer's own
34
+ blind spots first: links that opened a new tab, text that only reads broken
35
+ in the accessibility tree, infinite-scroll sentinels, lazy-loaded media. Each remaining issue
36
+ gets a repro test that asserts the expected behavior, and counts as a bug
37
+ only when that test fails with `ASSERTION_FAILED` on it; verifying
38
+ subagents, one per area, each drive their own
39
+ [`e2e mcp` session](/reference/mcp#several-sessions) to check locators
40
+ against the live app. The report lists confirmed bugs first, then
41
+ production risks the triage surfaced but could not verify locally, then
42
+ rejected findings grouped by reason, then warnings.
43
+
44
+ The steps, commands, and rules the agent follows are the skill topic
45
+ itself: `npx e2e guide bug-bash`.
46
+
47
+ ## Before you start
48
+
49
+ - The app must serve several explorers at once. Start it once and set
50
+ `reuseExisting: true` on the target's `command`, or declare the URL with
51
+ port `0` so each run starts its own copy. `reuseExisting` does not apply
52
+ when `CI` is set; use port `0` there. See
53
+ [Starting your app](/starting-your-app).
54
+ - When the target's `app.command` brings up a stack of its own, such as a
55
+ database from `docker compose` that it removes on exit, start the stack once
56
+ and explore with a config that points at the running app and declares no
57
+ command, so one explorer's exit never removes the database under the
58
+ others.
59
+ - Prefer a production build. A dev server that compiles a route on its
60
+ first visit looks to an explorer like a link that does nothing.
61
+ - Give each charter its own seeded account, and tell the explorers in the
62
+ agent's `context` which integrations the local app has no keys for.
63
+ - When a setup test signs in, start charters with
64
+ [`e2e explore --session <name>`](/explore): a setup that signs in without
65
+ typing a password keeps screenshots available, where the engine captures
66
+ them.
67
+ - On mobile, each explorer needs its own device: declare one target per
68
+ simulator or emulator.
69
+ - Put test accounts under `credentials`; explorers fill passwords by name
70
+ and never see them.
71
+ - Every charter costs its model calls. The last lines of each run print
72
+ tokens, cost, and duration.
73
+ - A bash against a deployed site others use is read-only: no signups, no
74
+ sign-ins, no submissions, nothing injection-shaped in URLs. A WAF block
75
+ is the firewall working, not a finding.
76
+
77
+ ## Repro tests
78
+
79
+ A repro test fails until its bug is fixed, so keep it out of the suite that
80
+ gates merges until then. Once the bug is fixed, the test passes and stays as
81
+ its regression test.
82
+
83
+ <CardGroup cols={2}>
84
+ <Card title="Exploring without a test" icon="compass" href="/explore">
85
+ What one `e2e explore` run does, its budgets, and its report.
86
+ </Card>
87
+ <Card title="MCP server" icon="plug" href="/reference/mcp">
88
+ Live sessions for coding agents, several at once.
89
+ </Card>
90
+ </CardGroup>