@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.
- package/AGENTS.md +180 -0
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/analysis/cluster.cjs +114 -0
- package/dist/analysis/cluster.d.cts +68 -0
- package/dist/analysis/cluster.d.ts +68 -0
- package/dist/analysis/cluster.js +109 -0
- package/dist/analysis/distances.cjs +124 -0
- package/dist/analysis/distances.d.cts +58 -0
- package/dist/analysis/distances.d.ts +58 -0
- package/dist/analysis/distances.js +119 -0
- package/dist/analysis/geometry.cjs +60 -0
- package/dist/analysis/geometry.d.cts +34 -0
- package/dist/analysis/geometry.d.ts +34 -0
- package/dist/analysis/geometry.js +53 -0
- package/dist/analysis/layout-tree.cjs +245 -0
- package/dist/analysis/layout-tree.d.cts +45 -0
- package/dist/analysis/layout-tree.d.ts +45 -0
- package/dist/analysis/layout-tree.js +242 -0
- package/dist/analysis/region-query.cjs +71 -0
- package/dist/analysis/region-query.d.cts +25 -0
- package/dist/analysis/region-query.d.ts +25 -0
- package/dist/analysis/region-query.js +67 -0
- package/dist/analysis/types.cjs +7 -0
- package/dist/analysis/types.d.cts +143 -0
- package/dist/analysis/types.d.ts +143 -0
- package/dist/analysis/types.js +6 -0
- package/dist/bundle.cjs +182 -0
- package/dist/bundle.d.cts +91 -0
- package/dist/bundle.d.ts +91 -0
- package/dist/bundle.js +175 -0
- package/dist/cli.cjs +755 -0
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +719 -0
- package/dist/diff/changes.cjs +656 -0
- package/dist/diff/changes.d.cts +2 -0
- package/dist/diff/changes.d.ts +2 -0
- package/dist/diff/changes.js +653 -0
- package/dist/diff/identity.cjs +167 -0
- package/dist/diff/identity.d.cts +62 -0
- package/dist/diff/identity.d.ts +62 -0
- package/dist/diff/identity.js +159 -0
- package/dist/diff/index.cjs +84 -0
- package/dist/diff/index.d.cts +76 -0
- package/dist/diff/index.d.ts +76 -0
- package/dist/diff/index.js +68 -0
- package/dist/diff/match.cjs +156 -0
- package/dist/diff/match.d.cts +17 -0
- package/dist/diff/match.d.ts +17 -0
- package/dist/diff/match.js +152 -0
- package/dist/diff/render.cjs +540 -0
- package/dist/diff/render.d.cts +2 -0
- package/dist/diff/render.d.ts +2 -0
- package/dist/diff/render.js +537 -0
- package/dist/diff/types.cjs +2 -0
- package/dist/diff/types.d.cts +211 -0
- package/dist/diff/types.d.ts +211 -0
- package/dist/diff/types.js +1 -0
- package/dist/digest/index.cjs +8 -0
- package/dist/digest/index.d.cts +4 -0
- package/dist/digest/index.d.ts +4 -0
- package/dist/digest/index.js +2 -0
- package/dist/digest/parse.cjs +163 -0
- package/dist/digest/parse.d.cts +80 -0
- package/dist/digest/parse.d.ts +80 -0
- package/dist/digest/parse.js +160 -0
- package/dist/digest/render.cjs +1575 -0
- package/dist/digest/render.d.cts +41 -0
- package/dist/digest/render.d.ts +41 -0
- package/dist/digest/render.js +1571 -0
- package/dist/digest/types.cjs +10 -0
- package/dist/digest/types.d.cts +86 -0
- package/dist/digest/types.d.ts +86 -0
- package/dist/digest/types.js +9 -0
- package/dist/index.cjs +61 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +19 -0
- package/dist/page.cjs +35 -0
- package/dist/page.d.cts +40 -0
- package/dist/page.d.ts +40 -0
- package/dist/page.js +31 -0
- package/dist/playwright.cjs +138 -0
- package/dist/playwright.d.cts +69 -0
- package/dist/playwright.d.ts +69 -0
- package/dist/playwright.js +131 -0
- package/dist/query.cjs +1152 -0
- package/dist/query.d.cts +47 -0
- package/dist/query.d.ts +47 -0
- package/dist/query.js +1146 -0
- package/dist/snapshot/capture.cjs +80 -0
- package/dist/snapshot/capture.d.cts +34 -0
- package/dist/snapshot/capture.d.ts +34 -0
- package/dist/snapshot/capture.js +77 -0
- package/dist/snapshot/cdp.cjs +450 -0
- package/dist/snapshot/cdp.d.cts +36 -0
- package/dist/snapshot/cdp.d.ts +36 -0
- package/dist/snapshot/cdp.js +447 -0
- package/dist/snapshot/clip.cjs +100 -0
- package/dist/snapshot/clip.d.cts +26 -0
- package/dist/snapshot/clip.d.ts +26 -0
- package/dist/snapshot/clip.js +96 -0
- package/dist/snapshot/collect.cjs +358 -0
- package/dist/snapshot/collect.d.cts +18 -0
- package/dist/snapshot/collect.d.ts +18 -0
- package/dist/snapshot/collect.js +355 -0
- package/dist/snapshot/color.cjs +1039 -0
- package/dist/snapshot/color.d.cts +316 -0
- package/dist/snapshot/color.d.ts +316 -0
- package/dist/snapshot/color.js +1026 -0
- package/dist/snapshot/facts.cjs +167 -0
- package/dist/snapshot/facts.d.cts +48 -0
- package/dist/snapshot/facts.d.ts +48 -0
- package/dist/snapshot/facts.js +161 -0
- package/dist/snapshot/limits.cjs +69 -0
- package/dist/snapshot/limits.d.cts +59 -0
- package/dist/snapshot/limits.d.ts +59 -0
- package/dist/snapshot/limits.js +65 -0
- package/dist/snapshot/not-measured.cjs +44 -0
- package/dist/snapshot/not-measured.d.cts +14 -0
- package/dist/snapshot/not-measured.d.ts +14 -0
- package/dist/snapshot/not-measured.js +41 -0
- package/dist/snapshot/types.cjs +2 -0
- package/dist/snapshot/types.d.cts +238 -0
- package/dist/snapshot/types.d.ts +238 -0
- package/dist/snapshot/types.js +1 -0
- package/dist/spec/index.cjs +9 -0
- package/dist/spec/index.d.cts +15 -0
- package/dist/spec/index.d.ts +15 -0
- package/dist/spec/index.js +6 -0
- package/package.json +141 -0
- package/skills/dnumb/SKILL.md +136 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.capture = capture;
|
|
4
|
+
/**
|
|
5
|
+
* Capture a page as an `ElementRecord[]`, by whichever path this browser
|
|
6
|
+
* supports.
|
|
7
|
+
*
|
|
8
|
+
* The CDP path is tried first and is allowed to fail silently — it is an
|
|
9
|
+
* experimental domain on a single browser engine, and a capture that works
|
|
10
|
+
* everywhere matters more than one that is 6x faster on Chromium. The portable
|
|
11
|
+
* walk is the floor, not the fallback of last resort.
|
|
12
|
+
*/
|
|
13
|
+
const page_js_1 = require("../page.cjs");
|
|
14
|
+
const cdp_js_1 = require("./cdp.cjs");
|
|
15
|
+
const clip_js_1 = require("./clip.cjs");
|
|
16
|
+
const collect_js_1 = require("./collect.cjs");
|
|
17
|
+
/** A compact description of a value for an error message — not a full dump. */
|
|
18
|
+
function describeValue(value) {
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return 'undefined';
|
|
21
|
+
if (value === null)
|
|
22
|
+
return 'null';
|
|
23
|
+
if (Array.isArray(value))
|
|
24
|
+
return `an array of length ${value.length}`;
|
|
25
|
+
if (typeof value === 'object') {
|
|
26
|
+
const keys = Object.keys(value);
|
|
27
|
+
return keys.length > 0 ? `an object with keys: ${keys.join(', ')}` : 'an empty object';
|
|
28
|
+
}
|
|
29
|
+
return `${typeof value} ${JSON.stringify(value)}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `PageLike` is deliberately minimal — anything with an `evaluate` that
|
|
33
|
+
* actually runs the function it is given and returns the result qualifies,
|
|
34
|
+
* Playwright or not. A double that does not hold up that contract (returns
|
|
35
|
+
* `undefined`, a canned mock, the wrong shape) used to reach `attachClipFacts`
|
|
36
|
+
* and die on `elements.map` inside a function the caller has never heard of.
|
|
37
|
+
* This checks the contract at the one point it can be checked without
|
|
38
|
+
* assuming anything about *which* driver is behind `page` — real Playwright,
|
|
39
|
+
* Puppeteer, or a hand-rolled test double all satisfy it identically when
|
|
40
|
+
* `evaluate` behaves.
|
|
41
|
+
*/
|
|
42
|
+
function assertSnapshot(value) {
|
|
43
|
+
if (typeof value !== 'object' ||
|
|
44
|
+
value === null ||
|
|
45
|
+
!Array.isArray(value.elements)) {
|
|
46
|
+
throw new Error('dnumb: page.evaluate(collectSnapshot) did not return a Snapshot ' +
|
|
47
|
+
'({ elements: ElementRecord[], truncated: boolean, viewport: { width, height } }); ' +
|
|
48
|
+
`got ${describeValue(value)}. This page satisfies the minimal PageLike shape ` +
|
|
49
|
+
'({ evaluate }), but its evaluate() did not actually run the function it was given ' +
|
|
50
|
+
'and hand back the result — check whatever is behind it: a mock, a test double, or a ' +
|
|
51
|
+
"driver whose evaluate() doesn't behave like Playwright's or Puppeteer's.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function capture(page, options = {}) {
|
|
55
|
+
const strategy = options.strategy ?? 'auto';
|
|
56
|
+
let cdpFellBack = false;
|
|
57
|
+
if (strategy !== 'walk' && (0, page_js_1.isCDPCapable)(page)) {
|
|
58
|
+
const viaCDP = await (0, cdp_js_1.captureViaCDP)(page);
|
|
59
|
+
if (viaCDP) {
|
|
60
|
+
(0, clip_js_1.attachClipFacts)(viaCDP.elements);
|
|
61
|
+
return { ...viaCDP, strategy: 'cdp' };
|
|
62
|
+
}
|
|
63
|
+
if (strategy === 'cdp') {
|
|
64
|
+
throw new Error('dnumb: the CDP capture path failed. It requires Chromium and the experimental ' +
|
|
65
|
+
'DOMSnapshot domain. Use strategy "auto" to fall back to the portable walk.');
|
|
66
|
+
}
|
|
67
|
+
// This page could open a CDP session -- it is Chromium -- but the
|
|
68
|
+
// DOMSnapshot call itself failed, so the walk below is an unplanned
|
|
69
|
+
// downgrade, not a deliberate portable capture.
|
|
70
|
+
cdpFellBack = true;
|
|
71
|
+
}
|
|
72
|
+
else if (strategy === 'cdp') {
|
|
73
|
+
throw new Error('dnumb: this page cannot open a CDP session. CDP capture is Chromium-only. ' +
|
|
74
|
+
'Use strategy "auto" or "walk".');
|
|
75
|
+
}
|
|
76
|
+
const walked = await page.evaluate(collect_js_1.collectSnapshot);
|
|
77
|
+
assertSnapshot(walked);
|
|
78
|
+
(0, clip_js_1.attachClipFacts)(walked.elements);
|
|
79
|
+
return { ...walked, strategy: 'walk', ...(cdpFellBack ? { cdpFellBack: true } : {}) };
|
|
80
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture a page as an `ElementRecord[]`, by whichever path this browser
|
|
3
|
+
* supports.
|
|
4
|
+
*
|
|
5
|
+
* The CDP path is tried first and is allowed to fail silently — it is an
|
|
6
|
+
* experimental domain on a single browser engine, and a capture that works
|
|
7
|
+
* everywhere matters more than one that is 6x faster on Chromium. The portable
|
|
8
|
+
* walk is the floor, not the fallback of last resort.
|
|
9
|
+
*/
|
|
10
|
+
import { type PageLike } from "../page.cjs";
|
|
11
|
+
import type { Snapshot } from "./types.cjs";
|
|
12
|
+
export interface CaptureOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Force a path instead of preferring CDP. `'walk'` is the portable in-page
|
|
15
|
+
* collector; `'cdp'` returns an error rather than silently degrading, which
|
|
16
|
+
* is what a test asserting the two paths agree needs.
|
|
17
|
+
*/
|
|
18
|
+
strategy?: 'auto' | 'walk' | 'cdp';
|
|
19
|
+
}
|
|
20
|
+
export interface CaptureResult extends Snapshot {
|
|
21
|
+
/** Which path produced these records. Recorded in the bundle, not inferred. */
|
|
22
|
+
strategy: 'cdp' | 'walk';
|
|
23
|
+
/**
|
|
24
|
+
* `true` only for a `'walk'` result that got there because the CDP fast
|
|
25
|
+
* path was tried on a CDP-capable (Chromium) page and its own protocol call
|
|
26
|
+
* failed — never for a `'walk'` result on a browser that was never
|
|
27
|
+
* CDP-capable to begin with. `strategy` alone cannot tell the two apart,
|
|
28
|
+
* and only the first is a downgrade worth flagging: it means this page's
|
|
29
|
+
* shadow DOM content, which CDP would have captured in full, went uncounted
|
|
30
|
+
* for a reason that had nothing to do with which browser this is.
|
|
31
|
+
*/
|
|
32
|
+
cdpFellBack?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare function capture(page: PageLike, options?: CaptureOptions): Promise<CaptureResult>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture a page as an `ElementRecord[]`, by whichever path this browser
|
|
3
|
+
* supports.
|
|
4
|
+
*
|
|
5
|
+
* The CDP path is tried first and is allowed to fail silently — it is an
|
|
6
|
+
* experimental domain on a single browser engine, and a capture that works
|
|
7
|
+
* everywhere matters more than one that is 6x faster on Chromium. The portable
|
|
8
|
+
* walk is the floor, not the fallback of last resort.
|
|
9
|
+
*/
|
|
10
|
+
import { type PageLike } from "../page.js";
|
|
11
|
+
import type { Snapshot } from "./types.js";
|
|
12
|
+
export interface CaptureOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Force a path instead of preferring CDP. `'walk'` is the portable in-page
|
|
15
|
+
* collector; `'cdp'` returns an error rather than silently degrading, which
|
|
16
|
+
* is what a test asserting the two paths agree needs.
|
|
17
|
+
*/
|
|
18
|
+
strategy?: 'auto' | 'walk' | 'cdp';
|
|
19
|
+
}
|
|
20
|
+
export interface CaptureResult extends Snapshot {
|
|
21
|
+
/** Which path produced these records. Recorded in the bundle, not inferred. */
|
|
22
|
+
strategy: 'cdp' | 'walk';
|
|
23
|
+
/**
|
|
24
|
+
* `true` only for a `'walk'` result that got there because the CDP fast
|
|
25
|
+
* path was tried on a CDP-capable (Chromium) page and its own protocol call
|
|
26
|
+
* failed — never for a `'walk'` result on a browser that was never
|
|
27
|
+
* CDP-capable to begin with. `strategy` alone cannot tell the two apart,
|
|
28
|
+
* and only the first is a downgrade worth flagging: it means this page's
|
|
29
|
+
* shadow DOM content, which CDP would have captured in full, went uncounted
|
|
30
|
+
* for a reason that had nothing to do with which browser this is.
|
|
31
|
+
*/
|
|
32
|
+
cdpFellBack?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare function capture(page: PageLike, options?: CaptureOptions): Promise<CaptureResult>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture a page as an `ElementRecord[]`, by whichever path this browser
|
|
3
|
+
* supports.
|
|
4
|
+
*
|
|
5
|
+
* The CDP path is tried first and is allowed to fail silently — it is an
|
|
6
|
+
* experimental domain on a single browser engine, and a capture that works
|
|
7
|
+
* everywhere matters more than one that is 6x faster on Chromium. The portable
|
|
8
|
+
* walk is the floor, not the fallback of last resort.
|
|
9
|
+
*/
|
|
10
|
+
import { isCDPCapable } from "../page.js";
|
|
11
|
+
import { captureViaCDP } from "./cdp.js";
|
|
12
|
+
import { attachClipFacts } from "./clip.js";
|
|
13
|
+
import { collectSnapshot } from "./collect.js";
|
|
14
|
+
/** A compact description of a value for an error message — not a full dump. */
|
|
15
|
+
function describeValue(value) {
|
|
16
|
+
if (value === undefined)
|
|
17
|
+
return 'undefined';
|
|
18
|
+
if (value === null)
|
|
19
|
+
return 'null';
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return `an array of length ${value.length}`;
|
|
22
|
+
if (typeof value === 'object') {
|
|
23
|
+
const keys = Object.keys(value);
|
|
24
|
+
return keys.length > 0 ? `an object with keys: ${keys.join(', ')}` : 'an empty object';
|
|
25
|
+
}
|
|
26
|
+
return `${typeof value} ${JSON.stringify(value)}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `PageLike` is deliberately minimal — anything with an `evaluate` that
|
|
30
|
+
* actually runs the function it is given and returns the result qualifies,
|
|
31
|
+
* Playwright or not. A double that does not hold up that contract (returns
|
|
32
|
+
* `undefined`, a canned mock, the wrong shape) used to reach `attachClipFacts`
|
|
33
|
+
* and die on `elements.map` inside a function the caller has never heard of.
|
|
34
|
+
* This checks the contract at the one point it can be checked without
|
|
35
|
+
* assuming anything about *which* driver is behind `page` — real Playwright,
|
|
36
|
+
* Puppeteer, or a hand-rolled test double all satisfy it identically when
|
|
37
|
+
* `evaluate` behaves.
|
|
38
|
+
*/
|
|
39
|
+
function assertSnapshot(value) {
|
|
40
|
+
if (typeof value !== 'object' ||
|
|
41
|
+
value === null ||
|
|
42
|
+
!Array.isArray(value.elements)) {
|
|
43
|
+
throw new Error('dnumb: page.evaluate(collectSnapshot) did not return a Snapshot ' +
|
|
44
|
+
'({ elements: ElementRecord[], truncated: boolean, viewport: { width, height } }); ' +
|
|
45
|
+
`got ${describeValue(value)}. This page satisfies the minimal PageLike shape ` +
|
|
46
|
+
'({ evaluate }), but its evaluate() did not actually run the function it was given ' +
|
|
47
|
+
'and hand back the result — check whatever is behind it: a mock, a test double, or a ' +
|
|
48
|
+
"driver whose evaluate() doesn't behave like Playwright's or Puppeteer's.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function capture(page, options = {}) {
|
|
52
|
+
const strategy = options.strategy ?? 'auto';
|
|
53
|
+
let cdpFellBack = false;
|
|
54
|
+
if (strategy !== 'walk' && isCDPCapable(page)) {
|
|
55
|
+
const viaCDP = await captureViaCDP(page);
|
|
56
|
+
if (viaCDP) {
|
|
57
|
+
attachClipFacts(viaCDP.elements);
|
|
58
|
+
return { ...viaCDP, strategy: 'cdp' };
|
|
59
|
+
}
|
|
60
|
+
if (strategy === 'cdp') {
|
|
61
|
+
throw new Error('dnumb: the CDP capture path failed. It requires Chromium and the experimental ' +
|
|
62
|
+
'DOMSnapshot domain. Use strategy "auto" to fall back to the portable walk.');
|
|
63
|
+
}
|
|
64
|
+
// This page could open a CDP session -- it is Chromium -- but the
|
|
65
|
+
// DOMSnapshot call itself failed, so the walk below is an unplanned
|
|
66
|
+
// downgrade, not a deliberate portable capture.
|
|
67
|
+
cdpFellBack = true;
|
|
68
|
+
}
|
|
69
|
+
else if (strategy === 'cdp') {
|
|
70
|
+
throw new Error('dnumb: this page cannot open a CDP session. CDP capture is Chromium-only. ' +
|
|
71
|
+
'Use strategy "auto" or "walk".');
|
|
72
|
+
}
|
|
73
|
+
const walked = await page.evaluate(collectSnapshot);
|
|
74
|
+
assertSnapshot(walked);
|
|
75
|
+
attachClipFacts(walked.elements);
|
|
76
|
+
return { ...walked, strategy: 'walk', ...(cdpFellBack ? { cdpFellBack: true } : {}) };
|
|
77
|
+
}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.captureViaCDP = captureViaCDP;
|
|
4
|
+
const limits_js_1 = require("./limits.cjs");
|
|
5
|
+
/**
|
|
6
|
+
* Requested in this exact order; `layout.styles[i]` comes back in it. Every
|
|
7
|
+
* name is kebab-case because Blink rejects camelCase and takes the whole call
|
|
8
|
+
* down with it (`whiteSpace` -> "Protocol error: invalid CSS property").
|
|
9
|
+
*/
|
|
10
|
+
// biome-ignore format: the order of this list is the wire format -- one name per
|
|
11
|
+
// line would hide the grouping that makes a mismatch with `S` visible.
|
|
12
|
+
const STYLES = [
|
|
13
|
+
'color', 'background-color', 'font-size', 'font-weight', 'text-align',
|
|
14
|
+
'overflow-x', 'overflow-y', 'position', 'display', 'visibility', 'opacity',
|
|
15
|
+
'flex-direction', 'grid-template-columns', 'row-gap', 'column-gap',
|
|
16
|
+
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
17
|
+
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
18
|
+
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
|
|
19
|
+
'text-overflow', 'white-space', 'overflow-wrap',
|
|
20
|
+
'background-image', 'text-shadow', '-webkit-text-stroke-width', '-webkit-text-stroke-color',
|
|
21
|
+
// Requested so `build()` can read `<html>`'s own values off this same
|
|
22
|
+
// per-node styles array -- see `DocumentBackground` (A2). Not per-element
|
|
23
|
+
// data; every other record's slot for this name is simply unused.
|
|
24
|
+
'color-scheme',
|
|
25
|
+
];
|
|
26
|
+
const S = Object.fromEntries(STYLES.map((name, i) => [name, i]));
|
|
27
|
+
const ELEMENT_NODE = 1;
|
|
28
|
+
const TEXT_NODE = 3;
|
|
29
|
+
/** Roles too generic to be worth carrying; matches the identity ladder's set. */
|
|
30
|
+
const WEAK_AX_ROLES = new Set(['generic', 'none', 'presentation', 'InlineTextBox']);
|
|
31
|
+
function rect(r, dx = 0, dy = 0) {
|
|
32
|
+
return {
|
|
33
|
+
x: (r?.[0] ?? 0) - dx,
|
|
34
|
+
y: (r?.[1] ?? 0) - dy,
|
|
35
|
+
width: r?.[2] ?? 0,
|
|
36
|
+
height: r?.[3] ?? 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function sides(values, strings, keys) {
|
|
40
|
+
const num = (i) => Number.parseFloat(strings[values?.[i] ?? -1] ?? '') || 0;
|
|
41
|
+
return { top: num(keys[0]), right: num(keys[1]), bottom: num(keys[2]), left: num(keys[3]) };
|
|
42
|
+
}
|
|
43
|
+
function trimName(value) {
|
|
44
|
+
const text = (value ?? '').replace(/\s+/g, ' ').trim();
|
|
45
|
+
return text ? text.slice(0, limits_js_1.NAME_LENGTH) : null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Same-origin vs cross-origin for an `<iframe>` found in `documents[0]`, using
|
|
49
|
+
* only that document's own data: resolve `src` against the document's own
|
|
50
|
+
* `baseURL`, exactly as the browser resolves it when it assigns `iframe.src`.
|
|
51
|
+
* This never reads `documents[1..]` -- the slots `captureViaCDP` does not use,
|
|
52
|
+
* per the file header -- classifying an iframe's origin is not the same as
|
|
53
|
+
* reading what is inside it.
|
|
54
|
+
*/
|
|
55
|
+
function iframeOrigin(attrs, baseURL) {
|
|
56
|
+
// `srcdoc` and no `src` both resolve to a document that inherits the
|
|
57
|
+
// parent's origin (`about:srcdoc` / `about:blank`), not the frame's own.
|
|
58
|
+
if (attrs.has('srcdoc'))
|
|
59
|
+
return 'same';
|
|
60
|
+
const src = (attrs.get('src') ?? '').trim();
|
|
61
|
+
if (src === '' || src.toLowerCase() === 'about:blank')
|
|
62
|
+
return 'same';
|
|
63
|
+
try {
|
|
64
|
+
return new URL(src, baseURL).origin === new URL(baseURL).origin ? 'same' : 'cross';
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return 'unknown';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Real open/closed shadow root counts from `nodes.shadowRootType`, or the
|
|
72
|
+
* honest "cannot know" shape when the payload has no such field at all.
|
|
73
|
+
*
|
|
74
|
+
* Every node flattened out of a shadow root is tagged `'open'` or `'closed'`
|
|
75
|
+
* (see `NodeArrays.shadowRootType`'s doc comment), not just the root itself,
|
|
76
|
+
* so counting tagged *nodes* would count a shadow root once per descendant it
|
|
77
|
+
* flattened in. Instead, for each tagged node this walks `parentIndex` up
|
|
78
|
+
* through other tagged nodes until it reaches the first untagged ancestor --
|
|
79
|
+
* that ancestor is the shadow host -- and counts each distinct host once,
|
|
80
|
+
* keyed by the type its tagged descendants carried. A shadow root with no
|
|
81
|
+
* flattened content at all (empty) has no tagged node to find and is not
|
|
82
|
+
* counted; a shadow root nested inside another shadow root is counted once
|
|
83
|
+
* under the outer host rather than twice, because the walk does not stop at
|
|
84
|
+
* the inner boundary -- an known, narrow gap on real pages, which is why this
|
|
85
|
+
* function is a best-effort count, not a claim of perfect precision.
|
|
86
|
+
*/
|
|
87
|
+
function countShadowRoots(nodes, strings) {
|
|
88
|
+
const srt = nodes.shadowRootType;
|
|
89
|
+
// The field is absent, not merely empty, on a payload this build has never
|
|
90
|
+
// seen shaped this way -- report the same "cannot know" shape the walk
|
|
91
|
+
// path uses rather than assuming zero.
|
|
92
|
+
if (!srt)
|
|
93
|
+
return { open: 0, closed: 'unknown' };
|
|
94
|
+
const typeByNode = new Map();
|
|
95
|
+
for (let i = 0; i < srt.index.length; i++) {
|
|
96
|
+
const nodeIdx = srt.index[i];
|
|
97
|
+
const valueIdx = srt.value[i];
|
|
98
|
+
if (nodeIdx === undefined || valueIdx === undefined)
|
|
99
|
+
continue;
|
|
100
|
+
const value = strings[valueIdx];
|
|
101
|
+
// `'user-agent'` (a browser-internal shadow root, e.g. inside `<video>`)
|
|
102
|
+
// is a real `ShadowRootType` this field can carry but which this package
|
|
103
|
+
// has no author-facing fact to report -- excluded from both counts rather
|
|
104
|
+
// than folded into either.
|
|
105
|
+
typeByNode.set(nodeIdx, value === 'open' ? 'open' : value === 'closed' ? 'closed' : null);
|
|
106
|
+
}
|
|
107
|
+
const hostType = new Map();
|
|
108
|
+
for (const [nodeIdx, type] of typeByNode) {
|
|
109
|
+
if (!type)
|
|
110
|
+
continue;
|
|
111
|
+
let cursor = nodes.parentIndex[nodeIdx];
|
|
112
|
+
while (cursor !== undefined && cursor >= 0 && typeByNode.has(cursor)) {
|
|
113
|
+
cursor = nodes.parentIndex[cursor];
|
|
114
|
+
}
|
|
115
|
+
if (cursor === undefined || cursor < 0)
|
|
116
|
+
continue;
|
|
117
|
+
hostType.set(cursor, type);
|
|
118
|
+
}
|
|
119
|
+
let open = 0;
|
|
120
|
+
let closed = 0;
|
|
121
|
+
for (const type of hostType.values()) {
|
|
122
|
+
if (type === 'open')
|
|
123
|
+
open++;
|
|
124
|
+
else
|
|
125
|
+
closed++;
|
|
126
|
+
}
|
|
127
|
+
return { open, closed };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Computed role and accessible name, which `captureSnapshot` does not return —
|
|
131
|
+
* it carries only the literal `role=` attribute, so a `<button>` comes back
|
|
132
|
+
* with no role at all. This is a second experimental call joined on
|
|
133
|
+
* `backendDOMNodeId`.
|
|
134
|
+
*
|
|
135
|
+
* Failure is non-fatal: without it the records simply carry the attribute role,
|
|
136
|
+
* which is the state the prototype shipped in.
|
|
137
|
+
*/
|
|
138
|
+
async function axIndex(cdp) {
|
|
139
|
+
const out = new Map();
|
|
140
|
+
let tree;
|
|
141
|
+
try {
|
|
142
|
+
tree = (await cdp.send('Accessibility.getFullAXTree'));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
for (const node of tree.nodes ?? []) {
|
|
148
|
+
if (node.backendDOMNodeId === undefined || node.ignored)
|
|
149
|
+
continue;
|
|
150
|
+
const role = typeof node.role?.value === 'string' ? node.role.value : null;
|
|
151
|
+
const name = typeof node.name?.value === 'string' ? node.name.value : null;
|
|
152
|
+
out.set(node.backendDOMNodeId, {
|
|
153
|
+
role: role && !WEAK_AX_ROLES.has(role) ? role : null,
|
|
154
|
+
name: trimName(name),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Capture via CDP, or `null` if this page or this Chromium cannot.
|
|
161
|
+
*
|
|
162
|
+
* Returning null rather than throwing is deliberate: the caller's job is to
|
|
163
|
+
* fall back to the portable walk, and an experimental domain failing is an
|
|
164
|
+
* expected outcome, not an error.
|
|
165
|
+
*/
|
|
166
|
+
async function captureViaCDP(page) {
|
|
167
|
+
let cdp;
|
|
168
|
+
try {
|
|
169
|
+
cdp = await page.context().newCDPSession(page);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const snapshot = (await cdp.send('DOMSnapshot.captureSnapshot', {
|
|
176
|
+
computedStyles: [...STYLES],
|
|
177
|
+
includeDOMRects: true,
|
|
178
|
+
includePaintOrder: true,
|
|
179
|
+
}));
|
|
180
|
+
const viewport = await page.evaluate(() => ({
|
|
181
|
+
width: window.innerWidth,
|
|
182
|
+
height: window.innerHeight,
|
|
183
|
+
}));
|
|
184
|
+
const ax = await axIndex(cdp);
|
|
185
|
+
const doc = snapshot.documents[0];
|
|
186
|
+
if (!doc)
|
|
187
|
+
return null;
|
|
188
|
+
return { ...build(doc, snapshot.strings, ax), viewport };
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
await cdp.detach().catch(() => { });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function build(doc, strings, ax) {
|
|
198
|
+
const { nodes, layout } = doc;
|
|
199
|
+
const str = (i) => i === undefined || i < 0 ? '' : (strings[i] ?? '');
|
|
200
|
+
// layout rows are sparse: only nodes that generated a box have one.
|
|
201
|
+
const layoutOf = new Map();
|
|
202
|
+
for (let layoutIdx = 0; layoutIdx < layout.nodeIndex.length; layoutIdx++) {
|
|
203
|
+
const nodeIdx = layout.nodeIndex[layoutIdx];
|
|
204
|
+
if (nodeIdx !== undefined)
|
|
205
|
+
layoutOf.set(nodeIdx, layoutIdx);
|
|
206
|
+
}
|
|
207
|
+
const attrsOf = (nodeIdx) => {
|
|
208
|
+
const flat = nodes.attributes[nodeIdx] ?? [];
|
|
209
|
+
const map = new Map();
|
|
210
|
+
for (let i = 0; i + 1 < flat.length; i += 2) {
|
|
211
|
+
map.set(str(flat[i]), str(flat[i + 1]));
|
|
212
|
+
}
|
|
213
|
+
return map;
|
|
214
|
+
};
|
|
215
|
+
// Own text: direct text-node children only, matching the walk's `ownText`.
|
|
216
|
+
const ownText = new Map();
|
|
217
|
+
nodes.nodeType.forEach((type, nodeIdx) => {
|
|
218
|
+
if (type !== TEXT_NODE)
|
|
219
|
+
return;
|
|
220
|
+
const parent = nodes.parentIndex[nodeIdx];
|
|
221
|
+
if (parent === undefined || parent < 0)
|
|
222
|
+
return;
|
|
223
|
+
ownText.set(parent, (ownText.get(parent) ?? '') + str(nodes.nodeValue[nodeIdx]));
|
|
224
|
+
});
|
|
225
|
+
// `<html>`'s own `background-color` / `color-scheme` -- the real terminal
|
|
226
|
+
// layer a compositing walk that runs off the top of `<body>`'s ancestor
|
|
227
|
+
// chain reaches (A2). Read the same way any element's own styles are, off
|
|
228
|
+
// this same per-node `layout.styles` array -- `<html>` itself is never one
|
|
229
|
+
// of `elements` below, this walk stays inside the <body> subtree exactly
|
|
230
|
+
// like the portable walk does, so without this it would be unreachable.
|
|
231
|
+
//
|
|
232
|
+
// The `nodeType === ELEMENT_NODE` guard is required, not defensive
|
|
233
|
+
// decoration: `<!doctype html>` is its own node in this same
|
|
234
|
+
// `nodes.nodeName` array (a `DocumentType` node, name `"html"`), and it
|
|
235
|
+
// sorts *before* the real `<html>` element -- an unguarded search finds
|
|
236
|
+
// the doctype first, which has no layout row at all, and every style
|
|
237
|
+
// this function reads off it comes back the empty string. *Verified*:
|
|
238
|
+
// this is exactly what happened on the first pass of this fix, caught by
|
|
239
|
+
// `test/browser/color-canvas.test.ts`'s `cdp`-path case.
|
|
240
|
+
const htmlIndex = nodes.nodeName.findIndex((name, i) => nodes.nodeType[i] === ELEMENT_NODE && str(name).toUpperCase() === 'HTML');
|
|
241
|
+
const htmlLayoutIdx = htmlIndex < 0 ? undefined : layoutOf.get(htmlIndex);
|
|
242
|
+
const htmlStyles = htmlLayoutIdx === undefined ? undefined : layout.styles[htmlLayoutIdx];
|
|
243
|
+
const htmlStyle = (name) => str(htmlStyles?.[S[name]]);
|
|
244
|
+
const documentBackground = {
|
|
245
|
+
backgroundColor: htmlStyle('background-color'),
|
|
246
|
+
colorScheme: htmlStyle('color-scheme'),
|
|
247
|
+
};
|
|
248
|
+
// Only the <body> subtree, matching the walk's entry point.
|
|
249
|
+
const bodyIndex = nodes.nodeName.findIndex((name) => str(name).toUpperCase() === 'BODY');
|
|
250
|
+
if (bodyIndex < 0) {
|
|
251
|
+
return {
|
|
252
|
+
elements: [],
|
|
253
|
+
truncated: false,
|
|
254
|
+
notMeasured: {
|
|
255
|
+
shadowRoots: countShadowRoots(nodes, strings),
|
|
256
|
+
iframes: { sameOrigin: 0, crossOrigin: 0, unknownOrigin: 0 },
|
|
257
|
+
},
|
|
258
|
+
documentBackground,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const SKIPPED = new Set(limits_js_1.SKIPPED_TAGS);
|
|
262
|
+
const idOf = new Map();
|
|
263
|
+
const elements = [];
|
|
264
|
+
let truncated = false;
|
|
265
|
+
const baseURL = str(doc.baseURL);
|
|
266
|
+
let iframesSameOrigin = 0;
|
|
267
|
+
let iframesCrossOrigin = 0;
|
|
268
|
+
let iframesUnknownOrigin = 0;
|
|
269
|
+
const inBody = (nodeIdx) => {
|
|
270
|
+
let cursor = nodeIdx;
|
|
271
|
+
while (cursor !== undefined && cursor >= 0) {
|
|
272
|
+
if (cursor === bodyIndex)
|
|
273
|
+
return true;
|
|
274
|
+
cursor = nodes.parentIndex[cursor];
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
};
|
|
278
|
+
// Document order is array order, so one forward pass keys parents before
|
|
279
|
+
// children — the same invariant the recursive walk gets for free.
|
|
280
|
+
for (let nodeIdx = 0; nodeIdx < nodes.nodeType.length; nodeIdx++) {
|
|
281
|
+
// Same ceiling as the walk, so `truncated` means one thing across paths.
|
|
282
|
+
if (elements.length >= limits_js_1.MAX_ELEMENTS) {
|
|
283
|
+
truncated = true;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
if (nodes.nodeType[nodeIdx] !== ELEMENT_NODE)
|
|
287
|
+
continue;
|
|
288
|
+
if (!inBody(nodeIdx))
|
|
289
|
+
continue;
|
|
290
|
+
const tagName = str(nodes.nodeName[nodeIdx]).toUpperCase();
|
|
291
|
+
if (SKIPPED.has(tagName))
|
|
292
|
+
continue;
|
|
293
|
+
// `documents[0]` is the only slot this path reads (see the file header),
|
|
294
|
+
// so an iframe's own content document is never entered -- counted here
|
|
295
|
+
// regardless of whether the iframe itself survives the ancestor check
|
|
296
|
+
// below, because it was encountered either way.
|
|
297
|
+
if (tagName === 'IFRAME') {
|
|
298
|
+
switch (iframeOrigin(attrsOf(nodeIdx), baseURL)) {
|
|
299
|
+
case 'same':
|
|
300
|
+
iframesSameOrigin++;
|
|
301
|
+
break;
|
|
302
|
+
case 'cross':
|
|
303
|
+
iframesCrossOrigin++;
|
|
304
|
+
break;
|
|
305
|
+
case 'unknown':
|
|
306
|
+
iframesUnknownOrigin++;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// An ancestor that was skipped takes its subtree with it.
|
|
311
|
+
let parentIdx = nodes.parentIndex[nodeIdx];
|
|
312
|
+
while (parentIdx !== undefined && parentIdx >= 0 && !idOf.has(parentIdx)) {
|
|
313
|
+
if (parentIdx === bodyIndex)
|
|
314
|
+
break;
|
|
315
|
+
parentIdx = nodes.parentIndex[parentIdx];
|
|
316
|
+
}
|
|
317
|
+
const parentId = parentIdx !== undefined && parentIdx >= 0 ? (idOf.get(parentIdx) ?? null) : null;
|
|
318
|
+
if (nodeIdx !== bodyIndex && parentId === null)
|
|
319
|
+
continue;
|
|
320
|
+
const id = `e${elements.length}`;
|
|
321
|
+
idOf.set(nodeIdx, id);
|
|
322
|
+
const attrs = attrsOf(nodeIdx);
|
|
323
|
+
const layoutIdx = layoutOf.get(nodeIdx);
|
|
324
|
+
const styles = layoutIdx === undefined ? undefined : layout.styles[layoutIdx];
|
|
325
|
+
const box = rect(layoutIdx === undefined ? undefined : layout.bounds[layoutIdx], doc.scrollOffsetX, doc.scrollOffsetY);
|
|
326
|
+
const scrollRect = layoutIdx === undefined ? undefined : layout.scrollRects?.[layoutIdx];
|
|
327
|
+
const clientRect = layoutIdx === undefined ? undefined : layout.clientRects?.[layoutIdx];
|
|
328
|
+
const style = (name) => str(styles?.[S[name]]);
|
|
329
|
+
// A node with no layout row generated no box: that absence is the signal.
|
|
330
|
+
const display = layoutIdx === undefined ? 'none' : style('display') || 'block';
|
|
331
|
+
const visibility = style('visibility') || 'visible';
|
|
332
|
+
const opacity = Number.parseFloat(style('opacity'));
|
|
333
|
+
let testId = null;
|
|
334
|
+
for (const attr of limits_js_1.TESTID_ATTRS) {
|
|
335
|
+
const value = attrs.get(attr);
|
|
336
|
+
if (value) {
|
|
337
|
+
testId = value;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const backendId = nodes.backendNodeId[nodeIdx];
|
|
342
|
+
const computed = backendId === undefined ? undefined : ax.get(backendId);
|
|
343
|
+
const attrRole = (attrs.get('role') ?? '').trim().split(/\s+/)[0] || null;
|
|
344
|
+
const ariaLevel = Number(attrs.get('aria-level'));
|
|
345
|
+
const headingMatch = /^H([1-6])$/.exec(tagName);
|
|
346
|
+
elements.push({
|
|
347
|
+
id,
|
|
348
|
+
tag: tagName.toLowerCase(),
|
|
349
|
+
testId,
|
|
350
|
+
domId: attrs.get('id') ?? null,
|
|
351
|
+
role: computed?.role ?? attrRole,
|
|
352
|
+
name: computed?.name ?? null,
|
|
353
|
+
className: attrs.get('class') ?? '',
|
|
354
|
+
headingLevel: Number.isFinite(ariaLevel) && attrs.has('aria-level')
|
|
355
|
+
? ariaLevel
|
|
356
|
+
: headingMatch?.[1]
|
|
357
|
+
? Number(headingMatch[1])
|
|
358
|
+
: null,
|
|
359
|
+
rect: box,
|
|
360
|
+
scrollWidth: scrollRect?.[2] ?? box.width,
|
|
361
|
+
scrollHeight: scrollRect?.[3] ?? box.height,
|
|
362
|
+
clientWidth: clientRect?.[2] ?? box.width,
|
|
363
|
+
clientHeight: clientRect?.[3] ?? box.height,
|
|
364
|
+
overflowX: style('overflow-x') || 'visible',
|
|
365
|
+
overflowY: style('overflow-y') || 'visible',
|
|
366
|
+
position: style('position') || 'static',
|
|
367
|
+
display,
|
|
368
|
+
visibility,
|
|
369
|
+
opacity: Number.isFinite(opacity) ? opacity : 1,
|
|
370
|
+
margin: sides(styles, strings, [
|
|
371
|
+
S['margin-top'],
|
|
372
|
+
S['margin-right'],
|
|
373
|
+
S['margin-bottom'],
|
|
374
|
+
S['margin-left'],
|
|
375
|
+
]),
|
|
376
|
+
padding: sides(styles, strings, [
|
|
377
|
+
S['padding-top'],
|
|
378
|
+
S['padding-right'],
|
|
379
|
+
S['padding-bottom'],
|
|
380
|
+
S['padding-left'],
|
|
381
|
+
]),
|
|
382
|
+
gap: {
|
|
383
|
+
row: Number.parseFloat(style('row-gap')) || 0,
|
|
384
|
+
column: Number.parseFloat(style('column-gap')) || 0,
|
|
385
|
+
},
|
|
386
|
+
border: sides(styles, strings, [
|
|
387
|
+
S['border-top-width'],
|
|
388
|
+
S['border-right-width'],
|
|
389
|
+
S['border-bottom-width'],
|
|
390
|
+
S['border-left-width'],
|
|
391
|
+
]),
|
|
392
|
+
color: style('color'),
|
|
393
|
+
backgroundColor: style('background-color'),
|
|
394
|
+
backgroundImage: (0, limits_js_1.capBackgroundImage)(style('background-image')),
|
|
395
|
+
textShadow: style('text-shadow').slice(0, limits_js_1.COLOR_TOKEN_CAP),
|
|
396
|
+
textStrokeWidth: style('-webkit-text-stroke-width') || '0px',
|
|
397
|
+
textStrokeColor: style('-webkit-text-stroke-color'),
|
|
398
|
+
fontSize: style('font-size'),
|
|
399
|
+
fontWeight: Number.parseFloat(style('font-weight')) || 400,
|
|
400
|
+
textAlign: style('text-align'),
|
|
401
|
+
flexDirection: style('flex-direction'),
|
|
402
|
+
gridTemplateColumns: style('grid-template-columns'),
|
|
403
|
+
textOverflow: style('text-overflow') || 'clip',
|
|
404
|
+
whiteSpace: style('white-space') || 'normal',
|
|
405
|
+
overflowWrap: style('overflow-wrap') || 'normal',
|
|
406
|
+
text: (ownText.get(nodeIdx) ?? '').replace(/\s+/g, ' ').trim().slice(0, limits_js_1.TEXT_PREVIEW_LENGTH),
|
|
407
|
+
parentId,
|
|
408
|
+
childIds: [],
|
|
409
|
+
visible: display !== 'none' &&
|
|
410
|
+
visibility !== 'hidden' &&
|
|
411
|
+
!(Number.isFinite(opacity) && opacity === 0) &&
|
|
412
|
+
box.width > 0 &&
|
|
413
|
+
box.height > 0,
|
|
414
|
+
ariaExpanded: attrs.get('aria-expanded') ?? null,
|
|
415
|
+
// `cdp` never calls `elementsFromPoint` -- it has `paintOrder` plus
|
|
416
|
+
// rects instead, which is the whole-rect answer to the same question
|
|
417
|
+
// `hitStackIds` answers on the `walk` path. See both fields' doc
|
|
418
|
+
// comments on `ElementRecord`.
|
|
419
|
+
hitStackIds: null,
|
|
420
|
+
paintOrder: layoutIdx === undefined ? null : (layout.paintOrders?.[layoutIdx] ?? null),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
const byId = new Map(elements.map((el) => [el.id, el]));
|
|
424
|
+
for (const el of elements) {
|
|
425
|
+
if (el.parentId)
|
|
426
|
+
byId.get(el.parentId)?.childIds.push(el.id);
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
elements,
|
|
430
|
+
truncated,
|
|
431
|
+
notMeasured: {
|
|
432
|
+
// Counted from `nodes.shadowRootType`, not assumed: `DOMSnapshot`
|
|
433
|
+
// flattens an author shadow root's content directly under its host
|
|
434
|
+
// (both open and closed -- verified in `test/browser/not-measured.test.ts`)
|
|
435
|
+
// and tags every flattened node, so `countShadowRoots` recovers the real
|
|
436
|
+
// per-host counts from those tags instead of asserting a permanent zero.
|
|
437
|
+
// A page whose shadow content this specific capture failed to flatten
|
|
438
|
+
// for any reason now reports that failure as a nonzero count rather
|
|
439
|
+
// than silence -- see `countShadowRoots`'s doc comment for the one
|
|
440
|
+
// known narrow gap (nested shadow roots) and the absent-field fallback.
|
|
441
|
+
shadowRoots: countShadowRoots(nodes, strings),
|
|
442
|
+
iframes: {
|
|
443
|
+
sameOrigin: iframesSameOrigin,
|
|
444
|
+
crossOrigin: iframesCrossOrigin,
|
|
445
|
+
unknownOrigin: iframesUnknownOrigin,
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
documentBackground,
|
|
449
|
+
};
|
|
450
|
+
}
|