@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.
Files changed (43) hide show
  1. package/bin/capture +3269 -1281
  2. package/package.json +4 -1
  3. package/src/capture.ts +30 -0
  4. package/src/cdp/args.ts +113 -10
  5. package/src/cdp/bridge/client.ts +49 -0
  6. package/src/cdp/bridge/protocol.ts +137 -0
  7. package/src/cdp/bridge/server.ts +241 -0
  8. package/src/cdp/bridge/spawn.ts +127 -0
  9. package/src/cdp/client.ts +7 -1
  10. package/src/cdp/commands/bridge-serve.ts +43 -0
  11. package/src/cdp/commands/cdp.ts +203 -0
  12. package/src/cdp/commands/measure/census.ts +31 -0
  13. package/src/cdp/commands/measure/check.ts +35 -0
  14. package/src/cdp/commands/measure/diff.ts +32 -0
  15. package/src/cdp/commands/measure/explain.ts +31 -0
  16. package/src/cdp/commands/measure/index.ts +114 -0
  17. package/src/cdp/commands/measure/map-focus.ts +24 -0
  18. package/src/cdp/commands/measure/map-layers.ts +25 -0
  19. package/src/cdp/commands/measure/map-scroll.ts +24 -0
  20. package/src/cdp/commands/measure/snap.ts +36 -0
  21. package/src/cdp/commands/measure/sweep.ts +30 -0
  22. package/src/cdp/commands/motion/index.ts +60 -0
  23. package/src/cdp/commands/motion/jank.ts +22 -0
  24. package/src/cdp/commands/motion/mask.ts +23 -0
  25. package/src/cdp/commands/motion/rec.ts +33 -0
  26. package/src/cdp/commands/motion/response.ts +26 -0
  27. package/src/cdp/commands/motion/timeline.ts +27 -0
  28. package/src/cdp/coordinates.ts +158 -0
  29. package/src/cdp/dispatch.ts +8 -0
  30. package/src/cdp/domains.ts +75 -0
  31. package/src/cdp/recorder-bridge.ts +521 -0
  32. package/src/cdp/source-map.ts +309 -0
  33. package/src/cdp/targets.ts +8 -1
  34. package/src/cdp/timing.ts +82 -0
  35. package/src/cdp/types.ts +71 -0
  36. package/src/cdp/usage.ts +9 -0
  37. package/src/output/artifact.ts +520 -0
  38. package/src/output/diff.ts +111 -0
  39. package/src/output/render.ts +593 -0
  40. package/src/output/selector.ts +185 -0
  41. package/src/session/artifacts.ts +229 -0
  42. package/src/session/commands.ts +183 -20
  43. 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
+ }
@@ -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
+ }