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.
- package/README.md +51 -24
- package/dist/.build.json +1 -1
- package/dist/agent/act.js +3 -3
- package/dist/agent/act.js.map +1 -1
- package/dist/agent/error.d.ts +16 -3
- package/dist/agent/error.d.ts.map +1 -1
- package/dist/agent/error.js +25 -6
- package/dist/agent/error.js.map +1 -1
- package/dist/agent/model/tool-choice.d.ts +24 -1
- package/dist/agent/model/tool-choice.d.ts.map +1 -1
- package/dist/agent/model/tool-choice.js +27 -1
- package/dist/agent/model/tool-choice.js.map +1 -1
- package/dist/agent/step-cache.d.ts +14 -3
- package/dist/agent/step-cache.d.ts.map +1 -1
- package/dist/agent/step-cache.js +13 -3
- package/dist/agent/step-cache.js.map +1 -1
- package/dist/agent/tool-loop.d.ts.map +1 -1
- package/dist/agent/tool-loop.js +62 -9
- package/dist/agent/tool-loop.js.map +1 -1
- package/dist/cache/context.d.ts +19 -6
- package/dist/cache/context.d.ts.map +1 -1
- package/dist/cache/context.js +15 -21
- package/dist/cache/context.js.map +1 -1
- package/dist/cli/init/scaffold.d.ts +1 -0
- package/dist/cli/init/scaffold.d.ts.map +1 -1
- package/dist/cli/init/scaffold.js +2 -1
- package/dist/cli/init/scaffold.js.map +1 -1
- package/dist/cli/init/sibling-versions.json +1 -1
- package/dist/expect/values.js +1 -1
- package/dist/expect/values.js.map +1 -1
- package/dist/run/execute.d.ts.map +1 -1
- package/dist/run/execute.js +21 -8
- package/dist/run/execute.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/agent-steps.mdx +126 -0
- package/docs/agents.mdx +217 -0
- package/docs/api-testing.mdx +65 -0
- package/docs/authentication.mdx +345 -0
- package/docs/browser.mdx +440 -0
- package/docs/bug-bash.mdx +90 -0
- package/docs/cache.mdx +306 -0
- package/docs/ci.mdx +210 -0
- package/docs/coding-agents.mdx +183 -0
- package/docs/debugging.mdx +291 -0
- package/docs/executors.mdx +243 -0
- package/docs/explore.mdx +174 -0
- package/docs/github.mdx +184 -0
- package/docs/index.mdx +82 -0
- package/docs/integrations/eas.mdx +216 -0
- package/docs/integrations/index.mdx +24 -0
- package/docs/integrations/kernel.mdx +163 -0
- package/docs/migrate/cypress.mdx +124 -0
- package/docs/migrate/detox.mdx +140 -0
- package/docs/migrate/maestro.mdx +229 -0
- package/docs/migrate/playwright.mdx +427 -0
- package/docs/migrate/selenium.mdx +110 -0
- package/docs/mobile-ci.mdx +567 -0
- package/docs/mobile.mdx +555 -0
- package/docs/models.mdx +241 -0
- package/docs/quickstart.mdx +260 -0
- package/docs/reference/agent.mdx +524 -0
- package/docs/reference/app.mdx +72 -0
- package/docs/reference/cli.mdx +817 -0
- package/docs/reference/config.mdx +986 -0
- package/docs/reference/engine.mdx +1362 -0
- package/docs/reference/environment.mdx +113 -0
- package/docs/reference/errors.mdx +143 -0
- package/docs/reference/expect.mdx +513 -0
- package/docs/reference/mcp.mdx +202 -0
- package/docs/reference/mobile.mdx +522 -0
- package/docs/reference/reporters.mdx +216 -0
- package/docs/reference/screen.mdx +688 -0
- package/docs/reference/test.mdx +449 -0
- package/docs/reference/web.mdx +611 -0
- package/docs/security.mdx +292 -0
- package/docs/starting-your-app.mdx +231 -0
- package/docs/subscriptions.mdx +157 -0
- package/docs/telemetry.mdx +185 -0
- package/docs/tools.mdx +150 -0
- package/docs/viewports.mdx +146 -0
- package/docs/writing-an-engine.mdx +316 -0
- package/docs/writing-tests.mdx +256 -0
- package/package.json +2 -1
- package/skills/e2e/SKILL.md +4 -1
- package/skills/e2e/references/agent.md +3 -1
- package/skills/e2e/references/setup.md +13 -4
package/docs/browser.mdx
ADDED
|
@@ -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>
|