craftdriver 1.7.0 → 1.9.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 +25 -12
- package/README.md +16 -6
- package/dist/cli/agentSession.d.ts +62 -0
- package/dist/cli/agentSession.d.ts.map +1 -0
- package/dist/cli/agentSession.js +100 -0
- package/dist/cli/agentSession.js.map +1 -0
- package/dist/cli/artifactPaths.d.ts +42 -0
- package/dist/cli/artifactPaths.d.ts.map +1 -0
- package/dist/cli/artifactPaths.js +144 -0
- package/dist/cli/artifactPaths.js.map +1 -0
- package/dist/cli/bounds.d.ts +88 -0
- package/dist/cli/bounds.d.ts.map +1 -0
- package/dist/cli/bounds.js +128 -0
- package/dist/cli/bounds.js.map +1 -0
- package/dist/cli/client.d.ts +3 -0
- package/dist/cli/client.d.ts.map +1 -1
- package/dist/cli/client.js +24 -8
- package/dist/cli/client.js.map +1 -1
- package/dist/cli/daemon.d.ts +3 -0
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +152 -39
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/defaults.d.ts +23 -3
- package/dist/cli/defaults.d.ts.map +1 -1
- package/dist/cli/defaults.js +50 -5
- package/dist/cli/defaults.js.map +1 -1
- package/dist/cli/dispatcher.d.ts +54 -11
- package/dist/cli/dispatcher.d.ts.map +1 -1
- package/dist/cli/dispatcher.js +851 -54
- package/dist/cli/dispatcher.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +305 -56
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +14 -14
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +328 -180
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/journal.d.ts +153 -0
- package/dist/cli/journal.d.ts.map +1 -0
- package/dist/cli/journal.js +252 -0
- package/dist/cli/journal.js.map +1 -0
- package/dist/cli/lineReader.d.ts +33 -0
- package/dist/cli/lineReader.d.ts.map +1 -0
- package/dist/cli/lineReader.js +66 -0
- package/dist/cli/lineReader.js.map +1 -0
- package/dist/cli/locatorCandidates.d.ts +44 -0
- package/dist/cli/locatorCandidates.d.ts.map +1 -0
- package/dist/cli/locatorCandidates.js +190 -0
- package/dist/cli/locatorCandidates.js.map +1 -0
- package/dist/cli/mcp/artifacts.d.ts +39 -1
- package/dist/cli/mcp/artifacts.d.ts.map +1 -1
- package/dist/cli/mcp/artifacts.js +104 -9
- package/dist/cli/mcp/artifacts.js.map +1 -1
- package/dist/cli/mcp/bounds.d.ts +65 -0
- package/dist/cli/mcp/bounds.d.ts.map +1 -0
- package/dist/cli/mcp/bounds.js +124 -0
- package/dist/cli/mcp/bounds.js.map +1 -0
- package/dist/cli/mcp/params.d.ts +38 -0
- package/dist/cli/mcp/params.d.ts.map +1 -0
- package/dist/cli/mcp/params.js +174 -0
- package/dist/cli/mcp/params.js.map +1 -0
- package/dist/cli/mcp/server.d.ts +94 -16
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/server.js +304 -150
- package/dist/cli/mcp/server.js.map +1 -1
- package/dist/cli/mcp/tools.d.ts +62 -17
- package/dist/cli/mcp/tools.d.ts.map +1 -1
- package/dist/cli/mcp/tools.js +373 -158
- package/dist/cli/mcp/tools.js.map +1 -1
- package/dist/cli/pageSemantics.d.ts +6 -0
- package/dist/cli/pageSemantics.d.ts.map +1 -0
- package/dist/cli/pageSemantics.js +6 -0
- package/dist/cli/pageSemantics.js.map +1 -0
- package/dist/cli/parseArgs.d.ts +6 -18
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +689 -45
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/cli/protocol.d.ts +8 -0
- package/dist/cli/protocol.d.ts.map +1 -1
- package/dist/cli/selector.d.ts +3 -3
- package/dist/cli/selector.d.ts.map +1 -1
- package/dist/cli/selector.js +6 -9
- package/dist/cli/selector.js.map +1 -1
- package/dist/cli/sessionRegistry.d.ts +42 -0
- package/dist/cli/sessionRegistry.d.ts.map +1 -0
- package/dist/cli/sessionRegistry.js +99 -0
- package/dist/cli/sessionRegistry.js.map +1 -0
- package/dist/cli/snapshot.d.ts +100 -19
- package/dist/cli/snapshot.d.ts.map +1 -1
- package/dist/cli/snapshot.js +295 -108
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/cli/stateStore.d.ts +50 -0
- package/dist/cli/stateStore.d.ts.map +1 -0
- package/dist/cli/stateStore.js +122 -0
- package/dist/cli/stateStore.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/a11y.d.ts +6 -2
- package/dist/lib/a11y.d.ts.map +1 -1
- package/dist/lib/a11y.js +3 -1
- package/dist/lib/a11y.js.map +1 -1
- package/dist/lib/bidi/evaluate.d.ts +11 -0
- package/dist/lib/bidi/evaluate.d.ts.map +1 -0
- package/dist/lib/bidi/evaluate.js +25 -0
- package/dist/lib/bidi/evaluate.js.map +1 -0
- package/dist/lib/bidi/index.d.ts +1 -1
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +2 -2
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts +3 -0
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +7 -0
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/bidi/network.d.ts +14 -1
- package/dist/lib/bidi/network.d.ts.map +1 -1
- package/dist/lib/bidi/network.js +45 -7
- package/dist/lib/bidi/network.js.map +1 -1
- package/dist/lib/bidi/storage.d.ts +30 -6
- package/dist/lib/bidi/storage.d.ts.map +1 -1
- package/dist/lib/bidi/storage.js +190 -45
- package/dist/lib/bidi/storage.js.map +1 -1
- package/dist/lib/bidi/types.d.ts +9 -2
- package/dist/lib/bidi/types.d.ts.map +1 -1
- package/dist/lib/browser.d.ts +7 -4
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +186 -65
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +58 -24
- package/dist/lib/browserContext.d.ts.map +1 -1
- package/dist/lib/browserContext.js +300 -99
- package/dist/lib/browserContext.js.map +1 -1
- package/dist/lib/by.d.ts +68 -30
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +54 -23
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/clearFastPath.d.ts.map +1 -1
- package/dist/lib/clearFastPath.js +2 -0
- package/dist/lib/clearFastPath.js.map +1 -1
- package/dist/lib/clickFastPath.d.ts.map +1 -1
- package/dist/lib/clickFastPath.js +2 -0
- package/dist/lib/clickFastPath.js.map +1 -1
- package/dist/lib/driver.d.ts +26 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +123 -4
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/elementHandle.d.ts +13 -10
- package/dist/lib/elementHandle.d.ts.map +1 -1
- package/dist/lib/elementHandle.js +119 -50
- package/dist/lib/elementHandle.js.map +1 -1
- package/dist/lib/errors.d.ts +12 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +12 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/expect.d.ts +10 -0
- package/dist/lib/expect.d.ts.map +1 -1
- package/dist/lib/expect.js +76 -49
- package/dist/lib/expect.js.map +1 -1
- package/dist/lib/fillFastPath.d.ts.map +1 -1
- package/dist/lib/fillFastPath.js +2 -0
- package/dist/lib/fillFastPath.js.map +1 -1
- package/dist/lib/frame.d.ts.map +1 -1
- package/dist/lib/frame.js +29 -17
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/initScript.d.ts +9 -0
- package/dist/lib/initScript.d.ts.map +1 -0
- package/dist/lib/initScript.js +14 -0
- package/dist/lib/initScript.js.map +1 -0
- package/dist/lib/locator.d.ts +21 -54
- package/dist/lib/locator.d.ts.map +1 -1
- package/dist/lib/locator.js +165 -208
- package/dist/lib/locator.js.map +1 -1
- package/dist/lib/page.d.ts +22 -0
- package/dist/lib/page.d.ts.map +1 -1
- package/dist/lib/page.js +71 -13
- package/dist/lib/page.js.map +1 -1
- package/dist/lib/pageSemantics.d.ts +42 -0
- package/dist/lib/pageSemantics.d.ts.map +1 -0
- package/dist/lib/pageSemantics.js +216 -0
- package/dist/lib/pageSemantics.js.map +1 -0
- package/dist/lib/query.d.ts +68 -0
- package/dist/lib/query.d.ts.map +1 -0
- package/dist/lib/query.js +590 -0
- package/dist/lib/query.js.map +1 -0
- package/dist/lib/secureFile.d.ts +8 -0
- package/dist/lib/secureFile.d.ts.map +1 -0
- package/dist/lib/secureFile.js +67 -0
- package/dist/lib/secureFile.js.map +1 -0
- package/dist/lib/sessionStateValidation.d.ts +35 -0
- package/dist/lib/sessionStateValidation.d.ts.map +1 -0
- package/dist/lib/sessionStateValidation.js +223 -0
- package/dist/lib/sessionStateValidation.js.map +1 -0
- package/dist/lib/shadowRoot.d.ts +21 -0
- package/dist/lib/shadowRoot.d.ts.map +1 -0
- package/dist/lib/shadowRoot.js +29 -0
- package/dist/lib/shadowRoot.js.map +1 -0
- package/dist/lib/shadowRootLocator.d.ts +31 -0
- package/dist/lib/shadowRootLocator.d.ts.map +1 -0
- package/dist/lib/shadowRootLocator.js +55 -0
- package/dist/lib/shadowRootLocator.js.map +1 -0
- package/dist/lib/timing.d.ts +2 -2
- package/dist/lib/timing.js +2 -2
- package/dist/lib/tracing.d.ts +2 -1
- package/dist/lib/tracing.d.ts.map +1 -1
- package/dist/lib/tracing.js +12 -1
- package/dist/lib/tracing.js.map +1 -1
- package/docs/accessibility.md +9 -1
- package/docs/agents.md +56 -61
- package/docs/api-reference.md +5 -1
- package/docs/browser-api.md +27 -7
- package/docs/browser-context.md +21 -15
- package/docs/cli.md +353 -30
- package/docs/error-codes.md +7 -4
- package/docs/getting-started.md +6 -1
- package/docs/index.md +1 -1
- package/docs/mcp.md +136 -65
- package/docs/public/examples/agent-actions.html +48 -0
- package/docs/public/examples/agent-debug.html +86 -0
- package/docs/public/examples/agent-late-mutation.html +18 -0
- package/docs/public/examples/agent-ref-shift.html +33 -0
- package/docs/public/examples/iframe-child.html +12 -1
- package/docs/public/examples/shadow-dom.html +137 -0
- package/docs/recipes/login-once-reuse-session.md +33 -5
- package/docs/selectors.md +61 -0
- package/docs/session-management.md +80 -12
- package/docs/why-craftdriver.md +1 -1
- package/package.json +5 -3
- package/skills/craftdriver/SKILL.md +59 -75
- package/skills/craftdriver/agents/openai.yaml +4 -0
- package/skills/craftdriver/cheatsheet.md +18 -4
- package/skills/craftdriver/cli.md +194 -68
- package/skills/craftdriver/patterns.md +17 -8
- package/skills/craftdriver/workflow.md +149 -0
|
@@ -17,8 +17,11 @@
|
|
|
17
17
|
* - `browser.defaultContext` — the implicit context the browser
|
|
18
18
|
* started in (id = `'default'`).
|
|
19
19
|
*/
|
|
20
|
-
import
|
|
20
|
+
import { writeSecureFile } from './secureFile.js';
|
|
21
|
+
import { CraftdriverError, ErrorCode } from './errors.js';
|
|
22
|
+
import { parseSessionState, hasNonEmptySessionStorage, isStorageStateError, nonEmptyOrigins, normalizeCookieForRestore, storageStateDetail, } from './sessionStateValidation.js';
|
|
21
23
|
import { Page } from './page.js';
|
|
24
|
+
import { AUTH_STATE_HYDRATE_PATH, publicPageInitScript } from './initScript.js';
|
|
22
25
|
export class BrowserContext {
|
|
23
26
|
/** Monotonic counter for synthetic route ids returned by `route()`. */
|
|
24
27
|
static _routeCounter = 0;
|
|
@@ -31,12 +34,6 @@ export class BrowserContext {
|
|
|
31
34
|
_hooks;
|
|
32
35
|
/** Per-context config (baseURL, extraHTTPHeaders). Read by Page for URL resolution. */
|
|
33
36
|
_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
37
|
/** Preload-script ids registered via {@link addInitScript}. */
|
|
41
38
|
_initScriptIds = new Set();
|
|
42
39
|
/**
|
|
@@ -58,6 +55,23 @@ export class BrowserContext {
|
|
|
58
55
|
* after the `browsingContext.contextCreated` event arrives.
|
|
59
56
|
*/
|
|
60
57
|
_notifiedPageIds = new Set();
|
|
58
|
+
/**
|
|
59
|
+
* Ids of internal (hydration) top-level contexts that must stay invisible to
|
|
60
|
+
* page tracking — never surfaced as a `'page'`, added to `_pageIds`, or given
|
|
61
|
+
* user routes/headers. See {@link _hydrateLocalStorage}.
|
|
62
|
+
*/
|
|
63
|
+
_internalPageIds = new Set();
|
|
64
|
+
/**
|
|
65
|
+
* `> 0` while an internal `browsingContext.create` is awaiting its id.
|
|
66
|
+
* Top-level `contextCreated` events that arrive in that window are quarantined
|
|
67
|
+
* (the event can beat the create response), then the internal one is dropped
|
|
68
|
+
* and the rest replayed in order once the id is known.
|
|
69
|
+
*/
|
|
70
|
+
_internalCreateInFlight = 0;
|
|
71
|
+
_quarantinedCreated = [];
|
|
72
|
+
_internalClassificationWaiters = new Set();
|
|
73
|
+
/** Settled tail used to serialize state overlays within this user context. */
|
|
74
|
+
_restoreTail = Promise.resolve();
|
|
61
75
|
/**
|
|
62
76
|
* The in-flight promise from the most recent call to {@link _ensurePageTracking}.
|
|
63
77
|
* Doubles as the "is tracking armed?" flag — truthy after the first call,
|
|
@@ -103,7 +117,6 @@ export class BrowserContext {
|
|
|
103
117
|
/** @internal Used by Page to keep preload-backed navigations on BiDi. */
|
|
104
118
|
_hasInitScriptsForNavigation() {
|
|
105
119
|
return (this._initScriptIds.size > 0 ||
|
|
106
|
-
this._localStoragePreloadId !== undefined ||
|
|
107
120
|
this._hooks?.hasBrowserInitScripts?.() === true);
|
|
108
121
|
}
|
|
109
122
|
/** BiDi partition descriptor for storage operations scoped to this user context. */
|
|
@@ -146,8 +159,11 @@ export class BrowserContext {
|
|
|
146
159
|
async pages() {
|
|
147
160
|
this.assertOpen();
|
|
148
161
|
const tree = await this.conn.send('browsingContext.getTree', { maxDepth: 0 });
|
|
162
|
+
// The create event can beat the command response that tells us which id is
|
|
163
|
+
// internal. Wait for that classification before exposing this tree.
|
|
164
|
+
await this._waitForInternalClassification();
|
|
149
165
|
return (tree.contexts ?? [])
|
|
150
|
-
.filter((c) => c.userContext === this._id && !c.parent)
|
|
166
|
+
.filter((c) => c.userContext === this._id && !c.parent && !this._internalPageIds.has(c.context))
|
|
151
167
|
.map((c) => new Page(this.driver, c.context, this.getDefaultTimeout, this.conn, this));
|
|
152
168
|
}
|
|
153
169
|
/**
|
|
@@ -165,7 +181,9 @@ export class BrowserContext {
|
|
|
165
181
|
if (this._trackingPromise)
|
|
166
182
|
await this._trackingPromise;
|
|
167
183
|
return new Promise((resolve, reject) => {
|
|
184
|
+
let settled = false;
|
|
168
185
|
const timer = setTimeout(() => {
|
|
186
|
+
settled = true;
|
|
169
187
|
off();
|
|
170
188
|
reject(new Error(`waitForPage() timed out after ${timeout}ms`));
|
|
171
189
|
}, timeout);
|
|
@@ -174,11 +192,27 @@ export class BrowserContext {
|
|
|
174
192
|
return; // nested frames
|
|
175
193
|
if (params.userContext !== this._id)
|
|
176
194
|
return;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
195
|
+
const id = params.context;
|
|
196
|
+
void this._isInternalPageContext(id).then((internal) => {
|
|
197
|
+
if (internal || settled)
|
|
198
|
+
return;
|
|
199
|
+
settled = true;
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
off();
|
|
202
|
+
resolve(new Page(this.driver, id, this.getDefaultTimeout, this.conn, this));
|
|
203
|
+
}).catch((err) => {
|
|
204
|
+
if (settled)
|
|
205
|
+
return;
|
|
206
|
+
settled = true;
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
off();
|
|
209
|
+
reject(err);
|
|
210
|
+
});
|
|
180
211
|
});
|
|
181
212
|
action().catch((err) => {
|
|
213
|
+
if (settled)
|
|
214
|
+
return;
|
|
215
|
+
settled = true;
|
|
182
216
|
clearTimeout(timer);
|
|
183
217
|
off();
|
|
184
218
|
reject(err);
|
|
@@ -190,8 +224,7 @@ export class BrowserContext {
|
|
|
190
224
|
* Return cookies scoped to this user context.
|
|
191
225
|
*
|
|
192
226
|
* Optionally filter by URL(s): only cookies whose `domain`/`path` would
|
|
193
|
-
* be sent on a request to that URL are returned.
|
|
194
|
-
* `BrowserContext.cookies(urls)` shape.
|
|
227
|
+
* be sent on a request to that URL are returned.
|
|
195
228
|
*
|
|
196
229
|
* @example
|
|
197
230
|
* ```ts
|
|
@@ -270,13 +303,12 @@ export class BrowserContext {
|
|
|
270
303
|
partition: this.partition(),
|
|
271
304
|
});
|
|
272
305
|
}
|
|
273
|
-
// ── Storage state (
|
|
306
|
+
// ── Storage state (CraftDriver-native JSON) ─────────────────────────────
|
|
274
307
|
/**
|
|
275
308
|
* Snapshot cookies + localStorage for this context.
|
|
276
309
|
*
|
|
277
|
-
* Returns `{ cookies, localStorage }`
|
|
278
|
-
*
|
|
279
|
-
* are interchangeable on a best-effort basis.
|
|
310
|
+
* Returns `{ cookies, localStorage }` in CraftDriver's native
|
|
311
|
+
* {@link SessionState} shape. It is not a cross-tool interchange format.
|
|
280
312
|
*
|
|
281
313
|
* **Limitation.** localStorage can only be read for origins that a page
|
|
282
314
|
* in this context is currently visiting — BiDi has no out-of-band
|
|
@@ -307,59 +339,73 @@ export class BrowserContext {
|
|
|
307
339
|
* const alice = await browser.newContext();
|
|
308
340
|
* const page = await alice.newPage({ url: 'https://app.example.com/login' });
|
|
309
341
|
* await loginAs(page, 'alice');
|
|
310
|
-
* await alice.saveStorageState('auth/alice.json');
|
|
342
|
+
* await alice.saveStorageState('.auth/alice.json');
|
|
311
343
|
* await alice.close();
|
|
312
344
|
* ```
|
|
313
345
|
*/
|
|
314
346
|
async saveStorageState(path, opts) {
|
|
315
347
|
const state = await this.storageState(opts);
|
|
316
|
-
await
|
|
348
|
+
await writeSecureFile(path, JSON.stringify(state, null, 2));
|
|
317
349
|
return state;
|
|
318
350
|
}
|
|
319
351
|
/**
|
|
320
352
|
* Apply a previously-captured snapshot to this context.
|
|
321
353
|
*
|
|
322
354
|
* - Cookies are written immediately.
|
|
323
|
-
* - localStorage
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* Calling `loadStorageState` again replaces the previous preload
|
|
329
|
-
* script; it does not stack.
|
|
355
|
+
* - localStorage is seeded once per captured origin through a private,
|
|
356
|
+
* intercepted same-origin document (BiDi has no "set localStorage for
|
|
357
|
+
* origin" command). Because it runs once — not as a per-navigation
|
|
358
|
+
* preload — the application owns the values afterward, so its own writes
|
|
359
|
+
* survive a reload.
|
|
330
360
|
*
|
|
331
361
|
* @param source A `SessionState` object, or a path to a JSON file
|
|
332
362
|
* produced by {@link saveStorageState}.
|
|
333
363
|
*/
|
|
334
364
|
async loadStorageState(source) {
|
|
365
|
+
return this._loadStorageState(source, 'BrowserContext.loadStorageState');
|
|
366
|
+
}
|
|
367
|
+
/** @internal Same public contract with an owning API name for structured errors. */
|
|
368
|
+
async _loadStorageState(source, operation) {
|
|
335
369
|
this.assertOpen();
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const { sameSite: _drop, ...rest } = c;
|
|
347
|
-
return rest;
|
|
348
|
-
}
|
|
349
|
-
return c;
|
|
370
|
+
const errorContext = this._storageStateErrorContext(operation);
|
|
371
|
+
const state = await parseSessionState(source, errorContext);
|
|
372
|
+
// A context/launch restore cannot carry tab-scoped sessionStorage to the
|
|
373
|
+
// caller's future pages — reject before any mutation rather than silently
|
|
374
|
+
// dropping it. (browser.loadState() takes the active-page path for that.)
|
|
375
|
+
if (hasNonEmptySessionStorage(state)) {
|
|
376
|
+
throw new CraftdriverError(ErrorCode.UNSUPPORTED, 'restoring sessionStorage is not supported for context/launch APIs (it is ' +
|
|
377
|
+
'tab-scoped); use browser.loadState() after navigating to the origin', {
|
|
378
|
+
detail: storageStateDetail(errorContext, 'validation', { section: 'sessionStorage' }),
|
|
379
|
+
hint: 'Navigate to the origin and call browser.loadState(), or omit sessionStorage.',
|
|
350
380
|
});
|
|
351
|
-
await this.addCookies(sanitized);
|
|
352
|
-
}
|
|
353
|
-
// Replace any previously-installed localStorage preload.
|
|
354
|
-
if (this._localStoragePreloadId) {
|
|
355
|
-
await this.conn
|
|
356
|
-
.send('script.removePreloadScript', { script: this._localStoragePreloadId })
|
|
357
|
-
.catch(() => { });
|
|
358
|
-
this._localStoragePreloadId = undefined;
|
|
359
|
-
}
|
|
360
|
-
if (state.localStorage && Object.keys(state.localStorage).length > 0) {
|
|
361
|
-
this._localStoragePreloadId = await this.installLocalStoragePreload(state.localStorage);
|
|
362
381
|
}
|
|
382
|
+
await this._runSerializedRestore(async () => {
|
|
383
|
+
let partialApplied = false;
|
|
384
|
+
// localStorage first — its private hydration page is where a restore failure
|
|
385
|
+
// would surface; applying cookies last means a hydration failure never
|
|
386
|
+
// leaves cookies behind.
|
|
387
|
+
if (nonEmptyOrigins(state.localStorage).length > 0) {
|
|
388
|
+
try {
|
|
389
|
+
partialApplied = await this._hydrateLocalStorage(state.localStorage, errorContext);
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
if (isStorageStateError(err))
|
|
393
|
+
throw err;
|
|
394
|
+
throw this._restoreDriverError(err, errorContext, 'localStorage', partialApplied);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (state.cookies?.length) {
|
|
398
|
+
for (const captured of state.cookies) {
|
|
399
|
+
try {
|
|
400
|
+
await this.addCookies([normalizeCookieForRestore(captured)]);
|
|
401
|
+
partialApplied = true;
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
throw this._restoreDriverError(err, errorContext, 'cookies', partialApplied);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
});
|
|
363
409
|
}
|
|
364
410
|
/**
|
|
365
411
|
* Remove this user context. All of its pages are closed and any
|
|
@@ -381,12 +427,6 @@ export class BrowserContext {
|
|
|
381
427
|
await this.conn.send('script.removePreloadScript', { script: id }).catch(() => { });
|
|
382
428
|
}
|
|
383
429
|
this._initScriptIds.clear();
|
|
384
|
-
if (this._localStoragePreloadId) {
|
|
385
|
-
await this.conn
|
|
386
|
-
.send('script.removePreloadScript', { script: this._localStoragePreloadId })
|
|
387
|
-
.catch(() => { });
|
|
388
|
-
this._localStoragePreloadId = undefined;
|
|
389
|
-
}
|
|
390
430
|
if (this._hooks && this._routes.size > 0) {
|
|
391
431
|
const net = this._hooks.getNetwork();
|
|
392
432
|
for (const entry of this._routes.values()) {
|
|
@@ -402,6 +442,8 @@ export class BrowserContext {
|
|
|
402
442
|
off();
|
|
403
443
|
this._trackingOffs = [];
|
|
404
444
|
this._trackingPromise = undefined;
|
|
445
|
+
this._internalPageIds.clear();
|
|
446
|
+
this._resolveInternalClassificationWaiters();
|
|
405
447
|
await this.conn.send('browser.removeUserContext', { userContext: this._id });
|
|
406
448
|
this._closed = true;
|
|
407
449
|
// Fire close listeners last, after the context is fully gone.
|
|
@@ -454,7 +496,7 @@ export class BrowserContext {
|
|
|
454
496
|
this.assertOpen();
|
|
455
497
|
const fnDecl = typeof script === 'function' ? script.toString() : `() => { ${script} }`;
|
|
456
498
|
const result = await this.conn.send('script.addPreloadScript', {
|
|
457
|
-
functionDeclaration: fnDecl,
|
|
499
|
+
functionDeclaration: publicPageInitScript(fnDecl),
|
|
458
500
|
userContexts: [this._id],
|
|
459
501
|
});
|
|
460
502
|
this._initScriptIds.add(result.script);
|
|
@@ -757,35 +799,197 @@ export class BrowserContext {
|
|
|
757
799
|
return byOrigin;
|
|
758
800
|
}
|
|
759
801
|
/**
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
802
|
+
* Seed localStorage for each captured origin exactly once.
|
|
803
|
+
*
|
|
804
|
+
* BiDi has no command to set localStorage for an arbitrary origin, so we
|
|
805
|
+
* visit each origin in a throwaway top-level context whose navigation is
|
|
806
|
+
* fulfilled locally (never reaching the network), write the entries, and
|
|
807
|
+
* close it. Unlike a preload script this runs once — the application owns the
|
|
808
|
+
* values afterward, so its own writes survive a reload.
|
|
809
|
+
*
|
|
810
|
+
* The private context is created at the connection level (not via `newPage`),
|
|
811
|
+
* so it never enters `_pageIds`, the `'page'` event, routes, or active-page
|
|
812
|
+
* selection. The intercept is scoped to it, and navigation is forced onto
|
|
813
|
+
* BiDi — a Classic-initiated navigation would stall against a BiDi intercept.
|
|
763
814
|
*/
|
|
764
|
-
async
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
815
|
+
async _hydrateLocalStorage(byOrigin, errorContext) {
|
|
816
|
+
const origins = Object.keys(byOrigin).filter((o) => byOrigin[o] && Object.keys(byOrigin[o]).length > 0);
|
|
817
|
+
if (origins.length === 0)
|
|
818
|
+
return false;
|
|
819
|
+
const net = this._hooks?.getNetwork();
|
|
820
|
+
if (!net) {
|
|
821
|
+
throw new CraftdriverError(ErrorCode.UNSUPPORTED, 'restoring localStorage requires a BiDi session with network interception', { detail: storageStateDetail(errorContext, 'setup', { section: 'localStorage' }) });
|
|
822
|
+
}
|
|
823
|
+
// Reserve the internal-context slot before the create so page tracking hides
|
|
824
|
+
// it even if its contextCreated event beats the create response.
|
|
825
|
+
this._internalCreateInFlight++;
|
|
826
|
+
let privId;
|
|
827
|
+
try {
|
|
828
|
+
const created = await this.conn.send('browsingContext.create', {
|
|
829
|
+
type: 'tab',
|
|
830
|
+
userContext: this._id,
|
|
831
|
+
});
|
|
832
|
+
privId = created.context;
|
|
833
|
+
this._internalPageIds.add(privId);
|
|
834
|
+
}
|
|
835
|
+
catch (err) {
|
|
836
|
+
throw this._restoreDriverError(err, errorContext, 'localStorage', false);
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
this._internalCreateInFlight--;
|
|
840
|
+
this._drainQuarantine();
|
|
841
|
+
this._resolveInternalClassificationWaiters();
|
|
842
|
+
}
|
|
843
|
+
if (!privId)
|
|
844
|
+
return false;
|
|
845
|
+
const priv = new Page(this.driver, privId, this.getDefaultTimeout, this.conn, this);
|
|
846
|
+
let interceptId;
|
|
847
|
+
let partialApplied = false;
|
|
848
|
+
try {
|
|
849
|
+
// Fulfil this private context's top-level navigations with a minimal
|
|
850
|
+
// document, locally — the request never reaches the network. The pattern
|
|
851
|
+
// is specific (a bare `**/*` does not register a working intercept): only
|
|
852
|
+
// this context's deterministic hydrate URL is fulfilled, and the intercept
|
|
853
|
+
// is scoped to `privId`, so no user page is ever touched.
|
|
854
|
+
interceptId = await net.intercept(`**${AUTH_STATE_HYDRATE_PATH}*`, () => ({
|
|
855
|
+
status: 200,
|
|
856
|
+
headers: { 'content-type': 'text/html' },
|
|
857
|
+
body: '<!doctype html><html><head></head><body></body></html>',
|
|
858
|
+
}), ['beforeRequestSent'], [privId], { strict: true } // never let a hydration request reach the real network
|
|
859
|
+
);
|
|
860
|
+
for (const origin of origins) {
|
|
861
|
+
// The path is fixed and irrelevant — the intercept fulfils it locally.
|
|
862
|
+
// `domcontentloaded` forces the BiDi navigate path so the intercept
|
|
863
|
+
// applies (a default-context Classic `load` navigation would stall).
|
|
864
|
+
await priv.navigateTo(`${origin}${AUTH_STATE_HYDRATE_PATH}`, {
|
|
865
|
+
waitUntil: 'domcontentloaded',
|
|
866
|
+
});
|
|
867
|
+
// Confirm the realm is on the requested origin (an HSTS upgrade or other
|
|
868
|
+
// rewrite could land elsewhere), then write the entries in one call. The
|
|
869
|
+
// values travel as structured string arguments — never interpolated into
|
|
870
|
+
// the source; the source is a string, so the DOM globals it names are the
|
|
871
|
+
// browser's, not Node's.
|
|
872
|
+
const res = await this.conn.send('script.callFunction', {
|
|
873
|
+
functionDeclaration: 'function (expected, json) {' +
|
|
874
|
+
' if (location.origin !== expected) return "MISMATCH:" + location.origin;' +
|
|
875
|
+
' var e = JSON.parse(json);' +
|
|
876
|
+
' for (var k in e) localStorage.setItem(k, e[k]);' +
|
|
877
|
+
' return "OK"; }',
|
|
878
|
+
target: { context: privId },
|
|
879
|
+
arguments: [
|
|
880
|
+
{ type: 'string', value: origin },
|
|
881
|
+
{ type: 'string', value: JSON.stringify(byOrigin[origin]) },
|
|
882
|
+
],
|
|
883
|
+
awaitPromise: false,
|
|
884
|
+
});
|
|
885
|
+
const value = res.type === 'success' ? res.result?.value : undefined;
|
|
886
|
+
if (value !== 'OK') {
|
|
887
|
+
const mismatch = new CraftdriverError(ErrorCode.STATE_INVALID, `could not hydrate localStorage for origin ${origin}`, { detail: storageStateDetail(errorContext, 'localStorage', { origin }) });
|
|
888
|
+
if (!partialApplied)
|
|
889
|
+
throw mismatch;
|
|
890
|
+
throw this._restoreDriverError(mismatch, errorContext, 'localStorage', true, { origin });
|
|
891
|
+
}
|
|
892
|
+
partialApplied = true;
|
|
893
|
+
}
|
|
894
|
+
return partialApplied;
|
|
895
|
+
}
|
|
896
|
+
catch (err) {
|
|
897
|
+
if (isStorageStateError(err))
|
|
898
|
+
throw err;
|
|
899
|
+
throw this._restoreDriverError(err, errorContext, 'localStorage', partialApplied);
|
|
900
|
+
}
|
|
901
|
+
finally {
|
|
902
|
+
if (interceptId)
|
|
903
|
+
await net.removeIntercept(interceptId).catch(() => { });
|
|
904
|
+
await this.conn.send('browsingContext.close', { context: privId }).catch(() => { });
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/** @internal Serialize restores without coupling independent user contexts. */
|
|
908
|
+
_runSerializedRestore(task) {
|
|
909
|
+
const run = this._restoreTail.then(task, task);
|
|
910
|
+
this._restoreTail = run.then(() => undefined, () => undefined);
|
|
911
|
+
return run;
|
|
912
|
+
}
|
|
913
|
+
/** @internal Synchronous filter for events emitted after an internal id is known. */
|
|
914
|
+
_isInternalPageId(id) {
|
|
915
|
+
return typeof id === 'string' && this._internalPageIds.has(id);
|
|
916
|
+
}
|
|
917
|
+
/** @internal Wait through the create-response race, then classify an event id. */
|
|
918
|
+
async _isInternalPageContext(id) {
|
|
919
|
+
await this._waitForInternalClassification();
|
|
920
|
+
return this._internalPageIds.has(id);
|
|
921
|
+
}
|
|
922
|
+
_waitForInternalClassification() {
|
|
923
|
+
if (this._internalCreateInFlight === 0)
|
|
924
|
+
return Promise.resolve();
|
|
925
|
+
return new Promise((resolve) => this._internalClassificationWaiters.add(resolve));
|
|
926
|
+
}
|
|
927
|
+
_resolveInternalClassificationWaiters() {
|
|
928
|
+
if (this._internalCreateInFlight !== 0)
|
|
929
|
+
return;
|
|
930
|
+
for (const resolve of this._internalClassificationWaiters)
|
|
931
|
+
resolve();
|
|
932
|
+
this._internalClassificationWaiters.clear();
|
|
933
|
+
}
|
|
934
|
+
_storageStateErrorContext(operation) {
|
|
935
|
+
return {
|
|
936
|
+
operation,
|
|
937
|
+
browserName: this._hooks?.getBrowserName?.() ?? 'unknown',
|
|
938
|
+
protocol: 'bidi',
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
_restoreDriverError(cause, context, phase, partialApplied, extra) {
|
|
942
|
+
return new CraftdriverError(ErrorCode.DRIVER_ERROR, `storage state restore failed while applying ${phase}`, {
|
|
943
|
+
cause,
|
|
944
|
+
detail: storageStateDetail(context, phase, { partialApplied, ...extra }),
|
|
945
|
+
hint: partialApplied
|
|
946
|
+
? 'Restore into a fresh browser context when failure isolation is required.'
|
|
947
|
+
: undefined,
|
|
781
948
|
});
|
|
782
|
-
return result.script;
|
|
783
949
|
}
|
|
784
950
|
/**
|
|
785
951
|
* Subscribe (once) to BiDi page-creation / page-destruction events and
|
|
786
952
|
* seed `_pageIds` with currently-open pages in this context. Powers
|
|
787
953
|
* both the `'page'` event and route membership filtering.
|
|
788
954
|
*/
|
|
955
|
+
/** Apply the normal new-page handling (tracking, headers, routes, the `'page'`
|
|
956
|
+
* event) for a real top-level context. */
|
|
957
|
+
_handleContextCreated(id) {
|
|
958
|
+
this._pageIds.add(id);
|
|
959
|
+
// Headers must be applied before the first request from this page.
|
|
960
|
+
this._applyExtraHeadersTo([id]).catch(() => { });
|
|
961
|
+
// Register every existing route on the new page.
|
|
962
|
+
for (const sid of this._routes.keys()) {
|
|
963
|
+
this._registerRouteOnPage(sid, id).catch(() => { });
|
|
964
|
+
}
|
|
965
|
+
// Notify page listeners exactly once per page.
|
|
966
|
+
if (this._notifiedPageIds.has(id))
|
|
967
|
+
return;
|
|
968
|
+
this._notifiedPageIds.add(id);
|
|
969
|
+
if (this._pageListeners.size > 0) {
|
|
970
|
+
const page = new Page(this.driver, id, this.getDefaultTimeout, this.conn, this);
|
|
971
|
+
for (const fn of this._pageListeners) {
|
|
972
|
+
try {
|
|
973
|
+
fn(page);
|
|
974
|
+
}
|
|
975
|
+
catch { /* listener errors must not break events */ }
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/** Replay contextCreated events buffered during an internal create, once none
|
|
980
|
+
* is in flight — dropping any that turned out to be internal. */
|
|
981
|
+
_drainQuarantine() {
|
|
982
|
+
if (this._internalCreateInFlight > 0)
|
|
983
|
+
return;
|
|
984
|
+
const buffered = this._quarantinedCreated;
|
|
985
|
+
this._quarantinedCreated = [];
|
|
986
|
+
for (const params of buffered) {
|
|
987
|
+
const id = params.context;
|
|
988
|
+
if (this._internalPageIds.has(id))
|
|
989
|
+
continue; // internal → stay hidden
|
|
990
|
+
this._handleContextCreated(id);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
789
993
|
_ensurePageTracking() {
|
|
790
994
|
if (this._closed)
|
|
791
995
|
return Promise.resolve();
|
|
@@ -805,31 +1009,25 @@ export class BrowserContext {
|
|
|
805
1009
|
if (params.userContext !== this._id)
|
|
806
1010
|
return;
|
|
807
1011
|
const id = params.context;
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
//
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
if (this._notifiedPageIds.has(id))
|
|
1012
|
+
// Internal hydration context — stays invisible to page tracking.
|
|
1013
|
+
if (this._internalPageIds.has(id))
|
|
1014
|
+
return;
|
|
1015
|
+
// Its id may not be known yet (the event can beat the create response):
|
|
1016
|
+
// quarantine every top-level create in this context while one is in
|
|
1017
|
+
// flight, then drop the internal one and replay the rest.
|
|
1018
|
+
if (this._internalCreateInFlight > 0) {
|
|
1019
|
+
this._quarantinedCreated.push(params);
|
|
817
1020
|
return;
|
|
818
|
-
this._notifiedPageIds.add(id);
|
|
819
|
-
if (this._pageListeners.size > 0) {
|
|
820
|
-
const page = new Page(this.driver, id, this.getDefaultTimeout, this.conn, this);
|
|
821
|
-
for (const fn of this._pageListeners) {
|
|
822
|
-
try {
|
|
823
|
-
fn(page);
|
|
824
|
-
}
|
|
825
|
-
catch { /* listener errors must not break events */ }
|
|
826
|
-
}
|
|
827
1021
|
}
|
|
1022
|
+
this._handleContextCreated(id);
|
|
828
1023
|
};
|
|
829
1024
|
const onDestroyed = (params) => {
|
|
830
1025
|
if (params.parent)
|
|
831
1026
|
return;
|
|
832
1027
|
const id = params.context;
|
|
1028
|
+
if (this._internalPageIds.has(id)) {
|
|
1029
|
+
return; // internal hydration context — never tracked, nothing to tear down
|
|
1030
|
+
}
|
|
833
1031
|
this._pageIds.delete(id);
|
|
834
1032
|
this._notifiedPageIds.delete(id);
|
|
835
1033
|
// Tear down any per-page intercepts registered against this page.
|
|
@@ -853,8 +1051,11 @@ export class BrowserContext {
|
|
|
853
1051
|
// miss a contextCreated event that races with this seeding.
|
|
854
1052
|
try {
|
|
855
1053
|
const tree = await this.conn.send('browsingContext.getTree', { maxDepth: 0 });
|
|
1054
|
+
await this._waitForInternalClassification();
|
|
856
1055
|
for (const c of tree.contexts ?? []) {
|
|
857
|
-
if (c.userContext === this._id &&
|
|
1056
|
+
if (c.userContext === this._id &&
|
|
1057
|
+
!c.parent &&
|
|
1058
|
+
!this._internalPageIds.has(c.context)) {
|
|
858
1059
|
this._pageIds.add(c.context);
|
|
859
1060
|
}
|
|
860
1061
|
}
|