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,937 @@
1
+ /**
2
+ * BrowserContext — a WebDriver BiDi user context (an isolated browser
3
+ * profile, equivalent to "open a new incognito window").
4
+ *
5
+ * Each `BrowserContext` owns its own cookies, localStorage, and
6
+ * IndexedDB, isolated from every other context including the default
7
+ * one. Use it to run multi-user scenarios (log in as Alice in one
8
+ * context, as Bob in another) without cookie cross-talk.
9
+ *
10
+ * **BiDi-only.** Maps directly onto BiDi `browser.createUserContext`
11
+ * (§7.2.4.2) and friends. Classic WebDriver has no equivalent and
12
+ * `browser.newContext()` throws there.
13
+ *
14
+ * Obtain via:
15
+ * - `browser.newContext()` — create a fresh isolated profile.
16
+ * - `browser.contexts()` — list all open user contexts.
17
+ * - `browser.defaultContext` — the implicit context the browser
18
+ * started in (id = `'default'`).
19
+ */
20
+ import fs from 'node:fs/promises';
21
+ import { Page } from './page.js';
22
+ export class BrowserContext {
23
+ /** Monotonic counter for synthetic route ids returned by `route()`. */
24
+ static _routeCounter = 0;
25
+ driver;
26
+ conn;
27
+ getDefaultTimeout;
28
+ getNavigationTimeout;
29
+ _id;
30
+ _closed = false;
31
+ _hooks;
32
+ /** Per-context config (baseURL, extraHTTPHeaders). Read by Page for URL resolution. */
33
+ _config;
34
+ /**
35
+ * Preload-script id installed by {@link loadStorageState} to write
36
+ * localStorage entries on first navigation to each captured origin.
37
+ * Tracked so a subsequent call replaces (rather than stacks) the script.
38
+ */
39
+ _localStoragePreloadId;
40
+ /** Preload-script ids registered via {@link addInitScript}. */
41
+ _initScriptIds = new Set();
42
+ /**
43
+ * Per-route bookkeeping for `route()`. The synthetic id we return to
44
+ * the user maps to its pattern, handler, and the live BiDi intercept
45
+ * ids — one per page that currently belongs to this user context.
46
+ *
47
+ * BiDi `network.addIntercept` does not accept a user-context filter,
48
+ * so we register a separate intercept per page (with `contexts: [pageId]`)
49
+ * and add/remove them as pages open and close.
50
+ */
51
+ _routes = new Map();
52
+ /** Top-level browsing-context (page) ids currently inside this user context. */
53
+ _pageIds = new Set();
54
+ /**
55
+ * Page ids we've already fired `'page'` listeners for. Tracked
56
+ * separately from `_pageIds` so that the listener fires exactly once
57
+ * regardless of whether `newPage()` populates `_pageIds` before or
58
+ * after the `browsingContext.contextCreated` event arrives.
59
+ */
60
+ _notifiedPageIds = new Set();
61
+ /**
62
+ * The in-flight promise from the most recent call to {@link _ensurePageTracking}.
63
+ * Doubles as the "is tracking armed?" flag — truthy after the first call,
64
+ * cleared in {@link close}. Used to await readiness when `on('page')` is
65
+ * followed immediately by `newPage()` — Firefox fires `contextCreated`
66
+ * before `session.subscribe` round-trips, so we must defer page creation
67
+ * until the listener is attached.
68
+ */
69
+ _trackingPromise;
70
+ /** Unsubscribe handles for the contextCreated/Destroyed listeners. */
71
+ _trackingOffs = [];
72
+ _pageListeners = new Set();
73
+ _closeListeners = new Set();
74
+ constructor(driver, conn, userContextId, getDefaultTimeout, getNavigationTimeout, hooks, config = {}) {
75
+ this.driver = driver;
76
+ this.conn = conn;
77
+ this._id = userContextId;
78
+ this.getDefaultTimeout = getDefaultTimeout;
79
+ this.getNavigationTimeout = getNavigationTimeout;
80
+ this._hooks = hooks;
81
+ this._config = { ...config };
82
+ }
83
+ /** The BiDi user-context id. The implicit default context has id `'default'`. */
84
+ get id() {
85
+ return this._id;
86
+ }
87
+ /** True after `close()` has removed the context. */
88
+ get isClosed() {
89
+ return this._closed;
90
+ }
91
+ /**
92
+ * Base URL used to resolve relative paths in `page.navigateTo()`.
93
+ * Configured via `browser.newContext({ baseURL })`. Read by {@link Page}.
94
+ */
95
+ get baseURL() {
96
+ return this._config.baseURL;
97
+ }
98
+ assertOpen() {
99
+ if (this._closed) {
100
+ throw new Error(`BrowserContext "${this._id}" is closed; create a new one with browser.newContext().`);
101
+ }
102
+ }
103
+ /** BiDi partition descriptor for storage operations scoped to this user context. */
104
+ partition() {
105
+ return { type: 'storageKey', userContext: this._id };
106
+ }
107
+ /**
108
+ * Open a new top-level browsing context (a tab or a window) inside this
109
+ * user context. Maps to BiDi `browsingContext.create` with the
110
+ * `userContext` parameter bound to this id.
111
+ */
112
+ async newPage(opts) {
113
+ this.assertOpen();
114
+ // If any code path has armed page tracking (typically `on('page', …)`),
115
+ // wait for the subscription to be in place before we trigger
116
+ // `contextCreated` — Firefox emits the event before `session.subscribe`
117
+ // round-trips, and we must not lose it.
118
+ if (this._trackingPromise)
119
+ await this._trackingPromise;
120
+ const created = await this.conn.send('browsingContext.create', {
121
+ type: opts?.type ?? 'tab',
122
+ userContext: this._id,
123
+ });
124
+ const page = new Page(this.driver, created.context, this.getDefaultTimeout, this.conn, this);
125
+ // Track and apply extra headers before navigation so the first request carries them.
126
+ this._pageIds.add(created.context);
127
+ await this._applyExtraHeadersTo([created.context]);
128
+ // Register any existing routes on this new page before navigation.
129
+ for (const sid of this._routes.keys()) {
130
+ await this._registerRouteOnPage(sid, created.context).catch(() => { });
131
+ }
132
+ if (opts?.url) {
133
+ await page.navigateTo(opts.url);
134
+ }
135
+ return page;
136
+ }
137
+ /**
138
+ * Return all open top-level pages that belong to this user context.
139
+ */
140
+ async pages() {
141
+ this.assertOpen();
142
+ const tree = await this.conn.send('browsingContext.getTree', {});
143
+ return (tree.contexts ?? [])
144
+ .filter((c) => c.userContext === this._id && !c.parent)
145
+ .map((c) => new Page(this.driver, c.context, this.getDefaultTimeout, this.conn, this));
146
+ }
147
+ /**
148
+ * Run `action` and resolve to the next new top-level page that opens
149
+ * **inside this user context**. Useful when a click in one of this
150
+ * context's pages spawns a popup.
151
+ */
152
+ async waitForPage(action, opts) {
153
+ this.assertOpen();
154
+ const timeout = opts?.timeout ?? this.getNavigationTimeout();
155
+ // Make sure any prior `on('page')` subscription is fully installed before
156
+ // we run `action()` — otherwise a fast popup could fire before we listen.
157
+ if (this._trackingPromise)
158
+ await this._trackingPromise;
159
+ await this.conn.subscribe(['browsingContext.contextCreated']).catch(() => { });
160
+ return new Promise((resolve, reject) => {
161
+ const timer = setTimeout(() => {
162
+ off();
163
+ reject(new Error(`waitForPage() timed out after ${timeout}ms`));
164
+ }, timeout);
165
+ const off = this.conn.on('browsingContext.contextCreated', (params) => {
166
+ if (params.parent)
167
+ return; // nested frames
168
+ if (params.userContext !== this._id)
169
+ return;
170
+ clearTimeout(timer);
171
+ off();
172
+ resolve(new Page(this.driver, params.context, this.getDefaultTimeout, this.conn, this));
173
+ });
174
+ action().catch((err) => {
175
+ clearTimeout(timer);
176
+ off();
177
+ reject(err);
178
+ });
179
+ });
180
+ }
181
+ // ── Cookies ──────────────────────────────────────────────────────────────
182
+ /**
183
+ * Return cookies scoped to this user context.
184
+ *
185
+ * Optionally filter by URL(s): only cookies whose `domain`/`path` would
186
+ * be sent on a request to that URL are returned. Mirrors Playwright's
187
+ * `BrowserContext.cookies(urls)` shape.
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * const session = await alice.cookies(['https://app.example.com']);
192
+ * expect(session.find(c => c.name === 'sid')).toBeDefined();
193
+ * ```
194
+ */
195
+ async cookies(urls) {
196
+ this.assertOpen();
197
+ const result = await this.conn.send('storage.getCookies', {
198
+ partition: this.partition(),
199
+ });
200
+ const all = (result.cookies ?? []).map(normalizeCookie);
201
+ if (urls === undefined)
202
+ return all;
203
+ const urlList = Array.isArray(urls) ? urls : [urls];
204
+ const parsed = urlList.map((u) => new URL(u));
205
+ return all.filter((c) => parsed.some((u) => cookieMatchesUrl(c, u)));
206
+ }
207
+ /**
208
+ * Add cookies to this user context. The `domain` field is required —
209
+ * BiDi rejects host-less cookies. Use `expiry` (UNIX seconds) or omit
210
+ * for a session cookie.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * // Pre-seed an authenticated session before the first navigation.
215
+ * await alice.addCookies([
216
+ * { name: 'sid', value: 'eyJhbGciOi…', domain: 'app.example.com', path: '/', secure: true, sameSite: 'lax' },
217
+ * ]);
218
+ * ```
219
+ */
220
+ async addCookies(cookies) {
221
+ this.assertOpen();
222
+ for (const c of cookies) {
223
+ // BiDi rejects `sameSite: 'none'` without `secure: true`. Be loud
224
+ // instead of silently rewriting the cookie.
225
+ if (c.sameSite === 'none' && !c.secure) {
226
+ throw new Error(`addCookies: cookie "${c.name}" has sameSite "none" but secure is false. ` +
227
+ `Set secure: true or change sameSite to 'lax'/'strict'.`);
228
+ }
229
+ const partial = {
230
+ name: c.name,
231
+ value: typeof c.value === 'string' ? { type: 'string', value: c.value } : c.value,
232
+ domain: c.domain,
233
+ path: c.path,
234
+ httpOnly: c.httpOnly,
235
+ secure: c.secure,
236
+ sameSite: c.sameSite,
237
+ expiry: c.expiry,
238
+ };
239
+ await this.conn.send('storage.setCookie', {
240
+ cookie: partial,
241
+ partition: this.partition(),
242
+ });
243
+ }
244
+ }
245
+ /**
246
+ * Remove cookies from this user context.
247
+ *
248
+ * - No filter: clears every cookie in this context.
249
+ * - Filter fields are AND-combined: a cookie must match every provided
250
+ * field to be removed.
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * await alice.clearCookies({ name: 'sid' }); // sign Alice out
255
+ * await alice.clearCookies({ domain: 'tracker.io' }); // wipe a single host
256
+ * await alice.clearCookies(); // full reset
257
+ * ```
258
+ */
259
+ async clearCookies(filter) {
260
+ this.assertOpen();
261
+ await this.conn.send('storage.deleteCookies', {
262
+ filter: filter ?? {},
263
+ partition: this.partition(),
264
+ });
265
+ }
266
+ // ── Storage state (Playwright-shape JSON) ────────────────────────────────
267
+ /**
268
+ * Snapshot cookies + localStorage for this context.
269
+ *
270
+ * Returns `{ cookies, localStorage }` — a superset of Playwright's
271
+ * `storageState()` JSON shape, kept close enough that dumped files
272
+ * are interchangeable on a best-effort basis.
273
+ *
274
+ * **Limitation.** localStorage can only be read for origins that a page
275
+ * in this context is currently visiting — BiDi has no out-of-band
276
+ * storage read API. Open one page per origin you want captured before
277
+ * snapshotting.
278
+ */
279
+ async storageState(opts) {
280
+ this.assertOpen();
281
+ const state = {};
282
+ if (opts?.includeCookies !== false) {
283
+ state.cookies = await this.cookies();
284
+ }
285
+ if (opts?.includeLocalStorage !== false) {
286
+ const local = await this.collectLocalStorage();
287
+ if (Object.keys(local).length > 0) {
288
+ state.localStorage = local;
289
+ }
290
+ }
291
+ return state;
292
+ }
293
+ /**
294
+ * Snapshot the context's state and write it to `path` as JSON.
295
+ * Returns the snapshot for inspection.
296
+ *
297
+ * @example
298
+ * ```ts
299
+ * // tests/setup/login.ts — runs once, before the test suite.
300
+ * const alice = await browser.newContext();
301
+ * const page = await alice.newPage({ url: 'https://app.example.com/login' });
302
+ * await loginAs(page, 'alice');
303
+ * await alice.saveStorageState('auth/alice.json');
304
+ * await alice.close();
305
+ * ```
306
+ */
307
+ async saveStorageState(path, opts) {
308
+ const state = await this.storageState(opts);
309
+ await fs.writeFile(path, JSON.stringify(state, null, 2), 'utf-8');
310
+ return state;
311
+ }
312
+ /**
313
+ * Apply a previously-captured snapshot to this context.
314
+ *
315
+ * - Cookies are written immediately.
316
+ * - localStorage entries are installed via a preload script that runs
317
+ * on first navigation to each origin, before any page script. This
318
+ * matches the way Playwright restores localStorage and survives full
319
+ * reloads.
320
+ *
321
+ * Calling `loadStorageState` again replaces the previous preload
322
+ * script; it does not stack.
323
+ *
324
+ * @param source A `SessionState` object, or a path to a JSON file
325
+ * produced by {@link saveStorageState}.
326
+ */
327
+ async loadStorageState(source) {
328
+ this.assertOpen();
329
+ const state = typeof source === 'string'
330
+ ? JSON.parse(await fs.readFile(source, 'utf-8'))
331
+ : source;
332
+ if (state.cookies?.length) {
333
+ // Sanitize: a snapshot may contain cookies with sameSite:'none' and
334
+ // secure:false because some engines report that combo for cookies
335
+ // originally set via document.cookie. BiDi rejects it. Drop the
336
+ // sameSite field on those so the engine picks its default.
337
+ const sanitized = state.cookies.map((c) => {
338
+ if (c.sameSite === 'none' && !c.secure) {
339
+ const { sameSite: _drop, ...rest } = c;
340
+ return rest;
341
+ }
342
+ return c;
343
+ });
344
+ await this.addCookies(sanitized);
345
+ }
346
+ // Replace any previously-installed localStorage preload.
347
+ if (this._localStoragePreloadId) {
348
+ await this.conn
349
+ .send('script.removePreloadScript', { script: this._localStoragePreloadId })
350
+ .catch(() => { });
351
+ this._localStoragePreloadId = undefined;
352
+ }
353
+ if (state.localStorage && Object.keys(state.localStorage).length > 0) {
354
+ this._localStoragePreloadId = await this.installLocalStoragePreload(state.localStorage);
355
+ }
356
+ }
357
+ /**
358
+ * Remove this user context. All of its pages are closed and any
359
+ * subsequent operation on this `BrowserContext` instance throws.
360
+ * Fires the `'close'` event before tearing down.
361
+ *
362
+ * The default context (`id === 'default'`) cannot be removed.
363
+ */
364
+ async close() {
365
+ if (this._closed)
366
+ return;
367
+ if (this._id === 'default') {
368
+ throw new Error('Cannot close the default BrowserContext. ' +
369
+ 'Quit the browser instead with browser.quit().');
370
+ }
371
+ // Best-effort cleanup of init scripts and routes before the context
372
+ // goes away — keeps the browser-level interceptor map from leaking.
373
+ for (const id of this._initScriptIds) {
374
+ await this.conn.send('script.removePreloadScript', { script: id }).catch(() => { });
375
+ }
376
+ this._initScriptIds.clear();
377
+ if (this._localStoragePreloadId) {
378
+ await this.conn
379
+ .send('script.removePreloadScript', { script: this._localStoragePreloadId })
380
+ .catch(() => { });
381
+ this._localStoragePreloadId = undefined;
382
+ }
383
+ if (this._hooks && this._routes.size > 0) {
384
+ const net = this._hooks.getNetwork();
385
+ for (const entry of this._routes.values()) {
386
+ for (const realId of entry.perPage.values()) {
387
+ if (!realId)
388
+ continue;
389
+ await net.removeIntercept(realId).catch(() => { });
390
+ }
391
+ }
392
+ }
393
+ this._routes.clear();
394
+ for (const off of this._trackingOffs)
395
+ off();
396
+ this._trackingOffs = [];
397
+ this._trackingPromise = undefined;
398
+ await this.conn.send('browser.removeUserContext', { userContext: this._id });
399
+ this._closed = true;
400
+ // Fire close listeners last, after the context is fully gone.
401
+ for (const fn of this._closeListeners) {
402
+ try {
403
+ fn();
404
+ }
405
+ catch { /* listener errors must not break close() */ }
406
+ }
407
+ this._closeListeners.clear();
408
+ this._pageListeners.clear();
409
+ }
410
+ on(event, listener) {
411
+ if (event === 'close') {
412
+ this._closeListeners.add(listener);
413
+ return () => this._closeListeners.delete(listener);
414
+ }
415
+ this._pageListeners.add(listener);
416
+ // Page tracking is what powers the 'page' event AND `route` membership
417
+ // filtering. Start it lazily on first listener.
418
+ this._ensurePageTracking().catch(() => { });
419
+ return () => this._pageListeners.delete(listener);
420
+ }
421
+ off(event, listener) {
422
+ if (event === 'close')
423
+ this._closeListeners.delete(listener);
424
+ else
425
+ this._pageListeners.delete(listener);
426
+ }
427
+ // ── Init scripts ─────────────────────────────────────────────────────────
428
+ /**
429
+ * Register a script that runs in every page of this context, **before
430
+ * any page script**, on every navigation including popups. Scoped to
431
+ * this user context via BiDi `script.addPreloadScript`'s `userContexts`
432
+ * parameter.
433
+ *
434
+ * Use it for the "every page in this profile gets this hook" patterns:
435
+ * feature flags, fake clocks, auth shims, deterministic Math.random.
436
+ *
437
+ * @example
438
+ * ```ts
439
+ * // Mark every page as running under E2E so the app can skip
440
+ * // animations and disable analytics — no per-page wiring needed.
441
+ * await ctx.addInitScript(() => {
442
+ * window.__E2E__ = true;
443
+ * });
444
+ * ```
445
+ */
446
+ async addInitScript(script) {
447
+ this.assertOpen();
448
+ const fnDecl = typeof script === 'function' ? script.toString() : `() => { ${script} }`;
449
+ const result = await this.conn.send('script.addPreloadScript', {
450
+ functionDeclaration: fnDecl,
451
+ userContexts: [this._id],
452
+ });
453
+ this._initScriptIds.add(result.script);
454
+ return {
455
+ id: result.script,
456
+ remove: async () => {
457
+ if (!this._initScriptIds.has(result.script))
458
+ return;
459
+ this._initScriptIds.delete(result.script);
460
+ await this.conn
461
+ .send('script.removePreloadScript', { script: result.script })
462
+ .catch(() => { });
463
+ },
464
+ };
465
+ }
466
+ /**
467
+ * Remove an init script by its handle id. Alternative to calling
468
+ * `handle.remove()` on the value returned by {@link addInitScript}.
469
+ */
470
+ async removeInitScript(id) {
471
+ if (!this._initScriptIds.has(id))
472
+ return;
473
+ this._initScriptIds.delete(id);
474
+ await this.conn.send('script.removePreloadScript', { script: id }).catch(() => { });
475
+ }
476
+ // ── Routing ──────────────────────────────────────────────────────────────
477
+ /**
478
+ * Intercept network requests originating from any page in this
479
+ * context. The handler runs for every matching request and decides
480
+ * how to answer it (`fulfill` / `continue` / `abort`).
481
+ *
482
+ * Routes are registered as a separate BiDi `network.addIntercept`
483
+ * per page, scoped to that page's browsing-context id. Pages opened
484
+ * later inherit every route automatically; closed pages have their
485
+ * intercept removed. Requests from *other* contexts are not seen.
486
+ *
487
+ * @example
488
+ * ```ts
489
+ * // Mock the authenticated user endpoint just for Alice's context.
490
+ * await alice.route('**\/api/me', (req) => ({
491
+ * status: 200,
492
+ * headers: { 'content-type': 'application/json' },
493
+ * body: { id: 1, name: 'Alice' },
494
+ * }));
495
+ * ```
496
+ */
497
+ async route(pattern, handler) {
498
+ this.assertOpen();
499
+ if (!this._hooks) {
500
+ throw new Error('ctx.route() requires a network interceptor — this BrowserContext was ' +
501
+ 'constructed without one. Create the context via browser.newContext().');
502
+ }
503
+ await this._ensurePageTracking();
504
+ // Synthetic id returned to the user. Stable across page churn.
505
+ const syntheticId = `ctx-route-${++BrowserContext._routeCounter}`;
506
+ const entry = { pattern, handler, perPage: new Map() };
507
+ this._routes.set(syntheticId, entry);
508
+ // Register intercept on every page that already exists in this context.
509
+ await Promise.all([...this._pageIds].map((pageId) => this._registerRouteOnPage(syntheticId, pageId).catch(() => { })));
510
+ return syntheticId;
511
+ }
512
+ /**
513
+ * Remove a route previously registered with {@link route}. With no
514
+ * argument, removes every route registered on this context.
515
+ *
516
+ * @example
517
+ * ```ts
518
+ * const id = await ctx.route('**\/api/feature-flags', mockFlags);
519
+ * // …test runs…
520
+ * await ctx.unroute(id);
521
+ * ```
522
+ */
523
+ async unroute(id) {
524
+ if (!this._hooks)
525
+ return;
526
+ const net = this._hooks.getNetwork();
527
+ const remove = async (syntheticId) => {
528
+ const entry = this._routes.get(syntheticId);
529
+ if (!entry)
530
+ return;
531
+ this._routes.delete(syntheticId);
532
+ for (const realId of entry.perPage.values()) {
533
+ if (!realId)
534
+ continue; // placeholder for an in-flight registration
535
+ await net.removeIntercept(realId).catch(() => { });
536
+ }
537
+ };
538
+ if (id) {
539
+ await remove(id);
540
+ return;
541
+ }
542
+ for (const sid of [...this._routes.keys()]) {
543
+ await remove(sid);
544
+ }
545
+ }
546
+ // ── Extra HTTP headers ───────────────────────────────────────────────────
547
+ /**
548
+ * Replace this context's extra-HTTP-headers map. Applied to every
549
+ * currently-open page in this context, and re-applied to each new page
550
+ * that opens afterwards.
551
+ *
552
+ * Pass `{}` to clear.
553
+ *
554
+ * @example
555
+ * ```ts
556
+ * await alice.setExtraHTTPHeaders({ 'X-Tenant': 'acme', 'X-Test': 'true' });
557
+ * ```
558
+ */
559
+ async setExtraHTTPHeaders(headers) {
560
+ this.assertOpen();
561
+ this._config.extraHTTPHeaders = { ...headers };
562
+ await this._ensurePageTracking();
563
+ if (this._pageIds.size > 0) {
564
+ await this._applyExtraHeadersTo([...this._pageIds]);
565
+ }
566
+ }
567
+ // ── Identity & device emulation (Milestone C) ────────────────────────────
568
+ /**
569
+ * Override the locale reported to every page in this context.
570
+ *
571
+ * Mapped to BiDi `emulation.setLocaleOverride` scoped to
572
+ * `userContexts: [this.id]`, so the override applies to every page
573
+ * currently open in this context **and** every page opened later.
574
+ * Pass `null` to clear the override.
575
+ *
576
+ * Cross-browser: works on Chrome and Firefox.
577
+ *
578
+ * @example
579
+ * ```ts
580
+ * // Render the UI in German for a checkout-translation test.
581
+ * await ctx.setLocale('de-DE');
582
+ * const page = await ctx.newPage({ url: '/checkout' });
583
+ * await page.expect(page.find('h1')).toContainText('Kasse');
584
+ * ```
585
+ */
586
+ async setLocale(locale) {
587
+ this.assertOpen();
588
+ await this.conn.send('emulation.setLocaleOverride', {
589
+ userContexts: [this._id],
590
+ locale,
591
+ });
592
+ }
593
+ /**
594
+ * Override the timezone reported to every page in this context.
595
+ *
596
+ * Mapped to BiDi `emulation.setTimezoneOverride` scoped to
597
+ * `userContexts: [this.id]`. Pass `null` to clear the override.
598
+ * Accepts any IANA timezone id (e.g. `'Europe/Berlin'`,
599
+ * `'America/Los_Angeles'`).
600
+ *
601
+ * Cross-browser: works on Chrome and Firefox.
602
+ *
603
+ * @example
604
+ * ```ts
605
+ * await ctx.setTimezone('Asia/Tokyo');
606
+ * const page = await ctx.newPage({ url: '/dashboard' });
607
+ * // `new Date().getTimezoneOffset()` in the page now reflects JST.
608
+ * ```
609
+ */
610
+ async setTimezone(timezoneId) {
611
+ this.assertOpen();
612
+ await this.conn.send('emulation.setTimezoneOverride', {
613
+ userContexts: [this._id],
614
+ timezone: timezoneId,
615
+ });
616
+ }
617
+ /**
618
+ * Override `navigator.geolocation` for every page in this context.
619
+ *
620
+ * Mapped to BiDi `emulation.setGeolocationOverride` scoped to
621
+ * `userContexts: [this.id]`. Pass `null` to clear and return to the
622
+ * real device location.
623
+ *
624
+ * The page still has to be granted the `geolocation` permission \u2014 use
625
+ * {@link grantPermissions} once the origin is known.
626
+ *
627
+ * @example
628
+ * ```ts
629
+ * await ctx.setGeolocation({ latitude: 51.5074, longitude: -0.1278 });
630
+ * const page = await ctx.newPage({ url: 'https://app.example.com' });
631
+ * await ctx.grantPermissions(['geolocation'], { origin: 'https://app.example.com' });
632
+ * ```
633
+ */
634
+ async setGeolocation(coords) {
635
+ this.assertOpen();
636
+ const params = { userContexts: [this._id] };
637
+ if (coords === null) {
638
+ params.coordinates = null;
639
+ }
640
+ else {
641
+ if (typeof coords.latitude !== 'number' ||
642
+ typeof coords.longitude !== 'number' ||
643
+ coords.latitude < -90 || coords.latitude > 90 ||
644
+ coords.longitude < -180 || coords.longitude > 180) {
645
+ throw new Error(`setGeolocation: invalid coordinates ${JSON.stringify(coords)}. ` +
646
+ 'latitude must be in [-90, 90] and longitude in [-180, 180].');
647
+ }
648
+ params.coordinates = {
649
+ latitude: coords.latitude,
650
+ longitude: coords.longitude,
651
+ accuracy: coords.accuracy ?? 1,
652
+ };
653
+ }
654
+ try {
655
+ await this.conn.send('emulation.setGeolocationOverride', params);
656
+ }
657
+ catch (err) {
658
+ const engine = this._hooks?.getBrowserName?.() ?? 'this browser';
659
+ throw new Error(`setGeolocation() failed on ${engine}: ${err.message}. ` +
660
+ 'BiDi `emulation.setGeolocationOverride` coverage is still uneven ' +
661
+ 'across engines; Chrome is the reliable target today.');
662
+ }
663
+ }
664
+ /**
665
+ * Grant (or deny) one or more permissions for an origin in this context.
666
+ *
667
+ * Mapped to BiDi `permissions.setPermission` with the `userContext`
668
+ * parameter, so the grant only affects pages inside **this** context.
669
+ * The set of valid permission names is the W3C Permissions catalogue
670
+ * (`'geolocation'`, `'notifications'`, `'clipboard-read'`,
671
+ * `'clipboard-write'`, `'camera'`, `'microphone'`, \u2026).
672
+ *
673
+ * `opts.origin` is required \u2014 BiDi has no "all origins" wildcard.
674
+ * `opts.state` defaults to `'granted'`.
675
+ *
676
+ * @example
677
+ * ```ts
678
+ * await ctx.grantPermissions(['geolocation'], { origin: 'https://app.example.com' });
679
+ * await ctx.grantPermissions(['notifications'], {
680
+ * origin: 'https://app.example.com',
681
+ * state: 'denied',
682
+ * });
683
+ * ```
684
+ */
685
+ async grantPermissions(permissions, opts) {
686
+ this.assertOpen();
687
+ if (!Array.isArray(permissions) || permissions.length === 0) {
688
+ throw new Error('grantPermissions: pass a non-empty array of permission names.');
689
+ }
690
+ if (!opts || typeof opts.origin !== 'string' || opts.origin.length === 0) {
691
+ throw new Error('grantPermissions: `origin` is required \u2014 BiDi `permissions.setPermission` ' +
692
+ 'has no "all origins" wildcard.');
693
+ }
694
+ const state = opts.state ?? 'granted';
695
+ for (const name of permissions) {
696
+ try {
697
+ await this.conn.send('permissions.setPermission', {
698
+ descriptor: { name },
699
+ state,
700
+ origin: opts.origin,
701
+ userContext: this._id,
702
+ });
703
+ }
704
+ catch (err) {
705
+ const engine = this._hooks?.getBrowserName?.() ?? 'this browser';
706
+ throw new Error(`grantPermissions(${JSON.stringify(name)}) failed on ${engine}: ` +
707
+ `${err.message}.`);
708
+ }
709
+ }
710
+ }
711
+ /**
712
+ * Reset the listed permissions for an origin back to the browser
713
+ * default (`'prompt'`). Convenience wrapper around
714
+ * {@link grantPermissions} with `state: 'prompt'`.
715
+ */
716
+ async clearPermissions(permissions, opts) {
717
+ await this.grantPermissions(permissions, { ...opts, state: 'prompt' });
718
+ }
719
+ // ── Internal helpers ─────────────────────────────────────────────────────
720
+ /**
721
+ * Read localStorage for every origin currently open in this context.
722
+ * Returns `{ origin: { key: value, ... }, ... }`.
723
+ */
724
+ async collectLocalStorage() {
725
+ const pages = await this.pages();
726
+ const byOrigin = {};
727
+ for (const page of pages) {
728
+ try {
729
+ const url = await page.url();
730
+ if (!url || url === 'about:blank')
731
+ continue;
732
+ const origin = new URL(url).origin;
733
+ if (byOrigin[origin])
734
+ continue; // first page wins
735
+ const entries = await page.evaluate(`const out = {};
736
+ for (let i = 0; i < localStorage.length; i++) {
737
+ const k = localStorage.key(i);
738
+ if (k !== null) out[k] = localStorage.getItem(k) ?? '';
739
+ }
740
+ return out;`);
741
+ if (entries && Object.keys(entries).length > 0) {
742
+ byOrigin[origin] = entries;
743
+ }
744
+ }
745
+ catch {
746
+ // Page may have been closed mid-snapshot; skip.
747
+ }
748
+ }
749
+ return byOrigin;
750
+ }
751
+ /**
752
+ * Install a preload script (scoped to this user context) that, on first
753
+ * navigation to each captured origin, writes the localStorage entries
754
+ * before any page script runs.
755
+ */
756
+ async installLocalStoragePreload(localStorage) {
757
+ // Serialize the origin → entries map into the function body. Safe because
758
+ // we control the JSON shape (strings only).
759
+ const payload = JSON.stringify(localStorage);
760
+ const fnSrc = `() => {
761
+ try {
762
+ const data = ${payload};
763
+ const entries = data[location.origin];
764
+ if (!entries) return;
765
+ for (const k in entries) {
766
+ try { window.localStorage.setItem(k, entries[k]); } catch (e) { /* quota / opaque origin */ }
767
+ }
768
+ } catch (e) { /* never break user pages */ }
769
+ }`;
770
+ const result = await this.conn.send('script.addPreloadScript', {
771
+ functionDeclaration: fnSrc,
772
+ userContexts: [this._id],
773
+ });
774
+ return result.script;
775
+ }
776
+ /**
777
+ * Subscribe (once) to BiDi page-creation / page-destruction events and
778
+ * seed `_pageIds` with currently-open pages in this context. Powers
779
+ * both the `'page'` event and route membership filtering.
780
+ */
781
+ _ensurePageTracking() {
782
+ if (this._closed)
783
+ return Promise.resolve();
784
+ if (this._trackingPromise)
785
+ return this._trackingPromise;
786
+ this._trackingPromise = this._startPageTracking();
787
+ return this._trackingPromise;
788
+ }
789
+ async _startPageTracking() {
790
+ await this.conn
791
+ .subscribe(['browsingContext.contextCreated', 'browsingContext.contextDestroyed'])
792
+ .catch(() => { });
793
+ const onCreated = (params) => {
794
+ if (params.parent)
795
+ return; // nested frames
796
+ if (params.userContext !== this._id)
797
+ return;
798
+ const id = params.context;
799
+ this._pageIds.add(id);
800
+ // Headers must be applied before the first request from this page.
801
+ this._applyExtraHeadersTo([id]).catch(() => { });
802
+ // Register every existing route on the new page.
803
+ for (const sid of this._routes.keys()) {
804
+ this._registerRouteOnPage(sid, id).catch(() => { });
805
+ }
806
+ // Notify page listeners exactly once per page.
807
+ if (this._notifiedPageIds.has(id))
808
+ return;
809
+ this._notifiedPageIds.add(id);
810
+ if (this._pageListeners.size > 0) {
811
+ const page = new Page(this.driver, id, this.getDefaultTimeout, this.conn, this);
812
+ for (const fn of this._pageListeners) {
813
+ try {
814
+ fn(page);
815
+ }
816
+ catch { /* listener errors must not break events */ }
817
+ }
818
+ }
819
+ };
820
+ const onDestroyed = (params) => {
821
+ if (params.parent)
822
+ return;
823
+ const id = params.context;
824
+ this._pageIds.delete(id);
825
+ this._notifiedPageIds.delete(id);
826
+ // Tear down any per-page intercepts registered against this page.
827
+ const net = this._hooks?.getNetwork();
828
+ for (const entry of this._routes.values()) {
829
+ const realId = entry.perPage.get(id);
830
+ if (realId && net) {
831
+ entry.perPage.delete(id);
832
+ net.removeIntercept(realId).catch(() => { });
833
+ }
834
+ else if (realId === '') {
835
+ // In-flight registration — drop the placeholder; the registration
836
+ // will detect the missing page and clean itself up.
837
+ entry.perPage.delete(id);
838
+ }
839
+ }
840
+ };
841
+ this._trackingOffs.push(this.conn.on('browsingContext.contextCreated', onCreated));
842
+ this._trackingOffs.push(this.conn.on('browsingContext.contextDestroyed', onDestroyed));
843
+ // Seed with currently-open pages AFTER handlers are attached so we don't
844
+ // miss a contextCreated event that races with this seeding.
845
+ try {
846
+ const tree = await this.conn.send('browsingContext.getTree', {});
847
+ for (const c of tree.contexts ?? []) {
848
+ if (c.userContext === this._id && !c.parent) {
849
+ this._pageIds.add(c.context);
850
+ }
851
+ }
852
+ }
853
+ catch { /* not fatal — set will fill from events */ }
854
+ }
855
+ /**
856
+ * Push the current `extraHTTPHeaders` onto a specific set of page ids.
857
+ * No-op when no headers are configured.
858
+ */
859
+ async _applyExtraHeadersTo(contexts) {
860
+ const headers = this._config.extraHTTPHeaders;
861
+ if (!headers || Object.keys(headers).length === 0 || contexts.length === 0)
862
+ return;
863
+ if (!this._hooks)
864
+ return;
865
+ await this._hooks.getNetwork().setExtraHeaders(headers, contexts);
866
+ }
867
+ /**
868
+ * Register a single route's BiDi intercept against one page id. Records
869
+ * the live intercept id in the route's `perPage` map so it can be
870
+ * cleaned up when the page closes or the route is removed.
871
+ *
872
+ * Reserves the slot synchronously (before awaiting `intercept`) so that
873
+ * concurrent callers — typically `newPage()` and the `contextCreated`
874
+ * event firing for the same page — don't both create an intercept.
875
+ */
876
+ async _registerRouteOnPage(syntheticId, pageId) {
877
+ const entry = this._routes.get(syntheticId);
878
+ if (!entry || !this._hooks)
879
+ return;
880
+ if (entry.perPage.has(pageId))
881
+ return;
882
+ // Reserve before the await with a placeholder so a parallel registration
883
+ // call observes `perPage.has(pageId)` === true and bails out early.
884
+ entry.perPage.set(pageId, '');
885
+ try {
886
+ const realId = await this._hooks.getNetwork().intercept(entry.pattern, entry.handler, ['beforeRequestSent'], [pageId]);
887
+ // The route may have been unrouted, or the page destroyed (which
888
+ // clears the placeholder via `onDestroyed`), while we were awaiting.
889
+ // In either case, drop the intercept and leave no stale entry behind.
890
+ if (!this._routes.has(syntheticId) || !this._pageIds.has(pageId)) {
891
+ entry.perPage.delete(pageId);
892
+ await this._hooks.getNetwork().removeIntercept(realId).catch(() => { });
893
+ return;
894
+ }
895
+ entry.perPage.set(pageId, realId);
896
+ }
897
+ catch (err) {
898
+ // Roll back the placeholder so a later retry can succeed.
899
+ if (entry.perPage.get(pageId) === '')
900
+ entry.perPage.delete(pageId);
901
+ throw err;
902
+ }
903
+ }
904
+ }
905
+ // ── Module-private helpers ─────────────────────────────────────────────────
906
+ function normalizeCookie(raw) {
907
+ return {
908
+ name: raw.name,
909
+ value: typeof raw.value === 'string' ? raw.value : raw.value.value,
910
+ domain: raw.domain,
911
+ path: raw.path,
912
+ size: raw.size,
913
+ httpOnly: raw.httpOnly,
914
+ secure: raw.secure,
915
+ sameSite: raw.sameSite,
916
+ expiry: raw.expiry,
917
+ };
918
+ }
919
+ /** Would `cookie` be sent on a request to `url`? RFC-6265 domain/path match (best effort). */
920
+ function cookieMatchesUrl(cookie, url) {
921
+ const cookieDomain = cookie.domain.replace(/^\./, '').toLowerCase();
922
+ const reqHost = url.hostname.toLowerCase();
923
+ const domainOk = reqHost === cookieDomain || reqHost.endsWith('.' + cookieDomain);
924
+ if (!domainOk)
925
+ return false;
926
+ const cookiePath = cookie.path || '/';
927
+ const reqPath = url.pathname || '/';
928
+ const pathOk = cookiePath === '/' ||
929
+ reqPath === cookiePath ||
930
+ reqPath.startsWith(cookiePath.endsWith('/') ? cookiePath : cookiePath + '/');
931
+ if (!pathOk)
932
+ return false;
933
+ if (cookie.secure && url.protocol !== 'https:')
934
+ return false;
935
+ return true;
936
+ }
937
+ //# sourceMappingURL=browserContext.js.map