craftdriver 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/README.md +157 -45
- package/bin/craftdriver.mjs +26 -0
- package/dist/cli/client.d.ts +17 -0
- package/dist/cli/client.d.ts.map +1 -0
- package/dist/cli/client.js +88 -0
- package/dist/cli/client.js.map +1 -0
- package/dist/cli/daemon.d.ts +15 -0
- package/dist/cli/daemon.d.ts.map +1 -0
- package/dist/cli/daemon.js +145 -0
- package/dist/cli/daemon.js.map +1 -0
- package/dist/cli/defaults.d.ts +10 -0
- package/dist/cli/defaults.d.ts.map +1 -0
- package/dist/cli/defaults.js +42 -0
- package/dist/cli/defaults.js.map +1 -0
- package/dist/cli/dispatcher.d.ts +28 -0
- package/dist/cli/dispatcher.d.ts.map +1 -0
- package/dist/cli/dispatcher.js +319 -0
- package/dist/cli/dispatcher.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +404 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +24 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +192 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/mcp/artifacts.d.ts +43 -0
- package/dist/cli/mcp/artifacts.d.ts.map +1 -0
- package/dist/cli/mcp/artifacts.js +104 -0
- package/dist/cli/mcp/artifacts.js.map +1 -0
- package/dist/cli/mcp/server.d.ts +34 -0
- package/dist/cli/mcp/server.d.ts.map +1 -0
- package/dist/cli/mcp/server.js +214 -0
- package/dist/cli/mcp/server.js.map +1 -0
- package/dist/cli/mcp/tools.d.ts +34 -0
- package/dist/cli/mcp/tools.d.ts.map +1 -0
- package/dist/cli/mcp/tools.js +236 -0
- package/dist/cli/mcp/tools.js.map +1 -0
- package/dist/cli/parseArgs.d.ts +37 -0
- package/dist/cli/parseArgs.d.ts.map +1 -0
- package/dist/cli/parseArgs.js +215 -0
- package/dist/cli/parseArgs.js.map +1 -0
- package/dist/cli/protocol.d.ts +34 -0
- package/dist/cli/protocol.d.ts.map +1 -0
- package/dist/cli/protocol.js +2 -0
- package/dist/cli/protocol.js.map +1 -0
- package/dist/cli/selector.d.ts +29 -0
- package/dist/cli/selector.d.ts.map +1 -0
- package/dist/cli/selector.js +100 -0
- package/dist/cli/selector.js.map +1 -0
- package/dist/cli/snapshot.d.ts +48 -0
- package/dist/cli/snapshot.d.ts.map +1 -0
- package/dist/cli/snapshot.js +179 -0
- package/dist/cli/snapshot.js.map +1 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/a11y.d.ts +81 -0
- package/dist/lib/a11y.d.ts.map +1 -0
- package/dist/lib/a11y.js +171 -0
- package/dist/lib/a11y.js.map +1 -0
- package/dist/lib/bidi/index.d.ts +26 -1
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +49 -0
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts +2 -2
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +2 -2
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/bidi/network.d.ts +74 -1
- package/dist/lib/bidi/network.d.ts.map +1 -1
- package/dist/lib/bidi/network.js +222 -4
- package/dist/lib/bidi/network.js.map +1 -1
- package/dist/lib/bidi/storage.d.ts +5 -3
- package/dist/lib/bidi/storage.d.ts.map +1 -1
- package/dist/lib/bidi/storage.js +14 -7
- package/dist/lib/bidi/storage.js.map +1 -1
- package/dist/lib/bidi/types.d.ts +27 -6
- package/dist/lib/bidi/types.d.ts.map +1 -1
- package/dist/lib/browser.d.ts +637 -16
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +1428 -102
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +486 -0
- package/dist/lib/browserContext.d.ts.map +1 -0
- package/dist/lib/browserContext.js +937 -0
- package/dist/lib/browserContext.js.map +1 -0
- package/dist/lib/builder.d.ts +3 -0
- package/dist/lib/builder.d.ts.map +1 -1
- package/dist/lib/builder.js +31 -4
- package/dist/lib/builder.js.map +1 -1
- package/dist/lib/by.d.ts +18 -2
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +20 -2
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/chrome.d.ts +13 -1
- package/dist/lib/chrome.d.ts.map +1 -1
- package/dist/lib/chrome.js +13 -17
- package/dist/lib/chrome.js.map +1 -1
- package/dist/lib/clock.d.ts +115 -0
- package/dist/lib/clock.d.ts.map +1 -0
- package/dist/lib/clock.js +407 -0
- package/dist/lib/clock.js.map +1 -0
- package/dist/lib/driver.d.ts +43 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +120 -0
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/driverManager.d.ts +25 -0
- package/dist/lib/driverManager.d.ts.map +1 -0
- package/dist/lib/driverManager.js +399 -0
- package/dist/lib/driverManager.js.map +1 -0
- package/dist/lib/elementHandle.d.ts +67 -3
- package/dist/lib/elementHandle.d.ts.map +1 -1
- package/dist/lib/elementHandle.js +209 -67
- package/dist/lib/elementHandle.js.map +1 -1
- package/dist/lib/errors.d.ts +71 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +73 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/expect.d.ts +6 -1
- package/dist/lib/expect.d.ts.map +1 -1
- package/dist/lib/expect.js +55 -37
- package/dist/lib/expect.js.map +1 -1
- package/dist/lib/firefox.d.ts +18 -0
- package/dist/lib/firefox.d.ts.map +1 -0
- package/dist/lib/firefox.js +33 -0
- package/dist/lib/firefox.js.map +1 -0
- package/dist/lib/frame.d.ts +57 -0
- package/dist/lib/frame.d.ts.map +1 -0
- package/dist/lib/frame.js +277 -0
- package/dist/lib/frame.js.map +1 -0
- package/dist/lib/keyboard.d.ts +1 -1
- package/dist/lib/keyboard.d.ts.map +1 -1
- package/dist/lib/keyboard.js +1 -1
- package/dist/lib/keyboard.js.map +1 -1
- package/dist/lib/locator.d.ts +71 -2
- package/dist/lib/locator.d.ts.map +1 -1
- package/dist/lib/locator.js +289 -31
- package/dist/lib/locator.js.map +1 -1
- package/dist/lib/mouse.d.ts +1 -1
- package/dist/lib/mouse.d.ts.map +1 -1
- package/dist/lib/mouse.js +1 -1
- package/dist/lib/mouse.js.map +1 -1
- package/dist/lib/page.d.ts +107 -0
- package/dist/lib/page.d.ts.map +1 -0
- package/dist/lib/page.js +388 -0
- package/dist/lib/page.js.map +1 -0
- package/dist/lib/tracing.d.ts +128 -0
- package/dist/lib/tracing.d.ts.map +1 -0
- package/dist/lib/tracing.js +272 -0
- package/dist/lib/tracing.js.map +1 -0
- package/dist/lib/wait.d.ts.map +1 -1
- package/dist/lib/wait.js +14 -1
- package/dist/lib/wait.js.map +1 -1
- package/dist/lib/webelement.d.ts +2 -0
- package/dist/lib/webelement.d.ts.map +1 -1
- package/dist/lib/webelement.js +18 -0
- package/dist/lib/webelement.js.map +1 -1
- package/docs/accessibility.md +177 -0
- package/docs/api-reference.md +71 -0
- package/docs/assertions.md +182 -0
- package/docs/bidi-features.md +462 -0
- package/docs/browser-api.md +673 -0
- package/docs/browser-context.md +522 -0
- package/docs/cli.md +233 -0
- package/docs/clock.md +245 -0
- package/docs/dialogs.md +143 -0
- package/docs/driver-configuration.md +64 -0
- package/docs/element-api.md +219 -0
- package/docs/emulation.md +155 -0
- package/docs/error-codes.md +67 -0
- package/docs/getting-started.md +168 -0
- package/docs/keyboard-mouse.md +250 -0
- package/docs/mcp.md +250 -0
- package/docs/mobile-emulation.md +194 -0
- package/docs/screenshots.md +146 -0
- package/docs/selectors.md +382 -0
- package/docs/session-management.md +229 -0
- package/docs/tracing.md +282 -0
- package/package.json +23 -6
- package/skills/craftdriver/SKILL.md +84 -0
- package/skills/craftdriver/cheatsheet.md +194 -0
- package/skills/craftdriver/cli.md +104 -0
- package/skills/craftdriver/patterns.md +139 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
# Browser Contexts (isolated profiles)
|
|
2
|
+
|
|
3
|
+
A `BrowserContext` is a **WebDriver BiDi user context** — an isolated
|
|
4
|
+
browser profile, equivalent to opening a new incognito window. Each
|
|
5
|
+
context has its own cookies, localStorage, IndexedDB, and service
|
|
6
|
+
workers, fully isolated from every other context including the default
|
|
7
|
+
one.
|
|
8
|
+
|
|
9
|
+
Use `BrowserContext` to run multi-user scenarios (log in as Alice in
|
|
10
|
+
one context, as Bob in another) without cookie cross-talk, or to test
|
|
11
|
+
cold-start behaviour without nuking the rest of your session.
|
|
12
|
+
|
|
13
|
+
> **BiDi-only.** This API maps directly onto BiDi
|
|
14
|
+
> `browser.createUserContext` / `browser.getUserContexts` /
|
|
15
|
+
> `browser.removeUserContext`. WebDriver Classic has no equivalent and
|
|
16
|
+
> these methods throw a clear error when `enableBiDi: false`.
|
|
17
|
+
|
|
18
|
+
## The hierarchy
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Browser — the BiDi session (one launched browser process)
|
|
22
|
+
└── BrowserContext — an isolated user context (this page)
|
|
23
|
+
└── Page — a top-level browsing context (tab or window)
|
|
24
|
+
└── Frame — a nested browsing context (iframe)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The `Browser` always exposes a `defaultContext` (id `'default'`). Pages
|
|
28
|
+
opened via `browser.openPage()` belong to it. Call `browser.newContext()`
|
|
29
|
+
to create additional isolated contexts.
|
|
30
|
+
|
|
31
|
+
## Quick reference
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { Browser } from 'craftdriver';
|
|
35
|
+
|
|
36
|
+
const browser = await Browser.launch();
|
|
37
|
+
|
|
38
|
+
// The default context (always present, id === 'default').
|
|
39
|
+
const defaultCtx = browser.defaultContext;
|
|
40
|
+
|
|
41
|
+
// Create a fresh isolated context.
|
|
42
|
+
const ctx = await browser.newContext();
|
|
43
|
+
|
|
44
|
+
// List every open context (including 'default').
|
|
45
|
+
const all = await browser.contexts();
|
|
46
|
+
|
|
47
|
+
// Pages inside a context.
|
|
48
|
+
const page = await ctx.newPage({ url: 'https://example.com' });
|
|
49
|
+
const pages = await ctx.pages();
|
|
50
|
+
|
|
51
|
+
// Capture a popup spawned from this context.
|
|
52
|
+
const popup = await ctx.waitForPage(() => page.find('#open').click());
|
|
53
|
+
|
|
54
|
+
// Tear it down — all of its pages close, the profile is wiped.
|
|
55
|
+
await ctx.close();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> **Heads-up.** `browser.click()`, `browser.find()` and the other
|
|
59
|
+
> `Browser`-level shortcuts always target a page in **`defaultContext`**.
|
|
60
|
+
> They never reach a `Page` you got from `ctx.newPage()` on a
|
|
61
|
+
> non-default context. Always call methods on the `Page` you got back —
|
|
62
|
+
> e.g. `await aPage.fill('#user', 'alice')`, not `browser.fill(...)`.
|
|
63
|
+
|
|
64
|
+
## The auth-fixture pattern (skip the login UI in every test)
|
|
65
|
+
|
|
66
|
+
Most test suites log in dozens of times against the same app. Do it
|
|
67
|
+
**once**, snapshot the session to disk, and reuse it from every later
|
|
68
|
+
test. This is the canonical real-world use of `storageState`.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// tests/setup/login.ts — runs once, before the suite.
|
|
72
|
+
import { Browser } from 'craftdriver';
|
|
73
|
+
|
|
74
|
+
const browser = await Browser.launch();
|
|
75
|
+
const ctx = await browser.newContext();
|
|
76
|
+
const page = await ctx.newPage({ url: 'https://app.example.com/login' });
|
|
77
|
+
await page.find('#username').fill('alice');
|
|
78
|
+
await page.find('#password').fill('secret');
|
|
79
|
+
await page.find('#submit').click();
|
|
80
|
+
await page.expect('#welcome').toBeVisible();
|
|
81
|
+
|
|
82
|
+
// Cookies + localStorage for every origin the context has visited.
|
|
83
|
+
await ctx.saveStorageState('auth/alice.json');
|
|
84
|
+
await browser.quit();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// every other test:
|
|
89
|
+
const ctx = await browser.newContext({ storageState: 'auth/alice.json' });
|
|
90
|
+
const page = await ctx.newPage({ url: 'https://app.example.com/dashboard' });
|
|
91
|
+
// Already logged in — no form, no waiting.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Multi-user login in one browser
|
|
95
|
+
|
|
96
|
+
Two contexts — two logged-in sessions — no cookie cross-talk. Useful for
|
|
97
|
+
testing permission boundaries (admin vs. guest), real-time features
|
|
98
|
+
(chat, collaboration), or any flow that needs two users live at once.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const alice = await browser.newContext();
|
|
102
|
+
const bob = await browser.newContext();
|
|
103
|
+
|
|
104
|
+
const aPage = await alice.newPage({ url: 'https://app.example.com/login' });
|
|
105
|
+
await loginAs(aPage, 'alice');
|
|
106
|
+
|
|
107
|
+
const bPage = await bob.newPage({ url: 'https://app.example.com/login' });
|
|
108
|
+
await loginAs(bPage, 'bob');
|
|
109
|
+
// bPage cannot see alice's session cookie, and vice versa.
|
|
110
|
+
|
|
111
|
+
await alice.close();
|
|
112
|
+
await bob.close();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## API
|
|
116
|
+
|
|
117
|
+
### `browser.newContext(opts?): Promise<BrowserContext>`
|
|
118
|
+
|
|
119
|
+
Create a new isolated user context. Backed by BiDi
|
|
120
|
+
`browser.createUserContext`. Throws in Classic mode.
|
|
121
|
+
|
|
122
|
+
- `opts.storageState`: a `SessionState` object **or** a path to a JSON
|
|
123
|
+
file produced by `BrowserContext.saveStorageState()`. Cookies are
|
|
124
|
+
applied immediately; localStorage entries land on first navigation to
|
|
125
|
+
each captured origin via an internal preload script.
|
|
126
|
+
- `opts.baseURL`: a base URL applied to every relative `url` passed to
|
|
127
|
+
`ctx.newPage()` or `page.navigateTo()` inside the context. Absolute
|
|
128
|
+
URLs pass through unchanged. Lets your tests say `'/login'` instead
|
|
129
|
+
of repeating `'https://staging.example.com/login'` everywhere.
|
|
130
|
+
- `opts.extraHTTPHeaders`: headers attached to **every** outgoing request
|
|
131
|
+
from every page in this context. Useful for staging tokens, tenant
|
|
132
|
+
ids, feature-flag overrides, or correlating test traffic in server
|
|
133
|
+
logs. Replace later with `ctx.setExtraHTTPHeaders(…)`.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const ctx = await browser.newContext({
|
|
137
|
+
baseURL: 'https://staging.example.com',
|
|
138
|
+
extraHTTPHeaders: { 'x-test-tenant': 'acme', 'x-staging-token': process.env.STAGING_TOKEN! },
|
|
139
|
+
storageState: 'auth/alice.json',
|
|
140
|
+
});
|
|
141
|
+
const page = await ctx.newPage({ url: '/dashboard' }); // resolves against baseURL
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### `browser.contexts(): Promise<BrowserContext[]>`
|
|
145
|
+
|
|
146
|
+
Return all open user contexts, including the default one. Backed by
|
|
147
|
+
BiDi `browser.getUserContexts`. Throws in Classic mode.
|
|
148
|
+
|
|
149
|
+
### `browser.defaultContext: BrowserContext`
|
|
150
|
+
|
|
151
|
+
The implicit context the browser started in (id `'default'`). Pages
|
|
152
|
+
opened via `browser.openPage()` / `browser.waitForPage()` live here.
|
|
153
|
+
|
|
154
|
+
### `BrowserContext.id: string`
|
|
155
|
+
|
|
156
|
+
The BiDi user-context id. The default context's id is the literal
|
|
157
|
+
string `'default'`.
|
|
158
|
+
|
|
159
|
+
### `BrowserContext.newPage(opts?): Promise<Page>`
|
|
160
|
+
|
|
161
|
+
Open a tab/window inside this context. `opts.url` navigates the new
|
|
162
|
+
page; `opts.type` is `'tab'` (default) or `'window'`.
|
|
163
|
+
|
|
164
|
+
### `BrowserContext.pages(): Promise<Page[]>`
|
|
165
|
+
|
|
166
|
+
All open top-level pages that belong to this context.
|
|
167
|
+
|
|
168
|
+
### `BrowserContext.waitForPage(action, opts?): Promise<Page>`
|
|
169
|
+
|
|
170
|
+
Run `action` and resolve to the next new page that opens **inside this
|
|
171
|
+
context**. Useful for popup-from-click flows.
|
|
172
|
+
|
|
173
|
+
### `BrowserContext.cookies(urls?): Promise<Cookie[]>`
|
|
174
|
+
|
|
175
|
+
Return cookies scoped to this user context. Pass a single URL or an
|
|
176
|
+
array to filter to cookies that would be sent on a request to that URL
|
|
177
|
+
(RFC-6265 domain/path match).
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const session = await ctx.cookies('https://app.example.com');
|
|
181
|
+
expect(session.find((c) => c.name === 'sid')).toBeDefined();
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `BrowserContext.addCookies(cookies): Promise<void>`
|
|
185
|
+
|
|
186
|
+
Add cookies to this context. `domain` is required (BiDi rejects
|
|
187
|
+
host-less cookies). Throws a clear error if you ask for
|
|
188
|
+
`sameSite: 'none'` without `secure: true`.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
await ctx.addCookies([
|
|
192
|
+
{
|
|
193
|
+
name: 'sid',
|
|
194
|
+
value: 'eyJ…',
|
|
195
|
+
domain: 'app.example.com',
|
|
196
|
+
path: '/',
|
|
197
|
+
secure: true,
|
|
198
|
+
sameSite: 'lax',
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### `BrowserContext.clearCookies(filter?): Promise<void>`
|
|
204
|
+
|
|
205
|
+
Remove cookies. With no filter, wipes every cookie in the context. With
|
|
206
|
+
a filter, removes cookies that match **all** provided fields (`name`,
|
|
207
|
+
`domain`, `path`).
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
await ctx.clearCookies({ name: 'sid' }); // sign the user out
|
|
211
|
+
await ctx.clearCookies({ domain: 'tracker.io' }); // wipe a third party
|
|
212
|
+
await ctx.clearCookies(); // full reset
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### `BrowserContext.storageState(opts?): Promise<SessionState>`
|
|
216
|
+
|
|
217
|
+
Snapshot cookies + localStorage for this context.
|
|
218
|
+
|
|
219
|
+
- `opts.includeCookies` — default `true`.
|
|
220
|
+
- `opts.includeLocalStorage` — default `true`. Only origins of **currently
|
|
221
|
+
open pages** are captured; BiDi has no API to read localStorage for
|
|
222
|
+
an origin no page is visiting. Open one page per origin you need
|
|
223
|
+
captured before snapshotting.
|
|
224
|
+
|
|
225
|
+
### `BrowserContext.saveStorageState(path, opts?): Promise<SessionState>`
|
|
226
|
+
|
|
227
|
+
Snapshot the context and write it to `path` as JSON. Returns the
|
|
228
|
+
snapshot for inspection. See the auth-fixture example above.
|
|
229
|
+
|
|
230
|
+
### `BrowserContext.loadStorageState(source): Promise<void>`
|
|
231
|
+
|
|
232
|
+
Apply a previously-captured snapshot. `source` is a `SessionState`
|
|
233
|
+
object or a path to JSON produced by `saveStorageState`. Cookies are
|
|
234
|
+
applied immediately; localStorage entries are installed via a preload
|
|
235
|
+
script that runs on first navigation to each captured origin. Calling
|
|
236
|
+
`loadStorageState` again replaces the previous preload — it does not
|
|
237
|
+
stack.
|
|
238
|
+
|
|
239
|
+
### `BrowserContext.close(): Promise<void>`
|
|
240
|
+
|
|
241
|
+
Remove the user context and close all its pages. Subsequent operations
|
|
242
|
+
on this `BrowserContext` instance throw. The default context cannot be
|
|
243
|
+
closed — quit the browser instead.
|
|
244
|
+
|
|
245
|
+
### `BrowserContext.isClosed: boolean`
|
|
246
|
+
|
|
247
|
+
`true` after `close()` has run.
|
|
248
|
+
|
|
249
|
+
### `BrowserContext.on(event, listener): () => void`
|
|
250
|
+
|
|
251
|
+
Subscribe to lifecycle events on this context. Returns an **unsubscribe
|
|
252
|
+
function**; call it to remove the listener. Events fire **only** for
|
|
253
|
+
this context.
|
|
254
|
+
|
|
255
|
+
- `'page'` — fires for every new top-level page in this context,
|
|
256
|
+
including popups opened by `window.open` or `target="_blank"` clicks.
|
|
257
|
+
Listener receives the new `Page`.
|
|
258
|
+
- `'close'` — fires once when the context is closed (via `ctx.close()`
|
|
259
|
+
or because the browser quit). Listener receives no arguments.
|
|
260
|
+
|
|
261
|
+
`ctx.off(event, listener)` is also available if you'd rather hold on to
|
|
262
|
+
the listener reference than the unsubscribe function.
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Real-world: capture every console error across every tab a test opens,
|
|
266
|
+
// including popups. No more "why did my test pass with a 500 in a popup".
|
|
267
|
+
const errors: string[] = [];
|
|
268
|
+
ctx.on('page', (page) => {
|
|
269
|
+
page.on('console', (msg) => {
|
|
270
|
+
if (msg.type() === 'error') errors.push(`${page.url()}: ${msg.text()}`);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const main = await ctx.newPage({ url: '/checkout' });
|
|
275
|
+
const receipt = await ctx.waitForPage(() => main.find('#print').click());
|
|
276
|
+
await receipt.waitForLoadState('load');
|
|
277
|
+
expect(errors).toEqual([]);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### `BrowserContext.addInitScript(script): Promise<InitScriptHandle>`
|
|
281
|
+
|
|
282
|
+
Register a script that runs in **every** page of this context _before_
|
|
283
|
+
any page script, on every navigation — including popups and iframes
|
|
284
|
+
that share the realm. Maps to BiDi `script.addPreloadScript` scoped to
|
|
285
|
+
this user context.
|
|
286
|
+
|
|
287
|
+
The handle exposes `.id` and `.remove()`. Removing only affects future
|
|
288
|
+
navigations.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
// Real-world: pin Date.now() and Math.random() for visual-regression
|
|
292
|
+
// stability; flip a feature flag before app code reads it.
|
|
293
|
+
const handle = await ctx.addInitScript(`
|
|
294
|
+
const fixed = new Date('2025-01-15T12:00:00Z').getTime();
|
|
295
|
+
Date.now = () => fixed;
|
|
296
|
+
Math.random = () => 0.42;
|
|
297
|
+
window.__FEATURES__ = { newCheckout: true };
|
|
298
|
+
`);
|
|
299
|
+
|
|
300
|
+
// …run tests…
|
|
301
|
+
|
|
302
|
+
await handle.remove(); // back to live time on the next navigation
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Init scripts do **not** leak between contexts: a script added to
|
|
306
|
+
`ctx.A` never runs in pages of `ctx.B`.
|
|
307
|
+
|
|
308
|
+
### `BrowserContext.removeInitScript(id): Promise<void>`
|
|
309
|
+
|
|
310
|
+
Remove a previously-registered init script by id. Equivalent to
|
|
311
|
+
`handle.remove()`. Unknown ids are silently ignored.
|
|
312
|
+
|
|
313
|
+
### `BrowserContext.setExtraHTTPHeaders(headers): Promise<void>`
|
|
314
|
+
|
|
315
|
+
Replace the context's `extraHTTPHeaders` map at runtime. Applies to
|
|
316
|
+
all **future** requests from existing and new pages in this context.
|
|
317
|
+
Pass `{}` to clear.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// Rotate a per-test correlation id partway through a flow.
|
|
321
|
+
await ctx.setExtraHTTPHeaders({ 'x-trace-id': crypto.randomUUID() });
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### `BrowserContext.route(pattern, handler): Promise<string>`
|
|
325
|
+
|
|
326
|
+
Intercept matching requests from pages in **this context only**.
|
|
327
|
+
Returns a route id you can pass to `unroute()`.
|
|
328
|
+
|
|
329
|
+
Under the hood we register one BiDi `network.addIntercept` **per page**,
|
|
330
|
+
scoped to that page's browsing-context id. Pages opened later inherit
|
|
331
|
+
every route automatically; closed pages have their intercept removed.
|
|
332
|
+
Requests from other contexts are not seen.
|
|
333
|
+
|
|
334
|
+
- `pattern`: a URL substring, a `RegExp`, or a glob (`*` matches any
|
|
335
|
+
run of non-`/` characters; `**` matches any run of characters).
|
|
336
|
+
- `handler({ request, fulfill, continue: cont, abort })`: called for
|
|
337
|
+
every matched request. Must call exactly one of:
|
|
338
|
+
- `fulfill({ status?, headers?, contentType?, body? })` — reply
|
|
339
|
+
immediately without hitting the network.
|
|
340
|
+
- `continue({ url?, method?, headers?, postData? })` — let the
|
|
341
|
+
request go through, optionally rewritten.
|
|
342
|
+
- `abort(reason?)` — fail the request.
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
// Real-world: serve a fixture to one tenant, let the real API answer
|
|
346
|
+
// the other — in the same test, side-by-side.
|
|
347
|
+
const admin = await browser.newContext();
|
|
348
|
+
const guest = await browser.newContext();
|
|
349
|
+
|
|
350
|
+
await admin.route('**/api/users', async ({ fulfill }) => {
|
|
351
|
+
await fulfill({
|
|
352
|
+
status: 200,
|
|
353
|
+
contentType: 'application/json',
|
|
354
|
+
body: JSON.stringify([{ id: 1, role: 'admin', name: 'Alice' }]),
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const aPage = await admin.newPage({ url: '/users' });
|
|
359
|
+
const gPage = await guest.newPage({ url: '/users' });
|
|
360
|
+
// aPage sees the mock; gPage hits the live backend.
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Glob matching is `**`-aware: `'**/api/users'` matches
|
|
364
|
+
`https://api.example.com/v1/api/users` but not
|
|
365
|
+
`https://example.com/api/users/123`.
|
|
366
|
+
|
|
367
|
+
### `BrowserContext.unroute(id?): Promise<void>`
|
|
368
|
+
|
|
369
|
+
Remove a single route by id, or with no argument remove **every** route
|
|
370
|
+
registered on this context.
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
const id = await ctx.route('**/api/flags', flagHandler);
|
|
374
|
+
// …
|
|
375
|
+
await ctx.unroute(id);
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### `Page.context(): BrowserContext`
|
|
379
|
+
|
|
380
|
+
Return the `BrowserContext` a page belongs to. Useful when a listener
|
|
381
|
+
or helper receives a `Page` and needs to reach back to the context
|
|
382
|
+
(e.g. to read cookies, set headers, or close the context).
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
ctx.on('page', (page) => {
|
|
386
|
+
// page.context() === ctx
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### `browser.newContext({ locale, timezoneId, geolocation })`
|
|
391
|
+
|
|
392
|
+
Identity-and-device overrides. Each is a thin wrapper over a BiDi
|
|
393
|
+
`emulation.*` command scoped to this user context, so **every** page in
|
|
394
|
+
the context — current and future, including popups — sees the override
|
|
395
|
+
without any per-page plumbing.
|
|
396
|
+
|
|
397
|
+
- `locale` — reported by `navigator.language`, `Intl.*`, and the
|
|
398
|
+
`Accept-Language` header. BiDi `emulation.setLocaleOverride`.
|
|
399
|
+
Cross-browser: Chrome and Firefox.
|
|
400
|
+
- `timezoneId` — IANA timezone applied to `Date` and
|
|
401
|
+
`Intl.DateTimeFormat`. BiDi `emulation.setTimezoneOverride`.
|
|
402
|
+
Cross-browser: Chrome and Firefox.
|
|
403
|
+
- `geolocation` — `{ latitude, longitude, accuracy? }` returned by
|
|
404
|
+
`navigator.geolocation.getCurrentPosition()`. BiDi
|
|
405
|
+
`emulation.setGeolocationOverride`. Reliable on Chrome; Firefox BiDi
|
|
406
|
+
coverage is uneven and the setter wraps the underlying error with a
|
|
407
|
+
clear engine name when it fails. The `geolocation` permission still
|
|
408
|
+
has to be granted — see {@link BrowserContext.grantPermissions}.
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// Real-world: render the German checkout, log times in Berlin, pin the
|
|
412
|
+
// user to a Berlin lat/lon for the geofence test — all in one context,
|
|
413
|
+
// fully isolated from the Tokyo-locale context next to it.
|
|
414
|
+
const de = await browser.newContext({
|
|
415
|
+
locale: 'de-DE',
|
|
416
|
+
timezoneId: 'Europe/Berlin',
|
|
417
|
+
geolocation: { latitude: 52.52, longitude: 13.4 },
|
|
418
|
+
});
|
|
419
|
+
await de.grantPermissions(['geolocation'], { origin: 'https://app.example.com' });
|
|
420
|
+
const page = await de.newPage({ url: 'https://app.example.com/checkout' });
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### `BrowserContext.setLocale(locale: string | null): Promise<void>`
|
|
424
|
+
|
|
425
|
+
Change the per-context locale at runtime. Pass `null` to clear. Existing
|
|
426
|
+
pages pick up the new locale on their next page-driven re-read (e.g.
|
|
427
|
+
reload, navigation, or any code that re-evaluates `navigator.language`).
|
|
428
|
+
Cross-browser: Chrome and Firefox.
|
|
429
|
+
|
|
430
|
+
### `BrowserContext.setTimezone(timezoneId: string | null): Promise<void>`
|
|
431
|
+
|
|
432
|
+
Change the per-context timezone at runtime. Pass `null` to clear.
|
|
433
|
+
Cross-browser: Chrome and Firefox.
|
|
434
|
+
|
|
435
|
+
### `BrowserContext.setGeolocation(coords | null): Promise<void>`
|
|
436
|
+
|
|
437
|
+
Replace the geolocation override for this context. Pass `null` to clear.
|
|
438
|
+
Validates that latitude ∈ [-90, 90] and longitude ∈ [-180, 180]. Wraps
|
|
439
|
+
the underlying BiDi error with the engine name on failure so you can
|
|
440
|
+
tell when a browser hasn't shipped this command yet.
|
|
441
|
+
|
|
442
|
+
### `BrowserContext.grantPermissions(permissions, { origin, state? })`
|
|
443
|
+
|
|
444
|
+
Grant, deny, or reset W3C permissions for an origin in **this context
|
|
445
|
+
only**. `origin` is required — BiDi `permissions.setPermission` has no
|
|
446
|
+
"all origins" wildcard. `state` defaults to `'granted'`; pass `'denied'`
|
|
447
|
+
to pre-reject the prompt, or `'prompt'` to clear back to the browser
|
|
448
|
+
default.
|
|
449
|
+
|
|
450
|
+
Valid `permissions` names are the W3C Permissions catalogue:
|
|
451
|
+
`'geolocation'`, `'notifications'`, `'clipboard-read'`,
|
|
452
|
+
`'clipboard-write'`, `'camera'`, `'microphone'`, `'midi'`,
|
|
453
|
+
`'background-sync'`, `'persistent-storage'`, …
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
// Real-world: don't make the test click through the notification prompt.
|
|
457
|
+
await ctx.grantPermissions(['notifications'], {
|
|
458
|
+
origin: 'https://app.example.com',
|
|
459
|
+
state: 'denied',
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### `BrowserContext.clearPermissions(permissions, { origin })`
|
|
464
|
+
|
|
465
|
+
Reset the listed permissions for an origin back to `'prompt'`. Sugar for
|
|
466
|
+
`grantPermissions(…, { state: 'prompt' })`.
|
|
467
|
+
|
|
468
|
+
## Cross-browser support
|
|
469
|
+
|
|
470
|
+
Most of the `BrowserContext` API — cookies, storage state,
|
|
471
|
+
`on('page'|'close')`, `addInitScript`, `route`/`unroute`,
|
|
472
|
+
`setExtraHTTPHeaders`, `baseURL`, `extraHTTPHeaders`, `page.context()`,
|
|
473
|
+
`setLocale`, `setTimezone`, `grantPermissions`/`clearPermissions` — works
|
|
474
|
+
on **Chrome** and **Firefox** via WebDriver BiDi. Every method throws a
|
|
475
|
+
clear error in Classic mode.
|
|
476
|
+
|
|
477
|
+
`setGeolocation` / `newContext({ geolocation })` is reliable on Chrome.
|
|
478
|
+
Firefox BiDi `emulation.setGeolocationOverride` coverage is still
|
|
479
|
+
uneven; the setter surfaces the engine error with a clear message when
|
|
480
|
+
the browser hasn't shipped it.
|
|
481
|
+
|
|
482
|
+
A default `browser.newContext()` (no options) is always cross-browser.
|
|
483
|
+
|
|
484
|
+
## Scope and precedence (gotchas)
|
|
485
|
+
|
|
486
|
+
A few things that aren't bugs but bite if you don't know them:
|
|
487
|
+
|
|
488
|
+
- **`storageState` covers cookies + localStorage only.** Not
|
|
489
|
+
sessionStorage, not IndexedDB, not Cache Storage. Apps that put their
|
|
490
|
+
auth token in sessionStorage (some Auth0 / MSAL fallback configs) will
|
|
491
|
+
need a custom restore step.
|
|
492
|
+
- **Browser-level vs context-level emulation race.** `browser.setGeolocation(...)`
|
|
493
|
+
is a session-wide override; `ctx.setGeolocation(...)` is a per-context
|
|
494
|
+
override. If both are set, the context-level one wins inside the
|
|
495
|
+
context (it carries a more specific `userContexts` scope at the BiDi
|
|
496
|
+
layer). To avoid surprises, pick one layer per concern.
|
|
497
|
+
- **`ctx.route()` does not see service-worker requests.** Per-page
|
|
498
|
+
intercepts are bound to the page's browsing context; fetches initiated
|
|
499
|
+
by a service worker or a dedicated/shared worker bypass them. Mock
|
|
500
|
+
those endpoints at the application layer, or unregister the service
|
|
501
|
+
worker via an `addInitScript`.
|
|
502
|
+
- **`grantPermissions(['a','b'], ...)` is not transactional.** Each
|
|
503
|
+
permission is a separate BiDi call; if the second one fails, the first
|
|
504
|
+
one stays granted. Re-grant idempotently or wrap in your own
|
|
505
|
+
try/finally if you need rollback.
|
|
506
|
+
- **`page.context()` reflects the BiDi user context, not the window
|
|
507
|
+
group.** Popups opened from a page in `ctx` belong to `ctx`. Pages
|
|
508
|
+
opened via `browser.openPage()` / `waitForPage()` belong to
|
|
509
|
+
`browser.defaultContext`.
|
|
510
|
+
|
|
511
|
+
## What's not (yet) supported
|
|
512
|
+
|
|
513
|
+
These are documented gaps, not bugs — open an issue if you need them:
|
|
514
|
+
|
|
515
|
+
- **Per-context viewport, `colorScheme`, `reducedMotion`, `forcedColors`,
|
|
516
|
+
`userAgent`.** These require either CDP (Chromium-only) or BiDi
|
|
517
|
+
primitives that aren't broadly shipped yet. Use
|
|
518
|
+
`browser.emulate({ … })` for the session-wide equivalents on Chromium.
|
|
519
|
+
- **localStorage capture for origins with no open page.** BiDi has no
|
|
520
|
+
out-of-band storage read API; only currently-visiting origins are in
|
|
521
|
+
the snapshot.
|
|
522
|
+
- **`httpCredentials` / `offline`.** Not modelled in BiDi yet.
|