@crouton-kit/capture 1.3.18 → 1.3.20
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 +4204 -3244
- package/package.json +1 -1
- package/src/capture.ts +4 -1
- package/src/cdp/args.ts +22 -3
- package/src/cdp/browser-process.ts +510 -0
- package/src/cdp/client.ts +10 -1
- package/src/cdp/color-scheme.ts +47 -0
- package/src/cdp/commands/measure/index.ts +1 -1
- package/src/cdp/commands/measure/snap.ts +50 -8
- package/src/cdp/commands/motion/mask.ts +8 -2
- package/src/cdp/commands/page/click.ts +14 -0
- package/src/cdp/commands/page/elements.ts +2 -2
- package/src/cdp/commands/page/index.ts +1 -1
- package/src/cdp/commands/page/shot.ts +58 -13
- package/src/cdp/commands/tab/index.ts +19 -8
- package/src/cdp/commands/tab/launch.ts +73 -0
- package/src/cdp/commands/tab/list.ts +14 -2
- package/src/cdp/commands/tab/quit.ts +78 -0
- package/src/cdp/har-recorder.ts +14 -20
- package/src/cdp/measure/collectors/ax.ts +3 -4
- package/src/cdp/measure/collectors/forms.ts +4 -6
- package/src/cdp/measure/collectors/media.ts +1 -2
- package/src/cdp/measure/collectors/states.ts +1 -6
- package/src/cdp/measure/collectors/style-provenance.ts +4 -16
- package/src/cdp/measure/collectors/styles.ts +21 -10
- package/src/cdp/measure/collectors/text.ts +3 -5
- package/src/cdp/measure/snapshot.ts +197 -29
- package/src/cdp/measure/types.ts +36 -2
- package/src/cdp/motion/jank.ts +8 -1
- package/src/cdp/screenshot.ts +83 -8
- package/src/cdp/targets.ts +5 -0
- package/src/cdp/types.ts +6 -0
- package/src/har-manager.ts +16 -15
- package/src/interact.ts +20 -4
- package/src/output/render.ts +2 -0
- package/src/registry.ts +1 -1
- package/src/session/commands.ts +223 -43
- package/src/session/har-redact.ts +125 -0
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
pickPreferredEndpoint,
|
|
14
14
|
} from '../../detect.js';
|
|
15
15
|
import { listTargets } from '../../targets.js';
|
|
16
|
+
import { ownedBrowsersByPort, sweepOwnedBrowsers } from '../../browser-process.js';
|
|
16
17
|
import { type ParsedArgs } from '../../types.js';
|
|
17
18
|
import {
|
|
18
19
|
data,
|
|
@@ -30,8 +31,8 @@ const USAGE = `capture tab list — discover CDP endpoints and list the page tab
|
|
|
30
31
|
input:
|
|
31
32
|
--port <port> probe one endpoint only, instead of discovering every localhost CDP endpoint
|
|
32
33
|
|
|
33
|
-
output: <tabs endpoints=… tabs=…> — one section per reachable endpoint (port, app, page-tab count, preferred marker) listing each page tab's target id (first 8 chars, enough for --target), title, and url; an endpoint that failed the tab fetch is an endpoints-unreachable fact, never a silent omission.
|
|
34
|
-
effects:
|
|
34
|
+
output: <tabs endpoints=… tabs=…> — one section per reachable endpoint (port, app, page-tab count, preferred marker, and a capture-owned pid marker on any browser \`tab launch\` started) listing each page tab's target id (first 8 chars, enough for --target), title, and url; an endpoint that failed the tab fetch is an endpoints-unreachable fact, never a silent omission.
|
|
35
|
+
effects: probes local CDP endpoints over HTTP/WebSocket; no page-observable writes. Also sweeps capture-launched browsers, stopping any that have exited, stopped answering CDP, or gone idle — browsers capture did not start are never signalled.`;
|
|
35
36
|
|
|
36
37
|
export interface TabRow {
|
|
37
38
|
id: string;
|
|
@@ -45,6 +46,9 @@ export interface EndpointTabs {
|
|
|
45
46
|
app?: string;
|
|
46
47
|
/** Marked on the endpoint auto-discovery would pick for port-less commands. */
|
|
47
48
|
preferred: boolean;
|
|
49
|
+
/** The pid of the capture-launched browser serving this endpoint, when this
|
|
50
|
+
* is one capture owns — absent for every foreign/user-owned endpoint. */
|
|
51
|
+
ownedPid?: number;
|
|
48
52
|
pages: TabRow[];
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -76,6 +80,7 @@ export function buildTabsResult(
|
|
|
76
80
|
...(ep.app !== undefined ? [line(text` — `, data(ep.app, 80))] : []),
|
|
77
81
|
fact` — ${ep.pages.length} page tab(s)`,
|
|
78
82
|
...(ep.preferred ? [text` [preferred]`] : []),
|
|
83
|
+
...(ep.ownedPid !== undefined ? [line(fact` [capture-owned pid=${ep.ownedPid}]`)] : []),
|
|
79
84
|
);
|
|
80
85
|
const rows = ep.pages.map((t) =>
|
|
81
86
|
line(text` `, data(t.id.slice(0, 8)), text` "`, data(t.title, 120), text`" `, data(t.url, 300)),
|
|
@@ -121,6 +126,11 @@ export async function cmdTabList(parsed: ParsedArgs, _args: string[]): Promise<v
|
|
|
121
126
|
return;
|
|
122
127
|
}
|
|
123
128
|
|
|
129
|
+
// The probe agents run constantly is also where abandoned owned browsers
|
|
130
|
+
// die: sweeping here is what keeps a launch from outliving the agent that
|
|
131
|
+
// made it, without any daemon in the picture.
|
|
132
|
+
await sweepOwnedBrowsers();
|
|
133
|
+
const owned = ownedBrowsersByPort();
|
|
124
134
|
const reachable: EndpointTabs[] = [];
|
|
125
135
|
const unreachable: UnreachableEndpoint[] = [];
|
|
126
136
|
|
|
@@ -130,6 +140,7 @@ export async function cmdTabList(parsed: ParsedArgs, _args: string[]): Promise<v
|
|
|
130
140
|
reachable.push({
|
|
131
141
|
port: parsed.port,
|
|
132
142
|
preferred: false,
|
|
143
|
+
...(owned.has(parsed.port) ? { ownedPid: owned.get(parsed.port)!.pid } : {}),
|
|
133
144
|
pages: targets
|
|
134
145
|
.filter((t) => t.type === 'page')
|
|
135
146
|
.map((t) => ({ id: t.id, title: t.title, url: t.url })),
|
|
@@ -149,6 +160,7 @@ export async function cmdTabList(parsed: ParsedArgs, _args: string[]): Promise<v
|
|
|
149
160
|
port: ep.port,
|
|
150
161
|
app: ep.app,
|
|
151
162
|
preferred: ep.port === preferredPort,
|
|
163
|
+
...(owned.has(ep.port) ? { ownedPid: owned.get(ep.port)!.pid } : {}),
|
|
152
164
|
pages: targets
|
|
153
165
|
.filter((t) => t.type === 'page')
|
|
154
166
|
.map((t) => ({ id: t.id, title: t.title, url: t.url })),
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `capture tab quit [<port>] [--all]` — stop browsers capture started.
|
|
3
|
+
*
|
|
4
|
+
* Named `quit` rather than `close` deliberately: `capture tab close` closes one
|
|
5
|
+
* TAB inside a browser, this ends the browser process itself. It reaches only
|
|
6
|
+
* browsers in capture's own registry, verified by PID birth — a port capture
|
|
7
|
+
* never launched (the user's own browser, or one an agent started by hand)
|
|
8
|
+
* matches nothing here and is never signalled.
|
|
9
|
+
*/
|
|
10
|
+
import { quitOwnedBrowsers, sweepOwnedBrowsers, type ReapedBrowser } from '../../browser-process.js';
|
|
11
|
+
import { invalidInput } from '../../../errors.js';
|
|
12
|
+
import { type ParsedArgs } from '../../types.js';
|
|
13
|
+
import { data, emitResult, fact, line, lineList, text, type RenderableResult } from '../../../output/render.js';
|
|
14
|
+
|
|
15
|
+
const USAGE = `capture tab quit — stop a browser capture started.
|
|
16
|
+
|
|
17
|
+
input:
|
|
18
|
+
<port> the port of a capture-launched browser (as reported by tab launch / tab list)
|
|
19
|
+
--all stop every browser capture launched
|
|
20
|
+
|
|
21
|
+
output: <browser-quit stopped=…> — one line per stopped browser (port, pid) and the reason it ended.
|
|
22
|
+
effects: SIGTERM (then SIGKILL if needed) to capture-launched browsers only, verified by process-birth identity, and removes their private profiles. A browser capture did not start is never signalled — a port with no capture record stops nothing.`;
|
|
23
|
+
|
|
24
|
+
/** Pure `<browser-quit>` result builder — exported for tests. */
|
|
25
|
+
export function buildBrowserQuitResult(stopped: readonly ReapedBrowser[], selector: string): RenderableResult {
|
|
26
|
+
return {
|
|
27
|
+
tag: 'browser-quit',
|
|
28
|
+
attrs: { stopped: stopped.length },
|
|
29
|
+
summary:
|
|
30
|
+
stopped.length === 0
|
|
31
|
+
? line(text`no capture-launched browser matched `, data(selector), text`; nothing was signalled.`)
|
|
32
|
+
: fact`stopped ${stopped.length} capture-launched browser(s).`,
|
|
33
|
+
sections:
|
|
34
|
+
stopped.length === 0
|
|
35
|
+
? []
|
|
36
|
+
: [
|
|
37
|
+
lineList(
|
|
38
|
+
stopped.map((browser) =>
|
|
39
|
+
line(
|
|
40
|
+
text`port `,
|
|
41
|
+
data(browser.port ?? 'unstarted'),
|
|
42
|
+
fact` pid ${browser.pid} `,
|
|
43
|
+
data(browser.reason),
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function cmdTabQuit(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
52
|
+
if (parsed.help) {
|
|
53
|
+
console.log(USAGE);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const token = parsed.positional[0];
|
|
58
|
+
if (!parsed.all && token === undefined) {
|
|
59
|
+
throw invalidInput('received: no target; expected: capture tab quit <port> or capture tab quit --all.', 'missing_argument');
|
|
60
|
+
}
|
|
61
|
+
if (parsed.all && token !== undefined) {
|
|
62
|
+
throw invalidInput('received: both a port and --all; expected: one or the other.');
|
|
63
|
+
}
|
|
64
|
+
if (token !== undefined && !/^\d+$/.test(token)) {
|
|
65
|
+
throw invalidInput(`Invalid port: ${token}.`);
|
|
66
|
+
}
|
|
67
|
+
const port = token === undefined ? undefined : Number(token);
|
|
68
|
+
|
|
69
|
+
// The sweep runs first so a browser that already exited is reported as
|
|
70
|
+
// reaped rather than as "no match" for the port the caller remembered.
|
|
71
|
+
const swept = await sweepOwnedBrowsers();
|
|
72
|
+
const stopped = await quitOwnedBrowsers({ port, all: parsed.all });
|
|
73
|
+
const matchedSweep = swept.filter((browser) => parsed.all || browser.port === port);
|
|
74
|
+
|
|
75
|
+
emitResult(buildBrowserQuitResult([...stopped, ...matchedSweep], parsed.all ? '--all' : String(port)), {
|
|
76
|
+
json: parsed.json,
|
|
77
|
+
});
|
|
78
|
+
}
|
package/src/cdp/har-recorder.ts
CHANGED
|
@@ -164,8 +164,6 @@ export class HARRecorder {
|
|
|
164
164
|
private readonly recordedKeys = new Set<string>();
|
|
165
165
|
/** Snapshot mode only — streaming mode retains no materialized values. */
|
|
166
166
|
private readonly materializedByKey = new Map<string, Materialized>();
|
|
167
|
-
/** Last terminal remains comparable until this CDP id opens its next generation. */
|
|
168
|
-
private readonly closedTerminals = new Map<string, Terminal>();
|
|
169
167
|
/** Sockets opened while this recorder is attached — CDP does not replay creation events for pre-existing sockets. */
|
|
170
168
|
private readonly webSockets = new Map<string, WebSocketConnection>();
|
|
171
169
|
private readonly bodyTasks = new Set<Promise<void>>();
|
|
@@ -245,11 +243,13 @@ export class HARRecorder {
|
|
|
245
243
|
const hasRedirect = 'redirectResponse' in params && params.redirectResponse !== undefined;
|
|
246
244
|
if (previous && !hasRedirect) fail('duplicate active requestId without redirectResponse');
|
|
247
245
|
if (hasRedirect && previous) {
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
// CDP uses status 0 when a redirect response is observable but never
|
|
247
|
+
// reached HTTP (for example, an extension redirect during navigation).
|
|
248
|
+
// Preserve that factual response rather than aborting the whole session.
|
|
249
|
+
const redirect = this.parseResponse(params.redirectResponse, 'redirectResponse', true);
|
|
250
|
+
if (redirect.status !== 0 && (redirect.status < 300 || redirect.status > 399)) fail('redirectResponse.status');
|
|
250
251
|
this.assignTerminal(previous, { kind: 'redirect', timestamp: monotonic }, redirect);
|
|
251
252
|
}
|
|
252
|
-
this.closedTerminals.delete(requestId);
|
|
253
253
|
const generation = this.nextGenerationByRequestId.get(requestId) ?? 1;
|
|
254
254
|
this.nextGenerationByRequestId.set(requestId, generation + 1);
|
|
255
255
|
this.activeByRequestId.set(requestId, {
|
|
@@ -338,11 +338,11 @@ export class HARRecorder {
|
|
|
338
338
|
});
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
private parseResponse(value: unknown, name: string): RedirectResponse {
|
|
341
|
+
private parseResponse(value: unknown, name: string, allowNoHttpStatus = false): RedirectResponse {
|
|
342
342
|
if (!isPlainObject(value)) fail(name);
|
|
343
343
|
nonempty(value.url, `${name}.url`);
|
|
344
344
|
const status = finite(value.status, `${name}.status`);
|
|
345
|
-
if (!Number.isInteger(status) || status <
|
|
345
|
+
if (!Number.isInteger(status) || status < 0 || status > 599 || (status < 100 && !(allowNoHttpStatus && status === 0))) fail(`${name}.status`);
|
|
346
346
|
return { status, headers: headers(value.headers, `${name}.headers`) };
|
|
347
347
|
}
|
|
348
348
|
|
|
@@ -366,11 +366,13 @@ export class HARRecorder {
|
|
|
366
366
|
const requestId = this.eventRequestId(params);
|
|
367
367
|
if (!requestId) return;
|
|
368
368
|
const current = this.activeByRequestId.get(requestId);
|
|
369
|
-
|
|
370
|
-
|
|
369
|
+
// Chromium can follow a loadingFailed with loadingFinished for the same
|
|
370
|
+
// request (for example, a blocked iframe during navigation). Once the
|
|
371
|
+
// first terminal materializes its generation, a later terminal cannot be
|
|
372
|
+
// assigned safely, so the first factual terminal remains authoritative.
|
|
373
|
+
if (!current) return;
|
|
371
374
|
const event = params as Record<string, unknown>;
|
|
372
375
|
const terminal: Terminal = { kind: 'finished', timestamp: finite(event.timestamp, 'timestamp'), encodedDataLength: finite(event.encodedDataLength, 'encodedDataLength') };
|
|
373
|
-
if (!current) return this.compareClosedTerminal(requestId, terminal);
|
|
374
376
|
if (!current.response) fail('loadingFinished without responseReceived');
|
|
375
377
|
this.assignTerminal(current, terminal);
|
|
376
378
|
}
|
|
@@ -379,8 +381,7 @@ export class HARRecorder {
|
|
|
379
381
|
const requestId = this.eventRequestId(params);
|
|
380
382
|
if (!requestId) return;
|
|
381
383
|
const current = this.activeByRequestId.get(requestId);
|
|
382
|
-
|
|
383
|
-
if (!current && !closed) return;
|
|
384
|
+
if (!current) return;
|
|
384
385
|
const event = params as Record<string, unknown>;
|
|
385
386
|
const terminal: Terminal = {
|
|
386
387
|
kind: 'failed',
|
|
@@ -390,22 +391,15 @@ export class HARRecorder {
|
|
|
390
391
|
blockedReason: event.blockedReason === undefined ? null : typeof event.blockedReason === 'string' ? event.blockedReason : fail('blockedReason'),
|
|
391
392
|
resourceType: event.type === undefined ? null : typeof event.type === 'string' ? event.type : fail('type'),
|
|
392
393
|
};
|
|
393
|
-
if (!current) return this.compareClosedTerminal(requestId, terminal);
|
|
394
394
|
this.assignTerminal(current, terminal);
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
-
private compareClosedTerminal(requestId: string, terminal: Terminal): void {
|
|
398
|
-
const previous = this.closedTerminals.get(requestId);
|
|
399
|
-
if (previous && !same(previous, terminal)) fail('conflicting terminal event');
|
|
400
|
-
}
|
|
401
|
-
|
|
402
397
|
private assignTerminal(current: Generation, terminal: Terminal, redirectResponse?: RedirectResponse): void {
|
|
403
398
|
if (current.terminal) {
|
|
404
|
-
if (!same(current.terminal, terminal)) fail(
|
|
399
|
+
if (!same(current.terminal, terminal)) fail(`conflicting terminal event for requestId ${current.requestId} generation ${current.generation}`);
|
|
405
400
|
return;
|
|
406
401
|
}
|
|
407
402
|
current.terminal = terminal;
|
|
408
|
-
this.closedTerminals.set(current.requestId, terminal);
|
|
409
403
|
if (redirectResponse) {
|
|
410
404
|
current.redirectResponse = redirectResponse;
|
|
411
405
|
this.materialize(current, { state: 'not_applicable', reason: 'redirect' });
|
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
* shared with `styles.json`/`media.json` per the U04 contract — no
|
|
8
8
|
* coordination with those files is needed.
|
|
9
9
|
*
|
|
10
|
-
* "geometry ids ... when possible":
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* read at capture time. Read here as "attach a `rect` fact directly per AX
|
|
10
|
+
* "geometry ids ... when possible": `geometry.json` is a separate
|
|
11
|
+
* collector artifact that this file does not depend on or read at capture
|
|
12
|
+
* time. Read here as "attach a `rect` fact directly per AX
|
|
14
13
|
* node via a best-effort `DOM.getBoxModel` lookup" rather than a join
|
|
15
14
|
* against `geometry.json` — a deliberate, bounded reading documented in
|
|
16
15
|
* the U08 build-plan report, not a defect.
|
|
@@ -41,9 +41,8 @@
|
|
|
41
41
|
* turns into a `backendNodeId`. A page can predefine a setter for any
|
|
42
42
|
* global name it can guess; it can observe nothing here, because nothing
|
|
43
43
|
* is ever set on it. No DOM mutation and no page-observable global either,
|
|
44
|
-
* so nothing in the emitted `screenshot.png`/`dom.html
|
|
45
|
-
* collector
|
|
46
|
-
* observe this collector having run.
|
|
44
|
+
* so nothing in the emitted `screenshot.png`/`dom.html` can observe this
|
|
45
|
+
* collector having run.
|
|
47
46
|
*/
|
|
48
47
|
|
|
49
48
|
import type { CDPClient } from '../../client.js';
|
|
@@ -628,9 +627,8 @@ async function resolveIndexedObjectIds(
|
|
|
628
627
|
export const collectForms: Collector = async (ctx) => {
|
|
629
628
|
// The walk's return value is a plain in-memory `{ facts, elements }`
|
|
630
629
|
// object — never a `window` global, never a live-DOM attribute — so
|
|
631
|
-
// nothing another baseline
|
|
632
|
-
//
|
|
633
|
-
// `snapshot.ts`) could ever observe. `Runtime.evaluate({returnByValue:
|
|
630
|
+
// nothing another baseline artifact capture could ever observe.
|
|
631
|
+
// `Runtime.evaluate({returnByValue:
|
|
634
632
|
// false})` hands that object back as a CDP `RemoteObject` (an `objectId`
|
|
635
633
|
// with zero page visibility into it); every held `objectId` (the
|
|
636
634
|
// container, `facts`, `elements`) is released via `Runtime.releaseObject`
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* state, and (for `object-fit`-bearing elements) computed crop/letterbox
|
|
5
5
|
* facts. One `Runtime.evaluate` gathers raw per-element facts in-page;
|
|
6
6
|
* `backendNodeId` correlation reuses `resolveNodeIds` from `./styles.js`
|
|
7
|
-
*
|
|
8
|
-
* concurrently, so no CDP call is shared across files).
|
|
7
|
+
* while issuing its own `DOM.getDocument` call.
|
|
9
8
|
*
|
|
10
9
|
* This is a `phase: 'baseline'` collector, so it must not create or pin
|
|
11
10
|
* any page-observable state: `canvas.getContext(...)` is deliberately NOT
|
|
@@ -5,12 +5,7 @@
|
|
|
5
5
|
* Written ONLY when at least one `--state` was requested (`ctx.state.length
|
|
6
6
|
* > 0`); otherwise this is a no-op (no file written). Owned by U12.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* `Promise.all` in `snapshot.ts` — there is no cross-collector read
|
|
10
|
-
* ordering, and `geometry.json`/`styles.json`/`hittest.json` may not exist
|
|
11
|
-
* yet when this runs), so this collector drives its own minimal
|
|
12
|
-
* geometry/style/hittest re-capture per forced element rather than reading
|
|
13
|
-
* those files. Scope: top-frame light DOM only — elements inside an
|
|
8
|
+
* This collector drives its own minimal geometry/style/hittest re-capture per forced element rather than reading those files. Scope: top-frame light DOM only — elements inside an
|
|
14
9
|
* iframe or a shadow root are out of reach of `DOM.querySelectorAll`'s
|
|
15
10
|
* plain (non-piercing) subtree walk and are not visited here; frame/shadow
|
|
16
11
|
* stitching is `geometry.json`'s concern, not this collector's.
|
|
@@ -29,22 +29,10 @@
|
|
|
29
29
|
* re-invoking `CSS.enable` while the domain is ALREADY enabled is a
|
|
30
30
|
* no-op that redelivers nothing — only a `CSS.disable` + `CSS.enable`
|
|
31
31
|
* pair forces Chrome to resend every currently-known stylesheet's
|
|
32
|
-
* header).
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* the headers for stylesheets already parsed at that time. Each caller
|
|
37
|
-
* runs `captureStyleSheetHeaders` independently, as the very first thing
|
|
38
|
-
* it does (before any other CSS-domain call), and awaits it before
|
|
39
|
-
* issuing any of its own `CSS.*` calls. Because listener registration is
|
|
40
|
-
* synchronous (before any `await`), and JS never yields to socket I/O
|
|
41
|
-
* mid-way through `Promise.all`'s synchronous dispatch of every baseline
|
|
42
|
-
* collector, both this file's and the other collector's listeners are
|
|
43
|
-
* always attached before either's disable/enable pair reaches Chrome —
|
|
44
|
-
* so neither can miss the other's redelivery burst, and once both
|
|
45
|
-
* callers' own `captureStyleSheetHeaders` calls resolve, the CSS domain
|
|
46
|
-
* is stably enabled for the rest of the snapshot (neither file disables
|
|
47
|
-
* it again).
|
|
32
|
+
* header). `enableDomainsForSnap` already enabled CSS before either
|
|
33
|
+
* collector starts, so each collector captures its own headers as its
|
|
34
|
+
* first CSS-domain action and awaits that capture before its provenance
|
|
35
|
+
* reads. This keeps either artifact self-contained.
|
|
48
36
|
*/
|
|
49
37
|
|
|
50
38
|
import { resolveAuthoredSourceLocation, type ResolvedSourceLocation } from '../../source-map.js';
|
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
* file's `CSS.getMatchedStylesForNode` calls (which need a CDP `nodeId`,
|
|
15
15
|
* not just a `backendNodeId`) and reused by `media.ts` for its own
|
|
16
16
|
* per-element `backendNodeId` correlation. Both `styles.ts` and `media.ts`
|
|
17
|
-
* independently issue their own `Runtime.evaluate`/`DOM.getDocument`
|
|
18
|
-
*
|
|
19
|
-
* is no cross-collector call sharing beyond this shared, stateless logic.
|
|
17
|
+
* independently issue their own `Runtime.evaluate`/`DOM.getDocument` calls,
|
|
18
|
+
* so there is no cross-collector call sharing beyond this shared, stateless logic.
|
|
20
19
|
*/
|
|
21
20
|
|
|
22
21
|
import type { ResolvedSourceLocation } from '../../source-map.js';
|
|
@@ -35,6 +34,7 @@ export { computeSpecificity, resolveNodeIds };
|
|
|
35
34
|
export type { Specificity, CSSRange, WinningDeclaration } from './style-provenance.js';
|
|
36
35
|
|
|
37
36
|
const STYLES_MAX_ELEMENTS = 150;
|
|
37
|
+
const PROVENANCE_CONCURRENCY = 8;
|
|
38
38
|
|
|
39
39
|
/** Full computed-style snapshot tracked per element — broad, cheap (no CDP round trip; read via `getComputedStyle` in one `Runtime.evaluate`). */
|
|
40
40
|
const COMPUTED_PROPERTIES = [
|
|
@@ -205,6 +205,20 @@ function resolvedIdentity(backendNodeId: number | undefined): { backendNodeId: n
|
|
|
205
205
|
return backendNodeId === undefined ? { backendNodeId: null, identityUnresolved: true } : { backendNodeId };
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/** Limits expensive per-element CDP provenance reads without changing document order. */
|
|
209
|
+
async function mapWithConcurrency<T, R>(values: readonly T[], limit: number, fn: (value: T, index: number) => Promise<R>): Promise<R[]> {
|
|
210
|
+
const result = new Array<R>(values.length);
|
|
211
|
+
let next = 0;
|
|
212
|
+
const worker = async (): Promise<void> => {
|
|
213
|
+
while (next < values.length) {
|
|
214
|
+
const index = next++;
|
|
215
|
+
result[index] = await fn(values[index]!, index);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
await Promise.all(Array.from({ length: Math.min(limit, values.length) }, worker));
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
208
222
|
// ============================================================================
|
|
209
223
|
// Collector
|
|
210
224
|
// ============================================================================
|
|
@@ -213,9 +227,8 @@ export const collectStyles: Collector = async (ctx) => {
|
|
|
213
227
|
const { client } = ctx;
|
|
214
228
|
|
|
215
229
|
// Registers the CSS.styleSheetAdded listener and forces header redelivery
|
|
216
|
-
// BEFORE
|
|
217
|
-
// module doc for why
|
|
218
|
-
// concurrently-running `layers` collector's own copy of the same step.
|
|
230
|
+
// BEFORE this collector uses the CSS domain — see style-provenance.ts's
|
|
231
|
+
// module doc for why header capture must begin before provenance reads.
|
|
219
232
|
const {
|
|
220
233
|
urls: styleSheetUrls,
|
|
221
234
|
stop: stopTrackingStyleSheets,
|
|
@@ -292,8 +305,7 @@ async function collectStylesInner(
|
|
|
292
305
|
|
|
293
306
|
const sourceCache = new Map<string, Promise<ResolvedSourceLocation>>();
|
|
294
307
|
|
|
295
|
-
const elements
|
|
296
|
-
facts.map(async (fact, index) => {
|
|
308
|
+
const elements = await mapWithConcurrency(facts, PROVENANCE_CONCURRENCY, async (fact, index): Promise<StylesElementRecord> => {
|
|
297
309
|
const ref = resolved[index];
|
|
298
310
|
const computed = normalizeComputed(fact.computed);
|
|
299
311
|
|
|
@@ -334,8 +346,7 @@ async function collectStylesInner(
|
|
|
334
346
|
winningDeclarations,
|
|
335
347
|
provenanceUnavailable,
|
|
336
348
|
};
|
|
337
|
-
})
|
|
338
|
-
);
|
|
349
|
+
});
|
|
339
350
|
|
|
340
351
|
ctx.write.json('styles.json', {
|
|
341
352
|
elements,
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
* identifier `CSS.getPlatformFontsForNode` accepts — and `backendNodeId`.
|
|
28
28
|
* No `setAttribute`/`removeAttribute` pair and no page-observable global
|
|
29
29
|
* either, so nothing in the emitted `screenshot.png`/`dom.html`/any other
|
|
30
|
-
* baseline
|
|
31
|
-
* `snapshot.ts`) can ever observe this collector having run.
|
|
30
|
+
* baseline artifact capture can ever observe this collector having run.
|
|
32
31
|
*/
|
|
33
32
|
|
|
34
33
|
import type { CDPClient } from '../../client.js';
|
|
@@ -421,9 +420,8 @@ async function resolveIndexedObjectIds(
|
|
|
421
420
|
export const collectText: Collector = async (ctx) => {
|
|
422
421
|
// The walk's return value is a plain in-memory `{ facts, elements }`
|
|
423
422
|
// object — never a `window` global, never a live-DOM attribute — so
|
|
424
|
-
// nothing another baseline
|
|
425
|
-
//
|
|
426
|
-
// `snapshot.ts`) could ever observe. `Runtime.evaluate({returnByValue:
|
|
423
|
+
// nothing another baseline artifact capture could ever observe.
|
|
424
|
+
// `Runtime.evaluate({returnByValue:
|
|
427
425
|
// false})` hands that object back as a CDP `RemoteObject` (an `objectId`
|
|
428
426
|
// with zero page visibility into it); every held `objectId` (the
|
|
429
427
|
// container, `facts`, `elements`) is released via `Runtime.releaseObject`
|