@crouton-kit/capture 1.3.7 → 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 +2807 -1312
- package/package.json +4 -1
- package/src/capture.ts +10 -0
- package/src/cdp/args.ts +99 -10
- package/src/cdp/bridge/protocol.ts +99 -0
- package/src/cdp/bridge/server.ts +105 -43
- package/src/cdp/bridge/spawn.ts +62 -0
- package/src/cdp/commands/bridge-serve.ts +28 -3
- package/src/cdp/commands/cdp.ts +30 -2
- 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 +4 -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/timing.ts +82 -0
- package/src/cdp/types.ts +66 -0
- package/src/cdp/usage.ts +2 -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 +119 -15
- package/src/session-context.ts +48 -8
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source provenance helper: resolves a generated CSS/JS location (as reported by
|
|
3
|
+
* `CSS.getMatchedStylesForNode`'s stylesheet ranges, or any other generated-source location)
|
|
4
|
+
* to an authored file/line/column via the standard source-map-v3 `mappings` VLQ format, when
|
|
5
|
+
* the generated source carries a `sourceMappingURL`. Degrades to the generated source URL when
|
|
6
|
+
* no source map is present, fails to fetch, or fails to parse — this module never throws for
|
|
7
|
+
* "no source map available"; it throws only for a malformed CDP response.
|
|
8
|
+
*
|
|
9
|
+
* No `source-map` npm dependency is added (this unit does not own `package.json`); the VLQ
|
|
10
|
+
* decoder below is a self-contained implementation of the standard algorithm.
|
|
11
|
+
*
|
|
12
|
+
* Selector/specificity fields that round out full provenance (used by `styles.json`) are the
|
|
13
|
+
* caller's (U08 styles collector's) responsibility — this module resolves source *location*
|
|
14
|
+
* only.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { CDPClient } from './client.js';
|
|
18
|
+
|
|
19
|
+
/** Where a generated location came from and which position in it to resolve. */
|
|
20
|
+
export interface GeneratedSourceRef {
|
|
21
|
+
/** A CDP stylesheet id — when present, the generated text is read via `CSS.getStyleSheetText`. */
|
|
22
|
+
styleSheetId?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Absolute URL of the generated source. Used as the fetch target when `styleSheetId` is
|
|
25
|
+
* absent, as the base URL for resolving a relative `sourceMappingURL`, and as the reported
|
|
26
|
+
* generated-source URL either way.
|
|
27
|
+
*/
|
|
28
|
+
sourceURL?: string;
|
|
29
|
+
/** 0-indexed line in the generated source. */
|
|
30
|
+
line: number;
|
|
31
|
+
/** 0-indexed column in the generated source. */
|
|
32
|
+
column: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface GeneratedLocation {
|
|
36
|
+
kind: 'generated';
|
|
37
|
+
sourceURL: string;
|
|
38
|
+
line: number;
|
|
39
|
+
column: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AuthoredLocation {
|
|
43
|
+
kind: 'authored';
|
|
44
|
+
generated: { sourceURL: string; line: number; column: number };
|
|
45
|
+
/** Authored source file path/URL as recorded in the source map's `sources` (joined with `sourceRoot` when present). */
|
|
46
|
+
file: string;
|
|
47
|
+
/** 1-indexed authored line. */
|
|
48
|
+
line: number;
|
|
49
|
+
/** 0-indexed authored column. */
|
|
50
|
+
column: number;
|
|
51
|
+
/** Inlined authored source text, when the source map carries `sourcesContent` for this source. */
|
|
52
|
+
sourceContent?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ResolvedSourceLocation = AuthoredLocation | GeneratedLocation;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolves `ref` to an authored file/line/column when a `sourceMappingURL` is present and
|
|
59
|
+
* resolvable, otherwise returns the generated source location unchanged.
|
|
60
|
+
*/
|
|
61
|
+
export async function resolveAuthoredSourceLocation(
|
|
62
|
+
client: CDPClient,
|
|
63
|
+
ref: GeneratedSourceRef,
|
|
64
|
+
): Promise<ResolvedSourceLocation> {
|
|
65
|
+
const { text, sourceURL } = await fetchGeneratedText(client, ref);
|
|
66
|
+
const generated: GeneratedLocation = { kind: 'generated', sourceURL, line: ref.line, column: ref.column };
|
|
67
|
+
|
|
68
|
+
if (!text) return generated;
|
|
69
|
+
|
|
70
|
+
const mappingURL = extractSourceMappingURL(text);
|
|
71
|
+
if (!mappingURL) return generated;
|
|
72
|
+
|
|
73
|
+
const rawMap = await loadSourceMap(mappingURL, sourceURL);
|
|
74
|
+
if (!rawMap) return generated;
|
|
75
|
+
|
|
76
|
+
const authored = mapGeneratedPosition(rawMap, ref.line, ref.column);
|
|
77
|
+
if (!authored) return generated;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
kind: 'authored',
|
|
81
|
+
generated: { sourceURL, line: ref.line, column: ref.column },
|
|
82
|
+
file: authored.file,
|
|
83
|
+
line: authored.line,
|
|
84
|
+
column: authored.column,
|
|
85
|
+
sourceContent: authored.sourceContent,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function fetchGeneratedText(
|
|
90
|
+
client: CDPClient,
|
|
91
|
+
ref: GeneratedSourceRef,
|
|
92
|
+
): Promise<{ text: string | null; sourceURL: string }> {
|
|
93
|
+
if (ref.styleSheetId) {
|
|
94
|
+
try {
|
|
95
|
+
const result = (await client.send('CSS.getStyleSheetText', {
|
|
96
|
+
styleSheetId: ref.styleSheetId,
|
|
97
|
+
})) as { text: string };
|
|
98
|
+
return { text: result.text, sourceURL: ref.sourceURL ?? ref.styleSheetId };
|
|
99
|
+
} catch {
|
|
100
|
+
// Stylesheet text unavailable (e.g. a stale styleSheetId) — fall through to a direct fetch.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (ref.sourceURL) {
|
|
105
|
+
try {
|
|
106
|
+
const text = await fetchText(ref.sourceURL);
|
|
107
|
+
return { text, sourceURL: ref.sourceURL };
|
|
108
|
+
} catch {
|
|
109
|
+
return { text: null, sourceURL: ref.sourceURL };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { text: null, sourceURL: '' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const SOURCE_MAPPING_URL_RE = /\/\*[#@]\s*sourceMappingURL=([^\s*]+)\s*\*\/|\/\/[#@]\s*sourceMappingURL=([^\s]+)/g;
|
|
117
|
+
|
|
118
|
+
/** Finds the `sourceMappingURL` comment in generated CSS/JS text. When more than one is present, the last wins (spec convention). */
|
|
119
|
+
export function extractSourceMappingURL(text: string): string | null {
|
|
120
|
+
SOURCE_MAPPING_URL_RE.lastIndex = 0;
|
|
121
|
+
let match: RegExpExecArray | null;
|
|
122
|
+
let last: string | null = null;
|
|
123
|
+
while ((match = SOURCE_MAPPING_URL_RE.exec(text)) !== null) {
|
|
124
|
+
last = match[1] ?? match[2] ?? last;
|
|
125
|
+
}
|
|
126
|
+
return last;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface RawSourceMap {
|
|
130
|
+
version: number;
|
|
131
|
+
sources: string[];
|
|
132
|
+
sourcesContent?: (string | null)[];
|
|
133
|
+
names?: string[];
|
|
134
|
+
mappings: string;
|
|
135
|
+
sourceRoot?: string;
|
|
136
|
+
file?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function loadSourceMap(mappingURL: string, baseURL: string): Promise<RawSourceMap | null> {
|
|
140
|
+
if (mappingURL.startsWith('data:')) {
|
|
141
|
+
return parseDataURISourceMap(mappingURL);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const resolved = baseURL ? new URL(mappingURL, baseURL).toString() : mappingURL;
|
|
146
|
+
const text = await fetchText(resolved);
|
|
147
|
+
return JSON.parse(text) as RawSourceMap;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseDataURISourceMap(dataURI: string): RawSourceMap | null {
|
|
154
|
+
const commaIndex = dataURI.indexOf(',');
|
|
155
|
+
if (commaIndex === -1) return null;
|
|
156
|
+
const meta = dataURI.slice('data:'.length, commaIndex);
|
|
157
|
+
const payload = dataURI.slice(commaIndex + 1);
|
|
158
|
+
try {
|
|
159
|
+
const json = meta.includes(';base64') ? Buffer.from(payload, 'base64').toString('utf8') : decodeURIComponent(payload);
|
|
160
|
+
return JSON.parse(json) as RawSourceMap;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function fetchText(url: string): Promise<string> {
|
|
167
|
+
const response = await fetch(url);
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
throw new Error(`Fetching ${url} returned HTTP ${response.status}`);
|
|
170
|
+
}
|
|
171
|
+
return response.text();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface AuthoredPosition {
|
|
175
|
+
file: string;
|
|
176
|
+
/** 1-indexed authored line. */
|
|
177
|
+
line: number;
|
|
178
|
+
/** 0-indexed authored column. */
|
|
179
|
+
column: number;
|
|
180
|
+
sourceContent?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Maps a 0-indexed (generatedLine, generatedColumn) position through a parsed source-map-v3
|
|
185
|
+
* `mappings` string to the nearest preceding authored position on that generated line. Pure —
|
|
186
|
+
* no I/O, no CDP — so it is directly testable against a fixture map.
|
|
187
|
+
*/
|
|
188
|
+
export function mapGeneratedPosition(
|
|
189
|
+
rawMap: RawSourceMap,
|
|
190
|
+
generatedLine: number,
|
|
191
|
+
generatedColumn: number,
|
|
192
|
+
): AuthoredPosition | null {
|
|
193
|
+
const entries = decodeMappings(rawMap.mappings).filter(
|
|
194
|
+
(entry) => entry.generatedLine === generatedLine && entry.sourceIndex !== undefined,
|
|
195
|
+
);
|
|
196
|
+
if (entries.length === 0) return null;
|
|
197
|
+
|
|
198
|
+
let best: MappingEntry | undefined;
|
|
199
|
+
for (const entry of entries) {
|
|
200
|
+
if (entry.generatedColumn <= generatedColumn && (!best || entry.generatedColumn > best.generatedColumn)) {
|
|
201
|
+
best = entry;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
best = best ?? entries[0];
|
|
205
|
+
|
|
206
|
+
const sourceIndex = best.sourceIndex as number;
|
|
207
|
+
const sourcePath = rawMap.sources[sourceIndex];
|
|
208
|
+
if (sourcePath === undefined) return null;
|
|
209
|
+
|
|
210
|
+
const file = rawMap.sourceRoot ? joinSourceRoot(rawMap.sourceRoot, sourcePath) : sourcePath;
|
|
211
|
+
const sourceContent = rawMap.sourcesContent?.[sourceIndex] ?? undefined;
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
file,
|
|
215
|
+
line: (best.sourceLine ?? 0) + 1,
|
|
216
|
+
column: best.sourceColumn ?? 0,
|
|
217
|
+
sourceContent,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function joinSourceRoot(root: string, source: string): string {
|
|
222
|
+
if (source.startsWith('/') || /^[a-z]+:\/\//i.test(source)) return source;
|
|
223
|
+
return root.endsWith('/') ? `${root}${source}` : `${root}/${source}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
interface MappingEntry {
|
|
227
|
+
generatedLine: number;
|
|
228
|
+
generatedColumn: number;
|
|
229
|
+
sourceIndex?: number;
|
|
230
|
+
sourceLine?: number;
|
|
231
|
+
sourceColumn?: number;
|
|
232
|
+
nameIndex?: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
236
|
+
const BASE64_LOOKUP = new Map<string, number>(Array.from(BASE64_CHARS).map((char, index) => [char, index]));
|
|
237
|
+
|
|
238
|
+
const VLQ_BASE_SHIFT = 5;
|
|
239
|
+
const VLQ_BASE = 1 << VLQ_BASE_SHIFT;
|
|
240
|
+
const VLQ_BASE_MASK = VLQ_BASE - 1;
|
|
241
|
+
const VLQ_CONTINUATION_BIT = VLQ_BASE;
|
|
242
|
+
|
|
243
|
+
/** Decodes every VLQ-encoded number concatenated in one comma-delimited mapping segment. */
|
|
244
|
+
function decodeVLQSegment(segment: string): number[] {
|
|
245
|
+
const values: number[] = [];
|
|
246
|
+
let shift = 0;
|
|
247
|
+
let result = 0;
|
|
248
|
+
|
|
249
|
+
for (const char of segment) {
|
|
250
|
+
const digit = BASE64_LOOKUP.get(char);
|
|
251
|
+
if (digit === undefined) {
|
|
252
|
+
throw new Error(`Invalid base64 VLQ character in source map mappings: ${JSON.stringify(char)}`);
|
|
253
|
+
}
|
|
254
|
+
const continuation = digit & VLQ_CONTINUATION_BIT;
|
|
255
|
+
const bits = digit & VLQ_BASE_MASK;
|
|
256
|
+
result += bits << shift;
|
|
257
|
+
if (continuation) {
|
|
258
|
+
shift += VLQ_BASE_SHIFT;
|
|
259
|
+
} else {
|
|
260
|
+
const negate = result & 1;
|
|
261
|
+
result >>= 1;
|
|
262
|
+
values.push(negate ? -result : result);
|
|
263
|
+
result = 0;
|
|
264
|
+
shift = 0;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return values;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Decodes a full source-map-v3 `mappings` string into per-generated-position entries with sources/names deltas resolved. */
|
|
272
|
+
function decodeMappings(mappings: string): MappingEntry[] {
|
|
273
|
+
const entries: MappingEntry[] = [];
|
|
274
|
+
const lines = mappings.split(';');
|
|
275
|
+
|
|
276
|
+
let sourceIndex = 0;
|
|
277
|
+
let sourceLine = 0;
|
|
278
|
+
let sourceColumn = 0;
|
|
279
|
+
let nameIndex = 0;
|
|
280
|
+
|
|
281
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
282
|
+
const lineText = lines[lineIndex];
|
|
283
|
+
if (lineText === '') continue;
|
|
284
|
+
|
|
285
|
+
let generatedColumn = 0;
|
|
286
|
+
for (const segment of lineText.split(',')) {
|
|
287
|
+
if (segment === '') continue;
|
|
288
|
+
const fields = decodeVLQSegment(segment);
|
|
289
|
+
generatedColumn += fields[0];
|
|
290
|
+
|
|
291
|
+
const entry: MappingEntry = { generatedLine: lineIndex, generatedColumn };
|
|
292
|
+
if (fields.length > 1) {
|
|
293
|
+
sourceIndex += fields[1];
|
|
294
|
+
sourceLine += fields[2];
|
|
295
|
+
sourceColumn += fields[3];
|
|
296
|
+
entry.sourceIndex = sourceIndex;
|
|
297
|
+
entry.sourceLine = sourceLine;
|
|
298
|
+
entry.sourceColumn = sourceColumn;
|
|
299
|
+
if (fields.length > 4) {
|
|
300
|
+
nameIndex += fields[4];
|
|
301
|
+
entry.nameIndex = nameIndex;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
entries.push(entry);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return entries;
|
|
309
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared timing helpers for `measure`/`motion`. See the design's "Recorder timing model":
|
|
3
|
+
* the authoritative clock for every reported timestamp is `performance.now()`, relative to
|
|
4
|
+
* the recorder-armed marker (`t=0`). Screencast frames carry a wall-clock `metadata.timestamp`
|
|
5
|
+
* and Tracing events carry a monotonic `ts` in their own domains; the recorder bridge (U13/U14)
|
|
6
|
+
* converts both into the `performance.now()` domain by baseline-offset subtraction using the
|
|
7
|
+
* baseline this module reads.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CDPClient } from './client.js';
|
|
11
|
+
|
|
12
|
+
export interface BracketedTiming<T> {
|
|
13
|
+
result: T;
|
|
14
|
+
/** `performance.now()` immediately before `fn` ran. */
|
|
15
|
+
startPerformanceNow: number;
|
|
16
|
+
/** `performance.now()` immediately after `fn` resolved. */
|
|
17
|
+
endPerformanceNow: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Runs `fn`, bracketed by two `performance.now()` reads taken in the page's document execution
|
|
22
|
+
* context. Used to attribute a discrete action (e.g. an `Input.dispatch*` call) to a window in
|
|
23
|
+
* the recorder's authoritative `performance.now()` clock domain — this is the mechanism behind
|
|
24
|
+
* the recorder-client adapter's internal `performance.mark(input_name)` bracketing (U14).
|
|
25
|
+
*/
|
|
26
|
+
export async function withDocumentPerformanceNow<T>(
|
|
27
|
+
client: CDPClient,
|
|
28
|
+
fn: () => Promise<T>,
|
|
29
|
+
): Promise<BracketedTiming<T>> {
|
|
30
|
+
const startPerformanceNow = await readPerformanceNow(client);
|
|
31
|
+
const result = await fn();
|
|
32
|
+
const endPerformanceNow = await readPerformanceNow(client);
|
|
33
|
+
return { result, startPerformanceNow, endPerformanceNow };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readPerformanceNow(client: CDPClient): Promise<number> {
|
|
37
|
+
const evaluation = (await client.send('Runtime.evaluate', {
|
|
38
|
+
expression: 'performance.now()',
|
|
39
|
+
returnByValue: true,
|
|
40
|
+
})) as { result: { value?: unknown }; exceptionDetails?: unknown };
|
|
41
|
+
|
|
42
|
+
if (evaluation.exceptionDetails) {
|
|
43
|
+
throw new Error(`Failed to read performance.now(): ${JSON.stringify(evaluation.exceptionDetails)}`);
|
|
44
|
+
}
|
|
45
|
+
if (typeof evaluation.result.value !== 'number') {
|
|
46
|
+
throw new Error('performance.now() did not return a number');
|
|
47
|
+
}
|
|
48
|
+
return evaluation.result.value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TraceClockBaseline {
|
|
52
|
+
/** `performance.now()` (document-relative ms) at the moment the baseline was captured. */
|
|
53
|
+
performanceNowMs: number;
|
|
54
|
+
/** `Date.now()` (wall-clock epoch ms) at the same moment. */
|
|
55
|
+
wallClockMs: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reads one synchronized (`performance.now()`, wall-clock) pair from the page in a single
|
|
60
|
+
* round trip. The recorder bridge captures this once at `--start` as the `capture-rec-start`
|
|
61
|
+
* baseline stored in `markers.json`; screencast-frame `metadata.timestamp` (wall-clock seconds)
|
|
62
|
+
* and Tracing `ts` (wall-clock-anchored microseconds) are then converted into the
|
|
63
|
+
* `performance.now()` domain by subtracting this baseline's wall-clock offset.
|
|
64
|
+
*/
|
|
65
|
+
export async function readTraceClockBaseline(client: CDPClient): Promise<TraceClockBaseline> {
|
|
66
|
+
const evaluation = (await client.send('Runtime.evaluate', {
|
|
67
|
+
expression: '({ performanceNowMs: performance.now(), wallClockMs: Date.now() })',
|
|
68
|
+
returnByValue: true,
|
|
69
|
+
})) as {
|
|
70
|
+
result: { value?: { performanceNowMs?: unknown; wallClockMs?: unknown } };
|
|
71
|
+
exceptionDetails?: unknown;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (evaluation.exceptionDetails) {
|
|
75
|
+
throw new Error(`Failed to read trace clock baseline: ${JSON.stringify(evaluation.exceptionDetails)}`);
|
|
76
|
+
}
|
|
77
|
+
const value = evaluation.result.value;
|
|
78
|
+
if (!value || typeof value.performanceNowMs !== 'number' || typeof value.wallClockMs !== 'number') {
|
|
79
|
+
throw new Error('Trace clock baseline evaluation did not return the expected {performanceNowMs, wallClockMs} shape');
|
|
80
|
+
}
|
|
81
|
+
return { performanceNowMs: value.performanceNowMs, wallClockMs: value.wallClockMs };
|
|
82
|
+
}
|
package/src/cdp/types.ts
CHANGED
|
@@ -39,4 +39,70 @@ export interface ParsedArgs {
|
|
|
39
39
|
waitEvent?: string;
|
|
40
40
|
timeoutMs?: number;
|
|
41
41
|
socket?: string;
|
|
42
|
+
|
|
43
|
+
// -- measure/motion branch flags (settled in the capture measure+motion
|
|
44
|
+
// build plan's U02) -- every leaf under `capture measure`/`capture motion`
|
|
45
|
+
// parses these from the shared global parser instead of re-parsing argv
|
|
46
|
+
// itself.
|
|
47
|
+
/** `snap --freeze-animations` — pause CSS/WAAPI animation before capture. */
|
|
48
|
+
freezeAnimations?: boolean;
|
|
49
|
+
/** `snap --settle-timeout <ms>` — override the default 5000ms settle wait. */
|
|
50
|
+
settleTimeout?: number;
|
|
51
|
+
/** `snap --capture-unsettled` — write full substrate despite non-settlement. */
|
|
52
|
+
captureUnsettled?: boolean;
|
|
53
|
+
/** `snap|diff|mask --pixels` — include raster crop/diff facts. */
|
|
54
|
+
pixels?: boolean;
|
|
55
|
+
/** `snap --state <state[:selector]>` — repeatable; one entry per occurrence. */
|
|
56
|
+
state?: string[];
|
|
57
|
+
/** `check --for <list>` — comma-separated check names or categories. */
|
|
58
|
+
for?: string;
|
|
59
|
+
/** `diff --before <snap>` */
|
|
60
|
+
before?: string;
|
|
61
|
+
/** `diff --after <snap>` */
|
|
62
|
+
after?: string;
|
|
63
|
+
/** `diff --full` — expand to the complete state-matrix/per-element diff. */
|
|
64
|
+
full?: boolean;
|
|
65
|
+
/** `check|diff --gate` — exit 2 on findings/changes instead of 0. */
|
|
66
|
+
gate?: boolean;
|
|
67
|
+
/** `census --snap <id>` — repeatable; one entry per occurrence. */
|
|
68
|
+
snap?: string[];
|
|
69
|
+
/** `census --url <url>` — repeatable accumulator alongside the existing
|
|
70
|
+
* single-value `url` field above (which stays last-wins for every
|
|
71
|
+
* existing command's target-matching semantics). */
|
|
72
|
+
urls?: string[];
|
|
73
|
+
/** `census --set-file <path>` */
|
|
74
|
+
setFile?: string;
|
|
75
|
+
/** `census|sweep --axis <axis>` */
|
|
76
|
+
axis?: string;
|
|
77
|
+
/** `sweep --from <val>` — raw string; axis units vary (px/dpr/zoom). */
|
|
78
|
+
from?: string;
|
|
79
|
+
/** `sweep --to <val>` — raw string; axis units vary (px/dpr/zoom). */
|
|
80
|
+
to?: string;
|
|
81
|
+
/** `sweep --viewport-height <val>` — raw string, distinct from `--height`. */
|
|
82
|
+
viewportHeight?: string;
|
|
83
|
+
/** `motion rec --stop --rec-id <id>` — explicit recording id override. */
|
|
84
|
+
recId?: string;
|
|
85
|
+
/** `check|snap|census --viewport WxH` — repeatable accumulator alongside
|
|
86
|
+
* the existing single-value `viewport` field above (last-wins, unchanged). */
|
|
87
|
+
viewports?: string[];
|
|
88
|
+
/** `explain|map focus|... --selector <sel>` */
|
|
89
|
+
selector?: string;
|
|
90
|
+
/** `explain --size` — include size/layout provenance detail. */
|
|
91
|
+
size?: boolean;
|
|
92
|
+
/** `explain --text` — include text/line-box detail. */
|
|
93
|
+
text?: boolean;
|
|
94
|
+
/** `explain --form` — include form/caret/autofill detail. */
|
|
95
|
+
form?: boolean;
|
|
96
|
+
/** `motion rec --start` — arm the composed recorder. */
|
|
97
|
+
start?: boolean;
|
|
98
|
+
/** `motion rec --stop` — finalize the composed recorder. */
|
|
99
|
+
stop?: boolean;
|
|
100
|
+
/** `motion rec --do <action>` — one-shot scripted action. */
|
|
101
|
+
do?: string;
|
|
102
|
+
/** `motion timeline --element <sel>` */
|
|
103
|
+
element?: string;
|
|
104
|
+
/** `motion timeline --prop <name>` */
|
|
105
|
+
prop?: string;
|
|
106
|
+
/** `motion response --action <action>` */
|
|
107
|
+
action?: string;
|
|
42
108
|
}
|
package/src/cdp/usage.ts
CHANGED
|
@@ -22,6 +22,8 @@ Commands:
|
|
|
22
22
|
lib show <name> Lib + function summaries
|
|
23
23
|
lib read <name> [fn…] Full input/output schemas (+ .ts source path)
|
|
24
24
|
cdp <Domain.method> Send a raw CDP command capture doesn't wrap [--browser] [--params json] [--wait-event name]
|
|
25
|
+
measure <leaf> Enriched snapshot substrate + read-only queries — see \`capture measure --help\`
|
|
26
|
+
motion <leaf> Recorder lifecycle + read-only queries over a recording — see \`capture motion --help\`
|
|
25
27
|
|
|
26
28
|
Options:
|
|
27
29
|
--port <port> Override CDP port
|