@michaelmagan/dnumb 0.1.0

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.
Files changed (133) hide show
  1. package/AGENTS.md +180 -0
  2. package/LICENSE +21 -0
  3. package/README.md +297 -0
  4. package/dist/analysis/cluster.cjs +114 -0
  5. package/dist/analysis/cluster.d.cts +68 -0
  6. package/dist/analysis/cluster.d.ts +68 -0
  7. package/dist/analysis/cluster.js +109 -0
  8. package/dist/analysis/distances.cjs +124 -0
  9. package/dist/analysis/distances.d.cts +58 -0
  10. package/dist/analysis/distances.d.ts +58 -0
  11. package/dist/analysis/distances.js +119 -0
  12. package/dist/analysis/geometry.cjs +60 -0
  13. package/dist/analysis/geometry.d.cts +34 -0
  14. package/dist/analysis/geometry.d.ts +34 -0
  15. package/dist/analysis/geometry.js +53 -0
  16. package/dist/analysis/layout-tree.cjs +245 -0
  17. package/dist/analysis/layout-tree.d.cts +45 -0
  18. package/dist/analysis/layout-tree.d.ts +45 -0
  19. package/dist/analysis/layout-tree.js +242 -0
  20. package/dist/analysis/region-query.cjs +71 -0
  21. package/dist/analysis/region-query.d.cts +25 -0
  22. package/dist/analysis/region-query.d.ts +25 -0
  23. package/dist/analysis/region-query.js +67 -0
  24. package/dist/analysis/types.cjs +7 -0
  25. package/dist/analysis/types.d.cts +143 -0
  26. package/dist/analysis/types.d.ts +143 -0
  27. package/dist/analysis/types.js +6 -0
  28. package/dist/bundle.cjs +182 -0
  29. package/dist/bundle.d.cts +91 -0
  30. package/dist/bundle.d.ts +91 -0
  31. package/dist/bundle.js +175 -0
  32. package/dist/cli.cjs +755 -0
  33. package/dist/cli.d.cts +2 -0
  34. package/dist/cli.d.ts +2 -0
  35. package/dist/cli.js +719 -0
  36. package/dist/diff/changes.cjs +656 -0
  37. package/dist/diff/changes.d.cts +2 -0
  38. package/dist/diff/changes.d.ts +2 -0
  39. package/dist/diff/changes.js +653 -0
  40. package/dist/diff/identity.cjs +167 -0
  41. package/dist/diff/identity.d.cts +62 -0
  42. package/dist/diff/identity.d.ts +62 -0
  43. package/dist/diff/identity.js +159 -0
  44. package/dist/diff/index.cjs +84 -0
  45. package/dist/diff/index.d.cts +76 -0
  46. package/dist/diff/index.d.ts +76 -0
  47. package/dist/diff/index.js +68 -0
  48. package/dist/diff/match.cjs +156 -0
  49. package/dist/diff/match.d.cts +17 -0
  50. package/dist/diff/match.d.ts +17 -0
  51. package/dist/diff/match.js +152 -0
  52. package/dist/diff/render.cjs +540 -0
  53. package/dist/diff/render.d.cts +2 -0
  54. package/dist/diff/render.d.ts +2 -0
  55. package/dist/diff/render.js +537 -0
  56. package/dist/diff/types.cjs +2 -0
  57. package/dist/diff/types.d.cts +211 -0
  58. package/dist/diff/types.d.ts +211 -0
  59. package/dist/diff/types.js +1 -0
  60. package/dist/digest/index.cjs +8 -0
  61. package/dist/digest/index.d.cts +4 -0
  62. package/dist/digest/index.d.ts +4 -0
  63. package/dist/digest/index.js +2 -0
  64. package/dist/digest/parse.cjs +163 -0
  65. package/dist/digest/parse.d.cts +80 -0
  66. package/dist/digest/parse.d.ts +80 -0
  67. package/dist/digest/parse.js +160 -0
  68. package/dist/digest/render.cjs +1575 -0
  69. package/dist/digest/render.d.cts +41 -0
  70. package/dist/digest/render.d.ts +41 -0
  71. package/dist/digest/render.js +1571 -0
  72. package/dist/digest/types.cjs +10 -0
  73. package/dist/digest/types.d.cts +86 -0
  74. package/dist/digest/types.d.ts +86 -0
  75. package/dist/digest/types.js +9 -0
  76. package/dist/index.cjs +61 -0
  77. package/dist/index.d.cts +27 -0
  78. package/dist/index.d.ts +27 -0
  79. package/dist/index.js +19 -0
  80. package/dist/page.cjs +35 -0
  81. package/dist/page.d.cts +40 -0
  82. package/dist/page.d.ts +40 -0
  83. package/dist/page.js +31 -0
  84. package/dist/playwright.cjs +138 -0
  85. package/dist/playwright.d.cts +69 -0
  86. package/dist/playwright.d.ts +69 -0
  87. package/dist/playwright.js +131 -0
  88. package/dist/query.cjs +1152 -0
  89. package/dist/query.d.cts +47 -0
  90. package/dist/query.d.ts +47 -0
  91. package/dist/query.js +1146 -0
  92. package/dist/snapshot/capture.cjs +80 -0
  93. package/dist/snapshot/capture.d.cts +34 -0
  94. package/dist/snapshot/capture.d.ts +34 -0
  95. package/dist/snapshot/capture.js +77 -0
  96. package/dist/snapshot/cdp.cjs +450 -0
  97. package/dist/snapshot/cdp.d.cts +36 -0
  98. package/dist/snapshot/cdp.d.ts +36 -0
  99. package/dist/snapshot/cdp.js +447 -0
  100. package/dist/snapshot/clip.cjs +100 -0
  101. package/dist/snapshot/clip.d.cts +26 -0
  102. package/dist/snapshot/clip.d.ts +26 -0
  103. package/dist/snapshot/clip.js +96 -0
  104. package/dist/snapshot/collect.cjs +358 -0
  105. package/dist/snapshot/collect.d.cts +18 -0
  106. package/dist/snapshot/collect.d.ts +18 -0
  107. package/dist/snapshot/collect.js +355 -0
  108. package/dist/snapshot/color.cjs +1039 -0
  109. package/dist/snapshot/color.d.cts +316 -0
  110. package/dist/snapshot/color.d.ts +316 -0
  111. package/dist/snapshot/color.js +1026 -0
  112. package/dist/snapshot/facts.cjs +167 -0
  113. package/dist/snapshot/facts.d.cts +48 -0
  114. package/dist/snapshot/facts.d.ts +48 -0
  115. package/dist/snapshot/facts.js +161 -0
  116. package/dist/snapshot/limits.cjs +69 -0
  117. package/dist/snapshot/limits.d.cts +59 -0
  118. package/dist/snapshot/limits.d.ts +59 -0
  119. package/dist/snapshot/limits.js +65 -0
  120. package/dist/snapshot/not-measured.cjs +44 -0
  121. package/dist/snapshot/not-measured.d.cts +14 -0
  122. package/dist/snapshot/not-measured.d.ts +14 -0
  123. package/dist/snapshot/not-measured.js +41 -0
  124. package/dist/snapshot/types.cjs +2 -0
  125. package/dist/snapshot/types.d.cts +238 -0
  126. package/dist/snapshot/types.d.ts +238 -0
  127. package/dist/snapshot/types.js +1 -0
  128. package/dist/spec/index.cjs +9 -0
  129. package/dist/spec/index.d.cts +15 -0
  130. package/dist/spec/index.d.ts +15 -0
  131. package/dist/spec/index.js +6 -0
  132. package/package.json +141 -0
  133. package/skills/dnumb/SKILL.md +136 -0
@@ -0,0 +1,355 @@
1
+ export function collectSnapshot() {
2
+ // Inlined, not imported: `page.evaluate` serializes only this function's own
3
+ // source, so nothing from module scope survives the trip into the page.
4
+ // Kept in sync with `limits.ts` by `test/unit/limits.test.ts`.
5
+ const MAX_ELEMENTS = 5000;
6
+ const SKIPPED_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'SVG']);
7
+ const TEXT_PREVIEW_LENGTH = 60;
8
+ const NAME_LENGTH = 80;
9
+ const TESTID_ATTRS = ['data-testid', 'data-test-id', 'data-test', 'data-qa'];
10
+ // Kept in sync with `limits.ts#COLOR_TOKEN_CAP` by `test/unit/limits.test.ts` --
11
+ // duplicated, not imported, for the same reason `MAX_ELEMENTS` is: this
12
+ // function's source is serialized whole into the page, and a runtime import
13
+ // from another module would not survive that trip.
14
+ const COLOR_TOKEN_CAP = 120;
15
+ // Kept in sync with `limits.ts#capBackgroundImage` -- same reason, same
16
+ // discipline. Preserves both ends of a long gradient string rather than a
17
+ // plain prefix cut, so the *last* color stop survives capture; see that
18
+ // function's doc comment (A4).
19
+ function capBackgroundImage(raw) {
20
+ if (raw.length <= COLOR_TOKEN_CAP)
21
+ return raw;
22
+ const ELLIPSIS = '…';
23
+ const half = Math.max(0, Math.floor((COLOR_TOKEN_CAP - ELLIPSIS.length) / 2));
24
+ return `${raw.slice(0, half)}${ELLIPSIS}${raw.slice(raw.length - half)}`;
25
+ }
26
+ // ---- computed role ------------------------------------------------------
27
+ // The prototype captured `getAttribute('role')`, which is null on nearly
28
+ // every real element, so the identity ladder's strongest rung was dead on
29
+ // arrival. This is the implicit-role mapping that fixes it.
30
+ // biome-ignore format: a lookup table reads as a table; one tag per line is 37
31
+ // lines of noise for the same information.
32
+ const IMPLICIT_ROLE = {
33
+ ARTICLE: 'article', ASIDE: 'complementary', BUTTON: 'button', DIALOG: 'dialog',
34
+ FIELDSET: 'group', FIGURE: 'figure', FOOTER: 'contentinfo', FORM: 'form',
35
+ H1: 'heading', H2: 'heading', H3: 'heading', H4: 'heading', H5: 'heading',
36
+ H6: 'heading', HEADER: 'banner', HR: 'separator', IMG: 'img', LI: 'listitem',
37
+ MAIN: 'main', NAV: 'navigation', OL: 'list', OPTION: 'option', OUTPUT: 'status',
38
+ P: 'paragraph', PROGRESS: 'progressbar', SECTION: 'region', SELECT: 'combobox',
39
+ SUMMARY: 'button', TABLE: 'table', TBODY: 'rowgroup', TD: 'cell',
40
+ TEXTAREA: 'textbox', TFOOT: 'rowgroup', TH: 'columnheader', THEAD: 'rowgroup',
41
+ TR: 'row', UL: 'list',
42
+ };
43
+ // biome-ignore format: see above.
44
+ const INPUT_ROLE = {
45
+ button: 'button', checkbox: 'checkbox', email: 'textbox', image: 'button',
46
+ number: 'spinbutton', radio: 'radio', range: 'slider', reset: 'button',
47
+ search: 'searchbox', submit: 'button', tel: 'textbox', text: 'textbox',
48
+ url: 'textbox',
49
+ };
50
+ /** Roles whose accessible name comes from their own content. */
51
+ // biome-ignore format: see above.
52
+ const NAME_FROM_CONTENT = new Set([
53
+ 'button', 'cell', 'checkbox', 'columnheader', 'gridcell', 'heading', 'link',
54
+ 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'row',
55
+ 'rowheader', 'switch', 'tab', 'tooltip', 'treeitem',
56
+ ]);
57
+ function computedRole(el) {
58
+ const explicit = (el.getAttribute('role') ?? '').trim().split(/\s+/)[0];
59
+ if (explicit)
60
+ return explicit;
61
+ const tag = el.tagName.toUpperCase();
62
+ if (tag === 'A' || tag === 'AREA')
63
+ return el.hasAttribute('href') ? 'link' : null;
64
+ if (tag === 'INPUT') {
65
+ const type = (el.getAttribute('type') ?? 'text').toLowerCase();
66
+ return INPUT_ROLE[type] ?? 'textbox';
67
+ }
68
+ // `header` and `footer` are landmarks only at the top level.
69
+ if ((tag === 'HEADER' || tag === 'FOOTER') && el.closest('article,aside,main,nav,section')) {
70
+ return null;
71
+ }
72
+ // `section` is a region only when it has an accessible name.
73
+ if (tag === 'SECTION' &&
74
+ !el.getAttribute('aria-label') &&
75
+ !el.getAttribute('aria-labelledby')) {
76
+ return null;
77
+ }
78
+ return IMPLICIT_ROLE[tag] ?? null;
79
+ }
80
+ function trimName(value) {
81
+ const text = (value ?? '').replace(/\s+/g, ' ').trim();
82
+ return text ? text.slice(0, NAME_LENGTH) : null;
83
+ }
84
+ function accessibleName(el, role) {
85
+ const label = trimName(el.getAttribute('aria-label'));
86
+ if (label)
87
+ return label;
88
+ const labelledby = el.getAttribute('aria-labelledby');
89
+ if (labelledby) {
90
+ const parts = [];
91
+ for (const id of labelledby.split(/\s+/)) {
92
+ parts.push(document.getElementById(id)?.textContent ?? '');
93
+ }
94
+ const joined = trimName(parts.join(' '));
95
+ if (joined)
96
+ return joined;
97
+ }
98
+ const tag = el.tagName.toUpperCase();
99
+ if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') {
100
+ if (el.id) {
101
+ const forLabel = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
102
+ const text = trimName(forLabel?.textContent);
103
+ if (text)
104
+ return text;
105
+ }
106
+ const wrapping = trimName(el.closest('label')?.textContent);
107
+ if (wrapping)
108
+ return wrapping;
109
+ const placeholder = trimName(el.getAttribute('placeholder'));
110
+ if (placeholder)
111
+ return placeholder;
112
+ }
113
+ if (tag === 'IMG')
114
+ return trimName(el.getAttribute('alt'));
115
+ if (role && NAME_FROM_CONTENT.has(role)) {
116
+ const text = trimName(el.textContent);
117
+ if (text)
118
+ return text;
119
+ }
120
+ return trimName(el.getAttribute('title'));
121
+ }
122
+ // ---- geometry / style ---------------------------------------------------
123
+ function childrenOf(el) {
124
+ const result = [];
125
+ for (let i = 0; i < el.children.length; i++) {
126
+ const child = el.children[i];
127
+ if (child)
128
+ result.push(child);
129
+ }
130
+ return result;
131
+ }
132
+ function boxSides(cs, prefix) {
133
+ const suffix = prefix === 'border' ? 'Width' : '';
134
+ return {
135
+ top: Number.parseFloat(cs.getPropertyValue(`${prefix}-top${suffix}`)) || 0,
136
+ right: Number.parseFloat(cs.getPropertyValue(`${prefix}-right${suffix}`)) || 0,
137
+ bottom: Number.parseFloat(cs.getPropertyValue(`${prefix}-bottom${suffix}`)) || 0,
138
+ left: Number.parseFloat(cs.getPropertyValue(`${prefix}-left${suffix}`)) || 0,
139
+ };
140
+ }
141
+ function headingLevel(el) {
142
+ const ariaLevel = el.getAttribute('aria-level');
143
+ if (ariaLevel && Number.isFinite(Number(ariaLevel)))
144
+ return Number(ariaLevel);
145
+ const match = /^H([1-6])$/.exec(el.tagName);
146
+ return match?.[1] ? Number(match[1]) : null;
147
+ }
148
+ function ownText(el) {
149
+ let text = '';
150
+ for (const node of el.childNodes) {
151
+ if (node.nodeType === 3 /* TEXT_NODE */)
152
+ text += node.textContent ?? '';
153
+ }
154
+ return text.replace(/\s+/g, ' ').trim().slice(0, TEXT_PREVIEW_LENGTH);
155
+ }
156
+ const elements = [];
157
+ const byId = new Map();
158
+ // The DOM element behind each captured record, kept only long enough for
159
+ // the second, post-walk pass below to hit-test it -- `visit` is a pre-order
160
+ // walk, so an *unrelated* overlay elsewhere in the document (a modal
161
+ // appended near the end of `<body>`, positioned over content captured
162
+ // earlier) would not have an id yet if this ran inline during the walk.
163
+ const domById = new Map();
164
+ let nextId = 0;
165
+ let truncated = false;
166
+ // `NotMeasured` counters. See the doc comment on the type: this walk never
167
+ // enters a shadow root or an iframe document, so these are the only record
168
+ // of what it walked past.
169
+ let shadowRootsOpen = 0;
170
+ let iframesSameOrigin = 0;
171
+ let iframesUnknownOrigin = 0;
172
+ function visit(el, parentId) {
173
+ if (elements.length >= MAX_ELEMENTS) {
174
+ truncated = true;
175
+ return;
176
+ }
177
+ // SVG-namespace elements report a lowercase `tagName`, unlike HTML
178
+ // elements which are always uppercase — so this comparison must normalize
179
+ // case or the skip never fires for SVG internals.
180
+ const tagName = el.tagName.toUpperCase();
181
+ if (SKIPPED_TAGS.has(tagName))
182
+ return;
183
+ // `NotMeasured`: this walk reads `el.children`, which is light-DOM only --
184
+ // it never descends into a shadow root or an iframe document, so this is
185
+ // the only place either is ever seen at all.
186
+ //
187
+ // An *open* shadow root is detectable after the fact: `el.shadowRoot` is
188
+ // the live reference `attachShadow({mode:'open'})` returned. A *closed*
189
+ // one returns `null` from that same property -- indistinguishable from
190
+ // "no shadow root here" -- so closed roots stay uncounted; see the
191
+ // `NotMeasured` doc comment for why the header reports that as 'unknown'
192
+ // rather than guessing zero.
193
+ if (el.shadowRoot)
194
+ shadowRootsOpen++;
195
+ if (tagName === 'IFRAME') {
196
+ // `contentDocument` is `null` and does not throw whenever this document
197
+ // cannot read the frame's document -- but that is not only true
198
+ // cross-origin. It is also `null` for a same-origin frame on `file://`
199
+ // (no origin to compare against), a `sandbox`-ed frame missing
200
+ // `allow-same-origin` (an opaque origin, even when the `src` is the same
201
+ // server), and a frame that has not navigated yet. All four look
202
+ // identical from here, so a `null` reading can only ever mean "this walk
203
+ // cannot resolve the origin", never a positive cross-origin
204
+ // classification -- that is what `unknownOrigin` is for. A non-null
205
+ // `contentDocument` *is* a reliable same-origin signal, so that half
206
+ // stays a real count.
207
+ if (el.contentDocument !== null)
208
+ iframesSameOrigin++;
209
+ else
210
+ iframesUnknownOrigin++;
211
+ }
212
+ const cs = getComputedStyle(el);
213
+ const rect = el.getBoundingClientRect();
214
+ const opacity = Number.parseFloat(cs.getPropertyValue('opacity'));
215
+ const display = cs.getPropertyValue('display');
216
+ const visibility = cs.getPropertyValue('visibility');
217
+ const visible = display !== 'none' &&
218
+ visibility !== 'hidden' &&
219
+ !(Number.isFinite(opacity) && opacity === 0) &&
220
+ rect.width > 0 &&
221
+ rect.height > 0;
222
+ const id = `e${nextId}`;
223
+ nextId++;
224
+ let testId = null;
225
+ for (const attr of TESTID_ATTRS) {
226
+ const value = el.getAttribute(attr);
227
+ if (value) {
228
+ testId = value;
229
+ break;
230
+ }
231
+ }
232
+ const role = computedRole(el);
233
+ const record = {
234
+ id,
235
+ tag: el.tagName.toLowerCase(),
236
+ testId,
237
+ domId: el.id || null,
238
+ role,
239
+ name: accessibleName(el, role),
240
+ className: el.getAttribute('class') ?? '',
241
+ headingLevel: headingLevel(el),
242
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
243
+ scrollWidth: el.scrollWidth,
244
+ scrollHeight: el.scrollHeight,
245
+ clientWidth: el.clientWidth,
246
+ clientHeight: el.clientHeight,
247
+ overflowX: cs.getPropertyValue('overflow-x'),
248
+ overflowY: cs.getPropertyValue('overflow-y'),
249
+ position: cs.getPropertyValue('position'),
250
+ display,
251
+ visibility,
252
+ opacity: Number.isFinite(opacity) ? opacity : 1,
253
+ margin: boxSides(cs, 'margin'),
254
+ padding: boxSides(cs, 'padding'),
255
+ gap: {
256
+ row: Number.parseFloat(cs.getPropertyValue('row-gap')) || 0,
257
+ column: Number.parseFloat(cs.getPropertyValue('column-gap')) || 0,
258
+ },
259
+ border: boxSides(cs, 'border'),
260
+ color: cs.getPropertyValue('color'),
261
+ backgroundColor: cs.getPropertyValue('background-color'),
262
+ backgroundImage: capBackgroundImage(cs.getPropertyValue('background-image')),
263
+ textShadow: cs.getPropertyValue('text-shadow').slice(0, COLOR_TOKEN_CAP),
264
+ textStrokeWidth: cs.getPropertyValue('-webkit-text-stroke-width'),
265
+ textStrokeColor: cs.getPropertyValue('-webkit-text-stroke-color'),
266
+ fontSize: cs.getPropertyValue('font-size'),
267
+ fontWeight: Number.parseFloat(cs.getPropertyValue('font-weight')) || 400,
268
+ textAlign: cs.getPropertyValue('text-align'),
269
+ flexDirection: cs.getPropertyValue('flex-direction'),
270
+ gridTemplateColumns: cs.getPropertyValue('grid-template-columns'),
271
+ textOverflow: cs.getPropertyValue('text-overflow'),
272
+ whiteSpace: cs.getPropertyValue('white-space'),
273
+ overflowWrap: cs.getPropertyValue('overflow-wrap'),
274
+ text: ownText(el),
275
+ parentId,
276
+ childIds: [],
277
+ visible,
278
+ ariaExpanded: el.getAttribute('aria-expanded'),
279
+ // Filled in by the second pass below, once every id in the document
280
+ // exists to resolve a hit-test against -- see `domById`'s doc comment.
281
+ hitStackIds: null,
282
+ paintOrder: null,
283
+ };
284
+ elements.push(record);
285
+ byId.set(id, record);
286
+ domById.set(id, el);
287
+ if (parentId)
288
+ byId.get(parentId)?.childIds.push(id);
289
+ for (const child of childrenOf(el)) {
290
+ if (elements.length >= MAX_ELEMENTS) {
291
+ truncated = true;
292
+ break;
293
+ }
294
+ visit(child, id);
295
+ }
296
+ }
297
+ visit(document.body, null);
298
+ // The overlap signal `snapshot/color.ts` needs to tell "a real ancestor
299
+ // backdrop" from "some unrelated element painted over or behind this
300
+ // text" -- see `ElementRecord.hitStackIds`'s doc comment for why this is a
301
+ // second pass rather than done inline above. Gated on the same "visible,
302
+ // has its own text" condition the color fact itself uses (§4 of
303
+ // `docs/notes/03-color.md`): no text, no ink-on-backdrop question to probe,
304
+ // so no reason to pay for a hit test.
305
+ const reverseId = new Map();
306
+ for (const [id, domEl] of domById)
307
+ reverseId.set(domEl, id);
308
+ for (const [id, domEl] of domById) {
309
+ const record = byId.get(id);
310
+ if (!record?.visible || !record.text)
311
+ continue;
312
+ const rect = domEl.getBoundingClientRect();
313
+ const cx = rect.x + rect.width / 2;
314
+ const cy = rect.y + rect.height / 2;
315
+ const stack = document.elementsFromPoint(cx, cy);
316
+ const ids = [];
317
+ for (const hit of stack) {
318
+ const hitId = reverseId.get(hit);
319
+ // An entry this capture never assigned an id to (an `SVG` internal, a
320
+ // node past the element cap) carries no signal `color.ts` can act on --
321
+ // dropped rather than guessed at, the same discipline `NotMeasured`
322
+ // uses for what a capture walked past without measuring.
323
+ if (hitId)
324
+ ids.push(hitId);
325
+ }
326
+ record.hitStackIds = ids;
327
+ }
328
+ // `<html>` (`documentElement`) is never part of `elements` -- this walk
329
+ // starts at `<body>` (see this function's own `visit(document.body, null)`
330
+ // call below) -- so without reading it here, a compositing walk that runs
331
+ // off the top of the ancestor chain has no way to know the browser's real
332
+ // default canvas fill and no honest option but to guess (A2). See
333
+ // `DocumentBackground`'s doc comment.
334
+ const htmlStyle = getComputedStyle(document.documentElement);
335
+ return {
336
+ elements,
337
+ truncated,
338
+ viewport: { width: window.innerWidth, height: window.innerHeight },
339
+ notMeasured: {
340
+ shadowRoots: { open: shadowRootsOpen, closed: 'unknown' },
341
+ iframes: {
342
+ sameOrigin: iframesSameOrigin,
343
+ // This walk has no positive signal for cross-origin -- see the check
344
+ // above -- so it never asserts one. An unreachable frame is always
345
+ // counted as `unknownOrigin`, never guessed into `crossOrigin`.
346
+ crossOrigin: 0,
347
+ unknownOrigin: iframesUnknownOrigin,
348
+ },
349
+ },
350
+ documentBackground: {
351
+ backgroundColor: htmlStyle.getPropertyValue('background-color'),
352
+ colorScheme: htmlStyle.getPropertyValue('color-scheme'),
353
+ },
354
+ };
355
+ }