@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,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `scroll.json` collector — the scroll-container topology sampled during
|
|
3
|
+
* capture: each container's rect, extents, current/max offsets, overflow
|
|
4
|
+
* styles, scrollbar-gutter, snap points, sticky/fixed descendants, which
|
|
5
|
+
* children become visible at sampled offsets, nested-scroll ancestry, and
|
|
6
|
+
* visual-vs-layout viewport.
|
|
7
|
+
*
|
|
8
|
+
* ## Identity join key
|
|
9
|
+
* Every container/sticky/snap/visible-child record carries a
|
|
10
|
+
* `backendNodeId` (resolved via `DOM.describeNode` off the temporary
|
|
11
|
+
* `data-capture-scroll-id` markers the topology script stamps) wherever CDP
|
|
12
|
+
* can resolve one, so a logical DOM node joins across
|
|
13
|
+
* `scroll.json`/`geometry.json`/`hittest.json` by that stable id.
|
|
14
|
+
*
|
|
15
|
+
* ## Scope
|
|
16
|
+
* Top document, light DOM only — the walk uses `document.querySelectorAll`
|
|
17
|
+
* and does not descend into iframes or shadow roots. `scroll.json`'s
|
|
18
|
+
* `scope` field states this boundary as a fact (with counts of the
|
|
19
|
+
* iframes/open shadow hosts NOT traversed) so downstream cannot read
|
|
20
|
+
* omission as a negative finding.
|
|
21
|
+
*
|
|
22
|
+
* ## Restoration
|
|
23
|
+
* Every temporary `scrollTop`/`scrollLeft` write is restored inside an
|
|
24
|
+
* in-page `try`/`finally` before the topology script returns (so a throw
|
|
25
|
+
* mid-sampling can't leave a container scrolled), and the temporary
|
|
26
|
+
* `data-capture-scroll-id` markers are stripped node-side afterward. Both
|
|
27
|
+
* outcomes are recorded factually in `scroll.json`'s `restoration` field
|
|
28
|
+
* (`offsetsRestored`/`markersCleared`) — never as prose advice.
|
|
29
|
+
*
|
|
30
|
+
* ## Truncation facts
|
|
31
|
+
* Four caps bound this collector's page walk, each surfaced as an explicit
|
|
32
|
+
* fact rather than a silently short array. The scroll-container scan (cap
|
|
33
|
+
* 60) counts every real match regardless of the cap — cheap, since it
|
|
34
|
+
* already visits every element to count shadow hosts — so `ScrollReport`
|
|
35
|
+
* carries an exact `scrollContainersTotal`/`scrollContainersTruncated` pair
|
|
36
|
+
* at its top level, deliberately NOT nested inside `scope` (whose own exact
|
|
37
|
+
* 4-key shape — `root`/`shadowDom`/`iframesPresent`/`shadowHostsPresent` —
|
|
38
|
+
* is asserted verbatim by shared restoration tests). The
|
|
39
|
+
* per-container sticky/fixed-descendant and snap-descendant scans (cap 30
|
|
40
|
+
* each) and the per-sample visible-children scan (cap 30) stop enumerating
|
|
41
|
+
* the instant they hit their cap, so an exact total would require redoing
|
|
42
|
+
* the expensive part (a `getComputedStyle`/`getBoundingClientRect` call per
|
|
43
|
+
* remaining descendant) the cap exists to avoid — those three instead carry
|
|
44
|
+
* an honest `*Truncated` boolean (the cap was reached; more may exist)
|
|
45
|
+
* alongside their array, on the same record the array itself lives on.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import type { CDPClient } from '../../client.js';
|
|
49
|
+
import type { Collector } from '../types.js';
|
|
50
|
+
import { sanitizeString } from '../redaction.js';
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Injected script — one round trip: discover containers, sample reachable
|
|
54
|
+
// content at bracketed offsets, then restore every container's original
|
|
55
|
+
// scroll position before returning (offset writes are wrapped in an in-page
|
|
56
|
+
// try/finally so a throw mid-sampling still restores). Temporary
|
|
57
|
+
// `data-capture-scroll-id` markers are stamped here and cleaned up
|
|
58
|
+
// node-side after the identity join is resolved.
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
const SCROLL_TOPOLOGY_SCRIPT = `/* __captureScrollTopology */
|
|
62
|
+
(function() {
|
|
63
|
+
var scrollNextId = 1;
|
|
64
|
+
function tagId(el) {
|
|
65
|
+
try {
|
|
66
|
+
if (!el || el.nodeType !== 1) return null;
|
|
67
|
+
var existing = el.getAttribute('data-capture-scroll-id');
|
|
68
|
+
if (existing) return existing;
|
|
69
|
+
var id = 'scroll-' + (scrollNextId++);
|
|
70
|
+
el.setAttribute('data-capture-scroll-id', id);
|
|
71
|
+
return id;
|
|
72
|
+
} catch (e) { return null; }
|
|
73
|
+
}
|
|
74
|
+
function selectorOf(el) {
|
|
75
|
+
try {
|
|
76
|
+
if (!el || el.nodeType !== 1) return null;
|
|
77
|
+
var tag = el.tagName.toLowerCase();
|
|
78
|
+
var id = el.id ? ('#' + el.id) : '';
|
|
79
|
+
var cls = (el.className && typeof el.className === 'string' && el.className.trim())
|
|
80
|
+
? ('.' + el.className.trim().split(/\\s+/).filter(Boolean).join('.'))
|
|
81
|
+
: '';
|
|
82
|
+
return tag + id + cls;
|
|
83
|
+
} catch (e) { return null; }
|
|
84
|
+
}
|
|
85
|
+
function rectOf(el) {
|
|
86
|
+
try {
|
|
87
|
+
var r = el.getBoundingClientRect();
|
|
88
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
89
|
+
} catch (e) { return null; }
|
|
90
|
+
}
|
|
91
|
+
function isScrollContainer(el) {
|
|
92
|
+
try {
|
|
93
|
+
return (el.scrollHeight - el.clientHeight > 1) || (el.scrollWidth - el.clientWidth > 1);
|
|
94
|
+
} catch (e) { return false; }
|
|
95
|
+
}
|
|
96
|
+
function stickyOrFixedDescendants(el) {
|
|
97
|
+
var out = [];
|
|
98
|
+
var kids = el.querySelectorAll('*');
|
|
99
|
+
var i;
|
|
100
|
+
for (i = 0; i < kids.length && out.length < 30; i++) {
|
|
101
|
+
var cs;
|
|
102
|
+
try { cs = window.getComputedStyle(kids[i]); } catch (e) { continue; }
|
|
103
|
+
if (cs.position === 'sticky' || cs.position === 'fixed') {
|
|
104
|
+
out.push({ scrollId: tagId(kids[i]), selector: selectorOf(kids[i]), position: cs.position, rect: rectOf(kids[i]) });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// The scan stops the instant the cap is hit, so i < kids.length means
|
|
108
|
+
// real descendants past this point were never even checked.
|
|
109
|
+
return { items: out, truncated: i < kids.length };
|
|
110
|
+
}
|
|
111
|
+
function snapDescendants(el) {
|
|
112
|
+
var out = [];
|
|
113
|
+
var kids = el.querySelectorAll('*');
|
|
114
|
+
var i;
|
|
115
|
+
for (i = 0; i < kids.length && out.length < 30; i++) {
|
|
116
|
+
var cs;
|
|
117
|
+
try { cs = window.getComputedStyle(kids[i]); } catch (e) { continue; }
|
|
118
|
+
if (cs.scrollSnapAlign && cs.scrollSnapAlign !== 'none') {
|
|
119
|
+
out.push({ scrollId: tagId(kids[i]), selector: selectorOf(kids[i]), scrollSnapAlign: cs.scrollSnapAlign });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { items: out, truncated: i < kids.length };
|
|
123
|
+
}
|
|
124
|
+
function visibleChildren(el) {
|
|
125
|
+
var band = el.getBoundingClientRect();
|
|
126
|
+
var out = [];
|
|
127
|
+
var kids = el.children;
|
|
128
|
+
var i;
|
|
129
|
+
for (i = 0; i < kids.length && out.length < 30; i++) {
|
|
130
|
+
var r = kids[i].getBoundingClientRect();
|
|
131
|
+
var intersects = r.bottom > band.top && r.top < band.bottom && r.right > band.left && r.left < band.right;
|
|
132
|
+
if (intersects) out.push({ scrollId: tagId(kids[i]), selector: selectorOf(kids[i]), rect: rectOf(kids[i]) });
|
|
133
|
+
}
|
|
134
|
+
return { items: out, truncated: i < kids.length };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
var root = document.scrollingElement || document.documentElement;
|
|
138
|
+
var found = [root];
|
|
139
|
+
var all = document.querySelectorAll('*');
|
|
140
|
+
var shadowHosts = 0;
|
|
141
|
+
// root is unconditionally EMITTED into found/containers below regardless
|
|
142
|
+
// of whether it genuinely scrolls (it always carries useful rect/overflow
|
|
143
|
+
// facts for the top-level viewport) -- but the total this fact claims must
|
|
144
|
+
// count root as a genuine scroll container ONLY when it actually is one
|
|
145
|
+
// (isScrollContainer(root)), exactly like every other candidate below.
|
|
146
|
+
// rootMatches mirrors what the loop's own isScrollContainer check would
|
|
147
|
+
// have found for root had it not been excluded from the loop by the
|
|
148
|
+
// all[i] !== root guard.
|
|
149
|
+
var rootMatches = isScrollContainer(root) ? 1 : 0;
|
|
150
|
+
// Counted regardless of the 60-container cap — this loop already visits
|
|
151
|
+
// every element to count shadow hosts, so an exact total costs nothing
|
|
152
|
+
// extra (unlike the per-container descendant scans below).
|
|
153
|
+
var nonRootMatches = 0;
|
|
154
|
+
for (var i = 0; i < all.length; i++) {
|
|
155
|
+
if (all[i].shadowRoot) shadowHosts++;
|
|
156
|
+
if (all[i] !== root && isScrollContainer(all[i])) {
|
|
157
|
+
nonRootMatches++;
|
|
158
|
+
if (found.length < 60) found.push(all[i]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
var scrollContainerMatches = rootMatches + nonRootMatches;
|
|
162
|
+
|
|
163
|
+
// Snapshot every container's original offset up front so restoration in
|
|
164
|
+
// the finally is complete even if sampling throws midway through.
|
|
165
|
+
var originalOffsets = [];
|
|
166
|
+
for (var o = 0; o < found.length; o++) {
|
|
167
|
+
originalOffsets.push({ el: found[o], top: found[o].scrollTop, left: found[o].scrollLeft });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
var containers = [];
|
|
171
|
+
var offsetsRestored = true;
|
|
172
|
+
var scriptError = null;
|
|
173
|
+
try {
|
|
174
|
+
for (var c = 0; c < found.length; c++) {
|
|
175
|
+
var el = found[c];
|
|
176
|
+
var isRoot = el === root;
|
|
177
|
+
var cs = window.getComputedStyle(el);
|
|
178
|
+
var maxTop = Math.max(0, el.scrollHeight - el.clientHeight);
|
|
179
|
+
var maxLeft = Math.max(0, el.scrollWidth - el.clientWidth);
|
|
180
|
+
var originalTop = el.scrollTop;
|
|
181
|
+
var originalLeft = el.scrollLeft;
|
|
182
|
+
|
|
183
|
+
var samples = [];
|
|
184
|
+
var offsetsToSample = [];
|
|
185
|
+
var seen = {};
|
|
186
|
+
[originalTop, 0, maxTop].forEach(function(v) { if (!(v in seen)) { seen[v] = true; offsetsToSample.push(v); } });
|
|
187
|
+
for (var s = 0; s < offsetsToSample.length; s++) {
|
|
188
|
+
var top = offsetsToSample[s];
|
|
189
|
+
el.scrollTop = top;
|
|
190
|
+
var vc = isRoot ? { items: [], truncated: false } : visibleChildren(el);
|
|
191
|
+
samples.push({ offsetTop: top, visibleChildren: vc.items, visibleChildrenTruncated: vc.truncated });
|
|
192
|
+
}
|
|
193
|
+
el.scrollTop = originalTop;
|
|
194
|
+
el.scrollLeft = originalLeft;
|
|
195
|
+
|
|
196
|
+
var snapResult = isRoot ? { items: [], truncated: false } : snapDescendants(el);
|
|
197
|
+
var stickyResult = stickyOrFixedDescendants(el);
|
|
198
|
+
|
|
199
|
+
containers.push({
|
|
200
|
+
scrollId: tagId(el),
|
|
201
|
+
selector: isRoot ? '(document)' : selectorOf(el),
|
|
202
|
+
isRoot: isRoot,
|
|
203
|
+
rect: isRoot ? { x: 0, y: 0, width: document.documentElement.clientWidth, height: document.documentElement.clientHeight } : rectOf(el),
|
|
204
|
+
scrollWidth: el.scrollWidth,
|
|
205
|
+
scrollHeight: el.scrollHeight,
|
|
206
|
+
clientWidth: el.clientWidth,
|
|
207
|
+
clientHeight: el.clientHeight,
|
|
208
|
+
scrollTop: originalTop,
|
|
209
|
+
scrollLeft: originalLeft,
|
|
210
|
+
maxScrollTop: maxTop,
|
|
211
|
+
maxScrollLeft: maxLeft,
|
|
212
|
+
overflowX: cs.overflowX,
|
|
213
|
+
overflowY: cs.overflowY,
|
|
214
|
+
scrollbarGutter: cs.scrollbarGutter || null,
|
|
215
|
+
scrollSnapType: cs.scrollSnapType || null,
|
|
216
|
+
snapDescendants: snapResult.items,
|
|
217
|
+
snapDescendantsTruncated: snapResult.truncated,
|
|
218
|
+
stickyFixedDescendants: stickyResult.items,
|
|
219
|
+
stickyFixedDescendantsTruncated: stickyResult.truncated,
|
|
220
|
+
samples: samples,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Nested-scroll ancestry: for each non-root container, which OTHER
|
|
225
|
+
// container selectors are its DOM ancestors (nearest first).
|
|
226
|
+
for (var n = 0; n < found.length; n++) {
|
|
227
|
+
if (found[n] === root) continue;
|
|
228
|
+
var chain = [];
|
|
229
|
+
var walker = found[n].parentElement;
|
|
230
|
+
while (walker) {
|
|
231
|
+
for (var k = 0; k < found.length; k++) {
|
|
232
|
+
if (found[k] === walker) { chain.push(containers[k].selector); break; }
|
|
233
|
+
}
|
|
234
|
+
walker = walker.parentElement;
|
|
235
|
+
}
|
|
236
|
+
chain.push(containers[0].selector);
|
|
237
|
+
containers[n].nestedAncestry = chain;
|
|
238
|
+
}
|
|
239
|
+
if (containers.length > 0) containers[0].nestedAncestry = [];
|
|
240
|
+
} catch (e) {
|
|
241
|
+
scriptError = String((e && e.message) || e);
|
|
242
|
+
} finally {
|
|
243
|
+
// Restore every sampled container's original scroll position.
|
|
244
|
+
for (var r2 = 0; r2 < originalOffsets.length; r2++) {
|
|
245
|
+
try {
|
|
246
|
+
originalOffsets[r2].el.scrollTop = originalOffsets[r2].top;
|
|
247
|
+
originalOffsets[r2].el.scrollLeft = originalOffsets[r2].left;
|
|
248
|
+
if (originalOffsets[r2].el.scrollTop !== originalOffsets[r2].top || originalOffsets[r2].el.scrollLeft !== originalOffsets[r2].left) {
|
|
249
|
+
offsetsRestored = false;
|
|
250
|
+
}
|
|
251
|
+
} catch (e2) { offsetsRestored = false; }
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
containers: containers,
|
|
257
|
+
documentScrollHeight: document.documentElement.scrollHeight,
|
|
258
|
+
documentScrollWidth: document.documentElement.scrollWidth,
|
|
259
|
+
offsetsRestored: offsetsRestored,
|
|
260
|
+
iframesPresent: document.querySelectorAll('iframe').length,
|
|
261
|
+
shadowHostsPresent: shadowHosts,
|
|
262
|
+
// scrollContainerMatches already folds rootMatches in (see above), so a
|
|
263
|
+
// page whose ONLY scroll container is the root itself (no other div
|
|
264
|
+
// scrolls) now reports scrollContainersTotal === 1, matching the single
|
|
265
|
+
// emitted root container, instead of the pre-fix 0 (root was always
|
|
266
|
+
// emitted but never counted toward the total). Truncation still compares
|
|
267
|
+
// only the non-root portion against the non-root cap -- root is never
|
|
268
|
+
// subject to the 60-container cap (it is pushed into found before the
|
|
269
|
+
// cap check even runs), so it must not participate in that comparison.
|
|
270
|
+
scrollContainersTotal: scrollContainerMatches,
|
|
271
|
+
scrollContainersTruncated: nonRootMatches > (found.length - 1),
|
|
272
|
+
scriptError: scriptError,
|
|
273
|
+
};
|
|
274
|
+
})();`;
|
|
275
|
+
|
|
276
|
+
/** Strips every temporary `data-capture-scroll-id` marker and reports whether the DOM was left clean. Run node-side AFTER the identity join is resolved. */
|
|
277
|
+
const SCROLL_CLEANUP_SCRIPT = `/* __captureScrollCleanup */
|
|
278
|
+
(function() {
|
|
279
|
+
var m = document.querySelectorAll('[data-capture-scroll-id]');
|
|
280
|
+
for (var i = 0; i < m.length; i++) { m[i].removeAttribute('data-capture-scroll-id'); }
|
|
281
|
+
return { cleared: document.querySelectorAll('[data-capture-scroll-id]').length === 0 };
|
|
282
|
+
})();`;
|
|
283
|
+
|
|
284
|
+
// ============================================================================
|
|
285
|
+
// Types
|
|
286
|
+
// ============================================================================
|
|
287
|
+
|
|
288
|
+
type RectVal = { x: number; y: number; width: number; height: number } | null;
|
|
289
|
+
|
|
290
|
+
interface ScrollSampleRaw {
|
|
291
|
+
readonly offsetTop: number;
|
|
292
|
+
readonly visibleChildren: Array<{ scrollId?: string | null; selector: string | null; rect: RectVal }>;
|
|
293
|
+
/** `true` when the 30-child cap on `visibleChildren` stopped the scan before it checked every child — more may intersect the sampled band but were never tested. */
|
|
294
|
+
readonly visibleChildrenTruncated: boolean;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
interface ScrollContainerRaw {
|
|
298
|
+
readonly scrollId?: string | null;
|
|
299
|
+
readonly selector: string | null;
|
|
300
|
+
readonly isRoot: boolean;
|
|
301
|
+
readonly rect: RectVal;
|
|
302
|
+
readonly scrollWidth: number;
|
|
303
|
+
readonly scrollHeight: number;
|
|
304
|
+
readonly clientWidth: number;
|
|
305
|
+
readonly clientHeight: number;
|
|
306
|
+
readonly scrollTop: number;
|
|
307
|
+
readonly scrollLeft: number;
|
|
308
|
+
readonly maxScrollTop: number;
|
|
309
|
+
readonly maxScrollLeft: number;
|
|
310
|
+
readonly overflowX: string;
|
|
311
|
+
readonly overflowY: string;
|
|
312
|
+
readonly scrollbarGutter: string | null;
|
|
313
|
+
readonly scrollSnapType: string | null;
|
|
314
|
+
readonly snapDescendants: Array<{ scrollId?: string | null; selector: string | null; scrollSnapAlign: string }>;
|
|
315
|
+
/** `true` when the 30-descendant cap stopped the snap-descendant scan before it visited every descendant. */
|
|
316
|
+
readonly snapDescendantsTruncated: boolean;
|
|
317
|
+
readonly stickyFixedDescendants: Array<{ scrollId?: string | null; selector: string | null; position: string; rect: RectVal }>;
|
|
318
|
+
/** `true` when the 30-descendant cap stopped the sticky/fixed scan before it visited every descendant. */
|
|
319
|
+
readonly stickyFixedDescendantsTruncated: boolean;
|
|
320
|
+
readonly samples: ScrollSampleRaw[];
|
|
321
|
+
readonly nestedAncestry?: string[];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
interface ScrollTopologyRaw {
|
|
325
|
+
readonly containers: ScrollContainerRaw[];
|
|
326
|
+
readonly documentScrollHeight: number;
|
|
327
|
+
readonly documentScrollWidth: number;
|
|
328
|
+
readonly offsetsRestored?: boolean;
|
|
329
|
+
readonly iframesPresent?: number;
|
|
330
|
+
readonly shadowHostsPresent?: number;
|
|
331
|
+
/** Exact count of real scroll containers found on the page, regardless of the 60-container cap on `containers` (cheap to compute — see the module doc). */
|
|
332
|
+
readonly scrollContainersTotal?: number;
|
|
333
|
+
/** `true` when `scrollContainersTotal` exceeds `containers.length` — real containers exist beyond the cap. */
|
|
334
|
+
readonly scrollContainersTruncated?: boolean;
|
|
335
|
+
readonly scriptError?: string | null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ----- output (sanitized, identity-joined) shapes -----
|
|
339
|
+
|
|
340
|
+
export interface ScrollContainerOut {
|
|
341
|
+
/** `null` (never an omitted key) when this record's identity did not resolve — see {@link identityUnresolved}. Mirrors hittest.ts's per-record identity shape (I-3). */
|
|
342
|
+
readonly backendNodeId: number | null;
|
|
343
|
+
/** `true` when {@link backendNodeId} is `null` because marker→backendNodeId resolution failed. Absent (not `false`) when identity resolved. */
|
|
344
|
+
readonly identityUnresolved?: true;
|
|
345
|
+
readonly selector: string | null;
|
|
346
|
+
readonly isRoot: boolean;
|
|
347
|
+
readonly rect: RectVal;
|
|
348
|
+
readonly scrollWidth: number;
|
|
349
|
+
readonly scrollHeight: number;
|
|
350
|
+
readonly clientWidth: number;
|
|
351
|
+
readonly clientHeight: number;
|
|
352
|
+
readonly scrollTop: number;
|
|
353
|
+
readonly scrollLeft: number;
|
|
354
|
+
readonly maxScrollTop: number;
|
|
355
|
+
readonly maxScrollLeft: number;
|
|
356
|
+
readonly overflowX: string;
|
|
357
|
+
readonly overflowY: string;
|
|
358
|
+
readonly scrollbarGutter: string | null;
|
|
359
|
+
readonly scrollSnapType: string | null;
|
|
360
|
+
readonly snapDescendants: Array<{ backendNodeId: number | null; identityUnresolved?: true; selector: string | null; scrollSnapAlign: string }>;
|
|
361
|
+
readonly snapDescendantsTruncated: boolean;
|
|
362
|
+
readonly stickyFixedDescendants: Array<{ backendNodeId: number | null; identityUnresolved?: true; selector: string | null; position: string; rect: RectVal }>;
|
|
363
|
+
readonly stickyFixedDescendantsTruncated: boolean;
|
|
364
|
+
readonly samples: Array<{ offsetTop: number; visibleChildren: Array<{ backendNodeId: number | null; identityUnresolved?: true; selector: string | null; rect: RectVal }>; visibleChildrenTruncated: boolean }>;
|
|
365
|
+
readonly nestedAncestry: string[];
|
|
366
|
+
/** `true` (Layer 2, I-4/I-5) when this record's `nestedAncestry` field was MISSING from an otherwise-present container in the topology's return value — e.g. a script throw mid-way through the in-page nested-ancestry loop (after `containers` was already fully populated but before every entry's `nestedAncestry` was assigned; `scriptError`/`restoration.error` carries the in-page exception text for that same failure). `nestedAncestry` above then falls back to `[]`, which is NOT a genuine "no nested scrollable ancestors" observation. Absent on a normal run. */
|
|
367
|
+
readonly nestedAncestryUnavailable?: true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Unchanged 4-key shape (asserted verbatim by shared restoration tests) — the scroll-container cap fact lives on {@link ScrollReport} instead, not here. */
|
|
371
|
+
export interface ScrollScope {
|
|
372
|
+
readonly root: 'top-document';
|
|
373
|
+
readonly shadowDom: 'light-only';
|
|
374
|
+
readonly iframesPresent: number;
|
|
375
|
+
readonly shadowHostsPresent: number;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export interface ScrollRestoration {
|
|
379
|
+
readonly attempted: boolean;
|
|
380
|
+
readonly offsetsRestored: boolean;
|
|
381
|
+
readonly markersCleared: boolean;
|
|
382
|
+
readonly error?: string;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Fixed, factual reason the topology `Runtime.evaluate` itself could not be read (never a raw exception message, which is unbounded/page-influenced) — present only when {@link ScrollReport.available} is `false`. Mirrors hittest.ts's `HittestUnavailableReason` discipline. */
|
|
386
|
+
export type ScrollUnavailableReason = 'topology-evaluate-threw' | 'topology-evaluate-returned-no-value';
|
|
387
|
+
|
|
388
|
+
export interface ScrollReport {
|
|
389
|
+
readonly containers: ScrollContainerOut[];
|
|
390
|
+
/** Exact count of real scroll containers found on the page (see the module doc's Truncation facts section), counting the root ONLY when it genuinely scrolls (same `isScrollContainer` test as every other candidate) — root is unconditionally present in `containers` regardless, so `containers.length` can exceed this by exactly one on a page whose root does not itself scroll; `>` only when the 60-container cap dropped real (non-root) containers beyond it. */
|
|
391
|
+
readonly scrollContainersTotal: number;
|
|
392
|
+
/** `true` when real scroll containers exist beyond the 60-container cap. */
|
|
393
|
+
readonly scrollContainersTruncated: boolean;
|
|
394
|
+
/** `true` (Layer 2, I-4/I-5) when the topology evaluate itself succeeded (a value came back) but that otherwise-valid value was MISSING `scrollContainersTotal`/`scrollContainersTruncated` — a malformed successful topology, distinct from a genuinely-computed `scrollContainersTotal: 0`/`scrollContainersTruncated: false`. When `true`, `scrollContainersTotal` above falls back to `containers.length` and `scrollContainersTruncated` falls back to a same-length comparison — neither is a real measurement. Absent (not `false`) on a normal run; meaningless (and never set) when {@link available} is `false`, since that failure is already reported by `available`/`reason`. */
|
|
395
|
+
readonly scrollContainersCountUnavailable?: true;
|
|
396
|
+
readonly documentScrollHeight: number;
|
|
397
|
+
readonly documentScrollWidth: number;
|
|
398
|
+
readonly visualViewport: unknown;
|
|
399
|
+
/** `true` (Layer 2, I-4/I-5) when `Page.getLayoutMetrics` returned neither `cssVisualViewport` nor `visualViewport` — {@link visualViewport} is then `null` because the read failed, not because the browser genuinely reported an empty viewport. Absent on a normal run. */
|
|
400
|
+
readonly visualViewportUnavailable?: true;
|
|
401
|
+
readonly layoutViewport: unknown;
|
|
402
|
+
/** Same meaning as {@link visualViewportUnavailable}, for {@link layoutViewport} (`cssLayoutViewport`/`layoutViewport`). */
|
|
403
|
+
readonly layoutViewportUnavailable?: true;
|
|
404
|
+
readonly scope: ScrollScope;
|
|
405
|
+
/** `true` (Layer 2, I-4/I-5) when the topology evaluate itself succeeded but the otherwise-valid value was MISSING `iframesPresent`/`shadowHostsPresent` — the counts surfaced on {@link scope} then fall back to `0`, a malformed successful topology rather than a genuine "no iframes/shadow hosts" observation. Kept off `scope` itself (rather than nested inside it) so `scope`'s 4-key shape stays exactly what the shared restoration tests assert — same reasoning as `scrollContainersTotal`/`scrollContainersTruncated` living on `ScrollReport` instead of `scope` (see the module doc's Truncation facts section). Absent on a normal run. */
|
|
406
|
+
readonly scopeCountsUnavailable?: true;
|
|
407
|
+
readonly restoration: ScrollRestoration;
|
|
408
|
+
/** `false` when the topology `Runtime.evaluate` itself failed (threw, or resolved with no `value`) — `containers: []` with `scrollContainersTotal: 0` is then "could not collect", not "genuinely no scroll containers" (I-4/I-5). Always `true` on a normal run, including one where the page really has no scroll containers other than a non-scrolling root. */
|
|
409
|
+
readonly available: boolean;
|
|
410
|
+
/** Present only when `available` is `false`. */
|
|
411
|
+
readonly reason?: ScrollUnavailableReason;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ============================================================================
|
|
415
|
+
// Node-side identity join + sanitization
|
|
416
|
+
// ============================================================================
|
|
417
|
+
|
|
418
|
+
const sanitizeOrNull = (value: string | null): string | null => (value === null ? null : sanitizeString(value));
|
|
419
|
+
const backendOf = (scrollId: string | null | undefined, backendById: Map<string, number>): number | undefined =>
|
|
420
|
+
scrollId ? backendById.get(scrollId) : undefined;
|
|
421
|
+
|
|
422
|
+
/** `null` (never omitted) when the marker→backendNodeId join failed for this element-bearing record — see {@link ScrollContainerOut.identityUnresolved}. Mirrors hittest.ts's `resolvedIdentity` (I-3). */
|
|
423
|
+
function resolvedIdentity(backendNodeId: number | undefined): { backendNodeId: number | null; identityUnresolved?: true } {
|
|
424
|
+
return backendNodeId === undefined ? { backendNodeId: null, identityUnresolved: true } : { backendNodeId };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function toContainerOut(c: ScrollContainerRaw, backendById: Map<string, number>): ScrollContainerOut {
|
|
428
|
+
return {
|
|
429
|
+
...resolvedIdentity(backendOf(c.scrollId, backendById)),
|
|
430
|
+
selector: sanitizeOrNull(c.selector),
|
|
431
|
+
isRoot: c.isRoot,
|
|
432
|
+
rect: c.rect,
|
|
433
|
+
scrollWidth: c.scrollWidth,
|
|
434
|
+
scrollHeight: c.scrollHeight,
|
|
435
|
+
clientWidth: c.clientWidth,
|
|
436
|
+
clientHeight: c.clientHeight,
|
|
437
|
+
scrollTop: c.scrollTop,
|
|
438
|
+
scrollLeft: c.scrollLeft,
|
|
439
|
+
maxScrollTop: c.maxScrollTop,
|
|
440
|
+
maxScrollLeft: c.maxScrollLeft,
|
|
441
|
+
overflowX: c.overflowX,
|
|
442
|
+
overflowY: c.overflowY,
|
|
443
|
+
scrollbarGutter: c.scrollbarGutter,
|
|
444
|
+
scrollSnapType: c.scrollSnapType,
|
|
445
|
+
snapDescendants: c.snapDescendants.map((d) => ({ ...resolvedIdentity(backendOf(d.scrollId, backendById)), selector: sanitizeOrNull(d.selector), scrollSnapAlign: d.scrollSnapAlign })),
|
|
446
|
+
snapDescendantsTruncated: c.snapDescendantsTruncated,
|
|
447
|
+
stickyFixedDescendants: c.stickyFixedDescendants.map((d) => ({ ...resolvedIdentity(backendOf(d.scrollId, backendById)), selector: sanitizeOrNull(d.selector), position: d.position, rect: d.rect })),
|
|
448
|
+
stickyFixedDescendantsTruncated: c.stickyFixedDescendantsTruncated,
|
|
449
|
+
samples: c.samples.map((s) => ({ offsetTop: s.offsetTop, visibleChildren: s.visibleChildren.map((v) => ({ ...resolvedIdentity(backendOf(v.scrollId, backendById)), selector: sanitizeOrNull(v.selector), rect: v.rect })), visibleChildrenTruncated: s.visibleChildrenTruncated })),
|
|
450
|
+
nestedAncestry: (c.nestedAncestry ?? []).map((sel) => sanitizeString(sel)),
|
|
451
|
+
...(c.nestedAncestry === undefined ? { nestedAncestryUnavailable: true as const } : {}),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Resolves a `data-capture-scroll-id → backendNodeId` map off the temporary
|
|
457
|
+
* markers the topology script stamped, following `states.ts`'s
|
|
458
|
+
* `DOM.describeNode` pattern. Best-effort: any CDP hiccup yields an empty
|
|
459
|
+
* map, never a throw.
|
|
460
|
+
*/
|
|
461
|
+
async function resolveMarkerBackendIds(client: CDPClient, markerAttr: string): Promise<Map<string, number>> {
|
|
462
|
+
const map = new Map<string, number>();
|
|
463
|
+
try {
|
|
464
|
+
const doc = (await client.send('DOM.getDocument', { depth: -1, pierce: false })) as { root?: { nodeId?: number } };
|
|
465
|
+
const rootId = doc.root?.nodeId;
|
|
466
|
+
if (rootId === undefined) return map;
|
|
467
|
+
const res = (await client.send('DOM.querySelectorAll', { nodeId: rootId, selector: `[${markerAttr}]` })) as { nodeIds?: number[] };
|
|
468
|
+
for (const nodeId of res.nodeIds ?? []) {
|
|
469
|
+
const described = (await client.send('DOM.describeNode', { nodeId })) as { node?: { backendNodeId?: number; attributes?: string[] } };
|
|
470
|
+
const backendNodeId = described.node?.backendNodeId;
|
|
471
|
+
const attrs = described.node?.attributes ?? [];
|
|
472
|
+
if (backendNodeId === undefined) continue;
|
|
473
|
+
for (let i = 0; i + 1 < attrs.length; i += 2) {
|
|
474
|
+
if (attrs[i] === markerAttr) {
|
|
475
|
+
map.set(attrs[i + 1], backendNodeId);
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} catch {
|
|
481
|
+
// Best-effort identity resolution — leave the map empty on any failure.
|
|
482
|
+
}
|
|
483
|
+
return map;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ============================================================================
|
|
487
|
+
// Collector
|
|
488
|
+
// ============================================================================
|
|
489
|
+
|
|
490
|
+
export const collectScroll: Collector = async (ctx) => {
|
|
491
|
+
const { client } = ctx;
|
|
492
|
+
|
|
493
|
+
let topology: ScrollTopologyRaw = { containers: [], documentScrollHeight: 0, documentScrollWidth: 0 };
|
|
494
|
+
// I-4/I-5: distinguishes "the topology evaluate itself failed" from "the
|
|
495
|
+
// page really has zero scroll containers" — both would otherwise collapse
|
|
496
|
+
// to the same empty containers/scrollContainersTotal:0, falsely claiming a
|
|
497
|
+
// genuinely-measured-empty page when in fact the topology was never read.
|
|
498
|
+
let available = true;
|
|
499
|
+
let reason: ScrollUnavailableReason | undefined;
|
|
500
|
+
try {
|
|
501
|
+
const evalResponse = (await client.send('Runtime.evaluate', {
|
|
502
|
+
expression: SCROLL_TOPOLOGY_SCRIPT,
|
|
503
|
+
returnByValue: true,
|
|
504
|
+
})) as { result?: { value?: ScrollTopologyRaw } };
|
|
505
|
+
if (evalResponse.result?.value !== undefined) {
|
|
506
|
+
topology = evalResponse.result.value;
|
|
507
|
+
} else {
|
|
508
|
+
available = false;
|
|
509
|
+
reason = 'topology-evaluate-returned-no-value';
|
|
510
|
+
}
|
|
511
|
+
} catch {
|
|
512
|
+
// Topology eval failed outright — fall through with the empty topology
|
|
513
|
+
// (marker cleanup below still runs to leave the DOM clean), but the
|
|
514
|
+
// failure itself must be reported, never silently coerced to success.
|
|
515
|
+
available = false;
|
|
516
|
+
reason = 'topology-evaluate-threw';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Resolve the cross-artifact join keys, then strip the temporary markers.
|
|
520
|
+
const backendById = await resolveMarkerBackendIds(client, 'data-capture-scroll-id');
|
|
521
|
+
let markersCleared = false;
|
|
522
|
+
try {
|
|
523
|
+
const cleanup = (await client.send('Runtime.evaluate', {
|
|
524
|
+
expression: SCROLL_CLEANUP_SCRIPT,
|
|
525
|
+
returnByValue: true,
|
|
526
|
+
})) as { result?: { value?: { cleared?: boolean } } };
|
|
527
|
+
markersCleared = cleanup.result?.value?.cleared ?? false;
|
|
528
|
+
} catch {
|
|
529
|
+
markersCleared = false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const metrics = (await client.send('Page.getLayoutMetrics')) as {
|
|
533
|
+
visualViewport?: unknown;
|
|
534
|
+
layoutViewport?: unknown;
|
|
535
|
+
cssVisualViewport?: unknown;
|
|
536
|
+
cssLayoutViewport?: unknown;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const containersOut = topology.containers.map((c) => toContainerOut(c, backendById));
|
|
540
|
+
|
|
541
|
+
// I-4/I-5 (Layer 2): `topology` here is the value the topology evaluate
|
|
542
|
+
// ITSELF returned (a well-formed value, per the `available`/`reason` gate
|
|
543
|
+
// above) -- so a MISSING named field on it (as opposed to `available`
|
|
544
|
+
// being false) is a malformed successful response, not a genuine "zero"
|
|
545
|
+
// observation. `available` gates whether these malformed markers are even
|
|
546
|
+
// meaningful to compute: when the topology evaluate itself failed,
|
|
547
|
+
// `topology` is left at its `{ containers: [], documentScrollHeight: 0,
|
|
548
|
+
// documentScrollWidth: 0 }` default and every optional field is "missing"
|
|
549
|
+
// by construction -- that failure is already fully reported via
|
|
550
|
+
// `available`/`reason`, so flagging every field malformed on top of it
|
|
551
|
+
// would be redundant noise, not a new fact.
|
|
552
|
+
const scrollContainersCountUnavailable = available && (topology.scrollContainersTotal === undefined || topology.scrollContainersTruncated === undefined);
|
|
553
|
+
const scrollContainersTotal = topology.scrollContainersTotal ?? containersOut.length;
|
|
554
|
+
const scrollContainersTruncated = topology.scrollContainersTruncated ?? scrollContainersTotal > containersOut.length;
|
|
555
|
+
const scopeCountsUnavailable = available && (topology.iframesPresent === undefined || topology.shadowHostsPresent === undefined);
|
|
556
|
+
|
|
557
|
+
// Page.getLayoutMetrics's `css*` fields are a genuinely newer/optional
|
|
558
|
+
// addition (older Chrome CDP builds only ever populate the non-`css`
|
|
559
|
+
// pair) -- falling back from `cssVisualViewport` to `visualViewport` is a
|
|
560
|
+
// legitimate version fallback, not a malformed read. It IS a malformed
|
|
561
|
+
// read (Layer 2) when BOTH members of a pair are missing: the metrics
|
|
562
|
+
// call itself succeeded (this line has no catch -- see the module doc /
|
|
563
|
+
// audit row 36 -- so a throw here already propagates honestly), yet
|
|
564
|
+
// neither viewport shape came back.
|
|
565
|
+
const visualViewportRaw = metrics.cssVisualViewport ?? metrics.visualViewport;
|
|
566
|
+
const layoutViewportRaw = metrics.cssLayoutViewport ?? metrics.layoutViewport;
|
|
567
|
+
const visualViewportUnavailable = visualViewportRaw === undefined;
|
|
568
|
+
const layoutViewportUnavailable = layoutViewportRaw === undefined;
|
|
569
|
+
|
|
570
|
+
const report: ScrollReport = {
|
|
571
|
+
containers: containersOut,
|
|
572
|
+
scrollContainersTotal,
|
|
573
|
+
scrollContainersTruncated,
|
|
574
|
+
...(scrollContainersCountUnavailable ? { scrollContainersCountUnavailable: true as const } : {}),
|
|
575
|
+
documentScrollHeight: topology.documentScrollHeight,
|
|
576
|
+
documentScrollWidth: topology.documentScrollWidth,
|
|
577
|
+
visualViewport: visualViewportRaw ?? null,
|
|
578
|
+
...(visualViewportUnavailable ? { visualViewportUnavailable: true as const } : {}),
|
|
579
|
+
layoutViewport: layoutViewportRaw ?? null,
|
|
580
|
+
...(layoutViewportUnavailable ? { layoutViewportUnavailable: true as const } : {}),
|
|
581
|
+
available,
|
|
582
|
+
...(reason ? { reason } : {}),
|
|
583
|
+
scope: {
|
|
584
|
+
root: 'top-document',
|
|
585
|
+
shadowDom: 'light-only',
|
|
586
|
+
iframesPresent: topology.iframesPresent ?? 0,
|
|
587
|
+
shadowHostsPresent: topology.shadowHostsPresent ?? 0,
|
|
588
|
+
},
|
|
589
|
+
...(scopeCountsUnavailable ? { scopeCountsUnavailable: true as const } : {}),
|
|
590
|
+
restoration: {
|
|
591
|
+
attempted: true,
|
|
592
|
+
offsetsRestored: topology.offsetsRestored ?? false,
|
|
593
|
+
markersCleared,
|
|
594
|
+
...(topology.scriptError ? { error: sanitizeString(topology.scriptError) } : {}),
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
ctx.write.json('scroll.json', report);
|
|
599
|
+
};
|