browser-debugger-cli 0.11.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 +143 -79
- 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/css.d.ts +13 -0
- package/dist/commands/css.js +53 -0
- 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/audit.d.ts +14 -0
- package/dist/commands/dom/audit.js +87 -0
- 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 +42 -11
- 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/keyAttributes.d.ts +3 -2
- package/dist/commands/dom/helpers/keyAttributes.js +6 -4
- 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.d.ts +1 -0
- package/dist/commands/dom/helpers/screenshot.js +169 -49
- package/dist/commands/dom/index.js +7 -2
- package/dist/commands/dom/query.d.ts +19 -2
- package/dist/commands/dom/query.js +37 -6
- package/dist/commands/dom/screenshot.js +12 -7
- package/dist/commands/dom/semanticUtils.d.ts +3 -2
- package/dist/commands/dom/semanticUtils.js +40 -9
- package/dist/commands/dom/wait.js +5 -3
- package/dist/commands/helpJson.d.ts +82 -19
- package/dist/commands/helpJson.js +112 -41
- 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 +53 -16
- package/dist/commands/page.js +7 -4
- package/dist/commands/peek.d.ts +7 -0
- package/dist/commands/peek.js +65 -23
- package/dist/commands/shared/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 +9 -2
- package/dist/commands/shared/outputFile.js +6 -1
- package/dist/commands/start.d.ts +20 -5
- package/dist/commands/start.js +84 -23
- package/dist/commands/stop.d.ts +11 -0
- package/dist/commands/stop.js +24 -1
- package/dist/commands/tail.d.ts +7 -1
- package/dist/commands/tail.js +13 -62
- package/dist/commands.js +2 -0
- 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 +36 -14
- package/dist/daemon/session/interactions.d.ts +2 -1
- package/dist/daemon/session/interactions.js +13 -1
- package/dist/daemon/session/plugins.js +19 -53
- package/dist/daemon/session/teardown.js +1 -1
- package/dist/daemon.js +9234 -7222
- package/dist/errors/messages.d.ts +88 -15
- package/dist/errors/messages.js +177 -27
- package/dist/index.js +19322 -13961
- package/dist/ipc/client.d.ts +22 -2
- package/dist/ipc/client.js +34 -5
- package/dist/ipc/protocol/auditTypes.d.ts +135 -0
- package/dist/ipc/protocol/auditTypes.js +6 -0
- package/dist/ipc/protocol/commands.d.ts +35 -0
- package/dist/ipc/protocol/commands.js +2 -0
- package/dist/ipc/protocol/domTypes.d.ts +16 -0
- package/dist/ipc/protocol/inspectTypes.d.ts +73 -8
- package/dist/ipc/session/types.d.ts +2 -0
- package/dist/runtime/css/search.d.ts +39 -0
- package/dist/runtime/css/search.js +122 -0
- package/dist/runtime/dom/actionEffects.d.ts +9 -2
- package/dist/runtime/dom/actionEffects.js +30 -14
- package/dist/runtime/dom/audit.d.ts +19 -0
- package/dist/runtime/dom/audit.js +37 -0
- package/dist/runtime/dom/auditModel.d.ts +45 -0
- package/dist/runtime/dom/auditModel.js +220 -0
- package/dist/runtime/dom/auditScripts.d.ts +113 -0
- package/dist/runtime/dom/auditScripts.js +148 -0
- package/dist/runtime/dom/elementGeometry.d.ts +16 -3
- package/dist/runtime/dom/elementGeometry.js +49 -10
- package/dist/runtime/dom/elementInfo.d.ts +74 -17
- package/dist/runtime/dom/elementInfo.js +187 -34
- 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 +8 -12
- 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.d.ts +7 -0
- package/dist/runtime/dom/inspect.js +92 -28
- package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
- package/dist/runtime/dom/inspectAllStyles.js +90 -7
- package/dist/runtime/dom/inspectCascade.d.ts +19 -2
- package/dist/runtime/dom/inspectCascade.js +214 -44
- package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
- package/dist/runtime/dom/inspectCascadeModel.js +108 -34
- package/dist/runtime/dom/inspectHints.d.ts +26 -3
- package/dist/runtime/dom/inspectHints.js +125 -9
- package/dist/runtime/dom/inspectModel.d.ts +5 -1
- package/dist/runtime/dom/inspectModel.js +37 -10
- package/dist/runtime/dom/inspectPaintModel.d.ts +50 -22
- package/dist/runtime/dom/inspectPaintModel.js +182 -68
- package/dist/runtime/dom/inspectRules.d.ts +19 -0
- package/dist/runtime/dom/inspectRules.js +21 -5
- package/dist/runtime/dom/inspectScripts.d.ts +112 -12
- package/dist/runtime/dom/inspectScripts.js +357 -32
- package/dist/runtime/dom/inspectTree.js +10 -2
- package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
- package/dist/runtime/dom/inspectWhyModel.js +52 -10
- package/dist/runtime/dom/layout.js +40 -16
- package/dist/runtime/dom/reactEventHelpers.d.ts +21 -4
- package/dist/runtime/dom/reactEventHelpers.js +90 -36
- 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/emulation.d.ts +13 -4
- package/dist/runtime/page/emulation.js +69 -4
- package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
- package/dist/runtime/page/replacedBuiltins.js +136 -0
- package/dist/runtime/page/userAgent.d.ts +17 -0
- package/dist/runtime/page/userAgent.js +57 -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 +20 -0
- package/dist/ui/formatters/audit.d.ts +19 -0
- package/dist/ui/formatters/audit.js +115 -0
- 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 +2 -2
- package/dist/ui/formatters/dom.js +10 -8
- package/dist/ui/formatters/helpFormatters.js +1 -1
- package/dist/ui/formatters/inspect.js +50 -17
- 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/formatters/status.js +1 -1
- package/dist/ui/logging/logger.d.ts +1 -1
- package/dist/ui/messages/commands.d.ts +168 -11
- package/dist/ui/messages/commands.js +245 -18
- package/dist/ui/messages/consoleMessages.d.ts +24 -0
- package/dist/ui/messages/consoleMessages.js +32 -0
- package/dist/ui/messages/preview.d.ts +12 -0
- package/dist/ui/messages/preview.js +18 -2
- package/dist/ui/messages/session.d.ts +13 -2
- package/dist/ui/messages/session.js +22 -3
- package/dist/utils/cssValues.js +36 -4
- package/dist/utils/decisionTrees.js +0 -5
- 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/dist/utils/suggestions.d.ts +4 -2
- package/dist/utils/suggestions.js +7 -5
- package/dist/utils/taskMappings.js +1 -1
- package/package.json +3 -2
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* declaration sets all its longhands (listing every declaration it beats on
|
|
6
6
|
* any of them), else one per longhand.
|
|
7
7
|
*/
|
|
8
|
-
import { normalizeProperty } from './inspectAllStyles.js';
|
|
9
|
-
import { resolveCascade } from './inspectCascade.js';
|
|
8
|
+
import { collapsedValue, normalizeProperty } from './inspectAllStyles.js';
|
|
9
|
+
import { isInherited, resolveCascade, ruleField, } from './inspectCascade.js';
|
|
10
10
|
import { substituteVariables, unsetVariables, usedVariables, } from './inspectVariables.js';
|
|
11
11
|
/**
|
|
12
12
|
* `--why` for a property.
|
|
@@ -16,8 +16,8 @@ import { substituteVariables, unsetVariables, usedVariables, } from './inspectVa
|
|
|
16
16
|
* @param input - Matched rules, computed style and the label function
|
|
17
17
|
* @returns One entry, or one per longhand when they differ
|
|
18
18
|
*/
|
|
19
|
-
export function buildWhy(group, cascade, input) {
|
|
20
|
-
const whys = group.longhands.map((longhand) => whyOf(longhand, cascade.get(longhand), input));
|
|
19
|
+
export function buildWhy(group, cascade, input, hints = []) {
|
|
20
|
+
const whys = group.longhands.map((longhand) => whyOf(longhand, cascade.get(longhand), input, hints));
|
|
21
21
|
const [first] = whys;
|
|
22
22
|
if (!first || group.shorthand === undefined)
|
|
23
23
|
return whys;
|
|
@@ -25,9 +25,13 @@ export function buildWhy(group, cascade, input) {
|
|
|
25
25
|
if (!whys.every((why) => JSON.stringify(why.chain[0]) === winner))
|
|
26
26
|
return whys;
|
|
27
27
|
const values = whys.map((why) => why.computed);
|
|
28
|
-
const
|
|
28
|
+
const joined = values.every((value) => value === first.computed)
|
|
29
29
|
? first.computed
|
|
30
30
|
: values.join(' ');
|
|
31
|
+
const fromPage = input.whyComputed
|
|
32
|
+
? normalizeProperty(group.shorthand, input.whyComputed)
|
|
33
|
+
: undefined;
|
|
34
|
+
const computed = collapsedValue(group.shorthand, input.style) ?? fromPage ?? joined;
|
|
31
35
|
const beaten = whys.flatMap((why) => why.chain.slice(1));
|
|
32
36
|
const overridden = [...new Map(beaten.map((entry) => [JSON.stringify(entry), entry])).values()];
|
|
33
37
|
const chain = [...first.chain.slice(0, 1), ...overridden];
|
|
@@ -39,14 +43,14 @@ export function buildWhy(group, cascade, input) {
|
|
|
39
43
|
* @param property - Longhand
|
|
40
44
|
* @param resolution - Its resolution
|
|
41
45
|
* @param input - Matched rules, computed style and the label function
|
|
46
|
+
* @param hints - Hints, for a winner that has no effect
|
|
42
47
|
* @returns Why
|
|
43
48
|
*/
|
|
44
|
-
function whyOf(property, resolution, input) {
|
|
49
|
+
function whyOf(property, resolution, input, hints) {
|
|
45
50
|
const winner = resolution?.winner;
|
|
51
|
+
const status = winner?.ancestor !== undefined ? 'inherited' : 'applied';
|
|
46
52
|
const chain = [
|
|
47
|
-
...(winner
|
|
48
|
-
? [whyEntry(winner, winner.ancestor !== undefined ? 'inherited' : 'applied', input)]
|
|
49
|
-
: []),
|
|
53
|
+
...(winner ? [withNote(whyEntry(winner, status, input), winner, property, hints)] : []),
|
|
50
54
|
...(resolution?.overridden ?? []).map((d) => whyEntry(d, 'overridden', input)),
|
|
51
55
|
];
|
|
52
56
|
const variables = winner ? variableSources(winner, input) : [];
|
|
@@ -68,7 +72,10 @@ function whyOf(property, resolution, input) {
|
|
|
68
72
|
* @returns Entry
|
|
69
73
|
*/
|
|
70
74
|
function whyEntry(declaration, status, input) {
|
|
71
|
-
const
|
|
75
|
+
const written = declaration.via ? (declaration.written ?? declaration.value) : declaration.value;
|
|
76
|
+
const value = written === ''
|
|
77
|
+
? `${input.style[declaration.property] ?? ''} (set by the browser)`.trim()
|
|
78
|
+
: written;
|
|
72
79
|
const unset = value.includes('var(') ? unsetVariables(value, input.style) : [];
|
|
73
80
|
const resolved = value.includes('var(') ? resolvedValue(declaration, value, input) : value;
|
|
74
81
|
const { specificity, layer, condition } = declaration.source;
|
|
@@ -77,6 +84,7 @@ function whyEntry(declaration, status, input) {
|
|
|
77
84
|
...(declaration.via && { via: declaration.via }),
|
|
78
85
|
...(unset.length > 0 ? { unset } : resolved !== value && { resolved }),
|
|
79
86
|
source: input.label(declaration),
|
|
87
|
+
...ruleField(declaration),
|
|
80
88
|
...(specificity && { specificity }),
|
|
81
89
|
status,
|
|
82
90
|
...(declaration.important && { important: true }),
|
|
@@ -84,6 +92,40 @@ function whyEntry(declaration, status, input) {
|
|
|
84
92
|
...(condition && { condition }),
|
|
85
93
|
};
|
|
86
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* The winner's entry with why it changes nothing: the hint that says it has
|
|
97
|
+
* no effect, or what applies instead of an invalid `var()`.
|
|
98
|
+
*
|
|
99
|
+
* @param entry - Winner's entry
|
|
100
|
+
* @param winner - Winning declaration
|
|
101
|
+
* @param property - Longhand
|
|
102
|
+
* @param hints - Hints
|
|
103
|
+
* @returns Entry, with `note` when there is one
|
|
104
|
+
*/
|
|
105
|
+
function withNote(entry, winner, property, hints) {
|
|
106
|
+
if (entry.unset) {
|
|
107
|
+
const fallback = isInherited(property) ? 'the inherited value' : 'the initial value';
|
|
108
|
+
return { ...entry, note: `falls back to ${fallback}` };
|
|
109
|
+
}
|
|
110
|
+
const inactive = hints.find((hint) => hint.kind === 'inactive' &&
|
|
111
|
+
sameDeclaration(hint.declaration, winner) &&
|
|
112
|
+
(hint.only?.includes(property) ?? true));
|
|
113
|
+
return inactive ? { ...entry, note: `no effect: ${inactive.reason}` } : entry;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Whether two longhand declarations come from the same declaration as written.
|
|
117
|
+
*
|
|
118
|
+
* @param a - Declaration
|
|
119
|
+
* @param b - Declaration
|
|
120
|
+
* @returns True for the same place and property
|
|
121
|
+
*/
|
|
122
|
+
function sameDeclaration(a, b) {
|
|
123
|
+
return (a.source.kind === b.source.kind &&
|
|
124
|
+
a.source.styleSheetId === b.source.styleSheetId &&
|
|
125
|
+
a.source.line === b.source.line &&
|
|
126
|
+
a.source.column === b.source.column &&
|
|
127
|
+
(a.via ?? a.property) === (b.via ?? b.property));
|
|
128
|
+
}
|
|
87
129
|
/** How many levels of custom properties set from others are followed */
|
|
88
130
|
const MAX_VARIABLE_DEPTH = 5;
|
|
89
131
|
/**
|
|
@@ -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';
|
|
@@ -81,7 +82,7 @@ const SAME_CLICK_TARGET_JS = `(node, hit) => {
|
|
|
81
82
|
/**
|
|
82
83
|
* Page function: layout of the matches in `found` (all up to `limit`, or the
|
|
83
84
|
* one at `index`) and of the top-level page. An element covers another when
|
|
84
|
-
* it is
|
|
85
|
+
* it is painted above it at the center of the largest visible part of the
|
|
85
86
|
* other's boxes (a wrapped link has one per line; the strip where overlay
|
|
86
87
|
* scrollbars show is avoided when possible, {@link CLEAR_OF_SCROLLBAR_JS}) and does not lie inside it,
|
|
87
88
|
* nor part of the element's own click target ({@link SAME_CLICK_TARGET_JS}: the link, button
|
|
@@ -91,7 +92,12 @@ const SAME_CLICK_TARGET_JS = `(node, hit) => {
|
|
|
91
92
|
* `z-index`), as `dom click` finds: the element is in the hit-test stack below
|
|
92
93
|
* the ancestor. Hit-testing goes up through the iframes, so an overlay over
|
|
93
94
|
* an iframe covers the elements in it. Elements hit-testing skips
|
|
94
|
-
* (`pointer-events: none`, also through an iframe) get no cover.
|
|
95
|
+
* (`pointer-events: none`, also through an iframe) get no cover. The cover
|
|
96
|
+
* named is the first element above that paints there (a sticky header's
|
|
97
|
+
* background, not the transparent logo on it; inside a shadow host, what its
|
|
98
|
+
* shadow root paints), else the topmost one, marked transparent; named by
|
|
99
|
+
* its outermost fixed or sticky ancestor that does not hold the element (the
|
|
100
|
+
* fixed banner, not a span inside it).
|
|
95
101
|
*/
|
|
96
102
|
const LAYOUT_JS = `function (found, index, limit) {
|
|
97
103
|
const geometryOf = ${ELEMENT_GEOMETRY_JS};
|
|
@@ -121,13 +127,21 @@ const LAYOUT_JS = `function (found, index, limit) {
|
|
|
121
127
|
return false;
|
|
122
128
|
};
|
|
123
129
|
const sameTarget = ${SAME_CLICK_TARGET_JS};
|
|
130
|
+
const paintsAt = (n, x, y) => {
|
|
131
|
+
if (!paintsNothing(n)) return true;
|
|
132
|
+
if (!n.shadowRoot) return false;
|
|
133
|
+
return n.shadowRoot.elementsFromPoint(x, y).some((e) => e !== n && encloses(n, e) && !paintsNothing(e));
|
|
134
|
+
};
|
|
124
135
|
const coverAt = (node, x, y) => {
|
|
125
136
|
const root = node.getRootNode();
|
|
126
|
-
const scope = typeof root.
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
const scope = typeof root.elementsFromPoint === 'function' ? root : node.ownerDocument;
|
|
138
|
+
const stack = scope.elementsFromPoint(x, y);
|
|
139
|
+
const at = stack.indexOf(node);
|
|
140
|
+
const above = (at < 0 ? stack.slice(0, 1) : stack.slice(0, at)).filter((hit) =>
|
|
141
|
+
!encloses(node, hit) && !sameTarget(node, hit) && (at >= 0 || !encloses(hit, node)));
|
|
142
|
+
if (above.length === 0) return null;
|
|
143
|
+
const painter = above.find((hit) => paintsAt(hit, x, y));
|
|
144
|
+
return { element: overlayOf(painter || above[0], node), transparent: !painter };
|
|
131
145
|
};
|
|
132
146
|
const visibleCenter = (el, g) => {
|
|
133
147
|
const bounds = [{ x: 0, y: 0, width: page.viewport.width, height: page.viewport.height }].concat(g.clip ? [g.clip] : []);
|
|
@@ -154,7 +168,15 @@ const LAYOUT_JS = `function (found, index, limit) {
|
|
|
154
168
|
const s = node.ownerDocument.defaultView.getComputedStyle(node);
|
|
155
169
|
const clearColor = (c) => c === 'transparent' || /^rgba\\(.*,\\s*0\\)$/.test(c);
|
|
156
170
|
const ownText = Array.from(node.childNodes).some((n) => n.nodeType === 3 && n.data.trim() !== '');
|
|
157
|
-
|
|
171
|
+
const replaced = /^(img|video|canvas|iframe|embed|object|input|textarea|select)$/.test(node.localName);
|
|
172
|
+
return !replaced && clearColor(s.backgroundColor) && s.backgroundImage === 'none' && s.boxShadow === 'none' && !ownText;
|
|
173
|
+
};
|
|
174
|
+
const overlayOf = (hit, node) => {
|
|
175
|
+
let overlay = hit;
|
|
176
|
+
for (let p = hit.parentElement || (hit.parentNode && hit.parentNode.host); p && p !== p.ownerDocument.body && !encloses(p, node); p = p.parentElement || (p.parentNode && p.parentNode.host)) {
|
|
177
|
+
if (/^(fixed|sticky)$/.test(p.ownerDocument.defaultView.getComputedStyle(p).position)) overlay = p;
|
|
178
|
+
}
|
|
179
|
+
return overlay;
|
|
158
180
|
};
|
|
159
181
|
const hitTestable = (node) => node.ownerDocument.defaultView.getComputedStyle(node).pointerEvents !== 'none';
|
|
160
182
|
const coveredBy = (el, g) => {
|
|
@@ -170,7 +192,7 @@ const LAYOUT_JS = `function (found, index, limit) {
|
|
|
170
192
|
y += offset.y;
|
|
171
193
|
cover = coverAt(view.frameElement, x, y);
|
|
172
194
|
}
|
|
173
|
-
return cover ? { element: describe(cover), transparent:
|
|
195
|
+
return cover ? { element: describe(cover.element), transparent: cover.transparent } : null;
|
|
174
196
|
};
|
|
175
197
|
const elements = picked.map(([i, el]) => {
|
|
176
198
|
const style = el.ownerDocument.defaultView.getComputedStyle(el);
|
|
@@ -226,10 +248,10 @@ export async function inspectLayout(cdp, params) {
|
|
|
226
248
|
*/
|
|
227
249
|
async function measureSelector(cdp, params) {
|
|
228
250
|
const found = `(${DEEP_QUERY_JS})(${selectorArgsJS(params.selector)})`;
|
|
229
|
-
const response =
|
|
251
|
+
const response = await evaluateInBdgWorld(cdp, {
|
|
230
252
|
expression: `(${LAYOUT_JS})(${found}, ${params.index ?? null}, ${LAYOUT_ELEMENT_LIMIT})`,
|
|
231
253
|
returnByValue: true,
|
|
232
|
-
})
|
|
254
|
+
});
|
|
233
255
|
if (response.exceptionDetails) {
|
|
234
256
|
throwIfInvalidSelector(response.exceptionDetails, params.selector);
|
|
235
257
|
const err = operationFailedError('measure the elements', response.exceptionDetails.text);
|
|
@@ -248,9 +270,7 @@ async function measureSelector(cdp, params) {
|
|
|
248
270
|
* viewport through the iframe's own measurements.
|
|
249
271
|
*/
|
|
250
272
|
async function measureNode(cdp, backendNodeId, objectGroup) {
|
|
251
|
-
const resolved = (await cdp
|
|
252
|
-
.send('DOM.resolveNode', { backendNodeId, objectGroup })
|
|
253
|
-
.catch((error) => {
|
|
273
|
+
const resolved = (await resolveNodeInBdgWorld(cdp, { backendNodeId, objectGroup }).catch((error) => {
|
|
254
274
|
log.debug(`Node ${backendNodeId} not resolved: ${getErrorMessage(error)}`);
|
|
255
275
|
return {};
|
|
256
276
|
}));
|
|
@@ -305,14 +325,16 @@ function roundPage(page) {
|
|
|
305
325
|
};
|
|
306
326
|
}
|
|
307
327
|
/**
|
|
308
|
-
* 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.
|
|
309
330
|
*
|
|
310
331
|
* @param raw - Element measurements
|
|
311
332
|
* @param page - Page layout (viewport size and scroll position)
|
|
312
333
|
* @returns Element layout; `coveredBy` only for elements that are in view
|
|
313
334
|
*/
|
|
314
335
|
function elementLayout(raw, page) {
|
|
315
|
-
const {
|
|
336
|
+
const { content } = raw.geometry;
|
|
337
|
+
const rect = raw.computed.display === 'contents' && content ? content : raw.geometry.rect;
|
|
316
338
|
const placement = classifyViewportPosition(raw.geometry, page.viewport);
|
|
317
339
|
const inView = placement.inViewport === 'visible' || placement.inViewport === 'partly';
|
|
318
340
|
const text = textPreview(raw.text);
|
|
@@ -333,7 +355,9 @@ function elementLayout(raw, page) {
|
|
|
333
355
|
...(inView && raw.cover && { coveredBy: raw.cover.element }),
|
|
334
356
|
...(inView && raw.cover?.transparent && { coverTransparent: true }),
|
|
335
357
|
...(raw.geometry.invisible && { invisible: raw.geometry.invisible }),
|
|
358
|
+
...(raw.geometry.masked && { masked: raw.geometry.masked }),
|
|
336
359
|
...(raw.geometry.inert && { inert: true }),
|
|
360
|
+
...(raw.geometry.fixed && { fixed: true }),
|
|
337
361
|
computed: raw.computed,
|
|
338
362
|
};
|
|
339
363
|
}
|
|
@@ -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
|
|
@@ -34,6 +34,20 @@ export declare const FIELD_VALUES_JS = "(field) => {\n const fields = field.for
|
|
|
34
34
|
* fillable kind of element. Messages are {@link FILL_REFUSALS}.
|
|
35
35
|
*/
|
|
36
36
|
export declare const FILL_REFUSAL_JS: string;
|
|
37
|
+
/**
|
|
38
|
+
* Page-side: the one text field, select or editable element in an element's
|
|
39
|
+
* open shadow root (a web component such as `<sl-input>` wrapping a native
|
|
40
|
+
* input), which `dom fill` fills in its place; null when the element is a
|
|
41
|
+
* field itself or its shadow root has none or several.
|
|
42
|
+
*/
|
|
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}";
|
|
37
51
|
/**
|
|
38
52
|
* JavaScript function to fill an input element in a React-compatible way.
|
|
39
53
|
*
|
|
@@ -60,7 +74,7 @@ export declare const REACT_FILL_SCRIPT: string;
|
|
|
60
74
|
* name (`input#first-name`, `input[name="first"]`) or else its
|
|
61
75
|
* description, or undefined. Passwords are never searched for.
|
|
62
76
|
*/
|
|
63
|
-
export declare const MOVED_VALUE_JS
|
|
77
|
+
export declare const MOVED_VALUE_JS: string;
|
|
64
78
|
/**
|
|
65
79
|
* Page script reading back the field the last fill left in
|
|
66
80
|
* `window.__bdgFillCheck`, after one macrotask (so frameworks that render
|
|
@@ -71,11 +85,11 @@ export declare const MOVED_VALUE_JS = "(field, expected, before) => {\n const w
|
|
|
71
85
|
* value matches, nothing was left (the page navigated) or the field left the
|
|
72
86
|
* page.
|
|
73
87
|
*/
|
|
74
|
-
export declare const FILL_READ_BACK_SCRIPT
|
|
88
|
+
export declare const FILL_READ_BACK_SCRIPT: string;
|
|
75
89
|
/**
|
|
76
90
|
* JavaScript function to locate an element for clicking.
|
|
77
91
|
*
|
|
78
|
-
* 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
|
|
79
93
|
* is the topmost element there (`hittable`). It does not click; see
|
|
80
94
|
* `clickElement`, which dispatches real mouse events or falls back to
|
|
81
95
|
* `el.click()`.
|
|
@@ -91,6 +105,9 @@ export declare const FILL_READ_BACK_SCRIPT = "(() => {\n const check = window._
|
|
|
91
105
|
* a press, a probe (`window.__bdgPressProbe`) records whether the press
|
|
92
106
|
* reaches the element, and otherwise which element it landed on: a browser
|
|
93
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.
|
|
94
111
|
*/
|
|
95
112
|
export declare const CLICK_ELEMENT_SCRIPT: string;
|
|
96
113
|
/**
|
|
@@ -7,8 +7,17 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { FILL_REFUSALS, LABEL_WITHOUT_CONTROL, NAME_QUERY_PLACEHOLDER, VIA_LABEL_SUFFIX, } from '../../errors/messages.js';
|
|
9
9
|
import { REVEAL_SNAPSHOT_JS } from './actionEffectsScripts.js';
|
|
10
|
-
import { ELEMENT_DESCRIPTION_JS, ELEMENT_IDENTITY_JS } from './elementInfo.js';
|
|
10
|
+
import { DISABLED_CAUSE_JS, ELEMENT_DESCRIPTION_JS, ELEMENT_IDENTITY_JS, } from './elementInfo.js';
|
|
11
11
|
import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS } from './targetNode.js';
|
|
12
|
+
/**
|
|
13
|
+
* Page-side copy of a list (`select.options`, a NodeList) as an array,
|
|
14
|
+
* without `Array.from`, which pages replace.
|
|
15
|
+
*/
|
|
16
|
+
const LIST_JS = `(list) => {
|
|
17
|
+
const items = [];
|
|
18
|
+
for (let i = 0; i < list.length; i++) items[items.length] = list[i];
|
|
19
|
+
return items;
|
|
20
|
+
}`;
|
|
12
21
|
/**
|
|
13
22
|
* Page-side read-back of a filled field: a mismatch when its value is not
|
|
14
23
|
* what was asked for (the page rejected, reformatted or moved the input),
|
|
@@ -28,7 +37,7 @@ export const FILL_VALUE_MISMATCH_JS = `(field, expected) => {
|
|
|
28
37
|
: type === 'checkbox' || type === 'radio'
|
|
29
38
|
? (field.checked ? 'checked' : 'unchecked')
|
|
30
39
|
: field.localName === 'select' && field.multiple
|
|
31
|
-
?
|
|
40
|
+
? (${LIST_JS})(field.selectedOptions).map((o) => o.value).join(', ')
|
|
32
41
|
: String(field.value);
|
|
33
42
|
const time = (text) => text.trim().replace(' ', 'T').replace(/(\\d\\d:\\d\\d):00(\\.0+)?$/, '$1');
|
|
34
43
|
const normalize = (text) => {
|
|
@@ -81,9 +90,8 @@ export const FILL_REFUSAL_JS = `(el) => {
|
|
|
81
90
|
});
|
|
82
91
|
const tag = el.localName;
|
|
83
92
|
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
93
|
+
const disabled = (${DISABLED_CAUSE_JS})(el);
|
|
94
|
+
if (disabled !== null) return refuse('disabled', disabled);
|
|
87
95
|
return el.readOnly ? refuse('readOnly', 'readonly attribute') : null;
|
|
88
96
|
}
|
|
89
97
|
if (el.isContentEditable) return null;
|
|
@@ -94,8 +102,34 @@ export const FILL_REFUSAL_JS = `(el) => {
|
|
|
94
102
|
if (el.closest('[inert]')) return refuse('inert', 'inside an inert element');
|
|
95
103
|
if (el.getAttribute('aria-readonly') === 'true') return refuse('readOnly', 'aria-readonly="true"');
|
|
96
104
|
if (el.getAttribute('aria-disabled') === 'true') return refuse('disabled', 'aria-disabled="true"');
|
|
105
|
+
if (el.shadowRoot) {
|
|
106
|
+
const fields = el.shadowRoot.querySelectorAll('input:not([type="hidden"]), textarea, select, [contenteditable]:not([contenteditable="false"])').length;
|
|
107
|
+
return refuse('notFillable', '<' + tag + '> has ' + (fields === 0 ? 'no field' : fields + ' fields') + ' in its shadow root; select the one to fill, e.g. its [part~=…] or class');
|
|
108
|
+
}
|
|
97
109
|
return refuse('notFillable', '<' + tag + '> is not an input, textarea, select or contenteditable element');
|
|
98
110
|
}`;
|
|
111
|
+
/**
|
|
112
|
+
* Page-side: the one text field, select or editable element in an element's
|
|
113
|
+
* open shadow root (a web component such as `<sl-input>` wrapping a native
|
|
114
|
+
* input), which `dom fill` fills in its place; null when the element is a
|
|
115
|
+
* field itself or its shadow root has none or several.
|
|
116
|
+
*/
|
|
117
|
+
export const SHADOW_FIELD_JS = `(el) => {
|
|
118
|
+
if (!el.shadowRoot || /^(input|textarea|select)$/.test(el.localName) || el.isContentEditable) return null;
|
|
119
|
+
const fields = el.shadowRoot.querySelectorAll('input:not([type="hidden"]), textarea, select, [contenteditable]:not([contenteditable="false"])');
|
|
120
|
+
return fields.length === 1 ? fields[0] : null;
|
|
121
|
+
}`;
|
|
122
|
+
/**
|
|
123
|
+
* Page-side: fire a bubbling, non-cancelable event of a type on an element,
|
|
124
|
+
* like `new Event(type, { bubbles: true })`. The event comes from
|
|
125
|
+
* `document.createEvent`, which keeps working on pages that replace the
|
|
126
|
+
* `Event` constructor (MooTools 1.2 does).
|
|
127
|
+
*/
|
|
128
|
+
export const FIRE_EVENT_JS = `(target, type) => {
|
|
129
|
+
const event = target.ownerDocument.createEvent('Event');
|
|
130
|
+
event.initEvent(type, true, false);
|
|
131
|
+
return target.dispatchEvent(event);
|
|
132
|
+
}`;
|
|
99
133
|
/**
|
|
100
134
|
* JavaScript function to fill an input element in a React-compatible way.
|
|
101
135
|
*
|
|
@@ -117,6 +151,8 @@ export const FILL_REFUSAL_JS = `(el) => {
|
|
|
117
151
|
export const REACT_FILL_SCRIPT = `
|
|
118
152
|
(function(selector, parts, value, options) {
|
|
119
153
|
const allMatches = (${FIND_ELEMENTS_JS})(selector, parts);
|
|
154
|
+
const fire = ${FIRE_EVENT_JS};
|
|
155
|
+
const listOf = ${LIST_JS};
|
|
120
156
|
const warnings = [];
|
|
121
157
|
let expected = value;
|
|
122
158
|
// Why a user could not reach the field (the value is still set, so scripted
|
|
@@ -199,6 +235,9 @@ export const REACT_FILL_SCRIPT = `
|
|
|
199
235
|
}
|
|
200
236
|
const viaLabel = labelControl ? ${JSON.stringify(VIA_LABEL_SUFFIX)} : '';
|
|
201
237
|
if (labelControl) el = labelControl;
|
|
238
|
+
const shadowField = (${SHADOW_FIELD_JS})(el);
|
|
239
|
+
const viaShadow = shadowField ? ' in the shadow root of <' + el.localName + '>' : '';
|
|
240
|
+
if (shadowField) el = shadowField;
|
|
202
241
|
|
|
203
242
|
const tagName = el.tagName.toLowerCase();
|
|
204
243
|
const inputType = el.type?.toLowerCase();
|
|
@@ -208,7 +247,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
208
247
|
return {
|
|
209
248
|
success: false,
|
|
210
249
|
error: refusal.error,
|
|
211
|
-
elementType: tagName + viaLabel,
|
|
250
|
+
elementType: tagName + viaLabel + viaShadow,
|
|
212
251
|
suggestion: refusal.suggestion,
|
|
213
252
|
unsuitableElement: refusal.kind === 'notFillable'
|
|
214
253
|
};
|
|
@@ -225,7 +264,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
225
264
|
if (tagName === 'select' && el.multiple) {
|
|
226
265
|
// Several options, separated by commas (an option whose value contains a
|
|
227
266
|
// comma matches as a whole); "" selects none
|
|
228
|
-
const options =
|
|
267
|
+
const options = listOf(el.options);
|
|
229
268
|
const whole = options.some((o) => o.value === value || o.text.trim() === value);
|
|
230
269
|
const wanted = whole ? [value] : value.split(',').map((part) => part.trim()).filter(Boolean);
|
|
231
270
|
const chosen = wanted.map((part) =>
|
|
@@ -237,17 +276,17 @@ export const REACT_FILL_SCRIPT = `
|
|
|
237
276
|
success: false,
|
|
238
277
|
error: 'Option not found: ' + missing,
|
|
239
278
|
exitCode: 81,
|
|
240
|
-
elementType: tagName + viaLabel,
|
|
279
|
+
elementType: tagName + viaLabel + viaShadow,
|
|
241
280
|
suggestion: 'Available options: ' + options.slice(0, 10).map((o) => o.value || o.text.trim()).join(', ')
|
|
242
281
|
};
|
|
243
282
|
}
|
|
244
283
|
options.forEach((o) => { o.selected = chosen.includes(o); });
|
|
245
284
|
expected = chosen.map((o) => o.value).join(', ');
|
|
246
|
-
el
|
|
247
|
-
el
|
|
285
|
+
fire(el, 'input');
|
|
286
|
+
fire(el, 'change');
|
|
248
287
|
} else if (tagName === 'select') {
|
|
249
288
|
// Match by option value first, then by visible label
|
|
250
|
-
const options =
|
|
289
|
+
const options = listOf(el.options);
|
|
251
290
|
const option =
|
|
252
291
|
options.find((o) => o.value === value) ||
|
|
253
292
|
options.find((o) => o.text.trim() === value);
|
|
@@ -256,14 +295,14 @@ export const REACT_FILL_SCRIPT = `
|
|
|
256
295
|
success: false,
|
|
257
296
|
error: 'Option not found: ' + value,
|
|
258
297
|
exitCode: 81,
|
|
259
|
-
elementType: tagName + viaLabel,
|
|
298
|
+
elementType: tagName + viaLabel + viaShadow,
|
|
260
299
|
suggestion: 'Available options: ' + options.slice(0, 10).map((o) => o.value || o.text.trim()).join(', ')
|
|
261
300
|
};
|
|
262
301
|
}
|
|
263
302
|
el.value = option.value;
|
|
264
303
|
expected = option.value;
|
|
265
|
-
el
|
|
266
|
-
el
|
|
304
|
+
fire(el, 'input');
|
|
305
|
+
fire(el, 'change');
|
|
267
306
|
} else if (inputType === 'checkbox' || inputType === 'radio') {
|
|
268
307
|
// Toggle through a click, like a user: frameworks (React) track checkable
|
|
269
308
|
// state via click events and revert a programmatic \`checked\` assignment
|
|
@@ -274,7 +313,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
274
313
|
return {
|
|
275
314
|
success: false,
|
|
276
315
|
error: 'Expected true or false for a ' + inputType + ', got "' + value + '"',
|
|
277
|
-
elementType: tagName + viaLabel,
|
|
316
|
+
elementType: tagName + viaLabel + viaShadow,
|
|
278
317
|
inputType: inputType,
|
|
279
318
|
suggestion: 'Use true/false (also yes/no, on/off, 1/0)'
|
|
280
319
|
};
|
|
@@ -284,7 +323,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
284
323
|
return {
|
|
285
324
|
success: false,
|
|
286
325
|
error: 'A radio button cannot be unchecked',
|
|
287
|
-
elementType: tagName + viaLabel,
|
|
326
|
+
elementType: tagName + viaLabel + viaShadow,
|
|
288
327
|
inputType: inputType,
|
|
289
328
|
suggestion: 'Select another option in the same group instead'
|
|
290
329
|
};
|
|
@@ -297,13 +336,13 @@ export const REACT_FILL_SCRIPT = `
|
|
|
297
336
|
return {
|
|
298
337
|
success: false,
|
|
299
338
|
fileInput: true,
|
|
300
|
-
elementType: tagName + viaLabel,
|
|
339
|
+
elementType: tagName + viaLabel + viaShadow,
|
|
301
340
|
inputType: inputType,
|
|
302
341
|
error: 'File input'
|
|
303
342
|
};
|
|
304
343
|
} else if (el.isContentEditable) {
|
|
305
344
|
el.textContent = value;
|
|
306
|
-
el
|
|
345
|
+
fire(el, 'input');
|
|
307
346
|
} else {
|
|
308
347
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
309
348
|
el.ownerDocument.defaultView.HTMLInputElement.prototype,
|
|
@@ -323,7 +362,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
323
362
|
return {
|
|
324
363
|
success: false,
|
|
325
364
|
error: 'Value is ' + value.length + ' characters; the field accepts at most ' + el.maxLength,
|
|
326
|
-
elementType: tagName + viaLabel,
|
|
365
|
+
elementType: tagName + viaLabel + viaShadow,
|
|
327
366
|
inputType: inputType || null,
|
|
328
367
|
suggestion: 'Shorten the value (a user could not type more than maxlength characters)'
|
|
329
368
|
};
|
|
@@ -339,14 +378,14 @@ export const REACT_FILL_SCRIPT = `
|
|
|
339
378
|
return {
|
|
340
379
|
success: false,
|
|
341
380
|
error: rejection.error,
|
|
342
|
-
elementType: tagName + viaLabel,
|
|
381
|
+
elementType: tagName + viaLabel + viaShadow,
|
|
343
382
|
inputType: inputType,
|
|
344
383
|
suggestion: rejection.suggestion
|
|
345
384
|
};
|
|
346
385
|
}
|
|
347
386
|
|
|
348
|
-
el
|
|
349
|
-
el
|
|
387
|
+
fire(el, 'input');
|
|
388
|
+
fire(el, 'change');
|
|
350
389
|
if (el.validity && (el.validity.rangeOverflow || el.validity.rangeUnderflow)) {
|
|
351
390
|
warnings.push('The value is outside the allowed range (' + (el.min || 'no minimum') + ' to ' + (el.max || 'no maximum') + '); the form will not submit until it is fixed');
|
|
352
391
|
}
|
|
@@ -365,10 +404,10 @@ export const REACT_FILL_SCRIPT = `
|
|
|
365
404
|
: inputType === 'password'
|
|
366
405
|
? '********'
|
|
367
406
|
: tagName === 'select' && el.multiple
|
|
368
|
-
?
|
|
407
|
+
? listOf(el.selectedOptions).map((o) => o.value).join(', ')
|
|
369
408
|
: el.value,
|
|
370
409
|
element: (${ELEMENT_IDENTITY_JS})(el),
|
|
371
|
-
elementType: tagName + viaLabel,
|
|
410
|
+
elementType: tagName + viaLabel + viaShadow,
|
|
372
411
|
inputType: inputType || null,
|
|
373
412
|
checked: inputType === 'checkbox' || inputType === 'radio' ? el.checked : undefined,
|
|
374
413
|
matchCount: allMatches.length,
|
|
@@ -427,7 +466,7 @@ export const FILL_READ_BACK_SCRIPT = `(() => {
|
|
|
427
466
|
/**
|
|
428
467
|
* JavaScript function to locate an element for clicking.
|
|
429
468
|
*
|
|
430
|
-
* Scrolls the element into view and reports its center point and whether it
|
|
469
|
+
* Scrolls the element into view (reporting how far the page moved) and reports its center point and whether it
|
|
431
470
|
* is the topmost element there (`hittable`). It does not click; see
|
|
432
471
|
* `clickElement`, which dispatches real mouse events or falls back to
|
|
433
472
|
* `el.click()`.
|
|
@@ -443,6 +482,9 @@ export const FILL_READ_BACK_SCRIPT = `(() => {
|
|
|
443
482
|
* a press, a probe (`window.__bdgPressProbe`) records whether the press
|
|
444
483
|
* reaches the element, and otherwise which element it landed on: a browser
|
|
445
484
|
* dialog or bubble can swallow input while the page looks normal.
|
|
485
|
+
* Slotted text hit-tests as its shadow host, so an element in a shadow root
|
|
486
|
+
* that shows slotted content (a button labelled through a `<slot>`) is
|
|
487
|
+
* topmost where its host is hit.
|
|
446
488
|
*/
|
|
447
489
|
export const CLICK_ELEMENT_SCRIPT = `
|
|
448
490
|
(function(selector, parts, index, action) {
|
|
@@ -522,7 +564,7 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
522
564
|
};
|
|
523
565
|
}
|
|
524
566
|
// Disabled elements still get hover (tooltips often explain why)
|
|
525
|
-
if (action !== 'hover' && (el
|
|
567
|
+
if (action !== 'hover' && (${DISABLED_CAUSE_JS})(el) !== null) {
|
|
526
568
|
return {
|
|
527
569
|
success: false,
|
|
528
570
|
error: 'Element is disabled',
|
|
@@ -531,7 +573,11 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
531
573
|
suggestion: 'A user cannot click a disabled element; enable it first (it may depend on other fields)'
|
|
532
574
|
};
|
|
533
575
|
}
|
|
534
|
-
|
|
576
|
+
let topView = el.ownerDocument.defaultView;
|
|
577
|
+
while (topView.frameElement) topView = topView.parent;
|
|
578
|
+
const scrollBefore = { x: topView.scrollX, y: topView.scrollY };
|
|
579
|
+
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
580
|
+
const scrolledBy = { x: Math.round(topView.scrollX - scrollBefore.x), y: Math.round(topView.scrollY - scrollBefore.y) };
|
|
535
581
|
|
|
536
582
|
// The caller clicks with real mouse events at (x, y) when the element is the
|
|
537
583
|
// topmost thing there; otherwise it falls back to el.click() via this handle.
|
|
@@ -542,10 +588,11 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
542
588
|
window.__bdgClickTarget = el;
|
|
543
589
|
const rect = el.getBoundingClientRect();
|
|
544
590
|
const view = el.ownerDocument.defaultView;
|
|
591
|
+
const showsSlotted = (node) => Array.from(node.querySelectorAll('slot')).some((slot) => slot.assignedNodes().length > 0);
|
|
545
592
|
const hitTest = (node, px, py) => {
|
|
546
593
|
const root = node.getRootNode();
|
|
547
594
|
const hit = (typeof root.elementFromPoint === 'function' ? root : node.ownerDocument).elementFromPoint(px, py);
|
|
548
|
-
return hit !== null && (hit === node || node.contains(hit));
|
|
595
|
+
return hit !== null && (hit === node || node.contains(hit) || (hit === root.host && showsSlotted(node)));
|
|
549
596
|
};
|
|
550
597
|
const toTopPage = (px, py) => {
|
|
551
598
|
let hittable = hitTest(el, px, py);
|
|
@@ -569,16 +616,16 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
569
616
|
const fractions = [[0.5, 0.5], [0.5, 0.25], [0.5, 0.75], [0.25, 0.5], [0.75, 0.5], [0.25, 0.25], [0.75, 0.25], [0.25, 0.75], [0.75, 0.75]];
|
|
570
617
|
const hasSize = rect.width > 0 && rect.height > 0;
|
|
571
618
|
let point = null;
|
|
572
|
-
|
|
573
|
-
|
|
619
|
+
const tried = hasSize ? fractions : [[0.5, 0.5]];
|
|
620
|
+
for (let i = 0; i < tried.length; i++) {
|
|
621
|
+
const candidate = toTopPage(area.left + area.width * tried[i][0], area.top + area.height * tried[i][1]);
|
|
574
622
|
point = point || candidate;
|
|
575
623
|
if (hasSize && candidate.hittable) { point = candidate; break; }
|
|
576
624
|
}
|
|
577
625
|
const x = point.x;
|
|
578
626
|
const y = point.y;
|
|
579
627
|
const hittable = hasSize && point.hittable;
|
|
580
|
-
const describe =
|
|
581
|
-
(node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '');
|
|
628
|
+
const describe = ${ELEMENT_DESCRIPTION_JS};
|
|
582
629
|
const coveredBy = () => {
|
|
583
630
|
const root = el.getRootNode();
|
|
584
631
|
const hit = (typeof root.elementFromPoint === 'function' ? root : el.ownerDocument)
|
|
@@ -600,11 +647,17 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
600
647
|
const probe = { reached: false, landedOn: null };
|
|
601
648
|
const markReached = (event) => {
|
|
602
649
|
const path = event.composedPath();
|
|
603
|
-
|
|
650
|
+
let reached = false;
|
|
651
|
+
for (let i = 0; i < path.length; i++) reached = reached || path[i] === el;
|
|
652
|
+
if (reached) probe.reached = true;
|
|
604
653
|
else if (!probe.landedOn && path[0] && path[0].nodeType === 1) probe.landedOn = describe(path[0]);
|
|
605
654
|
};
|
|
606
|
-
|
|
607
|
-
|
|
655
|
+
view.addEventListener('pointerdown', markReached, true);
|
|
656
|
+
view.addEventListener('mousedown', markReached, true);
|
|
657
|
+
probe.stop = () => {
|
|
658
|
+
view.removeEventListener('pointerdown', markReached, true);
|
|
659
|
+
view.removeEventListener('mousedown', markReached, true);
|
|
660
|
+
};
|
|
608
661
|
window.__bdgPressProbe = probe;
|
|
609
662
|
}
|
|
610
663
|
|
|
@@ -618,7 +671,8 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
618
671
|
x: x,
|
|
619
672
|
y: y,
|
|
620
673
|
hittable: hittable,
|
|
621
|
-
obstruction: obstruction
|
|
674
|
+
obstruction: obstruction,
|
|
675
|
+
scrolledBy: scrolledBy.x !== 0 || scrolledBy.y !== 0 ? scrolledBy : undefined
|
|
622
676
|
};
|
|
623
677
|
})
|
|
624
678
|
`;
|