@crouton-kit/capture 1.3.7 → 1.3.9
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 +32786 -9989
- package/package.json +9 -3
- package/src/capture.ts +86 -163
- package/src/cdp/a11y.ts +113 -14
- package/src/cdp/args.ts +300 -116
- package/src/cdp/bridge/protocol.ts +187 -1
- package/src/cdp/bridge/server.ts +211 -84
- package/src/cdp/bridge/spawn.ts +110 -1
- package/src/cdp/client.ts +13 -3
- package/src/cdp/commands/bridge-serve.ts +35 -4
- package/src/cdp/commands/cdp.ts +266 -69
- package/src/cdp/commands/gate-guard.ts +43 -0
- package/src/cdp/commands/lib.ts +339 -213
- package/src/cdp/commands/measure/census.ts +87 -0
- package/src/cdp/commands/measure/check.ts +109 -0
- package/src/cdp/commands/measure/diff.ts +120 -0
- package/src/cdp/commands/measure/explain.ts +133 -0
- package/src/cdp/commands/measure/index.ts +106 -0
- package/src/cdp/commands/measure/map-ax.ts +52 -0
- package/src/cdp/commands/measure/map-focus.ts +52 -0
- package/src/cdp/commands/measure/map-layers.ts +59 -0
- package/src/cdp/commands/measure/map-scroll.ts +94 -0
- package/src/cdp/commands/measure/snap.ts +416 -0
- package/src/cdp/commands/measure/sweep.ts +227 -0
- package/src/cdp/commands/motion/index.ts +61 -0
- package/src/cdp/commands/motion/jank.ts +132 -0
- package/src/cdp/commands/motion/mask.ts +92 -0
- package/src/cdp/commands/motion/rec.ts +534 -0
- package/src/cdp/commands/motion/response.ts +76 -0
- package/src/cdp/commands/motion/timeline.ts +152 -0
- package/src/cdp/commands/page/click.ts +194 -0
- package/src/cdp/commands/page/elements.ts +154 -0
- package/src/cdp/commands/page/exec.ts +317 -0
- package/src/cdp/commands/page/index.ts +72 -0
- package/src/cdp/commands/page/navigate.ts +330 -0
- package/src/cdp/commands/page/scroll.ts +107 -0
- package/src/cdp/commands/page/shot.ts +208 -0
- package/src/cdp/commands/page/type.ts +104 -0
- package/src/cdp/commands/tab/index.ts +59 -0
- package/src/cdp/commands/tab/list.ts +167 -0
- package/src/cdp/commands/tab/network.ts +162 -0
- package/src/cdp/commands/tab/open.ts +90 -0
- package/src/cdp/commands/tab/reset.ts +160 -0
- package/src/cdp/connection.ts +368 -37
- package/src/cdp/coordinates.ts +158 -0
- package/src/cdp/detect.ts +42 -13
- package/src/cdp/dispatch.ts +76 -26
- package/src/cdp/domains.ts +75 -0
- package/src/cdp/har-recorder.ts +648 -115
- package/src/cdp/leaf-grammar.ts +99 -0
- package/src/cdp/measure/census.ts +205 -0
- package/src/cdp/measure/check.ts +318 -0
- package/src/cdp/measure/collectors/animation.ts +385 -0
- package/src/cdp/measure/collectors/ax.ts +278 -0
- package/src/cdp/measure/collectors/focus.ts +1041 -0
- package/src/cdp/measure/collectors/forms.ts +905 -0
- package/src/cdp/measure/collectors/geometry.ts +1038 -0
- package/src/cdp/measure/collectors/hittest.ts +835 -0
- package/src/cdp/measure/collectors/layers.ts +697 -0
- package/src/cdp/measure/collectors/media.ts +522 -0
- package/src/cdp/measure/collectors/pixels.ts +1584 -0
- package/src/cdp/measure/collectors/queries.ts +464 -0
- package/src/cdp/measure/collectors/scroll.ts +599 -0
- package/src/cdp/measure/collectors/states.ts +992 -0
- package/src/cdp/measure/collectors/style-provenance.ts +643 -0
- package/src/cdp/measure/collectors/styles.ts +355 -0
- package/src/cdp/measure/collectors/text.ts +713 -0
- package/src/cdp/measure/diff.ts +470 -0
- package/src/cdp/measure/explain.ts +498 -0
- package/src/cdp/measure/map-ax.ts +633 -0
- package/src/cdp/measure/map-focus.ts +188 -0
- package/src/cdp/measure/map-layers.ts +177 -0
- package/src/cdp/measure/map-scroll.ts +317 -0
- package/src/cdp/measure/redaction.ts +51 -0
- package/src/cdp/measure/settle.ts +1051 -0
- package/src/cdp/measure/snapshot.ts +390 -0
- package/src/cdp/measure/sweep.ts +330 -0
- package/src/cdp/measure/types.ts +220 -0
- package/src/cdp/measure/v2/collectors.ts +19 -0
- package/src/cdp/measure/v2/orchestrator.ts +46 -0
- package/src/cdp/measure/v2/snap-selection.ts +33 -0
- package/src/cdp/measure/v2/target-lock.ts +15 -0
- package/src/cdp/measure/v2/target.ts +16 -0
- package/src/cdp/motion/jank.ts +433 -0
- package/src/cdp/motion/mask.ts +421 -0
- package/src/cdp/motion/recorder.ts +764 -0
- package/src/cdp/motion/response.ts +356 -0
- package/src/cdp/motion/timeline.ts +215 -0
- package/src/cdp/record.ts +5 -212
- package/src/cdp/recorder-bridge.ts +1894 -0
- package/src/cdp/recorder-client.ts +326 -0
- package/src/cdp/scope-lock.ts +73 -0
- package/src/cdp/screenshot.ts +199 -66
- package/src/cdp/source-map.ts +309 -0
- package/src/cdp/targets.ts +21 -1
- package/src/cdp/timing.ts +87 -0
- package/src/cdp/types.ts +87 -6
- package/src/cdp/viewport.ts +21 -0
- package/src/cdp.ts +12 -58
- package/src/command-help.ts +29 -0
- package/src/command-resolution.ts +22 -0
- package/src/command-walker.ts +35 -0
- package/src/contracts/a11y.ts +33 -0
- package/src/contracts/cursor.ts +35 -0
- package/src/contracts/index.ts +15 -0
- package/src/contracts/motion.ts +19 -0
- package/src/contracts/neutral.ts +56 -0
- package/src/contracts/primitives.ts +205 -0
- package/src/contracts/release.ts +17 -0
- package/src/contracts/results.ts +208 -0
- package/src/contracts/routes.ts +140 -0
- package/src/contracts/snapshot.ts +92 -0
- package/src/cursor/index.ts +13 -0
- package/src/cursor/service.ts +310 -0
- package/src/errors.ts +71 -0
- package/src/har-manager.ts +992 -41
- package/src/interact.ts +410 -98
- package/src/output/artifact-lifecycle.ts +371 -0
- package/src/output/artifact.ts +526 -0
- package/src/output/diff.ts +111 -0
- package/src/output/exact-raw.ts +114 -0
- package/src/output/render.ts +987 -0
- package/src/output/selector.ts +185 -0
- package/src/output/structured.ts +211 -0
- package/src/registry.ts +208 -0
- package/src/session/artifacts.ts +481 -0
- package/src/session/commands.ts +1237 -368
- package/src/session/coordinator.ts +251 -0
- package/src/session/har-filter.ts +57 -0
- package/src/session/log-tailer.ts +653 -0
- package/src/session-context.ts +302 -34
- package/src/vault/bundle.ts +32 -17
- package/src/cdp/commands/exec.ts +0 -105
- package/src/cdp/commands/har.ts +0 -128
- package/src/cdp/commands/tabs.ts +0 -183
- package/src/cdp/commands/traffic.ts +0 -121
- package/src/cdp/commands/ui.ts +0 -172
- package/src/cdp/execute.ts +0 -113
- package/src/cdp/har-output.ts +0 -47
- package/src/cdp/usage.ts +0 -77
- package/src/tab-session.ts +0 -91
|
@@ -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/detect.ts
CHANGED
|
@@ -118,6 +118,37 @@ function getLocalhostListeningPorts(): number[] {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function getListeningProcessNames(): Map<number, string> {
|
|
122
|
+
const names = new Map<number, string>();
|
|
123
|
+
try {
|
|
124
|
+
const output = execSync('lsof -nP -iTCP -sTCP:LISTEN -Fpcn 2>/dev/null', {
|
|
125
|
+
encoding: 'utf-8',
|
|
126
|
+
});
|
|
127
|
+
let command: string | null = null;
|
|
128
|
+
for (const line of output.split('\n')) {
|
|
129
|
+
if (line.startsWith('c')) {
|
|
130
|
+
command = line.slice(1);
|
|
131
|
+
} else if (command && line.startsWith('n')) {
|
|
132
|
+
const match = line.match(/:(\d+)(?:\s|$)/);
|
|
133
|
+
if (match) names.set(Number(match[1]), command);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// Process identity is an optional refinement; CDP probing still works.
|
|
138
|
+
}
|
|
139
|
+
return names;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function identifyBrowserBundleId(browser: string, processName?: string): string {
|
|
143
|
+
// A listening process is stronger identity than Chromium's generic
|
|
144
|
+
// Browser/User-Agent strings: Arc and Spotify both report as "Chrome".
|
|
145
|
+
const identity = (processName ?? browser).toLowerCase();
|
|
146
|
+
for (const [bundleId, patterns] of Object.entries(BROWSER_PATTERNS)) {
|
|
147
|
+
if (patterns.some((pattern) => identity.includes(pattern))) return bundleId;
|
|
148
|
+
}
|
|
149
|
+
return 'unknown';
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
export interface CdpEndpoint {
|
|
122
153
|
port: number;
|
|
123
154
|
app: string;
|
|
@@ -128,25 +159,18 @@ export interface CdpEndpoint {
|
|
|
128
159
|
|
|
129
160
|
export async function detectCdpPortsAsync(): Promise<CdpEndpoint[]> {
|
|
130
161
|
const ports = getLocalhostListeningPorts();
|
|
162
|
+
const processNames = getListeningProcessNames();
|
|
131
163
|
const results: CdpEndpoint[] = [];
|
|
132
164
|
|
|
133
165
|
// Probe ports in parallel for speed
|
|
134
166
|
const probes = ports.map(async (port) => {
|
|
135
167
|
const probe = await probeCdpPort(port);
|
|
136
168
|
if (probe) {
|
|
137
|
-
|
|
138
|
-
const lower = probe.browser.toLowerCase();
|
|
139
|
-
let bundleId = 'unknown';
|
|
140
|
-
for (const [bid, patterns] of Object.entries(BROWSER_PATTERNS)) {
|
|
141
|
-
if (patterns.some((p) => lower.includes(p))) {
|
|
142
|
-
bundleId = bid;
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
169
|
+
const processName = processNames.get(port);
|
|
146
170
|
results.push({
|
|
147
171
|
port,
|
|
148
|
-
app: probe.app,
|
|
149
|
-
bundleId,
|
|
172
|
+
app: processName ?? probe.app,
|
|
173
|
+
bundleId: identifyBrowserBundleId(probe.browser, processName),
|
|
150
174
|
isElectron: probe.isElectron,
|
|
151
175
|
hasPageTarget: probe.hasPageTarget,
|
|
152
176
|
});
|
|
@@ -179,8 +203,13 @@ export function pickPreferredEndpoint(
|
|
|
179
203
|
if (match) return match;
|
|
180
204
|
}
|
|
181
205
|
|
|
182
|
-
//
|
|
183
|
-
|
|
206
|
+
// Prefer a recognized browser over other Chromium hosts (Spotify, app
|
|
207
|
+
// shells), then fall back to the original non-Electron ordering.
|
|
208
|
+
const knownBrowser = candidates.find(
|
|
209
|
+
(endpoint) => endpoint.bundleId !== 'unknown' && !endpoint.isElectron,
|
|
210
|
+
);
|
|
211
|
+
if (knownBrowser) return knownBrowser;
|
|
212
|
+
const nonElectron = candidates.find((endpoint) => !endpoint.isElectron);
|
|
184
213
|
return nonElectron ?? candidates[0];
|
|
185
214
|
}
|
|
186
215
|
|
package/src/cdp/dispatch.ts
CHANGED
|
@@ -1,36 +1,86 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shared dispatch for every routed command. Two jobs live here:
|
|
3
|
+
*
|
|
4
|
+
* 1. Route each root (`session`, `page`, `tab`, `measure`, `motion`,
|
|
5
|
+
* `cdp`, `lib`, hidden `__bridge-serve`) to its branch main.
|
|
6
|
+
* 2. Invoke the `--gate` guard ONCE for the whole surface: every command
|
|
7
|
+
* that is not `measure check|diff` rejects the flag structurally here,
|
|
8
|
+
* before any branch main runs.
|
|
9
|
+
*/
|
|
10
|
+
import { parseCliSyntax, resolveCliContext, validateCliInvocation } from './args.js';
|
|
11
|
+
import { type ParsedArgs } from './types.js';
|
|
12
|
+
import { isGateLeaf, rejectUnsupportedGate } from './commands/gate-guard.js';
|
|
13
|
+
import { getActiveSession } from '../session-context.js';
|
|
14
|
+
import { admitSessionOperation } from '../session/coordinator.js';
|
|
15
|
+
import { sessionMain } from '../session/commands.js';
|
|
16
|
+
import { pageMain } from './commands/page/index.js';
|
|
17
|
+
import { tabMain } from './commands/tab/index.js';
|
|
8
18
|
import { cmdLib } from './commands/lib.js';
|
|
9
19
|
import { cmdCdp } from './commands/cdp.js';
|
|
10
20
|
import { cmdBridgeServe } from './commands/bridge-serve.js';
|
|
21
|
+
import { measureMain } from './commands/measure/index.js';
|
|
22
|
+
import { motionMain } from './commands/motion/index.js';
|
|
23
|
+
|
|
24
|
+
/** The dotted leaf name the gate rejection reports, derived from the parsed
|
|
25
|
+
* command plus its first positional (the branch-leaf token). */
|
|
26
|
+
function gateLeafName(parsed: ParsedArgs): string {
|
|
27
|
+
const branch =
|
|
28
|
+
parsed.command === 'session' ||
|
|
29
|
+
parsed.command === 'page' ||
|
|
30
|
+
parsed.command === 'tab' ||
|
|
31
|
+
parsed.command === 'measure' ||
|
|
32
|
+
parsed.command === 'motion' ||
|
|
33
|
+
parsed.command === 'lib';
|
|
34
|
+
const leaf = parsed.positional[0];
|
|
35
|
+
return branch && leaf ? `${parsed.command} ${leaf}` : parsed.command;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Registers a session-bound operation (`page`/`measure`/`motion`) with the
|
|
40
|
+
* lifecycle coordinator BEFORE its first side effect and releases in `finally`,
|
|
41
|
+
* so a concurrent `session stop` (via `beginSessionStop`) waits for in-flight
|
|
42
|
+
* artifact writers to drain and excludes any operation that starts after stop
|
|
43
|
+
* marking (admission throws `session_stopping`). When there is no active
|
|
44
|
+
* session the operation is a one-shot with nothing to coordinate against, so it
|
|
45
|
+
* runs unwrapped. The active-session read here is never the first for these
|
|
46
|
+
* leaves — `resolveCliContext` already hydrated the same session — so it adds
|
|
47
|
+
* no new active-index side effect on the validation-rejected path.
|
|
48
|
+
*/
|
|
49
|
+
async function withActiveSessionAdmission(parsed: ParsedArgs, run: () => Promise<void>): Promise<void> {
|
|
50
|
+
const active = getActiveSession();
|
|
51
|
+
if (!active) return run();
|
|
52
|
+
const operation = await admitSessionOperation(active.dir);
|
|
53
|
+
try {
|
|
54
|
+
return await run();
|
|
55
|
+
} finally {
|
|
56
|
+
await operation.release();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
11
59
|
|
|
12
60
|
export async function cdpMain(): Promise<void> {
|
|
13
61
|
const args = process.argv.slice(2);
|
|
14
|
-
const parsed =
|
|
62
|
+
const parsed = parseCliSyntax(args);
|
|
63
|
+
|
|
64
|
+
if (parsed.gate && !isGateLeaf(parsed)) {
|
|
65
|
+
rejectUnsupportedGate(parsed, gateLeafName(parsed));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
validateCliInvocation(parsed);
|
|
70
|
+
const resolved = resolveCliContext(parsed);
|
|
15
71
|
|
|
16
|
-
switch (
|
|
17
|
-
case '
|
|
18
|
-
case '
|
|
19
|
-
case '
|
|
20
|
-
case '
|
|
21
|
-
case '
|
|
22
|
-
case '
|
|
23
|
-
case '
|
|
24
|
-
case '
|
|
25
|
-
case 'a11y': return cmdA11y(parsed, args);
|
|
26
|
-
case 'record': return cmdRecord(parsed, args);
|
|
27
|
-
case 'navigate': return cmdNavigate(parsed, args);
|
|
28
|
-
case 'network': return cmdNetwork(parsed, args);
|
|
29
|
-
case 'har': return cmdHar(parsed, args);
|
|
30
|
-
case 'lib': return cmdLib(parsed, args);
|
|
31
|
-
case 'cdp': return cmdCdp(parsed, args);
|
|
32
|
-
case '__bridge-serve': return cmdBridgeServe(parsed, args);
|
|
72
|
+
switch (resolved.command) {
|
|
73
|
+
case 'session': return sessionMain(resolved, args);
|
|
74
|
+
case 'page': return withActiveSessionAdmission(resolved, () => pageMain(resolved, args));
|
|
75
|
+
case 'tab': return tabMain(resolved, args);
|
|
76
|
+
case 'lib': return cmdLib(resolved, args);
|
|
77
|
+
case 'cdp': return cmdCdp(resolved, args);
|
|
78
|
+
case 'measure': return withActiveSessionAdmission(resolved, () => measureMain(resolved, args));
|
|
79
|
+
case 'motion': return withActiveSessionAdmission(resolved, () => motionMain(resolved, args));
|
|
80
|
+
case '__bridge-serve': return cmdBridgeServe(resolved, args);
|
|
33
81
|
default:
|
|
34
|
-
|
|
82
|
+
// capture.ts routes only the known roots into cdpMain(); an unknown
|
|
83
|
+
// token gets its structured error there and never reaches this switch.
|
|
84
|
+
throw new Error(`dispatch: unroutable command ${JSON.stringify(resolved.command)}`);
|
|
35
85
|
}
|
|
36
86
|
}
|
|
@@ -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
|
+
}
|