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.
Files changed (229) hide show
  1. package/CHANGELOG.md +35 -6
  2. package/README.md +17 -6
  3. package/dist/cli/agentSession.d.ts +62 -0
  4. package/dist/cli/agentSession.d.ts.map +1 -0
  5. package/dist/cli/agentSession.js +100 -0
  6. package/dist/cli/agentSession.js.map +1 -0
  7. package/dist/cli/artifactPaths.d.ts +42 -0
  8. package/dist/cli/artifactPaths.d.ts.map +1 -0
  9. package/dist/cli/artifactPaths.js +144 -0
  10. package/dist/cli/artifactPaths.js.map +1 -0
  11. package/dist/cli/bounds.d.ts +88 -0
  12. package/dist/cli/bounds.d.ts.map +1 -0
  13. package/dist/cli/bounds.js +128 -0
  14. package/dist/cli/bounds.js.map +1 -0
  15. package/dist/cli/client.d.ts +3 -0
  16. package/dist/cli/client.d.ts.map +1 -1
  17. package/dist/cli/client.js +24 -8
  18. package/dist/cli/client.js.map +1 -1
  19. package/dist/cli/daemon.d.ts +3 -0
  20. package/dist/cli/daemon.d.ts.map +1 -1
  21. package/dist/cli/daemon.js +152 -39
  22. package/dist/cli/daemon.js.map +1 -1
  23. package/dist/cli/defaults.d.ts +23 -3
  24. package/dist/cli/defaults.d.ts.map +1 -1
  25. package/dist/cli/defaults.js +50 -5
  26. package/dist/cli/defaults.js.map +1 -1
  27. package/dist/cli/dispatcher.d.ts +54 -11
  28. package/dist/cli/dispatcher.d.ts.map +1 -1
  29. package/dist/cli/dispatcher.js +851 -54
  30. package/dist/cli/dispatcher.js.map +1 -1
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +305 -56
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/init.d.ts +14 -14
  35. package/dist/cli/init.d.ts.map +1 -1
  36. package/dist/cli/init.js +328 -180
  37. package/dist/cli/init.js.map +1 -1
  38. package/dist/cli/journal.d.ts +153 -0
  39. package/dist/cli/journal.d.ts.map +1 -0
  40. package/dist/cli/journal.js +252 -0
  41. package/dist/cli/journal.js.map +1 -0
  42. package/dist/cli/lineReader.d.ts +33 -0
  43. package/dist/cli/lineReader.d.ts.map +1 -0
  44. package/dist/cli/lineReader.js +66 -0
  45. package/dist/cli/lineReader.js.map +1 -0
  46. package/dist/cli/locatorCandidates.d.ts +44 -0
  47. package/dist/cli/locatorCandidates.d.ts.map +1 -0
  48. package/dist/cli/locatorCandidates.js +190 -0
  49. package/dist/cli/locatorCandidates.js.map +1 -0
  50. package/dist/cli/mcp/artifacts.d.ts +39 -1
  51. package/dist/cli/mcp/artifacts.d.ts.map +1 -1
  52. package/dist/cli/mcp/artifacts.js +104 -9
  53. package/dist/cli/mcp/artifacts.js.map +1 -1
  54. package/dist/cli/mcp/bounds.d.ts +65 -0
  55. package/dist/cli/mcp/bounds.d.ts.map +1 -0
  56. package/dist/cli/mcp/bounds.js +124 -0
  57. package/dist/cli/mcp/bounds.js.map +1 -0
  58. package/dist/cli/mcp/params.d.ts +38 -0
  59. package/dist/cli/mcp/params.d.ts.map +1 -0
  60. package/dist/cli/mcp/params.js +174 -0
  61. package/dist/cli/mcp/params.js.map +1 -0
  62. package/dist/cli/mcp/server.d.ts +94 -16
  63. package/dist/cli/mcp/server.d.ts.map +1 -1
  64. package/dist/cli/mcp/server.js +304 -150
  65. package/dist/cli/mcp/server.js.map +1 -1
  66. package/dist/cli/mcp/tools.d.ts +62 -17
  67. package/dist/cli/mcp/tools.d.ts.map +1 -1
  68. package/dist/cli/mcp/tools.js +373 -158
  69. package/dist/cli/mcp/tools.js.map +1 -1
  70. package/dist/cli/pageSemantics.d.ts +42 -0
  71. package/dist/cli/pageSemantics.d.ts.map +1 -0
  72. package/dist/cli/pageSemantics.js +194 -0
  73. package/dist/cli/pageSemantics.js.map +1 -0
  74. package/dist/cli/parseArgs.d.ts +6 -18
  75. package/dist/cli/parseArgs.d.ts.map +1 -1
  76. package/dist/cli/parseArgs.js +689 -45
  77. package/dist/cli/parseArgs.js.map +1 -1
  78. package/dist/cli/protocol.d.ts +8 -0
  79. package/dist/cli/protocol.d.ts.map +1 -1
  80. package/dist/cli/sessionRegistry.d.ts +42 -0
  81. package/dist/cli/sessionRegistry.d.ts.map +1 -0
  82. package/dist/cli/sessionRegistry.js +99 -0
  83. package/dist/cli/sessionRegistry.js.map +1 -0
  84. package/dist/cli/snapshot.d.ts +100 -18
  85. package/dist/cli/snapshot.d.ts.map +1 -1
  86. package/dist/cli/snapshot.js +248 -102
  87. package/dist/cli/snapshot.js.map +1 -1
  88. package/dist/cli/stateStore.d.ts +50 -0
  89. package/dist/cli/stateStore.d.ts.map +1 -0
  90. package/dist/cli/stateStore.js +122 -0
  91. package/dist/cli/stateStore.js.map +1 -0
  92. package/dist/index.d.ts +2 -1
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +2 -0
  95. package/dist/index.js.map +1 -1
  96. package/dist/lib/bidi/index.d.ts +1 -1
  97. package/dist/lib/bidi/index.d.ts.map +1 -1
  98. package/dist/lib/bidi/index.js +2 -2
  99. package/dist/lib/bidi/index.js.map +1 -1
  100. package/dist/lib/bidi/logs.d.ts +3 -0
  101. package/dist/lib/bidi/logs.d.ts.map +1 -1
  102. package/dist/lib/bidi/logs.js +7 -0
  103. package/dist/lib/bidi/logs.js.map +1 -1
  104. package/dist/lib/bidi/network.d.ts +14 -1
  105. package/dist/lib/bidi/network.d.ts.map +1 -1
  106. package/dist/lib/bidi/network.js +45 -7
  107. package/dist/lib/bidi/network.js.map +1 -1
  108. package/dist/lib/bidi/storage.d.ts +30 -6
  109. package/dist/lib/bidi/storage.d.ts.map +1 -1
  110. package/dist/lib/bidi/storage.js +190 -45
  111. package/dist/lib/bidi/storage.js.map +1 -1
  112. package/dist/lib/bidi/types.d.ts +7 -2
  113. package/dist/lib/bidi/types.d.ts.map +1 -1
  114. package/dist/lib/browser.d.ts +38 -3
  115. package/dist/lib/browser.d.ts.map +1 -1
  116. package/dist/lib/browser.js +196 -30
  117. package/dist/lib/browser.js.map +1 -1
  118. package/dist/lib/browserContext.d.ts +54 -18
  119. package/dist/lib/browserContext.d.ts.map +1 -1
  120. package/dist/lib/browserContext.js +295 -92
  121. package/dist/lib/browserContext.js.map +1 -1
  122. package/dist/lib/by.d.ts +3 -3
  123. package/dist/lib/by.d.ts.map +1 -1
  124. package/dist/lib/by.js +7 -3
  125. package/dist/lib/by.js.map +1 -1
  126. package/dist/lib/driver.d.ts +7 -0
  127. package/dist/lib/driver.d.ts.map +1 -1
  128. package/dist/lib/driver.js +12 -0
  129. package/dist/lib/driver.js.map +1 -1
  130. package/dist/lib/errors.d.ts +15 -0
  131. package/dist/lib/errors.d.ts.map +1 -1
  132. package/dist/lib/errors.js +15 -0
  133. package/dist/lib/errors.js.map +1 -1
  134. package/dist/lib/initScript.d.ts +9 -0
  135. package/dist/lib/initScript.d.ts.map +1 -0
  136. package/dist/lib/initScript.js +14 -0
  137. package/dist/lib/initScript.js.map +1 -0
  138. package/dist/lib/page.d.ts +22 -0
  139. package/dist/lib/page.d.ts.map +1 -1
  140. package/dist/lib/page.js +48 -0
  141. package/dist/lib/page.js.map +1 -1
  142. package/dist/lib/secureFile.d.ts +8 -0
  143. package/dist/lib/secureFile.d.ts.map +1 -0
  144. package/dist/lib/secureFile.js +67 -0
  145. package/dist/lib/secureFile.js.map +1 -0
  146. package/dist/lib/sessionStateValidation.d.ts +35 -0
  147. package/dist/lib/sessionStateValidation.d.ts.map +1 -0
  148. package/dist/lib/sessionStateValidation.js +223 -0
  149. package/dist/lib/sessionStateValidation.js.map +1 -0
  150. package/dist/lib/timing.d.ts +8 -0
  151. package/dist/lib/timing.d.ts.map +1 -1
  152. package/dist/lib/timing.js +8 -0
  153. package/dist/lib/timing.js.map +1 -1
  154. package/dist/lib/tracing.d.ts +2 -1
  155. package/dist/lib/tracing.d.ts.map +1 -1
  156. package/dist/lib/tracing.js +12 -1
  157. package/dist/lib/tracing.js.map +1 -1
  158. package/dist/lib/visual/antialias.d.ts +41 -0
  159. package/dist/lib/visual/antialias.d.ts.map +1 -0
  160. package/dist/lib/visual/antialias.js +126 -0
  161. package/dist/lib/visual/antialias.js.map +1 -0
  162. package/dist/lib/visual/codec.d.ts +24 -0
  163. package/dist/lib/visual/codec.d.ts.map +1 -0
  164. package/dist/lib/visual/codec.js +11 -0
  165. package/dist/lib/visual/codec.js.map +1 -0
  166. package/dist/lib/visual/compare.d.ts +38 -0
  167. package/dist/lib/visual/compare.d.ts.map +1 -0
  168. package/dist/lib/visual/compare.js +209 -0
  169. package/dist/lib/visual/compare.js.map +1 -0
  170. package/dist/lib/visual/diff.d.ts +18 -0
  171. package/dist/lib/visual/diff.d.ts.map +1 -0
  172. package/dist/lib/visual/diff.js +23 -0
  173. package/dist/lib/visual/diff.js.map +1 -0
  174. package/dist/lib/visual/expectScreenshot.d.ts +65 -0
  175. package/dist/lib/visual/expectScreenshot.d.ts.map +1 -0
  176. package/dist/lib/visual/expectScreenshot.js +287 -0
  177. package/dist/lib/visual/expectScreenshot.js.map +1 -0
  178. package/dist/lib/visual/index.d.ts +10 -0
  179. package/dist/lib/visual/index.d.ts.map +1 -0
  180. package/dist/lib/visual/index.js +9 -0
  181. package/dist/lib/visual/index.js.map +1 -0
  182. package/dist/lib/visual/pixelMath.d.ts +32 -0
  183. package/dist/lib/visual/pixelMath.d.ts.map +1 -0
  184. package/dist/lib/visual/pixelMath.js +70 -0
  185. package/dist/lib/visual/pixelMath.js.map +1 -0
  186. package/dist/lib/visual/pngHeader.d.ts +27 -0
  187. package/dist/lib/visual/pngHeader.d.ts.map +1 -0
  188. package/dist/lib/visual/pngHeader.js +63 -0
  189. package/dist/lib/visual/pngHeader.js.map +1 -0
  190. package/dist/lib/visual/pngjsCodec.d.ts +22 -0
  191. package/dist/lib/visual/pngjsCodec.d.ts.map +1 -0
  192. package/dist/lib/visual/pngjsCodec.js +70 -0
  193. package/dist/lib/visual/pngjsCodec.js.map +1 -0
  194. package/dist/lib/visual/types.d.ts +89 -0
  195. package/dist/lib/visual/types.d.ts.map +1 -0
  196. package/dist/lib/visual/types.js +20 -0
  197. package/dist/lib/visual/types.js.map +1 -0
  198. package/dist/lib/visual/visualError.d.ts +32 -0
  199. package/dist/lib/visual/visualError.d.ts.map +1 -0
  200. package/dist/lib/visual/visualError.js +59 -0
  201. package/dist/lib/visual/visualError.js.map +1 -0
  202. package/dist/lib/wait.d.ts.map +1 -1
  203. package/dist/lib/wait.js +7 -1
  204. package/dist/lib/wait.js.map +1 -1
  205. package/docs/agents.md +56 -61
  206. package/docs/api-reference.md +16 -1
  207. package/docs/browser-api.md +24 -4
  208. package/docs/browser-context.md +18 -12
  209. package/docs/cli.md +346 -26
  210. package/docs/error-codes.md +6 -4
  211. package/docs/getting-started.md +6 -1
  212. package/docs/index.md +1 -1
  213. package/docs/mcp.md +129 -62
  214. package/docs/public/examples/agent-actions.html +48 -0
  215. package/docs/public/examples/agent-debug.html +86 -0
  216. package/docs/public/examples/agent-late-mutation.html +18 -0
  217. package/docs/public/examples/agent-ref-shift.html +33 -0
  218. package/docs/recipes/login-once-reuse-session.md +32 -4
  219. package/docs/screenshots.md +15 -6
  220. package/docs/session-management.md +64 -3
  221. package/docs/visual-testing.md +254 -0
  222. package/docs/why-craftdriver.md +4 -3
  223. package/package.json +5 -1
  224. package/skills/craftdriver/SKILL.md +59 -75
  225. package/skills/craftdriver/agents/openai.yaml +4 -0
  226. package/skills/craftdriver/cheatsheet.md +18 -4
  227. package/skills/craftdriver/cli.md +194 -68
  228. package/skills/craftdriver/patterns.md +14 -8
  229. package/skills/craftdriver/workflow.md +149 -0
@@ -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
- // Load session state if provided
484
- if (options.storageState) {
485
- await browser.loadState(options.storageState);
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
- await browser.loadState(options.storageState);
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(path) {
684
- return this.storage.loadState(path);
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
- return (tree.contexts ?? []).map((ctx) => new Page(this.driver, ctx.context, this.getDefaultTimeout, conn, this._wrapContext(ctx.userContext ?? 'default')));
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
- clearTimeout(timer);
1667
- off();
1668
- resolve(new Page(this.driver, params.context, this.getDefaultTimeout, conn, this._wrapContext(params.userContext ?? 'default')));
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
- if (opts?.storageState !== undefined) {
1809
- await ctx.loadStorageState(opts.storageState);
1810
- }
1811
- // Apply identity & device emulation options. Each setter is `userContexts`-scoped,
1812
- // so future pages in this context inherit automatically.
1813
- if (opts?.locale !== undefined)
1814
- await ctx.setLocale(opts.locale);
1815
- if (opts?.timezoneId !== undefined)
1816
- await ctx.setTimezone(opts.timezoneId);
1817
- if (opts?.geolocation !== undefined)
1818
- await ctx.setGeolocation(opts.geolocation);
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
- this._topLevelContextUserContexts.set(id, typeof params.userContext === 'string' ? params.userContext : '');
1890
- this._topLevelContextCacheVersion++;
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 && this._topLevelContextUserContexts.get(handle) === 'default') {
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 && this._topLevelContextUserContexts.get(handle) === 'default') {
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);