@crouton-kit/capture 1.3.8 → 1.3.10
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 +29841 -8539
- package/package.json +6 -3
- package/src/capture.ts +86 -173
- package/src/cdp/a11y.ts +113 -14
- package/src/cdp/args.ts +300 -205
- package/src/cdp/bridge/protocol.ts +93 -6
- package/src/cdp/bridge/server.ts +118 -53
- package/src/cdp/bridge/spawn.ts +53 -6
- package/src/cdp/client.ts +13 -3
- package/src/cdp/commands/bridge-serve.ts +20 -14
- package/src/cdp/commands/cdp.ts +259 -90
- package/src/cdp/commands/gate-guard.ts +43 -0
- package/src/cdp/commands/lib.ts +339 -213
- package/src/cdp/commands/measure/census.ts +76 -20
- package/src/cdp/commands/measure/check.ts +100 -26
- package/src/cdp/commands/measure/diff.ts +109 -21
- package/src/cdp/commands/measure/explain.ts +121 -19
- package/src/cdp/commands/measure/index.ts +31 -39
- package/src/cdp/commands/measure/map-ax.ts +52 -0
- package/src/cdp/commands/measure/map-focus.ts +41 -13
- package/src/cdp/commands/measure/map-layers.ts +49 -15
- package/src/cdp/commands/measure/map-scroll.ts +85 -15
- package/src/cdp/commands/measure/snap.ts +405 -25
- package/src/cdp/commands/measure/sweep.ts +217 -20
- package/src/cdp/commands/motion/index.ts +18 -17
- package/src/cdp/commands/motion/jank.ts +119 -9
- package/src/cdp/commands/motion/mask.ts +81 -12
- package/src/cdp/commands/motion/rec.ts +520 -19
- package/src/cdp/commands/motion/response.ts +65 -15
- package/src/cdp/commands/motion/timeline.ts +139 -14
- 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/detect.ts +42 -13
- package/src/cdp/dispatch.ts +74 -28
- 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 +1478 -105
- 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/targets.ts +21 -1
- package/src/cdp/timing.ts +15 -10
- package/src/cdp/types.ts +22 -7
- 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 +6 -0
- package/src/output/exact-raw.ts +114 -0
- package/src/output/render.ts +396 -2
- package/src/output/structured.ts +211 -0
- package/src/registry.ts +208 -0
- package/src/session/artifacts.ts +452 -200
- package/src/session/commands.ts +1162 -397
- 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 +279 -51
- 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 -79
- package/src/tab-session.ts +0 -91
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `styles.json` collector — computed style values plus winning-rule
|
|
3
|
+
* provenance (which selector/rule wins a property, by what specificity,
|
|
4
|
+
* from where in the authored source) for a bounded, document-order set of
|
|
5
|
+
* elements.
|
|
6
|
+
*
|
|
7
|
+
* The cascade/specificity/source-resolution engine and the
|
|
8
|
+
* `CSS.styleSheetAdded` header-capture helper live in `./style-provenance.js`
|
|
9
|
+
* — shared with `layers.ts`'s layer-triggering-property provenance so both
|
|
10
|
+
* collectors report identical winning-declaration semantics instead of two
|
|
11
|
+
* parallel implementations. This file also exports {@link resolveNodeIds}
|
|
12
|
+
* and {@link computeSpecificity} (re-exported from that shared module) —
|
|
13
|
+
* the CDP `nodeId`+`backendNodeId` correlation helper built for this
|
|
14
|
+
* file's `CSS.getMatchedStylesForNode` calls (which need a CDP `nodeId`,
|
|
15
|
+
* not just a `backendNodeId`) and reused by `media.ts` for its own
|
|
16
|
+
* per-element `backendNodeId` correlation. Both `styles.ts` and `media.ts`
|
|
17
|
+
* independently issue their own `Runtime.evaluate`/`DOM.getDocument`
|
|
18
|
+
* calls — they run concurrently in `snapshot.ts`'s `Promise.all`, so there
|
|
19
|
+
* is no cross-collector call sharing beyond this shared, stateless logic.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { ResolvedSourceLocation } from '../../source-map.js';
|
|
23
|
+
import { sanitizeString } from '../redaction.js';
|
|
24
|
+
import type { Collector } from '../types.js';
|
|
25
|
+
import {
|
|
26
|
+
buildWinningDeclarations,
|
|
27
|
+
captureStyleSheetHeaders,
|
|
28
|
+
computeSpecificity,
|
|
29
|
+
resolveNodeIds,
|
|
30
|
+
type CDPMatchedStylesResponse,
|
|
31
|
+
type WinningDeclaration,
|
|
32
|
+
} from './style-provenance.js';
|
|
33
|
+
|
|
34
|
+
export { computeSpecificity, resolveNodeIds };
|
|
35
|
+
export type { Specificity, CSSRange, WinningDeclaration } from './style-provenance.js';
|
|
36
|
+
|
|
37
|
+
const STYLES_MAX_ELEMENTS = 150;
|
|
38
|
+
|
|
39
|
+
/** Full computed-style snapshot tracked per element — broad, cheap (no CDP round trip; read via `getComputedStyle` in one `Runtime.evaluate`). */
|
|
40
|
+
const COMPUTED_PROPERTIES = [
|
|
41
|
+
'display', 'position', 'top', 'right', 'bottom', 'left', 'width', 'height',
|
|
42
|
+
'min-width', 'min-height', 'max-width', 'max-height',
|
|
43
|
+
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
44
|
+
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
45
|
+
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width', 'border-radius',
|
|
46
|
+
'box-sizing', 'z-index', 'opacity', 'visibility', 'overflow-x', 'overflow-y',
|
|
47
|
+
'color', 'background-color', 'font-family', 'font-size', 'font-weight', 'line-height',
|
|
48
|
+
'text-align', 'text-overflow', 'white-space',
|
|
49
|
+
'flex-direction', 'flex-wrap', 'justify-content', 'align-items', 'align-self', 'flex-grow', 'flex-shrink', 'flex-basis',
|
|
50
|
+
'grid-template-columns', 'grid-template-rows',
|
|
51
|
+
'transform', 'filter', 'cursor', 'pointer-events',
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
/** The smaller subset tracked for winning-declaration provenance — bounded by the `CSS.getMatchedStylesForNode` round trip cost per element. */
|
|
55
|
+
const PROVENANCE_PROPERTIES = [
|
|
56
|
+
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
57
|
+
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
58
|
+
'width', 'height', 'color', 'background-color', 'font-size', 'font-weight',
|
|
59
|
+
'border-radius', 'z-index', 'display', 'position',
|
|
60
|
+
] as const;
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Output shape
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
export interface StylesElementRecord {
|
|
67
|
+
id: string;
|
|
68
|
+
selector?: string;
|
|
69
|
+
/** `null` when this element's identity could not be resolved (see {@link identityUnresolved}) — never simply omitted (I-3). */
|
|
70
|
+
backendNodeId: number | null;
|
|
71
|
+
/** `true` when {@link backendNodeId} is `null` because this element's `cssPath` selector did not resolve to a CDP node — absent (not `false`) when resolved, matching hittest.ts's convention. */
|
|
72
|
+
identityUnresolved?: true;
|
|
73
|
+
computed: Record<string, string | null>;
|
|
74
|
+
winningDeclarations: WinningDeclaration[];
|
|
75
|
+
/** `true` when node correlation or `CSS.getMatchedStylesForNode` failed for this element — `winningDeclarations` is empty because provenance could not be inspected, not because no rule matched anything. This is DISTINCT from {@link identityUnresolved}: `provenanceUnavailable` means "couldn't inspect the winning CSS rule", not "couldn't resolve this element's identity" — an element can have a resolved `backendNodeId` and still have `provenanceUnavailable` (e.g. `CSS.getMatchedStylesForNode` itself failed). */
|
|
76
|
+
provenanceUnavailable?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Explicit factual scope for `styles.json` (D5). The in-page enumeration is
|
|
81
|
+
* `document.querySelectorAll('*')` on the top document — light-DOM only,
|
|
82
|
+
* non-piercing (`pierce:false` in spirit): it walks neither nested iframe
|
|
83
|
+
* documents nor shadow roots. Absent iframe/shadow styles are a stated scope
|
|
84
|
+
* boundary, not a negative fact. Counts state exactly what was skipped.
|
|
85
|
+
*/
|
|
86
|
+
export interface StylesCoverage {
|
|
87
|
+
scope: 'top-document';
|
|
88
|
+
/** Count of `<iframe>` elements in the top document whose contents are NOT enumerated. */
|
|
89
|
+
iframesNotWalked: number;
|
|
90
|
+
/** Count of shadow-root hosts in the top document whose shadow trees are NOT enumerated. */
|
|
91
|
+
shadowRootsNotWalked: number;
|
|
92
|
+
/** Total elements that passed the tag/visible-rect filter — the uncapped candidate count, before {@link STYLES_MAX_ELEMENTS} applies. */
|
|
93
|
+
totalCandidateElements: number;
|
|
94
|
+
/** Elements actually emitted in `elements[]` — `<= totalCandidateElements`, and `<= STYLES_MAX_ELEMENTS`. */
|
|
95
|
+
keptElements: number;
|
|
96
|
+
/** `true` when {@link STYLES_MAX_ELEMENTS} dropped one or more candidate elements from enumeration — an explicit fact (I-5), never a silent cap. */
|
|
97
|
+
elementsTruncated: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface StylesReport {
|
|
101
|
+
elements: StylesElementRecord[];
|
|
102
|
+
coverage: StylesCoverage;
|
|
103
|
+
/** Availability of the `CSS.styleSheetAdded` header capture this snap's provenance resolution depends on (I-5) — `available:false` means any missing `sourceStyleSheetUrl`/`generated` source below is a capture failure, not "genuinely no source". */
|
|
104
|
+
styleSheetHeaders: { available: boolean; reason?: string };
|
|
105
|
+
/** Explicit identity-resolution availability fact (I-4), mirroring media.json: `available:false` means `backendNodeId` was never attempted for ANY element this run (`DOM.getDocument`/`resolveNodeIds` failed) — distinct from a per-element `backendNodeId` simply being `null` because that one element's selector didn't resolve while the system was healthy. */
|
|
106
|
+
identity: { available: true } | { available: false; reason: string };
|
|
107
|
+
/** `false` when the `STYLES_SCRIPT` inventory `Runtime.evaluate` itself failed (threw, or returned no `value`) — `elements: []` is then "could not collect", not "genuinely no styleable elements" (I-5). Always `true` on a normal run. */
|
|
108
|
+
available: boolean;
|
|
109
|
+
/** Present only when `available` is `false`. */
|
|
110
|
+
unavailableReason?: StylesUnavailableReason;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Fixed, factual reason `STYLES_SCRIPT`'s `Runtime.evaluate` could not be read (never a raw exception message, which is unbounded/page-influenced) — present only when {@link StylesReport.available} is `false`. */
|
|
114
|
+
export type StylesUnavailableReason = 'styles-evaluate-returned-no-value' | 'styles-evaluate-threw';
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// In-page script — cssPath + computed style facts, no DOM domain calls
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
// `html`/`body` are excluded: `cssPathFromBody` is rooted AT `document.body`, so it produces `''` for
|
|
121
|
+
// the body element itself and `body > html` for the (out-of-subtree) html element — neither is a valid
|
|
122
|
+
// `body > ...` selector for `resolveNodeIds` to query. Both are structural roots, not measurable subjects.
|
|
123
|
+
const EXCLUDED_TAGS = ['html', 'body', 'script', 'style', 'head', 'meta', 'link', 'title', 'template', 'noscript', 'br', 'wbr'];
|
|
124
|
+
|
|
125
|
+
interface StylesFact {
|
|
126
|
+
cssPath: string;
|
|
127
|
+
computed: Record<string, string | null>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface StylesInventory {
|
|
131
|
+
elements: StylesFact[];
|
|
132
|
+
/** Total elements passing the tag/visible-rect filter, counted even past the {@link STYLES_MAX_ELEMENTS} cap (I-5) — `elements.length` alone cannot distinguish "the page had exactly this many" from "the cap stopped counting". */
|
|
133
|
+
total: number;
|
|
134
|
+
iframesNotWalked: number;
|
|
135
|
+
shadowRootsNotWalked: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const STYLES_SCRIPT = `/* __captureStylesInventory */
|
|
139
|
+
(function() {
|
|
140
|
+
var EXCLUDED = ${JSON.stringify(EXCLUDED_TAGS)};
|
|
141
|
+
var PROPS = ${JSON.stringify(COMPUTED_PROPERTIES)};
|
|
142
|
+
var excludedSet = {};
|
|
143
|
+
for (var e = 0; e < EXCLUDED.length; e++) excludedSet[EXCLUDED[e]] = true;
|
|
144
|
+
|
|
145
|
+
function cssPathFromBody(el) {
|
|
146
|
+
var parts = [];
|
|
147
|
+
var node = el;
|
|
148
|
+
var depth = 0;
|
|
149
|
+
while (node && node.nodeType === 1 && node !== document.body && depth < 40) {
|
|
150
|
+
var parent = node.parentElement;
|
|
151
|
+
if (!parent) { parts.unshift(node.tagName.toLowerCase()); break; }
|
|
152
|
+
var same = Array.prototype.filter.call(parent.children, function(c) { return c.tagName === node.tagName; });
|
|
153
|
+
var idx = same.indexOf(node) + 1;
|
|
154
|
+
parts.unshift(node.tagName.toLowerCase() + ':nth-of-type(' + idx + ')');
|
|
155
|
+
node = parent;
|
|
156
|
+
depth++;
|
|
157
|
+
}
|
|
158
|
+
return parts.join(' > ');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
var all = document.querySelectorAll('*');
|
|
162
|
+
// Scope facts (D5): the enumeration is light-DOM only (querySelectorAll does not
|
|
163
|
+
// pierce iframe documents or shadow roots), so count what is deliberately skipped.
|
|
164
|
+
var iframesNotWalked = 0;
|
|
165
|
+
var shadowRootsNotWalked = 0;
|
|
166
|
+
for (var c = 0; c < all.length; c++) {
|
|
167
|
+
if (all[c].tagName === 'IFRAME') iframesNotWalked++;
|
|
168
|
+
if (all[c].shadowRoot) shadowRootsNotWalked++;
|
|
169
|
+
}
|
|
170
|
+
var out = [];
|
|
171
|
+
var total = 0;
|
|
172
|
+
// Scans the FULL element set (not stopping at the cap) so total is an honest count of every
|
|
173
|
+
// candidate the cap dropped, not just an estimate (I-5) — the enumeration itself is cheap
|
|
174
|
+
// relative to the getComputedStyle() calls this loop already skips once the cap is reached.
|
|
175
|
+
for (var i = 0; i < all.length; i++) {
|
|
176
|
+
var el = all[i];
|
|
177
|
+
var tag = el.tagName.toLowerCase();
|
|
178
|
+
if (excludedSet[tag]) continue;
|
|
179
|
+
var rect = el.getBoundingClientRect();
|
|
180
|
+
if (rect.width <= 0 && rect.height <= 0) continue;
|
|
181
|
+
total++;
|
|
182
|
+
if (out.length >= ${STYLES_MAX_ELEMENTS}) continue;
|
|
183
|
+
var cs;
|
|
184
|
+
try { cs = getComputedStyle(el); } catch (err) { continue; }
|
|
185
|
+
var computed = {};
|
|
186
|
+
for (var p = 0; p < PROPS.length; p++) {
|
|
187
|
+
try { computed[PROPS[p]] = cs.getPropertyValue(PROPS[p]); } catch (err2) { computed[PROPS[p]] = null; }
|
|
188
|
+
}
|
|
189
|
+
out.push({ cssPath: cssPathFromBody(el), computed: computed });
|
|
190
|
+
}
|
|
191
|
+
return { elements: out, total: total, iframesNotWalked: iframesNotWalked, shadowRootsNotWalked: shadowRootsNotWalked };
|
|
192
|
+
})();`;
|
|
193
|
+
|
|
194
|
+
function normalizeComputed(raw: Record<string, string | null> | undefined): Record<string, string | null> {
|
|
195
|
+
const out: Record<string, string | null> = {};
|
|
196
|
+
for (const prop of COMPUTED_PROPERTIES) {
|
|
197
|
+
const value = raw?.[prop];
|
|
198
|
+
out[prop] = typeof value === 'string' ? sanitizeString(value) : null;
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Honest `{ backendNodeId, identityUnresolved }` pair for an element-bearing record — mirrors hittest.ts's `resolvedIdentity` helper (I-3/I-5, inlined locally per collector rather than imported/shared): a resolved identity carries just `backendNodeId`; an unresolved one carries `backendNodeId: null` + `identityUnresolved: true`, never a silently-omitted field. */
|
|
204
|
+
function resolvedIdentity(backendNodeId: number | undefined): { backendNodeId: number | null; identityUnresolved?: true } {
|
|
205
|
+
return backendNodeId === undefined ? { backendNodeId: null, identityUnresolved: true } : { backendNodeId };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// Collector
|
|
210
|
+
// ============================================================================
|
|
211
|
+
|
|
212
|
+
export const collectStyles: Collector = async (ctx) => {
|
|
213
|
+
const { client } = ctx;
|
|
214
|
+
|
|
215
|
+
// Registers the CSS.styleSheetAdded listener and forces header redelivery
|
|
216
|
+
// BEFORE anything else touches the CSS domain — see style-provenance.ts's
|
|
217
|
+
// module doc for why this must run first and why it's safe alongside the
|
|
218
|
+
// concurrently-running `layers` collector's own copy of the same step.
|
|
219
|
+
const {
|
|
220
|
+
urls: styleSheetUrls,
|
|
221
|
+
stop: stopTrackingStyleSheets,
|
|
222
|
+
available: styleSheetHeadersAvailable,
|
|
223
|
+
reason: styleSheetHeadersReason,
|
|
224
|
+
} = await captureStyleSheetHeaders(client);
|
|
225
|
+
try {
|
|
226
|
+
await collectStylesInner(ctx, styleSheetUrls, { available: styleSheetHeadersAvailable, reason: styleSheetHeadersReason });
|
|
227
|
+
} finally {
|
|
228
|
+
stopTrackingStyleSheets();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
async function collectStylesInner(
|
|
233
|
+
ctx: Parameters<Collector>[0],
|
|
234
|
+
styleSheetUrls: Map<string, string>,
|
|
235
|
+
styleSheetHeaders: { available: boolean; reason?: string },
|
|
236
|
+
): Promise<void> {
|
|
237
|
+
const { client } = ctx;
|
|
238
|
+
|
|
239
|
+
// I-5: a missing `value` (the eval failed/returned nothing) is currently coerced into an empty
|
|
240
|
+
// inventory, indistinguishable from a genuinely element-free page unless the failure itself is
|
|
241
|
+
// surfaced as an explicit report-level fact.
|
|
242
|
+
let inventory: StylesInventory;
|
|
243
|
+
let available = true;
|
|
244
|
+
let unavailableReason: StylesUnavailableReason | undefined;
|
|
245
|
+
try {
|
|
246
|
+
const evalResponse = (await client.send('Runtime.evaluate', {
|
|
247
|
+
expression: STYLES_SCRIPT,
|
|
248
|
+
returnByValue: true,
|
|
249
|
+
})) as { result?: { value?: StylesInventory } };
|
|
250
|
+
const value = evalResponse.result?.value;
|
|
251
|
+
if (value === undefined) {
|
|
252
|
+
inventory = { elements: [], total: 0, iframesNotWalked: 0, shadowRootsNotWalked: 0 };
|
|
253
|
+
available = false;
|
|
254
|
+
unavailableReason = 'styles-evaluate-returned-no-value';
|
|
255
|
+
} else {
|
|
256
|
+
inventory = value;
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
inventory = { elements: [], total: 0, iframesNotWalked: 0, shadowRootsNotWalked: 0 };
|
|
260
|
+
available = false;
|
|
261
|
+
unavailableReason = 'styles-evaluate-threw';
|
|
262
|
+
}
|
|
263
|
+
const facts = inventory.elements;
|
|
264
|
+
|
|
265
|
+
// I-3/I-4: distinguishes a whole-run identity-resolution failure (DOM.getDocument/resolveNodeIds
|
|
266
|
+
// never even ran) from a per-element miss (that one element's selector didn't resolve while the
|
|
267
|
+
// system was healthy) — mirrors media.ts's `identity` fact exactly.
|
|
268
|
+
let documentNodeId: number | undefined;
|
|
269
|
+
let identity: StylesReport['identity'] = { available: true };
|
|
270
|
+
try {
|
|
271
|
+
const docResponse = (await client.send('DOM.getDocument', { depth: 0 })) as { root?: { nodeId: number } };
|
|
272
|
+
documentNodeId = docResponse.root?.nodeId;
|
|
273
|
+
if (documentNodeId === undefined) {
|
|
274
|
+
identity = { available: false, reason: 'dom-getdocument-unavailable' };
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
documentNodeId = undefined;
|
|
278
|
+
identity = { available: false, reason: 'dom-getdocument-unavailable' };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let resolved: Array<{ nodeId?: number; backendNodeId?: number } | undefined>;
|
|
282
|
+
if (documentNodeId !== undefined) {
|
|
283
|
+
try {
|
|
284
|
+
resolved = await resolveNodeIds(client, documentNodeId, facts.map((f) => f.cssPath));
|
|
285
|
+
} catch {
|
|
286
|
+
resolved = facts.map(() => undefined);
|
|
287
|
+
identity = { available: false, reason: 'resolve-node-ids-failed' };
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
resolved = facts.map(() => undefined);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const sourceCache = new Map<string, Promise<ResolvedSourceLocation>>();
|
|
294
|
+
|
|
295
|
+
const elements: StylesElementRecord[] = await Promise.all(
|
|
296
|
+
facts.map(async (fact, index) => {
|
|
297
|
+
const ref = resolved[index];
|
|
298
|
+
const computed = normalizeComputed(fact.computed);
|
|
299
|
+
|
|
300
|
+
// A `noDeclarationRecord` is reserved for a SUCCESSFUL matched-styles response that genuinely had
|
|
301
|
+
// no candidate for a property — an honest "nothing declares this" fact. When provenance itself
|
|
302
|
+
// couldn't be inspected (no CDP nodeId, or `CSS.getMatchedStylesForNode` failed), that's a different
|
|
303
|
+
// fact — "unknown", not "none" — so `winningDeclarations` is left empty and `provenanceUnavailable`
|
|
304
|
+
// is set, rather than conflating the two into misleading no-declaration records.
|
|
305
|
+
let winningDeclarations: WinningDeclaration[];
|
|
306
|
+
let provenanceUnavailable: boolean | undefined;
|
|
307
|
+
if (ref?.nodeId !== undefined) {
|
|
308
|
+
try {
|
|
309
|
+
const matched = (await client.send('CSS.getMatchedStylesForNode', {
|
|
310
|
+
nodeId: ref.nodeId,
|
|
311
|
+
})) as CDPMatchedStylesResponse;
|
|
312
|
+
winningDeclarations = await buildWinningDeclarations(
|
|
313
|
+
client,
|
|
314
|
+
matched,
|
|
315
|
+
computed,
|
|
316
|
+
sourceCache,
|
|
317
|
+
styleSheetUrls,
|
|
318
|
+
PROVENANCE_PROPERTIES,
|
|
319
|
+
);
|
|
320
|
+
} catch {
|
|
321
|
+
winningDeclarations = [];
|
|
322
|
+
provenanceUnavailable = true;
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
winningDeclarations = [];
|
|
326
|
+
provenanceUnavailable = true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
id: `s-${index}`,
|
|
331
|
+
selector: sanitizeString(fact.cssPath),
|
|
332
|
+
...resolvedIdentity(ref?.backendNodeId),
|
|
333
|
+
computed,
|
|
334
|
+
winningDeclarations,
|
|
335
|
+
provenanceUnavailable,
|
|
336
|
+
};
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
ctx.write.json('styles.json', {
|
|
341
|
+
elements,
|
|
342
|
+
coverage: {
|
|
343
|
+
scope: 'top-document',
|
|
344
|
+
iframesNotWalked: inventory.iframesNotWalked,
|
|
345
|
+
shadowRootsNotWalked: inventory.shadowRootsNotWalked,
|
|
346
|
+
totalCandidateElements: inventory.total,
|
|
347
|
+
keptElements: elements.length,
|
|
348
|
+
elementsTruncated: inventory.total > elements.length,
|
|
349
|
+
},
|
|
350
|
+
styleSheetHeaders: styleSheetHeaders,
|
|
351
|
+
identity,
|
|
352
|
+
available,
|
|
353
|
+
...(unavailableReason ? { unavailableReason } : {}),
|
|
354
|
+
} satisfies StylesReport);
|
|
355
|
+
}
|