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.
Files changed (103) hide show
  1. package/.claude/skills/bdg/SKILL.md +268 -0
  2. package/README.md +148 -74
  3. package/dist/commands/css.d.ts +13 -0
  4. package/dist/commands/css.js +53 -0
  5. package/dist/commands/dom/audit.d.ts +14 -0
  6. package/dist/commands/dom/audit.js +87 -0
  7. package/dist/commands/dom/formInteraction.js +36 -6
  8. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  9. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  10. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  11. package/dist/commands/dom/helpers/screenshot.js +158 -38
  12. package/dist/commands/dom/index.js +4 -1
  13. package/dist/commands/dom/screenshot.js +10 -6
  14. package/dist/commands/dom/wait.js +5 -3
  15. package/dist/commands/helpJson.js +1 -1
  16. package/dist/commands/installSkill.d.ts +20 -0
  17. package/dist/commands/installSkill.js +87 -0
  18. package/dist/commands/optionBehaviors.js +21 -6
  19. package/dist/commands/page.js +7 -4
  20. package/dist/commands/peek.d.ts +7 -0
  21. package/dist/commands/peek.js +65 -23
  22. package/dist/commands/shared/optionTypes.d.ts +5 -1
  23. package/dist/commands/start.d.ts +13 -0
  24. package/dist/commands/start.js +19 -2
  25. package/dist/commands/tail.d.ts +7 -1
  26. package/dist/commands/tail.js +13 -62
  27. package/dist/commands.js +5 -0
  28. package/dist/daemon/session/commandRegistry.js +7 -1
  29. package/dist/daemon/session/plugins.js +4 -52
  30. package/dist/daemon.js +7986 -6848
  31. package/dist/errors/messages.d.ts +50 -4
  32. package/dist/errors/messages.js +94 -5
  33. package/dist/index.js +709 -190
  34. package/dist/ipc/client.d.ts +4 -0
  35. package/dist/ipc/client.js +8 -0
  36. package/dist/ipc/protocol/auditTypes.d.ts +129 -0
  37. package/dist/ipc/protocol/auditTypes.js +6 -0
  38. package/dist/ipc/protocol/commands.d.ts +23 -0
  39. package/dist/ipc/protocol/commands.js +2 -0
  40. package/dist/ipc/protocol/domTypes.d.ts +4 -0
  41. package/dist/ipc/protocol/inspectTypes.d.ts +71 -8
  42. package/dist/runtime/css/search.d.ts +39 -0
  43. package/dist/runtime/css/search.js +122 -0
  44. package/dist/runtime/dom/actionEffects.d.ts +4 -1
  45. package/dist/runtime/dom/actionEffects.js +8 -4
  46. package/dist/runtime/dom/audit.d.ts +19 -0
  47. package/dist/runtime/dom/audit.js +36 -0
  48. package/dist/runtime/dom/auditModel.d.ts +45 -0
  49. package/dist/runtime/dom/auditModel.js +215 -0
  50. package/dist/runtime/dom/auditScripts.d.ts +107 -0
  51. package/dist/runtime/dom/auditScripts.js +112 -0
  52. package/dist/runtime/dom/elementGeometry.d.ts +8 -2
  53. package/dist/runtime/dom/elementGeometry.js +24 -8
  54. package/dist/runtime/dom/elementInfo.d.ts +3 -2
  55. package/dist/runtime/dom/elementInfo.js +8 -2
  56. package/dist/runtime/dom/formFillHelpers/fill.js +2 -2
  57. package/dist/runtime/dom/inspect.d.ts +7 -0
  58. package/dist/runtime/dom/inspect.js +88 -23
  59. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  60. package/dist/runtime/dom/inspectAllStyles.js +89 -7
  61. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  62. package/dist/runtime/dom/inspectCascade.js +214 -44
  63. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  64. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  65. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  66. package/dist/runtime/dom/inspectHints.js +125 -9
  67. package/dist/runtime/dom/inspectModel.d.ts +3 -0
  68. package/dist/runtime/dom/inspectModel.js +30 -7
  69. package/dist/runtime/dom/inspectPaintModel.d.ts +48 -22
  70. package/dist/runtime/dom/inspectPaintModel.js +180 -68
  71. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  72. package/dist/runtime/dom/inspectRules.js +21 -5
  73. package/dist/runtime/dom/inspectScripts.d.ts +85 -12
  74. package/dist/runtime/dom/inspectScripts.js +314 -28
  75. package/dist/runtime/dom/inspectTree.js +10 -2
  76. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  77. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  78. package/dist/runtime/dom/layout.js +31 -9
  79. package/dist/runtime/dom/reactEventHelpers.d.ts +7 -0
  80. package/dist/runtime/dom/reactEventHelpers.js +27 -9
  81. package/dist/runtime/page/emulation.d.ts +13 -4
  82. package/dist/runtime/page/emulation.js +69 -4
  83. package/dist/runtime/page/userAgent.d.ts +17 -0
  84. package/dist/runtime/page/userAgent.js +57 -0
  85. package/dist/types.d.ts +12 -0
  86. package/dist/ui/formatters/audit.d.ts +19 -0
  87. package/dist/ui/formatters/audit.js +106 -0
  88. package/dist/ui/formatters/dom.d.ts +1 -1
  89. package/dist/ui/formatters/dom.js +6 -3
  90. package/dist/ui/formatters/inspect.js +42 -15
  91. package/dist/ui/formatters/installSkill.d.ts +11 -0
  92. package/dist/ui/formatters/installSkill.js +31 -0
  93. package/dist/ui/formatters/status.js +1 -1
  94. package/dist/ui/messages/commands.d.ts +44 -7
  95. package/dist/ui/messages/commands.js +83 -11
  96. package/dist/ui/messages/preview.d.ts +6 -0
  97. package/dist/ui/messages/preview.js +9 -1
  98. package/dist/utils/cssValues.js +36 -4
  99. package/dist/utils/decisionTrees.js +0 -5
  100. package/dist/utils/suggestions.d.ts +4 -2
  101. package/dist/utils/suggestions.js +7 -5
  102. package/dist/utils/taskMappings.js +1 -1
  103. 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 geometry - Page-side measurements
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(geometry: ElementGeometry, viewport: LayoutSize): ViewportPlacement;
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 hiddenReason = (style, box) => {
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 hidden = hiddenReason(styleOf(el), box) || own.collapsed;
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 geometry - Page-side measurements
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(geometry, viewport) {
490
- if (geometry.hidden)
491
- return { inViewport: 'hidden', hiddenReason: geometry.hidden };
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, e.g. `iframe#pay > shadow root of <x-card>`;
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 <' + describe(root.host) + '>');\n return parts.join(' > ');\n}";
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).