craftdriver 1.6.0 → 1.8.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 +35 -6
- package/README.md +17 -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 +42 -0
- package/dist/cli/pageSemantics.d.ts.map +1 -0
- package/dist/cli/pageSemantics.js +194 -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/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 -18
- package/dist/cli/snapshot.d.ts.map +1 -1
- package/dist/cli/snapshot.js +248 -102
- 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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- 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 +7 -2
- package/dist/lib/bidi/types.d.ts.map +1 -1
- package/dist/lib/browser.d.ts +38 -3
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +196 -30
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +54 -18
- package/dist/lib/browserContext.d.ts.map +1 -1
- package/dist/lib/browserContext.js +295 -92
- package/dist/lib/browserContext.js.map +1 -1
- package/dist/lib/by.d.ts +3 -3
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +7 -3
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/driver.d.ts +7 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +12 -0
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/errors.d.ts +15 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +15 -0
- package/dist/lib/errors.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/page.d.ts +22 -0
- package/dist/lib/page.d.ts.map +1 -1
- package/dist/lib/page.js +48 -0
- package/dist/lib/page.js.map +1 -1
- 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/timing.d.ts +8 -0
- package/dist/lib/timing.d.ts.map +1 -1
- package/dist/lib/timing.js +8 -0
- package/dist/lib/timing.js.map +1 -1
- 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/dist/lib/visual/antialias.d.ts +41 -0
- package/dist/lib/visual/antialias.d.ts.map +1 -0
- package/dist/lib/visual/antialias.js +126 -0
- package/dist/lib/visual/antialias.js.map +1 -0
- package/dist/lib/visual/codec.d.ts +24 -0
- package/dist/lib/visual/codec.d.ts.map +1 -0
- package/dist/lib/visual/codec.js +11 -0
- package/dist/lib/visual/codec.js.map +1 -0
- package/dist/lib/visual/compare.d.ts +38 -0
- package/dist/lib/visual/compare.d.ts.map +1 -0
- package/dist/lib/visual/compare.js +209 -0
- package/dist/lib/visual/compare.js.map +1 -0
- package/dist/lib/visual/diff.d.ts +18 -0
- package/dist/lib/visual/diff.d.ts.map +1 -0
- package/dist/lib/visual/diff.js +23 -0
- package/dist/lib/visual/diff.js.map +1 -0
- package/dist/lib/visual/expectScreenshot.d.ts +65 -0
- package/dist/lib/visual/expectScreenshot.d.ts.map +1 -0
- package/dist/lib/visual/expectScreenshot.js +287 -0
- package/dist/lib/visual/expectScreenshot.js.map +1 -0
- package/dist/lib/visual/index.d.ts +10 -0
- package/dist/lib/visual/index.d.ts.map +1 -0
- package/dist/lib/visual/index.js +9 -0
- package/dist/lib/visual/index.js.map +1 -0
- package/dist/lib/visual/pixelMath.d.ts +32 -0
- package/dist/lib/visual/pixelMath.d.ts.map +1 -0
- package/dist/lib/visual/pixelMath.js +70 -0
- package/dist/lib/visual/pixelMath.js.map +1 -0
- package/dist/lib/visual/pngHeader.d.ts +27 -0
- package/dist/lib/visual/pngHeader.d.ts.map +1 -0
- package/dist/lib/visual/pngHeader.js +63 -0
- package/dist/lib/visual/pngHeader.js.map +1 -0
- package/dist/lib/visual/pngjsCodec.d.ts +22 -0
- package/dist/lib/visual/pngjsCodec.d.ts.map +1 -0
- package/dist/lib/visual/pngjsCodec.js +70 -0
- package/dist/lib/visual/pngjsCodec.js.map +1 -0
- package/dist/lib/visual/types.d.ts +89 -0
- package/dist/lib/visual/types.d.ts.map +1 -0
- package/dist/lib/visual/types.js +20 -0
- package/dist/lib/visual/types.js.map +1 -0
- package/dist/lib/visual/visualError.d.ts +32 -0
- package/dist/lib/visual/visualError.d.ts.map +1 -0
- package/dist/lib/visual/visualError.js +59 -0
- package/dist/lib/visual/visualError.js.map +1 -0
- package/dist/lib/wait.d.ts.map +1 -1
- package/dist/lib/wait.js +7 -1
- package/dist/lib/wait.js.map +1 -1
- package/docs/agents.md +56 -61
- package/docs/api-reference.md +16 -1
- package/docs/browser-api.md +24 -4
- package/docs/browser-context.md +18 -12
- package/docs/cli.md +346 -26
- package/docs/error-codes.md +6 -4
- package/docs/getting-started.md +6 -1
- package/docs/index.md +1 -1
- package/docs/mcp.md +129 -62
- 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/recipes/login-once-reuse-session.md +32 -4
- package/docs/screenshots.md +15 -6
- package/docs/session-management.md +64 -3
- package/docs/visual-testing.md +254 -0
- package/docs/why-craftdriver.md +4 -3
- package/package.json +5 -1
- 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 +14 -8
- package/skills/craftdriver/workflow.md +149 -0
package/dist/lib/browser.js
CHANGED
|
@@ -33,9 +33,12 @@ 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';
|
|
40
|
+
import { runExpectScreenshot, shouldUpdateVisualBaselines } from './visual/index.js';
|
|
41
|
+
import { publicPageInitScript } from './initScript.js';
|
|
39
42
|
/** Common mobile device presets */
|
|
40
43
|
export const devices = {
|
|
41
44
|
'iPhone 14': {
|
|
@@ -268,6 +271,7 @@ export class Browser {
|
|
|
268
271
|
_topLevelContextTracking;
|
|
269
272
|
_topLevelContextTrackingOffs = [];
|
|
270
273
|
_topLevelContextCacheVersion = 0;
|
|
274
|
+
_destroyedTopLevelContextIds = new Set();
|
|
271
275
|
_logs;
|
|
272
276
|
_tracer;
|
|
273
277
|
_storage;
|
|
@@ -480,9 +484,19 @@ export class Browser {
|
|
|
480
484
|
if (wsUrl && bidiRequested) {
|
|
481
485
|
await browser.initBiDi(wsUrl);
|
|
482
486
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
487
|
+
if (options.storageState !== undefined) {
|
|
488
|
+
const protocol = browser.bidiSession?.isConnected() ? 'bidi' : 'classic';
|
|
489
|
+
const errorContext = { operation: 'Browser.launch', browserName: name, protocol };
|
|
490
|
+
const state = await parseSessionState(options.storageState, errorContext);
|
|
491
|
+
if (browser.bidiSession?.isConnected()) {
|
|
492
|
+
await browser.defaultContext._loadStorageState(state, 'Browser.launch');
|
|
493
|
+
}
|
|
494
|
+
else if (!isSessionStateEmpty(state)) {
|
|
495
|
+
throw new CraftdriverError(ErrorCode.UNSUPPORTED, 'non-empty storageState cannot be restored at WebDriver Classic launch', {
|
|
496
|
+
detail: storageStateDetail(errorContext, 'capability', { partialApplied: false }),
|
|
497
|
+
hint: 'Enable BiDi, or launch first, navigate to the sole origin, then call browser.loadState().',
|
|
498
|
+
});
|
|
499
|
+
}
|
|
486
500
|
}
|
|
487
501
|
}
|
|
488
502
|
catch (err) {
|
|
@@ -516,8 +530,23 @@ export class Browser {
|
|
|
516
530
|
if (wsUrl && target.bidiRequested) {
|
|
517
531
|
await browser.initBiDi(wsUrl);
|
|
518
532
|
}
|
|
519
|
-
if (options.storageState) {
|
|
520
|
-
|
|
533
|
+
if (options.storageState !== undefined) {
|
|
534
|
+
const protocol = browser.bidiSession?.isConnected() ? 'bidi' : 'classic';
|
|
535
|
+
const errorContext = {
|
|
536
|
+
operation: 'Browser.launch',
|
|
537
|
+
browserName: target.browserName,
|
|
538
|
+
protocol,
|
|
539
|
+
};
|
|
540
|
+
const state = await parseSessionState(options.storageState, errorContext);
|
|
541
|
+
if (browser.bidiSession?.isConnected()) {
|
|
542
|
+
await browser.defaultContext._loadStorageState(state, 'Browser.launch');
|
|
543
|
+
}
|
|
544
|
+
else if (!isSessionStateEmpty(state)) {
|
|
545
|
+
throw new CraftdriverError(ErrorCode.UNSUPPORTED, 'non-empty storageState cannot be restored at WebDriver Classic launch', {
|
|
546
|
+
detail: storageStateDetail(errorContext, 'capability', { partialApplied: false }),
|
|
547
|
+
hint: 'Enable BiDi, or launch first, navigate to the sole origin, then call browser.loadState().',
|
|
548
|
+
});
|
|
549
|
+
}
|
|
521
550
|
}
|
|
522
551
|
}
|
|
523
552
|
catch (err) {
|
|
@@ -549,6 +578,8 @@ export class Browser {
|
|
|
549
578
|
this._topLevelContextTracking = this._startTopLevelContextTracking(contexts);
|
|
550
579
|
},
|
|
551
580
|
});
|
|
581
|
+
session.network.setInternalContextPredicate((context) => this._isInternalContextId(context));
|
|
582
|
+
session.logs.setInternalContextPredicate((context) => this._isInternalContextId(context));
|
|
552
583
|
await this._ensureTopLevelContextTracking().catch(() => { });
|
|
553
584
|
return; // success
|
|
554
585
|
}
|
|
@@ -617,7 +648,13 @@ export class Browser {
|
|
|
617
648
|
*/
|
|
618
649
|
get storage() {
|
|
619
650
|
if (!this._storage) {
|
|
620
|
-
this._storage = new SessionStateManager(this.driver, this.bidiSession?.isConnected() ? this.bidiSession.getConnection() : null
|
|
651
|
+
this._storage = new SessionStateManager(this.driver, this.bidiSession?.isConnected() ? this.bidiSession.getConnection() : null, undefined, {
|
|
652
|
+
browserName: this._browserName,
|
|
653
|
+
protocol: this.bidiSession?.isConnected() ? 'bidi' : 'classic',
|
|
654
|
+
runSerializedRestore: this.bidiSession?.isConnected()
|
|
655
|
+
? (task) => this.defaultContext._runSerializedRestore(task)
|
|
656
|
+
: undefined,
|
|
657
|
+
});
|
|
621
658
|
}
|
|
622
659
|
return this._storage;
|
|
623
660
|
}
|
|
@@ -680,8 +717,23 @@ export class Browser {
|
|
|
680
717
|
/**
|
|
681
718
|
* Load session state from file
|
|
682
719
|
*/
|
|
683
|
-
async loadState(
|
|
684
|
-
|
|
720
|
+
async loadState(source) {
|
|
721
|
+
const protocol = this.bidiSession?.isConnected() ? 'bidi' : 'classic';
|
|
722
|
+
const state = await parseSessionState(source, {
|
|
723
|
+
operation: 'browser.loadState',
|
|
724
|
+
browserName: this._browserName,
|
|
725
|
+
protocol,
|
|
726
|
+
});
|
|
727
|
+
// BiDi with no sessionStorage → the validated hydrator (multi-origin, no
|
|
728
|
+
// active page required). Otherwise (Classic, or sessionStorage present) take
|
|
729
|
+
// the active-page path, which restores a single origin matching the active
|
|
730
|
+
// page and can carry sessionStorage.
|
|
731
|
+
if (this.bidiSession?.isConnected() && !hasNonEmptySessionStorage(state)) {
|
|
732
|
+
await this.defaultContext._loadStorageState(state, 'browser.loadState');
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
await this.storage.setState(state, 'browser.loadState');
|
|
736
|
+
}
|
|
685
737
|
}
|
|
686
738
|
async _runTracedAction(name, args, selector, run) {
|
|
687
739
|
const actionIndex = this._tracer?.recordAction(name, args, selector);
|
|
@@ -1134,6 +1186,7 @@ export class Browser {
|
|
|
1134
1186
|
this._topLevelContextTrackingOffs = [];
|
|
1135
1187
|
this._topLevelContextTracking = undefined;
|
|
1136
1188
|
this._topLevelContextUserContexts.clear();
|
|
1189
|
+
this._destroyedTopLevelContextIds.clear();
|
|
1137
1190
|
this._topLevelContextCacheVersion++;
|
|
1138
1191
|
// DELETE the WebDriver session — this tells the driver service to close the browser.
|
|
1139
1192
|
await this.driver.quit().catch(() => { });
|
|
@@ -1178,7 +1231,7 @@ export class Browser {
|
|
|
1178
1231
|
'Tracing relies on BiDi events; Classic WebDriver does not expose them.', { detail: { browserName: this._browserName, feature: 'startTrace' } });
|
|
1179
1232
|
}
|
|
1180
1233
|
if (!this._tracer) {
|
|
1181
|
-
this._tracer = new Tracer(this, this.bidiSession.getConnection(), this._engine);
|
|
1234
|
+
this._tracer = new Tracer(this, this.bidiSession.getConnection(), this._engine, (context) => this._isInternalContextId(context));
|
|
1182
1235
|
}
|
|
1183
1236
|
await this._tracer.start(opts);
|
|
1184
1237
|
}
|
|
@@ -1277,7 +1330,7 @@ export class Browser {
|
|
|
1277
1330
|
const conn = this.bidiSession.getConnection();
|
|
1278
1331
|
const fnSrc = typeof fnOrSrc === 'function' ? fnOrSrc.toString() : `() => { ${fnOrSrc} }`;
|
|
1279
1332
|
const result = await conn.send('script.addPreloadScript', {
|
|
1280
|
-
functionDeclaration: fnSrc,
|
|
1333
|
+
functionDeclaration: publicPageInitScript(fnSrc),
|
|
1281
1334
|
});
|
|
1282
1335
|
const scriptId = result.script;
|
|
1283
1336
|
this._browserInitScriptIds.add(scriptId);
|
|
@@ -1610,7 +1663,14 @@ export class Browser {
|
|
|
1610
1663
|
const tree = await conn.send('browsingContext.getTree', {
|
|
1611
1664
|
maxDepth: 0,
|
|
1612
1665
|
});
|
|
1613
|
-
|
|
1666
|
+
const pages = [];
|
|
1667
|
+
for (const ctx of tree.contexts ?? []) {
|
|
1668
|
+
const owner = this._wrapContext(ctx.userContext ?? 'default');
|
|
1669
|
+
if (await owner._isInternalPageContext(ctx.context))
|
|
1670
|
+
continue;
|
|
1671
|
+
pages.push(new Page(this.driver, ctx.context, this.getDefaultTimeout, conn, owner));
|
|
1672
|
+
}
|
|
1673
|
+
return pages;
|
|
1614
1674
|
}
|
|
1615
1675
|
// Classic fallback: use window handles
|
|
1616
1676
|
const handles = await this.driver.getWindowHandles();
|
|
@@ -1656,19 +1716,38 @@ export class Browser {
|
|
|
1656
1716
|
// `BiDiSession.connect()` (which must have run for `isConnected()` to be
|
|
1657
1717
|
// true), so no per-call `subscribe()` round trip is needed here.
|
|
1658
1718
|
return new Promise((resolve, reject) => {
|
|
1719
|
+
let settled = false;
|
|
1659
1720
|
const timer = setTimeout(() => {
|
|
1721
|
+
settled = true;
|
|
1660
1722
|
off();
|
|
1661
1723
|
reject(new Error(`waitForPage() timed out after ${timeout}ms`));
|
|
1662
1724
|
}, timeout);
|
|
1663
1725
|
const off = conn.on('browsingContext.contextCreated', (params) => {
|
|
1664
1726
|
// Only top-level contexts have no parent
|
|
1665
1727
|
if (!params.parent) {
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1728
|
+
const id = params.context;
|
|
1729
|
+
const owner = this._wrapContext(params.userContext ?? 'default');
|
|
1730
|
+
void owner._isInternalPageContext(id).then((internal) => {
|
|
1731
|
+
if (internal || settled)
|
|
1732
|
+
return;
|
|
1733
|
+
settled = true;
|
|
1734
|
+
clearTimeout(timer);
|
|
1735
|
+
off();
|
|
1736
|
+
resolve(new Page(this.driver, id, this.getDefaultTimeout, conn, owner));
|
|
1737
|
+
}).catch((err) => {
|
|
1738
|
+
if (settled)
|
|
1739
|
+
return;
|
|
1740
|
+
settled = true;
|
|
1741
|
+
clearTimeout(timer);
|
|
1742
|
+
off();
|
|
1743
|
+
reject(err);
|
|
1744
|
+
});
|
|
1669
1745
|
}
|
|
1670
1746
|
});
|
|
1671
1747
|
action().catch((err) => {
|
|
1748
|
+
if (settled)
|
|
1749
|
+
return;
|
|
1750
|
+
settled = true;
|
|
1672
1751
|
clearTimeout(timer);
|
|
1673
1752
|
off();
|
|
1674
1753
|
reject(err);
|
|
@@ -1796,6 +1875,15 @@ export class Browser {
|
|
|
1796
1875
|
});
|
|
1797
1876
|
return ctx;
|
|
1798
1877
|
}
|
|
1878
|
+
_isInternalContextId(context) {
|
|
1879
|
+
if (typeof context !== 'string')
|
|
1880
|
+
return false;
|
|
1881
|
+
for (const ctx of this._contextsById.values()) {
|
|
1882
|
+
if (ctx._isInternalPageId(context))
|
|
1883
|
+
return true;
|
|
1884
|
+
}
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1799
1887
|
async newContext(opts) {
|
|
1800
1888
|
this.requireBiDi('newContext()', 'newContext() requires BiDi (enableBiDi: true). ' +
|
|
1801
1889
|
'WebDriver Classic has no concept of user contexts.');
|
|
@@ -1805,17 +1893,25 @@ export class Browser {
|
|
|
1805
1893
|
baseURL: opts?.baseURL,
|
|
1806
1894
|
extraHTTPHeaders: opts?.extraHTTPHeaders,
|
|
1807
1895
|
});
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1896
|
+
try {
|
|
1897
|
+
if (opts?.storageState !== undefined) {
|
|
1898
|
+
await ctx._loadStorageState(opts.storageState, 'browser.newContext');
|
|
1899
|
+
}
|
|
1900
|
+
// Apply identity & device emulation options. Each setter is `userContexts`-scoped,
|
|
1901
|
+
// so future pages in this context inherit automatically.
|
|
1902
|
+
if (opts?.locale !== undefined)
|
|
1903
|
+
await ctx.setLocale(opts.locale);
|
|
1904
|
+
if (opts?.timezoneId !== undefined)
|
|
1905
|
+
await ctx.setTimezone(opts.timezoneId);
|
|
1906
|
+
if (opts?.geolocation !== undefined)
|
|
1907
|
+
await ctx.setGeolocation(opts.geolocation);
|
|
1908
|
+
}
|
|
1909
|
+
catch (err) {
|
|
1910
|
+
// Restore/emulation failed — tear the just-created context down so a
|
|
1911
|
+
// rejected newContext() never leaves an orphaned user context behind.
|
|
1912
|
+
await ctx.close().catch(() => { });
|
|
1913
|
+
throw err;
|
|
1914
|
+
}
|
|
1819
1915
|
return ctx;
|
|
1820
1916
|
}
|
|
1821
1917
|
/**
|
|
@@ -1886,13 +1982,21 @@ export class Browser {
|
|
|
1886
1982
|
const id = params.context;
|
|
1887
1983
|
if (typeof id !== 'string')
|
|
1888
1984
|
return;
|
|
1889
|
-
|
|
1890
|
-
this.
|
|
1985
|
+
const userContext = typeof params.userContext === 'string' ? params.userContext : 'default';
|
|
1986
|
+
this._destroyedTopLevelContextIds.delete(id);
|
|
1987
|
+
const owner = this._wrapContext(userContext);
|
|
1988
|
+
void owner._isInternalPageContext(id).then((internal) => {
|
|
1989
|
+
if (internal || this._destroyedTopLevelContextIds.has(id))
|
|
1990
|
+
return;
|
|
1991
|
+
this._topLevelContextUserContexts.set(id, userContext);
|
|
1992
|
+
this._topLevelContextCacheVersion++;
|
|
1993
|
+
}).catch(() => { });
|
|
1891
1994
|
};
|
|
1892
1995
|
const onDestroyed = (params) => {
|
|
1893
1996
|
if (params.parent)
|
|
1894
1997
|
return;
|
|
1895
1998
|
if (typeof params.context === 'string') {
|
|
1999
|
+
this._destroyedTopLevelContextIds.add(params.context);
|
|
1896
2000
|
this._topLevelContextUserContexts.delete(params.context);
|
|
1897
2001
|
this._topLevelContextCacheVersion++;
|
|
1898
2002
|
}
|
|
@@ -1921,6 +2025,9 @@ export class Browser {
|
|
|
1921
2025
|
}
|
|
1922
2026
|
for (const ctx of tree.contexts ?? []) {
|
|
1923
2027
|
if (!ctx.parent) {
|
|
2028
|
+
const owner = this._wrapContext(ctx.userContext ?? 'default');
|
|
2029
|
+
if (await owner._isInternalPageContext(ctx.context))
|
|
2030
|
+
continue;
|
|
1924
2031
|
this._topLevelContextUserContexts.set(ctx.context, ctx.userContext ?? 'default');
|
|
1925
2032
|
}
|
|
1926
2033
|
}
|
|
@@ -1928,7 +2035,7 @@ export class Browser {
|
|
|
1928
2035
|
}
|
|
1929
2036
|
_firstDefaultTopLevelContext() {
|
|
1930
2037
|
for (const [id, userContext] of this._topLevelContextUserContexts) {
|
|
1931
|
-
if (userContext === 'default')
|
|
2038
|
+
if (userContext === 'default' && !this._isInternalContextId(id))
|
|
1932
2039
|
return id;
|
|
1933
2040
|
}
|
|
1934
2041
|
return undefined;
|
|
@@ -1957,13 +2064,17 @@ export class Browser {
|
|
|
1957
2064
|
const conn = this.bidiSession.getConnection();
|
|
1958
2065
|
const handle = await this.driver.getCurrentWindowHandle().catch(() => '');
|
|
1959
2066
|
await this._ensureTopLevelContextTracking();
|
|
1960
|
-
if (handle &&
|
|
2067
|
+
if (handle &&
|
|
2068
|
+
this._topLevelContextUserContexts.get(handle) === 'default' &&
|
|
2069
|
+
!this._isInternalContextId(handle)) {
|
|
1961
2070
|
return new Page(this.driver, handle, this.getDefaultTimeout, conn, this.defaultContext);
|
|
1962
2071
|
}
|
|
1963
2072
|
// Events should keep the cache warm, but a cheap top-level sync covers
|
|
1964
2073
|
// missed startup events and direct protocol/browser changes.
|
|
1965
2074
|
await this._refreshTopLevelContextCache(conn);
|
|
1966
|
-
if (handle &&
|
|
2075
|
+
if (handle &&
|
|
2076
|
+
this._topLevelContextUserContexts.get(handle) === 'default' &&
|
|
2077
|
+
!this._isInternalContextId(handle)) {
|
|
1967
2078
|
return new Page(this.driver, handle, this.getDefaultTimeout, conn, this.defaultContext);
|
|
1968
2079
|
}
|
|
1969
2080
|
const fallback = this._firstDefaultTopLevelContext();
|
|
@@ -2109,6 +2220,61 @@ export class Browser {
|
|
|
2109
2220
|
await fs.writeFile(opts.path, buf);
|
|
2110
2221
|
return buf;
|
|
2111
2222
|
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Assert that a screenshot matches a baseline PNG on disk, auto-retrying
|
|
2225
|
+
* until it matches or `timeout` elapses. Returns the match result on success;
|
|
2226
|
+
* throws `VisualMismatchError` (code `VISUAL_MISMATCH`) carrying the final
|
|
2227
|
+
* actual and diff PNG buffers on failure.
|
|
2228
|
+
*
|
|
2229
|
+
* Baselines are managed for you, WebdriverIO-style:
|
|
2230
|
+
* - **Missing** (`expectedPath` doesn't exist): the screenshot is captured
|
|
2231
|
+
* until it settles, written as the new baseline, and the assertion passes
|
|
2232
|
+
* (`result.baseline === 'created'`). This is always on — no flag required.
|
|
2233
|
+
* - **Matches**: passes (`result.baseline === 'matched'`).
|
|
2234
|
+
* - **Differs**: throws `VisualMismatchError` — unless
|
|
2235
|
+
* `CRAFTDRIVER_UPDATE_VISUAL_BASELINES=true`, in which case the baseline is
|
|
2236
|
+
* overwritten with the new screenshot and the assertion passes
|
|
2237
|
+
* (`result.baseline === 'updated'`). Creates and updates are reported to
|
|
2238
|
+
* stderr; behaviour is identical locally and in CI.
|
|
2239
|
+
*
|
|
2240
|
+
* A baseline that is present but unreadable/corrupt stays a hard error even
|
|
2241
|
+
* under update mode — it is never overwritten. Comparison supports per-channel
|
|
2242
|
+
* RGB tolerance plus max different-pixel count and percentage; anti-aliasing
|
|
2243
|
+
* can optionally be ignored. `screenshot.fullPage` requires BiDi and is
|
|
2244
|
+
* mutually exclusive with `screenshot.selector`.
|
|
2245
|
+
*
|
|
2246
|
+
* @example
|
|
2247
|
+
* // First run creates 'baselines/home.png'; later runs assert against it.
|
|
2248
|
+
* await browser.expectScreenshot('baselines/home.png', {
|
|
2249
|
+
* screenshot: { fullPage: true },
|
|
2250
|
+
* maxDiffPixels: 100,
|
|
2251
|
+
* });
|
|
2252
|
+
*/
|
|
2253
|
+
async expectScreenshot(expectedPath, options = {}) {
|
|
2254
|
+
// Parsed before the first capture so a malformed value fails fast.
|
|
2255
|
+
const update = shouldUpdateVisualBaselines(process.env.CRAFTDRIVER_UPDATE_VISUAL_BASELINES);
|
|
2256
|
+
const scope = options.screenshot;
|
|
2257
|
+
const capture = (remainingMs) => {
|
|
2258
|
+
if (scope && 'selector' in scope && scope.selector !== undefined) {
|
|
2259
|
+
// Bound the element-visibility wait by the remaining assertion time so
|
|
2260
|
+
// one capture can't ignore the outer deadline. The wait still evaluates
|
|
2261
|
+
// visibility at least once even at 0 ms (see WebDriverWait.until), so a
|
|
2262
|
+
// present element is captured without extending the deadline.
|
|
2263
|
+
return this.screenshot({ selector: scope.selector, timeout: remainingMs });
|
|
2264
|
+
}
|
|
2265
|
+
if (scope && 'fullPage' in scope && scope.fullPage === true) {
|
|
2266
|
+
return this.screenshot({ fullPage: true });
|
|
2267
|
+
}
|
|
2268
|
+
return this.screenshot();
|
|
2269
|
+
};
|
|
2270
|
+
return runExpectScreenshot({
|
|
2271
|
+
expectedPath,
|
|
2272
|
+
options,
|
|
2273
|
+
defaultTimeout: this.defaults.timeout,
|
|
2274
|
+
capture,
|
|
2275
|
+
update,
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2112
2278
|
expect(selector) {
|
|
2113
2279
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
2114
2280
|
return expectSelector(this.driver, by, this.getDefaultTimeout);
|