cdp-toolkit 1.4.0 → 1.6.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/dist/index.js CHANGED
@@ -680,6 +680,8 @@ var REQUIRED_CAPABILITIES = {
680
680
  performance_trace: ["trace.performance"],
681
681
  take_heapsnapshot: ["heap.snapshot"],
682
682
  lighthouse_audit: ["audit.lighthouse"],
683
+ start_screen_recording: ["capture.screencast"],
684
+ stop_screen_recording: ["capture.screencast"],
683
685
  mock_request: ["network.intercept"],
684
686
  list_mocks: ["network.intercept"],
685
687
  clear_mocks: ["network.intercept"]
@@ -935,7 +937,8 @@ var CDP_CAPABILITIES = new Set([
935
937
  "snapshot.accessibilityTree",
936
938
  "input.insertTextAtomic",
937
939
  "locate.text",
938
- "locate.xpath"
940
+ "locate.xpath",
941
+ "capture.screencast"
939
942
  ]);
940
943
 
941
944
  class CdpPageDriver {
@@ -2577,11 +2580,375 @@ async function performanceAnalyzeInsight(args = {}) {
2577
2580
  return { ...analyzeEvents(events), tracePath: args.tracePath };
2578
2581
  }
2579
2582
 
2580
- // src/tools/heap.ts
2581
- import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
2583
+ // src/tools/screencast.ts
2584
+ import { spawn } from "node:child_process";
2585
+ import { mkdir as mkdir6, rm, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
2582
2586
  import { join as join5 } from "node:path";
2583
2587
  var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2584
- function stamp3(targetId) {
2588
+ var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
2589
+ function frameDurationsMs(ledger, stopWallclockMs) {
2590
+ return ledger.map((entry, i) => {
2591
+ const next = i + 1 < ledger.length ? ledger[i + 1].timestamp : stopWallclockMs;
2592
+ return Math.max(1, Math.round(next - entry.timestamp));
2593
+ });
2594
+ }
2595
+ var CONCAT_GRID_MS = 40;
2596
+ function coalesceToConcatGrid(ledger, stopWallclockMs, gridMs = CONCAT_GRID_MS) {
2597
+ if (!ledger.length)
2598
+ return [];
2599
+ const raw = frameDurationsMs(ledger, stopWallclockMs);
2600
+ let elapsed = 0;
2601
+ const kept = [];
2602
+ let lastSlot = -1;
2603
+ for (let i = 0;i < ledger.length; i++) {
2604
+ const slot = Math.round(elapsed / gridMs);
2605
+ if (slot > lastSlot) {
2606
+ kept.push({ file: ledger[i].file, slot });
2607
+ lastSlot = slot;
2608
+ }
2609
+ elapsed += raw[i];
2610
+ }
2611
+ const endSlot = Math.max(lastSlot + 1, Math.round(elapsed / gridMs));
2612
+ return kept.map((frame, i) => ({
2613
+ file: frame.file,
2614
+ durationMs: ((i + 1 < kept.length ? kept[i + 1].slot : endSlot) - frame.slot) * gridMs
2615
+ }));
2616
+ }
2617
+ function buildConcatManifest(frames) {
2618
+ if (!frames.length)
2619
+ throw new CdpError("cannot build a concat manifest from an empty frame ledger");
2620
+ const lines = ["ffconcat version 1.0"];
2621
+ for (const frame of frames) {
2622
+ lines.push(`file ${frame.file}`);
2623
+ lines.push(`duration ${(frame.durationMs / 1000).toFixed(6)}`);
2624
+ }
2625
+ lines.push(`file ${frames[frames.length - 1].file}`);
2626
+ return `${lines.join(`
2627
+ `)}
2628
+ `;
2629
+ }
2630
+ function evenDimensions(width, height) {
2631
+ return { width: Math.max(0, Math.trunc(width / 2) * 2), height: Math.max(0, Math.trunc(height / 2) * 2) };
2632
+ }
2633
+ function pngSize(buf) {
2634
+ if (buf.length < 24)
2635
+ return;
2636
+ if (buf.readUInt32BE(0) !== 2303741511 || buf.readUInt32BE(4) !== 218765834)
2637
+ return;
2638
+ if (buf.toString("latin1", 12, 16) !== "IHDR")
2639
+ return;
2640
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
2641
+ }
2642
+ var JPEG_SOF = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
2643
+ function jpegSize(buf) {
2644
+ if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216)
2645
+ return;
2646
+ let i = 2;
2647
+ while (i + 3 < buf.length) {
2648
+ if (buf[i] !== 255) {
2649
+ i += 1;
2650
+ continue;
2651
+ }
2652
+ const marker = buf[i + 1];
2653
+ if (marker === 255) {
2654
+ i += 1;
2655
+ continue;
2656
+ }
2657
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
2658
+ i += 2;
2659
+ continue;
2660
+ }
2661
+ if (i + 3 >= buf.length)
2662
+ return;
2663
+ const segLen = buf.readUInt16BE(i + 2);
2664
+ if (JPEG_SOF.has(marker)) {
2665
+ if (i + 9 > buf.length)
2666
+ return;
2667
+ return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
2668
+ }
2669
+ if (segLen < 2)
2670
+ return;
2671
+ i += 2 + segLen;
2672
+ }
2673
+ return;
2674
+ }
2675
+ function imageSize(buf) {
2676
+ return pngSize(buf) ?? jpegSize(buf);
2677
+ }
2678
+ var ENCODER_LADDER = [
2679
+ { encoder: "hevc_videotoolbox", codec: "hevc", tag: "hvc1" },
2680
+ { encoder: "h264_videotoolbox", codec: "h264" },
2681
+ { encoder: "libx265", codec: "hevc", tag: "hvc1" },
2682
+ { encoder: "libx264", codec: "h264" }
2683
+ ];
2684
+ function ffmpegEncoderNames(encodersOutput) {
2685
+ const names = new Set;
2686
+ for (const line of encodersOutput.split(`
2687
+ `)) {
2688
+ const m = /^\s*[VAS][A-Z.]{5}\s+(\S+)/.exec(line);
2689
+ if (m)
2690
+ names.add(m[1]);
2691
+ }
2692
+ return names;
2693
+ }
2694
+ function selectEncoder(encodersOutput) {
2695
+ const available = ffmpegEncoderNames(encodersOutput);
2696
+ for (const candidate of ENCODER_LADDER) {
2697
+ if (available.has(candidate.encoder))
2698
+ return candidate;
2699
+ }
2700
+ throw new CdpError(`ffmpeg reports none of the supported video encoders (${ENCODER_LADDER.map((c) => c.encoder).join(", ")}). ` + "Install a full ffmpeg build: 'brew install ffmpeg' (macOS) or 'apt install ffmpeg' (Debian/Ubuntu).");
2701
+ }
2702
+ function buildFfmpegArgs(manifestPath, outPath, choice) {
2703
+ return [
2704
+ "-hide_banner",
2705
+ "-loglevel",
2706
+ "error",
2707
+ "-y",
2708
+ "-f",
2709
+ "concat",
2710
+ "-safe",
2711
+ "0",
2712
+ "-i",
2713
+ manifestPath,
2714
+ "-vsync",
2715
+ "vfr",
2716
+ "-vf",
2717
+ "scale=trunc(iw/2)*2:trunc(ih/2)*2",
2718
+ "-c:v",
2719
+ choice.encoder,
2720
+ ...choice.tag ? ["-tag:v", choice.tag] : [],
2721
+ "-pix_fmt",
2722
+ "yuv420p",
2723
+ "-movflags",
2724
+ "+faststart",
2725
+ "-an",
2726
+ outPath
2727
+ ];
2728
+ }
2729
+ function run(bin, args) {
2730
+ return new Promise((resolve, reject) => {
2731
+ const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
2732
+ let stdout = "";
2733
+ let stderr = "";
2734
+ child.stdout?.on("data", (chunk) => {
2735
+ stdout += chunk.toString("utf8");
2736
+ });
2737
+ child.stderr?.on("data", (chunk) => {
2738
+ stderr += chunk.toString("utf8");
2739
+ });
2740
+ child.on("error", reject);
2741
+ child.on("close", (code) => resolve({ code, stdout, stderr }));
2742
+ });
2743
+ }
2744
+ var encoderProbe;
2745
+ var FFMPEG_MISSING = `ffmpeg is required to encode a screen recording but '${FFMPEG_BIN}' could not be executed. ` + "Install it with 'brew install ffmpeg' (macOS) or 'apt install ffmpeg' (Debian/Ubuntu), " + "or point CDP_FFMPEG at the binary.";
2746
+ async function probeEncoder() {
2747
+ if (!encoderProbe) {
2748
+ encoderProbe = (async () => {
2749
+ let res;
2750
+ try {
2751
+ res = await run(FFMPEG_BIN, ["-hide_banner", "-encoders"]);
2752
+ } catch (err) {
2753
+ throw new CdpError(`${FFMPEG_MISSING} (${err.message})`);
2754
+ }
2755
+ if (res.code !== 0)
2756
+ throw new CdpError(`${FFMPEG_MISSING} ('-encoders' exited ${res.code}: ${res.stderr.trim().slice(0, 300)})`);
2757
+ return selectEncoder(`${res.stdout}
2758
+ ${res.stderr}`);
2759
+ })();
2760
+ encoderProbe.catch(() => {
2761
+ encoderProbe = undefined;
2762
+ });
2763
+ }
2764
+ return encoderProbe;
2765
+ }
2766
+ var liveRecordings = new Map;
2767
+ function shortId2(id) {
2768
+ return id.slice(0, 8);
2769
+ }
2770
+ function assertNotRecording(targetId, activeIds) {
2771
+ if (activeIds.includes(targetId)) {
2772
+ throw new CdpError(`a screen recording is already in progress for target ${shortId2(targetId)}; call stop_screen_recording first`);
2773
+ }
2774
+ }
2775
+ function chooseRecordingId(activeIds, selector) {
2776
+ if (!activeIds.length) {
2777
+ throw new CdpError("no screen recording is in progress in this process. start_screen_recording parks a live CDP connection in memory, so a recording cannot be stopped from a different process than the one that started it (the CLI is one process per call: use the MCP server for the pair).");
2778
+ }
2779
+ if (selector === undefined || selector === "active") {
2780
+ if (activeIds.length === 1)
2781
+ return activeIds[0];
2782
+ throw new CdpError(`${activeIds.length} screen recordings are in progress (targets: ${activeIds.map(shortId2).join(", ")}); pass 'target' to say which one to stop`);
2783
+ }
2784
+ return activeIds.find((id) => id === selector);
2785
+ }
2786
+ function stamp3() {
2787
+ return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
2788
+ }
2789
+ function target32(t) {
2790
+ return { id: t.id, url: t.url, title: t.title };
2791
+ }
2792
+ async function startScreenRecording(args = {}) {
2793
+ const format = args.format ?? "jpeg";
2794
+ if (format !== "jpeg" && format !== "png")
2795
+ throw new CdpError("start_screen_recording: format must be 'jpeg' or 'png'");
2796
+ if (args.quality !== undefined && (!Number.isFinite(args.quality) || args.quality < 0 || args.quality > 100)) {
2797
+ throw new CdpError("start_screen_recording: quality must be a number between 0 and 100 (jpeg only)");
2798
+ }
2799
+ const encoder = await probeEncoder();
2800
+ const { conn, target } = await openPage(args.target);
2801
+ try {
2802
+ assertNotRecording(target.id, [...liveRecordings.keys()]);
2803
+ } catch (err) {
2804
+ conn.close();
2805
+ throw err;
2806
+ }
2807
+ const spoolDir = join5(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
2808
+ try {
2809
+ await mkdir6(spoolDir, { recursive: true });
2810
+ } catch (err) {
2811
+ conn.close();
2812
+ throw new CdpError(`cannot create the screencast spool directory '${spoolDir}': ${err.message}`);
2813
+ }
2814
+ const ext = format === "png" ? "png" : "jpg";
2815
+ const rec = {
2816
+ conn,
2817
+ target,
2818
+ spoolDir,
2819
+ ledger: [],
2820
+ startedAt: Date.now(),
2821
+ format,
2822
+ encoder,
2823
+ writes: Promise.resolve(),
2824
+ dropped: 0,
2825
+ unwritten: new Set,
2826
+ off: () => {}
2827
+ };
2828
+ let seq = 0;
2829
+ rec.off = conn.on("Page.screencastFrame", (params) => {
2830
+ const sessionId = params.sessionId;
2831
+ if (typeof sessionId === "number") {
2832
+ conn.send("Page.screencastFrameAck", { sessionId }).catch(() => {});
2833
+ }
2834
+ const data = params.data;
2835
+ if (typeof data !== "string" || !data.length) {
2836
+ rec.dropped += 1;
2837
+ return;
2838
+ }
2839
+ const meta = params.metadata;
2840
+ const timestamp = typeof meta?.timestamp === "number" ? Math.round(meta.timestamp * 1000) : Date.now();
2841
+ const buf = Buffer.from(data, "base64");
2842
+ const size = imageSize(buf);
2843
+ if (!size || size.width <= 0 || size.height <= 0) {
2844
+ rec.dropped += 1;
2845
+ return;
2846
+ }
2847
+ seq += 1;
2848
+ const file = `frame-${String(seq).padStart(6, "0")}.${ext}`;
2849
+ rec.ledger.push({ file, timestamp, width: size.width, height: size.height });
2850
+ rec.writes = rec.writes.then(() => writeFile6(join5(spoolDir, file), buf)).catch(() => {
2851
+ rec.dropped += 1;
2852
+ rec.unwritten.add(file);
2853
+ });
2854
+ });
2855
+ try {
2856
+ await conn.send("Page.enable");
2857
+ if (args.bringToFront)
2858
+ await conn.send("Page.bringToFront").catch(() => {});
2859
+ const params = { format };
2860
+ if (format === "jpeg" && args.quality !== undefined)
2861
+ params.quality = args.quality;
2862
+ if (args.maxWidth !== undefined)
2863
+ params.maxWidth = args.maxWidth;
2864
+ if (args.maxHeight !== undefined)
2865
+ params.maxHeight = args.maxHeight;
2866
+ if (args.everyNthFrame !== undefined)
2867
+ params.everyNthFrame = args.everyNthFrame;
2868
+ await conn.send("Page.startScreencast", params);
2869
+ } catch (err) {
2870
+ rec.off();
2871
+ conn.close();
2872
+ await rm(spoolDir, { recursive: true, force: true }).catch(() => {});
2873
+ throw err;
2874
+ }
2875
+ liveRecordings.set(target.id, rec);
2876
+ return {
2877
+ target: target32(target),
2878
+ format,
2879
+ spoolDir,
2880
+ encoder: encoder.encoder,
2881
+ codec: encoder.codec,
2882
+ startedAt: rec.startedAt,
2883
+ note: "Recording. Call stop_screen_recording in THIS process to encode the MP4; frames only arrive on repaint, so a still page yields one long-held frame."
2884
+ };
2885
+ }
2886
+ async function pickRecording(selector) {
2887
+ const chosen = chooseRecordingId([...liveRecordings.keys()], selector);
2888
+ if (chosen)
2889
+ return liveRecordings.get(chosen);
2890
+ const { conn, target } = await openPage(selector);
2891
+ conn.close();
2892
+ const hit = liveRecordings.get(target.id);
2893
+ if (!hit) {
2894
+ throw new CdpError(`no screen recording is in progress for target ${shortId2(target.id)} (recording: ${[...liveRecordings.keys()].map(shortId2).join(", ") || "none"})`);
2895
+ }
2896
+ return hit;
2897
+ }
2898
+ async function stopScreenRecording(args = {}) {
2899
+ const rec = await pickRecording(args.target);
2900
+ rec.off();
2901
+ try {
2902
+ await rec.conn.send("Page.stopScreencast");
2903
+ } catch {}
2904
+ const stoppedAt = Date.now();
2905
+ await rec.writes.catch(() => {});
2906
+ rec.conn.close();
2907
+ liveRecordings.delete(rec.target.id);
2908
+ const ledger = rec.unwritten.size ? rec.ledger.filter((entry) => !rec.unwritten.has(entry.file)) : rec.ledger;
2909
+ if (!ledger.length) {
2910
+ await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
2911
+ throw new CdpError(`screen recording captured 0 frames for target ${shortId2(rec.target.id)}. Chrome emits a screencast frame only on repaint: a fully backgrounded or occluded tab may never paint. Retry with bringToFront:true, or drive the page while recording.`);
2912
+ }
2913
+ await mkdir6(ARTIFACT_DIR4, { recursive: true });
2914
+ const outPath = args.savePath ?? join5(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
2915
+ const frames = coalesceToConcatGrid(ledger, stoppedAt);
2916
+ const manifestPath = join5(rec.spoolDir, "frames.ffconcat");
2917
+ await writeFile6(manifestPath, buildConcatManifest(frames), "utf8");
2918
+ const durationMs = frames.reduce((total, f) => total + f.durationMs, 0);
2919
+ const { width, height } = evenDimensions(ledger[0].width, ledger[0].height);
2920
+ let res;
2921
+ try {
2922
+ res = await run(FFMPEG_BIN, buildFfmpegArgs(manifestPath, outPath, rec.encoder));
2923
+ } catch (err) {
2924
+ throw new CdpError(`${FFMPEG_MISSING} Spooled frames kept at ${rec.spoolDir}. (${err.message})`);
2925
+ }
2926
+ if (res.code !== 0) {
2927
+ throw new CdpError(`ffmpeg (${rec.encoder.encoder}) exited ${res.code} encoding ${frames.length} frames. Spooled frames kept at ${rec.spoolDir}, re-run by hand with: ` + `${FFMPEG_BIN} ${buildFfmpegArgs(manifestPath, outPath, rec.encoder).join(" ")}
2928
+ ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
2929
+ }
2930
+ const { size } = await stat2(outPath);
2931
+ await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
2932
+ return {
2933
+ path: outPath,
2934
+ bytes: size,
2935
+ durationMs,
2936
+ frameCount: ledger.length,
2937
+ encodedFrames: frames.length,
2938
+ codec: rec.encoder.codec,
2939
+ encoder: rec.encoder.encoder,
2940
+ width,
2941
+ height,
2942
+ droppedFrames: rec.dropped,
2943
+ target: target32(rec.target)
2944
+ };
2945
+ }
2946
+
2947
+ // src/tools/heap.ts
2948
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
2949
+ import { join as join6 } from "node:path";
2950
+ var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2951
+ function stamp4(targetId) {
2585
2952
  const short = targetId.slice(0, 8);
2586
2953
  const iso = new Date().toISOString().replace(/[:.]/g, "-");
2587
2954
  return `${short}-${iso}`;
@@ -2604,9 +2971,9 @@ async function takeHeapsnapshot(args = {}) {
2604
2971
  if (snapshot.length === 0) {
2605
2972
  throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
2606
2973
  }
2607
- await mkdir6(ARTIFACT_DIR4, { recursive: true });
2608
- const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join5(ARTIFACT_DIR4, args.savePath) : join5(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
2609
- await writeFile6(path, snapshot, "utf8");
2974
+ await mkdir7(ARTIFACT_DIR5, { recursive: true });
2975
+ const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join6(ARTIFACT_DIR5, args.savePath) : join6(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
2976
+ await writeFile7(path, snapshot, "utf8");
2610
2977
  const bytes = Buffer.byteLength(snapshot, "utf8");
2611
2978
  return {
2612
2979
  path,
@@ -2618,15 +2985,15 @@ async function takeHeapsnapshot(args = {}) {
2618
2985
  }
2619
2986
 
2620
2987
  // src/tools/lighthouse.ts
2621
- import { spawn } from "node:child_process";
2622
- import { mkdir as mkdir7, readFile as readFile6, stat as stat2 } from "node:fs/promises";
2623
- var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2988
+ import { spawn as spawn2 } from "node:child_process";
2989
+ import { mkdir as mkdir8, readFile as readFile6, stat as stat3 } from "node:fs/promises";
2990
+ var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2624
2991
  function cdpPort() {
2625
2992
  const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
2626
2993
  const m = base.match(/:(\d+)/);
2627
2994
  return m?.[1] ? Number(m[1]) : 9222;
2628
2995
  }
2629
- function stamp4() {
2996
+ function stamp5() {
2630
2997
  return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
2631
2998
  }
2632
2999
  function shortHost(url) {
@@ -2643,7 +3010,7 @@ function runProcess(cmd, args, opts) {
2643
3010
  let stdout = "";
2644
3011
  let stderr = "";
2645
3012
  let settled = false;
2646
- const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
3013
+ const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
2647
3014
  const timer = setTimeout(() => {
2648
3015
  if (settled)
2649
3016
  return;
@@ -2680,8 +3047,8 @@ async function lighthouseAudit(args) {
2680
3047
  const url = args.url.trim();
2681
3048
  const timeoutMs = args.timeoutMs ?? 120000;
2682
3049
  const port = cdpPort();
2683
- await mkdir7(ARTIFACT_DIR5, { recursive: true });
2684
- const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp4()}.json`;
3050
+ await mkdir8(ARTIFACT_DIR6, { recursive: true });
3051
+ const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
2685
3052
  const formFactor = args.formFactor ?? "desktop";
2686
3053
  const lhArgs = [
2687
3054
  "--yes",
@@ -2736,7 +3103,7 @@ async function lighthouseAudit(args) {
2736
3103
  if (Object.keys(categories).length === 0) {
2737
3104
  throw new CdpError(`lighthouse_audit report contained no category scores (exit code ${res.code}). stderr: ${truncate(res.stderr)}`);
2738
3105
  }
2739
- const { size } = await stat2(outPath);
3106
+ const { size } = await stat3(outPath);
2740
3107
  return {
2741
3108
  path: outPath,
2742
3109
  bytes: size,
@@ -2790,6 +3157,8 @@ var TOOLS = {
2790
3157
  performance_stop_trace: performanceStopTrace,
2791
3158
  performance_analyze_insight: performanceAnalyzeInsight,
2792
3159
  performance_trace: performanceTrace,
3160
+ start_screen_recording: startScreenRecording,
3161
+ stop_screen_recording: stopScreenRecording,
2793
3162
  take_heapsnapshot: takeHeapsnapshot,
2794
3163
  lighthouse_audit: lighthouseAudit,
2795
3164
  mock_request: mockRequest,