@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,1038 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `geometry.json` collector — element rects, quads, box-model detail,
|
|
3
|
+
* layout provenance (flex/grid/clipping/stacking-context), and (on an
|
|
4
|
+
* unsettled `--capture-unsettled` capture) the unstable-region markers.
|
|
5
|
+
*
|
|
6
|
+
* Architecture: ONE `Runtime.evaluate` walks the whole reachable tree
|
|
7
|
+
* (document body, open shadow roots, and same-origin iframe
|
|
8
|
+
* `contentDocument`s) and returns page-side FACTS (selector, computed-style
|
|
9
|
+
* provenance, frame/shadow context) keyed by `idx` — never geometry. The
|
|
10
|
+
* walk never assigns anything to `window` or any other page-observable
|
|
11
|
+
* location: its return value is a plain in-memory `{ facts, elements }`
|
|
12
|
+
* object, read back purely through CDP's own remote-object identity —
|
|
13
|
+
* `Runtime.evaluate({returnByValue: false})` hands back an `objectId` for
|
|
14
|
+
* that held object with zero page visibility into it, `Runtime.getProperties`
|
|
15
|
+
* resolves `facts`'/`elements`'s own `objectId`s ({@link ownPropertyObjectIds}),
|
|
16
|
+
* `Runtime.callFunctionOn({returnByValue: true})` reads `facts` out by value
|
|
17
|
+
* ({@link readHeldValue}), and a second `Runtime.getProperties` on the held
|
|
18
|
+
* `elements` objectId resolves each matched element's own `objectId` in one
|
|
19
|
+
* round trip ({@link resolveIndexedObjectIds}) — no N separate evaluates,
|
|
20
|
+
* and no page-observable global for a page to define a setter against. From
|
|
21
|
+
* there, `DOM.describeNode` (backendNodeId) is fetched per element, and
|
|
22
|
+
* `DOM.getContentQuads`/`DOM.getBoxModel` are read through this file's own
|
|
23
|
+
* {@link readContentQuads}/{@link readBoxModel} — deliberately NOT
|
|
24
|
+
* `../../coordinates.js`'s combined `getContentQuadBox`, because Chrome
|
|
25
|
+
* throws `DOM.getBoxModel` for EVERY node with no layout box (the identical
|
|
26
|
+
* throw a genuinely invalid/detached node reference produces), so a
|
|
27
|
+
* combined one-catch read would make a real `display:none`/no-box element
|
|
28
|
+
* indistinguishable from an actual failed read. `readContentQuads` is
|
|
29
|
+
* called first and alone; only a THROW from it — never a successful empty
|
|
30
|
+
* result — counts as a per-element geometry read failure (I-4/I-5,
|
|
31
|
+
* {@link GeometryElementRecord.geometryUnavailable}). `readBoxModel` is
|
|
32
|
+
* only attempted once real (non-empty) quads prove a box exists, so its own
|
|
33
|
+
* throw can only mean the detail read failed, not that the box never
|
|
34
|
+
* existed. Every held container/facts/elements
|
|
35
|
+
* `objectId` is released via `Runtime.releaseObject` in `finally`
|
|
36
|
+
* (unconditionally), so a later capture on the same tab never collides with
|
|
37
|
+
* anything this run held — a release failure is recorded as the factual
|
|
38
|
+
* `bridgeCleanupFailed`, but can never imply a contaminated baseline: there
|
|
39
|
+
* is no page-observable state left to leak, only CDP-session-scoped
|
|
40
|
+
* remote-object memory freed when the tab closes.
|
|
41
|
+
*
|
|
42
|
+
* Coordinate space: every reported `rect`/`quads`/`boxModel` comes from
|
|
43
|
+
* real CDP `DOM.getContentQuads`/`DOM.getBoxModel` calls — never from JS
|
|
44
|
+
* `getBoundingClientRect()` math. Empirically confirmed (see U07's final
|
|
45
|
+
* report) that CDP already returns these quads in TOP-VIEWPORT space for
|
|
46
|
+
* same-process (non-OOPIF) nested iframes over one un-sessioned CDP
|
|
47
|
+
* connection — including correct vertical margin-collapse behavior — so no
|
|
48
|
+
* `composeFrameTransform`/`toTopViewportQuad` stitching is needed here.
|
|
49
|
+
* `getBoundingClientRect()` is used ONLY for same-document-local math
|
|
50
|
+
* (visibility/zero-size flags, the clipping-ancestor ratio calc) — never
|
|
51
|
+
* reported as final geometry.
|
|
52
|
+
*
|
|
53
|
+
* U07 note: this file keeps spreading `ctx.unstableRegions` into the
|
|
54
|
+
* written object's `unstableRegions` field exactly as the original stub
|
|
55
|
+
* did; it is NOT recomputed here. The orchestrator (`snapshot.ts`) already
|
|
56
|
+
* computed and passed it in via `ctx` — this is what makes
|
|
57
|
+
* "`--capture-unsettled` writes geometry.json with unstable-region ids"
|
|
58
|
+
* true regardless of which unit last touched this file.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
import type { CDPClient } from '../../client.js';
|
|
62
|
+
import { axisAlignedRectFromQuad, type Quad, type Rect } from '../../coordinates.js';
|
|
63
|
+
import { capArray, sanitizeString } from '../redaction.js';
|
|
64
|
+
import type { Collector, ElementRecord } from '../types.js';
|
|
65
|
+
|
|
66
|
+
/** Defensive cap on how many elements one geometry walk will record for a single snapshot. */
|
|
67
|
+
const MAX_ELEMENTS = 1200;
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Shared JS helpers — inlined (by string concatenation) into BOTH this
|
|
71
|
+
// file's walk script and hittest.ts's walk/sample script. Exported so
|
|
72
|
+
// hittest.ts imports rather than duplicates. NONE of these cap in-page:
|
|
73
|
+
// every page-controlled string they build is returned at full length and
|
|
74
|
+
// is capped node-side through redaction.ts `sanitizeString` (the single
|
|
75
|
+
// cap authority).
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
export const SELECTOR_HELPER_JS = `
|
|
79
|
+
function __selectorOf(el) {
|
|
80
|
+
try {
|
|
81
|
+
if (!el || el.nodeType !== 1) return undefined;
|
|
82
|
+
if (el.id) return '#' + el.id;
|
|
83
|
+
var parts = [];
|
|
84
|
+
var node = el;
|
|
85
|
+
var depth = 0;
|
|
86
|
+
while (node && node.nodeType === 1 && depth < 6) {
|
|
87
|
+
var part = node.tagName.toLowerCase();
|
|
88
|
+
if (node.id) {
|
|
89
|
+
part += '#' + node.id;
|
|
90
|
+
parts.unshift(part);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (node.classList && node.classList.length) {
|
|
94
|
+
part += '.' + Array.prototype.slice.call(node.classList).slice(0, 3).join('.');
|
|
95
|
+
}
|
|
96
|
+
var parent = node.parentElement;
|
|
97
|
+
if (parent) {
|
|
98
|
+
var siblings = Array.prototype.filter.call(parent.children, function (c) { return c.tagName === node.tagName; });
|
|
99
|
+
if (siblings.length > 1) part += ':nth-of-type(' + (siblings.indexOf(node) + 1) + ')';
|
|
100
|
+
}
|
|
101
|
+
parts.unshift(part);
|
|
102
|
+
node = parent;
|
|
103
|
+
depth += 1;
|
|
104
|
+
}
|
|
105
|
+
return parts.join(' > ');
|
|
106
|
+
} catch (e) { return undefined; }
|
|
107
|
+
}
|
|
108
|
+
function __domPathOf(el) {
|
|
109
|
+
try {
|
|
110
|
+
var parts = [];
|
|
111
|
+
var node = el;
|
|
112
|
+
var hops = 0;
|
|
113
|
+
while (node && node.nodeType === 1 && hops < 200) {
|
|
114
|
+
var idx = 0;
|
|
115
|
+
var sib = node;
|
|
116
|
+
while ((sib = sib.previousElementSibling)) idx += 1;
|
|
117
|
+
parts.unshift(node.tagName.toLowerCase() + '[' + idx + ']');
|
|
118
|
+
var parentEl = node.parentElement;
|
|
119
|
+
if (!parentEl) {
|
|
120
|
+
var root = node.getRootNode ? node.getRootNode() : null;
|
|
121
|
+
parentEl = root && root.host ? root.host : null;
|
|
122
|
+
}
|
|
123
|
+
node = parentEl;
|
|
124
|
+
hops += 1;
|
|
125
|
+
}
|
|
126
|
+
return parts.join('/') || '';
|
|
127
|
+
} catch (e) { return ''; }
|
|
128
|
+
}
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// The page-side walk. Runs once via `Runtime.evaluate({returnByValue: false})`
|
|
133
|
+
// -- the return value is held as a remote object (see the module doc's
|
|
134
|
+
// CDP-only identity bridge), never read back by value directly.
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
function buildGeometryWalkScript(maxElements: number): string {
|
|
138
|
+
return `(function () {
|
|
139
|
+
var MAX_ELEMENTS = ${maxElements};
|
|
140
|
+
var SKIP_TAGS = { SCRIPT: 1, STYLE: 1, TEMPLATE: 1, HEAD: 1, META: 1, LINK: 1, TITLE: 1, BASE: 1, NOSCRIPT: 1 };
|
|
141
|
+
var facts = [];
|
|
142
|
+
var elements = [];
|
|
143
|
+
var frameCounter = 0;
|
|
144
|
+
// Exact count of elements the MAX_ELEMENTS cap chose not to record --
|
|
145
|
+
// real elements the walk visited (recursed into) but skipped the
|
|
146
|
+
// expensive per-element fact-building for, never a page-observable value.
|
|
147
|
+
var elementsOverflowCount = 0;
|
|
148
|
+
|
|
149
|
+
function stackingReasons(style) {
|
|
150
|
+
var reasons = [];
|
|
151
|
+
if (style.position !== 'static' && style.zIndex !== 'auto') reasons.push('position+z-index');
|
|
152
|
+
var op = parseFloat(style.opacity);
|
|
153
|
+
if (!isNaN(op) && op < 1) reasons.push('opacity<1');
|
|
154
|
+
if (style.transform && style.transform !== 'none') reasons.push('transform');
|
|
155
|
+
if (style.filter && style.filter !== 'none') reasons.push('filter');
|
|
156
|
+
if (style.perspective && style.perspective !== 'none') reasons.push('perspective');
|
|
157
|
+
if (style.mixBlendMode && style.mixBlendMode !== 'normal') reasons.push('mix-blend-mode');
|
|
158
|
+
if (style.isolation === 'isolate') reasons.push('isolate');
|
|
159
|
+
if (style.willChange && /transform|opacity|filter/.test(style.willChange)) reasons.push('will-change');
|
|
160
|
+
if (style.contain && /layout|paint|strict|content/.test(style.contain)) reasons.push('contain');
|
|
161
|
+
if (style.position === 'fixed') reasons.push('fixed');
|
|
162
|
+
if (style.position === 'sticky') reasons.push('sticky');
|
|
163
|
+
return reasons;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function clippingInfo(el, rect) {
|
|
167
|
+
try {
|
|
168
|
+
if (rect.width === 0 && rect.height === 0) return null;
|
|
169
|
+
var node = el.parentElement;
|
|
170
|
+
var hops = 0;
|
|
171
|
+
var docEl = el.ownerDocument.documentElement;
|
|
172
|
+
while (node && node.nodeType === 1 && node !== docEl && hops < 50) {
|
|
173
|
+
var s = getComputedStyle(node);
|
|
174
|
+
var clipX = /hidden|auto|scroll|clip/.test(s.overflowX);
|
|
175
|
+
var clipY = /hidden|auto|scroll|clip/.test(s.overflowY);
|
|
176
|
+
if (clipX || clipY) {
|
|
177
|
+
var ar = node.getBoundingClientRect();
|
|
178
|
+
var ix1 = Math.max(rect.left, ar.left);
|
|
179
|
+
var iy1 = Math.max(rect.top, ar.top);
|
|
180
|
+
var ix2 = Math.min(rect.right, ar.right);
|
|
181
|
+
var iy2 = Math.min(rect.bottom, ar.bottom);
|
|
182
|
+
var iw = Math.max(0, ix2 - ix1);
|
|
183
|
+
var ih = Math.max(0, iy2 - iy1);
|
|
184
|
+
var selfArea = rect.width * rect.height;
|
|
185
|
+
var fraction = selfArea > 0 ? (iw * ih) / selfArea : 0;
|
|
186
|
+
if (fraction < 0.999) {
|
|
187
|
+
return { clippedBy: __selectorOf(node), clippedFraction: Math.round(fraction * 1000) / 1000 };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
node = node.parentElement;
|
|
191
|
+
hops += 1;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
} catch (e) { return null; }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function flexFactsFor(el, parentStyle) {
|
|
198
|
+
try {
|
|
199
|
+
var style = getComputedStyle(el);
|
|
200
|
+
return {
|
|
201
|
+
grow: parseFloat(style.flexGrow) || 0,
|
|
202
|
+
shrink: parseFloat(style.flexShrink) || 0,
|
|
203
|
+
basis: style.flexBasis,
|
|
204
|
+
alignSelf: style.alignSelf,
|
|
205
|
+
order: parseInt(style.order, 10) || 0,
|
|
206
|
+
container: {
|
|
207
|
+
direction: parentStyle.flexDirection,
|
|
208
|
+
wrap: parentStyle.flexWrap,
|
|
209
|
+
justifyContent: parentStyle.justifyContent,
|
|
210
|
+
alignItems: parentStyle.alignItems,
|
|
211
|
+
gap: parentStyle.gap,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
} catch (e) { return null; }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function gridFactsFor(el, parentStyle) {
|
|
218
|
+
try {
|
|
219
|
+
var style = getComputedStyle(el);
|
|
220
|
+
return {
|
|
221
|
+
columnStart: style.gridColumnStart,
|
|
222
|
+
columnEnd: style.gridColumnEnd,
|
|
223
|
+
rowStart: style.gridRowStart,
|
|
224
|
+
rowEnd: style.gridRowEnd,
|
|
225
|
+
container: {
|
|
226
|
+
templateColumns: parentStyle.gridTemplateColumns.split(' ').filter(Boolean),
|
|
227
|
+
templateRows: parentStyle.gridTemplateRows.split(' ').filter(Boolean),
|
|
228
|
+
columnGap: parentStyle.columnGap,
|
|
229
|
+
rowGap: parentStyle.rowGap,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
} catch (e) { return null; }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function directText(el) {
|
|
236
|
+
var out = '';
|
|
237
|
+
for (var i = 0; i < el.childNodes.length; i++) {
|
|
238
|
+
var n = el.childNodes[i];
|
|
239
|
+
if (n.nodeType === 3 && n.textContent) out += n.textContent;
|
|
240
|
+
}
|
|
241
|
+
out = out.replace(/\\s+/g, ' ').trim();
|
|
242
|
+
return out || undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function walk(el, frameId, isTop, ancestorFrameIds, shadowCtx) {
|
|
246
|
+
if (!el || el.nodeType !== 1) return;
|
|
247
|
+
var tag = el.tagName;
|
|
248
|
+
if (SKIP_TAGS[tag]) return;
|
|
249
|
+
|
|
250
|
+
// The MAX_ELEMENTS cap bounds only the EXPENSIVE per-element work below
|
|
251
|
+
// (getComputedStyle/rect/clip-ancestor-walk facts) -- recursion into
|
|
252
|
+
// shadow roots, same-origin iframes, and children always continues
|
|
253
|
+
// past the cap so elementsTruncated below is an EXACT count of the
|
|
254
|
+
// real elements this walk chose not to record, not a guess.
|
|
255
|
+
// Captures the fact object THIS call pushed (if any) so the IFRAME
|
|
256
|
+
// branch below can attach an honest failure marker directly to it --
|
|
257
|
+
// stays null when this element's own record was skipped by the
|
|
258
|
+
// MAX_ELEMENTS cap (recursion still continues past the cap, so a
|
|
259
|
+
// capped IFRAME's contentDocument read failure has no record to mark,
|
|
260
|
+
// same limitation geometryUnavailable already accepts for capped
|
|
261
|
+
// elements).
|
|
262
|
+
var pushedFact = null;
|
|
263
|
+
if (facts.length >= MAX_ELEMENTS) {
|
|
264
|
+
elementsOverflowCount += 1;
|
|
265
|
+
} else {
|
|
266
|
+
var style = getComputedStyle(el);
|
|
267
|
+
var rect = el.getBoundingClientRect();
|
|
268
|
+
var displayNone = style.display === 'none';
|
|
269
|
+
var visibilityHidden = style.visibility === 'hidden';
|
|
270
|
+
var opacity = parseFloat(style.opacity);
|
|
271
|
+
if (isNaN(opacity)) opacity = 1;
|
|
272
|
+
var zeroSize = rect.width === 0 && rect.height === 0;
|
|
273
|
+
var visible = !displayNone && !visibilityHidden && opacity > 0 && !zeroSize;
|
|
274
|
+
|
|
275
|
+
var parent = el.parentElement;
|
|
276
|
+
var parentStyle = parent ? getComputedStyle(parent) : null;
|
|
277
|
+
var isFlexChild = parentStyle && (parentStyle.display === 'flex' || parentStyle.display === 'inline-flex');
|
|
278
|
+
var isGridChild = parentStyle && (parentStyle.display === 'grid' || parentStyle.display === 'inline-grid');
|
|
279
|
+
|
|
280
|
+
var idx = facts.length;
|
|
281
|
+
elements.push(el);
|
|
282
|
+
var reasons = stackingReasons(style);
|
|
283
|
+
var factObj = {
|
|
284
|
+
idx: idx,
|
|
285
|
+
tag: tag.toLowerCase(),
|
|
286
|
+
selector: __selectorOf(el),
|
|
287
|
+
domPath: __domPathOf(el),
|
|
288
|
+
text: directText(el),
|
|
289
|
+
frame: { frameId: frameId, isTopFrame: isTop, ancestorFrameIds: ancestorFrameIds.slice() },
|
|
290
|
+
shadow: shadowCtx,
|
|
291
|
+
zIndex: style.zIndex || 'auto',
|
|
292
|
+
stackingContext: { creates: reasons.length > 0, reasons: reasons },
|
|
293
|
+
visibility: { visible: visible, opacity: opacity, displayNone: displayNone, visibilityHidden: visibilityHidden, zeroSize: zeroSize },
|
|
294
|
+
clipping: clippingInfo(el, rect),
|
|
295
|
+
layout: {
|
|
296
|
+
boxSizing: style.boxSizing,
|
|
297
|
+
position: style.position,
|
|
298
|
+
display: style.display,
|
|
299
|
+
overflowX: style.overflowX,
|
|
300
|
+
overflowY: style.overflowY,
|
|
301
|
+
scrollWidth: el.scrollWidth,
|
|
302
|
+
scrollHeight: el.scrollHeight,
|
|
303
|
+
clientWidth: el.clientWidth,
|
|
304
|
+
clientHeight: el.clientHeight,
|
|
305
|
+
contributesOverflowX: el.scrollWidth > el.clientWidth,
|
|
306
|
+
contributesOverflowY: el.scrollHeight > el.clientHeight,
|
|
307
|
+
minWidth: style.minWidth,
|
|
308
|
+
maxWidth: style.maxWidth,
|
|
309
|
+
minHeight: style.minHeight,
|
|
310
|
+
maxHeight: style.maxHeight,
|
|
311
|
+
aspectRatio: style.aspectRatio,
|
|
312
|
+
flex: isFlexChild ? flexFactsFor(el, parentStyle) : null,
|
|
313
|
+
grid: isGridChild ? gridFactsFor(el, parentStyle) : null,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
facts.push(factObj);
|
|
317
|
+
pushedFact = factObj;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (el.shadowRoot) {
|
|
321
|
+
var hostSel = __selectorOf(el);
|
|
322
|
+
var newDepth = (shadowCtx ? shadowCtx.chainDepth : 0) + 1;
|
|
323
|
+
var shadowKids = el.shadowRoot.children;
|
|
324
|
+
for (var si = 0; si < shadowKids.length; si++) {
|
|
325
|
+
walk(shadowKids[si], frameId, isTop, ancestorFrameIds, { inShadowDom: true, hostSelector: hostSel, chainDepth: newDepth });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (tag === 'IFRAME') {
|
|
330
|
+
var innerDoc = null;
|
|
331
|
+
var contentDocumentReadThrew = false;
|
|
332
|
+
try { innerDoc = el.contentDocument; } catch (e) { innerDoc = null; contentDocumentReadThrew = true; }
|
|
333
|
+
// G6: a THROWN contentDocument read (a genuine failure, e.g. a
|
|
334
|
+
// hostile/broken page patching the accessor) is distinct from a
|
|
335
|
+
// clean null (the ordinary cross-origin-iframe case, which throws
|
|
336
|
+
// nothing and is not a read failure) -- only the throw gets an
|
|
337
|
+
// honest unavailable marker, attached directly to the iframe's own
|
|
338
|
+
// already-pushed fact record so a same-origin subtree that silently
|
|
339
|
+
// went unwalked is never indistinguishable from a genuinely childless
|
|
340
|
+
// or cross-origin iframe.
|
|
341
|
+
if (contentDocumentReadThrew && pushedFact) {
|
|
342
|
+
pushedFact.iframeContentUnavailable = true;
|
|
343
|
+
pushedFact.iframeContentUnavailableReason = 'content-document-read-threw';
|
|
344
|
+
}
|
|
345
|
+
if (innerDoc && innerDoc.body) {
|
|
346
|
+
frameCounter += 1;
|
|
347
|
+
var childFrameId = 'frame-' + frameCounter;
|
|
348
|
+
var childAncestors = ancestorFrameIds.concat([frameId]);
|
|
349
|
+
var innerKids = innerDoc.body.children;
|
|
350
|
+
for (var ii = 0; ii < innerKids.length; ii++) {
|
|
351
|
+
walk(innerKids[ii], childFrameId, false, childAncestors, null);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
var children = el.children;
|
|
358
|
+
for (var c = 0; c < children.length; c++) {
|
|
359
|
+
walk(children[c], frameId, isTop, ancestorFrameIds, shadowCtx);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
var bodyKids = document.body ? document.body.children : [];
|
|
364
|
+
for (var bi = 0; bi < bodyKids.length; bi++) {
|
|
365
|
+
walk(bodyKids[bi], 'frame-0', true, [], null);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return { facts: facts, elements: elements, meta: { elementsTruncated: elementsOverflowCount } };
|
|
369
|
+
})()`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ============================================================================
|
|
373
|
+
// Bridge helpers — exported for hittest.ts to reuse. NONE of these ever
|
|
374
|
+
// assigns to `window` or any other page-observable location: they only
|
|
375
|
+
// ever read a held CDP `RemoteObject` back through its own `objectId`, so a
|
|
376
|
+
// page that predefines a setter for a guessed global name has nothing to
|
|
377
|
+
// observe.
|
|
378
|
+
// ============================================================================
|
|
379
|
+
|
|
380
|
+
/** Resolves each own-property `objectId` of a held CDP object (e.g. the walk's `{ facts, elements }` return-value container) in one `Runtime.getProperties` round trip — how `facts`/`elements` are found inside a held reference rather than a page-observable global. */
|
|
381
|
+
export async function ownPropertyObjectIds(client: CDPClient, objectId: string): Promise<Map<string, string>> {
|
|
382
|
+
const propsResult = (await client.send('Runtime.getProperties', {
|
|
383
|
+
objectId,
|
|
384
|
+
ownProperties: true,
|
|
385
|
+
})) as { result?: Array<{ name: string; value?: { objectId?: string } }> };
|
|
386
|
+
const out = new Map<string, string>();
|
|
387
|
+
for (const prop of propsResult.result ?? []) {
|
|
388
|
+
if (prop.value?.objectId) out.set(prop.name, prop.value.objectId);
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Reads a held CDP object out by value via one `Runtime.callFunctionOn({returnByValue:true})` round trip on its OWN `objectId` — the only way a by-value JSON-safe blob (rects/strings/numbers, no DOM handles) leaves a held reference without ever touching a page-observable global. */
|
|
394
|
+
export async function readHeldValue<T>(client: CDPClient, objectId: string): Promise<T | undefined> {
|
|
395
|
+
const res = (await client.send('Runtime.callFunctionOn', {
|
|
396
|
+
objectId,
|
|
397
|
+
functionDeclaration: 'function() { return this; }',
|
|
398
|
+
returnByValue: true,
|
|
399
|
+
})) as { result?: { value?: T } };
|
|
400
|
+
return res.result?.value;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Resolves each numeric-index own property of a held live-array `objectId`
|
|
405
|
+
* to that element's own `objectId`, in exactly ONE `Runtime.getProperties`
|
|
406
|
+
* round trip. `count` bounds the returned array's length (indices outside
|
|
407
|
+
* `[0, count)` are ignored even if present). This is the primitive every
|
|
408
|
+
* collector's CDP-only identity bridge (geometry, hittest, animation)
|
|
409
|
+
* shares — each resolves the `elements` array of its held
|
|
410
|
+
* `{ facts, elements }` container through this, with no page-observable
|
|
411
|
+
* global anywhere.
|
|
412
|
+
*/
|
|
413
|
+
export async function resolveIndexedObjectIds(
|
|
414
|
+
client: CDPClient,
|
|
415
|
+
arrayObjectId: string,
|
|
416
|
+
count: number,
|
|
417
|
+
): Promise<Array<string | undefined>> {
|
|
418
|
+
const out = new Array<string | undefined>(count).fill(undefined);
|
|
419
|
+
if (count <= 0) return out;
|
|
420
|
+
|
|
421
|
+
const propsResult = (await client.send('Runtime.getProperties', {
|
|
422
|
+
objectId: arrayObjectId,
|
|
423
|
+
ownProperties: true,
|
|
424
|
+
})) as { result?: Array<{ name: string; value?: { objectId?: string } }> };
|
|
425
|
+
|
|
426
|
+
for (const prop of propsResult.result ?? []) {
|
|
427
|
+
if (!/^\d+$/.test(prop.name)) continue;
|
|
428
|
+
const idx = Number(prop.name);
|
|
429
|
+
if (idx < 0 || idx >= count) continue;
|
|
430
|
+
out[idx] = prop.value?.objectId;
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Resolves one element's `backendNodeId` from its `objectId` via `DOM.describeNode`. Best-effort — returns `undefined` (never throws) if CDP can't describe the node. */
|
|
436
|
+
export async function describeBackendNodeId(client: CDPClient, objectId: string): Promise<number | undefined> {
|
|
437
|
+
try {
|
|
438
|
+
const res = (await client.send('DOM.describeNode', { objectId })) as { node?: { backendNodeId?: number } };
|
|
439
|
+
return res.node?.backendNodeId;
|
|
440
|
+
} catch {
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Same 8-number validation as `../../coordinates.js`'s private `asQuad` — reimplemented locally because this file needs `DOM.getContentQuads`'s raw quads read independently of `DOM.getBoxModel` (see {@link readContentQuads}/{@link readBoxModel}'s doc comments for why). */
|
|
446
|
+
function quadFromPoints(points: number[]): Quad {
|
|
447
|
+
if (points.length !== 8) {
|
|
448
|
+
throw new Error(`Expected an 8-number quad (x1,y1,x2,y2,x3,y3,x4,y4), got ${points.length} numbers`);
|
|
449
|
+
}
|
|
450
|
+
return points as Quad;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Reads ONLY `DOM.getContentQuads` for one element — deliberately NOT
|
|
455
|
+
* bundled with `DOM.getBoxModel` (unlike `../../coordinates.js`'s
|
|
456
|
+
* `getContentQuadBox`). Empirically confirmed against real Chrome: a
|
|
457
|
+
* `display:none` (or otherwise boxless) element makes
|
|
458
|
+
* `DOM.getContentQuads` resolve normally with a real, honest empty array —
|
|
459
|
+
* never throw — while `DOM.getBoxModel` throws "Could not compute box
|
|
460
|
+
* model" for that SAME element. So this call's own throw can only mean the
|
|
461
|
+
* quad read itself failed (invalid/detached node reference, a protocol
|
|
462
|
+
* error) — never that the element has no layout box, which always surfaces
|
|
463
|
+
* as a clean empty array here instead. Callers must let a throw here
|
|
464
|
+
* propagate as a genuine per-element geometry failure (I-4/I-5), not
|
|
465
|
+
* degrade it to the same shape a real empty result produces.
|
|
466
|
+
*/
|
|
467
|
+
async function readContentQuads(client: CDPClient, objectId: string): Promise<Quad[]> {
|
|
468
|
+
const res = (await client.send('DOM.getContentQuads', { objectId })) as { quads: number[][] };
|
|
469
|
+
return res.quads.map(quadFromPoints);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Reads `DOM.getBoxModel` for one element. Callers must only invoke this
|
|
474
|
+
* once {@link readContentQuads} has already proven a real (non-empty) box —
|
|
475
|
+
* see {@link readContentQuads}'s doc comment for why calling this
|
|
476
|
+
* unconditionally would make a genuinely boxless element indistinguishable
|
|
477
|
+
* from an actual box-model read failure.
|
|
478
|
+
*/
|
|
479
|
+
async function readBoxModel(client: CDPClient, objectId: string): Promise<GeometryBoxModel> {
|
|
480
|
+
const res = (await client.send('DOM.getBoxModel', { objectId })) as {
|
|
481
|
+
model: { content: number[]; padding: number[]; border: number[]; margin: number[]; width: number; height: number };
|
|
482
|
+
};
|
|
483
|
+
const model = res.model;
|
|
484
|
+
return {
|
|
485
|
+
content: quadFromPoints(model.content),
|
|
486
|
+
padding: quadFromPoints(model.padding),
|
|
487
|
+
border: quadFromPoints(model.border),
|
|
488
|
+
margin: quadFromPoints(model.margin),
|
|
489
|
+
width: model.width,
|
|
490
|
+
height: model.height,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Union AABB of every quad in `quads`, via `axisAlignedRectFromQuad`. Multi-fragment elements (inline runs wrapped across lines) can return more than one quad — this unions them rather than taking `quads[0]`. */
|
|
495
|
+
function unionRect(quads: Quad[]): Rect {
|
|
496
|
+
let minX = Infinity;
|
|
497
|
+
let minY = Infinity;
|
|
498
|
+
let maxX = -Infinity;
|
|
499
|
+
let maxY = -Infinity;
|
|
500
|
+
for (const quad of quads) {
|
|
501
|
+
const r = axisAlignedRectFromQuad(quad);
|
|
502
|
+
minX = Math.min(minX, r.x);
|
|
503
|
+
minY = Math.min(minY, r.y);
|
|
504
|
+
maxX = Math.max(maxX, r.x + r.width);
|
|
505
|
+
maxY = Math.max(maxY, r.y + r.height);
|
|
506
|
+
}
|
|
507
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ============================================================================
|
|
511
|
+
// Record shapes
|
|
512
|
+
// ============================================================================
|
|
513
|
+
|
|
514
|
+
interface RawFrameFact {
|
|
515
|
+
readonly frameId: string;
|
|
516
|
+
readonly isTopFrame: boolean;
|
|
517
|
+
readonly ancestorFrameIds: string[];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
interface RawShadowFact {
|
|
521
|
+
readonly inShadowDom: boolean;
|
|
522
|
+
readonly hostSelector?: string;
|
|
523
|
+
readonly chainDepth: number;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
interface RawStackingContextFact {
|
|
527
|
+
readonly creates: boolean;
|
|
528
|
+
readonly reasons: string[];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
interface RawVisibilityFact {
|
|
532
|
+
readonly visible: boolean;
|
|
533
|
+
readonly opacity: number;
|
|
534
|
+
readonly displayNone: boolean;
|
|
535
|
+
readonly visibilityHidden: boolean;
|
|
536
|
+
readonly zeroSize: boolean;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
interface RawClippingFact {
|
|
540
|
+
readonly clippedBy?: string;
|
|
541
|
+
readonly clippedFraction?: number;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
interface RawFlexFact {
|
|
545
|
+
readonly grow: number;
|
|
546
|
+
readonly shrink: number;
|
|
547
|
+
readonly basis: string;
|
|
548
|
+
readonly alignSelf: string;
|
|
549
|
+
readonly order: number;
|
|
550
|
+
readonly container: {
|
|
551
|
+
readonly direction: string;
|
|
552
|
+
readonly wrap: string;
|
|
553
|
+
readonly justifyContent: string;
|
|
554
|
+
readonly alignItems: string;
|
|
555
|
+
readonly gap: string;
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
interface RawGridFact {
|
|
560
|
+
readonly columnStart: string;
|
|
561
|
+
readonly columnEnd: string;
|
|
562
|
+
readonly rowStart: string;
|
|
563
|
+
readonly rowEnd: string;
|
|
564
|
+
readonly container: {
|
|
565
|
+
readonly templateColumns: string[];
|
|
566
|
+
readonly templateRows: string[];
|
|
567
|
+
readonly columnGap: string;
|
|
568
|
+
readonly rowGap: string;
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
interface RawLayoutFact {
|
|
573
|
+
readonly boxSizing: string;
|
|
574
|
+
readonly position: string;
|
|
575
|
+
readonly display: string;
|
|
576
|
+
readonly overflowX: string;
|
|
577
|
+
readonly overflowY: string;
|
|
578
|
+
readonly scrollWidth: number;
|
|
579
|
+
readonly scrollHeight: number;
|
|
580
|
+
readonly clientWidth: number;
|
|
581
|
+
readonly clientHeight: number;
|
|
582
|
+
readonly contributesOverflowX: boolean;
|
|
583
|
+
readonly contributesOverflowY: boolean;
|
|
584
|
+
readonly minWidth: string;
|
|
585
|
+
readonly maxWidth: string;
|
|
586
|
+
readonly minHeight: string;
|
|
587
|
+
readonly maxHeight: string;
|
|
588
|
+
readonly aspectRatio: string;
|
|
589
|
+
readonly flex: RawFlexFact | null;
|
|
590
|
+
readonly grid: RawGridFact | null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** Post-cap `grid` fact ({@link capGridFact}'s output) — same shape as {@link RawGridFact} except its track arrays are bounded and carry an explicit dropped-track count per axis, per I-5. */
|
|
594
|
+
interface GridFact extends Omit<RawGridFact, 'container'> {
|
|
595
|
+
readonly container: RawGridFact['container'] & {
|
|
596
|
+
/** Count of real `grid-template-columns` tracks the {@link MAX_GRID_TRACKS} cap dropped (0 when nothing was dropped). */
|
|
597
|
+
readonly columnTracksTruncated: number;
|
|
598
|
+
/** Count of real `grid-template-rows` tracks the {@link MAX_GRID_TRACKS} cap dropped (0 when nothing was dropped). */
|
|
599
|
+
readonly rowTracksTruncated: number;
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Post-cap `layout` fact ({@link capLayoutFact}'s output) — same shape as {@link RawLayoutFact} except `grid` is the post-cap {@link GridFact}. */
|
|
604
|
+
interface LayoutFact extends Omit<RawLayoutFact, 'grid'> {
|
|
605
|
+
readonly grid: GridFact | null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
interface RawGeometryFact {
|
|
609
|
+
readonly idx: number;
|
|
610
|
+
readonly tag: string;
|
|
611
|
+
readonly selector?: string;
|
|
612
|
+
readonly domPath: string;
|
|
613
|
+
readonly text?: string;
|
|
614
|
+
readonly frame: RawFrameFact;
|
|
615
|
+
readonly shadow: RawShadowFact | null;
|
|
616
|
+
readonly zIndex: string;
|
|
617
|
+
readonly stackingContext: RawStackingContextFact;
|
|
618
|
+
readonly visibility: RawVisibilityFact;
|
|
619
|
+
readonly clipping: RawClippingFact | null;
|
|
620
|
+
readonly layout: RawLayoutFact;
|
|
621
|
+
/** Present only on an `<iframe>` element's own fact when its `contentDocument` read threw (G6) -- see {@link GeometryElementRecord.iframeContentUnavailable}. */
|
|
622
|
+
readonly iframeContentUnavailable?: boolean;
|
|
623
|
+
readonly iframeContentUnavailableReason?: GeometryIframeUnavailableReason;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Fixed, factual reason an `<iframe>` element's same-origin subtree was not walked because reading its `contentDocument` itself failed -- never a raw exception message. A clean `null` (the ordinary cross-origin-iframe case) is NOT this: it throws nothing and is not a read failure, so it carries no marker. */
|
|
627
|
+
export type GeometryIframeUnavailableReason = 'content-document-read-threw';
|
|
628
|
+
|
|
629
|
+
export interface GeometryBoxModel {
|
|
630
|
+
readonly content: Quad;
|
|
631
|
+
readonly padding: Quad;
|
|
632
|
+
readonly border: Quad;
|
|
633
|
+
readonly margin: Quad;
|
|
634
|
+
readonly width: number;
|
|
635
|
+
readonly height: number;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** `geometry.json` per-element record shape — mirrors {@link ElementRecord} but requires `backendNodeId: number | null` (never omitted) plus an honest `identityUnresolved` marker when identity resolution failed, the same shape `hittest.ts`/`text.ts` already use (I-3/I-5). */
|
|
639
|
+
export interface GeometryElementRecord extends Omit<ElementRecord, 'backendNodeId'> {
|
|
640
|
+
readonly id: string;
|
|
641
|
+
readonly tag: string;
|
|
642
|
+
readonly domPath: string;
|
|
643
|
+
readonly frame: RawFrameFact;
|
|
644
|
+
readonly shadow: RawShadowFact | null;
|
|
645
|
+
readonly rect: Rect;
|
|
646
|
+
readonly quads: Quad[];
|
|
647
|
+
readonly boxModel: GeometryBoxModel | null;
|
|
648
|
+
readonly zIndex: string;
|
|
649
|
+
readonly stackingContext: RawStackingContextFact;
|
|
650
|
+
readonly visibility: RawVisibilityFact;
|
|
651
|
+
readonly clipping: RawClippingFact | null;
|
|
652
|
+
readonly layout: LayoutFact;
|
|
653
|
+
/** `null` (never an omitted key) when this record's identity did not resolve — see {@link identityUnresolved}. */
|
|
654
|
+
readonly backendNodeId: number | null;
|
|
655
|
+
/** `true` when {@link backendNodeId} is `null` because identity resolution failed (no bridged `objectId`, or `DOM.describeNode` threw/returned nothing) — never omit this alongside a `null` backendNodeId, so a downstream join can never mistake an unresolved record for a resolved one. Absent (not `false`) when identity resolved. */
|
|
656
|
+
readonly identityUnresolved?: true;
|
|
657
|
+
/**
|
|
658
|
+
* `true` when this record's per-element CDP geometry read did not fully
|
|
659
|
+
* succeed — see {@link geometryUnavailableReason} for which read failed.
|
|
660
|
+
* When `true`:
|
|
661
|
+
* - `quads-read-threw`/`no-element-object-id`: `DOM.getContentQuads`
|
|
662
|
+
* never ran (no bridged `objectId`) or threw (a genuine read failure,
|
|
663
|
+
* NOT proof of "no layout box" — a real no-box element resolves
|
|
664
|
+
* `DOM.getContentQuads` with a clean empty array instead of throwing).
|
|
665
|
+
* `rect` stays the placeholder zero rect, `quads` stays `[]`,
|
|
666
|
+
* `boxModel` stays `null`, and `visibility.zeroSize`/`visibility.visible`
|
|
667
|
+
* fall back to the JS-side (`getBoundingClientRect()`) facts alone —
|
|
668
|
+
* NEVER the CDP-quad-derived zero-size/invisible shape a genuine
|
|
669
|
+
* empty-quads result earns (I-4/I-5).
|
|
670
|
+
* - `box-model-read-threw`: `DOM.getContentQuads` succeeded with a real
|
|
671
|
+
* (non-empty) box, so `rect`/`quads`/`visibility.zeroSize`/`visibility.visible`
|
|
672
|
+
* ARE a proven observation and are computed normally — only `boxModel`
|
|
673
|
+
* (padding/border/margin detail) stays `null`, honestly marked as an
|
|
674
|
+
* unread detail rather than "this element genuinely has no box".
|
|
675
|
+
*
|
|
676
|
+
* Absent (not `false`) when the per-element geometry read fully succeeded.
|
|
677
|
+
*/
|
|
678
|
+
readonly geometryUnavailable?: true;
|
|
679
|
+
/** Present only when {@link geometryUnavailable} is `true`. */
|
|
680
|
+
readonly geometryUnavailableReason?: GeometryElementUnavailableReason;
|
|
681
|
+
/** `true` on an `<iframe>` element's own record when its `contentDocument` read THREW (G6) — its same-origin subtree was never walked because the read itself failed, not because the frame is genuinely cross-origin/childless (that case throws nothing and carries no marker). Absent (not `false`) otherwise, including for a non-iframe element or an iframe whose subtree walked normally or was genuinely cross-origin/empty. */
|
|
682
|
+
readonly iframeContentUnavailable?: true;
|
|
683
|
+
/** Present only when {@link iframeContentUnavailable} is `true`. */
|
|
684
|
+
readonly iframeContentUnavailableReason?: GeometryIframeUnavailableReason;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Fixed, factual reason a single element's `DOM.getContentQuads`/
|
|
689
|
+
* `DOM.getBoxModel` read did not complete — never a raw exception message.
|
|
690
|
+
* Present only on {@link GeometryElementRecord.geometryUnavailable}.
|
|
691
|
+
*
|
|
692
|
+
* - `no-element-object-id`: this element's index never resolved to a
|
|
693
|
+
* bridged `objectId` at all (the `elements` bridge array had no entry for
|
|
694
|
+
* it), so the quad/box read was never even attempted.
|
|
695
|
+
* - `quads-read-threw`: a bridged `objectId` existed, but `DOM.getContentQuads`
|
|
696
|
+
* itself threw — a genuine failure (invalid/detached node reference, a
|
|
697
|
+
* protocol error), distinct from the clean empty array a real
|
|
698
|
+
* no-layout-box element (e.g. `display:none`) returns without throwing.
|
|
699
|
+
* - `box-model-read-threw`: `DOM.getContentQuads` succeeded with a real,
|
|
700
|
+
* non-empty box, but the follow-up `DOM.getBoxModel` detail read threw —
|
|
701
|
+
* `rect`/`quads`/visibility are still trustworthy; only `boxModel` is
|
|
702
|
+
* unavailable.
|
|
703
|
+
*/
|
|
704
|
+
export type GeometryElementUnavailableReason = 'no-element-object-id' | 'quads-read-threw' | 'box-model-read-threw';
|
|
705
|
+
|
|
706
|
+
/** Builds the honest `{ backendNodeId, identityUnresolved }` pair every geometry element-bearing record carries — `null` (never an omitted key) when identity did not resolve, mirroring `hittest.ts`'s/`text.ts`'s `resolvedIdentity` (I-3/I-5). */
|
|
707
|
+
function resolvedIdentity(backendNodeId: number | undefined): { backendNodeId: number | null; identityUnresolved?: true } {
|
|
708
|
+
return backendNodeId === undefined ? { backendNodeId: null, identityUnresolved: true } : { backendNodeId };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ============================================================================
|
|
712
|
+
// String-sanitizing — `layout` (and its nested `flex`/`grid` facts) is a
|
|
713
|
+
// page-controlled bag of computed-CSS-value strings, exactly like
|
|
714
|
+
// selector/domPath/text above. Each string is routed through the single
|
|
715
|
+
// redaction.ts `sanitizeString` authority (capped at CSS_VALUE_MAX),
|
|
716
|
+
// never a private capper. `capTrackList` additionally
|
|
717
|
+
// bounds the grid template-track ARRAY itself (a pathological
|
|
718
|
+
// `grid-template-columns: repeat(100000, 1fr)` would otherwise emit an
|
|
719
|
+
// unbounded array of per-track strings) through the one authoritative
|
|
720
|
+
// array-capper (`capArray`, `redaction.ts`), reporting how many real tracks
|
|
721
|
+
// it dropped rather than silently slicing them away.
|
|
722
|
+
// ============================================================================
|
|
723
|
+
|
|
724
|
+
/** Cap for ordinary computed-CSS-value strings (position/display/min-max sizes/flex+grid provenance). */
|
|
725
|
+
const CSS_VALUE_MAX = 200;
|
|
726
|
+
/** Cap on how many grid template-track strings are kept per axis. */
|
|
727
|
+
const MAX_GRID_TRACKS = 64;
|
|
728
|
+
|
|
729
|
+
function capTrackList(values: readonly string[]): { items: string[]; truncated: number } {
|
|
730
|
+
const { items, truncated } = capArray(values, MAX_GRID_TRACKS);
|
|
731
|
+
return { items: items.map((v) => sanitizeString(v, { max: CSS_VALUE_MAX })), truncated };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function capFlexFact(flex: RawFlexFact): RawFlexFact {
|
|
735
|
+
return {
|
|
736
|
+
...flex,
|
|
737
|
+
basis: sanitizeString(flex.basis, { max: CSS_VALUE_MAX }),
|
|
738
|
+
alignSelf: sanitizeString(flex.alignSelf, { max: CSS_VALUE_MAX }),
|
|
739
|
+
container: {
|
|
740
|
+
...flex.container,
|
|
741
|
+
direction: sanitizeString(flex.container.direction, { max: CSS_VALUE_MAX }),
|
|
742
|
+
wrap: sanitizeString(flex.container.wrap, { max: CSS_VALUE_MAX }),
|
|
743
|
+
justifyContent: sanitizeString(flex.container.justifyContent, { max: CSS_VALUE_MAX }),
|
|
744
|
+
alignItems: sanitizeString(flex.container.alignItems, { max: CSS_VALUE_MAX }),
|
|
745
|
+
gap: sanitizeString(flex.container.gap, { max: CSS_VALUE_MAX }),
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function capGridFact(grid: RawGridFact): GridFact {
|
|
751
|
+
const columns = capTrackList(grid.container.templateColumns);
|
|
752
|
+
const rows = capTrackList(grid.container.templateRows);
|
|
753
|
+
return {
|
|
754
|
+
...grid,
|
|
755
|
+
columnStart: sanitizeString(grid.columnStart, { max: CSS_VALUE_MAX }),
|
|
756
|
+
columnEnd: sanitizeString(grid.columnEnd, { max: CSS_VALUE_MAX }),
|
|
757
|
+
rowStart: sanitizeString(grid.rowStart, { max: CSS_VALUE_MAX }),
|
|
758
|
+
rowEnd: sanitizeString(grid.rowEnd, { max: CSS_VALUE_MAX }),
|
|
759
|
+
container: {
|
|
760
|
+
...grid.container,
|
|
761
|
+
templateColumns: columns.items,
|
|
762
|
+
templateRows: rows.items,
|
|
763
|
+
columnTracksTruncated: columns.truncated,
|
|
764
|
+
rowTracksTruncated: rows.truncated,
|
|
765
|
+
columnGap: sanitizeString(grid.container.columnGap, { max: CSS_VALUE_MAX }),
|
|
766
|
+
rowGap: sanitizeString(grid.container.rowGap, { max: CSS_VALUE_MAX }),
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function capLayoutFact(layout: RawLayoutFact): LayoutFact {
|
|
772
|
+
return {
|
|
773
|
+
...layout,
|
|
774
|
+
boxSizing: sanitizeString(layout.boxSizing, { max: CSS_VALUE_MAX }),
|
|
775
|
+
position: sanitizeString(layout.position, { max: CSS_VALUE_MAX }),
|
|
776
|
+
display: sanitizeString(layout.display, { max: CSS_VALUE_MAX }),
|
|
777
|
+
overflowX: sanitizeString(layout.overflowX, { max: CSS_VALUE_MAX }),
|
|
778
|
+
overflowY: sanitizeString(layout.overflowY, { max: CSS_VALUE_MAX }),
|
|
779
|
+
minWidth: sanitizeString(layout.minWidth, { max: CSS_VALUE_MAX }),
|
|
780
|
+
maxWidth: sanitizeString(layout.maxWidth, { max: CSS_VALUE_MAX }),
|
|
781
|
+
minHeight: sanitizeString(layout.minHeight, { max: CSS_VALUE_MAX }),
|
|
782
|
+
maxHeight: sanitizeString(layout.maxHeight, { max: CSS_VALUE_MAX }),
|
|
783
|
+
aspectRatio: sanitizeString(layout.aspectRatio, { max: CSS_VALUE_MAX }),
|
|
784
|
+
flex: layout.flex ? capFlexFact(layout.flex) : null,
|
|
785
|
+
grid: layout.grid ? capGridFact(layout.grid) : null,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ============================================================================
|
|
790
|
+
// Collector
|
|
791
|
+
// ============================================================================
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Fixed, factual reason the walk evaluate/bridge could not be read (never a
|
|
795
|
+
* raw exception message, which is unbounded/page-influenced) — present
|
|
796
|
+
* only when {@link GeometryJson.available} is `false`.
|
|
797
|
+
*
|
|
798
|
+
* - `walk-facts-unavailable`: the held container came back without a
|
|
799
|
+
* `facts` objectId, OR `readHeldValue()` resolved (without throwing) to
|
|
800
|
+
* `undefined` — either way the required `facts` read did not happen, so
|
|
801
|
+
* both collapse to the one reason rather than two indistinguishable
|
|
802
|
+
* ones.
|
|
803
|
+
* - `walk-meta-unavailable`: `facts` read successfully, but the held
|
|
804
|
+
* container's `meta` objectId was absent or its `readHeldValue()` also
|
|
805
|
+
* resolved to `undefined`. Since `meta` rides the SAME returned object
|
|
806
|
+
* literal as `facts` (`{ facts, elements, meta }`, one evaluate), `meta`
|
|
807
|
+
* going missing right after `facts` succeeded signals a broken bridge
|
|
808
|
+
* read, not "nothing was truncated" — so this collapses the whole
|
|
809
|
+
* collection to unavailable rather than silently stamping
|
|
810
|
+
* `elementsTruncated: 0`.
|
|
811
|
+
*/
|
|
812
|
+
export type GeometryUnavailableReason =
|
|
813
|
+
| 'walk-evaluate-threw'
|
|
814
|
+
| 'walk-evaluate-returned-no-object'
|
|
815
|
+
| 'walk-facts-unavailable'
|
|
816
|
+
| 'walk-meta-unavailable';
|
|
817
|
+
|
|
818
|
+
export interface GeometryJson {
|
|
819
|
+
readonly elements: GeometryElementRecord[];
|
|
820
|
+
/** Exact count of real elements the {@link MAX_ELEMENTS} cap chose not to record — always present (0 when the whole reachable tree fit), per I-5. When {@link elementsTruncatedUnknown} is `true` this is a `0` placeholder, not a proven count — see that field. */
|
|
821
|
+
readonly elementsTruncated: number;
|
|
822
|
+
/** `true` when the held `meta` object read successfully (so `available` stays `true`) but its `elementsTruncated` field was missing or not a number — a malformed named field on an otherwise-vouched-for object (Layer 2, I-4/I-5). `elementsTruncated` is then `0` as a placeholder, never a proven observation of "nothing was truncated". Absent (not `false`) when `elementsTruncated` read as a real number. */
|
|
823
|
+
readonly elementsTruncatedUnknown?: true;
|
|
824
|
+
/** `false` when the walk evaluate/bridge itself failed — `elements: []` + `elementsTruncated: 0` is then "could not collect", not "genuinely empty page" (I-4/I-5). Always `true` on a normal run, including one where the page really has zero elements. */
|
|
825
|
+
readonly available: boolean;
|
|
826
|
+
/** Present only when `available` is `false`. */
|
|
827
|
+
readonly unavailableReason?: GeometryUnavailableReason;
|
|
828
|
+
readonly unstableRegions?: string[];
|
|
829
|
+
/** True when the try/finally release of a held CDP bridge object threw — never a diagnosis, never page-observable state. Absent when release succeeded. */
|
|
830
|
+
readonly bridgeCleanupFailed?: boolean;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export const collectGeometry: Collector = async (ctx) => {
|
|
834
|
+
let raw: RawGeometryFact[] = [];
|
|
835
|
+
let objectIds: Array<string | undefined> = [];
|
|
836
|
+
let elementsTruncated = 0;
|
|
837
|
+
let elementsTruncatedUnknown = false;
|
|
838
|
+
let bridgeCleanupFailed = false;
|
|
839
|
+
// I-5/I-4: distinguishes "the walk evaluate/bridge failed" from "the page
|
|
840
|
+
// really has zero elements" -- both would otherwise collapse to the same
|
|
841
|
+
// empty `elements` array with elementsTruncated:0, falsely claiming
|
|
842
|
+
// nothing was dropped when in fact nothing was ever read.
|
|
843
|
+
let available = true;
|
|
844
|
+
let unavailableReason: GeometryUnavailableReason | undefined;
|
|
845
|
+
const heldObjectIds: string[] = [];
|
|
846
|
+
try {
|
|
847
|
+
const walkEval = (await ctx.client.send('Runtime.evaluate', {
|
|
848
|
+
expression: SELECTOR_HELPER_JS + buildGeometryWalkScript(MAX_ELEMENTS),
|
|
849
|
+
returnByValue: false,
|
|
850
|
+
})) as { result?: { objectId?: string } };
|
|
851
|
+
const resultObjectId = walkEval.result?.objectId;
|
|
852
|
+
|
|
853
|
+
if (resultObjectId) {
|
|
854
|
+
heldObjectIds.push(resultObjectId);
|
|
855
|
+
const containerIds = await ownPropertyObjectIds(ctx.client, resultObjectId);
|
|
856
|
+
const factsObjectId = containerIds.get('facts');
|
|
857
|
+
const elementsObjectId = containerIds.get('elements');
|
|
858
|
+
const metaObjectId = containerIds.get('meta');
|
|
859
|
+
|
|
860
|
+
// I-4: a missing objectId for a required held property, OR a
|
|
861
|
+
// readHeldValue() that resolves (without throwing) to `undefined`, is
|
|
862
|
+
// the SAME fact as a thrown read -- the property was never actually
|
|
863
|
+
// read. Read both before deciding success/failure so neither can
|
|
864
|
+
// silently fall through to the initialized empty/default artifact.
|
|
865
|
+
let factsValue: RawGeometryFact[] | undefined;
|
|
866
|
+
if (factsObjectId) {
|
|
867
|
+
heldObjectIds.push(factsObjectId);
|
|
868
|
+
factsValue = await readHeldValue<RawGeometryFact[]>(ctx.client, factsObjectId);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
let metaValue: { elementsTruncated?: unknown } | undefined;
|
|
872
|
+
if (metaObjectId) {
|
|
873
|
+
heldObjectIds.push(metaObjectId);
|
|
874
|
+
metaValue = await readHeldValue<{ elementsTruncated?: unknown }>(ctx.client, metaObjectId);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (factsValue === undefined) {
|
|
878
|
+
available = false;
|
|
879
|
+
unavailableReason = 'walk-facts-unavailable';
|
|
880
|
+
} else if (metaValue === undefined) {
|
|
881
|
+
// `meta` rides the same returned object literal as `facts` -- its
|
|
882
|
+
// absence right after a successful `facts` read signals a broken
|
|
883
|
+
// bridge read, not "the walk truncated nothing". Treat the whole
|
|
884
|
+
// collection as unavailable rather than stamping elementsTruncated:0.
|
|
885
|
+
available = false;
|
|
886
|
+
unavailableReason = 'walk-meta-unavailable';
|
|
887
|
+
} else {
|
|
888
|
+
raw = factsValue;
|
|
889
|
+
// G17 (Layer 2): `meta` itself vouched for (its objectId resolved and
|
|
890
|
+
// readHeldValue() returned a real object above), but its named
|
|
891
|
+
// `elementsTruncated` FIELD may still be malformed (missing, or not a
|
|
892
|
+
// number) -- `?? 0` would silently treat that as a proven "nothing
|
|
893
|
+
// truncated" observation. Only a genuine number is trusted; anything
|
|
894
|
+
// else marks elementsTruncatedUnknown rather than defaulting mute.
|
|
895
|
+
if (typeof metaValue.elementsTruncated === 'number') {
|
|
896
|
+
elementsTruncated = metaValue.elementsTruncated;
|
|
897
|
+
} else {
|
|
898
|
+
elementsTruncated = 0;
|
|
899
|
+
elementsTruncatedUnknown = true;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (elementsObjectId && raw.length > 0) {
|
|
903
|
+
heldObjectIds.push(elementsObjectId);
|
|
904
|
+
objectIds = await resolveIndexedObjectIds(ctx.client, elementsObjectId, raw.length);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
} else {
|
|
908
|
+
available = false;
|
|
909
|
+
unavailableReason = 'walk-evaluate-returned-no-object';
|
|
910
|
+
}
|
|
911
|
+
} catch {
|
|
912
|
+
raw = [];
|
|
913
|
+
available = false;
|
|
914
|
+
unavailableReason = 'walk-evaluate-threw';
|
|
915
|
+
} finally {
|
|
916
|
+
for (const id of heldObjectIds) {
|
|
917
|
+
try {
|
|
918
|
+
await ctx.client.send('Runtime.releaseObject', { objectId: id });
|
|
919
|
+
} catch {
|
|
920
|
+
bridgeCleanupFailed = true;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const elements: GeometryElementRecord[] = [];
|
|
926
|
+
if (raw.length > 0) {
|
|
927
|
+
const resolved = await Promise.all(
|
|
928
|
+
raw.map(async (fact): Promise<GeometryElementRecord> => {
|
|
929
|
+
const objectId = objectIds[fact.idx];
|
|
930
|
+
let backendNodeId: number | undefined;
|
|
931
|
+
let quads: Quad[] = [];
|
|
932
|
+
let boxModel: GeometryBoxModel | null = null;
|
|
933
|
+
// I-4/I-5: `quadsUnavailable` drives whether zeroSize/visible below
|
|
934
|
+
// fall back to the JS-side facts alone -- true only when the
|
|
935
|
+
// per-element DOM.getContentQuads read itself never proved
|
|
936
|
+
// anything (never ran, or threw). A DOM.getBoxModel-only failure
|
|
937
|
+
// (quadsUnavailable stays false) does NOT trigger that fallback,
|
|
938
|
+
// since real quads already proved rect/visibility honestly.
|
|
939
|
+
let quadsUnavailable = false;
|
|
940
|
+
let geometryUnavailableReason: GeometryElementUnavailableReason | undefined;
|
|
941
|
+
|
|
942
|
+
if (objectId) {
|
|
943
|
+
backendNodeId = await describeBackendNodeId(ctx.client, objectId);
|
|
944
|
+
try {
|
|
945
|
+
quads = await readContentQuads(ctx.client, objectId);
|
|
946
|
+
} catch {
|
|
947
|
+
// A thrown DOM.getContentQuads read is a genuine failure, NOT
|
|
948
|
+
// proof of "no layout box" -- Chrome resolves that case with a
|
|
949
|
+
// real, non-throwing empty array instead (see readContentQuads's
|
|
950
|
+
// doc comment). Surface it honestly rather than degrading to
|
|
951
|
+
// the same zero-size/invisible shape a real empty result earns.
|
|
952
|
+
quadsUnavailable = true;
|
|
953
|
+
geometryUnavailableReason = 'quads-read-threw';
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (!quadsUnavailable && quads.length > 0) {
|
|
957
|
+
// Only attempt DOM.getBoxModel once real quads proved a box
|
|
958
|
+
// exists -- Chrome throws "Could not compute box model" for
|
|
959
|
+
// EVERY boxless node (the identical throw an actual failed
|
|
960
|
+
// read produces), so calling it unconditionally would make a
|
|
961
|
+
// genuine no-layout-box element indistinguishable from a real
|
|
962
|
+
// box-model read failure.
|
|
963
|
+
try {
|
|
964
|
+
boxModel = await readBoxModel(ctx.client, objectId);
|
|
965
|
+
} catch {
|
|
966
|
+
geometryUnavailableReason = 'box-model-read-threw';
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
} else {
|
|
970
|
+
// No bridged objectId at all for this element -- the quad/box
|
|
971
|
+
// read was never even attempted, so it must not silently degrade
|
|
972
|
+
// into the same zero-size/invisible shape a genuine no-layout-box
|
|
973
|
+
// element gets.
|
|
974
|
+
quadsUnavailable = true;
|
|
975
|
+
geometryUnavailableReason = 'no-element-object-id';
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const geometryUnavailable = geometryUnavailableReason !== undefined;
|
|
979
|
+
const rect = quads.length > 0 ? unionRect(quads) : { x: 0, y: 0, width: 0, height: 0 };
|
|
980
|
+
// Only a REAL (non-throwing) quads read can strengthen zeroSize/visible
|
|
981
|
+
// beyond the JS-side facts -- when the quads read itself never proved
|
|
982
|
+
// anything, fall back to fact.visibility (getBoundingClientRect())
|
|
983
|
+
// alone rather than a fabricated CDP-derived zero-size/invisible claim.
|
|
984
|
+
const zeroSize = quadsUnavailable ? fact.visibility.zeroSize : fact.visibility.zeroSize || quads.length === 0;
|
|
985
|
+
const visible = quadsUnavailable ? fact.visibility.visible : fact.visibility.visible && quads.length > 0;
|
|
986
|
+
|
|
987
|
+
return {
|
|
988
|
+
id: `el-${fact.idx}`,
|
|
989
|
+
tag: sanitizeString(fact.tag, { max: 64 }),
|
|
990
|
+
selector: fact.selector !== undefined ? sanitizeString(fact.selector, { max: 300 }) : undefined,
|
|
991
|
+
domPath: sanitizeString(fact.domPath, { max: 500 }),
|
|
992
|
+
...resolvedIdentity(backendNodeId),
|
|
993
|
+
text: fact.text !== undefined ? sanitizeString(fact.text, { max: 200 }) : undefined,
|
|
994
|
+
frame: fact.frame,
|
|
995
|
+
shadow: fact.shadow
|
|
996
|
+
? {
|
|
997
|
+
...fact.shadow,
|
|
998
|
+
hostSelector:
|
|
999
|
+
fact.shadow.hostSelector !== undefined ? sanitizeString(fact.shadow.hostSelector, { max: 300 }) : undefined,
|
|
1000
|
+
}
|
|
1001
|
+
: null,
|
|
1002
|
+
rect,
|
|
1003
|
+
quads,
|
|
1004
|
+
boxModel,
|
|
1005
|
+
zIndex: sanitizeString(fact.zIndex, { max: 20 }),
|
|
1006
|
+
stackingContext: fact.stackingContext,
|
|
1007
|
+
visibility: { ...fact.visibility, visible, zeroSize },
|
|
1008
|
+
clipping: fact.clipping
|
|
1009
|
+
? {
|
|
1010
|
+
clippedBy:
|
|
1011
|
+
fact.clipping.clippedBy !== undefined ? sanitizeString(fact.clipping.clippedBy, { max: 300 }) : undefined,
|
|
1012
|
+
clippedFraction: fact.clipping.clippedFraction,
|
|
1013
|
+
}
|
|
1014
|
+
: null,
|
|
1015
|
+
layout: capLayoutFact(fact.layout),
|
|
1016
|
+
...(geometryUnavailable ? { geometryUnavailable: true as const, geometryUnavailableReason } : {}),
|
|
1017
|
+
...(fact.iframeContentUnavailable
|
|
1018
|
+
? { iframeContentUnavailable: true as const, iframeContentUnavailableReason: fact.iframeContentUnavailableReason }
|
|
1019
|
+
: {}),
|
|
1020
|
+
};
|
|
1021
|
+
}),
|
|
1022
|
+
);
|
|
1023
|
+
elements.push(...resolved);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
ctx.write.json('geometry.json', {
|
|
1027
|
+
elements,
|
|
1028
|
+
// Exact count of real elements the MAX_ELEMENTS cap chose not to record
|
|
1029
|
+
// (0 when the whole reachable tree fit) -- always present, never a
|
|
1030
|
+
// silently-omitted field, per I-5.
|
|
1031
|
+
elementsTruncated,
|
|
1032
|
+
...(elementsTruncatedUnknown ? { elementsTruncatedUnknown: true } : {}),
|
|
1033
|
+
available,
|
|
1034
|
+
...(unavailableReason ? { unavailableReason } : {}),
|
|
1035
|
+
...(ctx.unstableRegions.length ? { unstableRegions: [...ctx.unstableRegions] } : {}),
|
|
1036
|
+
...(bridgeCleanupFailed ? { bridgeCleanupFailed: true } : {}),
|
|
1037
|
+
} satisfies GeometryJson);
|
|
1038
|
+
};
|