@crouton-kit/capture 1.3.19 → 1.3.20
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 +99 -19
- package/package.json +1 -1
- package/src/cdp/args.ts +8 -1
- package/src/cdp/color-scheme.ts +47 -0
- package/src/cdp/commands/measure/index.ts +1 -1
- package/src/cdp/commands/measure/snap.ts +13 -6
- package/src/cdp/commands/page/index.ts +1 -1
- package/src/cdp/commands/page/shot.ts +33 -9
- package/src/cdp/measure/snapshot.ts +2 -0
- package/src/cdp/measure/types.ts +4 -0
- package/src/cdp/types.ts +2 -0
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.
|
|
10234
|
+
const version = "1.3.20";
|
|
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.
|
|
11993
|
+
const version = "1.3.20";
|
|
11984
11994
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
11985
11995
|
}
|
|
11986
11996
|
function deepFreeze(value) {
|
|
@@ -227597,6 +227607,49 @@ var init_viewport = __esm({
|
|
|
227597
227607
|
}
|
|
227598
227608
|
});
|
|
227599
227609
|
|
|
227610
|
+
// src/cdp/color-scheme.ts
|
|
227611
|
+
function parseColorScheme(value) {
|
|
227612
|
+
if (value === void 0) return void 0;
|
|
227613
|
+
if (value === "dark" || value === "light") return value;
|
|
227614
|
+
throw new Error("--color-scheme must be dark or light");
|
|
227615
|
+
}
|
|
227616
|
+
async function withAppliedColorScheme(client, colorScheme, fn) {
|
|
227617
|
+
if (!colorScheme) return fn();
|
|
227618
|
+
let ownsMediaOverride = false;
|
|
227619
|
+
let primaryFailed = false;
|
|
227620
|
+
let primaryError;
|
|
227621
|
+
try {
|
|
227622
|
+
ownsMediaOverride = true;
|
|
227623
|
+
await client.send("Emulation.setEmulatedMedia", {
|
|
227624
|
+
features: [{ name: "prefers-color-scheme", value: colorScheme }]
|
|
227625
|
+
});
|
|
227626
|
+
return await fn();
|
|
227627
|
+
} catch (error) {
|
|
227628
|
+
primaryFailed = true;
|
|
227629
|
+
primaryError = error;
|
|
227630
|
+
throw error;
|
|
227631
|
+
} finally {
|
|
227632
|
+
if (ownsMediaOverride) {
|
|
227633
|
+
try {
|
|
227634
|
+
await client.send("Emulation.setEmulatedMedia", { features: [] });
|
|
227635
|
+
} catch (cleanupError) {
|
|
227636
|
+
if (primaryFailed) {
|
|
227637
|
+
throw new AggregateError(
|
|
227638
|
+
[primaryError, cleanupError],
|
|
227639
|
+
"Color-scheme capture failed and media-emulation cleanup also failed.",
|
|
227640
|
+
{ cause: primaryError }
|
|
227641
|
+
);
|
|
227642
|
+
}
|
|
227643
|
+
throw cleanupError;
|
|
227644
|
+
}
|
|
227645
|
+
}
|
|
227646
|
+
}
|
|
227647
|
+
}
|
|
227648
|
+
var init_color_scheme = __esm({
|
|
227649
|
+
"src/cdp/color-scheme.ts"() {
|
|
227650
|
+
}
|
|
227651
|
+
});
|
|
227652
|
+
|
|
227600
227653
|
// src/cdp/commands/page/shot.ts
|
|
227601
227654
|
function pngDimensions(png) {
|
|
227602
227655
|
if (png.length < 24) return null;
|
|
@@ -227625,7 +227678,8 @@ function buildScreenshotResult(f) {
|
|
|
227625
227678
|
const scale = f.dimensions && f.cssViewport ? { x: f.dimensions.width / f.cssViewport.width, y: f.dimensions.height / f.cssViewport.height } : void 0;
|
|
227626
227679
|
const sections = [
|
|
227627
227680
|
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
|
|
227681
|
+
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.`,
|
|
227682
|
+
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
227683
|
];
|
|
227630
227684
|
return {
|
|
227631
227685
|
tag: "screenshot",
|
|
@@ -227639,7 +227693,8 @@ function buildScreenshotResult(f) {
|
|
|
227639
227693
|
"css-y": f.cssViewport === void 0 ? void 0 : round6(f.cssViewport.y),
|
|
227640
227694
|
"css-to-image-x": scale?.x.toFixed(6),
|
|
227641
227695
|
"css-to-image-y": scale?.y.toFixed(6),
|
|
227642
|
-
emulation: f.emulation
|
|
227696
|
+
emulation: f.emulation,
|
|
227697
|
+
"color-scheme": f.colorScheme
|
|
227643
227698
|
},
|
|
227644
227699
|
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
227700
|
sections
|
|
@@ -227655,7 +227710,7 @@ async function cmdPageShot(parsed, _args) {
|
|
|
227655
227710
|
{
|
|
227656
227711
|
tag: "error",
|
|
227657
227712
|
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>]\`.`
|
|
227713
|
+
summary: fact`received: ${parsed.positional.length} positional arguments; expected none — \`capture page shot [--viewport <WxH>] [--full-page] [--color-scheme <dark|light>] [--out <path>]\`.`
|
|
227659
227714
|
},
|
|
227660
227715
|
{ json: parsed.json }
|
|
227661
227716
|
);
|
|
@@ -227679,18 +227734,33 @@ async function cmdPageShot(parsed, _args) {
|
|
|
227679
227734
|
return;
|
|
227680
227735
|
}
|
|
227681
227736
|
}
|
|
227737
|
+
let colorScheme;
|
|
227738
|
+
try {
|
|
227739
|
+
colorScheme = parseColorScheme(parsed.colorScheme);
|
|
227740
|
+
} catch {
|
|
227741
|
+
emitResult(
|
|
227742
|
+
{
|
|
227743
|
+
tag: "error",
|
|
227744
|
+
attrs: { command: "page shot", code: "invalid_color_scheme" },
|
|
227745
|
+
summary: fact`received: --color-scheme ${parsed.colorScheme}; expected dark or light.`
|
|
227746
|
+
},
|
|
227747
|
+
{ json: parsed.json }
|
|
227748
|
+
);
|
|
227749
|
+
process.exitCode = 1;
|
|
227750
|
+
return;
|
|
227751
|
+
}
|
|
227682
227752
|
const emulation = parsed.fullPage ? "full-page" : viewport2 ? "viewport" : "none";
|
|
227683
227753
|
const result = await deps4.withConnection(
|
|
227684
227754
|
{ ...parsed, command: "shot" },
|
|
227685
|
-
async (client) => {
|
|
227755
|
+
async (client) => withAppliedColorScheme(client, colorScheme, async () => {
|
|
227686
227756
|
const screenshot = await captureScreenshotWithCssViewport(client, viewport2, { fullPage: parsed.fullPage });
|
|
227687
227757
|
const outPath = await resolveOutPath(parsed);
|
|
227688
227758
|
writeScreenshot(outPath, screenshot.png);
|
|
227689
227759
|
return { path: outPath, bytes: screenshot.png.length, dimensions: pngDimensions(screenshot.png), cssViewport: screenshot.cssViewport };
|
|
227690
|
-
},
|
|
227760
|
+
}),
|
|
227691
227761
|
{ settle: 0 }
|
|
227692
227762
|
);
|
|
227693
|
-
emitResult(buildScreenshotResult({ ...result, emulation, viewport: viewport2 }), { json: parsed.json });
|
|
227763
|
+
emitResult(buildScreenshotResult({ ...result, emulation, viewport: viewport2, colorScheme }), { json: parsed.json });
|
|
227694
227764
|
}
|
|
227695
227765
|
var fs15, path17, USAGE6, deps4, PNG_SIGNATURE;
|
|
227696
227766
|
var init_shot = __esm({
|
|
@@ -227700,6 +227770,7 @@ var init_shot = __esm({
|
|
|
227700
227770
|
init_connection();
|
|
227701
227771
|
init_screenshot();
|
|
227702
227772
|
init_viewport();
|
|
227773
|
+
init_color_scheme();
|
|
227703
227774
|
init_session_context();
|
|
227704
227775
|
init_commands();
|
|
227705
227776
|
init_artifacts();
|
|
@@ -227709,13 +227780,14 @@ var init_shot = __esm({
|
|
|
227709
227780
|
input:
|
|
227710
227781
|
--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
227782
|
--full-page transient override to the full scrollable content height for this capture
|
|
227783
|
+
--color-scheme <dark|light> transient prefers-color-scheme override for this capture; cleared afterward
|
|
227712
227784
|
--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
227785
|
--target <tabId> | --url <pattern> | --port <n> tab targeting; defaults to the active session tab
|
|
227714
227786
|
--json mirror the result as JSON
|
|
227715
227787
|
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
|
|
227788
|
+
<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
227789
|
effects:
|
|
227718
|
-
no flags: none \u2014 the capture reads the viewport as-is, with zero Emulation.* calls.
|
|
227790
|
+
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
227791
|
deps4 = { withConnection, nextStepPath, createOneshotSession };
|
|
227720
227792
|
PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
227721
227793
|
}
|
|
@@ -227856,7 +227928,7 @@ During a live composed recording every verb routes through the recorder.
|
|
|
227856
227928
|
<subcommand name="scroll" args="<target> --to <top|bottom|px> [--no-screenshot]" whenToUse="scroll one resolved container to a position"/>
|
|
227857
227929
|
<subcommand name="navigate" args="<url> [--settle <ms>]" whenToUse="navigate the tab and wait for load + settle"/>
|
|
227858
227930
|
<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"/>
|
|
227931
|
+
<subcommand name="shot" args="[--viewport <WxH>] [--full-page] [--color-scheme <dark|light>] [--out <path>]" whenToUse="look at the page right now without acting"/>
|
|
227860
227932
|
<subcommand name="elements" args="[--all] [--limit <n>]" whenToUse="list what can be acted on \u2014 role, name, backend:<id> per element"/>
|
|
227861
227933
|
|
|
227862
227934
|
capture page <leaf> -h Per-leaf usage`;
|
|
@@ -235901,6 +235973,7 @@ async function captureSnapshotSubstrate(options) {
|
|
|
235901
235973
|
const pixels = options.pixels ?? false;
|
|
235902
235974
|
const state = options.state ?? [];
|
|
235903
235975
|
const viewport2 = options.viewport ?? null;
|
|
235976
|
+
const colorScheme = options.colorScheme;
|
|
235904
235977
|
const url = options.url ? sanitizeString(options.url) : null;
|
|
235905
235978
|
const artifacts = [];
|
|
235906
235979
|
const phaseClient = (phase) => boundedClient(client, phase, dir);
|
|
@@ -236043,6 +236116,7 @@ async function captureSnapshotSubstrate(options) {
|
|
|
236043
236116
|
captureUnsettled,
|
|
236044
236117
|
pixels,
|
|
236045
236118
|
states: state.map((spec) => sanitizeString(spec)),
|
|
236119
|
+
...colorScheme ? { colorScheme } : {},
|
|
236046
236120
|
unstableRegionCount: unstableRegions.length,
|
|
236047
236121
|
...freezeAnimations ? { animationsRestored: animationsRestored ?? false } : {},
|
|
236048
236122
|
// #8: the browser-wide `Animation.setPlaybackRate({playbackRate:0})`
|
|
@@ -236568,7 +236642,7 @@ function settledNote(settled, captured, settleMs, settleTimeoutMs, unstableCount
|
|
|
236568
236642
|
}
|
|
236569
236643
|
return fact`Page did not settle within ${settleTimeoutMs}ms. No queryable substrate was written; evidence artifacts only (${unstableCount} unstable region(s)).`;
|
|
236570
236644
|
}
|
|
236571
|
-
async function captureMeasureSnap(parsed, targetRef = parsed.positional[0], viewportValue = parsed.viewport) {
|
|
236645
|
+
async function captureMeasureSnap(parsed, targetRef = parsed.positional[0], viewportValue = parsed.viewport, colorSchemeValue = parsed.colorScheme) {
|
|
236572
236646
|
if (parsed.positional.length > 1) {
|
|
236573
236647
|
throw invalidInput("measure snap accepts at most one positional URL or snapshot reference");
|
|
236574
236648
|
}
|
|
@@ -236581,6 +236655,7 @@ async function captureMeasureSnap(parsed, targetRef = parsed.positional[0], view
|
|
|
236581
236655
|
}
|
|
236582
236656
|
}
|
|
236583
236657
|
const viewport2 = viewportValue === void 0 ? void 0 : { label: viewportValue, ...parseViewport(viewportValue) };
|
|
236658
|
+
const colorScheme = parseColorScheme(colorSchemeValue);
|
|
236584
236659
|
let base;
|
|
236585
236660
|
let baseUrl = null;
|
|
236586
236661
|
if (targetRef && !isUrlRef(targetRef)) {
|
|
@@ -236595,7 +236670,7 @@ async function captureMeasureSnap(parsed, targetRef = parsed.positional[0], view
|
|
|
236595
236670
|
try {
|
|
236596
236671
|
let artifacts = [];
|
|
236597
236672
|
await withConnection(connectionArgs, async (client, tab) => {
|
|
236598
|
-
await withAppliedViewport(client, viewport2, async () => {
|
|
236673
|
+
await withAppliedColorScheme(client, colorScheme, async () => withAppliedViewport(client, viewport2, async () => {
|
|
236599
236674
|
const oneshot = active ? void 0 : createOneshotSession("measure");
|
|
236600
236675
|
const destination = active ? path22.join(active.dir, "measure", "snaps") : oneshot.artifactsDir;
|
|
236601
236676
|
snapDir = path22.join(destination, snapId);
|
|
@@ -236611,10 +236686,11 @@ async function captureMeasureSnap(parsed, targetRef = parsed.positional[0], view
|
|
|
236611
236686
|
pixels: parsed.pixels,
|
|
236612
236687
|
state: parsed.state ?? [],
|
|
236613
236688
|
viewport: viewport2?.label ?? null,
|
|
236689
|
+
colorScheme,
|
|
236614
236690
|
skipCollectors: parsed.skipCollector ?? []
|
|
236615
236691
|
});
|
|
236616
236692
|
artifacts = result.artifacts;
|
|
236617
|
-
});
|
|
236693
|
+
}));
|
|
236618
236694
|
}, { settle: 0 });
|
|
236619
236695
|
if (!snapDir) throw new Error("measure snap did not allocate an artifact directory");
|
|
236620
236696
|
return { id: snapId, dir: snapDir, artifacts, ...base ? { base } : {} };
|
|
@@ -236640,6 +236716,7 @@ function buildSnapshotResult(captured) {
|
|
|
236640
236716
|
attrs: {
|
|
236641
236717
|
url: meta.url ?? void 0,
|
|
236642
236718
|
viewport: meta.viewport ?? void 0,
|
|
236719
|
+
"color-scheme": meta.colorScheme,
|
|
236643
236720
|
elements,
|
|
236644
236721
|
settled: meta.settled,
|
|
236645
236722
|
"settle-ms": meta.settleMs,
|
|
@@ -236670,7 +236747,7 @@ function classifySnapError(err) {
|
|
|
236670
236747
|
if (message.startsWith("--viewport cannot ")) {
|
|
236671
236748
|
return { status: "viewport_unavailable", message };
|
|
236672
236749
|
}
|
|
236673
|
-
if (message.startsWith("measure snap accepts") || message.startsWith("--settle-timeout") || message.startsWith("--viewport")) {
|
|
236750
|
+
if (message.startsWith("measure snap accepts") || message.startsWith("--settle-timeout") || message.startsWith("--viewport") || message.startsWith("--color-scheme")) {
|
|
236674
236751
|
return { status: "invalid_input", message };
|
|
236675
236752
|
}
|
|
236676
236753
|
if (message.includes("No tab found") || message.includes("Use --target") || message.includes("Tab has no WebSocket debugger URL")) {
|
|
@@ -236715,9 +236792,10 @@ async function cmdMeasureSnap(parsed, _args) {
|
|
|
236715
236792
|
if (value === void 0) return void 0;
|
|
236716
236793
|
return { label: value, ...parseViewport(value) };
|
|
236717
236794
|
});
|
|
236795
|
+
const colorScheme = parseColorScheme(parsed.colorScheme);
|
|
236718
236796
|
const results = [];
|
|
236719
236797
|
for (const viewport2 of viewports) {
|
|
236720
|
-
const capture = await captureMeasureSnap(parsed, parsed.positional[0], viewport2?.label);
|
|
236798
|
+
const capture = await captureMeasureSnap(parsed, parsed.positional[0], viewport2?.label, colorScheme);
|
|
236721
236799
|
captures.push(capture);
|
|
236722
236800
|
results.push(buildSnapshotResult(capture));
|
|
236723
236801
|
}
|
|
@@ -236767,6 +236845,7 @@ var init_snap = __esm({
|
|
|
236767
236845
|
init_connection();
|
|
236768
236846
|
init_recorder_client();
|
|
236769
236847
|
init_viewport();
|
|
236848
|
+
init_color_scheme();
|
|
236770
236849
|
init_snapshot();
|
|
236771
236850
|
init_redaction();
|
|
236772
236851
|
init_settle();
|
|
@@ -236787,8 +236866,9 @@ input:
|
|
|
236787
236866
|
--pixels also write per-element raster crops
|
|
236788
236867
|
--state <state[:selector]> force a pseudo-state or real control state (repeatable)
|
|
236789
236868
|
--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
|
|
236869
|
+
--color-scheme <dark|light> temporarily capture under this prefers-color-scheme value; cleared afterward
|
|
236790
236870
|
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
|
|
236871
|
+
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
236872
|
}
|
|
236793
236873
|
});
|
|
236794
236874
|
|
|
@@ -240154,7 +240234,7 @@ Every leaf defaults to rendered prose; --json mirrors the same result.
|
|
|
240154
240234
|
Findings exit 0 \u2014 a report, not a failure.
|
|
240155
240235
|
\`--gate\` (exit 2 on findings/changes) is accepted only by check and diff.
|
|
240156
240236
|
|
|
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"/>
|
|
240237
|
+
<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
240238
|
<subcommand name="check" args="[url|snap] [--for <checks>] [--gate]" whenToUse="read threshold/fact measurements from one snapshot"/>
|
|
240159
240239
|
<subcommand name="diff" args="--before <snap> --after <snap> [--pixels] [--full] [--gate]" whenToUse="structured before/after delta between two snapshots"/>
|
|
240160
240240
|
<subcommand name="census" args="[--snap <id>]... [--url <url>]... [--set-file <path>] --axis <axis>" whenToUse="value distributions across one or more snapshots"/>
|
|
@@ -242098,7 +242178,7 @@ CDP_PORT / CDP_TARGET pin the browser + tab for orchestrators \u2014 precedence:
|
|
|
242098
242178
|
explicit flag > active session > env.`;
|
|
242099
242179
|
}
|
|
242100
242180
|
function printVersion() {
|
|
242101
|
-
const declared = "1.3.
|
|
242181
|
+
const declared = "1.3.20";
|
|
242102
242182
|
if (declared) {
|
|
242103
242183
|
console.log(declared);
|
|
242104
242184
|
return;
|
package/package.json
CHANGED
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++; }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CDPClient } from './client.js';
|
|
2
|
+
|
|
3
|
+
export type ColorScheme = 'dark' | 'light';
|
|
4
|
+
|
|
5
|
+
type MediaClient = Pick<CDPClient, 'send'>;
|
|
6
|
+
|
|
7
|
+
export function parseColorScheme(value: string | undefined): ColorScheme | undefined {
|
|
8
|
+
if (value === undefined) return undefined;
|
|
9
|
+
if (value === 'dark' || value === 'light') return value;
|
|
10
|
+
throw new Error('--color-scheme must be dark or light');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Applies one media-feature override only for the supplied capture operation. */
|
|
14
|
+
export async function withAppliedColorScheme<T>(client: MediaClient, colorScheme: ColorScheme | undefined, fn: () => Promise<T>): Promise<T> {
|
|
15
|
+
if (!colorScheme) return fn();
|
|
16
|
+
let ownsMediaOverride = false;
|
|
17
|
+
let primaryFailed = false;
|
|
18
|
+
let primaryError: unknown;
|
|
19
|
+
try {
|
|
20
|
+
// A rejected response does not prove Chrome rejected the request. Claim
|
|
21
|
+
// cleanup responsibility before awaiting it, just as viewport capture does.
|
|
22
|
+
ownsMediaOverride = true;
|
|
23
|
+
await client.send('Emulation.setEmulatedMedia', {
|
|
24
|
+
features: [{ name: 'prefers-color-scheme', value: colorScheme }],
|
|
25
|
+
});
|
|
26
|
+
return await fn();
|
|
27
|
+
} catch (error) {
|
|
28
|
+
primaryFailed = true;
|
|
29
|
+
primaryError = error;
|
|
30
|
+
throw error;
|
|
31
|
+
} finally {
|
|
32
|
+
if (ownsMediaOverride) {
|
|
33
|
+
try {
|
|
34
|
+
await client.send('Emulation.setEmulatedMedia', { features: [] });
|
|
35
|
+
} catch (cleanupError) {
|
|
36
|
+
if (primaryFailed) {
|
|
37
|
+
throw new AggregateError(
|
|
38
|
+
[primaryError, cleanupError],
|
|
39
|
+
'Color-scheme capture failed and media-emulation cleanup also failed.',
|
|
40
|
+
{ cause: primaryError },
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
throw cleanupError;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -36,7 +36,7 @@ Every leaf defaults to rendered prose; --json mirrors the same result.
|
|
|
36
36
|
Findings exit 0 — a report, not a failure.
|
|
37
37
|
\`--gate\` (exit 2 on findings/changes) is accepted only by check and diff.
|
|
38
38
|
|
|
39
|
-
<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"/>
|
|
39
|
+
<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"/>
|
|
40
40
|
<subcommand name="check" args="[url|snap] [--for <checks>] [--gate]" whenToUse="read threshold/fact measurements from one snapshot"/>
|
|
41
41
|
<subcommand name="diff" args="--before <snap> --after <snap> [--pixels] [--full] [--gate]" whenToUse="structured before/after delta between two snapshots"/>
|
|
42
42
|
<subcommand name="census" args="[--snap <id>]... [--url <url>]... [--set-file <path>] --axis <axis>" whenToUse="value distributions across one or more snapshots"/>
|
|
@@ -6,6 +6,7 @@ import { type ParsedArgs } from '../../types.js';
|
|
|
6
6
|
import { withConnection } from '../../connection.js';
|
|
7
7
|
import { isRecorderHeldClient } from '../../recorder-client.js';
|
|
8
8
|
import { parseViewport, type Viewport } from '../../viewport.js';
|
|
9
|
+
import { parseColorScheme, withAppliedColorScheme, type ColorScheme } from '../../color-scheme.js';
|
|
9
10
|
import { captureSnapshotSubstrate, COLLECTOR_NAMES, SnapshotCaptureTimeout } from '../../measure/snapshot.js';
|
|
10
11
|
import type { CollectorOutcome } from '../../measure/types.js';
|
|
11
12
|
import { sanitizeString } from '../../measure/redaction.js';
|
|
@@ -36,8 +37,9 @@ input:
|
|
|
36
37
|
--pixels also write per-element raster crops
|
|
37
38
|
--state <state[:selector]> force a pseudo-state or real control state (repeatable)
|
|
38
39
|
--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
|
|
40
|
+
--color-scheme <dark|light> temporarily capture under this prefers-color-scheme value; cleared afterward
|
|
39
41
|
output: <snapshot id=… path=…> — 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
|
|
40
|
-
effects: drives the target page to capture; writes one snapshot artifact directory under the active (or a private one-shot) session
|
|
42
|
+
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.`;
|
|
41
43
|
|
|
42
44
|
interface SnapshotMetaForBase {
|
|
43
45
|
readonly url?: string | null;
|
|
@@ -216,7 +218,7 @@ function settledNote(settled: boolean, captured: boolean, settleMs: number, sett
|
|
|
216
218
|
* seam rather than importing the substrate directly, preserving snap as the
|
|
217
219
|
* sole live-driving primitive.
|
|
218
220
|
*/
|
|
219
|
-
export async function captureMeasureSnap(parsed: ParsedArgs, targetRef = parsed.positional[0], viewportValue = parsed.viewport): Promise<MeasureSnapCapture> {
|
|
221
|
+
export async function captureMeasureSnap(parsed: ParsedArgs, targetRef = parsed.positional[0], viewportValue = parsed.viewport, colorSchemeValue = parsed.colorScheme): Promise<MeasureSnapCapture> {
|
|
220
222
|
if (parsed.positional.length > 1) {
|
|
221
223
|
throw invalidInput('measure snap accepts at most one positional URL or snapshot reference');
|
|
222
224
|
}
|
|
@@ -231,6 +233,7 @@ export async function captureMeasureSnap(parsed: ParsedArgs, targetRef = parsed.
|
|
|
231
233
|
const viewport: ViewportSpec | undefined = viewportValue === undefined
|
|
232
234
|
? undefined
|
|
233
235
|
: { label: viewportValue, ...parseViewport(viewportValue) };
|
|
236
|
+
const colorScheme = parseColorScheme(colorSchemeValue);
|
|
234
237
|
|
|
235
238
|
let base: SnapRef | undefined;
|
|
236
239
|
let baseUrl: string | null = null;
|
|
@@ -256,7 +259,7 @@ export async function captureMeasureSnap(parsed: ParsedArgs, targetRef = parsed.
|
|
|
256
259
|
try {
|
|
257
260
|
let artifacts: readonly string[] = [];
|
|
258
261
|
await withConnection(connectionArgs, async (client, tab) => {
|
|
259
|
-
await withAppliedViewport(client, viewport, async () => {
|
|
262
|
+
await withAppliedColorScheme(client, colorScheme, async () => withAppliedViewport(client, viewport, async () => {
|
|
260
263
|
const oneshot = active ? undefined : createOneshotSession('measure');
|
|
261
264
|
const destination = active ? path.join(active.dir, 'measure', 'snaps') : oneshot!.artifactsDir;
|
|
262
265
|
snapDir = path.join(destination, snapId);
|
|
@@ -272,10 +275,11 @@ export async function captureMeasureSnap(parsed: ParsedArgs, targetRef = parsed.
|
|
|
272
275
|
pixels: parsed.pixels,
|
|
273
276
|
state: parsed.state ?? [],
|
|
274
277
|
viewport: viewport?.label ?? null,
|
|
278
|
+
colorScheme,
|
|
275
279
|
skipCollectors: parsed.skipCollector ?? [],
|
|
276
280
|
});
|
|
277
281
|
artifacts = result.artifacts;
|
|
278
|
-
});
|
|
282
|
+
}));
|
|
279
283
|
}, { settle: 0 });
|
|
280
284
|
|
|
281
285
|
if (!snapDir) throw new Error('measure snap did not allocate an artifact directory');
|
|
@@ -290,6 +294,7 @@ function buildSnapshotResult(captured: MeasureSnapCapture): RenderableResult {
|
|
|
290
294
|
const meta = readMeta<{
|
|
291
295
|
url: string | null;
|
|
292
296
|
viewport: string | null;
|
|
297
|
+
colorScheme?: ColorScheme;
|
|
293
298
|
settled: boolean;
|
|
294
299
|
settleMs: number;
|
|
295
300
|
settleTimeoutMs: number;
|
|
@@ -313,6 +318,7 @@ function buildSnapshotResult(captured: MeasureSnapCapture): RenderableResult {
|
|
|
313
318
|
attrs: {
|
|
314
319
|
url: meta.url ?? undefined,
|
|
315
320
|
viewport: meta.viewport ?? undefined,
|
|
321
|
+
'color-scheme': meta.colorScheme,
|
|
316
322
|
elements,
|
|
317
323
|
settled: meta.settled,
|
|
318
324
|
'settle-ms': meta.settleMs,
|
|
@@ -358,7 +364,7 @@ export function classifySnapError(err: unknown): SnapErrorDetails {
|
|
|
358
364
|
if (message.startsWith('--viewport cannot ')) {
|
|
359
365
|
return { status: 'viewport_unavailable', message };
|
|
360
366
|
}
|
|
361
|
-
if (message.startsWith('measure snap accepts') || message.startsWith('--settle-timeout') || message.startsWith('--viewport')) {
|
|
367
|
+
if (message.startsWith('measure snap accepts') || message.startsWith('--settle-timeout') || message.startsWith('--viewport') || message.startsWith('--color-scheme')) {
|
|
362
368
|
return { status: 'invalid_input', message };
|
|
363
369
|
}
|
|
364
370
|
if (message.includes('No tab found') || message.includes('Use --target') || message.includes('Tab has no WebSocket debugger URL')) {
|
|
@@ -408,9 +414,10 @@ export async function cmdMeasureSnap(parsed: ParsedArgs, _args: string[]): Promi
|
|
|
408
414
|
if (value === undefined) return undefined;
|
|
409
415
|
return { label: value, ...parseViewport(value) };
|
|
410
416
|
});
|
|
417
|
+
const colorScheme = parseColorScheme(parsed.colorScheme);
|
|
411
418
|
const results = [] as RenderableResult[];
|
|
412
419
|
for (const viewport of viewports) {
|
|
413
|
-
const capture = await captureMeasureSnap(parsed, parsed.positional[0], viewport?.label);
|
|
420
|
+
const capture = await captureMeasureSnap(parsed, parsed.positional[0], viewport?.label, colorScheme);
|
|
414
421
|
captures.push(capture);
|
|
415
422
|
results.push(buildSnapshotResult(capture));
|
|
416
423
|
}
|
|
@@ -39,7 +39,7 @@ During a live composed recording every verb routes through the recorder.
|
|
|
39
39
|
<subcommand name="scroll" args="<target> --to <top|bottom|px> [--no-screenshot]" whenToUse="scroll one resolved container to a position"/>
|
|
40
40
|
<subcommand name="navigate" args="<url> [--settle <ms>]" whenToUse="navigate the tab and wait for load + settle"/>
|
|
41
41
|
<subcommand name="exec" args="<code> | --file <path>" whenToUse="run arbitrary JS in the tab (expressions, return, await)"/>
|
|
42
|
-
<subcommand name="shot" args="[--viewport <WxH>] [--full-page] [--out <path>]" whenToUse="look at the page right now without acting"/>
|
|
42
|
+
<subcommand name="shot" args="[--viewport <WxH>] [--full-page] [--color-scheme <dark|light>] [--out <path>]" whenToUse="look at the page right now without acting"/>
|
|
43
43
|
<subcommand name="elements" args="[--all] [--limit <n>]" whenToUse="list what can be acted on — role, name, backend:<id> per element"/>
|
|
44
44
|
|
|
45
45
|
capture page <leaf> -h Per-leaf usage`;
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* The navigational look: vision as navigator, never inspector — cheap, no
|
|
5
5
|
* settling, no collectors. With no flags it captures the browser's ACTUAL
|
|
6
6
|
* current viewport and performs zero `Emulation.*` calls; emulation exists
|
|
7
|
-
* only when explicitly asked (`--viewport <WxH>` / `--full-page`
|
|
8
|
-
*
|
|
7
|
+
* only when explicitly asked (`--viewport <WxH>` / `--full-page` /
|
|
8
|
+
* `--color-scheme dark|light`), applied transiently; viewport emulation uses
|
|
9
|
+
* `Emulation.setDeviceMetricsOverride` (+ ~150ms re-layout)
|
|
9
10
|
* and cleared after — two page-observable resizes, declared as the leaf's
|
|
10
11
|
* effect in `-h` (I-1/I-2 posture). Explicit viewport emulation uses the
|
|
11
12
|
* single `WxH` grammar (D10).
|
|
@@ -20,6 +21,7 @@ import { type ParsedArgs } from '../../types.js';
|
|
|
20
21
|
import { withConnection } from '../../connection.js';
|
|
21
22
|
import { captureScreenshotWithCssViewport, type CapturedRegion } from '../../screenshot.js';
|
|
22
23
|
import { parseViewport, type Viewport } from '../../viewport.js';
|
|
24
|
+
import { parseColorScheme, withAppliedColorScheme, type ColorScheme } from '../../color-scheme.js';
|
|
23
25
|
import { nextStepPath } from '../../../session-context.js';
|
|
24
26
|
import { createOneshotSession } from '../../../session/commands.js';
|
|
25
27
|
import { assertUnderCaptureRoot, writeBinaryPrivate } from '../../../session/artifacts.js';
|
|
@@ -36,13 +38,14 @@ const USAGE = `capture page shot — capture a PNG of the tab as it is right now
|
|
|
36
38
|
input:
|
|
37
39
|
--viewport <WxH> transient device-metrics override for this capture; grammar: <positive-safe-int>x<positive-safe-int>, exact lowercase x with no whitespace — preset names are not accepted. Absent → no emulation, the browser's actual current viewport is captured
|
|
38
40
|
--full-page transient override to the full scrollable content height for this capture
|
|
41
|
+
--color-scheme <dark|light> transient prefers-color-scheme override for this capture; cleared afterward
|
|
39
42
|
--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
|
|
40
43
|
--target <tabId> | --url <pattern> | --port <n> tab targeting; defaults to the active session tab
|
|
41
44
|
--json mirror the result as JSON
|
|
42
45
|
output:
|
|
43
|
-
<screenshot path=… width=… height=… css-width=… css-height=… css-x=… css-y=… css-to-image-x=… css-to-image-y=… emulation=none|viewport|full-page> — 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
|
|
46
|
+
<screenshot path=… width=… height=… css-width=… css-height=… css-x=… css-y=… css-to-image-x=… css-to-image-y=… emulation=none|viewport|full-page color-scheme=dark|light> — 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
|
|
44
47
|
effects:
|
|
45
|
-
no flags: none — the capture reads the viewport as-is, with zero Emulation.* calls.
|
|
48
|
+
no flags: none — 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 — two page-observable resizes. --color-scheme applies a transient Emulation.setEmulatedMedia prefers-color-scheme override and clears it after the capture.`;
|
|
46
49
|
|
|
47
50
|
// ---------------------------------------------------------------------------
|
|
48
51
|
// Test-injectable dependency seam (the CDP-stub test pattern; the capture
|
|
@@ -128,6 +131,7 @@ export function buildScreenshotResult(f: {
|
|
|
128
131
|
cssViewport?: CapturedRegion;
|
|
129
132
|
emulation: EmulationMode;
|
|
130
133
|
viewport?: { width: number; height: number };
|
|
134
|
+
colorScheme?: ColorScheme;
|
|
131
135
|
}): RenderableResult {
|
|
132
136
|
// The denominator is the CSS region the image was actually cut from, so the
|
|
133
137
|
// ratio stays true under page zoom, device pixel ratio, and the local
|
|
@@ -140,12 +144,15 @@ export function buildScreenshotResult(f: {
|
|
|
140
144
|
? 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)}).`
|
|
141
145
|
: text`CSS-to-image scale unavailable: the browser did not return the CSS region this capture covers.`,
|
|
142
146
|
f.emulation === 'none'
|
|
143
|
-
? text`emulation: none — the browser's actual current viewport was captured
|
|
147
|
+
? text`viewport emulation: none — the browser's actual current viewport was captured.`
|
|
144
148
|
: f.emulation === 'viewport'
|
|
145
149
|
? fact`emulation: transient ${f.viewport!.width}x${f.viewport!.height} device-metrics override applied for the capture and cleared after — two page-observable resizes.`
|
|
146
150
|
: f.viewport
|
|
147
151
|
? 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.`
|
|
148
152
|
: text`emulation: transient full-page device-metrics override applied for the capture and cleared after — two page-observable resizes.`,
|
|
153
|
+
f.colorScheme
|
|
154
|
+
? fact`color scheme: transient prefers-color-scheme=${f.colorScheme} override applied for the capture and cleared after.`
|
|
155
|
+
: text`color scheme: browser default — no Emulation.setEmulatedMedia call was made.`,
|
|
149
156
|
];
|
|
150
157
|
return {
|
|
151
158
|
tag: 'screenshot',
|
|
@@ -160,6 +167,7 @@ export function buildScreenshotResult(f: {
|
|
|
160
167
|
'css-to-image-x': scale?.x.toFixed(6),
|
|
161
168
|
'css-to-image-y': scale?.y.toFixed(6),
|
|
162
169
|
emulation: f.emulation,
|
|
170
|
+
'color-scheme': f.colorScheme,
|
|
163
171
|
},
|
|
164
172
|
summary: f.dimensions
|
|
165
173
|
? fact`saved ${f.path} — ${f.dimensions.width}x${f.dimensions.height}px, ${f.bytes} bytes.`
|
|
@@ -183,7 +191,7 @@ export async function cmdPageShot(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
183
191
|
{
|
|
184
192
|
tag: 'error',
|
|
185
193
|
attrs: { command: 'page shot', code: 'invalid_input' },
|
|
186
|
-
summary: fact`received: ${parsed.positional.length} positional arguments; expected none — \`capture page shot [--viewport <WxH>] [--full-page] [--out <path>]\`.`,
|
|
194
|
+
summary: fact`received: ${parsed.positional.length} positional arguments; expected none — \`capture page shot [--viewport <WxH>] [--full-page] [--color-scheme <dark|light>] [--out <path>]\`.`,
|
|
187
195
|
},
|
|
188
196
|
{ json: parsed.json },
|
|
189
197
|
);
|
|
@@ -209,6 +217,22 @@ export async function cmdPageShot(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
209
217
|
}
|
|
210
218
|
}
|
|
211
219
|
|
|
220
|
+
let colorScheme: ColorScheme | undefined;
|
|
221
|
+
try {
|
|
222
|
+
colorScheme = parseColorScheme(parsed.colorScheme);
|
|
223
|
+
} catch {
|
|
224
|
+
emitResult(
|
|
225
|
+
{
|
|
226
|
+
tag: 'error',
|
|
227
|
+
attrs: { command: 'page shot', code: 'invalid_color_scheme' },
|
|
228
|
+
summary: fact`received: --color-scheme ${parsed.colorScheme}; expected dark or light.`,
|
|
229
|
+
},
|
|
230
|
+
{ json: parsed.json },
|
|
231
|
+
);
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
212
236
|
const emulation: EmulationMode = parsed.fullPage ? 'full-page' : viewport ? 'viewport' : 'none';
|
|
213
237
|
|
|
214
238
|
// connection.ts derives the recorder-routed action label from
|
|
@@ -216,14 +240,14 @@ export async function cmdPageShot(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
216
240
|
// restore the verb so stderr diagnostics identify this leaf.
|
|
217
241
|
const result = await deps.withConnection(
|
|
218
242
|
{ ...parsed, command: 'shot' },
|
|
219
|
-
async (client) => {
|
|
243
|
+
async (client) => withAppliedColorScheme(client, colorScheme, async () => {
|
|
220
244
|
const screenshot = await captureScreenshotWithCssViewport(client, viewport, { fullPage: parsed.fullPage });
|
|
221
245
|
const outPath = await resolveOutPath(parsed);
|
|
222
246
|
writeScreenshot(outPath, screenshot.png);
|
|
223
247
|
return { path: outPath, bytes: screenshot.png.length, dimensions: pngDimensions(screenshot.png), cssViewport: screenshot.cssViewport };
|
|
224
|
-
},
|
|
248
|
+
}),
|
|
225
249
|
{ settle: 0 },
|
|
226
250
|
);
|
|
227
251
|
|
|
228
|
-
emitResult(buildScreenshotResult({ ...result, emulation, viewport }), { json: parsed.json });
|
|
252
|
+
emitResult(buildScreenshotResult({ ...result, emulation, viewport, colorScheme }), { json: parsed.json });
|
|
229
253
|
}
|
|
@@ -250,6 +250,7 @@ export async function captureSnapshotSubstrate(options: CaptureSnapshotOptions):
|
|
|
250
250
|
const pixels = options.pixels ?? false;
|
|
251
251
|
const state = options.state ?? [];
|
|
252
252
|
const viewport = options.viewport ?? null;
|
|
253
|
+
const colorScheme = options.colorScheme;
|
|
253
254
|
// Apply the shared artifact-string cap once at the source. URL contents
|
|
254
255
|
// otherwise survive unchanged in meta.json, the result, and collector context.
|
|
255
256
|
const url = options.url ? sanitizeString(options.url) : null;
|
|
@@ -483,6 +484,7 @@ export async function captureSnapshotSubstrate(options: CaptureSnapshotOptions):
|
|
|
483
484
|
captureUnsettled,
|
|
484
485
|
pixels,
|
|
485
486
|
states: state.map((spec) => sanitizeString(spec)),
|
|
487
|
+
...(colorScheme ? { colorScheme } : {}),
|
|
486
488
|
unstableRegionCount: unstableRegions.length,
|
|
487
489
|
...(freezeAnimations ? { animationsRestored: animationsRestored ?? false } : {}),
|
|
488
490
|
// #8: the browser-wide `Animation.setPlaybackRate({playbackRate:0})`
|
package/src/cdp/measure/types.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface CaptureSnapshotOptions {
|
|
|
53
53
|
/** `--state <state[:selector]>` (repeatable); default `[]`. */
|
|
54
54
|
readonly state?: readonly string[];
|
|
55
55
|
readonly viewport?: string | null;
|
|
56
|
+
/** Transient `prefers-color-scheme` media override used while capturing. */
|
|
57
|
+
readonly colorScheme?: 'dark' | 'light';
|
|
56
58
|
/**
|
|
57
59
|
* Poll interval for the settledness loop; default {@link DEFAULT_POLL_INTERVAL_MS}
|
|
58
60
|
* (100ms). Not part of the CLI surface — a test-only override so the
|
|
@@ -109,6 +111,8 @@ export interface SnapshotMeta extends SnapMeta {
|
|
|
109
111
|
readonly captureUnsettled: boolean;
|
|
110
112
|
readonly pixels: boolean;
|
|
111
113
|
readonly states: readonly string[];
|
|
114
|
+
/** Present only when this snapshot applied a transient color-scheme override. */
|
|
115
|
+
readonly colorScheme?: 'dark' | 'light';
|
|
112
116
|
readonly unstableRegionCount: number;
|
|
113
117
|
/** Present only when `freezeAnimations` is true — whether every animation `freezeAnimationsBeforeCapture` paused was confirmed resumed to its pre-freeze `playState` (I-6). `false` means restoration could not be guaranteed (origin capture failed, or the restore call itself threw) — never omitted to imply a clean restore. */
|
|
114
118
|
readonly animationsRestored?: boolean;
|
package/src/cdp/types.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface ParsedArgs {
|
|
|
38
38
|
into?: string;
|
|
39
39
|
noScreenshot?: boolean;
|
|
40
40
|
viewport?: string;
|
|
41
|
+
/** `page shot|measure snap --color-scheme dark|light` — transient media override. */
|
|
42
|
+
colorScheme?: string;
|
|
41
43
|
fullPage?: boolean;
|
|
42
44
|
help?: boolean;
|
|
43
45
|
/** `--all` — boolean scope-widener (per-leaf semantics). */
|