browser-debugger-cli 0.10.0 → 0.12.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/.claude/skills/bdg/SKILL.md +268 -0
- package/README.md +148 -74
- package/dist/commands/css.d.ts +13 -0
- package/dist/commands/css.js +53 -0
- package/dist/commands/dom/audit.d.ts +14 -0
- package/dist/commands/dom/audit.js +87 -0
- package/dist/commands/dom/formInteraction.js +36 -6
- package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
- package/dist/commands/dom/helpers/keyAttributes.js +6 -4
- package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
- package/dist/commands/dom/helpers/screenshot.js +158 -38
- package/dist/commands/dom/index.js +4 -1
- package/dist/commands/dom/screenshot.js +10 -6
- package/dist/commands/dom/wait.js +5 -3
- package/dist/commands/helpJson.js +1 -1
- package/dist/commands/installSkill.d.ts +20 -0
- package/dist/commands/installSkill.js +87 -0
- package/dist/commands/optionBehaviors.js +21 -6
- package/dist/commands/page.js +7 -4
- package/dist/commands/peek.d.ts +7 -0
- package/dist/commands/peek.js +65 -23
- package/dist/commands/shared/optionTypes.d.ts +5 -1
- package/dist/commands/start.d.ts +13 -0
- package/dist/commands/start.js +19 -2
- package/dist/commands/tail.d.ts +7 -1
- package/dist/commands/tail.js +13 -62
- package/dist/commands.js +5 -0
- package/dist/daemon/session/commandRegistry.js +7 -1
- package/dist/daemon/session/plugins.js +4 -52
- package/dist/daemon.js +7986 -6848
- package/dist/errors/messages.d.ts +50 -4
- package/dist/errors/messages.js +94 -5
- package/dist/index.js +709 -190
- package/dist/ipc/client.d.ts +4 -0
- package/dist/ipc/client.js +8 -0
- package/dist/ipc/protocol/auditTypes.d.ts +129 -0
- package/dist/ipc/protocol/auditTypes.js +6 -0
- package/dist/ipc/protocol/commands.d.ts +23 -0
- package/dist/ipc/protocol/commands.js +2 -0
- package/dist/ipc/protocol/domTypes.d.ts +4 -0
- package/dist/ipc/protocol/inspectTypes.d.ts +71 -8
- package/dist/runtime/css/search.d.ts +39 -0
- package/dist/runtime/css/search.js +122 -0
- package/dist/runtime/dom/actionEffects.d.ts +4 -1
- package/dist/runtime/dom/actionEffects.js +8 -4
- package/dist/runtime/dom/audit.d.ts +19 -0
- package/dist/runtime/dom/audit.js +36 -0
- package/dist/runtime/dom/auditModel.d.ts +45 -0
- package/dist/runtime/dom/auditModel.js +215 -0
- package/dist/runtime/dom/auditScripts.d.ts +107 -0
- package/dist/runtime/dom/auditScripts.js +112 -0
- package/dist/runtime/dom/elementGeometry.d.ts +8 -2
- package/dist/runtime/dom/elementGeometry.js +24 -8
- package/dist/runtime/dom/elementInfo.d.ts +3 -2
- package/dist/runtime/dom/elementInfo.js +8 -2
- package/dist/runtime/dom/formFillHelpers/fill.js +2 -2
- package/dist/runtime/dom/inspect.d.ts +7 -0
- package/dist/runtime/dom/inspect.js +88 -23
- package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
- package/dist/runtime/dom/inspectAllStyles.js +89 -7
- package/dist/runtime/dom/inspectCascade.d.ts +19 -2
- package/dist/runtime/dom/inspectCascade.js +214 -44
- package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
- package/dist/runtime/dom/inspectCascadeModel.js +108 -34
- package/dist/runtime/dom/inspectHints.d.ts +26 -3
- package/dist/runtime/dom/inspectHints.js +125 -9
- package/dist/runtime/dom/inspectModel.d.ts +3 -0
- package/dist/runtime/dom/inspectModel.js +30 -7
- package/dist/runtime/dom/inspectPaintModel.d.ts +48 -22
- package/dist/runtime/dom/inspectPaintModel.js +180 -68
- package/dist/runtime/dom/inspectRules.d.ts +19 -0
- package/dist/runtime/dom/inspectRules.js +21 -5
- package/dist/runtime/dom/inspectScripts.d.ts +85 -12
- package/dist/runtime/dom/inspectScripts.js +314 -28
- package/dist/runtime/dom/inspectTree.js +10 -2
- package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
- package/dist/runtime/dom/inspectWhyModel.js +52 -10
- package/dist/runtime/dom/layout.js +31 -9
- package/dist/runtime/dom/reactEventHelpers.d.ts +7 -0
- package/dist/runtime/dom/reactEventHelpers.js +27 -9
- package/dist/runtime/page/emulation.d.ts +13 -4
- package/dist/runtime/page/emulation.js +69 -4
- package/dist/runtime/page/userAgent.d.ts +17 -0
- package/dist/runtime/page/userAgent.js +57 -0
- package/dist/types.d.ts +12 -0
- package/dist/ui/formatters/audit.d.ts +19 -0
- package/dist/ui/formatters/audit.js +106 -0
- package/dist/ui/formatters/dom.d.ts +1 -1
- package/dist/ui/formatters/dom.js +6 -3
- package/dist/ui/formatters/inspect.js +42 -15
- package/dist/ui/formatters/installSkill.d.ts +11 -0
- package/dist/ui/formatters/installSkill.js +31 -0
- package/dist/ui/formatters/status.js +1 -1
- package/dist/ui/messages/commands.d.ts +44 -7
- package/dist/ui/messages/commands.js +83 -11
- package/dist/ui/messages/preview.d.ts +6 -0
- package/dist/ui/messages/preview.js +9 -1
- package/dist/utils/cssValues.js +36 -4
- package/dist/utils/decisionTrees.js +0 -5
- package/dist/utils/suggestions.d.ts +4 -2
- package/dist/utils/suggestions.js +7 -5
- package/dist/utils/taskMappings.js +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bdg dom audit`: page-wide checks in one page walk
|
|
3
|
+
* ({@link AUDIT_PAGE_JS}), turned into findings by {@link buildAudit}.
|
|
4
|
+
*/
|
|
5
|
+
import { CommandError } from '../../errors/index.js';
|
|
6
|
+
import { operationFailedError } from '../../errors/messages.js';
|
|
7
|
+
import { buildAudit } from './auditModel.js';
|
|
8
|
+
import { AUDIT_PAGE_JS } from './auditScripts.js';
|
|
9
|
+
import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
10
|
+
/** Findings listed per check without `--limit` */
|
|
11
|
+
export const DEFAULT_AUDIT_LIMIT = 20;
|
|
12
|
+
/**
|
|
13
|
+
* Run the page-wide checks.
|
|
14
|
+
*
|
|
15
|
+
* @param cdp - CDP connection
|
|
16
|
+
* @param params - Checks, WCAG level and limit
|
|
17
|
+
* @returns Findings
|
|
18
|
+
* @throws CommandError (91) when the page script fails
|
|
19
|
+
*/
|
|
20
|
+
export async function auditPage(cdp, params) {
|
|
21
|
+
const response = (await cdp.send('Runtime.evaluate', {
|
|
22
|
+
expression: `(${AUDIT_PAGE_JS})(${JSON.stringify(params.checks)})`,
|
|
23
|
+
returnByValue: true,
|
|
24
|
+
}));
|
|
25
|
+
const raw = response.result.value;
|
|
26
|
+
if (response.exceptionDetails || !raw) {
|
|
27
|
+
const err = operationFailedError('audit the page', response.exceptionDetails?.exception?.description ?? 'the page script failed');
|
|
28
|
+
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SCRIPT_ERROR);
|
|
29
|
+
}
|
|
30
|
+
return buildAudit(raw, {
|
|
31
|
+
checks: params.checks,
|
|
32
|
+
level: params.level ?? 'AA',
|
|
33
|
+
limit: params.limit ?? DEFAULT_AUDIT_LIMIT,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bdg dom audit` results from the page-side walk ({@link AUDIT_PAGE_JS}):
|
|
3
|
+
* text below a WCAG contrast level (composited like `dom inspect`), what
|
|
4
|
+
* makes the page scroll sideways, cut-off text, scaled or distorted images,
|
|
5
|
+
* fixed and sticky layers and running animations. Pure: tested without a
|
|
6
|
+
* browser.
|
|
7
|
+
*/
|
|
8
|
+
import type { AuditContrastItem, AuditImage, AuditResult, AuditCheck } from '../../ipc/protocol/auditTypes.js';
|
|
9
|
+
import type { RawAudit, RawImage } from './auditScripts.js';
|
|
10
|
+
/** What `dom audit` was asked for */
|
|
11
|
+
export interface AuditOptions {
|
|
12
|
+
checks: AuditCheck[];
|
|
13
|
+
/** WCAG level text must reach */
|
|
14
|
+
level: 'AA' | 'AAA';
|
|
15
|
+
/** Findings listed per check */
|
|
16
|
+
limit: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The audit result.
|
|
20
|
+
*
|
|
21
|
+
* @param raw - Page-side walk
|
|
22
|
+
* @param options - Checks, level and limit
|
|
23
|
+
* @returns Result
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildAudit(raw: RawAudit, options: AuditOptions): AuditResult;
|
|
26
|
+
/**
|
|
27
|
+
* The ratio WCAG asks of a text at a level: large text (24px, or 18.66px
|
|
28
|
+
* bold) needs less.
|
|
29
|
+
*
|
|
30
|
+
* @param item - Text size and weight
|
|
31
|
+
* @param level - AA or AAA
|
|
32
|
+
* @returns Minimum ratio
|
|
33
|
+
*/
|
|
34
|
+
export declare function requiredRatio(item: Pick<AuditContrastItem, 'size' | 'weight'>, level: 'AA' | 'AAA'): number;
|
|
35
|
+
/**
|
|
36
|
+
* An image drawn with fewer pixels than the screen needs (upscaled, blurry:
|
|
37
|
+
* rendered size × pixel ratio over its own pixels) or with another aspect
|
|
38
|
+
* ratio (distorted, unless `object-fit` keeps the ratio).
|
|
39
|
+
*
|
|
40
|
+
* @param image - Page-side image
|
|
41
|
+
* @param pixelRatio - Device pixel ratio
|
|
42
|
+
* @returns Finding, or undefined when it is sharp and keeps its ratio
|
|
43
|
+
*/
|
|
44
|
+
export declare function imageFinding(image: RawImage, pixelRatio?: number): AuditImage | undefined;
|
|
45
|
+
//# sourceMappingURL=auditModel.d.ts.map
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bdg dom audit` results from the page-side walk ({@link AUDIT_PAGE_JS}):
|
|
3
|
+
* text below a WCAG contrast level (composited like `dom inspect`), what
|
|
4
|
+
* makes the page scroll sideways, cut-off text, scaled or distorted images,
|
|
5
|
+
* fixed and sticky layers and running animations. Pure: tested without a
|
|
6
|
+
* browser.
|
|
7
|
+
*/
|
|
8
|
+
import { textContrast } from './inspectPaintModel.js';
|
|
9
|
+
import { hexColor } from '../../utils/color.js';
|
|
10
|
+
import { pxNumber, round1 } from '../../utils/cssValues.js';
|
|
11
|
+
/** Scale or aspect change (fraction) below which an image counts as drawn at its size */
|
|
12
|
+
const IMAGE_TOLERANCE = 0.05;
|
|
13
|
+
/**
|
|
14
|
+
* The audit result.
|
|
15
|
+
*
|
|
16
|
+
* @param raw - Page-side walk
|
|
17
|
+
* @param options - Checks, level and limit
|
|
18
|
+
* @returns Result
|
|
19
|
+
*/
|
|
20
|
+
export function buildAudit(raw, options) {
|
|
21
|
+
return {
|
|
22
|
+
checks: options.checks,
|
|
23
|
+
walked: raw.walked,
|
|
24
|
+
...(raw.capped && { capped: true }),
|
|
25
|
+
...(raw.texts && { contrast: contrastFindings(raw, options) }),
|
|
26
|
+
...(raw.wide && { overflow: overflowFindings(raw, options.limit) }),
|
|
27
|
+
...(raw.layers && {
|
|
28
|
+
layers: raw.layers.slice(0, options.limit).map((layer) => ({
|
|
29
|
+
element: layer.label,
|
|
30
|
+
position: layer.position,
|
|
31
|
+
zIndex: layer.zIndex,
|
|
32
|
+
rect: {
|
|
33
|
+
x: round1(layer.rect.x),
|
|
34
|
+
y: round1(layer.rect.y),
|
|
35
|
+
w: round1(layer.rect.w),
|
|
36
|
+
h: round1(layer.rect.h),
|
|
37
|
+
},
|
|
38
|
+
inView: layer.inView,
|
|
39
|
+
})),
|
|
40
|
+
}),
|
|
41
|
+
...(raw.animations && {
|
|
42
|
+
animations: grouped(raw.animations.map((animation) => ({
|
|
43
|
+
element: animation.label,
|
|
44
|
+
name: animation.name,
|
|
45
|
+
type: animation.type,
|
|
46
|
+
duration: animation.duration,
|
|
47
|
+
iterations: animation.iterations,
|
|
48
|
+
...(animation.scrollDriven && { scrollDriven: true }),
|
|
49
|
+
}))).slice(0, options.limit),
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Text below the level, weakest first.
|
|
55
|
+
*
|
|
56
|
+
* @param raw - Page-side walk
|
|
57
|
+
* @param options - Level and limit
|
|
58
|
+
* @returns How many were checked and failed, and the weakest
|
|
59
|
+
*/
|
|
60
|
+
function contrastFindings(raw, options) {
|
|
61
|
+
const checked = (raw.texts ?? []).map((text) => contrastItem(text, raw.canvasDark));
|
|
62
|
+
const failing = checked
|
|
63
|
+
.filter((item) => item !== undefined)
|
|
64
|
+
.filter((item) => item.ratio < requiredRatio(item, options.level))
|
|
65
|
+
.sort((a, b) => a.ratio - b.ratio);
|
|
66
|
+
return {
|
|
67
|
+
level: options.level,
|
|
68
|
+
checked: checked.length,
|
|
69
|
+
failing: failing.length,
|
|
70
|
+
items: failing.slice(0, options.limit),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* One text holder's contrast.
|
|
75
|
+
*
|
|
76
|
+
* @param text - Page-side text holder
|
|
77
|
+
* @param canvasDark - The page canvas is dark
|
|
78
|
+
* @returns Finding, or undefined when its color cannot be read
|
|
79
|
+
*/
|
|
80
|
+
function contrastItem(text, canvasDark) {
|
|
81
|
+
const contrast = textContrast({ color: text.color, 'font-size': text.fontSize, 'font-weight': text.fontWeight }, {
|
|
82
|
+
backgrounds: text.backgrounds,
|
|
83
|
+
canvasDark,
|
|
84
|
+
opacity: text.opacity,
|
|
85
|
+
paintRisks: text.risks,
|
|
86
|
+
});
|
|
87
|
+
if (!contrast)
|
|
88
|
+
return undefined;
|
|
89
|
+
return {
|
|
90
|
+
element: text.label,
|
|
91
|
+
text: text.text,
|
|
92
|
+
ratio: contrast.ratio,
|
|
93
|
+
color: hexColor(text.color),
|
|
94
|
+
background: contrast.background,
|
|
95
|
+
size: pxNumber(text.fontSize) ?? 16,
|
|
96
|
+
weight: Number(text.fontWeight) || 400,
|
|
97
|
+
inView: text.inView,
|
|
98
|
+
...(contrast.approximate && { approximate: contrast.approximate }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The ratio WCAG asks of a text at a level: large text (24px, or 18.66px
|
|
103
|
+
* bold) needs less.
|
|
104
|
+
*
|
|
105
|
+
* @param item - Text size and weight
|
|
106
|
+
* @param level - AA or AAA
|
|
107
|
+
* @returns Minimum ratio
|
|
108
|
+
*/
|
|
109
|
+
export function requiredRatio(item, level) {
|
|
110
|
+
const large = item.size >= 24 || (item.size >= 18.66 && item.weight >= 700);
|
|
111
|
+
if (level === 'AAA')
|
|
112
|
+
return large ? 4.5 : 7;
|
|
113
|
+
return large ? 3 : 4.5;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* What makes the page scroll sideways, cut-off text and scaled images.
|
|
117
|
+
*
|
|
118
|
+
* @param raw - Page-side walk
|
|
119
|
+
* @param limit - Findings per list
|
|
120
|
+
* @returns Overflow findings
|
|
121
|
+
*/
|
|
122
|
+
function overflowFindings(raw, limit) {
|
|
123
|
+
const wide = [...(raw.wide ?? [])].sort((a, b) => b.right - a.right);
|
|
124
|
+
const images = grouped((raw.images ?? [])
|
|
125
|
+
.map((image) => imageFinding(image, raw.pixelRatio))
|
|
126
|
+
.filter((image) => image !== undefined));
|
|
127
|
+
return {
|
|
128
|
+
pageWidth: raw.pageWidth,
|
|
129
|
+
viewportWidth: raw.viewport.width,
|
|
130
|
+
scrollsSideways: raw.pageWidth > raw.viewport.width + 1,
|
|
131
|
+
wide: wide.slice(0, limit).map((element) => ({
|
|
132
|
+
element: element.label,
|
|
133
|
+
right: round1(element.right),
|
|
134
|
+
width: round1(element.width),
|
|
135
|
+
})),
|
|
136
|
+
truncated: grouped((raw.truncated ?? []).map((text) => ({
|
|
137
|
+
element: text.label,
|
|
138
|
+
text: text.text,
|
|
139
|
+
kind: text.kind,
|
|
140
|
+
}))).slice(0, limit),
|
|
141
|
+
images: images.slice(0, limit),
|
|
142
|
+
pixelRatio: raw.pixelRatio,
|
|
143
|
+
scrollers: (raw.scrollers ?? []).slice(0, limit).map((scroller) => ({
|
|
144
|
+
element: scroller.label,
|
|
145
|
+
scrollWidth: scroller.scrollWidth,
|
|
146
|
+
width: scroller.width,
|
|
147
|
+
})),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* How much an image's pixels are stretched on screen, by its `object-fit`:
|
|
152
|
+
* `fill` and `cover` stretch to the larger ratio, `contain` to the smaller,
|
|
153
|
+
* `none` not at all, `scale-down` at most to its own size.
|
|
154
|
+
*
|
|
155
|
+
* @param image - Page-side image
|
|
156
|
+
* @param pixelRatio - Device pixel ratio
|
|
157
|
+
* @returns Screen pixels per image pixel
|
|
158
|
+
*/
|
|
159
|
+
function drawnScale(image, pixelRatio) {
|
|
160
|
+
const across = (image.rendered.w * pixelRatio) / image.natural.w;
|
|
161
|
+
const down = (image.rendered.h * pixelRatio) / image.natural.h;
|
|
162
|
+
if (image.objectFit === 'none')
|
|
163
|
+
return pixelRatio;
|
|
164
|
+
if (image.objectFit === 'contain')
|
|
165
|
+
return Math.min(across, down);
|
|
166
|
+
if (image.objectFit === 'scale-down')
|
|
167
|
+
return Math.min(pixelRatio, Math.min(across, down));
|
|
168
|
+
return Math.max(across, down);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Findings with identical ones merged into the first, with a `count`.
|
|
172
|
+
*
|
|
173
|
+
* @param findings - Findings in page order
|
|
174
|
+
* @returns Findings, each distinct one once
|
|
175
|
+
*/
|
|
176
|
+
function grouped(findings) {
|
|
177
|
+
const byKey = new Map();
|
|
178
|
+
for (const finding of findings) {
|
|
179
|
+
const key = JSON.stringify(finding);
|
|
180
|
+
const seen = byKey.get(key);
|
|
181
|
+
if (seen)
|
|
182
|
+
seen.count = (seen.count ?? 1) + 1;
|
|
183
|
+
else
|
|
184
|
+
byKey.set(key, { ...finding });
|
|
185
|
+
}
|
|
186
|
+
return [...byKey.values()];
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* An image drawn with fewer pixels than the screen needs (upscaled, blurry:
|
|
190
|
+
* rendered size × pixel ratio over its own pixels) or with another aspect
|
|
191
|
+
* ratio (distorted, unless `object-fit` keeps the ratio).
|
|
192
|
+
*
|
|
193
|
+
* @param image - Page-side image
|
|
194
|
+
* @param pixelRatio - Device pixel ratio
|
|
195
|
+
* @returns Finding, or undefined when it is sharp and keeps its ratio
|
|
196
|
+
*/
|
|
197
|
+
export function imageFinding(image, pixelRatio = 1) {
|
|
198
|
+
const scale = drawnScale(image, pixelRatio);
|
|
199
|
+
const naturalRatio = image.natural.w / image.natural.h;
|
|
200
|
+
const renderedRatio = image.rendered.w / image.rendered.h;
|
|
201
|
+
const distorted = image.objectFit === 'fill' &&
|
|
202
|
+
Math.abs(renderedRatio - naturalRatio) / naturalRatio > IMAGE_TOLERANCE;
|
|
203
|
+
const upscaled = scale > 1 + IMAGE_TOLERANCE;
|
|
204
|
+
if (!distorted && !upscaled)
|
|
205
|
+
return undefined;
|
|
206
|
+
return {
|
|
207
|
+
element: image.label,
|
|
208
|
+
natural: image.natural,
|
|
209
|
+
rendered: { w: round1(image.rendered.w), h: round1(image.rendered.h) },
|
|
210
|
+
scale: Math.round(scale * 100) / 100,
|
|
211
|
+
...(upscaled && { upscaled: true }),
|
|
212
|
+
...(distorted && { distorted: true }),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=auditModel.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-side part of `bdg dom audit`: one walk over the rendered elements of
|
|
3
|
+
* the page (open shadow roots and the light DOM slotted into them, at most
|
|
4
|
+
* {@link AUDIT_ELEMENT_CAP}; nothing inside a fixed element counts as making
|
|
5
|
+
* the page wider; SVG images and placeholders of 2px or less are skipped)
|
|
6
|
+
* that collects what each check needs. Colors and contrast are computed in
|
|
7
|
+
* the daemon ({@link buildAudit}), the same way `dom inspect` computes them.
|
|
8
|
+
*/
|
|
9
|
+
import type { RawBackground } from './inspectScripts.js';
|
|
10
|
+
/** Elements the walk looks at, at most */
|
|
11
|
+
export declare const AUDIT_ELEMENT_CAP = 20000;
|
|
12
|
+
/** A text holder: an element that draws text of its own */
|
|
13
|
+
export interface RawAuditText {
|
|
14
|
+
label: string;
|
|
15
|
+
text: string;
|
|
16
|
+
color: string;
|
|
17
|
+
fontSize: string;
|
|
18
|
+
fontWeight: string;
|
|
19
|
+
/** Backgrounds from the element up to the root, each with its own opacity */
|
|
20
|
+
backgrounds: RawBackground[];
|
|
21
|
+
opacity: number;
|
|
22
|
+
/** Blend modes and filters on it or an ancestor */
|
|
23
|
+
risks: string[];
|
|
24
|
+
/** Inside the viewport */
|
|
25
|
+
inView: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** An element that reaches past the right edge of the page's viewport */
|
|
28
|
+
export interface RawWideElement {
|
|
29
|
+
label: string;
|
|
30
|
+
right: number;
|
|
31
|
+
width: number;
|
|
32
|
+
}
|
|
33
|
+
/** Text cut off by its box */
|
|
34
|
+
export interface RawTruncated {
|
|
35
|
+
label: string;
|
|
36
|
+
text: string;
|
|
37
|
+
/** `ellipsis`, `clamp` or `clip` */
|
|
38
|
+
kind: string;
|
|
39
|
+
}
|
|
40
|
+
/** An image drawn at another size than its own */
|
|
41
|
+
export interface RawImage {
|
|
42
|
+
label: string;
|
|
43
|
+
natural: {
|
|
44
|
+
w: number;
|
|
45
|
+
h: number;
|
|
46
|
+
};
|
|
47
|
+
rendered: {
|
|
48
|
+
w: number;
|
|
49
|
+
h: number;
|
|
50
|
+
};
|
|
51
|
+
objectFit: string;
|
|
52
|
+
}
|
|
53
|
+
/** A fixed or sticky element */
|
|
54
|
+
export interface RawLayer {
|
|
55
|
+
label: string;
|
|
56
|
+
position: string;
|
|
57
|
+
zIndex: string;
|
|
58
|
+
rect: {
|
|
59
|
+
x: number;
|
|
60
|
+
y: number;
|
|
61
|
+
w: number;
|
|
62
|
+
h: number;
|
|
63
|
+
};
|
|
64
|
+
inView: boolean;
|
|
65
|
+
}
|
|
66
|
+
/** A running animation or transition */
|
|
67
|
+
export interface RawAnimation {
|
|
68
|
+
label: string;
|
|
69
|
+
/** Animation name, transitioned property, or `animation` for a Web Animation */
|
|
70
|
+
name: string;
|
|
71
|
+
type: string;
|
|
72
|
+
duration: number | string;
|
|
73
|
+
iterations: number | string;
|
|
74
|
+
scrollDriven: boolean;
|
|
75
|
+
}
|
|
76
|
+
/** What {@link AUDIT_PAGE_JS} returns */
|
|
77
|
+
export interface RawAudit {
|
|
78
|
+
viewport: {
|
|
79
|
+
width: number;
|
|
80
|
+
height: number;
|
|
81
|
+
};
|
|
82
|
+
/** Device pixel ratio: an image needs that many pixels per CSS px to look sharp */
|
|
83
|
+
pixelRatio: number;
|
|
84
|
+
pageWidth: number;
|
|
85
|
+
canvasDark: boolean;
|
|
86
|
+
/** Elements walked, and whether the walk stopped at the cap */
|
|
87
|
+
walked: number;
|
|
88
|
+
capped: boolean;
|
|
89
|
+
texts?: RawAuditText[];
|
|
90
|
+
wide?: RawWideElement[];
|
|
91
|
+
truncated?: RawTruncated[];
|
|
92
|
+
images?: RawImage[];
|
|
93
|
+
/** Elements whose content scrolls sideways inside them (carousels, tab strips) */
|
|
94
|
+
scrollers?: Array<{
|
|
95
|
+
label: string;
|
|
96
|
+
scrollWidth: number;
|
|
97
|
+
width: number;
|
|
98
|
+
}>;
|
|
99
|
+
layers?: RawLayer[];
|
|
100
|
+
animations?: RawAnimation[];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Page-side audit walk. Arguments: the checks to collect for
|
|
104
|
+
* (`contrast`, `overflow`, `layers`, `animations`).
|
|
105
|
+
*/
|
|
106
|
+
export declare const AUDIT_PAGE_JS = "function (checks) {\n const want = (name) => checks.includes(name);\n const doc = document;\n const view = window;\n const style = (n) => view.getComputedStyle(n);\n const scroller = doc.scrollingElement || doc.documentElement;\n const viewport = { width: doc.documentElement.clientWidth, height: view.innerHeight };\n const label = (n) => n.localName + (n.id ? '#' + n.id : n.classList && n.classList.length ? '.' + n.classList[0] : '');\n const parentOf = (n) => n.assignedSlot || n.parentElement || (n.parentNode && n.parentNode.host) || null;\n const short = (text) => text.replace(/\\s+/g, ' ').trim().slice(0, 60);\n const ownText = (n) => Array.from(n.childNodes).filter((c) => c.nodeType === 3).map((c) => c.data).join(' ').replace(/\\s+/g, ' ').trim();\n const inView = (r) => r.bottom > 0 && r.right > 0 && r.top < viewport.height && r.left < viewport.width;\n const meta = doc.querySelector('meta[name=\"color-scheme\"]');\n const rootScheme = style(doc.documentElement).colorScheme;\n const scheme = rootScheme && rootScheme !== 'normal' ? rootScheme : (meta && meta.content) || '';\n const canvasDark = /dark/.test(scheme) && (!/light/.test(scheme) || view.matchMedia('(prefers-color-scheme: dark)').matches);\n const result = { viewport: viewport, pixelRatio: view.devicePixelRatio || 1, pageWidth: scroller.scrollWidth, canvasDark: canvasDark, walked: 0, capped: false };\n const texts = [], wide = [], truncated = [], images = [], layers = [], scrollers = [];\n const chainOf = (n) => {\n const backgrounds = [];\n const risks = [];\n let opacity = 1;\n for (let p = n; p && backgrounds.length < 60; p = parentOf(p)) {\n const s = style(p);\n const own = Number(s.opacity) || 0;\n const background = { color: s.backgroundColor, image: s.backgroundImage !== 'none' };\n backgrounds.push(own < 1 ? Object.assign(background, { opacity: own }) : background);\n opacity *= own;\n if (s.mixBlendMode !== 'normal') risks.push('mix-blend-mode ' + s.mixBlendMode + ' on ' + label(p));\n if (s.filter !== 'none') risks.push('filter on ' + label(p));\n }\n return { backgrounds: backgrounds, risks: risks, opacity: opacity };\n };\n const visit = (n, fixed) => {\n if (result.walked >= 20000) { result.capped = true; return; }\n if (/^(script|style|template|noscript|head|meta|link|title)$/.test(n.localName)) return;\n const s = style(n);\n if (s.display === 'none') return;\n result.walked++;\n const inFixed = fixed || s.position === 'fixed';\n const kids = n.shadowRoot ? [...Array.from(n.shadowRoot.children), ...Array.from(n.children)] : Array.from(n.children);\n const slot = n.localName === 'slot';\n const box = slot && parentOf(n) ? parentOf(n) : n;\n const r = box.getBoundingClientRect();\n const shown = r.width > 0 && r.height > 0 && s.visibility === 'visible';\n const gradientText = s.backgroundClip === 'text' && /rgba\\(0, 0, 0, 0\\)|transparent/.test(s.webkitTextFillColor);\n if (shown && want('contrast') && !gradientText) {\n const text = slot ? short(n.assignedNodes({ flatten: true }).filter((c) => c.nodeType === 3).map((c) => c.data).join(' ')) : n.shadowRoot ? '' : ownText(n);\n if (text && Number(s.opacity) > 0 && r.width > 2 && r.height > 2) {\n const chain = chainOf(n);\n if (chain.opacity > 0) texts.push(Object.assign({ label: label(n), text: short(text), color: s.color, fontSize: s.fontSize, fontWeight: s.fontWeight, inView: inView(r) }, chain));\n }\n }\n if (shown && want('overflow')) {\n if (r.right > viewport.width + 1 && !inFixed) wide.push({ label: label(n), right: r.right + view.scrollX, width: r.width });\n const cut = s.overflowX !== 'visible' && n.scrollWidth > n.clientWidth + 1 ? (s.textOverflow === 'ellipsis' ? 'ellipsis' : 'clip')\n : s.webkitLineClamp !== 'none' && n.scrollHeight > n.clientHeight + 1 ? 'clamp' : null;\n const textContent = cut && short(n.innerText || '');\n const visuallyHidden = r.width <= 2 || r.height <= 2;\n if (cut && textContent && !visuallyHidden) truncated.push({ label: label(n), text: textContent, kind: cut });\n const vector = /\\.svg([?#]|$)|^data:image\\/svg/i.test(n.currentSrc || '');\n if (n.localName === 'img' && n.naturalWidth > 2 && n.naturalHeight > 2 && n.complete && !vector) images.push({ label: label(n), natural: { w: n.naturalWidth, h: n.naturalHeight }, rendered: { w: r.width, h: r.height }, objectFit: s.objectFit });\n }\n if (shown && want('overflow') && /^(auto|scroll)$/.test(s.overflowX) && n !== doc.documentElement && n !== doc.body && n.scrollWidth > n.clientWidth + 1) {\n scrollers.push({ label: label(n), scrollWidth: n.scrollWidth, width: n.clientWidth });\n }\n if (want('layers') && shown && (s.position === 'fixed' || s.position === 'sticky')) {\n layers.push({ label: label(n), position: s.position, zIndex: s.zIndex, rect: { x: r.left, y: r.top, w: r.width, h: r.height }, inView: inView(r) });\n }\n if (s.overflowX !== 'visible' && s.overflowX !== 'clip' && n !== doc.documentElement && n !== doc.body) {\n const wideBefore = wide.length;\n for (const c of kids) visit(c, inFixed);\n wide.length = wideBefore;\n return;\n }\n for (const c of kids) visit(c, inFixed);\n };\n visit(doc.documentElement, false);\n if (want('contrast')) result.texts = texts;\n if (want('overflow')) Object.assign(result, { wide: wide, truncated: truncated, images: images, scrollers: scrollers });\n if (want('layers')) result.layers = layers;\n if (want('animations')) {\n result.animations = doc.getAnimations().filter((a) => a.playState === 'running').slice(0, 200).map((a) => {\n const timing = a.effect && a.effect.getTiming ? a.effect.getTiming() : {};\n const target = a.effect && a.effect.target;\n return {\n label: target && target.localName ? label(target) : '(none)',\n name: a.animationName || a.transitionProperty || a.id || 'animation',\n type: a.constructor.name,\n duration: typeof timing.duration === 'number' ? timing.duration : String(timing.duration),\n iterations: timing.iterations === Infinity ? 'infinite' : timing.iterations,\n scrollDriven: Boolean(a.timeline) && a.timeline !== doc.timeline\n };\n });\n }\n return result;\n}";
|
|
107
|
+
//# sourceMappingURL=auditScripts.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-side part of `bdg dom audit`: one walk over the rendered elements of
|
|
3
|
+
* the page (open shadow roots and the light DOM slotted into them, at most
|
|
4
|
+
* {@link AUDIT_ELEMENT_CAP}; nothing inside a fixed element counts as making
|
|
5
|
+
* the page wider; SVG images and placeholders of 2px or less are skipped)
|
|
6
|
+
* that collects what each check needs. Colors and contrast are computed in
|
|
7
|
+
* the daemon ({@link buildAudit}), the same way `dom inspect` computes them.
|
|
8
|
+
*/
|
|
9
|
+
/** Elements the walk looks at, at most */
|
|
10
|
+
export const AUDIT_ELEMENT_CAP = 20000;
|
|
11
|
+
/**
|
|
12
|
+
* Page-side audit walk. Arguments: the checks to collect for
|
|
13
|
+
* (`contrast`, `overflow`, `layers`, `animations`).
|
|
14
|
+
*/
|
|
15
|
+
export const AUDIT_PAGE_JS = `function (checks) {
|
|
16
|
+
const want = (name) => checks.includes(name);
|
|
17
|
+
const doc = document;
|
|
18
|
+
const view = window;
|
|
19
|
+
const style = (n) => view.getComputedStyle(n);
|
|
20
|
+
const scroller = doc.scrollingElement || doc.documentElement;
|
|
21
|
+
const viewport = { width: doc.documentElement.clientWidth, height: view.innerHeight };
|
|
22
|
+
const label = (n) => n.localName + (n.id ? '#' + n.id : n.classList && n.classList.length ? '.' + n.classList[0] : '');
|
|
23
|
+
const parentOf = (n) => n.assignedSlot || n.parentElement || (n.parentNode && n.parentNode.host) || null;
|
|
24
|
+
const short = (text) => text.replace(/\\s+/g, ' ').trim().slice(0, 60);
|
|
25
|
+
const ownText = (n) => Array.from(n.childNodes).filter((c) => c.nodeType === 3).map((c) => c.data).join(' ').replace(/\\s+/g, ' ').trim();
|
|
26
|
+
const inView = (r) => r.bottom > 0 && r.right > 0 && r.top < viewport.height && r.left < viewport.width;
|
|
27
|
+
const meta = doc.querySelector('meta[name="color-scheme"]');
|
|
28
|
+
const rootScheme = style(doc.documentElement).colorScheme;
|
|
29
|
+
const scheme = rootScheme && rootScheme !== 'normal' ? rootScheme : (meta && meta.content) || '';
|
|
30
|
+
const canvasDark = /dark/.test(scheme) && (!/light/.test(scheme) || view.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
31
|
+
const result = { viewport: viewport, pixelRatio: view.devicePixelRatio || 1, pageWidth: scroller.scrollWidth, canvasDark: canvasDark, walked: 0, capped: false };
|
|
32
|
+
const texts = [], wide = [], truncated = [], images = [], layers = [], scrollers = [];
|
|
33
|
+
const chainOf = (n) => {
|
|
34
|
+
const backgrounds = [];
|
|
35
|
+
const risks = [];
|
|
36
|
+
let opacity = 1;
|
|
37
|
+
for (let p = n; p && backgrounds.length < 60; p = parentOf(p)) {
|
|
38
|
+
const s = style(p);
|
|
39
|
+
const own = Number(s.opacity) || 0;
|
|
40
|
+
const background = { color: s.backgroundColor, image: s.backgroundImage !== 'none' };
|
|
41
|
+
backgrounds.push(own < 1 ? Object.assign(background, { opacity: own }) : background);
|
|
42
|
+
opacity *= own;
|
|
43
|
+
if (s.mixBlendMode !== 'normal') risks.push('mix-blend-mode ' + s.mixBlendMode + ' on ' + label(p));
|
|
44
|
+
if (s.filter !== 'none') risks.push('filter on ' + label(p));
|
|
45
|
+
}
|
|
46
|
+
return { backgrounds: backgrounds, risks: risks, opacity: opacity };
|
|
47
|
+
};
|
|
48
|
+
const visit = (n, fixed) => {
|
|
49
|
+
if (result.walked >= ${AUDIT_ELEMENT_CAP}) { result.capped = true; return; }
|
|
50
|
+
if (/^(script|style|template|noscript|head|meta|link|title)$/.test(n.localName)) return;
|
|
51
|
+
const s = style(n);
|
|
52
|
+
if (s.display === 'none') return;
|
|
53
|
+
result.walked++;
|
|
54
|
+
const inFixed = fixed || s.position === 'fixed';
|
|
55
|
+
const kids = n.shadowRoot ? [...Array.from(n.shadowRoot.children), ...Array.from(n.children)] : Array.from(n.children);
|
|
56
|
+
const slot = n.localName === 'slot';
|
|
57
|
+
const box = slot && parentOf(n) ? parentOf(n) : n;
|
|
58
|
+
const r = box.getBoundingClientRect();
|
|
59
|
+
const shown = r.width > 0 && r.height > 0 && s.visibility === 'visible';
|
|
60
|
+
const gradientText = s.backgroundClip === 'text' && /rgba\\(0, 0, 0, 0\\)|transparent/.test(s.webkitTextFillColor);
|
|
61
|
+
if (shown && want('contrast') && !gradientText) {
|
|
62
|
+
const text = slot ? short(n.assignedNodes({ flatten: true }).filter((c) => c.nodeType === 3).map((c) => c.data).join(' ')) : n.shadowRoot ? '' : ownText(n);
|
|
63
|
+
if (text && Number(s.opacity) > 0 && r.width > 2 && r.height > 2) {
|
|
64
|
+
const chain = chainOf(n);
|
|
65
|
+
if (chain.opacity > 0) texts.push(Object.assign({ label: label(n), text: short(text), color: s.color, fontSize: s.fontSize, fontWeight: s.fontWeight, inView: inView(r) }, chain));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (shown && want('overflow')) {
|
|
69
|
+
if (r.right > viewport.width + 1 && !inFixed) wide.push({ label: label(n), right: r.right + view.scrollX, width: r.width });
|
|
70
|
+
const cut = s.overflowX !== 'visible' && n.scrollWidth > n.clientWidth + 1 ? (s.textOverflow === 'ellipsis' ? 'ellipsis' : 'clip')
|
|
71
|
+
: s.webkitLineClamp !== 'none' && n.scrollHeight > n.clientHeight + 1 ? 'clamp' : null;
|
|
72
|
+
const textContent = cut && short(n.innerText || '');
|
|
73
|
+
const visuallyHidden = r.width <= 2 || r.height <= 2;
|
|
74
|
+
if (cut && textContent && !visuallyHidden) truncated.push({ label: label(n), text: textContent, kind: cut });
|
|
75
|
+
const vector = /\\.svg([?#]|$)|^data:image\\/svg/i.test(n.currentSrc || '');
|
|
76
|
+
if (n.localName === 'img' && n.naturalWidth > 2 && n.naturalHeight > 2 && n.complete && !vector) images.push({ label: label(n), natural: { w: n.naturalWidth, h: n.naturalHeight }, rendered: { w: r.width, h: r.height }, objectFit: s.objectFit });
|
|
77
|
+
}
|
|
78
|
+
if (shown && want('overflow') && /^(auto|scroll)$/.test(s.overflowX) && n !== doc.documentElement && n !== doc.body && n.scrollWidth > n.clientWidth + 1) {
|
|
79
|
+
scrollers.push({ label: label(n), scrollWidth: n.scrollWidth, width: n.clientWidth });
|
|
80
|
+
}
|
|
81
|
+
if (want('layers') && shown && (s.position === 'fixed' || s.position === 'sticky')) {
|
|
82
|
+
layers.push({ label: label(n), position: s.position, zIndex: s.zIndex, rect: { x: r.left, y: r.top, w: r.width, h: r.height }, inView: inView(r) });
|
|
83
|
+
}
|
|
84
|
+
if (s.overflowX !== 'visible' && s.overflowX !== 'clip' && n !== doc.documentElement && n !== doc.body) {
|
|
85
|
+
const wideBefore = wide.length;
|
|
86
|
+
for (const c of kids) visit(c, inFixed);
|
|
87
|
+
wide.length = wideBefore;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const c of kids) visit(c, inFixed);
|
|
91
|
+
};
|
|
92
|
+
visit(doc.documentElement, false);
|
|
93
|
+
if (want('contrast')) result.texts = texts;
|
|
94
|
+
if (want('overflow')) Object.assign(result, { wide: wide, truncated: truncated, images: images, scrollers: scrollers });
|
|
95
|
+
if (want('layers')) result.layers = layers;
|
|
96
|
+
if (want('animations')) {
|
|
97
|
+
result.animations = doc.getAnimations().filter((a) => a.playState === 'running').slice(0, 200).map((a) => {
|
|
98
|
+
const timing = a.effect && a.effect.getTiming ? a.effect.getTiming() : {};
|
|
99
|
+
const target = a.effect && a.effect.target;
|
|
100
|
+
return {
|
|
101
|
+
label: target && target.localName ? label(target) : '(none)',
|
|
102
|
+
name: a.animationName || a.transitionProperty || a.id || 'animation',
|
|
103
|
+
type: a.constructor.name,
|
|
104
|
+
duration: typeof timing.duration === 'number' ? timing.duration : String(timing.duration),
|
|
105
|
+
iterations: timing.iterations === Infinity ? 'infinite' : timing.iterations,
|
|
106
|
+
scrollDriven: Boolean(a.timeline) && a.timeline !== doc.timeline
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}`;
|
|
112
|
+
//# sourceMappingURL=auditScripts.js.map
|
|
@@ -14,6 +14,12 @@ import type { ViewportPosition } from '../../types.js';
|
|
|
14
14
|
export interface ElementGeometry {
|
|
15
15
|
/** Border box in top-level viewport coordinates */
|
|
16
16
|
rect: LayoutBox;
|
|
17
|
+
/**
|
|
18
|
+
* For a zero-size element whose children show (an inline list around
|
|
19
|
+
* floated items): the box around those children, which is what can be
|
|
20
|
+
* seen of it
|
|
21
|
+
*/
|
|
22
|
+
content?: LayoutBox | null;
|
|
17
23
|
/** Area its iframes and overflow-clipping ancestors leave visible, in the same coordinates */
|
|
18
24
|
clip: LayoutBox | null;
|
|
19
25
|
/** Whether overlay scrollbars of the clipping containers show along the clip's right and bottom edges */
|
|
@@ -161,10 +167,10 @@ export declare function scrollTarget(start: number, size: number, viewSize: numb
|
|
|
161
167
|
* out of a list), and `hidden` when it is not rendered at all. Being inert
|
|
162
168
|
* does not change the position: inert elements are shown.
|
|
163
169
|
*
|
|
164
|
-
* @param
|
|
170
|
+
* @param measured - Page-side measurements (a zero-size holder is placed by its children)
|
|
165
171
|
* @param viewport - Top-level viewport size
|
|
166
172
|
* @returns Position, with the visible share, hidden reason, clipping
|
|
167
173
|
* ancestor or page scroll when relevant
|
|
168
174
|
*/
|
|
169
|
-
export declare function classifyViewportPosition(
|
|
175
|
+
export declare function classifyViewportPosition(measured: ElementGeometry, viewport: LayoutSize): ViewportPlacement;
|
|
170
176
|
//# sourceMappingURL=elementGeometry.d.ts.map
|
|
@@ -311,7 +311,20 @@ export const ELEMENT_GEOMETRY_JS = `(el) => {
|
|
|
311
311
|
}
|
|
312
312
|
return 'not rendered';
|
|
313
313
|
};
|
|
314
|
-
const
|
|
314
|
+
const shownChildren = (node) => {
|
|
315
|
+
const s = styleOf(node);
|
|
316
|
+
if (s.overflowX !== 'visible' || s.overflowY !== 'visible') return null;
|
|
317
|
+
let union = null;
|
|
318
|
+
for (const c of Array.from(node.children).slice(0, 50)) {
|
|
319
|
+
const r = c.getBoundingClientRect();
|
|
320
|
+
if (r.width <= 0 || r.height <= 0 || styleOf(c).visibility !== 'visible') continue;
|
|
321
|
+
union = union
|
|
322
|
+
? { left: Math.min(union.left, r.left), top: Math.min(union.top, r.top), right: Math.max(union.right, r.right), bottom: Math.max(union.bottom, r.bottom) }
|
|
323
|
+
: { left: r.left, top: r.top, right: r.right, bottom: r.bottom };
|
|
324
|
+
}
|
|
325
|
+
return union;
|
|
326
|
+
};
|
|
327
|
+
const hiddenReason = (style, box, content) => {
|
|
315
328
|
if (style.display === 'none') return 'display: none';
|
|
316
329
|
if (style.display === 'contents') return 'display: contents (no box of its own)';
|
|
317
330
|
if (el.getClientRects().length === 0) {
|
|
@@ -320,7 +333,7 @@ export const ELEMENT_GEOMETRY_JS = `(el) => {
|
|
|
320
333
|
const skipped = skippedReason(el);
|
|
321
334
|
if (skipped) return skipped;
|
|
322
335
|
if (style.visibility !== 'visible') return 'visibility: ' + style.visibility;
|
|
323
|
-
if (box.width === 0 || box.height === 0) return 'zero size';
|
|
336
|
+
if ((box.width === 0 || box.height === 0) && !content) return 'zero size';
|
|
324
337
|
return null;
|
|
325
338
|
};
|
|
326
339
|
const isSticky = () => {
|
|
@@ -343,7 +356,8 @@ export const ELEMENT_GEOMETRY_JS = `(el) => {
|
|
|
343
356
|
let clipper = own.clipper;
|
|
344
357
|
let fixed = own.fixed;
|
|
345
358
|
let fixedBy = own.fixedBy;
|
|
346
|
-
let
|
|
359
|
+
let content = box.width === 0 || box.height === 0 ? shownChildren(el) : null;
|
|
360
|
+
let hidden = hiddenReason(styleOf(el), box, content) || own.collapsed;
|
|
347
361
|
let x = 0;
|
|
348
362
|
let y = 0;
|
|
349
363
|
for (let view = el.ownerDocument.defaultView; view && view.frameElement; view = view.parent) {
|
|
@@ -355,6 +369,7 @@ export const ELEMENT_GEOMETRY_JS = `(el) => {
|
|
|
355
369
|
overlay = { right: !!unframed && unframed.right <= clip.right && overlay.right, bottom: !!unframed && unframed.bottom <= clip.bottom && overlay.bottom };
|
|
356
370
|
const offset = frameOffset(frame);
|
|
357
371
|
rect = shift(rect, offset.x, offset.y);
|
|
372
|
+
content = shift(content, offset.x, offset.y);
|
|
358
373
|
const outer = ancestorClip(frame, rect, describe);
|
|
359
374
|
const inner = shift(clip, offset.x, offset.y);
|
|
360
375
|
clip = intersect(inner, outer.clip);
|
|
@@ -373,7 +388,7 @@ export const ELEMENT_GEOMETRY_JS = `(el) => {
|
|
|
373
388
|
const invisible = hidden ? null : invisibleReason(el, describe);
|
|
374
389
|
const lock = scrollLock(top, describe);
|
|
375
390
|
if (lock && fixedBy === top.document.body) fixed = false;
|
|
376
|
-
return { rect: toBox(rect), clip: toBox(clip), clipOverlay: overlay, clipper: clipper, hidden: hidden, invisible: invisible, inert: isInert(), fixed: fixed, sticky: !fixed && isSticky(), pageScroll: scrollRange(top), scrollLock: lock, offset: { x: x, y: y } };
|
|
391
|
+
return { rect: toBox(rect), content: toBox(content), clip: toBox(clip), clipOverlay: overlay, clipper: clipper, hidden: hidden, invisible: invisible, inert: isInert(), fixed: fixed, sticky: !fixed && isSticky(), pageScroll: scrollRange(top), scrollLock: lock, offset: { x: x, y: y } };
|
|
377
392
|
}`;
|
|
378
393
|
/**
|
|
379
394
|
* Edges of a box.
|
|
@@ -481,14 +496,15 @@ function axisScroll(start, size, viewSize, range, aim) {
|
|
|
481
496
|
* out of a list), and `hidden` when it is not rendered at all. Being inert
|
|
482
497
|
* does not change the position: inert elements are shown.
|
|
483
498
|
*
|
|
484
|
-
* @param
|
|
499
|
+
* @param measured - Page-side measurements (a zero-size holder is placed by its children)
|
|
485
500
|
* @param viewport - Top-level viewport size
|
|
486
501
|
* @returns Position, with the visible share, hidden reason, clipping
|
|
487
502
|
* ancestor or page scroll when relevant
|
|
488
503
|
*/
|
|
489
|
-
export function classifyViewportPosition(
|
|
490
|
-
if (
|
|
491
|
-
return { inViewport: 'hidden', hiddenReason:
|
|
504
|
+
export function classifyViewportPosition(measured, viewport) {
|
|
505
|
+
if (measured.hidden)
|
|
506
|
+
return { inViewport: 'hidden', hiddenReason: measured.hidden };
|
|
507
|
+
const geometry = measured.content ? { ...measured, rect: measured.content } : measured;
|
|
492
508
|
const rect = edgesOf(geometry.rect);
|
|
493
509
|
const screen = edgesOf({ x: 0, y: 0, ...viewport });
|
|
494
510
|
const view = geometry.clip ? overlap(screen, edgesOf(geometry.clip)) : screen;
|
|
@@ -87,10 +87,11 @@ export declare const SIBLING_POSITION_JS = "(el) => {\n const describe = (node)
|
|
|
87
87
|
export declare const ELEMENT_IDENTITY_JS = "(el) => {\n const describe = (node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '');\n const siblingPosition = (el) => {\n const describe = (node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '');\n const parent = el.parentElement;\n if (!parent) return '';\n const own = describe(el);\n const same = Array.from(parent.children).filter((child) => describe(child) === own);\n if (same.length < 2) return '';\n const n = same.indexOf(el) + 1;\n const teen = n % 100 >= 11 && n % 100 <= 13;\n const suffix = teen ? 'th' : ({ 1: 'st', 2: 'nd', 3: 'rd' })[n % 10] || 'th';\n return '(' + n + suffix + ' of ' + same.length + ')';\n};\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const clean = (text) => (text || '').replace(/\\s+/g, ' ').trim();\n const cut = (text) => {\n const characters = Array.from(text);\n return characters.length > 40 ? characters.slice(0, 40).join('') + '\u2026' : text;\n };\n const isControl = (node) => /^(input|select|textarea)$/.test(node.localName) || node.isContentEditable;\n const shownText = (node) =>\n isControl(node) ? '' : clean(withoutDecorations(node, typeof node.innerText === 'string' ? node.innerText : node.textContent));\n const buttonValue = (node) => (node.localName === 'input' && /^(submit|button|reset)$/i.test(node.type) ? clean(node.value) : '');\n const attributeText = (node) =>\n clean(node.getAttribute('aria-label')) || clean(node.getAttribute('placeholder')) || clean(node.getAttribute('title'));\n const selectName = (node) =>\n clean(node.labels && node.labels[0] && node.labels[0].innerText) ||\n clean(node.getAttribute('aria-label')) ||\n clean(node.getAttribute('name')) ||\n clean(node.selectedOptions && node.selectedOptions[0] && node.selectedOptions[0].label);\n const shown = (node, options) => !node.checkVisibility || node.checkVisibility(options);\n const textOutsideSelects = (node) => {\n if (!node.querySelector('select')) return shownText(node);\n const hiddenLeftOut = shown(node);\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_TEXT);\n let text = '';\n for (let visited = 0; visited < 500 && text.length < 200 && walker.nextNode(); visited++) {\n const parent = walker.currentNode.parentElement;\n if (!parent || parent.closest('select, textarea')) continue;\n if (!hiddenLeftOut || shown(parent, { visibilityProperty: true })) text += ' ' + walker.currentNode.data;\n }\n return clean(text);\n };\n const quoted = (text) => (text ? ' \"' + cut(text) + '\"' : '');\n const visible = shownText(el) || buttonValue(el) || (el.localName === 'select' ? selectName(el) : '');\n if (visible) return describe(el) + quoted(visible);\n const attribute = attributeText(el);\n const position = siblingPosition(el);\n if (position) return describe(el) + ' ' + position + quoted(attribute);\n let ancestor = el.id || el.isContentEditable ? null : el.parentElement;\n for (let depth = 0; ancestor && depth < 3; depth++, ancestor = ancestor.parentElement) {\n if (isControl(ancestor)) break;\n const text = textOutsideSelects(ancestor);\n if (text) return describe(el) + ' in ' + describe(ancestor) + quoted(text);\n }\n return describe(el) + quoted(attribute);\n}";
|
|
88
88
|
/**
|
|
89
89
|
* Page-side location of an element: the iframes it is in (outermost first)
|
|
90
|
-
* and the shadow root holding it,
|
|
90
|
+
* and the shadow root holding it, with the host's first class and light-DOM
|
|
91
|
+
* text, e.g. `iframe#pay > shadow root of <sl-button.primary "Save">`;
|
|
91
92
|
* empty for the main document.
|
|
92
93
|
*/
|
|
93
|
-
export declare const ELEMENT_CONTEXT_JS = "(el) => {\n const describe = (node) => node.tagName.toLowerCase() + (node.id ? '#' + node.id : '');\n const parts = [];\n for (let doc = el.ownerDocument; doc && doc.defaultView && doc.defaultView.frameElement; ) {\n const frame = doc.defaultView.frameElement;\n parts.unshift(describe(frame));\n doc = frame.ownerDocument;\n }\n const root = el.getRootNode();\n if (root.host) parts.push('shadow root of <' +
|
|
94
|
+
export declare const ELEMENT_CONTEXT_JS = "(el) => {\n const describe = (node) => node.tagName.toLowerCase() + (node.id ? '#' + node.id : '');\n const parts = [];\n for (let doc = el.ownerDocument; doc && doc.defaultView && doc.defaultView.frameElement; ) {\n const frame = doc.defaultView.frameElement;\n parts.unshift(describe(frame));\n doc = frame.ownerDocument;\n }\n const root = el.getRootNode();\n if (root.host) {\n const host = root.host;\n const label = describe(host) + (!host.id && host.classList.length ? '.' + host.classList[0] : '');\n const text = (host.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 30);\n parts.push('shadow root of <' + label + (text ? ' \"' + text + '\"' : '') + '>');\n }\n return parts.join(' > ');\n}";
|
|
94
95
|
/**
|
|
95
96
|
* Short text preview of an element's text: whitespace collapsed, cut on a
|
|
96
97
|
* whole character (an emoji is never split, which would make JSON invalid).
|