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.
Files changed (222) hide show
  1. package/.claude/skills/bdg/SKILL.md +4 -4
  2. package/README.md +143 -79
  3. package/dist/commands/cdp.d.ts +22 -1
  4. package/dist/commands/cdp.js +100 -43
  5. package/dist/commands/console.d.ts +12 -0
  6. package/dist/commands/console.js +62 -12
  7. package/dist/commands/css.d.ts +13 -0
  8. package/dist/commands/css.js +53 -0
  9. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  10. package/dist/commands/dom/DomElementResolver.js +10 -3
  11. package/dist/commands/dom/a11y.js +3 -2
  12. package/dist/commands/dom/audit.d.ts +14 -0
  13. package/dist/commands/dom/audit.js +87 -0
  14. package/dist/commands/dom/eval.d.ts +3 -2
  15. package/dist/commands/dom/eval.js +11 -5
  16. package/dist/commands/dom/form.js +10 -9
  17. package/dist/commands/dom/formInteraction.js +42 -11
  18. package/dist/commands/dom/get.js +8 -8
  19. package/dist/commands/dom/helpers/index.d.ts +1 -1
  20. package/dist/commands/dom/helpers/index.js +1 -1
  21. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  22. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  23. package/dist/commands/dom/helpers/query.d.ts +27 -3
  24. package/dist/commands/dom/helpers/query.js +152 -64
  25. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  26. package/dist/commands/dom/helpers/screenshot.js +169 -49
  27. package/dist/commands/dom/index.js +7 -2
  28. package/dist/commands/dom/query.d.ts +19 -2
  29. package/dist/commands/dom/query.js +37 -6
  30. package/dist/commands/dom/screenshot.js +12 -7
  31. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  32. package/dist/commands/dom/semanticUtils.js +40 -9
  33. package/dist/commands/dom/wait.js +5 -3
  34. package/dist/commands/helpJson.d.ts +82 -19
  35. package/dist/commands/helpJson.js +112 -41
  36. package/dist/commands/helpTopic.d.ts +16 -1
  37. package/dist/commands/helpTopic.js +59 -1
  38. package/dist/commands/installSkill.d.ts +15 -5
  39. package/dist/commands/installSkill.js +86 -16
  40. package/dist/commands/network/list.js +22 -12
  41. package/dist/commands/optionBehaviors.js +53 -16
  42. package/dist/commands/page.js +7 -4
  43. package/dist/commands/peek.d.ts +7 -0
  44. package/dist/commands/peek.js +65 -23
  45. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  46. package/dist/commands/shared/daemonErrorHandler.js +20 -9
  47. package/dist/commands/shared/dataFetcher.d.ts +12 -4
  48. package/dist/commands/shared/dataFetcher.js +12 -4
  49. package/dist/commands/shared/followMode.d.ts +9 -1
  50. package/dist/commands/shared/followMode.js +22 -4
  51. package/dist/commands/shared/optionTypes.d.ts +9 -2
  52. package/dist/commands/shared/outputFile.js +6 -1
  53. package/dist/commands/start.d.ts +20 -5
  54. package/dist/commands/start.js +84 -23
  55. package/dist/commands/stop.d.ts +11 -0
  56. package/dist/commands/stop.js +24 -1
  57. package/dist/commands/tail.d.ts +7 -1
  58. package/dist/commands/tail.js +13 -62
  59. package/dist/commands.js +2 -0
  60. package/dist/connection/cdp.d.ts +7 -0
  61. package/dist/connection/cdp.js +9 -0
  62. package/dist/connection/launcher.js +3 -2
  63. package/dist/daemon/SessionController.js +6 -1
  64. package/dist/daemon/launcher.d.ts +3 -2
  65. package/dist/daemon/launcher.js +47 -3
  66. package/dist/daemon/session/Session.d.ts +4 -1
  67. package/dist/daemon/session/Session.js +33 -2
  68. package/dist/daemon/session/TelemetryStore.d.ts +8 -1
  69. package/dist/daemon/session/TelemetryStore.js +13 -1
  70. package/dist/daemon/session/commandRegistry.js +36 -14
  71. package/dist/daemon/session/interactions.d.ts +2 -1
  72. package/dist/daemon/session/interactions.js +13 -1
  73. package/dist/daemon/session/plugins.js +19 -53
  74. package/dist/daemon/session/teardown.js +1 -1
  75. package/dist/daemon.js +9234 -7222
  76. package/dist/errors/messages.d.ts +88 -15
  77. package/dist/errors/messages.js +177 -27
  78. package/dist/index.js +19322 -13961
  79. package/dist/ipc/client.d.ts +22 -2
  80. package/dist/ipc/client.js +34 -5
  81. package/dist/ipc/protocol/auditTypes.d.ts +135 -0
  82. package/dist/ipc/protocol/auditTypes.js +6 -0
  83. package/dist/ipc/protocol/commands.d.ts +35 -0
  84. package/dist/ipc/protocol/commands.js +2 -0
  85. package/dist/ipc/protocol/domTypes.d.ts +16 -0
  86. package/dist/ipc/protocol/inspectTypes.d.ts +73 -8
  87. package/dist/ipc/session/types.d.ts +2 -0
  88. package/dist/runtime/css/search.d.ts +39 -0
  89. package/dist/runtime/css/search.js +122 -0
  90. package/dist/runtime/dom/actionEffects.d.ts +9 -2
  91. package/dist/runtime/dom/actionEffects.js +30 -14
  92. package/dist/runtime/dom/audit.d.ts +19 -0
  93. package/dist/runtime/dom/audit.js +37 -0
  94. package/dist/runtime/dom/auditModel.d.ts +45 -0
  95. package/dist/runtime/dom/auditModel.js +220 -0
  96. package/dist/runtime/dom/auditScripts.d.ts +113 -0
  97. package/dist/runtime/dom/auditScripts.js +148 -0
  98. package/dist/runtime/dom/elementGeometry.d.ts +16 -3
  99. package/dist/runtime/dom/elementGeometry.js +49 -10
  100. package/dist/runtime/dom/elementInfo.d.ts +74 -17
  101. package/dist/runtime/dom/elementInfo.js +187 -34
  102. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  103. package/dist/runtime/dom/evalHelpers.js +67 -7
  104. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  105. package/dist/runtime/dom/formDiscovery.js +20 -3
  106. package/dist/runtime/dom/formFillHelpers/fill.js +8 -12
  107. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  108. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  109. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  110. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  111. package/dist/runtime/dom/frameLayout.js +1 -0
  112. package/dist/runtime/dom/inspect.d.ts +7 -0
  113. package/dist/runtime/dom/inspect.js +92 -28
  114. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  115. package/dist/runtime/dom/inspectAllStyles.js +90 -7
  116. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  117. package/dist/runtime/dom/inspectCascade.js +214 -44
  118. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  119. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  120. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  121. package/dist/runtime/dom/inspectHints.js +125 -9
  122. package/dist/runtime/dom/inspectModel.d.ts +5 -1
  123. package/dist/runtime/dom/inspectModel.js +37 -10
  124. package/dist/runtime/dom/inspectPaintModel.d.ts +50 -22
  125. package/dist/runtime/dom/inspectPaintModel.js +182 -68
  126. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  127. package/dist/runtime/dom/inspectRules.js +21 -5
  128. package/dist/runtime/dom/inspectScripts.d.ts +112 -12
  129. package/dist/runtime/dom/inspectScripts.js +357 -32
  130. package/dist/runtime/dom/inspectTree.js +10 -2
  131. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  132. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  133. package/dist/runtime/dom/layout.js +40 -16
  134. package/dist/runtime/dom/reactEventHelpers.d.ts +21 -4
  135. package/dist/runtime/dom/reactEventHelpers.js +90 -36
  136. package/dist/runtime/dom/targetNode.d.ts +18 -5
  137. package/dist/runtime/dom/targetNode.js +268 -8
  138. package/dist/runtime/dom/wait.js +2 -1
  139. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  140. package/dist/runtime/page/bdgWorld.js +180 -0
  141. package/dist/runtime/page/emulation.d.ts +13 -4
  142. package/dist/runtime/page/emulation.js +69 -4
  143. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  144. package/dist/runtime/page/replacedBuiltins.js +136 -0
  145. package/dist/runtime/page/userAgent.d.ts +17 -0
  146. package/dist/runtime/page/userAgent.js +57 -0
  147. package/dist/session/QueryCacheManager.d.ts +4 -1
  148. package/dist/session/QueryCacheManager.js +5 -2
  149. package/dist/session/chrome.d.ts +4 -1
  150. package/dist/session/chrome.js +7 -1
  151. package/dist/session/cleanup/staleSession.d.ts +21 -4
  152. package/dist/session/cleanup/staleSession.js +79 -9
  153. package/dist/session/cleanup/userCommands.d.ts +4 -1
  154. package/dist/session/cleanup/userCommands.js +10 -5
  155. package/dist/session/daemonSocket.d.ts +10 -0
  156. package/dist/session/daemonSocket.js +22 -0
  157. package/dist/session/lastSession.d.ts +6 -3
  158. package/dist/session/lastSession.js +11 -5
  159. package/dist/session/paths.d.ts +3 -1
  160. package/dist/session/paths.js +5 -5
  161. package/dist/session/portClaims.js +4 -3
  162. package/dist/session/sessionList.d.ts +13 -5
  163. package/dist/session/sessionList.js +31 -7
  164. package/dist/telemetry/a11y.js +2 -2
  165. package/dist/telemetry/console.d.ts +2 -1
  166. package/dist/telemetry/console.js +30 -21
  167. package/dist/telemetry/pageCrash.d.ts +26 -0
  168. package/dist/telemetry/pageCrash.js +53 -0
  169. package/dist/types.d.ts +20 -0
  170. package/dist/ui/formatters/audit.d.ts +19 -0
  171. package/dist/ui/formatters/audit.js +115 -0
  172. package/dist/ui/formatters/cdp.d.ts +138 -0
  173. package/dist/ui/formatters/cdp.js +131 -0
  174. package/dist/ui/formatters/console/chronological.js +3 -1
  175. package/dist/ui/formatters/console/follow.d.ts +2 -1
  176. package/dist/ui/formatters/console/follow.js +2 -2
  177. package/dist/ui/formatters/console/json.d.ts +2 -2
  178. package/dist/ui/formatters/console/json.js +11 -5
  179. package/dist/ui/formatters/console/shared.d.ts +30 -0
  180. package/dist/ui/formatters/console/shared.js +16 -0
  181. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  182. package/dist/ui/formatters/console/summarize.js +40 -9
  183. package/dist/ui/formatters/console.d.ts +2 -1
  184. package/dist/ui/formatters/console.js +7 -5
  185. package/dist/ui/formatters/details.js +3 -1
  186. package/dist/ui/formatters/dom.d.ts +2 -2
  187. package/dist/ui/formatters/dom.js +10 -8
  188. package/dist/ui/formatters/helpFormatters.js +1 -1
  189. package/dist/ui/formatters/inspect.js +50 -17
  190. package/dist/ui/formatters/installSkill.d.ts +9 -1
  191. package/dist/ui/formatters/installSkill.js +32 -6
  192. package/dist/ui/formatters/layout.js +2 -1
  193. package/dist/ui/formatters/networkList.d.ts +1 -1
  194. package/dist/ui/formatters/networkList.js +1 -2
  195. package/dist/ui/formatters/preview.d.ts +2 -0
  196. package/dist/ui/formatters/preview.js +17 -7
  197. package/dist/ui/formatters/sessions.d.ts +2 -2
  198. package/dist/ui/formatters/sessions.js +9 -2
  199. package/dist/ui/formatters/status.js +1 -1
  200. package/dist/ui/logging/logger.d.ts +1 -1
  201. package/dist/ui/messages/commands.d.ts +168 -11
  202. package/dist/ui/messages/commands.js +245 -18
  203. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  204. package/dist/ui/messages/consoleMessages.js +32 -0
  205. package/dist/ui/messages/preview.d.ts +12 -0
  206. package/dist/ui/messages/preview.js +18 -2
  207. package/dist/ui/messages/session.d.ts +13 -2
  208. package/dist/ui/messages/session.js +22 -3
  209. package/dist/utils/cssValues.js +36 -4
  210. package/dist/utils/decisionTrees.js +0 -5
  211. package/dist/utils/directories.d.ts +34 -0
  212. package/dist/utils/directories.js +88 -0
  213. package/dist/utils/display.d.ts +16 -0
  214. package/dist/utils/display.js +42 -0
  215. package/dist/utils/exitCodes.d.ts +1 -0
  216. package/dist/utils/exitCodes.js +6 -0
  217. package/dist/utils/process.d.ts +12 -0
  218. package/dist/utils/process.js +25 -0
  219. package/dist/utils/suggestions.d.ts +4 -2
  220. package/dist/utils/suggestions.js +7 -5
  221. package/dist/utils/taskMappings.js +1 -1
  222. 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 computed = values.every((value) => value === first.computed)
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 value = declaration.via ? (declaration.written ?? declaration.value) : declaration.value;
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 the topmost element at the center of the largest visible part of the
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.elementFromPoint === 'function' ? root : node.ownerDocument;
127
- const hit = scope.elementFromPoint(x, y);
128
- if (!hit || encloses(node, hit) || sameTarget(node, hit)) return null;
129
- if (!encloses(hit, node)) return hit;
130
- return scope.elementsFromPoint(x, y).indexOf(node) > 0 ? hit : null;
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
- return clearColor(s.backgroundColor) && s.backgroundImage === 'none' && s.boxShadow === 'none' && !ownText;
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: paintsNothing(cover) } : null;
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 = (await cdp.send('Runtime.evaluate', {
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 { rect } = raw.geometry;
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 ? Array.from(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}";
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 = "(field, expected, before) => {\n const wanted = String(expected).trim();\n if (!before || wanted.length < 2 || (field.type || '').toLowerCase() === 'password') return undefined;\n const other = Array.from(before.keys()).find((f) =>\n String(f.value).trim() === wanted && String(before.get(f)).trim() !== wanted);\n if (!other) return undefined;\n if (other.id) return other.localName + '#' + other.id;\n return other.name ? other.localName + '[name=\"' + other.name + '\"]' : ((node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : ''))(other);\n}";
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 = "(() => {\n const check = window.__bdgFillCheck;\n delete window.__bdgFillCheck;\n if (!check) return null;\n const readBack = () => {\n if (!check.el.isConnected) return null;\n const mismatch = ((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 ? Array.from(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})(check.el, check.expected);\n if (!mismatch) return null;\n const movedTo = ((field, expected, before) => {\n const wanted = String(expected).trim();\n if (!before || wanted.length < 2 || (field.type || '').toLowerCase() === 'password') return undefined;\n const other = Array.from(before.keys()).find((f) =>\n String(f.value).trim() === wanted && String(before.get(f)).trim() !== wanted);\n if (!other) return undefined;\n if (other.id) return other.localName + '#' + other.id;\n return other.name ? other.localName + '[name=\"' + other.name + '\"]' : ((node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : ''))(other);\n})(check.el, check.expected, check.before);\n return movedTo ? Object.assign(mismatch, { movedTo: movedTo }) : mismatch;\n };\n return new Promise((resolve) => {\n const channel = new MessageChannel();\n channel.port1.onmessage = () => {\n channel.port1.close();\n resolve(readBack());\n };\n channel.port2.postMessage(null);\n });\n})()";
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
- ? Array.from(field.selectedOptions).map((o) => o.value).join(', ')
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
- if (el.disabled || el.matches(':disabled')) {
85
- return refuse('disabled', el.hasAttribute('disabled') ? 'disabled attribute' : el.closest('fieldset[disabled]') ? 'inside a disabled <fieldset>' : '');
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 = Array.from(el.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.dispatchEvent(new Event('input', { bubbles: true }));
247
- el.dispatchEvent(new Event('change', { bubbles: true }));
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 = Array.from(el.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.dispatchEvent(new Event('input', { bubbles: true }));
266
- el.dispatchEvent(new Event('change', { bubbles: true }));
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.dispatchEvent(new Event('input', { bubbles: true }));
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.dispatchEvent(new Event('input', { bubbles: true }));
349
- el.dispatchEvent(new Event('change', { bubbles: true }));
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
- ? Array.from(el.selectedOptions).map((o) => o.value).join(', ')
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.disabled || el.matches(':disabled'))) {
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
- el.scrollIntoView({ behavior: 'auto', block: 'center' });
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
- for (const [fx, fy] of hasSize ? fractions : [[0.5, 0.5]]) {
573
- const candidate = toTopPage(area.left + area.width * fx, area.top + area.height * fy);
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 = (node) => node.tagName.toLowerCase() + (node.id ? '#' + node.id : '') +
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
- if (path.includes(el)) probe.reached = true;
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
- ['pointerdown', 'mousedown'].forEach((type) => view.addEventListener(type, markReached, true));
607
- probe.stop = () => ['pointerdown', 'mousedown'].forEach((type) => view.removeEventListener(type, markReached, true));
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
  `;