craftdriver 0.1.0 → 0.2.1

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 (186) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +157 -45
  3. package/bin/craftdriver.mjs +26 -0
  4. package/dist/cli/client.d.ts +17 -0
  5. package/dist/cli/client.d.ts.map +1 -0
  6. package/dist/cli/client.js +88 -0
  7. package/dist/cli/client.js.map +1 -0
  8. package/dist/cli/daemon.d.ts +15 -0
  9. package/dist/cli/daemon.d.ts.map +1 -0
  10. package/dist/cli/daemon.js +145 -0
  11. package/dist/cli/daemon.js.map +1 -0
  12. package/dist/cli/defaults.d.ts +10 -0
  13. package/dist/cli/defaults.d.ts.map +1 -0
  14. package/dist/cli/defaults.js +42 -0
  15. package/dist/cli/defaults.js.map +1 -0
  16. package/dist/cli/dispatcher.d.ts +28 -0
  17. package/dist/cli/dispatcher.d.ts.map +1 -0
  18. package/dist/cli/dispatcher.js +319 -0
  19. package/dist/cli/dispatcher.js.map +1 -0
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.d.ts.map +1 -0
  22. package/dist/cli/index.js +404 -0
  23. package/dist/cli/index.js.map +1 -0
  24. package/dist/cli/init.d.ts +24 -0
  25. package/dist/cli/init.d.ts.map +1 -0
  26. package/dist/cli/init.js +192 -0
  27. package/dist/cli/init.js.map +1 -0
  28. package/dist/cli/mcp/artifacts.d.ts +43 -0
  29. package/dist/cli/mcp/artifacts.d.ts.map +1 -0
  30. package/dist/cli/mcp/artifacts.js +104 -0
  31. package/dist/cli/mcp/artifacts.js.map +1 -0
  32. package/dist/cli/mcp/server.d.ts +34 -0
  33. package/dist/cli/mcp/server.d.ts.map +1 -0
  34. package/dist/cli/mcp/server.js +214 -0
  35. package/dist/cli/mcp/server.js.map +1 -0
  36. package/dist/cli/mcp/tools.d.ts +34 -0
  37. package/dist/cli/mcp/tools.d.ts.map +1 -0
  38. package/dist/cli/mcp/tools.js +236 -0
  39. package/dist/cli/mcp/tools.js.map +1 -0
  40. package/dist/cli/parseArgs.d.ts +37 -0
  41. package/dist/cli/parseArgs.d.ts.map +1 -0
  42. package/dist/cli/parseArgs.js +215 -0
  43. package/dist/cli/parseArgs.js.map +1 -0
  44. package/dist/cli/protocol.d.ts +34 -0
  45. package/dist/cli/protocol.d.ts.map +1 -0
  46. package/dist/cli/protocol.js +2 -0
  47. package/dist/cli/protocol.js.map +1 -0
  48. package/dist/cli/selector.d.ts +29 -0
  49. package/dist/cli/selector.d.ts.map +1 -0
  50. package/dist/cli/selector.js +100 -0
  51. package/dist/cli/selector.js.map +1 -0
  52. package/dist/cli/snapshot.d.ts +48 -0
  53. package/dist/cli/snapshot.d.ts.map +1 -0
  54. package/dist/cli/snapshot.js +179 -0
  55. package/dist/cli/snapshot.js.map +1 -0
  56. package/dist/index.d.ts +14 -2
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +15 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/lib/a11y.d.ts +81 -0
  61. package/dist/lib/a11y.d.ts.map +1 -0
  62. package/dist/lib/a11y.js +171 -0
  63. package/dist/lib/a11y.js.map +1 -0
  64. package/dist/lib/bidi/index.d.ts +26 -1
  65. package/dist/lib/bidi/index.d.ts.map +1 -1
  66. package/dist/lib/bidi/index.js +49 -0
  67. package/dist/lib/bidi/index.js.map +1 -1
  68. package/dist/lib/bidi/logs.d.ts +2 -2
  69. package/dist/lib/bidi/logs.d.ts.map +1 -1
  70. package/dist/lib/bidi/logs.js +2 -2
  71. package/dist/lib/bidi/logs.js.map +1 -1
  72. package/dist/lib/bidi/network.d.ts +74 -1
  73. package/dist/lib/bidi/network.d.ts.map +1 -1
  74. package/dist/lib/bidi/network.js +222 -4
  75. package/dist/lib/bidi/network.js.map +1 -1
  76. package/dist/lib/bidi/storage.d.ts +5 -3
  77. package/dist/lib/bidi/storage.d.ts.map +1 -1
  78. package/dist/lib/bidi/storage.js +14 -7
  79. package/dist/lib/bidi/storage.js.map +1 -1
  80. package/dist/lib/bidi/types.d.ts +27 -6
  81. package/dist/lib/bidi/types.d.ts.map +1 -1
  82. package/dist/lib/browser.d.ts +637 -16
  83. package/dist/lib/browser.d.ts.map +1 -1
  84. package/dist/lib/browser.js +1428 -102
  85. package/dist/lib/browser.js.map +1 -1
  86. package/dist/lib/browserContext.d.ts +486 -0
  87. package/dist/lib/browserContext.d.ts.map +1 -0
  88. package/dist/lib/browserContext.js +937 -0
  89. package/dist/lib/browserContext.js.map +1 -0
  90. package/dist/lib/builder.d.ts +3 -0
  91. package/dist/lib/builder.d.ts.map +1 -1
  92. package/dist/lib/builder.js +31 -4
  93. package/dist/lib/builder.js.map +1 -1
  94. package/dist/lib/by.d.ts +18 -2
  95. package/dist/lib/by.d.ts.map +1 -1
  96. package/dist/lib/by.js +20 -2
  97. package/dist/lib/by.js.map +1 -1
  98. package/dist/lib/chrome.d.ts +13 -1
  99. package/dist/lib/chrome.d.ts.map +1 -1
  100. package/dist/lib/chrome.js +13 -17
  101. package/dist/lib/chrome.js.map +1 -1
  102. package/dist/lib/clock.d.ts +115 -0
  103. package/dist/lib/clock.d.ts.map +1 -0
  104. package/dist/lib/clock.js +407 -0
  105. package/dist/lib/clock.js.map +1 -0
  106. package/dist/lib/driver.d.ts +43 -0
  107. package/dist/lib/driver.d.ts.map +1 -1
  108. package/dist/lib/driver.js +120 -0
  109. package/dist/lib/driver.js.map +1 -1
  110. package/dist/lib/driverManager.d.ts +25 -0
  111. package/dist/lib/driverManager.d.ts.map +1 -0
  112. package/dist/lib/driverManager.js +399 -0
  113. package/dist/lib/driverManager.js.map +1 -0
  114. package/dist/lib/elementHandle.d.ts +67 -3
  115. package/dist/lib/elementHandle.d.ts.map +1 -1
  116. package/dist/lib/elementHandle.js +209 -67
  117. package/dist/lib/elementHandle.js.map +1 -1
  118. package/dist/lib/errors.d.ts +71 -0
  119. package/dist/lib/errors.d.ts.map +1 -0
  120. package/dist/lib/errors.js +73 -0
  121. package/dist/lib/errors.js.map +1 -0
  122. package/dist/lib/expect.d.ts +6 -1
  123. package/dist/lib/expect.d.ts.map +1 -1
  124. package/dist/lib/expect.js +55 -37
  125. package/dist/lib/expect.js.map +1 -1
  126. package/dist/lib/firefox.d.ts +18 -0
  127. package/dist/lib/firefox.d.ts.map +1 -0
  128. package/dist/lib/firefox.js +33 -0
  129. package/dist/lib/firefox.js.map +1 -0
  130. package/dist/lib/frame.d.ts +57 -0
  131. package/dist/lib/frame.d.ts.map +1 -0
  132. package/dist/lib/frame.js +277 -0
  133. package/dist/lib/frame.js.map +1 -0
  134. package/dist/lib/keyboard.d.ts +1 -1
  135. package/dist/lib/keyboard.d.ts.map +1 -1
  136. package/dist/lib/keyboard.js +1 -1
  137. package/dist/lib/keyboard.js.map +1 -1
  138. package/dist/lib/locator.d.ts +71 -2
  139. package/dist/lib/locator.d.ts.map +1 -1
  140. package/dist/lib/locator.js +289 -31
  141. package/dist/lib/locator.js.map +1 -1
  142. package/dist/lib/mouse.d.ts +1 -1
  143. package/dist/lib/mouse.d.ts.map +1 -1
  144. package/dist/lib/mouse.js +1 -1
  145. package/dist/lib/mouse.js.map +1 -1
  146. package/dist/lib/page.d.ts +107 -0
  147. package/dist/lib/page.d.ts.map +1 -0
  148. package/dist/lib/page.js +388 -0
  149. package/dist/lib/page.js.map +1 -0
  150. package/dist/lib/tracing.d.ts +128 -0
  151. package/dist/lib/tracing.d.ts.map +1 -0
  152. package/dist/lib/tracing.js +272 -0
  153. package/dist/lib/tracing.js.map +1 -0
  154. package/dist/lib/wait.d.ts.map +1 -1
  155. package/dist/lib/wait.js +14 -1
  156. package/dist/lib/wait.js.map +1 -1
  157. package/dist/lib/webelement.d.ts +2 -0
  158. package/dist/lib/webelement.d.ts.map +1 -1
  159. package/dist/lib/webelement.js +18 -0
  160. package/dist/lib/webelement.js.map +1 -1
  161. package/docs/accessibility.md +177 -0
  162. package/docs/api-reference.md +71 -0
  163. package/docs/assertions.md +182 -0
  164. package/docs/bidi-features.md +462 -0
  165. package/docs/browser-api.md +673 -0
  166. package/docs/browser-context.md +522 -0
  167. package/docs/cli.md +233 -0
  168. package/docs/clock.md +245 -0
  169. package/docs/dialogs.md +143 -0
  170. package/docs/driver-configuration.md +64 -0
  171. package/docs/element-api.md +219 -0
  172. package/docs/emulation.md +155 -0
  173. package/docs/error-codes.md +67 -0
  174. package/docs/getting-started.md +168 -0
  175. package/docs/keyboard-mouse.md +250 -0
  176. package/docs/mcp.md +250 -0
  177. package/docs/mobile-emulation.md +194 -0
  178. package/docs/screenshots.md +146 -0
  179. package/docs/selectors.md +382 -0
  180. package/docs/session-management.md +229 -0
  181. package/docs/tracing.md +282 -0
  182. package/package.json +26 -8
  183. package/skills/craftdriver/SKILL.md +84 -0
  184. package/skills/craftdriver/cheatsheet.md +194 -0
  185. package/skills/craftdriver/cli.md +104 -0
  186. 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.