@orkestrel/browser 0.0.23 → 0.0.24

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,17 @@
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_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 viewport = parseBrowserViewport(BROWSE_VIEWPORT);
14
15
  for (const [name, value, parsed] of [[
15
16
  "BROWSE_HEADLESS",
16
17
  BROWSE_HEADLESS,
@@ -27,12 +28,17 @@ try {
27
28
  name: "BROWSE_POOL",
28
29
  value: BROWSE_POOL
29
30
  });
31
+ 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", {
32
+ name: "BROWSE_VIEWPORT",
33
+ value: BROWSE_VIEWPORT
34
+ });
30
35
  await createBrowserMCPServer({
31
36
  ...BROWSE_ROOT === void 0 || BROWSE_ROOT === "" ? {} : { root: BROWSE_ROOT },
32
37
  ...headless === void 0 ? {} : { headless },
33
38
  ...BROWSE_EXECUTABLE === void 0 || BROWSE_EXECUTABLE === "" ? {} : { executable: BROWSE_EXECUTABLE },
34
39
  ...readonly === void 0 ? {} : { readonly },
35
- ...size === void 0 ? {} : { pool: { size } }
40
+ ...size === void 0 ? {} : { pool: { size } },
41
+ ...viewport === void 0 ? {} : { viewport }
36
42
  }).start();
37
43
  } catch (error) {
38
44
  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_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 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\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\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,oBACG,QAAQ;CACZ,MAAM,WAAW,aAAa,eAAe;CAC7C,MAAM,WAAW,aAAa,eAAe;CAC7C,MAAM,OAAO,aAAa,WAAW;CACrC,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,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;EAC/C,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.24"
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]);
@@ -6451,9 +6464,11 @@ var BrowserPageElement = class {
6451
6464
  object: result["object"]["objectId"],
6452
6465
  context
6453
6466
  };
6454
- } catch {
6467
+ } catch (error) {
6455
6468
  options?.signal?.throwIfAborted();
6456
- throw new BrowserElementError(this.reference, "GONE");
6469
+ if (isCDPTimeoutError(error) || isCDPConnectionError(error)) throw error;
6470
+ 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");
6471
+ throw error;
6457
6472
  }
6458
6473
  }
6459
6474
  async #call(declaration, options, target) {
@@ -6482,7 +6497,7 @@ var BrowserPageElement = class {
6482
6497
  }
6483
6498
  #failure(options, error) {
6484
6499
  options?.signal?.throwIfAborted();
6485
- if (isBrowserElementError(error)) throw error;
6500
+ if (isBrowserElementError(error) || isCDPTimeoutError(error) || isCDPConnectionError(error)) throw error;
6486
6501
  const message = ((0, _orkestrel_contract.isError)(error) ? error.message : String(error)).split("\n", 1)[0] ?? "";
6487
6502
  const known = BROWSER_ELEMENT_REFUSALS.get(message.replace(/^Error: /, ""));
6488
6503
  if (known !== void 0) throw new BrowserElementError(this.reference, known.reason, known.detail);
@@ -6514,11 +6529,20 @@ var BrowserPageElement = class {
6514
6529
  session: this.#input.node.session,
6515
6530
  ...options
6516
6531
  }).catch(this.#failure.bind(this, options));
6517
- await this.#call(compileActionabilityFunction({
6532
+ await this.#call(`async function() {
6533
+ await (${compileActionabilityFunction({
6518
6534
  visible: true,
6519
6535
  enabled: true,
6520
6536
  stable: true
6521
- }), options);
6537
+ })}).call(this)
6538
+ let previous
6539
+ for (;;) {
6540
+ await new Promise((resolve) => requestAnimationFrame(resolve))
6541
+ const current = [window.scrollX, window.scrollY]
6542
+ if (previous && current.every((value, index) => value === previous[index])) return true
6543
+ previous = current
6544
+ }
6545
+ }`, options);
6522
6546
  const geometry = await this.#geometry(options);
6523
6547
  const point = geometry.page.center;
6524
6548
  const local = geometry.local.center;
@@ -6529,6 +6553,10 @@ var BrowserPageElement = class {
6529
6553
  }, {
6530
6554
  session: this.#input.node.session,
6531
6555
  ...options
6556
+ }).catch((error) => {
6557
+ options?.signal?.throwIfAborted();
6558
+ if (isCDPError(error) && error.context?.["message"] === "No node found at given location") throw new BrowserElementError(this.reference, "OCCLUDED", "hit-test location held no node");
6559
+ throw error;
6532
6560
  });
6533
6561
  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
6562
  if (hit["backendNodeId"] !== this.#input.backend && await this.#call(compileHitFunction(), options, hit["backendNodeId"]) !== true) {
@@ -12278,13 +12306,13 @@ function createMemoryBrowserRunStore() {
12278
12306
  //#endregion
12279
12307
  //#region src/core/BrowserJourneyToolset.ts
12280
12308
  /**
12281
- * Registers the journey tools `record`, `save`, `journeys`, `edit`, `replay`, and `forget` over a toolset
12309
+ * Registers the journey tools `record`, `save`, `journeys`, `edit`, `replay`, `forget`, and `capture` over a toolset
12282
12310
  * and owns the recording and the active replay.
12283
12311
  *
12284
12312
  * @remarks
12285
12313
  * The journey toolset composes the toolset's public members: `perform` reads the view a receipt
12286
12314
  * 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
12315
+ * an edit's `ref` to a target, and `tools` receives the journey tools at construction, which refuses
12288
12316
  * with `BROWSER_TOOLSET_RESERVED` when the manager already holds one of the names. Every refusal
12289
12317
  * is a sentence that names the next call, and a reason inside it is a clause without a directive
12290
12318
  * or a final period.
@@ -12300,7 +12328,7 @@ function createMemoryBrowserRunStore() {
12300
12328
  * by the view after the run.
12301
12329
  *
12302
12330
  * `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.
12331
+ * saving it, and removes the journey tools the manager still holds under the instances it added.
12304
12332
  *
12305
12333
  * @example
12306
12334
  * ```ts
@@ -12341,7 +12369,8 @@ var BrowserJourneyToolset = class {
12341
12369
  this.#create("journeys", this.#journeys.bind(this)),
12342
12370
  this.#create("edit", this.#edit.bind(this)),
12343
12371
  this.#create("replay", this.#replayJourney.bind(this)),
12344
- this.#create("forget", this.#forget.bind(this))
12372
+ this.#create("forget", this.#forget.bind(this)),
12373
+ this.#create("capture", this.#capture.bind(this))
12345
12374
  ]);
12346
12375
  toolset.tools.add(this.#tools);
12347
12376
  }
@@ -12368,6 +12397,22 @@ var BrowserJourneyToolset = class {
12368
12397
  signal.throwIfAborted();
12369
12398
  return handler(args, signal);
12370
12399
  }
12400
+ async #capture(args, signal) {
12401
+ const full = args["full"];
12402
+ if (!(0, _orkestrel_contract.isBoolean)(full)) throw new BrowserError("The full parameter must be true or false.", "BROWSER_TOOLSET_ARGUMENT");
12403
+ this.#idleReplay();
12404
+ const view = this.#toolset.view;
12405
+ if (!view.trusted) throw new BrowserError("This view is untrusted; capture requires a trusted browser view.", "BROWSER_CAPTURE_UNTRUSTED");
12406
+ if (view.screenshot === void 0) throw new BrowserError("This view cannot capture an image.", "BROWSER_CAPTURE_UNAVAILABLE");
12407
+ const runs = this.#runs;
12408
+ if (runs?.snapshot === void 0) throw new BrowserError("Capture requires a runs store with standalone file storage.", "BROWSER_CAPTURE_UNAVAILABLE");
12409
+ const screenshot = await view.screenshot({
12410
+ format: "png",
12411
+ full
12412
+ });
12413
+ signal.throwIfAborted();
12414
+ return runs.snapshot(screenshot.bytes, { signal });
12415
+ }
12371
12416
  async #record(args, signal) {
12372
12417
  if (this.#readonly) throw new BrowserError(BROWSER_JOURNEY_READONLY_REFUSAL, "BROWSER_JOURNEY_READONLY");
12373
12418
  this.#idleReplay();