@orkestrel/browser 0.0.23 → 0.0.25

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/dist/bin/main.js CHANGED
@@ -1,16 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseBoolean, parseInteger } from "@orkestrel/contract";
3
3
  import { BrowserError, isBrowserError } from "../src/core/index.js";
4
- import { createBrowserMCPServer } from "../src/server/index.js";
4
+ import { createBrowserMCPServer, parseBrowserViewport } from "../src/server/index.js";
5
5
  //#region src/bin/main.ts
6
6
  process.stderr.on("error", () => {
7
7
  process.exitCode = 1;
8
8
  });
9
9
  try {
10
- const { BROWSE_ROOT, BROWSE_HEADLESS, BROWSE_EXECUTABLE, BROWSE_READONLY, BROWSE_POOL } = process.env;
10
+ const { BROWSE_ROOT, BROWSE_HEADLESS, BROWSE_EXECUTABLE, BROWSE_READONLY, BROWSE_POOL, BROWSE_CONTEXTS, BROWSE_VIEWPORT } = process.env;
11
11
  const headless = parseBoolean(BROWSE_HEADLESS);
12
12
  const readonly = parseBoolean(BROWSE_READONLY);
13
13
  const size = parseInteger(BROWSE_POOL);
14
+ const contexts = parseInteger(BROWSE_CONTEXTS);
15
+ const viewport = parseBrowserViewport(BROWSE_VIEWPORT);
14
16
  for (const [name, value, parsed] of [[
15
17
  "BROWSE_HEADLESS",
16
18
  BROWSE_HEADLESS,
@@ -27,12 +29,24 @@ try {
27
29
  name: "BROWSE_POOL",
28
30
  value: BROWSE_POOL
29
31
  });
32
+ if (BROWSE_VIEWPORT !== void 0 && BROWSE_VIEWPORT !== "" && viewport === void 0) throw new BrowserError(`BROWSE_VIEWPORT must be positive integers in WIDTHxHEIGHT form, not ${JSON.stringify(BROWSE_VIEWPORT)}`, "BROWSER_SERVER_ENVIRONMENT", {
33
+ name: "BROWSE_VIEWPORT",
34
+ value: BROWSE_VIEWPORT
35
+ });
36
+ if (BROWSE_CONTEXTS !== void 0 && BROWSE_CONTEXTS !== "" && contexts === void 0) throw new BrowserError(`BROWSE_CONTEXTS must be an integer, not ${JSON.stringify(BROWSE_CONTEXTS)}`, "BROWSER_SERVER_ENVIRONMENT", {
37
+ name: "BROWSE_CONTEXTS",
38
+ value: BROWSE_CONTEXTS
39
+ });
30
40
  await createBrowserMCPServer({
31
41
  ...BROWSE_ROOT === void 0 || BROWSE_ROOT === "" ? {} : { root: BROWSE_ROOT },
32
42
  ...headless === void 0 ? {} : { headless },
33
43
  ...BROWSE_EXECUTABLE === void 0 || BROWSE_EXECUTABLE === "" ? {} : { executable: BROWSE_EXECUTABLE },
34
44
  ...readonly === void 0 ? {} : { readonly },
35
- ...size === void 0 ? {} : { pool: { size } }
45
+ pool: {
46
+ ...size === void 0 ? {} : { size },
47
+ ...contexts === void 0 ? {} : { contexts }
48
+ },
49
+ ...viewport === void 0 ? {} : { viewport }
36
50
  }).start();
37
51
  } catch (error) {
38
52
  if (!isBrowserError(error)) throw error;
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","names":[],"sources":["../../src/bin/main.ts"],"sourcesContent":["import { parseBoolean, parseInteger } from '@orkestrel/contract'\nimport { BrowserError, isBrowserError } from '@src/core'\nimport { createBrowserMCPServer } from '@src/server'\n\n// The entry owns stderr; a closed reader must not interrupt the server's teardown barrier.\nprocess.stderr.on('error', () => {\n\tprocess.exitCode = 1\n})\n\ntry {\n\tconst { BROWSE_ROOT, BROWSE_HEADLESS, BROWSE_EXECUTABLE, BROWSE_READONLY, BROWSE_POOL } =\n\t\tprocess.env\n\tconst headless = parseBoolean(BROWSE_HEADLESS)\n\tconst readonly = parseBoolean(BROWSE_READONLY)\n\tconst size = parseInteger(BROWSE_POOL)\n\t// An empty variable counts as unset, as a shell's `NAME=` leaves it.\n\tfor (const [name, value, parsed] of [\n\t\t['BROWSE_HEADLESS', BROWSE_HEADLESS, headless],\n\t\t['BROWSE_READONLY', BROWSE_READONLY, readonly],\n\t] as const) {\n\t\tif (value !== undefined && value !== '' && parsed === undefined) {\n\t\t\tthrow new BrowserError(\n\t\t\t\t`${name} must be true, false, 1, or 0, not ${JSON.stringify(value)}`,\n\t\t\t\t'BROWSER_SERVER_ENVIRONMENT',\n\t\t\t\t{ name, value },\n\t\t\t)\n\t\t}\n\t}\n\tif (BROWSE_POOL !== undefined && BROWSE_POOL !== '' && size === undefined)\n\t\tthrow new BrowserError(\n\t\t\t`BROWSE_POOL must be an integer, not ${JSON.stringify(BROWSE_POOL)}`,\n\t\t\t'BROWSER_SERVER_ENVIRONMENT',\n\t\t\t{ name: 'BROWSE_POOL', value: BROWSE_POOL },\n\t\t)\n\tawait createBrowserMCPServer({\n\t\t...(BROWSE_ROOT === undefined || BROWSE_ROOT === '' ? {} : { root: BROWSE_ROOT }),\n\t\t...(headless === undefined ? {} : { headless }),\n\t\t...(BROWSE_EXECUTABLE === undefined || BROWSE_EXECUTABLE === ''\n\t\t\t? {}\n\t\t\t: { executable: BROWSE_EXECUTABLE }),\n\t\t...(readonly === undefined ? {} : { readonly }),\n\t\t...(size === undefined ? {} : { pool: { size } }),\n\t}).start()\n} catch (error) {\n\tif (!isBrowserError(error)) throw error\n\tconsole.error(`browse: ${error.code}: ${error.message.split(/\\r\\n|\\n/u).join(' ')}`)\n\tprocess.exitCode = 1\n}\n"],"mappings":";;;;;AAKA,QAAQ,OAAO,GAAG,eAAe;CAChC,QAAQ,WAAW;AACpB,CAAC;AAED,IAAI;CACH,MAAM,EAAE,aAAa,iBAAiB,mBAAmB,iBAAiB,gBACzE,QAAQ;CACT,MAAM,WAAW,aAAa,eAAe;CAC7C,MAAM,WAAW,aAAa,eAAe;CAC7C,MAAM,OAAO,aAAa,WAAW;CAErC,KAAK,MAAM,CAAC,MAAM,OAAO,WAAW,CACnC;EAAC;EAAmB;EAAiB;CAAQ,GAC7C;EAAC;EAAmB;EAAiB;CAAQ,CAC9C,GACC,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,WAAW,KAAA,GACrD,MAAM,IAAI,aACT,GAAG,KAAK,qCAAqC,KAAK,UAAU,KAAK,KACjE,8BACA;EAAE;EAAM;CAAM,CACf;CAGF,IAAI,gBAAgB,KAAA,KAAa,gBAAgB,MAAM,SAAS,KAAA,GAC/D,MAAM,IAAI,aACT,uCAAuC,KAAK,UAAU,WAAW,KACjE,8BACA;EAAE,MAAM;EAAe,OAAO;CAAY,CAC3C;CACD,MAAM,uBAAuB;EAC5B,GAAI,gBAAgB,KAAA,KAAa,gBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,YAAY;EAC/E,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;EAC7C,GAAI,sBAAsB,KAAA,KAAa,sBAAsB,KAC1D,CAAC,IACD,EAAE,YAAY,kBAAkB;EACnC,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;EAC7C,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;CAChD,CAAC,CAAC,CAAC,MAAM;AACV,SAAS,OAAO;CACf,IAAI,CAAC,eAAe,KAAK,GAAG,MAAM;CAClC,QAAQ,MAAM,WAAW,MAAM,KAAK,IAAI,MAAM,QAAQ,MAAM,UAAU,CAAC,CAAC,KAAK,GAAG,GAAG;CACnF,QAAQ,WAAW;AACpB"}
1
+ {"version":3,"file":"main.js","names":[],"sources":["../../src/bin/main.ts"],"sourcesContent":["import { parseBoolean, parseInteger } from '@orkestrel/contract'\nimport { BrowserError, isBrowserError } from '@src/core'\nimport { createBrowserMCPServer, parseBrowserViewport } from '@src/server'\n\n// The entry owns stderr; a closed reader must not interrupt the server's teardown barrier.\nprocess.stderr.on('error', () => {\n\tprocess.exitCode = 1\n})\n\ntry {\n\tconst {\n\t\tBROWSE_ROOT,\n\t\tBROWSE_HEADLESS,\n\t\tBROWSE_EXECUTABLE,\n\t\tBROWSE_READONLY,\n\t\tBROWSE_POOL,\n\t\tBROWSE_CONTEXTS,\n\t\tBROWSE_VIEWPORT,\n\t} = process.env\n\tconst headless = parseBoolean(BROWSE_HEADLESS)\n\tconst readonly = parseBoolean(BROWSE_READONLY)\n\tconst size = parseInteger(BROWSE_POOL)\n\tconst contexts = parseInteger(BROWSE_CONTEXTS)\n\tconst viewport = parseBrowserViewport(BROWSE_VIEWPORT)\n\t// An empty variable counts as unset, as a shell's `NAME=` leaves it.\n\tfor (const [name, value, parsed] of [\n\t\t['BROWSE_HEADLESS', BROWSE_HEADLESS, headless],\n\t\t['BROWSE_READONLY', BROWSE_READONLY, readonly],\n\t] as const) {\n\t\tif (value !== undefined && value !== '' && parsed === undefined) {\n\t\t\tthrow new BrowserError(\n\t\t\t\t`${name} must be true, false, 1, or 0, not ${JSON.stringify(value)}`,\n\t\t\t\t'BROWSER_SERVER_ENVIRONMENT',\n\t\t\t\t{ name, value },\n\t\t\t)\n\t\t}\n\t}\n\tif (BROWSE_POOL !== undefined && BROWSE_POOL !== '' && size === undefined)\n\t\tthrow new BrowserError(\n\t\t\t`BROWSE_POOL must be an integer, not ${JSON.stringify(BROWSE_POOL)}`,\n\t\t\t'BROWSER_SERVER_ENVIRONMENT',\n\t\t\t{ name: 'BROWSE_POOL', value: BROWSE_POOL },\n\t\t)\n\tif (BROWSE_VIEWPORT !== undefined && BROWSE_VIEWPORT !== '' && viewport === undefined)\n\t\tthrow new BrowserError(\n\t\t\t`BROWSE_VIEWPORT must be positive integers in WIDTHxHEIGHT form, not ${JSON.stringify(BROWSE_VIEWPORT)}`,\n\t\t\t'BROWSER_SERVER_ENVIRONMENT',\n\t\t\t{ name: 'BROWSE_VIEWPORT', value: BROWSE_VIEWPORT },\n\t\t)\n\tif (BROWSE_CONTEXTS !== undefined && BROWSE_CONTEXTS !== '' && contexts === undefined)\n\t\tthrow new BrowserError(\n\t\t\t`BROWSE_CONTEXTS must be an integer, not ${JSON.stringify(BROWSE_CONTEXTS)}`,\n\t\t\t'BROWSER_SERVER_ENVIRONMENT',\n\t\t\t{ name: 'BROWSE_CONTEXTS', value: BROWSE_CONTEXTS },\n\t\t)\n\tawait createBrowserMCPServer({\n\t\t...(BROWSE_ROOT === undefined || BROWSE_ROOT === '' ? {} : { root: BROWSE_ROOT }),\n\t\t...(headless === undefined ? {} : { headless }),\n\t\t...(BROWSE_EXECUTABLE === undefined || BROWSE_EXECUTABLE === ''\n\t\t\t? {}\n\t\t\t: { executable: BROWSE_EXECUTABLE }),\n\t\t...(readonly === undefined ? {} : { readonly }),\n\t\tpool: {\n\t\t\t...(size === undefined ? {} : { size }),\n\t\t\t...(contexts === undefined ? {} : { contexts }),\n\t\t},\n\t\t...(viewport === undefined ? {} : { viewport }),\n\t}).start()\n} catch (error) {\n\tif (!isBrowserError(error)) throw error\n\tconsole.error(`browse: ${error.code}: ${error.message.split(/\\r\\n|\\n/u).join(' ')}`)\n\tprocess.exitCode = 1\n}\n"],"mappings":";;;;;AAKA,QAAQ,OAAO,GAAG,eAAe;CAChC,QAAQ,WAAW;AACpB,CAAC;AAED,IAAI;CACH,MAAM,EACL,aACA,iBACA,mBACA,iBACA,aACA,iBACA,oBACG,QAAQ;CACZ,MAAM,WAAW,aAAa,eAAe;CAC7C,MAAM,WAAW,aAAa,eAAe;CAC7C,MAAM,OAAO,aAAa,WAAW;CACrC,MAAM,WAAW,aAAa,eAAe;CAC7C,MAAM,WAAW,qBAAqB,eAAe;CAErD,KAAK,MAAM,CAAC,MAAM,OAAO,WAAW,CACnC;EAAC;EAAmB;EAAiB;CAAQ,GAC7C;EAAC;EAAmB;EAAiB;CAAQ,CAC9C,GACC,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,WAAW,KAAA,GACrD,MAAM,IAAI,aACT,GAAG,KAAK,qCAAqC,KAAK,UAAU,KAAK,KACjE,8BACA;EAAE;EAAM;CAAM,CACf;CAGF,IAAI,gBAAgB,KAAA,KAAa,gBAAgB,MAAM,SAAS,KAAA,GAC/D,MAAM,IAAI,aACT,uCAAuC,KAAK,UAAU,WAAW,KACjE,8BACA;EAAE,MAAM;EAAe,OAAO;CAAY,CAC3C;CACD,IAAI,oBAAoB,KAAA,KAAa,oBAAoB,MAAM,aAAa,KAAA,GAC3E,MAAM,IAAI,aACT,uEAAuE,KAAK,UAAU,eAAe,KACrG,8BACA;EAAE,MAAM;EAAmB,OAAO;CAAgB,CACnD;CACD,IAAI,oBAAoB,KAAA,KAAa,oBAAoB,MAAM,aAAa,KAAA,GAC3E,MAAM,IAAI,aACT,2CAA2C,KAAK,UAAU,eAAe,KACzE,8BACA;EAAE,MAAM;EAAmB,OAAO;CAAgB,CACnD;CACD,MAAM,uBAAuB;EAC5B,GAAI,gBAAgB,KAAA,KAAa,gBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,YAAY;EAC/E,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;EAC7C,GAAI,sBAAsB,KAAA,KAAa,sBAAsB,KAC1D,CAAC,IACD,EAAE,YAAY,kBAAkB;EACnC,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;EAC7C,MAAM;GACL,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK;GACrC,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;EAC9C;EACA,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;CAC9C,CAAC,CAAC,CAAC,MAAM;AACV,SAAS,OAAO;CACf,IAAI,CAAC,eAAe,KAAK,GAAG,MAAM;CAClC,QAAQ,MAAM,WAAW,MAAM,KAAK,IAAI,MAAM,QAAQ,MAAM,UAAU,CAAC,CAAC,KAAK,GAAG,GAAG;CACnF,QAAQ,WAAW;AACpB"}
@@ -163,7 +163,7 @@ var BROWSER_MOUSE_BUTTON_MASKS = Object.freeze({
163
163
  */
164
164
  var BROWSER_HAR_CREATOR = Object.freeze({
165
165
  name: "@orkestrel/browser",
166
- version: "0.0.23"
166
+ version: "0.0.25"
167
167
  });
168
168
  /** Names the attribute that tags temporary screenshot styles and masks. */
169
169
  var BROWSER_SCREENSHOT_ATTRIBUTE = "data-orkestrel-screenshot";
@@ -742,7 +742,7 @@ var BROWSER_TOOL_COPY = Object.freeze({
742
742
  description: "The journey name, such as add-kettle."
743
743
  }),
744
744
  edits: Object.freeze({
745
- description: "The changes, as an array or a JSON string of the array, applied in order; one invalid change refuses them all.",
745
+ description: "The changes, as an array or its JSON string, applied in order; one invalid change refuses them all.",
746
746
  anyOf: Object.freeze([Object.freeze({
747
747
  type: "array",
748
748
  items: Object.freeze({
@@ -818,6 +818,18 @@ var BROWSER_TOOL_COPY = Object.freeze({
818
818
  required: Object.freeze(["journey"])
819
819
  })
820
820
  }),
821
+ capture: Object.freeze({
822
+ name: "capture",
823
+ description: "Saves the current view as a PNG in the runs directory and returns its path.",
824
+ parameters: Object.freeze({
825
+ type: "object",
826
+ properties: Object.freeze({ full: Object.freeze({
827
+ type: "boolean",
828
+ description: "True captures the full page; false captures the viewport."
829
+ }) }),
830
+ required: Object.freeze(["full"])
831
+ })
832
+ }),
821
833
  forget: Object.freeze({
822
834
  name: "forget",
823
835
  description: "Removes a saved journey and all its runs; the name is free to record again.",
@@ -886,7 +898,7 @@ var BROWSER_ACTION_STAGES = Object.freeze([
886
898
  var BROWSER_JOURNEY_EMPTY_LISTING = "No journeys are saved; call record to start one.";
887
899
  /**
888
900
  * Names the journey tools a toolset constructed with `journeys` registers and reserves: `record`,
889
- * `save`, `journeys`, `edit`, `replay`, and `forget`.
901
+ * `save`, `journeys`, `edit`, `replay`, `forget`, and `capture`.
890
902
  */
891
903
  var BROWSER_JOURNEY_TOOL_NAMES = Object.freeze([
892
904
  "record",
@@ -894,7 +906,8 @@ var BROWSER_JOURNEY_TOOL_NAMES = Object.freeze([
894
906
  "journeys",
895
907
  "edit",
896
908
  "replay",
897
- "forget"
909
+ "forget",
910
+ "capture"
898
911
  ]);
899
912
  /** Names observation and journey tools that cannot become journey steps. */
900
913
  var BROWSER_JOURNEY_NON_STEP_TOOLS = Object.freeze([...BROWSER_OBSERVATION_TOOL_NAMES, ...BROWSER_JOURNEY_TOOL_NAMES]);
@@ -1000,9 +1013,7 @@ var CDPError = class extends BrowserError {
1000
1013
  }
1001
1014
  };
1002
1015
  /**
1003
- * Reports that a CDP request could not be sent or completed because the client was not in a
1004
- * connectable state — not connected, closed while connecting, or the connection dropped
1005
- * mid-request — under the code `BROWSER_CDP_CONNECTION_ERROR`.
1016
+ * Reports that a CDP request could not be sent or completed because the client was not connected, closed while connecting, lost its connection, or lost the request's target session, under the code `BROWSER_CDP_CONNECTION_ERROR`.
1006
1017
  */
1007
1018
  var CDPConnectionError = class extends BrowserError {
1008
1019
  constructor(message, context) {
@@ -5212,6 +5223,7 @@ var CDPClient = class {
5212
5223
  const listener = signal === void 0 ? void 0 : this.#abort(id, signal);
5213
5224
  this.#pending.set(id, {
5214
5225
  method,
5226
+ session: options?.session,
5215
5227
  resolve,
5216
5228
  reject,
5217
5229
  timer,
@@ -5356,6 +5368,16 @@ var CDPClient = class {
5356
5368
  const method = parsed["method"];
5357
5369
  const rawParams = parsed["params"];
5358
5370
  const params = (0, _orkestrel_contract.isRecord)(rawParams) ? rawParams : Object.freeze({});
5371
+ if (method === "Target.detachedFromTarget" && (0, _orkestrel_contract.isString)(params["sessionId"])) {
5372
+ const detached = params["sessionId"];
5373
+ for (const [id, entry] of this.#pending) {
5374
+ if (entry.session !== detached) continue;
5375
+ this.#settle(id)?.reject(new CDPConnectionError("CDP session detached", {
5376
+ method: entry.method,
5377
+ session: detached
5378
+ }));
5379
+ }
5380
+ }
5359
5381
  const global = this.#subscriptions.get(void 0)?.get(method);
5360
5382
  if (global !== void 0) this.#dispatch(global, method, params);
5361
5383
  const session = parsed["sessionId"];
@@ -6451,9 +6473,11 @@ var BrowserPageElement = class {
6451
6473
  object: result["object"]["objectId"],
6452
6474
  context
6453
6475
  };
6454
- } catch {
6476
+ } catch (error) {
6455
6477
  options?.signal?.throwIfAborted();
6456
- throw new BrowserElementError(this.reference, "GONE");
6478
+ if (isCDPTimeoutError(error) || isCDPConnectionError(error)) throw error;
6479
+ if (isCDPError(error) && /Could not find node|No node with given id|No node found for given backend id/i.test(error.message)) throw new BrowserElementError(this.reference, "GONE");
6480
+ throw error;
6457
6481
  }
6458
6482
  }
6459
6483
  async #call(declaration, options, target) {
@@ -6482,7 +6506,7 @@ var BrowserPageElement = class {
6482
6506
  }
6483
6507
  #failure(options, error) {
6484
6508
  options?.signal?.throwIfAborted();
6485
- if (isBrowserElementError(error)) throw error;
6509
+ if (isBrowserElementError(error) || isCDPTimeoutError(error) || isCDPConnectionError(error)) throw error;
6486
6510
  const message = ((0, _orkestrel_contract.isError)(error) ? error.message : String(error)).split("\n", 1)[0] ?? "";
6487
6511
  const known = BROWSER_ELEMENT_REFUSALS.get(message.replace(/^Error: /, ""));
6488
6512
  if (known !== void 0) throw new BrowserElementError(this.reference, known.reason, known.detail);
@@ -6514,11 +6538,20 @@ var BrowserPageElement = class {
6514
6538
  session: this.#input.node.session,
6515
6539
  ...options
6516
6540
  }).catch(this.#failure.bind(this, options));
6517
- await this.#call(compileActionabilityFunction({
6541
+ await this.#call(`async function() {
6542
+ await (${compileActionabilityFunction({
6518
6543
  visible: true,
6519
6544
  enabled: true,
6520
6545
  stable: true
6521
- }), options);
6546
+ })}).call(this)
6547
+ let previous
6548
+ for (;;) {
6549
+ await new Promise((resolve) => requestAnimationFrame(resolve))
6550
+ const current = [window.scrollX, window.scrollY]
6551
+ if (previous && current.every((value, index) => value === previous[index])) return true
6552
+ previous = current
6553
+ }
6554
+ }`, options);
6522
6555
  const geometry = await this.#geometry(options);
6523
6556
  const point = geometry.page.center;
6524
6557
  const local = geometry.local.center;
@@ -6529,6 +6562,10 @@ var BrowserPageElement = class {
6529
6562
  }, {
6530
6563
  session: this.#input.node.session,
6531
6564
  ...options
6565
+ }).catch((error) => {
6566
+ options?.signal?.throwIfAborted();
6567
+ if (isCDPError(error) && error.context?.["message"] === "No node found at given location") throw new BrowserElementError(this.reference, "OCCLUDED", "hit-test location held no node");
6568
+ throw error;
6532
6569
  });
6533
6570
  if (!(0, _orkestrel_contract.isRecord)(hit) || !(0, _orkestrel_contract.isInteger)(hit["backendNodeId"]) || hit["frameId"] !== void 0 && hit["frameId"] !== this.#input.frame) throw new BrowserElementError(this.reference, "OCCLUDED");
6534
6571
  if (hit["backendNodeId"] !== this.#input.backend && await this.#call(compileHitFunction(), options, hit["backendNodeId"]) !== true) {
@@ -7180,10 +7217,12 @@ var BrowserRegistry = class {
7180
7217
  for (const frame of new Set([...this.tools(), ...this.#pending].map((entry) => entry.frame))) this.#invalidate(frame);
7181
7218
  }
7182
7219
  #navigated(params) {
7220
+ if (this.#page.closed || this.#destroying !== void 0) return;
7183
7221
  const frame = params["frame"];
7184
7222
  if ((0, _orkestrel_contract.isRecord)(frame) && (0, _orkestrel_contract.isString)(frame["id"])) this.#invalidate(frame["id"]);
7185
7223
  }
7186
7224
  #detached(params) {
7225
+ if (this.#page.closed || this.#destroying !== void 0) return;
7187
7226
  if ((0, _orkestrel_contract.isString)(params["frameId"])) this.#invalidate(params["frameId"]);
7188
7227
  }
7189
7228
  #invalidate(frame) {
@@ -7197,6 +7236,7 @@ var BrowserRegistry = class {
7197
7236
  if (removed) this.#emitter.emit("change");
7198
7237
  }
7199
7238
  #add(session, params) {
7239
+ if (this.#page.closed || this.#destroying !== void 0) return;
7200
7240
  if (!(0, _orkestrel_contract.isArray)(params["tools"])) return;
7201
7241
  for (const value of params["tools"]) {
7202
7242
  const tool = parseBrowserTool(value);
@@ -7207,6 +7247,7 @@ var BrowserRegistry = class {
7207
7247
  this.#emitter.emit("change");
7208
7248
  }
7209
7249
  #remove(params) {
7250
+ if (this.#page.closed || this.#destroying !== void 0) return;
7210
7251
  if (!(0, _orkestrel_contract.isArray)(params["tools"])) return;
7211
7252
  for (const value of params["tools"]) {
7212
7253
  const key = parseBrowserRemoval(value);
@@ -10142,10 +10183,10 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
10142
10183
  } catch {}
10143
10184
  }
10144
10185
  async #close() {
10145
- await this.#release();
10146
10186
  try {
10147
10187
  await this.#client.send("Target.closeTarget", { targetId: this.#targetId });
10148
10188
  } catch {}
10189
+ await this.#release();
10149
10190
  }
10150
10191
  #release() {
10151
10192
  const active = this.#releasing;
@@ -10446,12 +10487,14 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
10446
10487
  }
10447
10488
  async #attachWorker(session, id, url, category) {
10448
10489
  try {
10449
- await this.#client.send("Runtime.enable", void 0, { session });
10490
+ const enabling = this.#client.send("Runtime.enable", void 0, { session });
10491
+ if (category === "service_worker") this.#resumeTarget(session);
10492
+ await enabling;
10450
10493
  if (this.#closed) {
10451
10494
  await this.#detachChild(session, this.#sessionId);
10452
10495
  return;
10453
10496
  }
10454
- this.#resumeTarget(session);
10497
+ if (category !== "service_worker") this.#resumeTarget(session);
10455
10498
  const worker = new BrowserWorker(this.#client, session, id, url, category);
10456
10499
  this.#workers.set(id, worker);
10457
10500
  this.#emitter.emit("worker", worker);
@@ -10788,6 +10831,7 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
10788
10831
  this.#emitter.emit("navigate", params["url"], true);
10789
10832
  }
10790
10833
  #handleFrameDetached(session, params) {
10834
+ if (this.#closed) return;
10791
10835
  const frame = params["frameId"];
10792
10836
  if (!(0, _orkestrel_contract.isString)(frame) || !this.#accepts(session, frame)) return;
10793
10837
  const swapped = params["reason"] === "swap";
@@ -10888,7 +10932,10 @@ var BrowserPage = class BrowserPage extends BrowserFrame {
10888
10932
  const session = params["sessionId"];
10889
10933
  if (!(0, _orkestrel_contract.isRecord)(target) || !(0, _orkestrel_contract.isString)(target["targetId"]) || !(0, _orkestrel_contract.isString)(session)) return;
10890
10934
  const category = target["type"];
10891
- if (owner !== this.#sessionId && category !== "iframe") return;
10935
+ if (owner !== this.#sessionId && category !== "iframe") {
10936
+ this.#detachChild(session, owner);
10937
+ return;
10938
+ }
10892
10939
  if (category === "worker" || category === "service_worker" || category === "shared_worker") {
10893
10940
  this.#attachWorker(session, target["targetId"], (0, _orkestrel_contract.isString)(target["url"]) ? target["url"] : "", category);
10894
10941
  return;
@@ -11196,6 +11243,7 @@ var BrowserContext = class {
11196
11243
  #publishing = /* @__PURE__ */ new Map();
11197
11244
  #observed = /* @__PURE__ */ new WeakSet();
11198
11245
  #shutdown;
11246
+ #disposal;
11199
11247
  #reference = 0;
11200
11248
  constructor(client, id, viewport, writer, emulation, downloads, options) {
11201
11249
  this.#client = client;
@@ -11220,6 +11268,9 @@ var BrowserContext = class {
11220
11268
  get id() {
11221
11269
  return this.#id;
11222
11270
  }
11271
+ get disposal() {
11272
+ return this.#disposal;
11273
+ }
11223
11274
  get cookies() {
11224
11275
  return this.#cookies;
11225
11276
  }
@@ -11262,14 +11313,14 @@ var BrowserContext = class {
11262
11313
  destroy() {
11263
11314
  const active = this.#shutdown;
11264
11315
  if (active !== void 0) return active;
11265
- const shutdown = this.#destroyResources();
11316
+ const shutdown = Promise.resolve().then(() => this.#destroyResources());
11266
11317
  this.#shutdown = shutdown;
11267
11318
  return shutdown;
11268
11319
  }
11269
11320
  close() {
11270
11321
  const active = this.#shutdown;
11271
11322
  if (active !== void 0) return active;
11272
- const shutdown = this.#closeResources();
11323
+ const shutdown = Promise.resolve().then(() => this.#closeResources());
11273
11324
  this.#shutdown = shutdown;
11274
11325
  return shutdown;
11275
11326
  }
@@ -11391,9 +11442,15 @@ var BrowserContext = class {
11391
11442
  let failure = await settleBrowserTeardown(...[...this.#pages.values()].map((page) => () => page.close()));
11392
11443
  this.#pages.clear();
11393
11444
  const id = this.#id;
11394
- if (id !== void 0) {
11395
- const settled = await settleBrowserTeardown(() => this.#client.send("Target.disposeBrowserContext", { browserContextId: id }));
11396
- failure ??= settled;
11445
+ if (id !== void 0) try {
11446
+ await this.#client.send("Target.disposeBrowserContext", { browserContextId: id });
11447
+ this.#disposal = { confirmed: true };
11448
+ } catch (error) {
11449
+ this.#disposal = {
11450
+ confirmed: false,
11451
+ error
11452
+ };
11453
+ failure ??= error;
11397
11454
  }
11398
11455
  if (failure !== void 0) throw failure;
11399
11456
  } finally {
@@ -12278,13 +12335,13 @@ function createMemoryBrowserRunStore() {
12278
12335
  //#endregion
12279
12336
  //#region src/core/BrowserJourneyToolset.ts
12280
12337
  /**
12281
- * Registers the journey tools `record`, `save`, `journeys`, `edit`, `replay`, and `forget` over a toolset
12338
+ * Registers the journey tools `record`, `save`, `journeys`, `edit`, `replay`, `forget`, and `capture` over a toolset
12282
12339
  * and owns the recording and the active replay.
12283
12340
  *
12284
12341
  * @remarks
12285
12342
  * The journey toolset composes the toolset's public members: `perform` reads the view a receipt
12286
12343
  * carries through `look`, `hold` and `emitter` drive the recorder and the replay, `view` converts
12287
- * an edit's `ref` to a target, and `tools` receives the six tools at construction, which refuses
12344
+ * an edit's `ref` to a target, and `tools` receives the journey tools at construction, which refuses
12288
12345
  * with `BROWSER_TOOLSET_RESERVED` when the manager already holds one of the names. Every refusal
12289
12346
  * is a sentence that names the next call, and a reason inside it is a clause without a directive
12290
12347
  * or a final period.
@@ -12300,7 +12357,7 @@ function createMemoryBrowserRunStore() {
12300
12357
  * by the view after the run.
12301
12358
  *
12302
12359
  * `destroy()` aborts the active replay and waits for it to finish, stops a recording without
12303
- * saving it, and removes the six tools the manager still holds under the instances it added.
12360
+ * saving it, and removes the journey tools the manager still holds under the instances it added.
12304
12361
  *
12305
12362
  * @example
12306
12363
  * ```ts
@@ -12341,7 +12398,8 @@ var BrowserJourneyToolset = class {
12341
12398
  this.#create("journeys", this.#journeys.bind(this)),
12342
12399
  this.#create("edit", this.#edit.bind(this)),
12343
12400
  this.#create("replay", this.#replayJourney.bind(this)),
12344
- this.#create("forget", this.#forget.bind(this))
12401
+ this.#create("forget", this.#forget.bind(this)),
12402
+ this.#create("capture", this.#capture.bind(this))
12345
12403
  ]);
12346
12404
  toolset.tools.add(this.#tools);
12347
12405
  }
@@ -12368,6 +12426,22 @@ var BrowserJourneyToolset = class {
12368
12426
  signal.throwIfAborted();
12369
12427
  return handler(args, signal);
12370
12428
  }
12429
+ async #capture(args, signal) {
12430
+ const full = args["full"];
12431
+ if (!(0, _orkestrel_contract.isBoolean)(full)) throw new BrowserError("The full parameter must be true or false.", "BROWSER_TOOLSET_ARGUMENT");
12432
+ this.#idleReplay();
12433
+ const view = this.#toolset.view;
12434
+ if (!view.trusted) throw new BrowserError("This view is untrusted; capture requires a trusted browser view.", "BROWSER_CAPTURE_UNTRUSTED");
12435
+ if (view.screenshot === void 0) throw new BrowserError("This view cannot capture an image.", "BROWSER_CAPTURE_UNAVAILABLE");
12436
+ const runs = this.#runs;
12437
+ if (runs?.snapshot === void 0) throw new BrowserError("Capture requires a runs store with standalone file storage.", "BROWSER_CAPTURE_UNAVAILABLE");
12438
+ const screenshot = await view.screenshot({
12439
+ format: "png",
12440
+ full
12441
+ });
12442
+ signal.throwIfAborted();
12443
+ return runs.snapshot(screenshot.bytes, { signal });
12444
+ }
12371
12445
  async #record(args, signal) {
12372
12446
  if (this.#readonly) throw new BrowserError(BROWSER_JOURNEY_READONLY_REFUSAL, "BROWSER_JOURNEY_READONLY");
12373
12447
  this.#idleReplay();