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
package/dist/lib/browser.js
CHANGED
|
@@ -12,7 +12,7 @@ import { resolveLaunchTarget } from './launchTarget.js';
|
|
|
12
12
|
import { parseRemoteEndpoint, buildRemoteCapabilities, redactUrlForLog } from './remote.js';
|
|
13
13
|
import { By } from './by.js';
|
|
14
14
|
import { until } from './wait.js';
|
|
15
|
-
import { DEFAULT_ELEMENT_TIMEOUT_MS, DEFAULT_NAVIGATION_TIMEOUT_MS, STATE_POLL_INTERVAL_MS, NETWORK_IDLE_SETTLE_MS, PORT_RELEASE_DELAY_MS, BIDI_CONNECT_MAX_ATTEMPTS, BIDI_CONNECT_BACKOFF_STEP_MS,
|
|
15
|
+
import { DEFAULT_ELEMENT_TIMEOUT_MS, DEFAULT_NAVIGATION_TIMEOUT_MS, STATE_POLL_INTERVAL_MS, NETWORK_IDLE_SETTLE_MS, PORT_RELEASE_DELAY_MS, BIDI_CONNECT_MAX_ATTEMPTS, BIDI_CONNECT_BACKOFF_STEP_MS, } from './timing.js';
|
|
16
16
|
import { ElementHandle } from './elementHandle.js';
|
|
17
17
|
import { Locator } from './locator.js';
|
|
18
18
|
import { expectSelector } from './expect.js';
|
|
@@ -33,10 +33,13 @@ import { Tracer } from './tracing.js';
|
|
|
33
33
|
import { A11y } from './a11y.js';
|
|
34
34
|
import { Clock } from './clock.js';
|
|
35
35
|
import { CraftdriverError, ErrorCode } from './errors.js';
|
|
36
|
+
import { parseSessionState, hasNonEmptySessionStorage, isSessionStateEmpty, storageStateDetail, } from './sessionStateValidation.js';
|
|
36
37
|
import { clickWithFastPath } from './clickFastPath.js';
|
|
37
38
|
import { fillWithFastPath } from './fillFastPath.js';
|
|
38
39
|
import { clearWithFastPath } from './clearFastPath.js';
|
|
39
40
|
import { runExpectScreenshot, shouldUpdateVisualBaselines } from './visual/index.js';
|
|
41
|
+
import { publicPageInitScript } from './initScript.js';
|
|
42
|
+
import { withRealmRetry } from './bidi/evaluate.js';
|
|
40
43
|
/** Common mobile device presets */
|
|
41
44
|
export const devices = {
|
|
42
45
|
'iPhone 14': {
|
|
@@ -269,6 +272,7 @@ export class Browser {
|
|
|
269
272
|
_topLevelContextTracking;
|
|
270
273
|
_topLevelContextTrackingOffs = [];
|
|
271
274
|
_topLevelContextCacheVersion = 0;
|
|
275
|
+
_destroyedTopLevelContextIds = new Set();
|
|
272
276
|
_logs;
|
|
273
277
|
_tracer;
|
|
274
278
|
_storage;
|
|
@@ -481,9 +485,19 @@ export class Browser {
|
|
|
481
485
|
if (wsUrl && bidiRequested) {
|
|
482
486
|
await browser.initBiDi(wsUrl);
|
|
483
487
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
488
|
+
if (options.storageState !== undefined) {
|
|
489
|
+
const protocol = browser.bidiSession?.isConnected() ? 'bidi' : 'classic';
|
|
490
|
+
const errorContext = { operation: 'Browser.launch', browserName: name, protocol };
|
|
491
|
+
const state = await parseSessionState(options.storageState, errorContext);
|
|
492
|
+
if (browser.bidiSession?.isConnected()) {
|
|
493
|
+
await browser.defaultContext._loadStorageState(state, 'Browser.launch');
|
|
494
|
+
}
|
|
495
|
+
else if (!isSessionStateEmpty(state)) {
|
|
496
|
+
throw new CraftdriverError(ErrorCode.UNSUPPORTED, 'non-empty storageState cannot be restored at WebDriver Classic launch', {
|
|
497
|
+
detail: storageStateDetail(errorContext, 'capability', { partialApplied: false }),
|
|
498
|
+
hint: 'Enable BiDi, or launch first, navigate to the sole origin, then call browser.loadState().',
|
|
499
|
+
});
|
|
500
|
+
}
|
|
487
501
|
}
|
|
488
502
|
}
|
|
489
503
|
catch (err) {
|
|
@@ -517,8 +531,23 @@ export class Browser {
|
|
|
517
531
|
if (wsUrl && target.bidiRequested) {
|
|
518
532
|
await browser.initBiDi(wsUrl);
|
|
519
533
|
}
|
|
520
|
-
if (options.storageState) {
|
|
521
|
-
|
|
534
|
+
if (options.storageState !== undefined) {
|
|
535
|
+
const protocol = browser.bidiSession?.isConnected() ? 'bidi' : 'classic';
|
|
536
|
+
const errorContext = {
|
|
537
|
+
operation: 'Browser.launch',
|
|
538
|
+
browserName: target.browserName,
|
|
539
|
+
protocol,
|
|
540
|
+
};
|
|
541
|
+
const state = await parseSessionState(options.storageState, errorContext);
|
|
542
|
+
if (browser.bidiSession?.isConnected()) {
|
|
543
|
+
await browser.defaultContext._loadStorageState(state, 'Browser.launch');
|
|
544
|
+
}
|
|
545
|
+
else if (!isSessionStateEmpty(state)) {
|
|
546
|
+
throw new CraftdriverError(ErrorCode.UNSUPPORTED, 'non-empty storageState cannot be restored at WebDriver Classic launch', {
|
|
547
|
+
detail: storageStateDetail(errorContext, 'capability', { partialApplied: false }),
|
|
548
|
+
hint: 'Enable BiDi, or launch first, navigate to the sole origin, then call browser.loadState().',
|
|
549
|
+
});
|
|
550
|
+
}
|
|
522
551
|
}
|
|
523
552
|
}
|
|
524
553
|
catch (err) {
|
|
@@ -550,6 +579,8 @@ export class Browser {
|
|
|
550
579
|
this._topLevelContextTracking = this._startTopLevelContextTracking(contexts);
|
|
551
580
|
},
|
|
552
581
|
});
|
|
582
|
+
session.network.setInternalContextPredicate((context) => this._isInternalContextId(context));
|
|
583
|
+
session.logs.setInternalContextPredicate((context) => this._isInternalContextId(context));
|
|
553
584
|
await this._ensureTopLevelContextTracking().catch(() => { });
|
|
554
585
|
return; // success
|
|
555
586
|
}
|
|
@@ -618,7 +649,13 @@ export class Browser {
|
|
|
618
649
|
*/
|
|
619
650
|
get storage() {
|
|
620
651
|
if (!this._storage) {
|
|
621
|
-
this._storage = new SessionStateManager(this.driver, this.bidiSession?.isConnected() ? this.bidiSession.getConnection() : null
|
|
652
|
+
this._storage = new SessionStateManager(this.driver, this.bidiSession?.isConnected() ? this.bidiSession.getConnection() : null, undefined, {
|
|
653
|
+
browserName: this._browserName,
|
|
654
|
+
protocol: this.bidiSession?.isConnected() ? 'bidi' : 'classic',
|
|
655
|
+
runSerializedRestore: this.bidiSession?.isConnected()
|
|
656
|
+
? (task) => this.defaultContext._runSerializedRestore(task)
|
|
657
|
+
: undefined,
|
|
658
|
+
});
|
|
622
659
|
}
|
|
623
660
|
return this._storage;
|
|
624
661
|
}
|
|
@@ -681,8 +718,23 @@ export class Browser {
|
|
|
681
718
|
/**
|
|
682
719
|
* Load session state from file
|
|
683
720
|
*/
|
|
684
|
-
async loadState(
|
|
685
|
-
|
|
721
|
+
async loadState(source) {
|
|
722
|
+
const protocol = this.bidiSession?.isConnected() ? 'bidi' : 'classic';
|
|
723
|
+
const state = await parseSessionState(source, {
|
|
724
|
+
operation: 'browser.loadState',
|
|
725
|
+
browserName: this._browserName,
|
|
726
|
+
protocol,
|
|
727
|
+
});
|
|
728
|
+
// BiDi with no sessionStorage → the validated hydrator (multi-origin, no
|
|
729
|
+
// active page required). Otherwise (Classic, or sessionStorage present) take
|
|
730
|
+
// the active-page path, which restores a single origin matching the active
|
|
731
|
+
// page and can carry sessionStorage.
|
|
732
|
+
if (this.bidiSession?.isConnected() && !hasNonEmptySessionStorage(state)) {
|
|
733
|
+
await this.defaultContext._loadStorageState(state, 'browser.loadState');
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
await this.storage.setState(state, 'browser.loadState');
|
|
737
|
+
}
|
|
686
738
|
}
|
|
687
739
|
async _runTracedAction(name, args, selector, run) {
|
|
688
740
|
const actionIndex = this._tracer?.recordAction(name, args, selector);
|
|
@@ -1135,6 +1187,7 @@ export class Browser {
|
|
|
1135
1187
|
this._topLevelContextTrackingOffs = [];
|
|
1136
1188
|
this._topLevelContextTracking = undefined;
|
|
1137
1189
|
this._topLevelContextUserContexts.clear();
|
|
1190
|
+
this._destroyedTopLevelContextIds.clear();
|
|
1138
1191
|
this._topLevelContextCacheVersion++;
|
|
1139
1192
|
// DELETE the WebDriver session — this tells the driver service to close the browser.
|
|
1140
1193
|
await this.driver.quit().catch(() => { });
|
|
@@ -1179,7 +1232,7 @@ export class Browser {
|
|
|
1179
1232
|
'Tracing relies on BiDi events; Classic WebDriver does not expose them.', { detail: { browserName: this._browserName, feature: 'startTrace' } });
|
|
1180
1233
|
}
|
|
1181
1234
|
if (!this._tracer) {
|
|
1182
|
-
this._tracer = new Tracer(this, this.bidiSession.getConnection(), this._engine);
|
|
1235
|
+
this._tracer = new Tracer(this, this.bidiSession.getConnection(), this._engine, (context) => this._isInternalContextId(context));
|
|
1183
1236
|
}
|
|
1184
1237
|
await this._tracer.start(opts);
|
|
1185
1238
|
}
|
|
@@ -1212,8 +1265,9 @@ export class Browser {
|
|
|
1212
1265
|
*/
|
|
1213
1266
|
async evaluate(fn, ...args) {
|
|
1214
1267
|
const fnSrc = typeof fn === 'function' ? fn.toString() : fn;
|
|
1215
|
-
|
|
1216
|
-
|
|
1268
|
+
const bidiSession = this.bidiSession;
|
|
1269
|
+
if (bidiSession?.isConnected()) {
|
|
1270
|
+
const conn = bidiSession.getConnection();
|
|
1217
1271
|
// Classic-first navigations return at readyState === 'complete', which is
|
|
1218
1272
|
// not a barrier the BiDi side respects: an immediately following
|
|
1219
1273
|
// { context } call can race the browser swapping the old realm for the
|
|
@@ -1223,28 +1277,16 @@ export class Browser {
|
|
|
1223
1277
|
// result.type === 'exception' path below and are never retried here.
|
|
1224
1278
|
const functionDeclaration = typeof fn === 'function' ? fnSrc : `function() { ${fnSrc} }`;
|
|
1225
1279
|
const callArgs = typeof fn === 'function' ? args.map(serializeLocalValue) : [];
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
const context = this.bidiSession.getContext();
|
|
1280
|
+
const result = await withRealmRetry(() => {
|
|
1281
|
+
const context = bidiSession.getContext();
|
|
1229
1282
|
const target = context ? { context } : {};
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
break;
|
|
1238
|
-
}
|
|
1239
|
-
catch (err) {
|
|
1240
|
-
if (attempt < EVAL_REALM_RETRY_ATTEMPTS &&
|
|
1241
|
-
String(err?.message).includes('execution contexts cleared')) {
|
|
1242
|
-
await new Promise((r) => setTimeout(r, EVAL_REALM_RETRY_DELAY_MS));
|
|
1243
|
-
continue;
|
|
1244
|
-
}
|
|
1245
|
-
throw err;
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1283
|
+
return conn.send('script.callFunction', {
|
|
1284
|
+
functionDeclaration,
|
|
1285
|
+
target,
|
|
1286
|
+
arguments: callArgs,
|
|
1287
|
+
awaitPromise: true,
|
|
1288
|
+
});
|
|
1289
|
+
});
|
|
1248
1290
|
if (result.type === 'exception') {
|
|
1249
1291
|
throw new CraftdriverError(ErrorCode.EVAL_THREW, `evaluate() threw an exception in the page: ${result.exceptionDetails?.text ?? 'unknown error'}`, { detail: { exception: result.exceptionDetails?.text ?? null } });
|
|
1250
1292
|
}
|
|
@@ -1278,7 +1320,7 @@ export class Browser {
|
|
|
1278
1320
|
const conn = this.bidiSession.getConnection();
|
|
1279
1321
|
const fnSrc = typeof fnOrSrc === 'function' ? fnOrSrc.toString() : `() => { ${fnOrSrc} }`;
|
|
1280
1322
|
const result = await conn.send('script.addPreloadScript', {
|
|
1281
|
-
functionDeclaration: fnSrc,
|
|
1323
|
+
functionDeclaration: publicPageInitScript(fnSrc),
|
|
1282
1324
|
});
|
|
1283
1325
|
const scriptId = result.script;
|
|
1284
1326
|
this._browserInitScriptIds.add(scriptId);
|
|
@@ -1611,7 +1653,14 @@ export class Browser {
|
|
|
1611
1653
|
const tree = await conn.send('browsingContext.getTree', {
|
|
1612
1654
|
maxDepth: 0,
|
|
1613
1655
|
});
|
|
1614
|
-
|
|
1656
|
+
const pages = [];
|
|
1657
|
+
for (const ctx of tree.contexts ?? []) {
|
|
1658
|
+
const owner = this._wrapContext(ctx.userContext ?? 'default');
|
|
1659
|
+
if (await owner._isInternalPageContext(ctx.context))
|
|
1660
|
+
continue;
|
|
1661
|
+
pages.push(new Page(this.driver, ctx.context, this.getDefaultTimeout, conn, owner));
|
|
1662
|
+
}
|
|
1663
|
+
return pages;
|
|
1615
1664
|
}
|
|
1616
1665
|
// Classic fallback: use window handles
|
|
1617
1666
|
const handles = await this.driver.getWindowHandles();
|
|
@@ -1657,19 +1706,38 @@ export class Browser {
|
|
|
1657
1706
|
// `BiDiSession.connect()` (which must have run for `isConnected()` to be
|
|
1658
1707
|
// true), so no per-call `subscribe()` round trip is needed here.
|
|
1659
1708
|
return new Promise((resolve, reject) => {
|
|
1709
|
+
let settled = false;
|
|
1660
1710
|
const timer = setTimeout(() => {
|
|
1711
|
+
settled = true;
|
|
1661
1712
|
off();
|
|
1662
1713
|
reject(new Error(`waitForPage() timed out after ${timeout}ms`));
|
|
1663
1714
|
}, timeout);
|
|
1664
1715
|
const off = conn.on('browsingContext.contextCreated', (params) => {
|
|
1665
1716
|
// Only top-level contexts have no parent
|
|
1666
1717
|
if (!params.parent) {
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1718
|
+
const id = params.context;
|
|
1719
|
+
const owner = this._wrapContext(params.userContext ?? 'default');
|
|
1720
|
+
void owner._isInternalPageContext(id).then((internal) => {
|
|
1721
|
+
if (internal || settled)
|
|
1722
|
+
return;
|
|
1723
|
+
settled = true;
|
|
1724
|
+
clearTimeout(timer);
|
|
1725
|
+
off();
|
|
1726
|
+
resolve(new Page(this.driver, id, this.getDefaultTimeout, conn, owner));
|
|
1727
|
+
}).catch((err) => {
|
|
1728
|
+
if (settled)
|
|
1729
|
+
return;
|
|
1730
|
+
settled = true;
|
|
1731
|
+
clearTimeout(timer);
|
|
1732
|
+
off();
|
|
1733
|
+
reject(err);
|
|
1734
|
+
});
|
|
1670
1735
|
}
|
|
1671
1736
|
});
|
|
1672
1737
|
action().catch((err) => {
|
|
1738
|
+
if (settled)
|
|
1739
|
+
return;
|
|
1740
|
+
settled = true;
|
|
1673
1741
|
clearTimeout(timer);
|
|
1674
1742
|
off();
|
|
1675
1743
|
reject(err);
|
|
@@ -1758,7 +1826,7 @@ export class Browser {
|
|
|
1758
1826
|
*
|
|
1759
1827
|
* @example
|
|
1760
1828
|
* // Skip the login UI in every test by reusing a saved session.
|
|
1761
|
-
* const ctx = await browser.newContext({ storageState: 'auth/alice.json' });
|
|
1829
|
+
* const ctx = await browser.newContext({ storageState: '.auth/alice.json' });
|
|
1762
1830
|
* const page = await ctx.newPage({ url: 'https://app.example.com/dashboard' });
|
|
1763
1831
|
*
|
|
1764
1832
|
* @example
|
|
@@ -1797,6 +1865,15 @@ export class Browser {
|
|
|
1797
1865
|
});
|
|
1798
1866
|
return ctx;
|
|
1799
1867
|
}
|
|
1868
|
+
_isInternalContextId(context) {
|
|
1869
|
+
if (typeof context !== 'string')
|
|
1870
|
+
return false;
|
|
1871
|
+
for (const ctx of this._contextsById.values()) {
|
|
1872
|
+
if (ctx._isInternalPageId(context))
|
|
1873
|
+
return true;
|
|
1874
|
+
}
|
|
1875
|
+
return false;
|
|
1876
|
+
}
|
|
1800
1877
|
async newContext(opts) {
|
|
1801
1878
|
this.requireBiDi('newContext()', 'newContext() requires BiDi (enableBiDi: true). ' +
|
|
1802
1879
|
'WebDriver Classic has no concept of user contexts.');
|
|
@@ -1806,17 +1883,25 @@ export class Browser {
|
|
|
1806
1883
|
baseURL: opts?.baseURL,
|
|
1807
1884
|
extraHTTPHeaders: opts?.extraHTTPHeaders,
|
|
1808
1885
|
});
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1886
|
+
try {
|
|
1887
|
+
if (opts?.storageState !== undefined) {
|
|
1888
|
+
await ctx._loadStorageState(opts.storageState, 'browser.newContext');
|
|
1889
|
+
}
|
|
1890
|
+
// Apply identity & device emulation options. Each setter is `userContexts`-scoped,
|
|
1891
|
+
// so future pages in this context inherit automatically.
|
|
1892
|
+
if (opts?.locale !== undefined)
|
|
1893
|
+
await ctx.setLocale(opts.locale);
|
|
1894
|
+
if (opts?.timezoneId !== undefined)
|
|
1895
|
+
await ctx.setTimezone(opts.timezoneId);
|
|
1896
|
+
if (opts?.geolocation !== undefined)
|
|
1897
|
+
await ctx.setGeolocation(opts.geolocation);
|
|
1898
|
+
}
|
|
1899
|
+
catch (err) {
|
|
1900
|
+
// Restore/emulation failed — tear the just-created context down so a
|
|
1901
|
+
// rejected newContext() never leaves an orphaned user context behind.
|
|
1902
|
+
await ctx.close().catch(() => { });
|
|
1903
|
+
throw err;
|
|
1904
|
+
}
|
|
1820
1905
|
return ctx;
|
|
1821
1906
|
}
|
|
1822
1907
|
/**
|
|
@@ -1887,13 +1972,21 @@ export class Browser {
|
|
|
1887
1972
|
const id = params.context;
|
|
1888
1973
|
if (typeof id !== 'string')
|
|
1889
1974
|
return;
|
|
1890
|
-
|
|
1891
|
-
this.
|
|
1975
|
+
const userContext = typeof params.userContext === 'string' ? params.userContext : 'default';
|
|
1976
|
+
this._destroyedTopLevelContextIds.delete(id);
|
|
1977
|
+
const owner = this._wrapContext(userContext);
|
|
1978
|
+
void owner._isInternalPageContext(id).then((internal) => {
|
|
1979
|
+
if (internal || this._destroyedTopLevelContextIds.has(id))
|
|
1980
|
+
return;
|
|
1981
|
+
this._topLevelContextUserContexts.set(id, userContext);
|
|
1982
|
+
this._topLevelContextCacheVersion++;
|
|
1983
|
+
}).catch(() => { });
|
|
1892
1984
|
};
|
|
1893
1985
|
const onDestroyed = (params) => {
|
|
1894
1986
|
if (params.parent)
|
|
1895
1987
|
return;
|
|
1896
1988
|
if (typeof params.context === 'string') {
|
|
1989
|
+
this._destroyedTopLevelContextIds.add(params.context);
|
|
1897
1990
|
this._topLevelContextUserContexts.delete(params.context);
|
|
1898
1991
|
this._topLevelContextCacheVersion++;
|
|
1899
1992
|
}
|
|
@@ -1922,6 +2015,9 @@ export class Browser {
|
|
|
1922
2015
|
}
|
|
1923
2016
|
for (const ctx of tree.contexts ?? []) {
|
|
1924
2017
|
if (!ctx.parent) {
|
|
2018
|
+
const owner = this._wrapContext(ctx.userContext ?? 'default');
|
|
2019
|
+
if (await owner._isInternalPageContext(ctx.context))
|
|
2020
|
+
continue;
|
|
1925
2021
|
this._topLevelContextUserContexts.set(ctx.context, ctx.userContext ?? 'default');
|
|
1926
2022
|
}
|
|
1927
2023
|
}
|
|
@@ -1929,7 +2025,7 @@ export class Browser {
|
|
|
1929
2025
|
}
|
|
1930
2026
|
_firstDefaultTopLevelContext() {
|
|
1931
2027
|
for (const [id, userContext] of this._topLevelContextUserContexts) {
|
|
1932
|
-
if (userContext === 'default')
|
|
2028
|
+
if (userContext === 'default' && !this._isInternalContextId(id))
|
|
1933
2029
|
return id;
|
|
1934
2030
|
}
|
|
1935
2031
|
return undefined;
|
|
@@ -1958,13 +2054,17 @@ export class Browser {
|
|
|
1958
2054
|
const conn = this.bidiSession.getConnection();
|
|
1959
2055
|
const handle = await this.driver.getCurrentWindowHandle().catch(() => '');
|
|
1960
2056
|
await this._ensureTopLevelContextTracking();
|
|
1961
|
-
if (handle &&
|
|
2057
|
+
if (handle &&
|
|
2058
|
+
this._topLevelContextUserContexts.get(handle) === 'default' &&
|
|
2059
|
+
!this._isInternalContextId(handle)) {
|
|
1962
2060
|
return new Page(this.driver, handle, this.getDefaultTimeout, conn, this.defaultContext);
|
|
1963
2061
|
}
|
|
1964
2062
|
// Events should keep the cache warm, but a cheap top-level sync covers
|
|
1965
2063
|
// missed startup events and direct protocol/browser changes.
|
|
1966
2064
|
await this._refreshTopLevelContextCache(conn);
|
|
1967
|
-
if (handle &&
|
|
2065
|
+
if (handle &&
|
|
2066
|
+
this._topLevelContextUserContexts.get(handle) === 'default' &&
|
|
2067
|
+
!this._isInternalContextId(handle)) {
|
|
1968
2068
|
return new Page(this.driver, handle, this.getDefaultTimeout, conn, this.defaultContext);
|
|
1969
2069
|
}
|
|
1970
2070
|
const fallback = this._firstDefaultTopLevelContext();
|
|
@@ -2171,7 +2271,14 @@ export class Browser {
|
|
|
2171
2271
|
}
|
|
2172
2272
|
find(selector) {
|
|
2173
2273
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
2174
|
-
return new ElementHandle(this.driver, by, this.getDefaultTimeout)
|
|
2274
|
+
return new ElementHandle(this.driver, by, this.getDefaultTimeout).withBiDi(() => {
|
|
2275
|
+
if (!this.bidiSession?.isConnected())
|
|
2276
|
+
return undefined;
|
|
2277
|
+
const contextId = this.bidiSession.getContext();
|
|
2278
|
+
return contextId
|
|
2279
|
+
? { connection: this.bidiSession.getConnection(), contextId }
|
|
2280
|
+
: undefined;
|
|
2281
|
+
});
|
|
2175
2282
|
}
|
|
2176
2283
|
/**
|
|
2177
2284
|
* Return a lazy, chainable `Locator` for the given selector.
|
|
@@ -2179,7 +2286,14 @@ export class Browser {
|
|
|
2179
2286
|
*/
|
|
2180
2287
|
locator(selector) {
|
|
2181
2288
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
2182
|
-
return new Locator(this.driver, by, this.getDefaultTimeout)
|
|
2289
|
+
return new Locator(this.driver, by, this.getDefaultTimeout).withBiDi(() => {
|
|
2290
|
+
if (!this.bidiSession?.isConnected())
|
|
2291
|
+
return undefined;
|
|
2292
|
+
const contextId = this.bidiSession.getContext();
|
|
2293
|
+
return contextId
|
|
2294
|
+
? { connection: this.bidiSession.getConnection(), contextId }
|
|
2295
|
+
: undefined;
|
|
2296
|
+
});
|
|
2183
2297
|
}
|
|
2184
2298
|
/**
|
|
2185
2299
|
* Return snapshot `ElementHandle`s for all elements matching `selector` at call time.
|
|
@@ -2188,28 +2302,35 @@ export class Browser {
|
|
|
2188
2302
|
async findAll(selector) {
|
|
2189
2303
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
2190
2304
|
const webElements = await this.driver.findElements(by);
|
|
2191
|
-
return webElements.map((we) => ElementHandle.fromWebElement(this.driver, we, this.getDefaultTimeout))
|
|
2305
|
+
return webElements.map((we) => ElementHandle.fromWebElement(this.driver, we, this.getDefaultTimeout).withBiDi(() => {
|
|
2306
|
+
if (!this.bidiSession?.isConnected())
|
|
2307
|
+
return undefined;
|
|
2308
|
+
const contextId = this.bidiSession.getContext();
|
|
2309
|
+
return contextId
|
|
2310
|
+
? { connection: this.bidiSession.getConnection(), contextId }
|
|
2311
|
+
: undefined;
|
|
2312
|
+
}));
|
|
2192
2313
|
}
|
|
2193
2314
|
getByRole(role, opts) {
|
|
2194
|
-
return
|
|
2315
|
+
return this.find(By.role(role, opts));
|
|
2195
2316
|
}
|
|
2196
2317
|
getByText(text, opts) {
|
|
2197
|
-
return
|
|
2318
|
+
return this.find(By.text(text, opts));
|
|
2198
2319
|
}
|
|
2199
2320
|
getByLabel(text, opts) {
|
|
2200
|
-
return
|
|
2321
|
+
return this.find(By.labelText(text, opts));
|
|
2201
2322
|
}
|
|
2202
2323
|
getByPlaceholder(text, opts) {
|
|
2203
|
-
return
|
|
2324
|
+
return this.find(By.placeholder(text, opts));
|
|
2204
2325
|
}
|
|
2205
2326
|
getByTestId(id) {
|
|
2206
|
-
return
|
|
2327
|
+
return this.find(By.testId(id));
|
|
2207
2328
|
}
|
|
2208
2329
|
getByAltText(text, opts) {
|
|
2209
|
-
return
|
|
2330
|
+
return this.find(By.altText(text, opts));
|
|
2210
2331
|
}
|
|
2211
2332
|
getByTitle(text, opts) {
|
|
2212
|
-
return
|
|
2333
|
+
return this.find(By.title(text, opts));
|
|
2213
2334
|
}
|
|
2214
2335
|
// Keyboard controller and enum for nicer usage: browser.keyboard.press(Key.Enter)
|
|
2215
2336
|
keyboard;
|