@crouton-kit/capture 1.3.20 → 1.4.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/bin/capture +2890 -1756
- package/package.json +1 -1
- package/src/capture.ts +3 -2
- package/src/cdp/args.ts +18 -3
- package/src/cdp/commands/measure/explain.ts +22 -7
- package/src/cdp/commands/measure/index.ts +8 -4
- package/src/cdp/commands/measure/map-paint.ts +273 -0
- package/src/cdp/commands/measure/snap.ts +34 -7
- package/src/cdp/commands/page/click.ts +32 -4
- package/src/cdp/commands/page/exec.ts +1 -1
- package/src/cdp/commands/page/index.ts +2 -2
- package/src/cdp/commands/page/scroll.ts +6 -4
- package/src/cdp/commands/page/shot.ts +118 -6
- package/src/cdp/commands/page/type.ts +6 -4
- package/src/cdp/connection.ts +19 -12
- package/src/cdp/measure/collectors/geometry.ts +66 -9
- package/src/cdp/measure/collectors/pixels.ts +9 -1
- package/src/cdp/measure/collectors/states.ts +243 -4
- package/src/cdp/measure/collectors/styles.ts +67 -43
- package/src/cdp/measure/explain.ts +123 -23
- package/src/cdp/measure/ink-source.ts +116 -0
- package/src/cdp/measure/ink.ts +180 -0
- package/src/cdp/measure/snapshot.ts +27 -13
- package/src/cdp/screenshot.ts +43 -11
- package/src/cdp/targets.ts +24 -0
- package/src/cdp/types.ts +15 -0
- package/src/contracts/index.ts +0 -2
- package/src/output/render.ts +1 -0
- package/src/output/selector.ts +238 -5
- package/src/session/commands.ts +47 -32
- package/src/command-help.ts +0 -29
- package/src/command-resolution.ts +0 -22
- package/src/command-walker.ts +0 -35
- package/src/contracts/results.ts +0 -208
- package/src/contracts/routes.ts +0 -140
- package/src/output/exact-raw.ts +0 -114
- package/src/registry.ts +0 -208
|
@@ -52,6 +52,33 @@ export function pageInputDeps(): PageInputDeps {
|
|
|
52
52
|
return deps;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export interface PageInputScreenshot {
|
|
56
|
+
readonly screenshot: string | null;
|
|
57
|
+
readonly screenshotWarning: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A dispatched page action stays successful when only its follow-up screenshot times out. */
|
|
61
|
+
export async function capturePageInputScreenshot(
|
|
62
|
+
client: Parameters<typeof autoScreenshot>[0],
|
|
63
|
+
action: string,
|
|
64
|
+
label: string,
|
|
65
|
+
noScreenshot?: boolean,
|
|
66
|
+
): Promise<PageInputScreenshot> {
|
|
67
|
+
try {
|
|
68
|
+
return {
|
|
69
|
+
screenshot: await deps.autoScreenshot(client, action, label, noScreenshot),
|
|
70
|
+
screenshotWarning: null,
|
|
71
|
+
};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
if (!/^CDP request timeout \(\d+ms\): Page\.captureScreenshot$/.test(message)) throw error;
|
|
75
|
+
return {
|
|
76
|
+
screenshot: null,
|
|
77
|
+
screenshotWarning: `automatic screenshot timed out after the page action completed; no screenshot was saved (${message})`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
55
82
|
/**
|
|
56
83
|
* The verb's settle window: `--settle <ms>` (including 0) wins outright;
|
|
57
84
|
* otherwise the default is keyed off active-session presence (the in-session
|
|
@@ -147,7 +174,7 @@ input:
|
|
|
147
174
|
--settle <ms> network-settle window applied after the click (default: 1000; 2500 with an active session; 0 disables)
|
|
148
175
|
--no-screenshot skip the auto-screenshot
|
|
149
176
|
output:
|
|
150
|
-
<clicked backend-node-id=… role=… name=…> — resolved identity, dispatched coordinates, the measured settle (requested/waited), screenshot artifact path; --json mirrors the same fields
|
|
177
|
+
<clicked backend-node-id=… role=… name=…> — 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
|
|
151
178
|
effects:
|
|
152
179
|
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`;
|
|
153
180
|
|
|
@@ -177,8 +204,8 @@ export async function cmdPageClick(parsed: ParsedArgs, _args: string[]): Promise
|
|
|
177
204
|
const resolved = await resolveLiveTarget(live, target);
|
|
178
205
|
if (!resolved.ok) return { failure: resolved } as const;
|
|
179
206
|
const dispatch = await clickResolved(live, resolved);
|
|
180
|
-
const
|
|
181
|
-
return { dispatch,
|
|
207
|
+
const screenshotResult = await capturePageInputScreenshot(client, 'click', target, parsed.noScreenshot);
|
|
208
|
+
return { dispatch, ...screenshotResult } as const;
|
|
182
209
|
},
|
|
183
210
|
);
|
|
184
211
|
|
|
@@ -186,12 +213,13 @@ export async function cmdPageClick(parsed: ParsedArgs, _args: string[]): Promise
|
|
|
186
213
|
return emitResolutionError(parsed, 'page click', outcome.failure);
|
|
187
214
|
}
|
|
188
215
|
|
|
189
|
-
const { dispatch, screenshot } = outcome;
|
|
216
|
+
const { dispatch, screenshot, screenshotWarning } = outcome;
|
|
190
217
|
const rows: FactLine[] = [
|
|
191
218
|
fact`clicked ${dispatch.role ?? 'unknown'} "${dispatch.name ?? ''}" (backend:${dispatch.backendNodeId}) at x=${dispatch.x} y=${dispatch.y}`,
|
|
192
219
|
fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`,
|
|
193
220
|
];
|
|
194
221
|
if (screenshot) rows.push(fact`screenshot: ${screenshot}`);
|
|
222
|
+
if (screenshotWarning) rows.push(fact`screenshot-warning: ${screenshotWarning}`);
|
|
195
223
|
|
|
196
224
|
emitResult(
|
|
197
225
|
{
|
|
@@ -50,7 +50,7 @@ input:
|
|
|
50
50
|
<code> JS source as one argument (exactly one of <code> / --file)
|
|
51
51
|
--file <path> read the JS source from a file instead of inline
|
|
52
52
|
--settle <ms> network-settle window after execution so an active session's HAR captures the requests the code triggers (default: ${DEFAULT_SETTLE_MS}; 0 disables); the block reports the measured requested/waited settle
|
|
53
|
-
--target <id> target a tab explicitly (default: the active session tab;
|
|
53
|
+
--target <id> target a tab explicitly (default: the active session tab, else exactly one available page tab; multiple page tabs require --target or --url)
|
|
54
54
|
--url <pattern> target the first tab whose URL matches <pattern>
|
|
55
55
|
output:
|
|
56
56
|
<exec-result result-chars=…> — the JSON-serialized return value inline, escaped and capped at ${GENEROUS_RESULT_CAP} chars; a larger result is also written whole to a private artifact file (the active session's page/ dir, else a one-shot artifact dir) whose absolute path appears in the block. --json mirrors the same block with the value at full fidelity.
|
|
@@ -22,7 +22,7 @@ import { cmdPageElements } from './elements.js';
|
|
|
22
22
|
/** Root-help representation of this branch, assembled by `src/capture.ts`. */
|
|
23
23
|
export const COMMAND_BLOCK = `<command name="page">
|
|
24
24
|
verbs against the live session tab — act (click, type, scroll, navigate, exec) and look (shot, elements)
|
|
25
|
-
use when driving or inspecting the
|
|
25
|
+
use when driving or inspecting the tab a session opened or adopted; all verbs auto-target the active session tab, --target/--url override
|
|
26
26
|
click · type · scroll · navigate · exec · shot · elements — \`capture page -h\`
|
|
27
27
|
</command>`;
|
|
28
28
|
|
|
@@ -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] [--color-scheme <dark|light>] [--out <path>]" whenToUse="look at the page right now without acting"/>
|
|
42
|
+
<subcommand name="shot" args="[--viewport <WxH>] [--full-page] [--color-scheme <dark|light>] [--crop <x,y,w,h> | --crop-selector <sel> [--pad <px>]] [--zoom <factor>] [--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`;
|
|
@@ -13,6 +13,7 @@ import { resolveLiveTarget, scrollResolved, type LiveClient } from '../../../int
|
|
|
13
13
|
import { emitResult, fact, lineList, type FactLine } from '../../../output/render.js';
|
|
14
14
|
import {
|
|
15
15
|
pageInputDeps,
|
|
16
|
+
capturePageInputScreenshot,
|
|
16
17
|
effectiveSettle,
|
|
17
18
|
emitInvalidInput,
|
|
18
19
|
emitResolutionError,
|
|
@@ -26,7 +27,7 @@ input:
|
|
|
26
27
|
--settle <ms> network-settle window applied after the scroll (default: 1000; 2500 with an active session; 0 disables)
|
|
27
28
|
--no-screenshot skip the auto-screenshot
|
|
28
29
|
output:
|
|
29
|
-
<scrolled backend-node-id=… role=… name=…> — resolved identity, destination, the container's resulting scrollTop, the measured settle (requested/waited), screenshot artifact path; --json mirrors the same fields
|
|
30
|
+
<scrolled backend-node-id=… role=… name=…> — 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
|
|
30
31
|
effects:
|
|
31
32
|
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`;
|
|
32
33
|
|
|
@@ -74,8 +75,8 @@ export async function cmdPageScroll(parsed: ParsedArgs, _args: string[]): Promis
|
|
|
74
75
|
// Same landmark shape `motion rec --do scroll:` records; carried by
|
|
75
76
|
// the one mutating call when the transport records landmarks.
|
|
76
77
|
const dispatch = await scrollResolved(live, resolved, to, { mark: `scroll:${target},to=${to}` });
|
|
77
|
-
const
|
|
78
|
-
return { dispatch,
|
|
78
|
+
const screenshotResult = await capturePageInputScreenshot(client, 'scroll', target, parsed.noScreenshot);
|
|
79
|
+
return { dispatch, ...screenshotResult } as const;
|
|
79
80
|
},
|
|
80
81
|
);
|
|
81
82
|
|
|
@@ -83,12 +84,13 @@ export async function cmdPageScroll(parsed: ParsedArgs, _args: string[]): Promis
|
|
|
83
84
|
return emitResolutionError(parsed, 'page scroll', outcome.failure);
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
const { dispatch, screenshot } = outcome;
|
|
87
|
+
const { dispatch, screenshot, screenshotWarning } = outcome;
|
|
87
88
|
const rows: FactLine[] = [
|
|
88
89
|
fact`scrolled ${dispatch.role ?? 'unknown'} "${dispatch.name ?? ''}" (backend:${dispatch.backendNodeId}) to ${dispatch.to} — scrollTop now ${dispatch.scrollTop}`,
|
|
89
90
|
fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`,
|
|
90
91
|
];
|
|
91
92
|
if (screenshot) rows.push(fact`screenshot: ${screenshot}`);
|
|
93
|
+
if (screenshotWarning) rows.push(fact`screenshot-warning: ${screenshotWarning}`);
|
|
92
94
|
|
|
93
95
|
emitResult(
|
|
94
96
|
{
|
|
@@ -19,11 +19,13 @@ import * as fs from 'fs';
|
|
|
19
19
|
import * as path from 'path';
|
|
20
20
|
import { type ParsedArgs } from '../../types.js';
|
|
21
21
|
import { withConnection } from '../../connection.js';
|
|
22
|
-
import { captureScreenshotWithCssViewport, type CapturedRegion } from '../../screenshot.js';
|
|
22
|
+
import { captureScreenshotWithCssViewport, type CapturedRegion, type ScreenshotCrop } from '../../screenshot.js';
|
|
23
23
|
import { parseViewport, type Viewport } from '../../viewport.js';
|
|
24
24
|
import { parseColorScheme, withAppliedColorScheme, type ColorScheme } from '../../color-scheme.js';
|
|
25
25
|
import { nextStepPath } from '../../../session-context.js';
|
|
26
26
|
import { createOneshotSession } from '../../../session/commands.js';
|
|
27
|
+
import { resolveLiveTarget, type LiveClient, type ResolutionFailure } from '../../../interact.js';
|
|
28
|
+
import { emitResolutionError } from './click.js';
|
|
27
29
|
import { assertUnderCaptureRoot, writeBinaryPrivate } from '../../../session/artifacts.js';
|
|
28
30
|
import {
|
|
29
31
|
emitResult,
|
|
@@ -39,13 +41,17 @@ input:
|
|
|
39
41
|
--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
|
|
40
42
|
--full-page transient override to the full scrollable content height for this capture
|
|
41
43
|
--color-scheme <dark|light> transient prefers-color-scheme override for this capture; cleared afterward
|
|
44
|
+
--crop <x,y,w,h> capture this page-coordinate CSS-pixel rect, intersected with the live visual viewport; x/y may be fractional, w/h must be positive
|
|
45
|
+
--crop-selector <sel> resolve exactly one live target (bare CSS, ax:<name>, axid:<id>, or backend:<id>), scroll it into view, then crop its border box and surroundings
|
|
46
|
+
--pad <px> nonnegative integer CSS pixels added on every side of --crop-selector (default: 0; not accepted with --crop)
|
|
47
|
+
--zoom <factor> positive decimal CSS-to-image scale requested for a CSS crop; the 1600px image cap may produce a smaller reported scale
|
|
42
48
|
--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
|
|
43
49
|
--target <tabId> | --url <pattern> | --port <n> tab targeting; defaults to the active session tab
|
|
44
50
|
--json mirror the result as JSON
|
|
45
51
|
output:
|
|
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
|
|
52
|
+
<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, crop provenance, and any requested transient emulation
|
|
47
53
|
effects:
|
|
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.`;
|
|
54
|
+
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. --crop-selector scrolls its resolved target into view. CSS crops are intersected with the source visual viewport; an empty intersection fails without writing an image. --crop/--crop-selector cannot be combined with --full-page.`;
|
|
49
55
|
|
|
50
56
|
// ---------------------------------------------------------------------------
|
|
51
57
|
// Test-injectable dependency seam (the CDP-stub test pattern; the capture
|
|
@@ -119,6 +125,15 @@ function writeScreenshot(outPath: string, png: Buffer): void {
|
|
|
119
125
|
|
|
120
126
|
export type EmulationMode = 'none' | 'viewport' | 'full-page';
|
|
121
127
|
|
|
128
|
+
interface CropProvenance {
|
|
129
|
+
readonly source: 'coordinates' | 'selector';
|
|
130
|
+
readonly requested: ScreenshotCrop;
|
|
131
|
+
readonly selector?: string;
|
|
132
|
+
readonly backendNodeId?: number;
|
|
133
|
+
readonly pad?: number;
|
|
134
|
+
readonly zoom: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
122
137
|
/** Renders a CSS-pixel measurement without inventing precision: an integer stays an integer, a fractional zoomed viewport keeps six decimals. */
|
|
123
138
|
function round6(value: number): number {
|
|
124
139
|
return Math.round(value * 1e6) / 1e6;
|
|
@@ -132,6 +147,7 @@ export function buildScreenshotResult(f: {
|
|
|
132
147
|
emulation: EmulationMode;
|
|
133
148
|
viewport?: { width: number; height: number };
|
|
134
149
|
colorScheme?: ColorScheme;
|
|
150
|
+
crop?: CropProvenance;
|
|
135
151
|
}): RenderableResult {
|
|
136
152
|
// The denominator is the CSS region the image was actually cut from, so the
|
|
137
153
|
// ratio stays true under page zoom, device pixel ratio, and the local
|
|
@@ -143,6 +159,9 @@ export function buildScreenshotResult(f: {
|
|
|
143
159
|
scale && f.cssViewport
|
|
144
160
|
? 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)}).`
|
|
145
161
|
: text`CSS-to-image scale unavailable: the browser did not return the CSS region this capture covers.`,
|
|
162
|
+
f.crop
|
|
163
|
+
? fact`CSS crop: ${f.crop.source === 'selector' ? `selector ${f.crop.selector} (backend:${f.crop.backendNodeId}), border box plus ${f.crop.pad}px padding` : 'coordinates'} requested ${formatRect(f.crop.requested)} at zoom ${f.crop.zoom}; the captured CSS region above is its intersection with the source visual viewport.`
|
|
164
|
+
: text`CSS crop: none — the captured CSS region above is the source visual viewport.`,
|
|
146
165
|
f.emulation === 'none'
|
|
147
166
|
? text`viewport emulation: none — the browser's actual current viewport was captured.`
|
|
148
167
|
: f.emulation === 'viewport'
|
|
@@ -168,6 +187,10 @@ export function buildScreenshotResult(f: {
|
|
|
168
187
|
'css-to-image-y': scale?.y.toFixed(6),
|
|
169
188
|
emulation: f.emulation,
|
|
170
189
|
'color-scheme': f.colorScheme,
|
|
190
|
+
'crop-source': f.crop?.source,
|
|
191
|
+
'crop-selector': f.crop?.selector,
|
|
192
|
+
'crop-backend-node-id': f.crop?.backendNodeId,
|
|
193
|
+
'requested-zoom': f.crop?.zoom,
|
|
171
194
|
},
|
|
172
195
|
summary: f.dimensions
|
|
173
196
|
? fact`saved ${f.path} — ${f.dimensions.width}x${f.dimensions.height}px, ${f.bytes} bytes.`
|
|
@@ -176,6 +199,38 @@ export function buildScreenshotResult(f: {
|
|
|
176
199
|
};
|
|
177
200
|
}
|
|
178
201
|
|
|
202
|
+
function formatRect(rect: ScreenshotCrop): string {
|
|
203
|
+
return `x=${round6(rect.x)} y=${round6(rect.y)} w=${round6(rect.width)} h=${round6(rect.height)}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseCrop(value: string): ScreenshotCrop | undefined {
|
|
207
|
+
const match = /^(-?(?:\d+\.?\d*|\.\d+)),(-?(?:\d+\.?\d*|\.\d+)),(\d+(?:\.\d*)?|\.\d+),(\d+(?:\.\d*)?|\.\d+)$/.exec(value);
|
|
208
|
+
if (!match) return undefined;
|
|
209
|
+
const [x, y, width, height] = match.slice(1).map(Number);
|
|
210
|
+
if (![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0) return undefined;
|
|
211
|
+
return { x, y, width, height };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseZoom(value: string): number | undefined {
|
|
215
|
+
if (!/^(?:\d+\.?\d*|\.\d+)$/.test(value)) return undefined;
|
|
216
|
+
const zoom = Number(value);
|
|
217
|
+
return Number.isFinite(zoom) && zoom > 0 ? zoom : undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function borderRect(model: unknown): ScreenshotCrop | undefined {
|
|
221
|
+
const border = (model as { border?: unknown } | undefined)?.border;
|
|
222
|
+
if (!Array.isArray(border) || border.length !== 8 || !border.every((point) => typeof point === 'number' && Number.isFinite(point))) return undefined;
|
|
223
|
+
const xs = [border[0], border[2], border[4], border[6]] as number[];
|
|
224
|
+
const ys = [border[1], border[3], border[5], border[7]] as number[];
|
|
225
|
+
const x = Math.min(...xs);
|
|
226
|
+
const y = Math.min(...ys);
|
|
227
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function padded(rect: ScreenshotCrop, pad: number): ScreenshotCrop {
|
|
231
|
+
return { x: rect.x - pad, y: rect.y - pad, width: rect.width + 2 * pad, height: rect.height + 2 * pad };
|
|
232
|
+
}
|
|
233
|
+
|
|
179
234
|
// ---------------------------------------------------------------------------
|
|
180
235
|
// page shot
|
|
181
236
|
// ---------------------------------------------------------------------------
|
|
@@ -191,7 +246,7 @@ export async function cmdPageShot(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
191
246
|
{
|
|
192
247
|
tag: 'error',
|
|
193
248
|
attrs: { command: 'page shot', code: 'invalid_input' },
|
|
194
|
-
summary: fact`received: ${parsed.positional.length} positional arguments; expected none — \`capture page shot [--viewport <WxH>] [--full-page] [--color-scheme <dark|light>] [--out <path>]\`.`,
|
|
249
|
+
summary: fact`received: ${parsed.positional.length} positional arguments; expected none — \`capture page shot [--viewport <WxH>] [--full-page] [--color-scheme <dark|light>] [--crop <x,y,w,h> | --crop-selector <sel> [--pad <px>]] [--zoom <factor>] [--out <path>]\`.`,
|
|
195
250
|
},
|
|
196
251
|
{ json: parsed.json },
|
|
197
252
|
);
|
|
@@ -199,6 +254,44 @@ export async function cmdPageShot(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
199
254
|
return;
|
|
200
255
|
}
|
|
201
256
|
|
|
257
|
+
const crop = parsed.crop === undefined ? undefined : parseCrop(parsed.crop);
|
|
258
|
+
if (parsed.crop !== undefined && !crop) {
|
|
259
|
+
emitResult({ tag: 'error', attrs: { command: 'page shot', code: 'invalid_crop' }, summary: fact`received: --crop ${parsed.crop}; expected x,y,w,h as finite CSS pixels without whitespace, with positive w and h.` }, { json: parsed.json });
|
|
260
|
+
process.exitCode = 1;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (parsed.crop !== undefined && parsed.cropSelector !== undefined) {
|
|
264
|
+
emitResult({ tag: 'error', attrs: { command: 'page shot', code: 'conflicting_crop' }, summary: text`--crop and --crop-selector cannot be combined; choose coordinates or one live target.` }, { json: parsed.json });
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (parsed.cropSelector !== undefined && !parsed.cropSelector.trim()) {
|
|
269
|
+
emitResult({ tag: 'error', attrs: { command: 'page shot', code: 'invalid_crop_selector' }, summary: text`received an empty --crop-selector; expected a bare CSS selector, ax:<name>, axid:<id>, or backend:<id>.` }, { json: parsed.json });
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (parsed.pad !== undefined && parsed.cropSelector === undefined) {
|
|
274
|
+
emitResult({ tag: 'error', attrs: { command: 'page shot', code: 'pad_requires_crop_selector' }, summary: text`--pad applies only to --crop-selector.` }, { json: parsed.json });
|
|
275
|
+
process.exitCode = 1;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const zoom = parsed.zoom === undefined ? 1 : parseZoom(parsed.zoom);
|
|
279
|
+
if (!zoom) {
|
|
280
|
+
emitResult({ tag: 'error', attrs: { command: 'page shot', code: 'invalid_zoom' }, summary: fact`received: --zoom ${parsed.zoom}; expected a positive decimal factor.` }, { json: parsed.json });
|
|
281
|
+
process.exitCode = 1;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (parsed.zoom !== undefined && !crop && parsed.cropSelector === undefined) {
|
|
285
|
+
emitResult({ tag: 'error', attrs: { command: 'page shot', code: 'zoom_requires_crop' }, summary: text`--zoom requires --crop or --crop-selector.` }, { json: parsed.json });
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (parsed.fullPage && (crop || parsed.cropSelector !== undefined)) {
|
|
290
|
+
emitResult({ tag: 'error', attrs: { command: 'page shot', code: 'crop_conflicts_with_full_page' }, summary: text`--crop and --crop-selector capture from the visual viewport and cannot be combined with --full-page.` }, { json: parsed.json });
|
|
291
|
+
process.exitCode = 1;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
202
295
|
let viewport: Viewport | undefined;
|
|
203
296
|
if (parsed.viewport !== undefined) {
|
|
204
297
|
try {
|
|
@@ -241,13 +334,32 @@ export async function cmdPageShot(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
241
334
|
const result = await deps.withConnection(
|
|
242
335
|
{ ...parsed, command: 'shot' },
|
|
243
336
|
async (client) => withAppliedColorScheme(client, colorScheme, async () => {
|
|
244
|
-
|
|
337
|
+
let cropProvenance: CropProvenance | undefined;
|
|
338
|
+
let requestedCrop = crop;
|
|
339
|
+
if (parsed.cropSelector !== undefined) {
|
|
340
|
+
const resolved = await resolveLiveTarget(client as unknown as LiveClient, parsed.cropSelector);
|
|
341
|
+
if (!resolved.ok) return { failure: resolved } as const;
|
|
342
|
+
await client.send('DOM.scrollIntoViewIfNeeded', { backendNodeId: resolved.backendNodeId });
|
|
343
|
+
const box = (await client.send('DOM.getBoxModel', { backendNodeId: resolved.backendNodeId })) as { model?: unknown };
|
|
344
|
+
const rect = borderRect(box.model);
|
|
345
|
+
if (!rect) throw new Error(`Resolved target backend:${resolved.backendNodeId} has no measurable border box for --crop-selector.`);
|
|
346
|
+
const pad = parsed.pad ?? 0;
|
|
347
|
+
requestedCrop = padded(rect, pad);
|
|
348
|
+
cropProvenance = { source: 'selector', requested: requestedCrop, selector: parsed.cropSelector, backendNodeId: resolved.backendNodeId, pad, zoom };
|
|
349
|
+
} else if (requestedCrop) {
|
|
350
|
+
cropProvenance = { source: 'coordinates', requested: requestedCrop, zoom };
|
|
351
|
+
}
|
|
352
|
+
const screenshot = await captureScreenshotWithCssViewport(client, viewport, { fullPage: parsed.fullPage, crop: requestedCrop, zoom });
|
|
245
353
|
const outPath = await resolveOutPath(parsed);
|
|
246
354
|
writeScreenshot(outPath, screenshot.png);
|
|
247
|
-
return { path: outPath, bytes: screenshot.png.length, dimensions: pngDimensions(screenshot.png), cssViewport: screenshot.cssViewport };
|
|
355
|
+
return { path: outPath, bytes: screenshot.png.length, dimensions: pngDimensions(screenshot.png), cssViewport: screenshot.cssViewport, crop: cropProvenance } as const;
|
|
248
356
|
}),
|
|
249
357
|
{ settle: 0 },
|
|
250
358
|
);
|
|
251
359
|
|
|
360
|
+
if ('failure' in result) {
|
|
361
|
+
emitResolutionError(parsed, 'page shot', result.failure as ResolutionFailure);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
252
364
|
emitResult(buildScreenshotResult({ ...result, emulation, viewport, colorScheme }), { json: parsed.json });
|
|
253
365
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { emitResult, fact, lineList, type FactLine } from '../../../output/render.js';
|
|
19
19
|
import {
|
|
20
20
|
pageInputDeps,
|
|
21
|
+
capturePageInputScreenshot,
|
|
21
22
|
effectiveSettle,
|
|
22
23
|
emitInvalidInput,
|
|
23
24
|
emitResolutionError,
|
|
@@ -31,7 +32,7 @@ input:
|
|
|
31
32
|
--settle <ms> network-settle window applied after typing (default: 500; 1500 with an active session; 0 disables)
|
|
32
33
|
--no-screenshot skip the auto-screenshot
|
|
33
34
|
output:
|
|
34
|
-
<typed> — 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
|
|
35
|
+
<typed> — 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
|
|
35
36
|
effects:
|
|
36
37
|
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`;
|
|
37
38
|
|
|
@@ -69,8 +70,8 @@ export async function cmdPageType(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
69
70
|
await typeText(live, textArg);
|
|
70
71
|
}
|
|
71
72
|
// Screenshot label identifies the field, never the typed content.
|
|
72
|
-
const
|
|
73
|
-
return { dispatch,
|
|
73
|
+
const screenshotResult = await capturePageInputScreenshot(client, 'type', parsed.into ?? 'focused element', parsed.noScreenshot);
|
|
74
|
+
return { dispatch, ...screenshotResult } as const;
|
|
74
75
|
},
|
|
75
76
|
);
|
|
76
77
|
|
|
@@ -78,7 +79,7 @@ export async function cmdPageType(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
78
79
|
return emitResolutionError(parsed, 'page type', outcome.failure);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
const { dispatch, screenshot } = outcome;
|
|
82
|
+
const { dispatch, screenshot, screenshotWarning } = outcome;
|
|
82
83
|
const rows: FactLine[] = [
|
|
83
84
|
dispatch
|
|
84
85
|
? fact`typed "${textArg}" into ${dispatch.role ?? 'unknown'} "${dispatch.name ?? ''}" (backend:${dispatch.backendNodeId}), focus click at x=${dispatch.x} y=${dispatch.y}`
|
|
@@ -86,6 +87,7 @@ export async function cmdPageType(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
86
87
|
fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`,
|
|
87
88
|
];
|
|
88
89
|
if (screenshot) rows.push(fact`screenshot: ${screenshot}`);
|
|
90
|
+
if (screenshotWarning) rows.push(fact`screenshot-warning: ${screenshotWarning}`);
|
|
89
91
|
|
|
90
92
|
emitResult(
|
|
91
93
|
{
|
package/src/cdp/connection.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import { performance } from 'node:perf_hooks';
|
|
3
3
|
import { CDPClient } from './client.js';
|
|
4
|
-
import {
|
|
4
|
+
import { detectCdpPortsAsync } from './detect.js';
|
|
5
|
+
import { AmbiguousPageTargetError, findTabByIdAcrossEndpoints, findTabByUrlAcrossEndpoints, findUnambiguousPageTabInPorts } from './targets.js';
|
|
5
6
|
import { type CDPTarget, type ParsedArgs } from './types.js';
|
|
6
7
|
import { ConsoleRecorder, printConsoleSummary } from './console-recorder.js';
|
|
7
8
|
import { HARRecorder } from './har-recorder.js';
|
|
@@ -13,7 +14,7 @@ import { getActiveSession, updateActiveSession, type ActiveSessionState } from '
|
|
|
13
14
|
import { withSessionScopeLifecycle } from '../session/coordinator.js';
|
|
14
15
|
import { RecorderHeldClient, isRecorderHeldClient } from './recorder-client.js';
|
|
15
16
|
import { recDirFor, readRecorderJson } from './motion/recorder.js';
|
|
16
|
-
import { captureError
|
|
17
|
+
import { captureError } from '../errors.js';
|
|
17
18
|
|
|
18
19
|
function getPortFromWebSocketDebuggerUrl(url?: string): number | null {
|
|
19
20
|
if (!url) return null;
|
|
@@ -51,10 +52,10 @@ export interface ConnectionSeams {
|
|
|
51
52
|
sleep: (ms: number) => Promise<void>;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
function defaultResolveTab(parsed: ParsedArgs): Promise<{ port: number; tab: CDPTarget } | null> {
|
|
55
|
-
return parsed.target
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
async function defaultResolveTab(parsed: ParsedArgs): Promise<{ port: number; tab: CDPTarget } | null> {
|
|
56
|
+
if (parsed.target) return findTabByIdAcrossEndpoints(parsed.target, parsed.port);
|
|
57
|
+
if (parsed.url) return findTabByUrlAcrossEndpoints(parsed.url, parsed.port);
|
|
58
|
+
return findUnambiguousPageTabInPorts(parsed.port === undefined ? (await detectCdpPortsAsync()).map((endpoint) => endpoint.port) : [parsed.port]);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
let seams: ConnectionSeams = {
|
|
@@ -210,11 +211,15 @@ export async function connectForCommand(
|
|
|
210
211
|
return routed;
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
let resolved: { port: number; tab: CDPTarget } | null;
|
|
215
|
+
try {
|
|
216
|
+
resolved = await seams.resolveTab(parsed);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error instanceof AmbiguousPageTargetError) {
|
|
219
|
+
throw captureError('precondition', 'target_unavailable', error.message, error);
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
215
222
|
}
|
|
216
|
-
|
|
217
|
-
const resolved = await seams.resolveTab(parsed);
|
|
218
223
|
const tab = resolved?.tab ?? null;
|
|
219
224
|
|
|
220
225
|
if (!tab) {
|
|
@@ -222,7 +227,9 @@ export async function connectForCommand(
|
|
|
222
227
|
throw captureError(
|
|
223
228
|
'precondition',
|
|
224
229
|
'target_unavailable',
|
|
225
|
-
|
|
230
|
+
query === undefined
|
|
231
|
+
? 'No page tab found. Run "capture tab list" to see available tabs.'
|
|
232
|
+
: `No tab found for ${parsed.target ? 'target' : 'URL pattern'} "${query}". Run "capture tab list" to see available tabs.`,
|
|
226
233
|
);
|
|
227
234
|
}
|
|
228
235
|
|
|
@@ -320,7 +327,7 @@ async function finishLocalHar(parsed: ParsedArgs, collectors: Collectors): Promi
|
|
|
320
327
|
if (batch.entries.length > 0 || batch.incompleteLifecycles.length > 0) {
|
|
321
328
|
await seams.appendHar(parsed.har, batch);
|
|
322
329
|
console.error(
|
|
323
|
-
` [har:${parsed.har}] +${batch.entries.length} entries +${batch.incompleteLifecycles.length} incomplete`,
|
|
330
|
+
` [har:${parsed.har}] +${batch.entries.length} entries +${batch.incompleteLifecycles.length} incomplete — incomplete lifecycles are retained as their own records (request plus whatever response was observed), separate from log.entries; read them with capture session har`,
|
|
324
331
|
);
|
|
325
332
|
}
|
|
326
333
|
}
|
|
@@ -284,6 +284,7 @@ function buildGeometryWalkScript(maxElements: number): string {
|
|
|
284
284
|
idx: idx,
|
|
285
285
|
tag: tag.toLowerCase(),
|
|
286
286
|
selector: __selectorOf(el),
|
|
287
|
+
attributes: Array.prototype.map.call(el.attributes, function (attr) { return [attr.name, attr.value]; }),
|
|
287
288
|
domPath: __domPathOf(el),
|
|
288
289
|
text: directText(el),
|
|
289
290
|
frame: { frameId: frameId, isTopFrame: isTop, ancestorFrameIds: ancestorFrameIds.slice() },
|
|
@@ -292,6 +293,16 @@ function buildGeometryWalkScript(maxElements: number): string {
|
|
|
292
293
|
stackingContext: { creates: reasons.length > 0, reasons: reasons },
|
|
293
294
|
visibility: { visible: visible, opacity: opacity, displayNone: displayNone, visibilityHidden: visibilityHidden, zeroSize: zeroSize },
|
|
294
295
|
clipping: clippingInfo(el, rect),
|
|
296
|
+
paint: {
|
|
297
|
+
'box-shadow': style.boxShadow,
|
|
298
|
+
'outline-width': style.outlineWidth,
|
|
299
|
+
'outline-style': style.outlineStyle,
|
|
300
|
+
'outline-offset': style.outlineOffset,
|
|
301
|
+
filter: style.filter,
|
|
302
|
+
transform: style.transform,
|
|
303
|
+
'clip-path': style.clipPath,
|
|
304
|
+
'border-radius': style.borderRadius,
|
|
305
|
+
},
|
|
295
306
|
layout: {
|
|
296
307
|
boxSizing: style.boxSizing,
|
|
297
308
|
position: style.position,
|
|
@@ -342,14 +353,11 @@ function buildGeometryWalkScript(maxElements: number): string {
|
|
|
342
353
|
pushedFact.iframeContentUnavailable = true;
|
|
343
354
|
pushedFact.iframeContentUnavailableReason = 'content-document-read-threw';
|
|
344
355
|
}
|
|
345
|
-
if (innerDoc && innerDoc.
|
|
356
|
+
if (innerDoc && innerDoc.documentElement) {
|
|
346
357
|
frameCounter += 1;
|
|
347
358
|
var childFrameId = 'frame-' + frameCounter;
|
|
348
359
|
var childAncestors = ancestorFrameIds.concat([frameId]);
|
|
349
|
-
|
|
350
|
-
for (var ii = 0; ii < innerKids.length; ii++) {
|
|
351
|
-
walk(innerKids[ii], childFrameId, false, childAncestors, null);
|
|
352
|
-
}
|
|
360
|
+
walk(innerDoc.documentElement, childFrameId, false, childAncestors, null);
|
|
353
361
|
}
|
|
354
362
|
return;
|
|
355
363
|
}
|
|
@@ -360,10 +368,7 @@ function buildGeometryWalkScript(maxElements: number): string {
|
|
|
360
368
|
}
|
|
361
369
|
}
|
|
362
370
|
|
|
363
|
-
|
|
364
|
-
for (var bi = 0; bi < bodyKids.length; bi++) {
|
|
365
|
-
walk(bodyKids[bi], 'frame-0', true, [], null);
|
|
366
|
-
}
|
|
371
|
+
if (document.documentElement) walk(document.documentElement, 'frame-0', true, [], null);
|
|
367
372
|
|
|
368
373
|
return { facts: facts, elements: elements, meta: { elementsTruncated: elementsOverflowCount } };
|
|
369
374
|
})()`;
|
|
@@ -569,6 +574,17 @@ interface RawGridFact {
|
|
|
569
574
|
};
|
|
570
575
|
}
|
|
571
576
|
|
|
577
|
+
interface RawPaintFact {
|
|
578
|
+
readonly 'box-shadow': string;
|
|
579
|
+
readonly 'outline-width': string;
|
|
580
|
+
readonly 'outline-style': string;
|
|
581
|
+
readonly 'outline-offset': string;
|
|
582
|
+
readonly filter: string;
|
|
583
|
+
readonly transform: string;
|
|
584
|
+
readonly 'clip-path': string;
|
|
585
|
+
readonly 'border-radius': string;
|
|
586
|
+
}
|
|
587
|
+
|
|
572
588
|
interface RawLayoutFact {
|
|
573
589
|
readonly boxSizing: string;
|
|
574
590
|
readonly position: string;
|
|
@@ -609,6 +625,7 @@ interface RawGeometryFact {
|
|
|
609
625
|
readonly idx: number;
|
|
610
626
|
readonly tag: string;
|
|
611
627
|
readonly selector?: string;
|
|
628
|
+
readonly attributes?: readonly (readonly [string, string])[];
|
|
612
629
|
readonly domPath: string;
|
|
613
630
|
readonly text?: string;
|
|
614
631
|
readonly frame: RawFrameFact;
|
|
@@ -617,6 +634,8 @@ interface RawGeometryFact {
|
|
|
617
634
|
readonly stackingContext: RawStackingContextFact;
|
|
618
635
|
readonly visibility: RawVisibilityFact;
|
|
619
636
|
readonly clipping: RawClippingFact | null;
|
|
637
|
+
/** Ink-relevant computed styles retained here rather than styles.json so every retained geometry element is queryable, including shadow/iframe records and records beyond styles.json's 150-element cap. */
|
|
638
|
+
readonly paint: RawPaintFact;
|
|
620
639
|
readonly layout: RawLayoutFact;
|
|
621
640
|
/** Present only on an `<iframe>` element's own fact when its `contentDocument` read threw (G6) -- see {@link GeometryElementRecord.iframeContentUnavailable}. */
|
|
622
641
|
readonly iframeContentUnavailable?: boolean;
|
|
@@ -639,6 +658,10 @@ export interface GeometryBoxModel {
|
|
|
639
658
|
export interface GeometryElementRecord extends Omit<ElementRecord, 'backendNodeId'> {
|
|
640
659
|
readonly id: string;
|
|
641
660
|
readonly tag: string;
|
|
661
|
+
/** The element's own HTML attributes, capped by {@link capAttributes}. */
|
|
662
|
+
readonly attributes: Readonly<Record<string, string>>;
|
|
663
|
+
/** Number of own attributes omitted by the per-element cap, when nonzero. */
|
|
664
|
+
readonly attributesTruncated?: number;
|
|
642
665
|
readonly domPath: string;
|
|
643
666
|
readonly frame: RawFrameFact;
|
|
644
667
|
readonly shadow: RawShadowFact | null;
|
|
@@ -649,6 +672,8 @@ export interface GeometryElementRecord extends Omit<ElementRecord, 'backendNodeI
|
|
|
649
672
|
readonly stackingContext: RawStackingContextFact;
|
|
650
673
|
readonly visibility: RawVisibilityFact;
|
|
651
674
|
readonly clipping: RawClippingFact | null;
|
|
675
|
+
/** Uncapped, per-geometry-record computed styles used for ink extents and clip-source description. Absent in snapshots captured before ink evidence existed. */
|
|
676
|
+
readonly paint?: RawPaintFact;
|
|
652
677
|
readonly layout: LayoutFact;
|
|
653
678
|
/** `null` (never an omitted key) when this record's identity did not resolve — see {@link identityUnresolved}. */
|
|
654
679
|
readonly backendNodeId: number | null;
|
|
@@ -723,9 +748,24 @@ function resolvedIdentity(backendNodeId: number | undefined): { backendNodeId: n
|
|
|
723
748
|
|
|
724
749
|
/** Cap for ordinary computed-CSS-value strings (position/display/min-max sizes/flex+grid provenance). */
|
|
725
750
|
const CSS_VALUE_MAX = 200;
|
|
751
|
+
/** Cap on how many HTML attributes are retained for one geometry element. */
|
|
752
|
+
const MAX_ATTRIBUTES = 32;
|
|
753
|
+
/** Cap on attribute names; values use the ordinary CSS/page-string cap. */
|
|
754
|
+
const ATTRIBUTE_NAME_MAX = 100;
|
|
726
755
|
/** Cap on how many grid template-track strings are kept per axis. */
|
|
727
756
|
const MAX_GRID_TRACKS = 64;
|
|
728
757
|
|
|
758
|
+
function capAttributes(attributes: readonly (readonly [string, string])[]): { readonly values: Readonly<Record<string, string>>; readonly truncated: number } {
|
|
759
|
+
const capped = capArray(attributes, MAX_ATTRIBUTES);
|
|
760
|
+
return {
|
|
761
|
+
values: Object.fromEntries(capped.items.map(([name, value]) => [
|
|
762
|
+
sanitizeString(name, { max: ATTRIBUTE_NAME_MAX }),
|
|
763
|
+
sanitizeString(value, { max: CSS_VALUE_MAX }),
|
|
764
|
+
])),
|
|
765
|
+
truncated: capped.truncated,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
729
769
|
function capTrackList(values: readonly string[]): { items: string[]; truncated: number } {
|
|
730
770
|
const { items, truncated } = capArray(values, MAX_GRID_TRACKS);
|
|
731
771
|
return { items: items.map((v) => sanitizeString(v, { max: CSS_VALUE_MAX })), truncated };
|
|
@@ -976,6 +1016,7 @@ export const collectGeometry: Collector = async (ctx) => {
|
|
|
976
1016
|
}
|
|
977
1017
|
|
|
978
1018
|
const geometryUnavailable = geometryUnavailableReason !== undefined;
|
|
1019
|
+
const capturedAttributes = capAttributes(fact.attributes ?? []);
|
|
979
1020
|
const rect = quads.length > 0 ? unionRect(quads) : { x: 0, y: 0, width: 0, height: 0 };
|
|
980
1021
|
// Only a REAL (non-throwing) quads read can strengthen zeroSize/visible
|
|
981
1022
|
// beyond the JS-side facts -- when the quads read itself never proved
|
|
@@ -988,6 +1029,8 @@ export const collectGeometry: Collector = async (ctx) => {
|
|
|
988
1029
|
id: `el-${fact.idx}`,
|
|
989
1030
|
tag: sanitizeString(fact.tag, { max: 64 }),
|
|
990
1031
|
selector: fact.selector !== undefined ? sanitizeString(fact.selector, { max: 300 }) : undefined,
|
|
1032
|
+
attributes: capturedAttributes.values,
|
|
1033
|
+
...(capturedAttributes.truncated ? { attributesTruncated: capturedAttributes.truncated } : {}),
|
|
991
1034
|
domPath: sanitizeString(fact.domPath, { max: 500 }),
|
|
992
1035
|
...resolvedIdentity(backendNodeId),
|
|
993
1036
|
text: fact.text !== undefined ? sanitizeString(fact.text, { max: 200 }) : undefined,
|
|
@@ -1012,6 +1055,20 @@ export const collectGeometry: Collector = async (ctx) => {
|
|
|
1012
1055
|
clippedFraction: fact.clipping.clippedFraction,
|
|
1013
1056
|
}
|
|
1014
1057
|
: null,
|
|
1058
|
+
...(fact.paint
|
|
1059
|
+
? {
|
|
1060
|
+
paint: {
|
|
1061
|
+
'box-shadow': sanitizeString(fact.paint['box-shadow']),
|
|
1062
|
+
'outline-width': sanitizeString(fact.paint['outline-width']),
|
|
1063
|
+
'outline-style': sanitizeString(fact.paint['outline-style']),
|
|
1064
|
+
'outline-offset': sanitizeString(fact.paint['outline-offset']),
|
|
1065
|
+
filter: sanitizeString(fact.paint.filter),
|
|
1066
|
+
transform: sanitizeString(fact.paint.transform),
|
|
1067
|
+
'clip-path': sanitizeString(fact.paint['clip-path']),
|
|
1068
|
+
'border-radius': sanitizeString(fact.paint['border-radius']),
|
|
1069
|
+
},
|
|
1070
|
+
}
|
|
1071
|
+
: {}),
|
|
1015
1072
|
layout: capLayoutFact(fact.layout),
|
|
1016
1073
|
...(geometryUnavailable ? { geometryUnavailable: true as const, geometryUnavailableReason } : {}),
|
|
1017
1074
|
...(fact.iframeContentUnavailable
|