browser-debugger-cli 0.12.0 → 0.13.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 +4 -4
- package/README.md +1 -0
- package/dist/commands/cdp.d.ts +22 -1
- package/dist/commands/cdp.js +100 -43
- package/dist/commands/console.d.ts +12 -0
- package/dist/commands/console.js +62 -12
- package/dist/commands/dom/DomElementResolver.d.ts +3 -1
- package/dist/commands/dom/DomElementResolver.js +10 -3
- package/dist/commands/dom/a11y.js +3 -2
- package/dist/commands/dom/eval.d.ts +3 -2
- package/dist/commands/dom/eval.js +11 -5
- package/dist/commands/dom/form.js +10 -9
- package/dist/commands/dom/formInteraction.js +8 -7
- package/dist/commands/dom/get.js +8 -8
- package/dist/commands/dom/helpers/index.d.ts +1 -1
- package/dist/commands/dom/helpers/index.js +1 -1
- package/dist/commands/dom/helpers/query.d.ts +27 -3
- package/dist/commands/dom/helpers/query.js +152 -64
- package/dist/commands/dom/helpers/screenshot.js +13 -13
- package/dist/commands/dom/index.js +4 -2
- package/dist/commands/dom/query.d.ts +19 -2
- package/dist/commands/dom/query.js +37 -6
- package/dist/commands/dom/screenshot.js +2 -1
- package/dist/commands/dom/semanticUtils.d.ts +3 -2
- package/dist/commands/dom/semanticUtils.js +40 -9
- package/dist/commands/helpJson.d.ts +82 -19
- package/dist/commands/helpJson.js +111 -40
- package/dist/commands/helpTopic.d.ts +16 -1
- package/dist/commands/helpTopic.js +59 -1
- package/dist/commands/installSkill.d.ts +15 -5
- package/dist/commands/installSkill.js +86 -16
- package/dist/commands/network/list.js +22 -12
- package/dist/commands/optionBehaviors.js +33 -11
- package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
- package/dist/commands/shared/daemonErrorHandler.js +20 -9
- package/dist/commands/shared/dataFetcher.d.ts +12 -4
- package/dist/commands/shared/dataFetcher.js +12 -4
- package/dist/commands/shared/followMode.d.ts +9 -1
- package/dist/commands/shared/followMode.js +22 -4
- package/dist/commands/shared/optionTypes.d.ts +4 -1
- package/dist/commands/shared/outputFile.js +6 -1
- package/dist/commands/start.d.ts +7 -5
- package/dist/commands/start.js +65 -21
- package/dist/commands/stop.d.ts +11 -0
- package/dist/commands/stop.js +24 -1
- package/dist/commands.js +1 -1
- package/dist/connection/cdp.d.ts +7 -0
- package/dist/connection/cdp.js +9 -0
- package/dist/connection/launcher.js +3 -2
- package/dist/daemon/SessionController.js +6 -1
- package/dist/daemon/launcher.d.ts +3 -2
- package/dist/daemon/launcher.js +47 -3
- package/dist/daemon/session/Session.d.ts +4 -1
- package/dist/daemon/session/Session.js +33 -2
- package/dist/daemon/session/TelemetryStore.d.ts +8 -1
- package/dist/daemon/session/TelemetryStore.js +13 -1
- package/dist/daemon/session/commandRegistry.js +29 -13
- package/dist/daemon/session/interactions.d.ts +2 -1
- package/dist/daemon/session/interactions.js +13 -1
- package/dist/daemon/session/plugins.js +16 -2
- package/dist/daemon/session/teardown.js +1 -1
- package/dist/daemon.js +1622 -748
- package/dist/errors/messages.d.ts +54 -11
- package/dist/errors/messages.js +109 -22
- package/dist/index.js +13733 -8796
- package/dist/ipc/client.d.ts +18 -2
- package/dist/ipc/client.js +26 -5
- package/dist/ipc/protocol/auditTypes.d.ts +8 -2
- package/dist/ipc/protocol/commands.d.ts +12 -0
- package/dist/ipc/protocol/domTypes.d.ts +12 -0
- package/dist/ipc/protocol/inspectTypes.d.ts +2 -0
- package/dist/ipc/session/types.d.ts +2 -0
- package/dist/runtime/dom/actionEffects.d.ts +5 -1
- package/dist/runtime/dom/actionEffects.js +26 -14
- package/dist/runtime/dom/audit.js +3 -2
- package/dist/runtime/dom/auditModel.js +6 -1
- package/dist/runtime/dom/auditScripts.d.ts +9 -3
- package/dist/runtime/dom/auditScripts.js +41 -5
- package/dist/runtime/dom/elementGeometry.d.ts +10 -3
- package/dist/runtime/dom/elementGeometry.js +27 -4
- package/dist/runtime/dom/elementInfo.d.ts +74 -18
- package/dist/runtime/dom/elementInfo.js +187 -40
- package/dist/runtime/dom/evalHelpers.d.ts +12 -2
- package/dist/runtime/dom/evalHelpers.js +67 -7
- package/dist/runtime/dom/formDiscovery.d.ts +6 -2
- package/dist/runtime/dom/formDiscovery.js +20 -3
- package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
- package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
- package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
- package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
- package/dist/runtime/dom/formSubmitHelpers.js +4 -3
- package/dist/runtime/dom/frameLayout.js +1 -0
- package/dist/runtime/dom/inspect.js +5 -6
- package/dist/runtime/dom/inspectAllStyles.js +1 -0
- package/dist/runtime/dom/inspectHints.d.ts +1 -1
- package/dist/runtime/dom/inspectModel.d.ts +2 -1
- package/dist/runtime/dom/inspectModel.js +7 -3
- package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
- package/dist/runtime/dom/inspectPaintModel.js +3 -1
- package/dist/runtime/dom/inspectScripts.d.ts +29 -2
- package/dist/runtime/dom/inspectScripts.js +49 -10
- package/dist/runtime/dom/layout.js +9 -7
- package/dist/runtime/dom/reactEventHelpers.d.ts +14 -4
- package/dist/runtime/dom/reactEventHelpers.js +63 -27
- package/dist/runtime/dom/targetNode.d.ts +18 -5
- package/dist/runtime/dom/targetNode.js +268 -8
- package/dist/runtime/dom/wait.js +2 -1
- package/dist/runtime/page/bdgWorld.d.ts +57 -0
- package/dist/runtime/page/bdgWorld.js +180 -0
- package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
- package/dist/runtime/page/replacedBuiltins.js +136 -0
- package/dist/session/QueryCacheManager.d.ts +4 -1
- package/dist/session/QueryCacheManager.js +5 -2
- package/dist/session/chrome.d.ts +4 -1
- package/dist/session/chrome.js +7 -1
- package/dist/session/cleanup/staleSession.d.ts +21 -4
- package/dist/session/cleanup/staleSession.js +79 -9
- package/dist/session/cleanup/userCommands.d.ts +4 -1
- package/dist/session/cleanup/userCommands.js +10 -5
- package/dist/session/daemonSocket.d.ts +10 -0
- package/dist/session/daemonSocket.js +22 -0
- package/dist/session/lastSession.d.ts +6 -3
- package/dist/session/lastSession.js +11 -5
- package/dist/session/paths.d.ts +3 -1
- package/dist/session/paths.js +5 -5
- package/dist/session/portClaims.js +4 -3
- package/dist/session/sessionList.d.ts +13 -5
- package/dist/session/sessionList.js +31 -7
- package/dist/telemetry/a11y.js +2 -2
- package/dist/telemetry/console.d.ts +2 -1
- package/dist/telemetry/console.js +30 -21
- package/dist/telemetry/pageCrash.d.ts +26 -0
- package/dist/telemetry/pageCrash.js +53 -0
- package/dist/types.d.ts +16 -0
- package/dist/ui/formatters/audit.js +14 -5
- package/dist/ui/formatters/cdp.d.ts +138 -0
- package/dist/ui/formatters/cdp.js +131 -0
- package/dist/ui/formatters/console/chronological.js +3 -1
- package/dist/ui/formatters/console/follow.d.ts +2 -1
- package/dist/ui/formatters/console/follow.js +2 -2
- package/dist/ui/formatters/console/json.d.ts +2 -2
- package/dist/ui/formatters/console/json.js +11 -5
- package/dist/ui/formatters/console/shared.d.ts +30 -0
- package/dist/ui/formatters/console/shared.js +16 -0
- package/dist/ui/formatters/console/summarize.d.ts +9 -2
- package/dist/ui/formatters/console/summarize.js +40 -9
- package/dist/ui/formatters/console.d.ts +2 -1
- package/dist/ui/formatters/console.js +7 -5
- package/dist/ui/formatters/details.js +3 -1
- package/dist/ui/formatters/dom.d.ts +1 -1
- package/dist/ui/formatters/dom.js +5 -6
- package/dist/ui/formatters/helpFormatters.js +1 -1
- package/dist/ui/formatters/inspect.js +9 -3
- package/dist/ui/formatters/installSkill.d.ts +9 -1
- package/dist/ui/formatters/installSkill.js +32 -6
- package/dist/ui/formatters/layout.js +2 -1
- package/dist/ui/formatters/networkList.d.ts +1 -1
- package/dist/ui/formatters/networkList.js +1 -2
- package/dist/ui/formatters/preview.d.ts +2 -0
- package/dist/ui/formatters/preview.js +17 -7
- package/dist/ui/formatters/sessions.d.ts +2 -2
- package/dist/ui/formatters/sessions.js +9 -2
- package/dist/ui/logging/logger.d.ts +1 -1
- package/dist/ui/messages/commands.d.ts +124 -4
- package/dist/ui/messages/commands.js +162 -7
- package/dist/ui/messages/consoleMessages.d.ts +24 -0
- package/dist/ui/messages/consoleMessages.js +32 -0
- package/dist/ui/messages/preview.d.ts +6 -0
- package/dist/ui/messages/preview.js +9 -1
- package/dist/ui/messages/session.d.ts +13 -2
- package/dist/ui/messages/session.js +22 -3
- package/dist/utils/directories.d.ts +34 -0
- package/dist/utils/directories.js +88 -0
- package/dist/utils/display.d.ts +16 -0
- package/dist/utils/display.js +42 -0
- package/dist/utils/exitCodes.d.ts +1 -0
- package/dist/utils/exitCodes.js +6 -0
- package/dist/utils/process.d.ts +12 -0
- package/dist/utils/process.js +25 -0
- package/package.json +1 -1
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* checked against authored declarations only (never the browser's own
|
|
5
5
|
* styles), and `var()` references to custom properties that are not set.
|
|
6
6
|
*/
|
|
7
|
+
import type { InspectHint } from '../../ipc/protocol/inspectTypes.js';
|
|
7
8
|
import type { Declaration, Resolution } from './inspectCascade.js';
|
|
8
9
|
import type { StyleMap } from './inspectLayoutModel.js';
|
|
9
|
-
import type { InspectHint } from '../../ipc/protocol/inspectTypes.js';
|
|
10
10
|
import type { VariableSetter } from './inspectScripts.js';
|
|
11
11
|
/** A declaration that has no effect */
|
|
12
12
|
export interface CssHint {
|
|
@@ -52,7 +52,8 @@ export interface InspectRequest {
|
|
|
52
52
|
propValues?: InspectResult['props'];
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* The element's label: tag, id and the first classes
|
|
55
|
+
* The element's label: tag, id and the first classes ({@link isLabelClass}),
|
|
56
|
+
* with a count of the rest.
|
|
56
57
|
*
|
|
57
58
|
* @param raw - Tag, id and classes
|
|
58
59
|
* @returns e.g. `a.z-1.max-sm:hidden(+11)`, `input#user-name`
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* tree. Pure: every input is plain data, so the rules are tested without a
|
|
6
6
|
* browser.
|
|
7
7
|
*/
|
|
8
|
+
import { isLabelClass } from './elementInfo.js';
|
|
8
9
|
import { allStyles } from './inspectAllStyles.js';
|
|
9
10
|
import { buildBox, buildLayout } from './inspectLayoutModel.js';
|
|
10
11
|
import { buildEffects, buildFills, buildSvgPaint, buildFx, buildOutline, buildPseudo, buildRadius, buildState, buildStrokes, buildText, effectiveBackground, } from './inspectPaintModel.js';
|
|
@@ -14,17 +15,19 @@ import { normalizeCssValue, round1 } from '../../utils/cssValues.js';
|
|
|
14
15
|
/** Classes shown in the label (the rest are counted) */
|
|
15
16
|
const LABEL_CLASSES = 2;
|
|
16
17
|
/**
|
|
17
|
-
* The element's label: tag, id and the first classes
|
|
18
|
+
* The element's label: tag, id and the first classes ({@link isLabelClass}),
|
|
19
|
+
* with a count of the rest.
|
|
18
20
|
*
|
|
19
21
|
* @param raw - Tag, id and classes
|
|
20
22
|
* @returns e.g. `a.z-1.max-sm:hidden(+11)`, `input#user-name`
|
|
21
23
|
*/
|
|
22
24
|
export function elementLabel(raw) {
|
|
23
|
-
const
|
|
25
|
+
const classes = raw.classes.filter(isLabelClass);
|
|
26
|
+
const shown = classes
|
|
24
27
|
.slice(0, LABEL_CLASSES)
|
|
25
28
|
.map((name) => `.${name}`)
|
|
26
29
|
.join('');
|
|
27
|
-
const more =
|
|
30
|
+
const more = classes.length - LABEL_CLASSES;
|
|
28
31
|
return `${raw.tag}${raw.id ? `#${raw.id}` : ''}${shown}${more > 0 ? `(+${more})` : ''}`;
|
|
29
32
|
}
|
|
30
33
|
/**
|
|
@@ -48,6 +51,7 @@ export function visibilityOf(layout, rendered) {
|
|
|
48
51
|
...(offscreen && { offscreen }),
|
|
49
52
|
...(layout.coveredBy && { coveredBy: layout.coveredBy }),
|
|
50
53
|
...(layout.coverTransparent && { coverTransparent: true }),
|
|
54
|
+
...(layout.masked && { masked: layout.masked }),
|
|
51
55
|
};
|
|
52
56
|
}
|
|
53
57
|
/**
|
|
@@ -46,6 +46,8 @@ export declare function effectiveBackground(backgrounds: readonly RawBackground[
|
|
|
46
46
|
inherited: boolean;
|
|
47
47
|
overImage: boolean;
|
|
48
48
|
};
|
|
49
|
+
/** Why a contrast over a background image or gradient is approximate (its pixels are unknown) */
|
|
50
|
+
export declare const OVER_IMAGE_RISK = "background image or gradient behind";
|
|
49
51
|
/**
|
|
50
52
|
* Contrast of the text color with the background behind it, both painted
|
|
51
53
|
* as the browser composites them ({@link paintOver}), with what makes the
|
|
@@ -128,6 +128,8 @@ export function effectiveBackground(backgrounds, canvasDark) {
|
|
|
128
128
|
overImage: painted.overImage,
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
+
/** Why a contrast over a background image or gradient is approximate (its pixels are unknown) */
|
|
132
|
+
export const OVER_IMAGE_RISK = 'background image or gradient behind';
|
|
131
133
|
/**
|
|
132
134
|
* Contrast of the text color with the background behind it, both painted
|
|
133
135
|
* as the browser composites them ({@link paintOver}), with what makes the
|
|
@@ -146,7 +148,7 @@ export function textContrast(style, raw) {
|
|
|
146
148
|
const ratio = Math.floor(contrastRatio(painted.text, painted.background) * 100) / 100;
|
|
147
149
|
const size = pxNumber(style['font-size']) ?? 16;
|
|
148
150
|
const weight = Number(style['font-weight'] ?? 400);
|
|
149
|
-
const approximate = raw.paintRisks ?? [];
|
|
151
|
+
const approximate = [...(raw.paintRisks ?? []), ...(painted.overImage ? [OVER_IMAGE_RISK] : [])];
|
|
150
152
|
return {
|
|
151
153
|
ratio,
|
|
152
154
|
level: contrastLevel(ratio, size, weight),
|
|
@@ -161,12 +161,39 @@ export interface RawInspect {
|
|
|
161
161
|
/** Properties of running CSS transitions, and names of running animations (time-based: scroll-driven ones do not change on their own) */
|
|
162
162
|
animating: string[];
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Page-side flat-tree helpers: the element's computed style, its flat-tree
|
|
166
|
+
* parent, the parent that lays it out (skipping `display: contents`), its
|
|
167
|
+
* element children in the flat tree and whether an element is rendered.
|
|
168
|
+
*/
|
|
169
|
+
export declare const FLAT_TREE_JS: string;
|
|
170
|
+
/**
|
|
171
|
+
* Page-side hit test that also sees elements with `pointer-events: none`
|
|
172
|
+
* (hero images, decorative layers and blended duplicates often have it, and
|
|
173
|
+
* `elementsFromPoint` skips them): for the duration of `test`, a constructed
|
|
174
|
+
* stylesheet makes every element of the given roots (documents and shadow
|
|
175
|
+
* roots) hittable. It is added and removed within one script turn, so nothing
|
|
176
|
+
* is painted with it and no DOM mutation is recorded. A root that already has
|
|
177
|
+
* it (an outer call covering a whole page walk) is left as it is, so nested
|
|
178
|
+
* calls cost no extra style recalculation.
|
|
179
|
+
*/
|
|
180
|
+
export declare const HIT_EVERYTHING_JS = "(roots, test) => {\n const added = [];\n for (const root of roots) {\n if (!root || !root.adoptedStyleSheets) continue;\n const view = (root.ownerDocument || root).defaultView;\n try {\n if (!view.__bdgHitSheet) {\n view.__bdgHitSheet = new view.CSSStyleSheet();\n view.__bdgHitSheet.replaceSync('* { pointer-events: auto !important; }');\n }\n if (root.adoptedStyleSheets.includes(view.__bdgHitSheet)) continue;\n root.adoptedStyleSheets = [...root.adoptedStyleSheets, view.__bdgHitSheet];\n added.push([root, view.__bdgHitSheet]);\n } catch (e) {\n continue;\n }\n }\n try {\n return test();\n } finally {\n for (const [root, sheet] of added) root.adoptedStyleSheets = root.adoptedStyleSheets.filter((s) => s !== sheet);\n }\n}";
|
|
181
|
+
/**
|
|
182
|
+
* Page-side reasons the contrast of an element's text is approximate: a
|
|
183
|
+
* blend mode or filter on it or an ancestor, and, hit-testing the middle of
|
|
184
|
+
* its first line of text (else of its first box; only in the viewport), the
|
|
185
|
+
* nearest element below it that is not an ancestor, above the first opaque
|
|
186
|
+
* background of its own chain, and
|
|
187
|
+
* paints (a canvas, video, image, background) and the nearest one on top of
|
|
188
|
+
* it that paints (an overlay).
|
|
189
|
+
*/
|
|
190
|
+
export declare const PAINT_RISKS_JS = "(el, tree, textParent) => {\n const risks = [];\n const chain = [];\n for (let n = el; n && chain.length < 60; n = tree.flatParent(n)) chain.push(n);\n for (const n of chain) {\n const s = tree.style(n);\n if (s.mixBlendMode !== 'normal') risks.push('mix-blend-mode ' + s.mixBlendMode + ' on ' + tree.label(n));\n if (s.filter !== 'none') risks.push('filter on ' + tree.label(n));\n }\n const textNode = Array.from(textParent.childNodes).find((c) => c.nodeType === 3 && c.data.trim() !== '');\n const range = el.ownerDocument.createRange();\n if (textNode) range.selectNodeContents(textNode);\n const box = tree.style(el).display === 'contents' ? tree.layoutParent(el) : el;\n const rect = (textNode && range.getClientRects()[0]) || (box && box.getClientRects()[0]);\n const view = el.ownerDocument.defaultView;\n if (!rect) return risks;\n const x = rect.left + rect.width / 2;\n const y = rect.top + rect.height / 2;\n if (x < 0 || y < 0 || x >= view.innerWidth || y >= view.innerHeight) return risks;\n const root = el.getRootNode();\n const hits = ((roots, test) => {\n const added = [];\n for (const root of roots) {\n if (!root || !root.adoptedStyleSheets) continue;\n const view = (root.ownerDocument || root).defaultView;\n try {\n if (!view.__bdgHitSheet) {\n view.__bdgHitSheet = new view.CSSStyleSheet();\n view.__bdgHitSheet.replaceSync('* { pointer-events: auto !important; }');\n }\n if (root.adoptedStyleSheets.includes(view.__bdgHitSheet)) continue;\n root.adoptedStyleSheets = [...root.adoptedStyleSheets, view.__bdgHitSheet];\n added.push([root, view.__bdgHitSheet]);\n } catch (e) {\n continue;\n }\n }\n try {\n return test();\n } finally {\n for (const [root, sheet] of added) root.adoptedStyleSheets = root.adoptedStyleSheets.filter((s) => s !== sheet);\n }\n})([el.ownerDocument, root], () => (root.elementsFromPoint ? root : el.ownerDocument).elementsFromPoint(x, y));\n const at = hits.findIndex((h) => chain.includes(h));\n if (at < 0) return risks;\n const clear = (color) => color === 'transparent' || /^rgba\\(.*,\\s*0\\)$/.test(color) || /\\/\\s*0\\)$/.test(color);\n const opaque = (n) => {\n const s = tree.style(n);\n return /^rgb\\(/.test(s.backgroundColor) && s.backgroundImage === 'none' && Number(s.opacity) === 1;\n };\n const paints = (n) => {\n const s = tree.style(n);\n return /^(canvas|video|img|iframe|embed|object|svg)$/.test(n.localName) || s.backgroundImage !== 'none' || !clear(s.backgroundColor);\n };\n let behind = null;\n for (const h of hits.slice(at)) {\n if (chain.includes(h)) {\n if (opaque(h)) break;\n } else if (paints(h)) {\n behind = h;\n break;\n }\n }\n if (behind) risks.push(tree.label(behind) + ' behind');\n const onTop = hits.slice(0, at).find((h) => !el.contains(h) && paints(h));\n if (onTop) risks.push(tree.label(onTop) + ' on top');\n return risks;\n}";
|
|
164
191
|
/**
|
|
165
192
|
* Page-side function run on the element (`this`), see {@link RawInspect}.
|
|
166
193
|
* Arguments: tree depth, the property names asked for with `--props` (or
|
|
167
194
|
* null) and the `--why` property (or null).
|
|
168
195
|
*/
|
|
169
|
-
export declare const INSPECT_PAGE_JS = "function (depth, props, why) {\n const el = this;\n const view = el.ownerDocument.defaultView;\n const tree = ((view) => {\n const style = (n, pseudo) => view.getComputedStyle(n, pseudo);\n const skipped = /^(script|style|template|link|meta|noscript|title|head|base)$/;\n const flatParent = (n) => n.assignedSlot || n.parentElement || (n.parentNode && n.parentNode.host) || null;\n const layoutParent = (n) => {\n let p = flatParent(n);\n while (p && style(p).display === 'contents') p = flatParent(p);\n return p;\n };\n const via = new WeakMap();\n const label = (n) => n.localName + (n.id ? '#' + n.id : n.classList && n.classList.length ? '.' + n.classList[0] : '');\n const slotText = (slot) => slot.assignedNodes({ flatten: true }).some((t) => t.nodeType === 3 && t.data.trim() !== '');\n const children = (n) => {\n const out = [];\n const add = (c, through) => {\n if (skipped.test(c.localName)) return;\n if (c.localName === 'slot') {\n const assigned = c.assignedElements({ flatten: true });\n if (assigned.length === 0 && slotText(c)) out.push(c);\n else (assigned.length ? assigned : Array.from(c.children)).forEach((a) => add(a, label(c)));\n } else if (style(c).display === 'contents') {\n Array.from((c.shadowRoot || c).children).forEach((a) => add(a, label(c) + ' (contents)'));\n } else {\n if (through) via.set(c, through);\n out.push(c);\n }\n };\n Array.from((n.shadowRoot || n).children).forEach((c) => add(c, null));\n return out;\n };\n const rendered = (n) => n.getClientRects().length > 0 &&\n (!n.checkVisibility || n.checkVisibility({ visibilityProperty: true }));\n const container = (n) => children(n).some((c) => rendered(c) && !/^inline/.test(style(c).display));\n return { style, skipped, flatParent, layoutParent, children, rendered, container, label, via, slotText };\n})(view);\n const textOf = (el) => {\n const isSensitive = (el) => {\n const autocomplete = el.getAttribute('autocomplete') || '';\n if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;\n if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;\n const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');\n if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;\n try {\n const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');\n return Boolean(security) && security !== 'none';\n } catch (e) {\n return false;\n }\n};\n const clean = (text) => String(text || '').replace(/\\s+/g, ' ').trim().slice(0, 200);\n if (el.localName === 'input' && el.type === 'hidden') return '';\n if (el.localName === 'input' || el.localName === 'textarea') {\n return el.value && isSensitive(el) ? '\u2022\u2022\u2022\u2022' : clean(el.value);\n }\n if (el.localName === 'select') {\n const chosen = clean(Array.from(el.selectedOptions || [], (o) => o.label).join(', '));\n return chosen && isSensitive(el) ? '\u2022\u2022\u2022\u2022' : chosen;\n }\n const outsideControls = () => {\n const parts = [];\n const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);\n for (let n = walker.nextNode(); n && parts.length < 400; n = walker.nextNode()) {\n const holder = n.parentElement;\n if (!holder || holder.closest('select, textarea, script, style, template, noscript')) continue;\n if (holder.checkVisibility && !holder.checkVisibility({ visibilityProperty: true })) continue;\n parts.push(n.data);\n }\n return parts.join(' ');\n };\n const holdsControls = Boolean(el.querySelector && el.querySelector('select, textarea'));\n const raw = holdsControls ? outsideControls() : typeof el.innerText === 'string' ? el.innerText : el.textContent;\n const text = clean(raw);\n const view = el.ownerDocument.defaultView;\n const security = view ? view.getComputedStyle(el).getPropertyValue('-webkit-text-security') : '';\n return text && security && security !== 'none' ? '\u2022\u2022\u2022\u2022' : text;\n};\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 s = tree.style(el);\n const formControl = /^(input|textarea|select|button)$/.test(el.localName);\n const textControl = ((el) => {\n if (el.localName === 'textarea' || el.localName === 'select') return true;\n return el.localName === 'input' && !/^(checkbox|radio|range|color|hidden|image)$/.test(el.type);\n})(el);\n const holder = textControl ? null : ((el, tree) => {\n const counts = new Map();\n const parents = new Map();\n let budget = 2000;\n const shown = (n) => tree.style(n).display === 'contents' || tree.rendered(n);\n const nodesOf = (n) => {\n if (n.localName !== 'slot') return Array.from((n.shadowRoot || n).childNodes);\n const assigned = n.assignedNodes({ flatten: true });\n return assigned.length ? assigned : Array.from(n.childNodes);\n };\n const visit = (n) => {\n if (budget-- <= 0) return;\n for (const c of nodesOf(n)) {\n if (budget <= 0) return;\n if (c.nodeType === 3) {\n const length = c.data.replace(/\\s+/g, '').length;\n if (length === 0) continue;\n counts.set(n, (counts.get(n) || 0) + length);\n if (!parents.has(n)) parents.set(n, c.parentElement || n);\n } else if (c.nodeType === 1 && !tree.skipped.test(c.localName) && shown(c)) {\n visit(c);\n }\n }\n };\n visit(el);\n let best = null;\n for (const [holder, length] of counts) if (!best || length > counts.get(best)) best = holder;\n let total = 0;\n for (const length of counts.values()) total += length;\n return best && { style: best, font: parents.get(best), share: counts.get(best) / total };\n})(el, tree);\n const textChildren = tree.children(el).filter((c) => tree.rendered(c) && (c.textContent || '').trim() !== '').length;\n const textual = textControl || Boolean(holder && (holder.style === el || (holder.share >= 0.9 && textChildren <= 1) || !tree.container(el)));\n const textFrom = holder && holder.style !== el ? holder.style : el;\n const content = textOf(el);\n const parent = tree.layoutParent(el);\n const typedMap = el.computedStyleMap ? el.computedStyleMap() : null;\n const typed = (name) => { try { return typedMap ? String(typedMap.get(name)) : ''; } catch (e) { return ''; } };\n const result = {\n tag: el.localName,\n id: el.id || '',\n classes: el.classList ? Array.from(el.classList) : [],\n context: ((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})(el),\n content: content,\n textual: textual,\n formControl: formControl,\n rendered: textual && textFrom !== el\n ? tree.style(textFrom).visibility !== 'hidden' && tree.rendered(tree.style(textFrom).display === 'contents' ? tree.layoutParent(textFrom) : textFrom)\n : tree.rendered(el),\n hasText: textControl || holder !== null,\n replaced: el instanceof SVGElement || /^(img|video|audio|canvas|iframe|embed|object|input|textarea|select|button|meter|progress)$/.test(el.localName),\n svg: el.namespaceURI === 'http://www.w3.org/2000/svg',\n typed: { width: typed('width'), height: typed('height') },\n parent: parent ? describe(parent) : undefined,\n animating: el.getAnimations ? [...new Set(el.getAnimations().filter((a) => a.playState === 'running' && (!a.timeline || a.timeline === el.ownerDocument.timeline)).map((a) => a.transitionProperty || a.animationName || 'animation'))] : []\n };\n if (textFrom !== el) result.textHolder = tree.label(textFrom);\n const clipped = (n) => {\n const cs = tree.style(n);\n if (cs.overflowX !== 'visible' && n.scrollWidth > n.clientWidth + 1) return true;\n return cs.webkitLineClamp !== 'none' && cs.overflowY !== 'visible' && n.scrollHeight > n.clientHeight + 1;\n };\n if (textual && !textControl && (clipped(el) || (textFrom !== el && clipped(textFrom)))) result.truncated = true;\n if (((n, tree, text) => {\n const unquote = (name) => name.trim().replace(/^[\"']|[\"']$/g, '').toLowerCase();\n const match = /^\\s*(\"[^\"]*\"|'[^']*'|[^,]*)/.exec(tree.style(n).fontFamily);\n const first = unquote(match ? match[1] : '');\n if (!first || !n.ownerDocument.fonts) return false;\n const ranges = [];\n for (const face of n.ownerDocument.fonts) {\n if (face.status !== 'loaded' || unquote(face.family) !== first) continue;\n for (const part of face.unicodeRange.split(',')) {\n const [from, to] = part.trim().replace(/^U\\+/i, '').split('-');\n const low = parseInt(from.replace(/\\?/g, '0'), 16);\n const high = parseInt((to || from).replace(/\\?/g, 'F'), 16);\n if (!Number.isNaN(low) && !Number.isNaN(high)) ranges.push([low, high]);\n }\n }\n if (ranges.length === 0) return false;\n const covered = (code) => ranges.some(([low, high]) => code >= low && code <= high);\n return Array.from(text.replace(/\\s+/g, '')).every((char) => covered(char.codePointAt(0)));\n})(textFrom, tree, textOf(textFrom))) result.familyLoaded = true;\n if (/^(input|textarea)$/.test(el.localName) && el.placeholder && !el.value) {\n result.placeholder = el.placeholder.replace(/\\s+/g, ' ').trim();\n const placeholderStyle = tree.style(el, '::placeholder');\n result.placeholderColor = placeholderStyle.color;\n result.placeholderFont = { style: placeholderStyle.fontStyle, weight: placeholderStyle.fontWeight };\n }\n if (el.clientWidth > 0 && (el.scrollWidth > el.clientWidth + 1 || el.scrollHeight > el.clientHeight + 1)) {\n result.scroll = { w: el.scrollWidth, h: el.scrollHeight, clientW: el.clientWidth, clientH: el.clientHeight };\n }\n if (props) {\n const known = (name) => name.startsWith('--') || CSS.supports(name, 'inherit');\n const valueOf = (name) => {\n const value = s.getPropertyValue(name).trim();\n if (value !== '' || name.startsWith('--')) return value;\n const sides = ['top', 'right', 'bottom', 'left'].map((side) => name + '-' + side).filter(known);\n return sides.map((side) => side.slice(name.length + 1) + ' ' + s.getPropertyValue(side).trim()).join(' / ');\n };\n result.props = Object.fromEntries(props.filter(known).map((name) => [name, valueOf(name)]));\n result.rootFontSize = parseFloat(tree.style(el.ownerDocument.documentElement).fontSize) || 16;\n result.unknownProps = props.filter((name) => !known(name));\n }\n if (why && !why.startsWith('--')) {\n if (!CSS.supports(why, 'inherit')) result.unknownWhy = true;\n else {\n const probe = el.ownerDocument.createElement('div').style;\n probe.setProperty(why, 'inherit');\n if (probe.length > 1) {\n result.whyLonghands = Array.from(probe);\n result.whyComputed = s.getPropertyValue(why).trim();\n }\n const inactive = ((el, names) => {\n const view = el.ownerDocument.defaultView;\n const found = [];\n let budget = 20000;\n const unmet = (rule) => {\n if (rule.media && rule.media.mediaText && !view.matchMedia(rule.media.mediaText).matches) return '@media ' + rule.media.mediaText;\n if (typeof CSSSupportsRule !== 'undefined' && rule instanceof CSSSupportsRule && !CSS.supports(rule.conditionText)) return '@supports ' + rule.conditionText;\n return null;\n };\n const sets = (rule) => names.map((name) => rule.style.getPropertyValue(name).trim()).find((value) => value !== '');\n const matches = (selector) => {\n try { return el.matches(selector); } catch (e) { return false; }\n };\n const visit = (rules, condition) => {\n for (const rule of Array.from(rules)) {\n if (budget-- <= 0 || found.length >= 5) return;\n if (typeof CSSImportRule !== 'undefined' && rule instanceof CSSImportRule) {\n try { if (rule.styleSheet) visit(rule.styleSheet.cssRules, condition); } catch (e) { continue; }\n continue;\n }\n const value = condition && rule.style && rule.selectorText ? sets(rule) : undefined;\n if (value !== undefined && matches(rule.selectorText)) found.push({ selector: rule.selectorText, condition: condition, value: value });\n if (rule.cssRules) visit(rule.cssRules, condition || unmet(rule));\n }\n };\n for (const scope of new Set([el.getRootNode(), el.ownerDocument])) {\n for (const sheet of [...Array.from(scope.styleSheets || []), ...Array.from(scope.adoptedStyleSheets || [])]) {\n try { visit(sheet.cssRules, null); } catch (e) { continue; }\n }\n }\n return found;\n})(el, [why].concat(result.whyLonghands || []));\n if (inactive.length > 0) result.whyInactive = inactive;\n }\n }\n return Object.assign(result,\n ((el, tree) => {\n const parent = tree.layoutParent(el);\n if (!parent || tree.style(el).position === 'fixed' || !tree.rendered(el)) return {};\n const r = el.getBoundingClientRect();\n const p = parent.getBoundingClientRect();\n const ps = tree.style(parent);\n const zoom = parent.currentCSSZoom || 1;\n const edge = (side) => (parseFloat(ps['border-' + side + '-width']) + parseFloat(ps['padding-' + side])) * zoom;\n const inParent = {\n left: r.left - (p.left + edge('left')),\n top: r.top - (p.top + edge('top')),\n right: (p.right - edge('right')) - r.right,\n bottom: (p.bottom - edge('bottom')) - r.bottom\n };\n const inFlow = (n) => {\n if (n === el || !tree.rendered(n) || /^(absolute|fixed)$/.test(tree.style(n).position)) return false;\n const box = n.getBoundingClientRect();\n return box.width > 0 || box.height > 0;\n };\n const siblings = tree.children(parent);\n const at = siblings.indexOf(el);\n const before = siblings.slice(0, Math.max(at, 0)).reverse().find(inFlow);\n const after = at < 0 ? undefined : siblings.slice(at + 1).find(inFlow);\n const gaps = {};\n for (const sibling of [before, after]) {\n if (!sibling) continue;\n const s = sibling.getBoundingClientRect();\n if (s.bottom <= r.top + 0.5) gaps.top = r.top - s.bottom;\n else if (s.top >= r.bottom - 0.5) gaps.bottom = s.top - r.bottom;\n else if (s.right <= r.left + 0.5) gaps.left = r.left - s.right;\n else if (s.left >= r.right - 0.5) gaps.right = s.left - r.right;\n }\n return { inParent: inParent, siblings: gaps };\n})(el, tree),\n ((el, tree, view) => {\n const backgrounds = [];\n let opacity = 1;\n for (let n = el; n && backgrounds.length < 60; n = tree.flatParent(n)) {\n const s = tree.style(n);\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 }\n const doc = el.ownerDocument;\n const meta = doc.querySelector('meta[name=\"color-scheme\"]');\n const rootScheme = tree.style(doc.documentElement).colorScheme;\n const scheme = rootScheme && rootScheme !== 'normal' ? rootScheme : (meta && meta.content) || '';\n const prefersDark = view.matchMedia('(prefers-color-scheme: dark)').matches;\n const canvasDark = /dark/.test(scheme) && (!/light/.test(scheme) || prefersDark);\n return { backgrounds: backgrounds, canvasDark: canvasDark, opacity: opacity };\n})(textual ? textFrom : el, tree, view),\n textual ? { paintRisks: ((el, tree, textParent) => {\n const risks = [];\n const chain = [];\n for (let n = el; n && chain.length < 60; n = tree.flatParent(n)) chain.push(n);\n for (const n of chain) {\n const s = tree.style(n);\n if (s.mixBlendMode !== 'normal') risks.push('mix-blend-mode ' + s.mixBlendMode + ' on ' + tree.label(n));\n if (s.filter !== 'none') risks.push('filter on ' + tree.label(n));\n }\n const textNode = Array.from(textParent.childNodes).find((c) => c.nodeType === 3 && c.data.trim() !== '');\n const range = el.ownerDocument.createRange();\n if (textNode) range.selectNodeContents(textNode);\n const box = tree.style(el).display === 'contents' ? tree.layoutParent(el) : el;\n const rect = (textNode && range.getClientRects()[0]) || (box && box.getClientRects()[0]);\n const view = el.ownerDocument.defaultView;\n if (!rect) return risks;\n const x = rect.left + rect.width / 2;\n const y = rect.top + rect.height / 2;\n if (x < 0 || y < 0 || x >= view.innerWidth || y >= view.innerHeight) return risks;\n const root = el.getRootNode();\n const hits = (root.elementsFromPoint ? root : el.ownerDocument).elementsFromPoint(x, y);\n const at = hits.findIndex((h) => chain.includes(h));\n if (at < 0) return risks;\n const clear = (color) => color === 'transparent' || /^rgba\\(.*,\\s*0\\)$/.test(color) || /\\/\\s*0\\)$/.test(color);\n const opaque = (n) => {\n const s = tree.style(n);\n return /^rgb\\(/.test(s.backgroundColor) && s.backgroundImage === 'none' && Number(s.opacity) === 1;\n };\n const paints = (n) => {\n const s = tree.style(n);\n return /^(canvas|video|img|iframe|embed|object|svg)$/.test(n.localName) || s.backgroundImage !== 'none' || !clear(s.backgroundColor);\n };\n let behind = null;\n for (const h of hits.slice(at)) {\n if (chain.includes(h)) {\n if (opaque(h)) break;\n } else if (paints(h)) {\n behind = h;\n break;\n }\n }\n if (behind) risks.push(tree.label(behind) + ' behind');\n const onTop = hits.slice(0, at).find((h) => !el.contains(h) && paints(h));\n if (onTop) risks.push(tree.label(onTop) + ' on top');\n return risks;\n})(textFrom, tree, holder ? holder.font : el) } : {},\n ((el, tree, textOf, depth) => {\n let budget = 400;\n let skipped = 0;\n const walk = (kids, level, origin) => {\n const shown = kids.filter((k) => tree.rendered(k) || k.localName === 'slot');\n const nodes = [];\n for (const k of shown) {\n if (budget <= 0) { skipped++; continue; }\n budget--;\n nodes.push(nodeOf(k, level, origin));\n }\n return { nodes: nodes, hidden: kids.length - shown.length };\n };\n const nodeOf = (n, level, origin) => {\n const r = n.getBoundingClientRect();\n const node = { label: tree.label(n), x: r.left - origin.left, y: r.top - origin.top, w: r.width, h: r.height, display: tree.style(n).display };\n if (tree.via.has(n)) node.via = tree.via.get(n);\n if (n.parentNode && n.parentNode.nodeType === 11 && n.parentNode.host) node.shadow = true;\n if (n.localName === 'slot') {\n node.text = n.assignedNodes({ flatten: true }).map((t) => t.textContent).join(' ').replace(/\\s+/g, ' ').trim().slice(0, 60);\n return node;\n }\n const kids = tree.children(n);\n if (!tree.container(n)) node.text = textOf(n).slice(0, 60);\n if (level >= depth) {\n node.childCount = kids.filter(tree.rendered).length;\n return node;\n }\n const sub = walk(kids, level + 1, r);\n node.children = sub.nodes;\n node.hidden = sub.hidden;\n return node;\n };\n if (depth <= 0) return { hiddenChildren: 0, treeSkipped: 0 };\n const top = walk(tree.children(el), 1, el.getBoundingClientRect());\n return { tree: top.nodes, hiddenChildren: top.hidden, treeSkipped: skipped };\n})(el, tree, textOf, depth));\n}";
|
|
196
|
+
export declare const INSPECT_PAGE_JS: string;
|
|
170
197
|
/**
|
|
171
198
|
* Page-side function run on the element (`this`) that returns another
|
|
172
199
|
* element whose styles CDP reads: `"parent"`, the parent that lays it out
|
|
@@ -176,7 +203,7 @@ export declare const INSPECT_PAGE_JS = "function (depth, props, why) {\n const
|
|
|
176
203
|
* `"fontHolder"`, the parent of that text's nodes (for the rendered font: a
|
|
177
204
|
* slot's text nodes belong to the host's light DOM). Null when there is none.
|
|
178
205
|
*/
|
|
179
|
-
export declare const RELATED_NODE_JS
|
|
206
|
+
export declare const RELATED_NODE_JS: string;
|
|
180
207
|
/** Where a custom property is set, as {@link VARIABLE_SETTERS_JS} finds it */
|
|
181
208
|
export interface VariableSetter {
|
|
182
209
|
/** Selector of the rule, or `@keyframes name` */
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* instead of the light DOM, slots replaced by what is assigned to them, and
|
|
13
13
|
* `display: contents` wrappers by their children.
|
|
14
14
|
*/
|
|
15
|
-
import { ELEMENT_CONTEXT_JS, ELEMENT_DESCRIPTION_JS, MASKED_VALUE, SENSITIVE_FIELD_JS, } from './elementInfo.js';
|
|
15
|
+
import { COMPOSED_JS, ELEMENT_CONTEXT_JS, ELEMENT_DESCRIPTION_JS, FLAT_TEXT_JS, LABEL_CLASSES_JS, MASKED_VALUE, SENSITIVE_FIELD_JS, } from './elementInfo.js';
|
|
16
16
|
/** Elements walked for the child tree at most (the rest are counted) */
|
|
17
17
|
export const TREE_NODE_CAP = 400;
|
|
18
18
|
/**
|
|
@@ -20,7 +20,7 @@ export const TREE_NODE_CAP = 400;
|
|
|
20
20
|
* parent, the parent that lays it out (skipping `display: contents`), its
|
|
21
21
|
* element children in the flat tree and whether an element is rendered.
|
|
22
22
|
*/
|
|
23
|
-
const FLAT_TREE_JS = `(view) => {
|
|
23
|
+
export const FLAT_TREE_JS = `(view) => {
|
|
24
24
|
const style = (n, pseudo) => view.getComputedStyle(n, pseudo);
|
|
25
25
|
const skipped = /^(script|style|template|link|meta|noscript|title|head|base)$/;
|
|
26
26
|
const flatParent = (n) => n.assignedSlot || n.parentElement || (n.parentNode && n.parentNode.host) || null;
|
|
@@ -30,7 +30,8 @@ const FLAT_TREE_JS = `(view) => {
|
|
|
30
30
|
return p;
|
|
31
31
|
};
|
|
32
32
|
const via = new WeakMap();
|
|
33
|
-
const
|
|
33
|
+
const firstClass = (n) => (${LABEL_CLASSES_JS})(n)[0];
|
|
34
|
+
const label = (n) => n.localName + (n.id ? '#' + n.id : firstClass(n) ? '.' + firstClass(n) : '');
|
|
34
35
|
const slotText = (slot) => slot.assignedNodes({ flatten: true }).some((t) => t.nodeType === 3 && t.data.trim() !== '');
|
|
35
36
|
const children = (n) => {
|
|
36
37
|
const out = [];
|
|
@@ -60,14 +61,15 @@ const FLAT_TREE_JS = `(view) => {
|
|
|
60
61
|
* of the visible text in its flat tree (characters, whitespace aside), and
|
|
61
62
|
* the element whose child text nodes those are (for the rendered font).
|
|
62
63
|
* Text slotted into a shadow root is drawn with the slot's styles, and text
|
|
63
|
-
* of an element with a shadow root that is not slotted is not drawn
|
|
64
|
-
*
|
|
64
|
+
* of an element with a shadow root that is not slotted is not drawn, nor is
|
|
65
|
+
* text inside a fully transparent descendant (a measuring copy under a
|
|
66
|
+
* mask). Null when there is no visible text.
|
|
65
67
|
*/
|
|
66
68
|
const TEXT_HOLDER_JS = `(el, tree) => {
|
|
67
69
|
const counts = new Map();
|
|
68
70
|
const parents = new Map();
|
|
69
71
|
let budget = 2000;
|
|
70
|
-
const shown = (n) => tree.style(n).display === 'contents' || tree.rendered(n);
|
|
72
|
+
const shown = (n) => Number(tree.style(n).opacity) !== 0 && (tree.style(n).display === 'contents' || tree.rendered(n));
|
|
71
73
|
const nodesOf = (n) => {
|
|
72
74
|
if (n.localName !== 'slot') return Array.from((n.shadowRoot || n).childNodes);
|
|
73
75
|
const assigned = n.assignedNodes({ flatten: true });
|
|
@@ -132,13 +134,16 @@ const TEXT_CONTROL_JS = `(el) => {
|
|
|
132
134
|
/**
|
|
133
135
|
* Page-side text of an element: a form control's value or a select's chosen
|
|
134
136
|
* options (masked for secrets, {@link SENSITIVE_FIELD_JS}), else its
|
|
135
|
-
* `innerText` (`textContent` for SVG
|
|
137
|
+
* `innerText` (`textContent` for SVG; the flat tree's text for web
|
|
138
|
+
* components and slots, {@link FLAT_TEXT_JS}), masked when CSS hides it as a secret
|
|
136
139
|
* (`-webkit-text-security`), whitespace collapsed. A container holding a
|
|
137
140
|
* select or textarea gets its visible text without theirs, so a form's text
|
|
138
141
|
* never carries a chosen option or typed text.
|
|
139
142
|
*/
|
|
140
143
|
const TEXT_JS = `(el) => {
|
|
141
144
|
const isSensitive = ${SENSITIVE_FIELD_JS};
|
|
145
|
+
const composed = ${COMPOSED_JS};
|
|
146
|
+
const flatText = ${FLAT_TEXT_JS};
|
|
142
147
|
const clean = (text) => String(text || '').replace(/\\s+/g, ' ').trim().slice(0, 200);
|
|
143
148
|
if (el.localName === 'input' && el.type === 'hidden') return '';
|
|
144
149
|
if (el.localName === 'input' || el.localName === 'textarea') {
|
|
@@ -160,7 +165,8 @@ const TEXT_JS = `(el) => {
|
|
|
160
165
|
return parts.join(' ');
|
|
161
166
|
};
|
|
162
167
|
const holdsControls = Boolean(el.querySelector && el.querySelector('select, textarea'));
|
|
163
|
-
const
|
|
168
|
+
const ownText = () => (typeof el.innerText === 'string' ? el.innerText : el.textContent);
|
|
169
|
+
const raw = holdsControls ? outsideControls() : composed(el) ? flatText(el, 2000) : ownText();
|
|
164
170
|
const text = clean(raw);
|
|
165
171
|
const view = el.ownerDocument.defaultView;
|
|
166
172
|
const security = view ? view.getComputedStyle(el).getPropertyValue('-webkit-text-security') : '';
|
|
@@ -232,6 +238,39 @@ const BACKGROUNDS_JS = `(el, tree, view) => {
|
|
|
232
238
|
const canvasDark = /dark/.test(scheme) && (!/light/.test(scheme) || prefersDark);
|
|
233
239
|
return { backgrounds: backgrounds, canvasDark: canvasDark, opacity: opacity };
|
|
234
240
|
}`;
|
|
241
|
+
/**
|
|
242
|
+
* Page-side hit test that also sees elements with `pointer-events: none`
|
|
243
|
+
* (hero images, decorative layers and blended duplicates often have it, and
|
|
244
|
+
* `elementsFromPoint` skips them): for the duration of `test`, a constructed
|
|
245
|
+
* stylesheet makes every element of the given roots (documents and shadow
|
|
246
|
+
* roots) hittable. It is added and removed within one script turn, so nothing
|
|
247
|
+
* is painted with it and no DOM mutation is recorded. A root that already has
|
|
248
|
+
* it (an outer call covering a whole page walk) is left as it is, so nested
|
|
249
|
+
* calls cost no extra style recalculation.
|
|
250
|
+
*/
|
|
251
|
+
export const HIT_EVERYTHING_JS = `(roots, test) => {
|
|
252
|
+
const added = [];
|
|
253
|
+
for (const root of roots) {
|
|
254
|
+
if (!root || !root.adoptedStyleSheets) continue;
|
|
255
|
+
const view = (root.ownerDocument || root).defaultView;
|
|
256
|
+
try {
|
|
257
|
+
if (!view.__bdgHitSheet) {
|
|
258
|
+
view.__bdgHitSheet = new view.CSSStyleSheet();
|
|
259
|
+
view.__bdgHitSheet.replaceSync('* { pointer-events: auto !important; }');
|
|
260
|
+
}
|
|
261
|
+
if (root.adoptedStyleSheets.includes(view.__bdgHitSheet)) continue;
|
|
262
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, view.__bdgHitSheet];
|
|
263
|
+
added.push([root, view.__bdgHitSheet]);
|
|
264
|
+
} catch (e) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
return test();
|
|
270
|
+
} finally {
|
|
271
|
+
for (const [root, sheet] of added) root.adoptedStyleSheets = root.adoptedStyleSheets.filter((s) => s !== sheet);
|
|
272
|
+
}
|
|
273
|
+
}`;
|
|
235
274
|
/**
|
|
236
275
|
* Page-side reasons the contrast of an element's text is approximate: a
|
|
237
276
|
* blend mode or filter on it or an ancestor, and, hit-testing the middle of
|
|
@@ -241,7 +280,7 @@ const BACKGROUNDS_JS = `(el, tree, view) => {
|
|
|
241
280
|
* paints (a canvas, video, image, background) and the nearest one on top of
|
|
242
281
|
* it that paints (an overlay).
|
|
243
282
|
*/
|
|
244
|
-
const PAINT_RISKS_JS = `(el, tree, textParent) => {
|
|
283
|
+
export const PAINT_RISKS_JS = `(el, tree, textParent) => {
|
|
245
284
|
const risks = [];
|
|
246
285
|
const chain = [];
|
|
247
286
|
for (let n = el; n && chain.length < 60; n = tree.flatParent(n)) chain.push(n);
|
|
@@ -261,7 +300,7 @@ const PAINT_RISKS_JS = `(el, tree, textParent) => {
|
|
|
261
300
|
const y = rect.top + rect.height / 2;
|
|
262
301
|
if (x < 0 || y < 0 || x >= view.innerWidth || y >= view.innerHeight) return risks;
|
|
263
302
|
const root = el.getRootNode();
|
|
264
|
-
const hits = (root.elementsFromPoint ? root : el.ownerDocument).elementsFromPoint(x, y);
|
|
303
|
+
const hits = (${HIT_EVERYTHING_JS})([el.ownerDocument, root], () => (root.elementsFromPoint ? root : el.ownerDocument).elementsFromPoint(x, y));
|
|
265
304
|
const at = hits.findIndex((h) => chain.includes(h));
|
|
266
305
|
if (at < 0) return risks;
|
|
267
306
|
const clear = (color) => color === 'transparent' || /^rgba\\(.*,\\s*0\\)$/.test(color) || /\\/\\s*0\\)$/.test(color);
|
|
@@ -15,6 +15,7 @@ import { throwIfInvalidSelector } from './formFillHelpers/shared.js';
|
|
|
15
15
|
import { findFrameOwner, placeInOwnerFrame } from './frameLayout.js';
|
|
16
16
|
import { measureFrameMapping } from './frameScopedConnection.js';
|
|
17
17
|
import { DEEP_QUERY_JS, missingElementError, selectorArgsJS } from './targetNode.js';
|
|
18
|
+
import { evaluateInBdgWorld, resolveNodeInBdgWorld } from '../page/bdgWorld.js';
|
|
18
19
|
import { createLogger } from '../../ui/logging/index.js';
|
|
19
20
|
import { getErrorMessage } from '../../utils/errors.js';
|
|
20
21
|
import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
@@ -247,10 +248,10 @@ export async function inspectLayout(cdp, params) {
|
|
|
247
248
|
*/
|
|
248
249
|
async function measureSelector(cdp, params) {
|
|
249
250
|
const found = `(${DEEP_QUERY_JS})(${selectorArgsJS(params.selector)})`;
|
|
250
|
-
const response =
|
|
251
|
+
const response = await evaluateInBdgWorld(cdp, {
|
|
251
252
|
expression: `(${LAYOUT_JS})(${found}, ${params.index ?? null}, ${LAYOUT_ELEMENT_LIMIT})`,
|
|
252
253
|
returnByValue: true,
|
|
253
|
-
})
|
|
254
|
+
});
|
|
254
255
|
if (response.exceptionDetails) {
|
|
255
256
|
throwIfInvalidSelector(response.exceptionDetails, params.selector);
|
|
256
257
|
const err = operationFailedError('measure the elements', response.exceptionDetails.text);
|
|
@@ -269,9 +270,7 @@ async function measureSelector(cdp, params) {
|
|
|
269
270
|
* viewport through the iframe's own measurements.
|
|
270
271
|
*/
|
|
271
272
|
async function measureNode(cdp, backendNodeId, objectGroup) {
|
|
272
|
-
const resolved = (await cdp
|
|
273
|
-
.send('DOM.resolveNode', { backendNodeId, objectGroup })
|
|
274
|
-
.catch((error) => {
|
|
273
|
+
const resolved = (await resolveNodeInBdgWorld(cdp, { backendNodeId, objectGroup }).catch((error) => {
|
|
275
274
|
log.debug(`Node ${backendNodeId} not resolved: ${getErrorMessage(error)}`);
|
|
276
275
|
return {};
|
|
277
276
|
}));
|
|
@@ -326,14 +325,16 @@ function roundPage(page) {
|
|
|
326
325
|
};
|
|
327
326
|
}
|
|
328
327
|
/**
|
|
329
|
-
* Layout of one element.
|
|
328
|
+
* Layout of one element. A `display: contents` element (no box of its own)
|
|
329
|
+
* is placed by the box around its shown children.
|
|
330
330
|
*
|
|
331
331
|
* @param raw - Element measurements
|
|
332
332
|
* @param page - Page layout (viewport size and scroll position)
|
|
333
333
|
* @returns Element layout; `coveredBy` only for elements that are in view
|
|
334
334
|
*/
|
|
335
335
|
function elementLayout(raw, page) {
|
|
336
|
-
const {
|
|
336
|
+
const { content } = raw.geometry;
|
|
337
|
+
const rect = raw.computed.display === 'contents' && content ? content : raw.geometry.rect;
|
|
337
338
|
const placement = classifyViewportPosition(raw.geometry, page.viewport);
|
|
338
339
|
const inView = placement.inViewport === 'visible' || placement.inViewport === 'partly';
|
|
339
340
|
const text = textPreview(raw.text);
|
|
@@ -354,6 +355,7 @@ function elementLayout(raw, page) {
|
|
|
354
355
|
...(inView && raw.cover && { coveredBy: raw.cover.element }),
|
|
355
356
|
...(inView && raw.cover?.transparent && { coverTransparent: true }),
|
|
356
357
|
...(raw.geometry.invisible && { invisible: raw.geometry.invisible }),
|
|
358
|
+
...(raw.geometry.masked && { masked: raw.geometry.masked }),
|
|
357
359
|
...(raw.geometry.inert && { inert: true }),
|
|
358
360
|
...(raw.geometry.fixed && { fixed: true }),
|
|
359
361
|
computed: raw.computed,
|
|
@@ -18,7 +18,7 @@ import type { FillResult, ClickResult } from '../../ipc/protocol/domTypes.js';
|
|
|
18
18
|
* whitespace collapsed. A value cut to the field's maxlength sets
|
|
19
19
|
* `truncatedTo`; a password mismatch gives masked values and both lengths.
|
|
20
20
|
*/
|
|
21
|
-
export declare const FILL_VALUE_MISMATCH_JS = "(field, expected) => {\n const type = (field.type || '').toLowerCase();\n const actual = field.isContentEditable\n ? (field.textContent || '')\n : type === 'checkbox' || type === 'radio'\n ? (field.checked ? 'checked' : 'unchecked')\n : field.localName === 'select' && field.multiple\n ?
|
|
21
|
+
export declare const FILL_VALUE_MISMATCH_JS = "(field, expected) => {\n const type = (field.type || '').toLowerCase();\n const actual = field.isContentEditable\n ? (field.textContent || '')\n : type === 'checkbox' || type === 'radio'\n ? (field.checked ? 'checked' : 'unchecked')\n : field.localName === 'select' && field.multiple\n ? ((list) => {\n const items = [];\n for (let i = 0; i < list.length; i++) items[items.length] = list[i];\n return items;\n})(field.selectedOptions).map((o) => o.value).join(', ')\n : String(field.value);\n const time = (text) => text.trim().replace(' ', 'T').replace(/(\\d\\d:\\d\\d):00(\\.0+)?$/, '$1');\n const normalize = (text) => {\n text = String(text);\n if (field.isContentEditable) return text.replace(/\\s+/g, ' ').trim();\n if (field.localName === 'textarea') return text.replace(/\\r\\n?/g, '\\n');\n if (type === 'color') return text.trim().toLowerCase();\n if (type === 'email') return text.trim();\n if (type === 'time' || type === 'datetime-local') return time(text);\n return text;\n };\n const numeric = (type === 'number' || type === 'range') && actual.trim() !== '' && String(expected).trim() !== '';\n const same = numeric ? Number(actual) === Number(expected) : normalize(actual) === normalize(expected);\n if (same) return undefined;\n if (type === 'password') {\n const mask = (text) => (text === '' ? '' : '********');\n return { expected: mask(expected), actual: mask(actual), expectedLength: expected.length, actualLength: actual.length };\n }\n const cut = field.maxLength > 0 && actual.length === field.maxLength && expected.length > actual.length &&\n expected.startsWith(actual);\n return cut ? { expected: expected, actual: actual, truncatedTo: actual.length } : { expected: expected, actual: actual };\n}";
|
|
22
22
|
/**
|
|
23
23
|
* Page-side list of the other text-like fields of a field's form (of the
|
|
24
24
|
* document when it has none), with their values: taken before a fill, so
|
|
@@ -41,6 +41,13 @@ export declare const FILL_REFUSAL_JS: string;
|
|
|
41
41
|
* field itself or its shadow root has none or several.
|
|
42
42
|
*/
|
|
43
43
|
export declare const SHADOW_FIELD_JS = "(el) => {\n if (!el.shadowRoot || /^(input|textarea|select)$/.test(el.localName) || el.isContentEditable) return null;\n const fields = el.shadowRoot.querySelectorAll('input:not([type=\"hidden\"]), textarea, select, [contenteditable]:not([contenteditable=\"false\"])');\n return fields.length === 1 ? fields[0] : null;\n}";
|
|
44
|
+
/**
|
|
45
|
+
* Page-side: fire a bubbling, non-cancelable event of a type on an element,
|
|
46
|
+
* like `new Event(type, { bubbles: true })`. The event comes from
|
|
47
|
+
* `document.createEvent`, which keeps working on pages that replace the
|
|
48
|
+
* `Event` constructor (MooTools 1.2 does).
|
|
49
|
+
*/
|
|
50
|
+
export declare const FIRE_EVENT_JS = "(target, type) => {\n const event = target.ownerDocument.createEvent('Event');\n event.initEvent(type, true, false);\n return target.dispatchEvent(event);\n}";
|
|
44
51
|
/**
|
|
45
52
|
* JavaScript function to fill an input element in a React-compatible way.
|
|
46
53
|
*
|
|
@@ -67,7 +74,7 @@ export declare const REACT_FILL_SCRIPT: string;
|
|
|
67
74
|
* name (`input#first-name`, `input[name="first"]`) or else its
|
|
68
75
|
* description, or undefined. Passwords are never searched for.
|
|
69
76
|
*/
|
|
70
|
-
export declare const MOVED_VALUE_JS
|
|
77
|
+
export declare const MOVED_VALUE_JS: string;
|
|
71
78
|
/**
|
|
72
79
|
* Page script reading back the field the last fill left in
|
|
73
80
|
* `window.__bdgFillCheck`, after one macrotask (so frameworks that render
|
|
@@ -78,11 +85,11 @@ export declare const MOVED_VALUE_JS = "(field, expected, before) => {\n const w
|
|
|
78
85
|
* value matches, nothing was left (the page navigated) or the field left the
|
|
79
86
|
* page.
|
|
80
87
|
*/
|
|
81
|
-
export declare const FILL_READ_BACK_SCRIPT
|
|
88
|
+
export declare const FILL_READ_BACK_SCRIPT: string;
|
|
82
89
|
/**
|
|
83
90
|
* JavaScript function to locate an element for clicking.
|
|
84
91
|
*
|
|
85
|
-
* Scrolls the element into view and reports its center point and whether it
|
|
92
|
+
* Scrolls the element into view (reporting how far the page moved) and reports its center point and whether it
|
|
86
93
|
* is the topmost element there (`hittable`). It does not click; see
|
|
87
94
|
* `clickElement`, which dispatches real mouse events or falls back to
|
|
88
95
|
* `el.click()`.
|
|
@@ -98,6 +105,9 @@ export declare const FILL_READ_BACK_SCRIPT = "(() => {\n const check = window._
|
|
|
98
105
|
* a press, a probe (`window.__bdgPressProbe`) records whether the press
|
|
99
106
|
* reaches the element, and otherwise which element it landed on: a browser
|
|
100
107
|
* dialog or bubble can swallow input while the page looks normal.
|
|
108
|
+
* Slotted text hit-tests as its shadow host, so an element in a shadow root
|
|
109
|
+
* that shows slotted content (a button labelled through a `<slot>`) is
|
|
110
|
+
* topmost where its host is hit.
|
|
101
111
|
*/
|
|
102
112
|
export declare const CLICK_ELEMENT_SCRIPT: string;
|
|
103
113
|
/**
|