@crouton-kit/capture 1.3.6 → 1.3.8
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 +3269 -1281
- package/package.json +4 -1
- package/src/capture.ts +30 -0
- package/src/cdp/args.ts +113 -10
- package/src/cdp/bridge/client.ts +49 -0
- package/src/cdp/bridge/protocol.ts +137 -0
- package/src/cdp/bridge/server.ts +241 -0
- package/src/cdp/bridge/spawn.ts +127 -0
- package/src/cdp/client.ts +7 -1
- package/src/cdp/commands/bridge-serve.ts +43 -0
- package/src/cdp/commands/cdp.ts +203 -0
- package/src/cdp/commands/measure/census.ts +31 -0
- package/src/cdp/commands/measure/check.ts +35 -0
- package/src/cdp/commands/measure/diff.ts +32 -0
- package/src/cdp/commands/measure/explain.ts +31 -0
- package/src/cdp/commands/measure/index.ts +114 -0
- package/src/cdp/commands/measure/map-focus.ts +24 -0
- package/src/cdp/commands/measure/map-layers.ts +25 -0
- package/src/cdp/commands/measure/map-scroll.ts +24 -0
- package/src/cdp/commands/measure/snap.ts +36 -0
- package/src/cdp/commands/measure/sweep.ts +30 -0
- package/src/cdp/commands/motion/index.ts +60 -0
- package/src/cdp/commands/motion/jank.ts +22 -0
- package/src/cdp/commands/motion/mask.ts +23 -0
- package/src/cdp/commands/motion/rec.ts +33 -0
- package/src/cdp/commands/motion/response.ts +26 -0
- package/src/cdp/commands/motion/timeline.ts +27 -0
- package/src/cdp/coordinates.ts +158 -0
- package/src/cdp/dispatch.ts +8 -0
- package/src/cdp/domains.ts +75 -0
- package/src/cdp/recorder-bridge.ts +521 -0
- package/src/cdp/source-map.ts +309 -0
- package/src/cdp/targets.ts +8 -1
- package/src/cdp/timing.ts +82 -0
- package/src/cdp/types.ts +71 -0
- package/src/cdp/usage.ts +9 -0
- package/src/output/artifact.ts +520 -0
- package/src/output/diff.ts +111 -0
- package/src/output/render.ts +593 -0
- package/src/output/selector.ts +185 -0
- package/src/session/artifacts.ts +229 -0
- package/src/session/commands.ts +183 -20
- package/src/session-context.ts +50 -8
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ParsedArgs } from '../../types.js';
|
|
2
|
+
import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
|
|
3
|
+
|
|
4
|
+
const USAGE = `Usage: capture measure map layers [url|snap]
|
|
5
|
+
|
|
6
|
+
Paint/compositor layer map recorded in a snapshot's layers.json: layers,
|
|
7
|
+
bounds, compositing reasons, paint order, per-node membership, and source
|
|
8
|
+
provenance for layer-affecting declarations. A URL target creates a snap
|
|
9
|
+
first.
|
|
10
|
+
|
|
11
|
+
Not yet implemented.`;
|
|
12
|
+
|
|
13
|
+
export async function cmdMeasureMapLayers(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
14
|
+
if (parsed.help) {
|
|
15
|
+
console.log(USAGE);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
const result: RenderableResult = {
|
|
19
|
+
tag: 'error',
|
|
20
|
+
attrs: { command: 'measure map layers', status: 'not_implemented' },
|
|
21
|
+
summary: fact`\`measure map layers\` is not implemented yet.`,
|
|
22
|
+
};
|
|
23
|
+
emitResult(result, { json: parsed.json });
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ParsedArgs } from '../../types.js';
|
|
2
|
+
import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
|
|
3
|
+
|
|
4
|
+
const USAGE = `Usage: capture measure map scroll [url|snap]
|
|
5
|
+
|
|
6
|
+
Scroll-container topology recorded in a snapshot's scroll.json: containers,
|
|
7
|
+
ranges, current/max offsets, sticky/fixed occupancy, snap points, and
|
|
8
|
+
visual/layout viewport facts. A URL target creates a snap first.
|
|
9
|
+
|
|
10
|
+
Not yet implemented.`;
|
|
11
|
+
|
|
12
|
+
export async function cmdMeasureMapScroll(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
13
|
+
if (parsed.help) {
|
|
14
|
+
console.log(USAGE);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
const result: RenderableResult = {
|
|
18
|
+
tag: 'error',
|
|
19
|
+
attrs: { command: 'measure map scroll', status: 'not_implemented' },
|
|
20
|
+
summary: fact`\`measure map scroll\` is not implemented yet.`,
|
|
21
|
+
};
|
|
22
|
+
emitResult(result, { json: parsed.json });
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ParsedArgs } from '../../types.js';
|
|
2
|
+
import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
|
|
3
|
+
|
|
4
|
+
const USAGE = `Usage: capture measure snap [url|snap] [--freeze-animations] [--settle-timeout <ms>] [--capture-unsettled] [--pixels] [--state <state[:selector]>]... [--viewport <WxH>]...
|
|
5
|
+
|
|
6
|
+
Drive the page (or re-capture states over a base snapshot) and write one
|
|
7
|
+
settled snapshot substrate directory: geometry, styles, hit-test, text,
|
|
8
|
+
forms, animation, ax, queries, focus, scroll, layers, and (with --pixels)
|
|
9
|
+
per-element crops. Every other measure/motion query leaf reads this
|
|
10
|
+
artifact instead of re-driving the browser.
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--freeze-animations Pause CSS/WAAPI animation before capture
|
|
14
|
+
--settle-timeout <ms> Override the default 5000ms settle wait
|
|
15
|
+
--capture-unsettled Write full substrate despite non-settlement,
|
|
16
|
+
marking unstable regions
|
|
17
|
+
--pixels Also write per-element raster crops
|
|
18
|
+
--state <state[:selector]> Force a pseudo-state or real control state
|
|
19
|
+
(repeatable)
|
|
20
|
+
--viewport <WxH> Viewport to snap at (repeatable)
|
|
21
|
+
|
|
22
|
+
Not yet implemented.`;
|
|
23
|
+
|
|
24
|
+
export async function cmdMeasureSnap(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
25
|
+
if (parsed.help) {
|
|
26
|
+
console.log(USAGE);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
const result: RenderableResult = {
|
|
30
|
+
tag: 'error',
|
|
31
|
+
attrs: { command: 'measure snap', status: 'not_implemented' },
|
|
32
|
+
summary: fact`\`measure snap\` is not implemented yet.`,
|
|
33
|
+
};
|
|
34
|
+
emitResult(result, { json: parsed.json });
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ParsedArgs } from '../../types.js';
|
|
2
|
+
import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
|
|
3
|
+
|
|
4
|
+
const USAGE = `Usage: capture measure sweep [url] --axis <axis> [--from <val>] [--to <val>] [--viewport-height <val>]
|
|
5
|
+
|
|
6
|
+
Responsive/environment sampling: repeated snap captures across an axis
|
|
7
|
+
(width, dpr, zoom, color-scheme, reduced-motion, ...), binary-searched to
|
|
8
|
+
find transition points and piecewise-stable ranges.
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--axis <axis> The sampled axis (default: width)
|
|
12
|
+
--from <val> Range start (axis-dependent: width px, dpr, zoom, ...)
|
|
13
|
+
--to <val> Range end (axis-dependent)
|
|
14
|
+
--viewport-height <val> Fixed viewport height while sweeping width/dpr/zoom
|
|
15
|
+
|
|
16
|
+
Not yet implemented.`;
|
|
17
|
+
|
|
18
|
+
export async function cmdMeasureSweep(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
19
|
+
if (parsed.help) {
|
|
20
|
+
console.log(USAGE);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const result: RenderableResult = {
|
|
24
|
+
tag: 'error',
|
|
25
|
+
attrs: { command: 'measure sweep', status: 'not_implemented' },
|
|
26
|
+
summary: fact`\`measure sweep\` is not implemented yet.`,
|
|
27
|
+
};
|
|
28
|
+
emitResult(result, { json: parsed.json });
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `capture motion` branch router.
|
|
3
|
+
*
|
|
4
|
+
* Shifts the consumed leaf token out of `parsed.positional` before handing
|
|
5
|
+
* off, so every leaf command still finds its own primary target (a URL, a
|
|
6
|
+
* recording id, ...) at `positional[0]` exactly as every other capture
|
|
7
|
+
* command does.
|
|
8
|
+
*/
|
|
9
|
+
import { type ParsedArgs } from '../../types.js';
|
|
10
|
+
import { cmdMotionRec } from './rec.js';
|
|
11
|
+
import { cmdMotionMask } from './mask.js';
|
|
12
|
+
import { cmdMotionTimeline } from './timeline.js';
|
|
13
|
+
import { cmdMotionJank } from './jank.js';
|
|
14
|
+
import { cmdMotionResponse } from './response.js';
|
|
15
|
+
|
|
16
|
+
export const MOTION_USAGE = `capture motion — recorder lifecycle + read-only queries over a finalized recording.
|
|
17
|
+
|
|
18
|
+
\`rec\` drives (and records) the browser, one-shot or composed across
|
|
19
|
+
intervening commands; every other leaf below is a cheap read over the
|
|
20
|
+
finalized recording artifact.
|
|
21
|
+
|
|
22
|
+
Leaves:
|
|
23
|
+
rec [url] --do <action> [--duration <ms>] One-shot: drive one action, record it
|
|
24
|
+
rec --start Composed: arm the recorder (needs an active session)
|
|
25
|
+
rec --stop Composed: finalize the recorder
|
|
26
|
+
|
|
27
|
+
mask <rec> Motion-diff composite image + per-region facts
|
|
28
|
+
timeline <rec> --element <sel> [--prop <prop>]
|
|
29
|
+
Per-frame geometry/scroll/property timeline
|
|
30
|
+
jank <rec> Dropped-frame/long-task/layout-shift facts
|
|
31
|
+
response <rec> [--action <action>] Input-to-settled response timeline
|
|
32
|
+
|
|
33
|
+
Every leaf defaults to rendered prose; --json mirrors the same result as JSON.
|
|
34
|
+
Exit codes: 0 always — findings are a report, not a failure. No leaf accepts --gate.
|
|
35
|
+
|
|
36
|
+
capture motion <leaf> --help Per-leaf usage`;
|
|
37
|
+
|
|
38
|
+
export async function motionMain(parsed: ParsedArgs, args: string[]): Promise<void> {
|
|
39
|
+
const leaf = parsed.positional[0];
|
|
40
|
+
const rest: ParsedArgs = { ...parsed, positional: parsed.positional.slice(1) };
|
|
41
|
+
|
|
42
|
+
switch (leaf) {
|
|
43
|
+
case 'rec':
|
|
44
|
+
return cmdMotionRec(rest, args);
|
|
45
|
+
case 'mask':
|
|
46
|
+
return cmdMotionMask(rest, args);
|
|
47
|
+
case 'timeline':
|
|
48
|
+
return cmdMotionTimeline(rest, args);
|
|
49
|
+
case 'jank':
|
|
50
|
+
return cmdMotionJank(rest, args);
|
|
51
|
+
case 'response':
|
|
52
|
+
return cmdMotionResponse(rest, args);
|
|
53
|
+
case undefined:
|
|
54
|
+
console.log(MOTION_USAGE);
|
|
55
|
+
return;
|
|
56
|
+
default:
|
|
57
|
+
console.error(`Unknown motion leaf: ${leaf}\n\n${MOTION_USAGE}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ParsedArgs } from '../../types.js';
|
|
2
|
+
import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
|
|
3
|
+
|
|
4
|
+
const USAGE = `Usage: capture motion jank <rec>
|
|
5
|
+
|
|
6
|
+
Dropped-frame/long-task/layout-shift facts over a finalized recording.
|
|
7
|
+
|
|
8
|
+
Not yet implemented.`;
|
|
9
|
+
|
|
10
|
+
export async function cmdMotionJank(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
11
|
+
if (parsed.help) {
|
|
12
|
+
console.log(USAGE);
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
const result: RenderableResult = {
|
|
16
|
+
tag: 'error',
|
|
17
|
+
attrs: { command: 'motion jank', status: 'not_implemented' },
|
|
18
|
+
summary: fact`\`motion jank\` is not implemented yet.`,
|
|
19
|
+
};
|
|
20
|
+
emitResult(result, { json: parsed.json });
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ParsedArgs } from '../../types.js';
|
|
2
|
+
import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
|
|
3
|
+
|
|
4
|
+
const USAGE = `Usage: capture motion mask <rec>
|
|
5
|
+
|
|
6
|
+
Motion-diff composite image over a finalized recording, plus per-region
|
|
7
|
+
area/distance/velocity facts and element attribution where resolvable.
|
|
8
|
+
|
|
9
|
+
Not yet implemented.`;
|
|
10
|
+
|
|
11
|
+
export async function cmdMotionMask(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
12
|
+
if (parsed.help) {
|
|
13
|
+
console.log(USAGE);
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
const result: RenderableResult = {
|
|
17
|
+
tag: 'error',
|
|
18
|
+
attrs: { command: 'motion mask', status: 'not_implemented' },
|
|
19
|
+
summary: fact`\`motion mask\` is not implemented yet.`,
|
|
20
|
+
};
|
|
21
|
+
emitResult(result, { json: parsed.json });
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ParsedArgs } from '../../types.js';
|
|
2
|
+
import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
|
|
3
|
+
|
|
4
|
+
const USAGE = `Usage: capture motion rec [url] --do <action> [--duration <ms>]
|
|
5
|
+
capture motion rec --start
|
|
6
|
+
capture motion rec --stop [--rec-id <id>]
|
|
7
|
+
|
|
8
|
+
One-shot (\`--do\`): drives one scripted action and records it end to end.
|
|
9
|
+
Composed (\`--start\` ... intervening commands ... \`--stop\`): records
|
|
10
|
+
whatever the active session does across multiple independent commands.
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--do <action> One-shot action to drive and record (e.g. click:SEL)
|
|
14
|
+
--duration <ms> Recording duration override
|
|
15
|
+
--start Arm the composed recorder (requires an active session)
|
|
16
|
+
--stop Finalize the composed recorder
|
|
17
|
+
--rec-id <id> Explicit recording id (default: the session's active recording)
|
|
18
|
+
|
|
19
|
+
Not yet implemented.`;
|
|
20
|
+
|
|
21
|
+
export async function cmdMotionRec(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
22
|
+
if (parsed.help) {
|
|
23
|
+
console.log(USAGE);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
const result: RenderableResult = {
|
|
27
|
+
tag: 'error',
|
|
28
|
+
attrs: { command: 'motion rec', status: 'not_implemented' },
|
|
29
|
+
summary: fact`\`motion rec\` is not implemented yet.`,
|
|
30
|
+
};
|
|
31
|
+
emitResult(result, { json: parsed.json });
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ParsedArgs } from '../../types.js';
|
|
2
|
+
import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
|
|
3
|
+
|
|
4
|
+
const USAGE = `Usage: capture motion response <rec> [--action <action>]
|
|
5
|
+
|
|
6
|
+
Input-to-settled response timeline over a finalized recording:
|
|
7
|
+
input -> mutation -> layout -> paint -> network -> settle.
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--action <action> Narrow to one recorded action
|
|
11
|
+
|
|
12
|
+
Not yet implemented.`;
|
|
13
|
+
|
|
14
|
+
export async function cmdMotionResponse(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
15
|
+
if (parsed.help) {
|
|
16
|
+
console.log(USAGE);
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
const result: RenderableResult = {
|
|
20
|
+
tag: 'error',
|
|
21
|
+
attrs: { command: 'motion response', status: 'not_implemented' },
|
|
22
|
+
summary: fact`\`motion response\` is not implemented yet.`,
|
|
23
|
+
};
|
|
24
|
+
emitResult(result, { json: parsed.json });
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ParsedArgs } from '../../types.js';
|
|
2
|
+
import { emitResult, fact, type RenderableResult } from '../../../output/render.js';
|
|
3
|
+
|
|
4
|
+
const USAGE = `Usage: capture motion timeline <rec> --element <sel> [--prop <prop>]
|
|
5
|
+
|
|
6
|
+
Per-frame geometry/scroll/property values for one element across a
|
|
7
|
+
finalized recording.
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--element <sel> Element selector to track (required)
|
|
11
|
+
--prop <prop> Additional CSS/computed property to sample per frame
|
|
12
|
+
|
|
13
|
+
Not yet implemented.`;
|
|
14
|
+
|
|
15
|
+
export async function cmdMotionTimeline(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
16
|
+
if (parsed.help) {
|
|
17
|
+
console.log(USAGE);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
const result: RenderableResult = {
|
|
21
|
+
tag: 'error',
|
|
22
|
+
attrs: { command: 'motion timeline', status: 'not_implemented' },
|
|
23
|
+
summary: fact`\`motion timeline\` is not implemented yet.`,
|
|
24
|
+
};
|
|
25
|
+
emitResult(result, { json: parsed.json });
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared quad math and frame/shadow coordinate stitching helpers for `measure snap`.
|
|
3
|
+
*
|
|
4
|
+
* These helpers are pure (`axisAlignedRectFromQuad`, `composeFrameTransform`,
|
|
5
|
+
* `toTopViewportQuad`) except `getContentQuadBox`, which calls the existing `CDPClient.send`
|
|
6
|
+
* directly (no new client abstraction). Quad helpers always preserve the original four
|
|
7
|
+
* points from CDP; an axis-aligned rect is only ever a derived convenience alongside them,
|
|
8
|
+
* never a replacement — several instruments (raster crops, hit-test, motion masks) need the
|
|
9
|
+
* real quad for transformed/rotated/wrapped elements.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CDPClient } from './client.js';
|
|
13
|
+
|
|
14
|
+
/** Four corner points clockwise from top-left, in a single local frame's coordinate space: [x1,y1,x2,y2,x3,y3,x4,y4]. */
|
|
15
|
+
export type Quad = [number, number, number, number, number, number, number, number];
|
|
16
|
+
|
|
17
|
+
export interface Rect {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BoxModelQuads {
|
|
25
|
+
content: Quad;
|
|
26
|
+
padding: Quad;
|
|
27
|
+
border: Quad;
|
|
28
|
+
margin: Quad;
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ContentQuadBox {
|
|
34
|
+
/**
|
|
35
|
+
* One quad per DOM fragment. Most elements produce exactly one; an inline element wrapped
|
|
36
|
+
* across lines (or an element split by column/fragmentation) produces more than one, and
|
|
37
|
+
* every fragment is preserved rather than collapsed into a single bounding rect.
|
|
38
|
+
*/
|
|
39
|
+
quads: Quad[];
|
|
40
|
+
/** `DOM.getBoxModel`'s content/padding/border/margin quads plus width/height, in the same local coordinate space as `quads`. */
|
|
41
|
+
boxModel: BoxModelQuads;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Identifies a DOM node the way CDP's `DOM.getContentQuads`/`DOM.getBoxModel` accept it. */
|
|
45
|
+
export type NodeRef = { backendNodeId: number } | { nodeId: number } | { objectId: string };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reads both the content quad(s) and the full box model (content/padding/border/margin) for
|
|
49
|
+
* one node via `DOM.getContentQuads` and `DOM.getBoxModel`. Coordinates are in the node's own
|
|
50
|
+
* frame's local viewport space — use `composeFrameTransform`/`toTopViewportQuad` to stitch
|
|
51
|
+
* iframe-nested coordinates into top-viewport space.
|
|
52
|
+
*/
|
|
53
|
+
export async function getContentQuadBox(client: CDPClient, ref: NodeRef): Promise<ContentQuadBox> {
|
|
54
|
+
const [quadsResult, boxResult] = await Promise.all([
|
|
55
|
+
client.send('DOM.getContentQuads', ref as unknown as Record<string, unknown>) as Promise<{
|
|
56
|
+
quads: number[][];
|
|
57
|
+
}>,
|
|
58
|
+
client.send('DOM.getBoxModel', ref as unknown as Record<string, unknown>) as Promise<{
|
|
59
|
+
model: {
|
|
60
|
+
content: number[];
|
|
61
|
+
padding: number[];
|
|
62
|
+
border: number[];
|
|
63
|
+
margin: number[];
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
};
|
|
67
|
+
}>,
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const model = boxResult.model;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
quads: quadsResult.quads.map(asQuad),
|
|
74
|
+
boxModel: {
|
|
75
|
+
content: asQuad(model.content),
|
|
76
|
+
padding: asQuad(model.padding),
|
|
77
|
+
border: asQuad(model.border),
|
|
78
|
+
margin: asQuad(model.margin),
|
|
79
|
+
width: model.width,
|
|
80
|
+
height: model.height,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function asQuad(points: number[]): Quad {
|
|
86
|
+
if (points.length !== 8) {
|
|
87
|
+
throw new Error(`Expected an 8-number quad (x1,y1,x2,y2,x3,y3,x4,y4), got ${points.length} numbers`);
|
|
88
|
+
}
|
|
89
|
+
return points as Quad;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Derives the axis-aligned bounding rect of a quad. Purely a convenience — `quad` itself stays the source of truth. */
|
|
93
|
+
export function axisAlignedRectFromQuad(quad: Quad): Rect {
|
|
94
|
+
const xs = [quad[0], quad[2], quad[4], quad[6]];
|
|
95
|
+
const ys = [quad[1], quad[3], quad[5], quad[7]];
|
|
96
|
+
const minX = Math.min(...xs);
|
|
97
|
+
const maxX = Math.max(...xs);
|
|
98
|
+
const minY = Math.min(...ys);
|
|
99
|
+
const maxY = Math.max(...ys);
|
|
100
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One nested frame's offset: the translation (and optional scale, e.g. CSS zoom) of a child
|
|
105
|
+
* frame's content-box origin within its parent frame's local coordinate space.
|
|
106
|
+
*/
|
|
107
|
+
export interface FrameOffset {
|
|
108
|
+
dx: number;
|
|
109
|
+
dy: number;
|
|
110
|
+
/** Defaults to 1 (no scale) when omitted. */
|
|
111
|
+
scaleX?: number;
|
|
112
|
+
scaleY?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface FrameTransform {
|
|
116
|
+
dx: number;
|
|
117
|
+
dy: number;
|
|
118
|
+
scaleX: number;
|
|
119
|
+
scaleY: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Composes a chain of nested iframe offsets into one affine transform from the innermost
|
|
124
|
+
* (target) frame's local coordinates to top-viewport coordinates. `chain` is ordered from the
|
|
125
|
+
* outermost transition first (the top frame's offset into its direct child) to the innermost
|
|
126
|
+
* transition last (the target's immediate parent frame's offset into the target frame).
|
|
127
|
+
*
|
|
128
|
+
* Shadow DOM does not need this — shadow-root content shares its host element's coordinate
|
|
129
|
+
* space, so shadow-root traversal only affects which node a query resolves to, not its quad.
|
|
130
|
+
*/
|
|
131
|
+
export function composeFrameTransform(chain: FrameOffset[]): FrameTransform {
|
|
132
|
+
let transform: FrameTransform = { dx: 0, dy: 0, scaleX: 1, scaleY: 1 };
|
|
133
|
+
for (const offset of chain) {
|
|
134
|
+
const scaleX = offset.scaleX ?? 1;
|
|
135
|
+
const scaleY = offset.scaleY ?? 1;
|
|
136
|
+
transform = {
|
|
137
|
+
dx: transform.dx + offset.dx * transform.scaleX,
|
|
138
|
+
dy: transform.dy + offset.dy * transform.scaleY,
|
|
139
|
+
scaleX: transform.scaleX * scaleX,
|
|
140
|
+
scaleY: transform.scaleY * scaleY,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return transform;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Applies a composed `FrameTransform` to every point of a quad, mapping it into top-viewport space. */
|
|
147
|
+
export function toTopViewportQuad(quad: Quad, transform: FrameTransform): Quad {
|
|
148
|
+
return [
|
|
149
|
+
quad[0] * transform.scaleX + transform.dx,
|
|
150
|
+
quad[1] * transform.scaleY + transform.dy,
|
|
151
|
+
quad[2] * transform.scaleX + transform.dx,
|
|
152
|
+
quad[3] * transform.scaleY + transform.dy,
|
|
153
|
+
quad[4] * transform.scaleX + transform.dx,
|
|
154
|
+
quad[5] * transform.scaleY + transform.dy,
|
|
155
|
+
quad[6] * transform.scaleX + transform.dx,
|
|
156
|
+
quad[7] * transform.scaleY + transform.dy,
|
|
157
|
+
];
|
|
158
|
+
}
|
package/src/cdp/dispatch.ts
CHANGED
|
@@ -6,6 +6,10 @@ import { cmdScreenshot, cmdClick, cmdType, cmdA11y } from './commands/ui.js';
|
|
|
6
6
|
import { cmdRecord, cmdNavigate, cmdNetwork } from './commands/traffic.js';
|
|
7
7
|
import { cmdHar } from './commands/har.js';
|
|
8
8
|
import { cmdLib } from './commands/lib.js';
|
|
9
|
+
import { cmdCdp } from './commands/cdp.js';
|
|
10
|
+
import { cmdBridgeServe } from './commands/bridge-serve.js';
|
|
11
|
+
import { measureMain } from './commands/measure/index.js';
|
|
12
|
+
import { motionMain } from './commands/motion/index.js';
|
|
9
13
|
|
|
10
14
|
export async function cdpMain(): Promise<void> {
|
|
11
15
|
const args = process.argv.slice(2);
|
|
@@ -26,6 +30,10 @@ export async function cdpMain(): Promise<void> {
|
|
|
26
30
|
case 'network': return cmdNetwork(parsed, args);
|
|
27
31
|
case 'har': return cmdHar(parsed, args);
|
|
28
32
|
case 'lib': return cmdLib(parsed, args);
|
|
33
|
+
case 'cdp': return cmdCdp(parsed, args);
|
|
34
|
+
case 'measure': return measureMain(parsed, args);
|
|
35
|
+
case 'motion': return motionMain(parsed, args);
|
|
36
|
+
case '__bridge-serve': return cmdBridgeServe(parsed, args);
|
|
29
37
|
default:
|
|
30
38
|
console.log(CDP_USAGE);
|
|
31
39
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CDP domain enablement and reversible state-forcing helpers for `measure`/`motion`.
|
|
3
|
+
*
|
|
4
|
+
* Every helper here takes an already-connected `CDPClient` (see ./client.ts) and calls
|
|
5
|
+
* `client.send(...)` directly — there is no separate client abstraction. Domain `.enable`
|
|
6
|
+
* calls are idempotent in CDP, so callers may call these more than once per connection
|
|
7
|
+
* (e.g. once per collector) without ill effect.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CDPClient } from './client.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Enables the CDP domains the `measure snap` substrate collectors need: frame/navigation
|
|
14
|
+
* metadata (Page), element geometry and tree access (DOM), computed style + winning-rule
|
|
15
|
+
* provenance (CSS — requires DOM.enable first), the accessibility tree (Accessibility),
|
|
16
|
+
* compositor layer facts (LayerTree), the animation inventory (Animation), and script
|
|
17
|
+
* execution for hit-test/text/form introspection (Runtime).
|
|
18
|
+
*/
|
|
19
|
+
export async function enableDomainsForSnap(client: CDPClient): Promise<void> {
|
|
20
|
+
await client.send('Page.enable');
|
|
21
|
+
await client.send('DOM.enable');
|
|
22
|
+
await client.send('CSS.enable');
|
|
23
|
+
await Promise.all([
|
|
24
|
+
client.send('Accessibility.enable'),
|
|
25
|
+
client.send('LayerTree.enable'),
|
|
26
|
+
client.send('Animation.enable'),
|
|
27
|
+
client.send('Runtime.enable'),
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Enables the CDP domains the motion recorder bridge needs: frame/navigation metadata and
|
|
33
|
+
* `Page.startScreencast` (Page), element geometry for rect/selector resolution (DOM), script
|
|
34
|
+
* execution for the injected Mutation/Resize/Performance observers (Runtime), request/response
|
|
35
|
+
* timing for `motion response` (Network), and playback-rate control for the animation inventory
|
|
36
|
+
* (Animation). `Tracing.start` is configured with its own category set by the recorder bridge
|
|
37
|
+
* (U13) and is not a plain `.enable` call, so it is not included here.
|
|
38
|
+
*/
|
|
39
|
+
export async function enableDomainsForMotionRec(client: CDPClient): Promise<void> {
|
|
40
|
+
await client.send('Page.enable');
|
|
41
|
+
await client.send('DOM.enable');
|
|
42
|
+
await Promise.all([
|
|
43
|
+
client.send('Runtime.enable'),
|
|
44
|
+
client.send('Network.enable'),
|
|
45
|
+
client.send('Animation.enable'),
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* CSS pseudo-classes CDP can force directly via `CSS.forcePseudoState`. Real control states
|
|
51
|
+
* (checked/open/disabled/invalid) are not pseudo-classes in this sense — collectors toggle
|
|
52
|
+
* those with reversible DOM property writes and restore them after capture instead of using
|
|
53
|
+
* this helper.
|
|
54
|
+
*/
|
|
55
|
+
export type ForcedPseudoClass =
|
|
56
|
+
| 'active'
|
|
57
|
+
| 'focus'
|
|
58
|
+
| 'focus-visible'
|
|
59
|
+
| 'focus-within'
|
|
60
|
+
| 'hover'
|
|
61
|
+
| 'target'
|
|
62
|
+
| 'visited';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Forces (or, called with an empty array, clears) a set of CSS pseudo-classes on a node.
|
|
66
|
+
* Requires `CSS.enable` (see `enableDomainsForSnap`). The caller is responsible for restoring
|
|
67
|
+
* state after capture by calling this again with `[]`.
|
|
68
|
+
*/
|
|
69
|
+
export async function forcePseudoStateForNode(
|
|
70
|
+
client: CDPClient,
|
|
71
|
+
nodeId: number,
|
|
72
|
+
forcedPseudoClasses: ForcedPseudoClass[],
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
await client.send('CSS.forcePseudoState', { nodeId, forcedPseudoClasses });
|
|
75
|
+
}
|