@crouton-kit/capture 1.3.19 → 1.3.21

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/bin/capture CHANGED
@@ -9154,6 +9154,9 @@ function validateCliInvocation(parsed) {
9154
9154
  throw invalidInput(`Invalid --viewport: ${parsed.viewport}.`);
9155
9155
  }
9156
9156
  }
9157
+ if (leaf === "shot" && parsed.colorScheme !== void 0 && parsed.colorScheme !== "dark" && parsed.colorScheme !== "light") {
9158
+ throw invalidInput(`Invalid --color-scheme: ${parsed.colorScheme}; expected dark or light.`);
9159
+ }
9157
9160
  return;
9158
9161
  }
9159
9162
  if (parsed.command === "tab") {
@@ -9167,6 +9170,9 @@ function validateCliInvocation(parsed) {
9167
9170
  }
9168
9171
  if (parsed.command === "measure") {
9169
9172
  if (leaf === "snap" || leaf === "check" || leaf === "sweep") requireCount(values, 0, 1, `measure ${leaf}`);
9173
+ if (leaf === "snap" && parsed.colorScheme !== void 0 && parsed.colorScheme !== "dark" && parsed.colorScheme !== "light") {
9174
+ throw invalidInput(`Invalid --color-scheme: ${parsed.colorScheme}; expected dark or light.`);
9175
+ }
9170
9176
  if (leaf === "diff" || leaf === "census") requireCount(values, 0, 0, `measure ${leaf}`);
9171
9177
  if (leaf === "diff" && (!parsed.before || !parsed.after)) throw invalidInput("measure diff requires --before and --after.");
9172
9178
  if (leaf === "census" && !["color", "font", "spacing", "radius", "shadow", "animation", "geometry", "media", "queries"].includes(parsed.axis ?? "")) {
@@ -9272,6 +9278,9 @@ function parseCliSyntax(argv) {
9272
9278
  parsed.viewport = value;
9273
9279
  (parsed.viewports ??= []).push(value);
9274
9280
  i++;
9281
+ } else if (arg === "--color-scheme") {
9282
+ parsed.colorScheme = valueFor(arg, next);
9283
+ i++;
9275
9284
  } else if (arg === "--full-page") parsed.fullPage = true;
9276
9285
  else if (arg === "--all") parsed.all = true;
9277
9286
  else if (arg === "--session") {
@@ -9431,6 +9440,7 @@ var init_args = __esm({
9431
9440
  "--url",
9432
9441
  "--into",
9433
9442
  "--viewport",
9443
+ "--color-scheme",
9434
9444
  "--session",
9435
9445
  "--filter",
9436
9446
  "--name",
@@ -10221,7 +10231,7 @@ function validateHarAppendBatch(value, source) {
10221
10231
  return { entries, incompleteLifecycles };
10222
10232
  }
10223
10233
  function captureVersion() {
10224
- const version = "1.3.19";
10234
+ const version = "1.3.21";
10225
10235
  return typeof version === "string" && version.length > 0 ? version : "development";
10226
10236
  }
10227
10237
  function token() {
@@ -11980,7 +11990,7 @@ function wallTime(value) {
11980
11990
  }
11981
11991
  }
11982
11992
  function captureVersion2() {
11983
- const version = "1.3.19";
11993
+ const version = "1.3.21";
11984
11994
  return typeof version === "string" && version.length > 0 ? version : "development";
11985
11995
  }
11986
11996
  function deepFreeze(value) {
@@ -15933,6 +15943,21 @@ var init_interact = __esm({
15933
15943
  function pageInputDeps() {
15934
15944
  return deps2;
15935
15945
  }
15946
+ async function capturePageInputScreenshot(client, action, label2, noScreenshot) {
15947
+ try {
15948
+ return {
15949
+ screenshot: await deps2.autoScreenshot(client, action, label2, noScreenshot),
15950
+ screenshotWarning: null
15951
+ };
15952
+ } catch (error) {
15953
+ const message = error instanceof Error ? error.message : String(error);
15954
+ if (!/^CDP request timeout \(\d+ms\): Page\.captureScreenshot$/.test(message)) throw error;
15955
+ return {
15956
+ screenshot: null,
15957
+ screenshotWarning: `automatic screenshot timed out after the page action completed; no screenshot was saved (${message})`
15958
+ };
15959
+ }
15960
+ }
15936
15961
  function effectiveSettle(parsed, defaults) {
15937
15962
  if (parsed.settle !== void 0) return parsed.settle;
15938
15963
  return deps2.getActiveSession() !== null ? defaults.session : defaults.standalone;
@@ -16014,19 +16039,20 @@ async function cmdPageClick(parsed, _args) {
16014
16039
  const resolved = await resolveLiveTarget(live, target);
16015
16040
  if (!resolved.ok) return { failure: resolved };
16016
16041
  const dispatch2 = await clickResolved(live, resolved);
16017
- const screenshot2 = await deps2.autoScreenshot(client, "click", target, parsed.noScreenshot);
16018
- return { dispatch: dispatch2, screenshot: screenshot2 };
16042
+ const screenshotResult = await capturePageInputScreenshot(client, "click", target, parsed.noScreenshot);
16043
+ return { dispatch: dispatch2, ...screenshotResult };
16019
16044
  }
16020
16045
  );
16021
16046
  if ("failure" in outcome) {
16022
16047
  return emitResolutionError(parsed, "page click", outcome.failure);
16023
16048
  }
16024
- const { dispatch, screenshot } = outcome;
16049
+ const { dispatch, screenshot, screenshotWarning } = outcome;
16025
16050
  const rows = [
16026
16051
  fact`clicked ${dispatch.role ?? "unknown"} "${dispatch.name ?? ""}" (backend:${dispatch.backendNodeId}) at x=${dispatch.x} y=${dispatch.y}`,
16027
16052
  fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`
16028
16053
  ];
16029
16054
  if (screenshot) rows.push(fact`screenshot: ${screenshot}`);
16055
+ if (screenshotWarning) rows.push(fact`screenshot-warning: ${screenshotWarning}`);
16030
16056
  emitResult(
16031
16057
  {
16032
16058
  tag: "clicked",
@@ -16056,7 +16082,7 @@ input:
16056
16082
  --settle <ms> network-settle window applied after the click (default: 1000; 2500 with an active session; 0 disables)
16057
16083
  --no-screenshot skip the auto-screenshot
16058
16084
  output:
16059
- <clicked backend-node-id=\u2026 role=\u2026 name=\u2026> \u2014 resolved identity, dispatched coordinates, the measured settle (requested/waited), screenshot artifact path; --json mirrors the same fields
16085
+ <clicked backend-node-id=\u2026 role=\u2026 name=\u2026> \u2014 resolved identity, dispatched coordinates, the measured settle (requested/waited), and either the screenshot artifact path or a warning when only that follow-up capture timed out; --json mirrors the same fields
16060
16086
  effects:
16061
16087
  scrolls the target into view, then dispatches a real mouse press/release at its center; writes one screenshot into the active session's shots/ sequence unless --no-screenshot`;
16062
16088
  }
@@ -16091,19 +16117,20 @@ async function cmdPageType(parsed, _args) {
16091
16117
  } else {
16092
16118
  await typeText(live, textArg);
16093
16119
  }
16094
- const screenshot2 = await deps7.autoScreenshot(client, "type", parsed.into ?? "focused element", parsed.noScreenshot);
16095
- return { dispatch: dispatch2, screenshot: screenshot2 };
16120
+ const screenshotResult = await capturePageInputScreenshot(client, "type", parsed.into ?? "focused element", parsed.noScreenshot);
16121
+ return { dispatch: dispatch2, ...screenshotResult };
16096
16122
  }
16097
16123
  );
16098
16124
  if ("failure" in outcome) {
16099
16125
  return emitResolutionError(parsed, "page type", outcome.failure);
16100
16126
  }
16101
- const { dispatch, screenshot } = outcome;
16127
+ const { dispatch, screenshot, screenshotWarning } = outcome;
16102
16128
  const rows = [
16103
16129
  dispatch ? fact`typed "${textArg}" into ${dispatch.role ?? "unknown"} "${dispatch.name ?? ""}" (backend:${dispatch.backendNodeId}), focus click at x=${dispatch.x} y=${dispatch.y}` : fact`typed "${textArg}" into the focused element`,
16104
16130
  fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`
16105
16131
  ];
16106
16132
  if (screenshot) rows.push(fact`screenshot: ${screenshot}`);
16133
+ if (screenshotWarning) rows.push(fact`screenshot-warning: ${screenshotWarning}`);
16107
16134
  emitResult(
16108
16135
  {
16109
16136
  tag: "typed",
@@ -16131,7 +16158,7 @@ input:
16131
16158
  --settle <ms> network-settle window applied after typing (default: 500; 1500 with an active session; 0 disables)
16132
16159
  --no-screenshot skip the auto-screenshot
16133
16160
  output:
16134
- <typed> \u2014 the typed text, the resolved field identity (backend-node-id, role, name) and focus-click coordinates when --into was given, the measured settle (requested/waited), screenshot artifact path; --json mirrors the same fields
16161
+ <typed> \u2014 the typed text, the resolved field identity (backend-node-id, role, name) and focus-click coordinates when --into was given, the measured settle (requested/waited), and either the screenshot artifact path or a warning when only that follow-up capture timed out; --json mirrors the same fields
16135
16162
  effects:
16136
16163
  dispatches a real focus click (when --into) followed by real text insertion; writes one screenshot into the active session's shots/ sequence unless --no-screenshot`;
16137
16164
  }
@@ -16176,19 +16203,20 @@ async function cmdPageScroll(parsed, _args) {
16176
16203
  const resolved = await resolveLiveTarget(live, target);
16177
16204
  if (!resolved.ok) return { failure: resolved };
16178
16205
  const dispatch2 = await scrollResolved(live, resolved, to, { mark: `scroll:${target},to=${to}` });
16179
- const screenshot2 = await deps7.autoScreenshot(client, "scroll", target, parsed.noScreenshot);
16180
- return { dispatch: dispatch2, screenshot: screenshot2 };
16206
+ const screenshotResult = await capturePageInputScreenshot(client, "scroll", target, parsed.noScreenshot);
16207
+ return { dispatch: dispatch2, ...screenshotResult };
16181
16208
  }
16182
16209
  );
16183
16210
  if ("failure" in outcome) {
16184
16211
  return emitResolutionError(parsed, "page scroll", outcome.failure);
16185
16212
  }
16186
- const { dispatch, screenshot } = outcome;
16213
+ const { dispatch, screenshot, screenshotWarning } = outcome;
16187
16214
  const rows = [
16188
16215
  fact`scrolled ${dispatch.role ?? "unknown"} "${dispatch.name ?? ""}" (backend:${dispatch.backendNodeId}) to ${dispatch.to} — scrollTop now ${dispatch.scrollTop}`,
16189
16216
  fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`
16190
16217
  ];
16191
16218
  if (screenshot) rows.push(fact`screenshot: ${screenshot}`);
16219
+ if (screenshotWarning) rows.push(fact`screenshot-warning: ${screenshotWarning}`);
16192
16220
  emitResult(
16193
16221
  {
16194
16222
  tag: "scrolled",
@@ -16219,7 +16247,7 @@ input:
16219
16247
  --settle <ms> network-settle window applied after the scroll (default: 1000; 2500 with an active session; 0 disables)
16220
16248
  --no-screenshot skip the auto-screenshot
16221
16249
  output:
16222
- <scrolled backend-node-id=\u2026 role=\u2026 name=\u2026> \u2014 resolved identity, destination, the container's resulting scrollTop, the measured settle (requested/waited), screenshot artifact path; --json mirrors the same fields
16250
+ <scrolled backend-node-id=\u2026 role=\u2026 name=\u2026> \u2014 resolved identity, destination, the container's resulting scrollTop, the measured settle (requested/waited), and either the screenshot artifact path or a warning when only that follow-up capture timed out; --json mirrors the same fields
16223
16251
  effects:
16224
16252
  assigns the container's scrollTop in-page (may trigger lazy-load network); writes one screenshot into the active session's shots/ sequence unless --no-screenshot`;
16225
16253
  }
@@ -227597,6 +227625,49 @@ var init_viewport = __esm({
227597
227625
  }
227598
227626
  });
227599
227627
 
227628
+ // src/cdp/color-scheme.ts
227629
+ function parseColorScheme(value) {
227630
+ if (value === void 0) return void 0;
227631
+ if (value === "dark" || value === "light") return value;
227632
+ throw new Error("--color-scheme must be dark or light");
227633
+ }
227634
+ async function withAppliedColorScheme(client, colorScheme, fn) {
227635
+ if (!colorScheme) return fn();
227636
+ let ownsMediaOverride = false;
227637
+ let primaryFailed = false;
227638
+ let primaryError;
227639
+ try {
227640
+ ownsMediaOverride = true;
227641
+ await client.send("Emulation.setEmulatedMedia", {
227642
+ features: [{ name: "prefers-color-scheme", value: colorScheme }]
227643
+ });
227644
+ return await fn();
227645
+ } catch (error) {
227646
+ primaryFailed = true;
227647
+ primaryError = error;
227648
+ throw error;
227649
+ } finally {
227650
+ if (ownsMediaOverride) {
227651
+ try {
227652
+ await client.send("Emulation.setEmulatedMedia", { features: [] });
227653
+ } catch (cleanupError) {
227654
+ if (primaryFailed) {
227655
+ throw new AggregateError(
227656
+ [primaryError, cleanupError],
227657
+ "Color-scheme capture failed and media-emulation cleanup also failed.",
227658
+ { cause: primaryError }
227659
+ );
227660
+ }
227661
+ throw cleanupError;
227662
+ }
227663
+ }
227664
+ }
227665
+ }
227666
+ var init_color_scheme = __esm({
227667
+ "src/cdp/color-scheme.ts"() {
227668
+ }
227669
+ });
227670
+
227600
227671
  // src/cdp/commands/page/shot.ts
227601
227672
  function pngDimensions(png) {
227602
227673
  if (png.length < 24) return null;
@@ -227625,7 +227696,8 @@ function buildScreenshotResult(f) {
227625
227696
  const scale = f.dimensions && f.cssViewport ? { x: f.dimensions.width / f.cssViewport.width, y: f.dimensions.height / f.cssViewport.height } : void 0;
227626
227697
  const sections = [
227627
227698
  scale && f.cssViewport ? fact`CSS-to-image scale: ${scale.x.toFixed(6)} image px/CSS px horizontally and ${scale.y.toFixed(6)} image px/CSS px vertically (captured CSS region ${round6(f.cssViewport.width)}×${round6(f.cssViewport.height)} at page origin ${round6(f.cssViewport.x)},${round6(f.cssViewport.y)}).` : text`CSS-to-image scale unavailable: the browser did not return the CSS region this capture covers.`,
227628
- f.emulation === "none" ? text`emulation: none — the browser's actual current viewport was captured; no Emulation call was made.` : f.emulation === "viewport" ? fact`emulation: transient ${f.viewport.width}x${f.viewport.height} device-metrics override applied for the capture and cleared after — two page-observable resizes.` : f.viewport ? fact`emulation: transient full-page device-metrics override (from ${f.viewport.width}x${f.viewport.height}) applied for the capture and cleared after — two page-observable resizes.` : text`emulation: transient full-page device-metrics override applied for the capture and cleared after — two page-observable resizes.`
227699
+ f.emulation === "none" ? text`viewport emulation: none — the browser's actual current viewport was captured.` : f.emulation === "viewport" ? fact`emulation: transient ${f.viewport.width}x${f.viewport.height} device-metrics override applied for the capture and cleared after — two page-observable resizes.` : f.viewport ? fact`emulation: transient full-page device-metrics override (from ${f.viewport.width}x${f.viewport.height}) applied for the capture and cleared after — two page-observable resizes.` : text`emulation: transient full-page device-metrics override applied for the capture and cleared after — two page-observable resizes.`,
227700
+ f.colorScheme ? fact`color scheme: transient prefers-color-scheme=${f.colorScheme} override applied for the capture and cleared after.` : text`color scheme: browser default — no Emulation.setEmulatedMedia call was made.`
227629
227701
  ];
227630
227702
  return {
227631
227703
  tag: "screenshot",
@@ -227639,7 +227711,8 @@ function buildScreenshotResult(f) {
227639
227711
  "css-y": f.cssViewport === void 0 ? void 0 : round6(f.cssViewport.y),
227640
227712
  "css-to-image-x": scale?.x.toFixed(6),
227641
227713
  "css-to-image-y": scale?.y.toFixed(6),
227642
- emulation: f.emulation
227714
+ emulation: f.emulation,
227715
+ "color-scheme": f.colorScheme
227643
227716
  },
227644
227717
  summary: f.dimensions ? fact`saved ${f.path} — ${f.dimensions.width}x${f.dimensions.height}px, ${f.bytes} bytes.` : fact`saved ${f.path} — ${f.bytes} bytes.`,
227645
227718
  sections
@@ -227655,7 +227728,7 @@ async function cmdPageShot(parsed, _args) {
227655
227728
  {
227656
227729
  tag: "error",
227657
227730
  attrs: { command: "page shot", code: "invalid_input" },
227658
- summary: fact`received: ${parsed.positional.length} positional arguments; expected none — \`capture page shot [--viewport <WxH>] [--full-page] [--out <path>]\`.`
227731
+ summary: fact`received: ${parsed.positional.length} positional arguments; expected none — \`capture page shot [--viewport <WxH>] [--full-page] [--color-scheme <dark|light>] [--out <path>]\`.`
227659
227732
  },
227660
227733
  { json: parsed.json }
227661
227734
  );
@@ -227679,18 +227752,33 @@ async function cmdPageShot(parsed, _args) {
227679
227752
  return;
227680
227753
  }
227681
227754
  }
227755
+ let colorScheme;
227756
+ try {
227757
+ colorScheme = parseColorScheme(parsed.colorScheme);
227758
+ } catch {
227759
+ emitResult(
227760
+ {
227761
+ tag: "error",
227762
+ attrs: { command: "page shot", code: "invalid_color_scheme" },
227763
+ summary: fact`received: --color-scheme ${parsed.colorScheme}; expected dark or light.`
227764
+ },
227765
+ { json: parsed.json }
227766
+ );
227767
+ process.exitCode = 1;
227768
+ return;
227769
+ }
227682
227770
  const emulation = parsed.fullPage ? "full-page" : viewport2 ? "viewport" : "none";
227683
227771
  const result = await deps4.withConnection(
227684
227772
  { ...parsed, command: "shot" },
227685
- async (client) => {
227773
+ async (client) => withAppliedColorScheme(client, colorScheme, async () => {
227686
227774
  const screenshot = await captureScreenshotWithCssViewport(client, viewport2, { fullPage: parsed.fullPage });
227687
227775
  const outPath = await resolveOutPath(parsed);
227688
227776
  writeScreenshot(outPath, screenshot.png);
227689
227777
  return { path: outPath, bytes: screenshot.png.length, dimensions: pngDimensions(screenshot.png), cssViewport: screenshot.cssViewport };
227690
- },
227778
+ }),
227691
227779
  { settle: 0 }
227692
227780
  );
227693
- emitResult(buildScreenshotResult({ ...result, emulation, viewport: viewport2 }), { json: parsed.json });
227781
+ emitResult(buildScreenshotResult({ ...result, emulation, viewport: viewport2, colorScheme }), { json: parsed.json });
227694
227782
  }
227695
227783
  var fs15, path17, USAGE6, deps4, PNG_SIGNATURE;
227696
227784
  var init_shot = __esm({
@@ -227700,6 +227788,7 @@ var init_shot = __esm({
227700
227788
  init_connection();
227701
227789
  init_screenshot();
227702
227790
  init_viewport();
227791
+ init_color_scheme();
227703
227792
  init_session_context();
227704
227793
  init_commands();
227705
227794
  init_artifacts();
@@ -227709,13 +227798,14 @@ var init_shot = __esm({
227709
227798
  input:
227710
227799
  --viewport <WxH> transient device-metrics override for this capture; grammar: <positive-safe-int>x<positive-safe-int>, exact lowercase x with no whitespace \u2014 preset names are not accepted. Absent \u2192 no emulation, the browser's actual current viewport is captured
227711
227800
  --full-page transient override to the full scrollable content height for this capture
227801
+ --color-scheme <dark|light> transient prefers-color-scheme override for this capture; cleared afterward
227712
227802
  --out <path> destination file; default: the active session's shots/ sequence, or a fresh oneshot-*/page/ dir under the capture root when no session is active
227713
227803
  --target <tabId> | --url <pattern> | --port <n> tab targeting; defaults to the active session tab
227714
227804
  --json mirror the result as JSON
227715
227805
  output:
227716
- <screenshot path=\u2026 width=\u2026 height=\u2026 css-width=\u2026 css-height=\u2026 css-x=\u2026 css-y=\u2026 css-to-image-x=\u2026 css-to-image-y=\u2026 emulation=none|viewport|full-page> \u2014 saved path, PNG pixel dimensions, the CSS-pixel region the image covers (its page-coordinate origin and size) with the CSS-to-image scale derived from it, byte size, and the emulation fact (whether a transient override was applied, or its absence)
227806
+ <screenshot path=\u2026 width=\u2026 height=\u2026 css-width=\u2026 css-height=\u2026 css-x=\u2026 css-y=\u2026 css-to-image-x=\u2026 css-to-image-y=\u2026 emulation=none|viewport|full-page color-scheme=dark|light> \u2014 saved path, PNG pixel dimensions, the CSS-pixel region the image covers (its page-coordinate origin and size) with the CSS-to-image scale derived from it, byte size, and any requested transient emulation
227717
227807
  effects:
227718
- no flags: none \u2014 the capture reads the viewport as-is, with zero Emulation.* calls. With --viewport/--full-page: applies a transient Emulation.setDeviceMetricsOverride (~150ms re-layout wait) and clears it after the capture \u2014 two page-observable resizes (resize events fire; media queries flip and flip back)`;
227808
+ no flags: none \u2014 the capture reads the viewport as-is, with zero Emulation.* calls. --viewport/--full-page applies a transient Emulation.setDeviceMetricsOverride (~150ms re-layout wait) and clears it after the capture \u2014 two page-observable resizes. --color-scheme applies a transient Emulation.setEmulatedMedia prefers-color-scheme override and clears it after the capture.`;
227719
227809
  deps4 = { withConnection, nextStepPath, createOneshotSession };
227720
227810
  PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
227721
227811
  }
@@ -227856,7 +227946,7 @@ During a live composed recording every verb routes through the recorder.
227856
227946
  <subcommand name="scroll" args="<target> --to <top|bottom|px> [--no-screenshot]" whenToUse="scroll one resolved container to a position"/>
227857
227947
  <subcommand name="navigate" args="<url> [--settle <ms>]" whenToUse="navigate the tab and wait for load + settle"/>
227858
227948
  <subcommand name="exec" args="<code> | --file <path>" whenToUse="run arbitrary JS in the tab (expressions, return, await)"/>
227859
- <subcommand name="shot" args="[--viewport <WxH>] [--full-page] [--out <path>]" whenToUse="look at the page right now without acting"/>
227949
+ <subcommand name="shot" args="[--viewport <WxH>] [--full-page] [--color-scheme <dark|light>] [--out <path>]" whenToUse="look at the page right now without acting"/>
227860
227950
  <subcommand name="elements" args="[--all] [--limit <n>]" whenToUse="list what can be acted on \u2014 role, name, backend:<id> per element"/>
227861
227951
 
227862
227952
  capture page <leaf> -h Per-leaf usage`;
@@ -230854,17 +230944,23 @@ function normalizeComputed(raw) {
230854
230944
  function resolvedIdentity3(backendNodeId2) {
230855
230945
  return backendNodeId2 === void 0 ? { backendNodeId: null, identityUnresolved: true } : { backendNodeId: backendNodeId2 };
230856
230946
  }
230857
- async function mapWithConcurrency(values, limit, fn) {
230858
- const result = new Array(values.length);
230947
+ async function mapWithConcurrency(values, limit, budgetMs, fn) {
230948
+ const result = Array(values.length).fill(void 0);
230949
+ const deadline = Date.now() + budgetMs;
230859
230950
  let next = 0;
230951
+ let truncated = false;
230860
230952
  const worker = async () => {
230861
230953
  while (next < values.length) {
230954
+ if (Date.now() >= deadline) {
230955
+ truncated = true;
230956
+ return;
230957
+ }
230862
230958
  const index = next++;
230863
230959
  result[index] = await fn(values[index], index);
230864
230960
  }
230865
230961
  };
230866
230962
  await Promise.all(Array.from({ length: Math.min(limit, values.length) }, worker));
230867
- return result;
230963
+ return { values: result, truncated };
230868
230964
  }
230869
230965
  async function collectStylesInner(ctx, styleSheetUrls, styleSheetHeaders) {
230870
230966
  const { client } = ctx;
@@ -230914,7 +231010,7 @@ async function collectStylesInner(ctx, styleSheetUrls, styleSheetHeaders) {
230914
231010
  resolved = facts.map(() => void 0);
230915
231011
  }
230916
231012
  const sourceCache = /* @__PURE__ */ new Map();
230917
- const elements = await mapWithConcurrency(facts, PROVENANCE_CONCURRENCY, async (fact4, index) => {
231013
+ const provenance = await mapWithConcurrency(facts, PROVENANCE_CONCURRENCY, PROVENANCE_BUDGET_MS, async (fact4, index) => {
230918
231014
  const ref = resolved[index];
230919
231015
  const computed2 = normalizeComputed(fact4.computed);
230920
231016
  let winningDeclarations;
@@ -230949,6 +231045,7 @@ async function collectStylesInner(ctx, styleSheetUrls, styleSheetHeaders) {
230949
231045
  provenanceUnavailable
230950
231046
  };
230951
231047
  });
231048
+ const elements = provenance.values.filter((element) => element !== void 0);
230952
231049
  ctx.write.json("styles.json", {
230953
231050
  elements,
230954
231051
  coverage: {
@@ -230957,7 +231054,9 @@ async function collectStylesInner(ctx, styleSheetUrls, styleSheetHeaders) {
230957
231054
  shadowRootsNotWalked: inventory.shadowRootsNotWalked,
230958
231055
  totalCandidateElements: inventory.total,
230959
231056
  keptElements: elements.length,
230960
- elementsTruncated: inventory.total > elements.length
231057
+ elementsTruncated: inventory.total > facts.length,
231058
+ provenanceTruncated: provenance.truncated,
231059
+ ...provenance.truncated ? { provenanceTruncationReason: "collector-time-budget" } : {}
230961
231060
  },
230962
231061
  styleSheetHeaders,
230963
231062
  identity: identity3,
@@ -230965,13 +231064,14 @@ async function collectStylesInner(ctx, styleSheetUrls, styleSheetHeaders) {
230965
231064
  ...unavailableReason ? { unavailableReason } : {}
230966
231065
  });
230967
231066
  }
230968
- var STYLES_MAX_ELEMENTS, PROVENANCE_CONCURRENCY, COMPUTED_PROPERTIES, PROVENANCE_PROPERTIES, EXCLUDED_TAGS, STYLES_SCRIPT, collectStyles;
231067
+ var STYLES_MAX_ELEMENTS, PROVENANCE_CONCURRENCY, PROVENANCE_BUDGET_MS, COMPUTED_PROPERTIES, PROVENANCE_PROPERTIES, EXCLUDED_TAGS, STYLES_SCRIPT, collectStyles;
230969
231068
  var init_styles = __esm({
230970
231069
  "src/cdp/measure/collectors/styles.ts"() {
230971
231070
  init_redaction();
230972
231071
  init_style_provenance();
230973
231072
  STYLES_MAX_ELEMENTS = 150;
230974
231073
  PROVENANCE_CONCURRENCY = 8;
231074
+ PROVENANCE_BUDGET_MS = 5e3;
230975
231075
  COMPUTED_PROPERTIES = [
230976
231076
  "display",
230977
231077
  "position",
@@ -235864,22 +235964,24 @@ function boundedClient(client, phase, partialPath, onTimeout) {
235864
235964
  const timeoutMs = Math.min(timeout, SNAPSHOT_REQUEST_TIMEOUT_MS);
235865
235965
  let timer;
235866
235966
  let timedOut = false;
235867
- const markTimedOut = () => {
235967
+ const markTimedOut = (error) => {
235868
235968
  if (timedOut) return;
235869
235969
  timedOut = true;
235870
- onTimeout?.();
235970
+ onTimeout?.(error);
235871
235971
  };
235872
235972
  const deadline = new Promise((_, reject) => {
235873
235973
  timer = setTimeout(() => {
235874
- markTimedOut();
235875
- reject(new SnapshotCaptureTimeout(phase, method, timeoutMs, partialPath));
235974
+ const error = new SnapshotCaptureTimeout(phase, method, timeoutMs, partialPath);
235975
+ markTimedOut(error);
235976
+ reject(error);
235876
235977
  }, timeoutMs);
235877
235978
  });
235878
235979
  const request = Promise.resolve().then(() => client.send(method, params, timeoutMs, sessionId)).catch((error) => {
235879
235980
  if (error instanceof SnapshotCaptureTimeout) throw error;
235880
235981
  if (error instanceof Error && error.message.startsWith("CDP request timeout")) {
235881
- markTimedOut();
235882
- throw new SnapshotCaptureTimeout(phase, method, timeoutMs, partialPath);
235982
+ const timeoutError = new SnapshotCaptureTimeout(phase, method, timeoutMs, partialPath);
235983
+ markTimedOut(timeoutError);
235984
+ throw timeoutError;
235883
235985
  }
235884
235986
  throw error;
235885
235987
  });
@@ -235901,6 +236003,7 @@ async function captureSnapshotSubstrate(options) {
235901
236003
  const pixels = options.pixels ?? false;
235902
236004
  const state = options.state ?? [];
235903
236005
  const viewport2 = options.viewport ?? null;
236006
+ const colorScheme = options.colorScheme;
235904
236007
  const url = options.url ? sanitizeString(options.url) : null;
235905
236008
  const artifacts = [];
235906
236009
  const phaseClient = (phase) => boundedClient(client, phase, dir);
@@ -235982,21 +236085,25 @@ async function captureSnapshotSubstrate(options) {
235982
236085
  if (skipCollectors.has(collector.name)) return { name: collector.name, status: "skipped" };
235983
236086
  const startedAt = Date.now();
235984
236087
  let requestTimeouts = 0;
236088
+ let timeoutError;
235985
236089
  const collectorWriter = makeCollectorWriter(ctx.write);
235986
236090
  try {
235987
236091
  await collector.fn({
235988
236092
  ...ctx,
235989
- client: boundedClient(client, `collector:${collector.name}`, dir, () => {
236093
+ client: boundedClient(client, `collector:${collector.name}`, dir, (error) => {
235990
236094
  requestTimeouts += 1;
236095
+ timeoutError ??= error;
235991
236096
  }),
235992
236097
  write: collectorWriter.writer
235993
236098
  });
236099
+ if (collector.name === "styles" && timeoutError) throw timeoutError;
235994
236100
  const ms = Date.now() - startedAt;
235995
236101
  collectorWriter.commit();
235996
236102
  return requestTimeouts > 0 ? { name: collector.name, status: "degraded", ms, requestTimeouts } : { name: collector.name, status: "ok", ms };
235997
- } catch {
236103
+ } catch (error) {
235998
236104
  const ms = Date.now() - startedAt;
235999
236105
  collectorWriter.discard();
236106
+ if (collector.name === "styles" && error instanceof SnapshotCaptureTimeout) throw error;
236000
236107
  return { name: collector.name, status: "error", ms, ...requestTimeouts > 0 ? { requestTimeouts } : {} };
236001
236108
  }
236002
236109
  };
@@ -236043,6 +236150,7 @@ async function captureSnapshotSubstrate(options) {
236043
236150
  captureUnsettled,
236044
236151
  pixels,
236045
236152
  states: state.map((spec) => sanitizeString(spec)),
236153
+ ...colorScheme ? { colorScheme } : {},
236046
236154
  unstableRegionCount: unstableRegions.length,
236047
236155
  ...freezeAnimations ? { animationsRestored: animationsRestored ?? false } : {},
236048
236156
  // #8: the browser-wide `Animation.setPlaybackRate({playbackRate:0})`
@@ -236568,7 +236676,7 @@ function settledNote(settled, captured, settleMs, settleTimeoutMs, unstableCount
236568
236676
  }
236569
236677
  return fact`Page did not settle within ${settleTimeoutMs}ms. No queryable substrate was written; evidence artifacts only (${unstableCount} unstable region(s)).`;
236570
236678
  }
236571
- async function captureMeasureSnap(parsed, targetRef = parsed.positional[0], viewportValue = parsed.viewport) {
236679
+ async function captureMeasureSnap(parsed, targetRef = parsed.positional[0], viewportValue = parsed.viewport, colorSchemeValue = parsed.colorScheme) {
236572
236680
  if (parsed.positional.length > 1) {
236573
236681
  throw invalidInput("measure snap accepts at most one positional URL or snapshot reference");
236574
236682
  }
@@ -236581,6 +236689,7 @@ async function captureMeasureSnap(parsed, targetRef = parsed.positional[0], view
236581
236689
  }
236582
236690
  }
236583
236691
  const viewport2 = viewportValue === void 0 ? void 0 : { label: viewportValue, ...parseViewport(viewportValue) };
236692
+ const colorScheme = parseColorScheme(colorSchemeValue);
236584
236693
  let base;
236585
236694
  let baseUrl = null;
236586
236695
  if (targetRef && !isUrlRef(targetRef)) {
@@ -236595,7 +236704,7 @@ async function captureMeasureSnap(parsed, targetRef = parsed.positional[0], view
236595
236704
  try {
236596
236705
  let artifacts = [];
236597
236706
  await withConnection(connectionArgs, async (client, tab) => {
236598
- await withAppliedViewport(client, viewport2, async () => {
236707
+ await withAppliedColorScheme(client, colorScheme, async () => withAppliedViewport(client, viewport2, async () => {
236599
236708
  const oneshot = active ? void 0 : createOneshotSession("measure");
236600
236709
  const destination = active ? path22.join(active.dir, "measure", "snaps") : oneshot.artifactsDir;
236601
236710
  snapDir = path22.join(destination, snapId);
@@ -236611,10 +236720,11 @@ async function captureMeasureSnap(parsed, targetRef = parsed.positional[0], view
236611
236720
  pixels: parsed.pixels,
236612
236721
  state: parsed.state ?? [],
236613
236722
  viewport: viewport2?.label ?? null,
236723
+ colorScheme,
236614
236724
  skipCollectors: parsed.skipCollector ?? []
236615
236725
  });
236616
236726
  artifacts = result.artifacts;
236617
- });
236727
+ }));
236618
236728
  }, { settle: 0 });
236619
236729
  if (!snapDir) throw new Error("measure snap did not allocate an artifact directory");
236620
236730
  return { id: snapId, dir: snapDir, artifacts, ...base ? { base } : {} };
@@ -236640,6 +236750,7 @@ function buildSnapshotResult(captured) {
236640
236750
  attrs: {
236641
236751
  url: meta.url ?? void 0,
236642
236752
  viewport: meta.viewport ?? void 0,
236753
+ "color-scheme": meta.colorScheme,
236643
236754
  elements,
236644
236755
  settled: meta.settled,
236645
236756
  "settle-ms": meta.settleMs,
@@ -236670,7 +236781,7 @@ function classifySnapError(err) {
236670
236781
  if (message.startsWith("--viewport cannot ")) {
236671
236782
  return { status: "viewport_unavailable", message };
236672
236783
  }
236673
- if (message.startsWith("measure snap accepts") || message.startsWith("--settle-timeout") || message.startsWith("--viewport")) {
236784
+ if (message.startsWith("measure snap accepts") || message.startsWith("--settle-timeout") || message.startsWith("--viewport") || message.startsWith("--color-scheme")) {
236674
236785
  return { status: "invalid_input", message };
236675
236786
  }
236676
236787
  if (message.includes("No tab found") || message.includes("Use --target") || message.includes("Tab has no WebSocket debugger URL")) {
@@ -236715,9 +236826,10 @@ async function cmdMeasureSnap(parsed, _args) {
236715
236826
  if (value === void 0) return void 0;
236716
236827
  return { label: value, ...parseViewport(value) };
236717
236828
  });
236829
+ const colorScheme = parseColorScheme(parsed.colorScheme);
236718
236830
  const results = [];
236719
236831
  for (const viewport2 of viewports) {
236720
- const capture = await captureMeasureSnap(parsed, parsed.positional[0], viewport2?.label);
236832
+ const capture = await captureMeasureSnap(parsed, parsed.positional[0], viewport2?.label, colorScheme);
236721
236833
  captures.push(capture);
236722
236834
  results.push(buildSnapshotResult(capture));
236723
236835
  }
@@ -236767,6 +236879,7 @@ var init_snap = __esm({
236767
236879
  init_connection();
236768
236880
  init_recorder_client();
236769
236881
  init_viewport();
236882
+ init_color_scheme();
236770
236883
  init_snapshot();
236771
236884
  init_redaction();
236772
236885
  init_settle();
@@ -236787,8 +236900,9 @@ input:
236787
236900
  --pixels also write per-element raster crops
236788
236901
  --state <state[:selector]> force a pseudo-state or real control state (repeatable)
236789
236902
  --viewport <WxH> temporarily capture at a CSS-pixel viewport (repeatable); exact <positive-safe-int>x<positive-safe-int> grammar with lowercase x and no whitespace
236903
+ --color-scheme <dark|light> temporarily capture under this prefers-color-scheme value; cleared afterward
236790
236904
  output: <snapshot id=\u2026 path=\u2026> \u2014 the settled artifact directory (geometry, styles, hit-test, text, forms, animation, ax, queries, focus, scroll, layers; per-element crops with --pixels) every other measure/motion query leaf reads instead of re-driving the browser; --json mirrors
236791
- effects: drives the target page to capture; writes one snapshot artifact directory under the active (or a private one-shot) session`;
236905
+ effects: drives the target page to capture; writes one snapshot artifact directory under the active (or a private one-shot) session. --color-scheme transiently applies Emulation.setEmulatedMedia and clears it after capture.`;
236792
236906
  }
236793
236907
  });
236794
236908
 
@@ -240154,7 +240268,7 @@ Every leaf defaults to rendered prose; --json mirrors the same result.
240154
240268
  Findings exit 0 \u2014 a report, not a failure.
240155
240269
  \`--gate\` (exit 2 on findings/changes) is accepted only by check and diff.
240156
240270
 
240157
- <subcommand name="snap" args="[url|snap] [--freeze-animations] [--settle-timeout <ms>] [--capture-unsettled] [--pixels] [--state <state[:selector]>]... [--viewport <WxH>]..." whenToUse="drive + write the settled snapshot substrate every other leaf reads"/>
240271
+ <subcommand name="snap" args="[url|snap] [--freeze-animations] [--settle-timeout <ms>] [--capture-unsettled] [--pixels] [--state <state[:selector]>]... [--viewport <WxH>]... [--color-scheme <dark|light>]" whenToUse="drive + write the settled snapshot substrate every other leaf reads"/>
240158
240272
  <subcommand name="check" args="[url|snap] [--for <checks>] [--gate]" whenToUse="read threshold/fact measurements from one snapshot"/>
240159
240273
  <subcommand name="diff" args="--before <snap> --after <snap> [--pixels] [--full] [--gate]" whenToUse="structured before/after delta between two snapshots"/>
240160
240274
  <subcommand name="census" args="[--snap <id>]... [--url <url>]... [--set-file <path>] --axis <axis>" whenToUse="value distributions across one or more snapshots"/>
@@ -242098,7 +242212,7 @@ CDP_PORT / CDP_TARGET pin the browser + tab for orchestrators \u2014 precedence:
242098
242212
  explicit flag > active session > env.`;
242099
242213
  }
242100
242214
  function printVersion() {
242101
- const declared = "1.3.19";
242215
+ const declared = "1.3.21";
242102
242216
  if (declared) {
242103
242217
  console.log(declared);
242104
242218
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/capture",
3
- "version": "1.3.19",
3
+ "version": "1.3.21",
4
4
  "description": "Browser automation and UI validation via CDP — session-based screenshots, HAR, a11y, click/type, and JS exec",
5
5
  "bin": {
6
6
  "capture": "./bin/capture"
package/src/cdp/args.ts CHANGED
@@ -15,7 +15,7 @@ export function expandEqualsFlags(argv: string[]): string[] {
15
15
  }
16
16
 
17
17
  const VALUE_FLAGS = new Set([
18
- '--port', '--out', '--duration', '--settle', '--file', '--target', '--url', '--into', '--viewport', '--session',
18
+ '--port', '--out', '--duration', '--settle', '--file', '--target', '--url', '--into', '--viewport', '--color-scheme', '--session',
19
19
  '--filter', '--name', '--filter-url', '--filter-status', '--filter-method', '--limit', '--params', '--wait-event',
20
20
  '--timeout', '--socket', '--settle-timeout', '--state', '--for', '--before', '--after', '--snap', '--set-file',
21
21
  '--axis', '--from', '--to', '--viewport-height', '--rec-id', '--selector', '--do', '--element', '--prop', '--action', '--occurrence',
@@ -126,6 +126,9 @@ export function validateCliInvocation(parsed: ParsedArgs): void {
126
126
  throw invalidInput(`Invalid --viewport: ${parsed.viewport}.`);
127
127
  }
128
128
  }
129
+ if (leaf === 'shot' && parsed.colorScheme !== undefined && parsed.colorScheme !== 'dark' && parsed.colorScheme !== 'light') {
130
+ throw invalidInput(`Invalid --color-scheme: ${parsed.colorScheme}; expected dark or light.`);
131
+ }
129
132
  return;
130
133
  }
131
134
 
@@ -143,6 +146,9 @@ export function validateCliInvocation(parsed: ParsedArgs): void {
143
146
 
144
147
  if (parsed.command === 'measure') {
145
148
  if (leaf === 'snap' || leaf === 'check' || leaf === 'sweep') requireCount(values, 0, 1, `measure ${leaf}`);
149
+ if (leaf === 'snap' && parsed.colorScheme !== undefined && parsed.colorScheme !== 'dark' && parsed.colorScheme !== 'light') {
150
+ throw invalidInput(`Invalid --color-scheme: ${parsed.colorScheme}; expected dark or light.`);
151
+ }
146
152
  if (leaf === 'diff' || leaf === 'census') requireCount(values, 0, 0, `measure ${leaf}`);
147
153
  if (leaf === 'diff' && (!parsed.before || !parsed.after)) throw invalidInput('measure diff requires --before and --after.');
148
154
  if (leaf === 'census' && !['color', 'font', 'spacing', 'radius', 'shadow', 'animation', 'geometry', 'media', 'queries'].includes(parsed.axis ?? '')) {
@@ -233,6 +239,7 @@ export function parseCliSyntax(argv: string[]): ParsedArgs {
233
239
  else if (arg === '--into') { parsed.into = valueFor(arg, next); i++; }
234
240
  else if (arg === '--no-screenshot') parsed.noScreenshot = true;
235
241
  else if (arg === '--viewport') { const value = valueFor(arg, next); parsed.viewport = value; (parsed.viewports ??= []).push(value); i++; }
242
+ else if (arg === '--color-scheme') { parsed.colorScheme = valueFor(arg, next); i++; }
236
243
  else if (arg === '--full-page') parsed.fullPage = true;
237
244
  else if (arg === '--all') parsed.all = true;
238
245
  else if (arg === '--session') { parsed.session = valueFor(arg, next); i++; }