browser-debugger-cli 0.12.0 → 0.14.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 (224) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +5 -4
  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 +67 -13
  7. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  8. package/dist/commands/dom/DomElementResolver.js +10 -3
  9. package/dist/commands/dom/a11y.d.ts +1 -1
  10. package/dist/commands/dom/a11y.js +23 -22
  11. package/dist/commands/dom/eval.d.ts +4 -2
  12. package/dist/commands/dom/eval.js +31 -7
  13. package/dist/commands/dom/form.js +10 -9
  14. package/dist/commands/dom/formInteraction.js +9 -8
  15. package/dist/commands/dom/get.js +32 -14
  16. package/dist/commands/dom/helpers/index.d.ts +1 -1
  17. package/dist/commands/dom/helpers/index.js +1 -1
  18. package/dist/commands/dom/helpers/query.d.ts +27 -3
  19. package/dist/commands/dom/helpers/query.js +152 -64
  20. package/dist/commands/dom/helpers/screenshot.js +13 -13
  21. package/dist/commands/dom/index.js +10 -3
  22. package/dist/commands/dom/query.d.ts +20 -2
  23. package/dist/commands/dom/query.js +39 -6
  24. package/dist/commands/dom/screenshot.js +3 -1
  25. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  26. package/dist/commands/dom/semanticUtils.js +40 -9
  27. package/dist/commands/helpJson.d.ts +82 -19
  28. package/dist/commands/helpJson.js +112 -41
  29. package/dist/commands/helpTopic.d.ts +16 -1
  30. package/dist/commands/helpTopic.js +59 -1
  31. package/dist/commands/installSkill.d.ts +15 -5
  32. package/dist/commands/installSkill.js +86 -16
  33. package/dist/commands/network/list.js +65 -12
  34. package/dist/commands/optionBehaviors.d.ts +25 -2
  35. package/dist/commands/optionBehaviors.js +81 -46
  36. package/dist/commands/peek.js +3 -0
  37. package/dist/commands/shared/CommandRunner.js +13 -13
  38. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  39. package/dist/commands/shared/daemonErrorHandler.js +21 -10
  40. package/dist/commands/shared/dataFetcher.d.ts +14 -4
  41. package/dist/commands/shared/dataFetcher.js +20 -4
  42. package/dist/commands/shared/followMode.d.ts +9 -1
  43. package/dist/commands/shared/followMode.js +22 -4
  44. package/dist/commands/shared/handleValidationError.js +3 -3
  45. package/dist/commands/shared/optionTypes.d.ts +17 -3
  46. package/dist/commands/shared/outputFile.js +6 -1
  47. package/dist/commands/shared/startHelpers.js +3 -3
  48. package/dist/commands/start.d.ts +7 -5
  49. package/dist/commands/start.js +65 -21
  50. package/dist/commands/stop.d.ts +11 -0
  51. package/dist/commands/stop.js +24 -1
  52. package/dist/commands.js +1 -1
  53. package/dist/connection/cdp.d.ts +7 -0
  54. package/dist/connection/cdp.js +9 -0
  55. package/dist/connection/chromeIdentity.d.ts +8 -2
  56. package/dist/connection/chromeIdentity.js +85 -13
  57. package/dist/connection/launcher.js +3 -2
  58. package/dist/constants.d.ts +29 -1
  59. package/dist/constants.js +35 -1
  60. package/dist/daemon/SessionController.js +8 -1
  61. package/dist/daemon/launcher.d.ts +3 -2
  62. package/dist/daemon/launcher.js +47 -3
  63. package/dist/daemon/session/Session.d.ts +5 -1
  64. package/dist/daemon/session/Session.js +42 -3
  65. package/dist/daemon/session/TelemetryStore.d.ts +15 -1
  66. package/dist/daemon/session/TelemetryStore.js +19 -1
  67. package/dist/daemon/session/commandRegistry.js +52 -18
  68. package/dist/daemon/session/interactions.d.ts +2 -1
  69. package/dist/daemon/session/interactions.js +13 -1
  70. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  71. package/dist/daemon/session/matchedStylesReset.js +46 -0
  72. package/dist/daemon/session/plugins.js +17 -2
  73. package/dist/daemon/session/teardown.js +1 -1
  74. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  75. package/dist/daemon/session/triggeredRequests.js +13 -7
  76. package/dist/daemon.js +2385 -1229
  77. package/dist/errors/messages.d.ts +62 -11
  78. package/dist/errors/messages.js +119 -22
  79. package/dist/index.js +14995 -9866
  80. package/dist/ipc/client.d.ts +18 -2
  81. package/dist/ipc/client.js +26 -5
  82. package/dist/ipc/protocol/auditTypes.d.ts +8 -2
  83. package/dist/ipc/protocol/commands.d.ts +16 -0
  84. package/dist/ipc/protocol/domTypes.d.ts +12 -0
  85. package/dist/ipc/protocol/inspectTypes.d.ts +7 -2
  86. package/dist/ipc/session/types.d.ts +7 -1
  87. package/dist/program.d.ts +14 -0
  88. package/dist/program.js +53 -0
  89. package/dist/runtime/dom/actionEffects.d.ts +5 -1
  90. package/dist/runtime/dom/actionEffects.js +26 -14
  91. package/dist/runtime/dom/audit.js +3 -2
  92. package/dist/runtime/dom/auditModel.js +6 -1
  93. package/dist/runtime/dom/auditScripts.d.ts +9 -3
  94. package/dist/runtime/dom/auditScripts.js +41 -5
  95. package/dist/runtime/dom/elementGeometry.d.ts +33 -3
  96. package/dist/runtime/dom/elementGeometry.js +44 -19
  97. package/dist/runtime/dom/elementInfo.d.ts +76 -18
  98. package/dist/runtime/dom/elementInfo.js +190 -40
  99. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  100. package/dist/runtime/dom/evalHelpers.js +67 -7
  101. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  102. package/dist/runtime/dom/formDiscovery.js +20 -3
  103. package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
  104. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  105. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  106. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  107. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  108. package/dist/runtime/dom/frameLayout.js +1 -0
  109. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  110. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  111. package/dist/runtime/dom/inspect.d.ts +17 -3
  112. package/dist/runtime/dom/inspect.js +45 -32
  113. package/dist/runtime/dom/inspectAllStyles.js +1 -0
  114. package/dist/runtime/dom/inspectHints.d.ts +1 -1
  115. package/dist/runtime/dom/inspectModel.d.ts +5 -4
  116. package/dist/runtime/dom/inspectModel.js +7 -3
  117. package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
  118. package/dist/runtime/dom/inspectPaintModel.js +3 -1
  119. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  120. package/dist/runtime/dom/inspectRules.js +205 -11
  121. package/dist/runtime/dom/inspectScripts.d.ts +29 -2
  122. package/dist/runtime/dom/inspectScripts.js +49 -10
  123. package/dist/runtime/dom/layout.d.ts +0 -2
  124. package/dist/runtime/dom/layout.js +10 -9
  125. package/dist/runtime/dom/reactEventHelpers.d.ts +17 -4
  126. package/dist/runtime/dom/reactEventHelpers.js +71 -28
  127. package/dist/runtime/dom/targetNode.d.ts +27 -10
  128. package/dist/runtime/dom/targetNode.js +283 -16
  129. package/dist/runtime/dom/wait.js +2 -1
  130. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  131. package/dist/runtime/page/bdgWorld.js +180 -0
  132. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  133. package/dist/runtime/page/replacedBuiltins.js +136 -0
  134. package/dist/session/QueryCacheManager.d.ts +4 -1
  135. package/dist/session/QueryCacheManager.js +5 -2
  136. package/dist/session/chrome.d.ts +4 -1
  137. package/dist/session/chrome.js +7 -1
  138. package/dist/session/cleanup/staleSession.d.ts +21 -4
  139. package/dist/session/cleanup/staleSession.js +79 -9
  140. package/dist/session/cleanup/userCommands.d.ts +4 -1
  141. package/dist/session/cleanup/userCommands.js +10 -5
  142. package/dist/session/daemonSocket.d.ts +10 -0
  143. package/dist/session/daemonSocket.js +22 -0
  144. package/dist/session/lastSession.d.ts +6 -3
  145. package/dist/session/lastSession.js +11 -5
  146. package/dist/session/paths.d.ts +3 -1
  147. package/dist/session/paths.js +5 -5
  148. package/dist/session/portClaims.js +4 -3
  149. package/dist/session/sessionList.d.ts +13 -5
  150. package/dist/session/sessionList.js +31 -7
  151. package/dist/telemetry/a11y.d.ts +15 -1
  152. package/dist/telemetry/a11y.js +85 -2
  153. package/dist/telemetry/console.d.ts +2 -1
  154. package/dist/telemetry/console.js +30 -21
  155. package/dist/telemetry/har/builder.js +1 -1
  156. package/dist/telemetry/network.d.ts +13 -16
  157. package/dist/telemetry/network.js +30 -52
  158. package/dist/telemetry/networkRetention.d.ts +83 -0
  159. package/dist/telemetry/networkRetention.js +117 -0
  160. package/dist/telemetry/pageCrash.d.ts +26 -0
  161. package/dist/telemetry/pageCrash.js +53 -0
  162. package/dist/types.d.ts +42 -0
  163. package/dist/ui/OutputBuilder.d.ts +10 -0
  164. package/dist/ui/OutputBuilder.js +12 -0
  165. package/dist/ui/formatters/a11y.d.ts +5 -7
  166. package/dist/ui/formatters/a11y.js +7 -61
  167. package/dist/ui/formatters/audit.js +14 -5
  168. package/dist/ui/formatters/cdp.d.ts +138 -0
  169. package/dist/ui/formatters/cdp.js +131 -0
  170. package/dist/ui/formatters/console/chronological.js +7 -5
  171. package/dist/ui/formatters/console/follow.d.ts +5 -2
  172. package/dist/ui/formatters/console/follow.js +7 -4
  173. package/dist/ui/formatters/console/json.d.ts +4 -7
  174. package/dist/ui/formatters/console/json.js +16 -14
  175. package/dist/ui/formatters/console/shared.d.ts +47 -2
  176. package/dist/ui/formatters/console/shared.js +33 -0
  177. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  178. package/dist/ui/formatters/console/summarize.js +57 -11
  179. package/dist/ui/formatters/console.d.ts +3 -2
  180. package/dist/ui/formatters/console.js +8 -10
  181. package/dist/ui/formatters/details.js +4 -2
  182. package/dist/ui/formatters/dom.d.ts +14 -5
  183. package/dist/ui/formatters/dom.js +30 -13
  184. package/dist/ui/formatters/helpFormatters.js +1 -1
  185. package/dist/ui/formatters/inspect.js +9 -3
  186. package/dist/ui/formatters/installSkill.d.ts +9 -1
  187. package/dist/ui/formatters/installSkill.js +32 -6
  188. package/dist/ui/formatters/layout.js +4 -2
  189. package/dist/ui/formatters/longValues.d.ts +14 -0
  190. package/dist/ui/formatters/longValues.js +23 -0
  191. package/dist/ui/formatters/networkList.d.ts +8 -2
  192. package/dist/ui/formatters/networkList.js +11 -3
  193. package/dist/ui/formatters/preview.d.ts +6 -1
  194. package/dist/ui/formatters/preview.js +67 -15
  195. package/dist/ui/formatters/sessions.d.ts +2 -2
  196. package/dist/ui/formatters/sessions.js +9 -2
  197. package/dist/ui/formatters/status.js +7 -0
  198. package/dist/ui/formatters/triggeredRequests.js +2 -1
  199. package/dist/ui/logging/logger.d.ts +1 -1
  200. package/dist/ui/messages/chrome.d.ts +20 -1
  201. package/dist/ui/messages/chrome.js +29 -3
  202. package/dist/ui/messages/commands.d.ts +153 -12
  203. package/dist/ui/messages/commands.js +198 -15
  204. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  205. package/dist/ui/messages/consoleMessages.js +32 -0
  206. package/dist/ui/messages/networkMessages.d.ts +24 -0
  207. package/dist/ui/messages/networkMessages.js +45 -0
  208. package/dist/ui/messages/preview.d.ts +6 -0
  209. package/dist/ui/messages/preview.js +9 -1
  210. package/dist/ui/messages/session.d.ts +13 -2
  211. package/dist/ui/messages/session.js +22 -3
  212. package/dist/utils/directories.d.ts +34 -0
  213. package/dist/utils/directories.js +88 -0
  214. package/dist/utils/display.d.ts +16 -0
  215. package/dist/utils/display.js +42 -0
  216. package/dist/utils/exitCodes.d.ts +1 -0
  217. package/dist/utils/exitCodes.js +6 -0
  218. package/dist/utils/http.d.ts +9 -2
  219. package/dist/utils/http.js +4 -3
  220. package/dist/utils/process.d.ts +12 -0
  221. package/dist/utils/process.js +25 -0
  222. package/dist/utils/strings.d.ts +19 -0
  223. package/dist/utils/strings.js +16 -0
  224. package/package.json +2 -2
@@ -7,8 +7,18 @@
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 { ANCESTOR_CLIP_JS } from './elementGeometry.js';
11
+ import { DISABLED_CAUSE_JS, ELEMENT_DESCRIPTION_JS, ELEMENT_IDENTITY_JS, } from './elementInfo.js';
11
12
  import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS } from './targetNode.js';
13
+ /**
14
+ * Page-side copy of a list (`select.options`, a NodeList) as an array,
15
+ * without `Array.from`, which pages replace.
16
+ */
17
+ const LIST_JS = `(list) => {
18
+ const items = [];
19
+ for (let i = 0; i < list.length; i++) items[items.length] = list[i];
20
+ return items;
21
+ }`;
12
22
  /**
13
23
  * Page-side read-back of a filled field: a mismatch when its value is not
14
24
  * what was asked for (the page rejected, reformatted or moved the input),
@@ -28,7 +38,7 @@ export const FILL_VALUE_MISMATCH_JS = `(field, expected) => {
28
38
  : type === 'checkbox' || type === 'radio'
29
39
  ? (field.checked ? 'checked' : 'unchecked')
30
40
  : field.localName === 'select' && field.multiple
31
- ? Array.from(field.selectedOptions).map((o) => o.value).join(', ')
41
+ ? (${LIST_JS})(field.selectedOptions).map((o) => o.value).join(', ')
32
42
  : String(field.value);
33
43
  const time = (text) => text.trim().replace(' ', 'T').replace(/(\\d\\d:\\d\\d):00(\\.0+)?$/, '$1');
34
44
  const normalize = (text) => {
@@ -81,9 +91,8 @@ export const FILL_REFUSAL_JS = `(el) => {
81
91
  });
82
92
  const tag = el.localName;
83
93
  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
- }
94
+ const disabled = (${DISABLED_CAUSE_JS})(el);
95
+ if (disabled !== null) return refuse('disabled', disabled);
87
96
  return el.readOnly ? refuse('readOnly', 'readonly attribute') : null;
88
97
  }
89
98
  if (el.isContentEditable) return null;
@@ -111,6 +120,17 @@ export const SHADOW_FIELD_JS = `(el) => {
111
120
  const fields = el.shadowRoot.querySelectorAll('input:not([type="hidden"]), textarea, select, [contenteditable]:not([contenteditable="false"])');
112
121
  return fields.length === 1 ? fields[0] : null;
113
122
  }`;
123
+ /**
124
+ * Page-side: fire a bubbling, non-cancelable event of a type on an element,
125
+ * like `new Event(type, { bubbles: true })`. The event comes from
126
+ * `document.createEvent`, which keeps working on pages that replace the
127
+ * `Event` constructor (MooTools 1.2 does).
128
+ */
129
+ export const FIRE_EVENT_JS = `(target, type) => {
130
+ const event = target.ownerDocument.createEvent('Event');
131
+ event.initEvent(type, true, false);
132
+ return target.dispatchEvent(event);
133
+ }`;
114
134
  /**
115
135
  * JavaScript function to fill an input element in a React-compatible way.
116
136
  *
@@ -132,6 +152,8 @@ export const SHADOW_FIELD_JS = `(el) => {
132
152
  export const REACT_FILL_SCRIPT = `
133
153
  (function(selector, parts, value, options) {
134
154
  const allMatches = (${FIND_ELEMENTS_JS})(selector, parts);
155
+ const fire = ${FIRE_EVENT_JS};
156
+ const listOf = ${LIST_JS};
135
157
  const warnings = [];
136
158
  let expected = value;
137
159
  // Why a user could not reach the field (the value is still set, so scripted
@@ -243,7 +265,7 @@ export const REACT_FILL_SCRIPT = `
243
265
  if (tagName === 'select' && el.multiple) {
244
266
  // Several options, separated by commas (an option whose value contains a
245
267
  // comma matches as a whole); "" selects none
246
- const options = Array.from(el.options);
268
+ const options = listOf(el.options);
247
269
  const whole = options.some((o) => o.value === value || o.text.trim() === value);
248
270
  const wanted = whole ? [value] : value.split(',').map((part) => part.trim()).filter(Boolean);
249
271
  const chosen = wanted.map((part) =>
@@ -261,11 +283,11 @@ export const REACT_FILL_SCRIPT = `
261
283
  }
262
284
  options.forEach((o) => { o.selected = chosen.includes(o); });
263
285
  expected = chosen.map((o) => o.value).join(', ');
264
- el.dispatchEvent(new Event('input', { bubbles: true }));
265
- el.dispatchEvent(new Event('change', { bubbles: true }));
286
+ fire(el, 'input');
287
+ fire(el, 'change');
266
288
  } else if (tagName === 'select') {
267
289
  // Match by option value first, then by visible label
268
- const options = Array.from(el.options);
290
+ const options = listOf(el.options);
269
291
  const option =
270
292
  options.find((o) => o.value === value) ||
271
293
  options.find((o) => o.text.trim() === value);
@@ -280,8 +302,8 @@ export const REACT_FILL_SCRIPT = `
280
302
  }
281
303
  el.value = option.value;
282
304
  expected = option.value;
283
- el.dispatchEvent(new Event('input', { bubbles: true }));
284
- el.dispatchEvent(new Event('change', { bubbles: true }));
305
+ fire(el, 'input');
306
+ fire(el, 'change');
285
307
  } else if (inputType === 'checkbox' || inputType === 'radio') {
286
308
  // Toggle through a click, like a user: frameworks (React) track checkable
287
309
  // state via click events and revert a programmatic \`checked\` assignment
@@ -321,7 +343,7 @@ export const REACT_FILL_SCRIPT = `
321
343
  };
322
344
  } else if (el.isContentEditable) {
323
345
  el.textContent = value;
324
- el.dispatchEvent(new Event('input', { bubbles: true }));
346
+ fire(el, 'input');
325
347
  } else {
326
348
  const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
327
349
  el.ownerDocument.defaultView.HTMLInputElement.prototype,
@@ -363,8 +385,8 @@ export const REACT_FILL_SCRIPT = `
363
385
  };
364
386
  }
365
387
 
366
- el.dispatchEvent(new Event('input', { bubbles: true }));
367
- el.dispatchEvent(new Event('change', { bubbles: true }));
388
+ fire(el, 'input');
389
+ fire(el, 'change');
368
390
  if (el.validity && (el.validity.rangeOverflow || el.validity.rangeUnderflow)) {
369
391
  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');
370
392
  }
@@ -383,7 +405,7 @@ export const REACT_FILL_SCRIPT = `
383
405
  : inputType === 'password'
384
406
  ? '********'
385
407
  : tagName === 'select' && el.multiple
386
- ? Array.from(el.selectedOptions).map((o) => o.value).join(', ')
408
+ ? listOf(el.selectedOptions).map((o) => o.value).join(', ')
387
409
  : el.value,
388
410
  element: (${ELEMENT_IDENTITY_JS})(el),
389
411
  elementType: tagName + viaLabel + viaShadow,
@@ -445,7 +467,7 @@ export const FILL_READ_BACK_SCRIPT = `(() => {
445
467
  /**
446
468
  * JavaScript function to locate an element for clicking.
447
469
  *
448
- * Scrolls the element into view and reports its center point and whether it
470
+ * Scrolls the element into view (reporting how far the page moved) and reports its center point and whether it
449
471
  * is the topmost element there (`hittable`). It does not click; see
450
472
  * `clickElement`, which dispatches real mouse events or falls back to
451
473
  * `el.click()`.
@@ -461,6 +483,12 @@ export const FILL_READ_BACK_SCRIPT = `(() => {
461
483
  * a press, a probe (`window.__bdgPressProbe`) records whether the press
462
484
  * reaches the element, and otherwise which element it landed on: a browser
463
485
  * dialog or bubble can swallow input while the page looks normal.
486
+ * Slotted text hit-tests as its shadow host, so an element in a shadow root
487
+ * that shows slotted content (a button labelled through a `<slot>`) is
488
+ * topmost where its host is hit. An element that is not topmost because an
489
+ * ancestor in the flat tree clips it away (a collapsed
490
+ * `height: 0; overflow: hidden` accordion, {@link ANCESTOR_CLIP_JS}) is
491
+ * reported as hidden by it, not as covered.
464
492
  */
465
493
  export const CLICK_ELEMENT_SCRIPT = `
466
494
  (function(selector, parts, index, action) {
@@ -540,7 +568,7 @@ export const CLICK_ELEMENT_SCRIPT = `
540
568
  };
541
569
  }
542
570
  // Disabled elements still get hover (tooltips often explain why)
543
- if (action !== 'hover' && (el.disabled || el.matches(':disabled'))) {
571
+ if (action !== 'hover' && (${DISABLED_CAUSE_JS})(el) !== null) {
544
572
  return {
545
573
  success: false,
546
574
  error: 'Element is disabled',
@@ -549,7 +577,11 @@ export const CLICK_ELEMENT_SCRIPT = `
549
577
  suggestion: 'A user cannot click a disabled element; enable it first (it may depend on other fields)'
550
578
  };
551
579
  }
552
- el.scrollIntoView({ behavior: 'auto', block: 'center' });
580
+ let topView = el.ownerDocument.defaultView;
581
+ while (topView.frameElement) topView = topView.parent;
582
+ const scrollBefore = { x: topView.scrollX, y: topView.scrollY };
583
+ el.scrollIntoView({ behavior: 'instant', block: 'center' });
584
+ const scrolledBy = { x: Math.round(topView.scrollX - scrollBefore.x), y: Math.round(topView.scrollY - scrollBefore.y) };
553
585
 
554
586
  // The caller clicks with real mouse events at (x, y) when the element is the
555
587
  // topmost thing there; otherwise it falls back to el.click() via this handle.
@@ -560,10 +592,11 @@ export const CLICK_ELEMENT_SCRIPT = `
560
592
  window.__bdgClickTarget = el;
561
593
  const rect = el.getBoundingClientRect();
562
594
  const view = el.ownerDocument.defaultView;
595
+ const showsSlotted = (node) => Array.from(node.querySelectorAll('slot')).some((slot) => slot.assignedNodes().length > 0);
563
596
  const hitTest = (node, px, py) => {
564
597
  const root = node.getRootNode();
565
598
  const hit = (typeof root.elementFromPoint === 'function' ? root : node.ownerDocument).elementFromPoint(px, py);
566
- return hit !== null && (hit === node || node.contains(hit));
599
+ return hit !== null && (hit === node || node.contains(hit) || (hit === root.host && showsSlotted(node)));
567
600
  };
568
601
  const toTopPage = (px, py) => {
569
602
  let hittable = hitTest(el, px, py);
@@ -587,16 +620,16 @@ export const CLICK_ELEMENT_SCRIPT = `
587
620
  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]];
588
621
  const hasSize = rect.width > 0 && rect.height > 0;
589
622
  let point = null;
590
- for (const [fx, fy] of hasSize ? fractions : [[0.5, 0.5]]) {
591
- const candidate = toTopPage(area.left + area.width * fx, area.top + area.height * fy);
623
+ const tried = hasSize ? fractions : [[0.5, 0.5]];
624
+ for (let i = 0; i < tried.length; i++) {
625
+ const candidate = toTopPage(area.left + area.width * tried[i][0], area.top + area.height * tried[i][1]);
592
626
  point = point || candidate;
593
627
  if (hasSize && candidate.hittable) { point = candidate; break; }
594
628
  }
595
629
  const x = point.x;
596
630
  const y = point.y;
597
631
  const hittable = hasSize && point.hittable;
598
- const describe = (node) => node.tagName.toLowerCase() + (node.id ? '#' + node.id : '') +
599
- (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '');
632
+ const describe = ${ELEMENT_DESCRIPTION_JS};
600
633
  const coveredBy = () => {
601
634
  const root = el.getRootNode();
602
635
  const hit = (typeof root.elementFromPoint === 'function' ? root : el.ownerDocument)
@@ -610,7 +643,10 @@ export const CLICK_ELEMENT_SCRIPT = `
610
643
  else if (el.closest('[inert]')) obstruction = 'inert (the page made it non-interactive)';
611
644
  else if (style.pointerEvents === 'none') obstruction = 'not clickable (pointer-events: none)';
612
645
  else if (!hasSize) obstruction = 'zero-size';
613
- else if (!hittable) obstruction = 'covered by another element' + coveredBy();
646
+ else if (!hittable) {
647
+ const collapsed = (${ANCESTOR_CLIP_JS})(el, rect, describe).collapsed;
648
+ obstruction = collapsed ? 'hidden (' + collapsed + ')' : 'covered by another element' + coveredBy();
649
+ }
614
650
 
615
651
  if (action === 'hover') (${REVEAL_SNAPSHOT_JS})(el);
616
652
 
@@ -618,11 +654,17 @@ export const CLICK_ELEMENT_SCRIPT = `
618
654
  const probe = { reached: false, landedOn: null };
619
655
  const markReached = (event) => {
620
656
  const path = event.composedPath();
621
- if (path.includes(el)) probe.reached = true;
657
+ let reached = false;
658
+ for (let i = 0; i < path.length; i++) reached = reached || path[i] === el;
659
+ if (reached) probe.reached = true;
622
660
  else if (!probe.landedOn && path[0] && path[0].nodeType === 1) probe.landedOn = describe(path[0]);
623
661
  };
624
- ['pointerdown', 'mousedown'].forEach((type) => view.addEventListener(type, markReached, true));
625
- probe.stop = () => ['pointerdown', 'mousedown'].forEach((type) => view.removeEventListener(type, markReached, true));
662
+ view.addEventListener('pointerdown', markReached, true);
663
+ view.addEventListener('mousedown', markReached, true);
664
+ probe.stop = () => {
665
+ view.removeEventListener('pointerdown', markReached, true);
666
+ view.removeEventListener('mousedown', markReached, true);
667
+ };
626
668
  window.__bdgPressProbe = probe;
627
669
  }
628
670
 
@@ -636,7 +678,8 @@ export const CLICK_ELEMENT_SCRIPT = `
636
678
  x: x,
637
679
  y: y,
638
680
  hittable: hittable,
639
- obstruction: obstruction
681
+ obstruction: obstruction,
682
+ scrolledBy: scrolledBy.x !== 0 || scrolledBy.y !== 0 ? scrolledBy : undefined
640
683
  };
641
684
  })
642
685
  `;
@@ -12,8 +12,8 @@ import type { CDPConnection } from '../../connection/cdp.js';
12
12
  import { CommandError } from '../../errors/index.js';
13
13
  /** Selector placeholder that makes page scripts use the bound node. */
14
14
  export declare const BOUND_TARGET_SELECTOR = "__bdg_bound_target__";
15
- /** Removes the node bound for index-based commands from the window it was stored on */
16
- export declare const UNBIND_TARGET_SCRIPT = "delete window.__bdgTarget";
15
+ /** Removes the node bound for index-based commands, and matches bound for a selector, from the window they were stored on */
16
+ export declare const UNBIND_TARGET_SCRIPT = "delete window.__bdgTarget; delete window.__bdgMatches";
17
17
  /**
18
18
  * Page-side matching of filters ({@link SelectorFilter}) and scoped steps
19
19
  * ({@link ScopedStep}): called with the open shadow roots of the page, returns
@@ -26,16 +26,20 @@ export declare const UNBIND_TARGET_SCRIPT = "delete window.__bdgTarget";
26
26
  * the text nodes of hidden ones (display or visibility; not those of
27
27
  * `<script>`, `<style>` or `<noscript>`), so text filters match hidden
28
28
  * elements like Playwright's and `:visible` decides visibility; button inputs
29
- * use their value. Whitespace is collapsed; filter texts arrive normalized
30
- * (`has-text` lowercased). `:visible` is checked before the text filters,
31
- * which read the text.
29
+ * use their value. Text is read in the flat tree, as `bdg dom query` shows
30
+ * it: the text a web component's open shadow root renders, slotted content
31
+ * in place of its slots. A visible component, slot or element holding either
32
+ * ({@link COMPOSED_JS}) is read with {@link FLAT_TEXT_JS}, including the
33
+ * selects and editable regions `innerText` reads. Whitespace is collapsed;
34
+ * filter texts arrive normalized (`has-text` lowercased). `:visible` is
35
+ * checked before the text filters, which read the text.
32
36
  *
33
37
  * Steps and `:has()` match CSS relative to an element (`:scope > css`). A
34
38
  * descendant step also searches the open shadow roots under the element (the
35
39
  * whole step CSS inside one shadow tree); a child step stays in the
36
40
  * element's own tree.
37
41
  */
38
- export declare const FILTER_MATCHING_JS = "(shadowRoots) => {\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const walker = el.ownerDocument.createTreeWalker(el, 5, {\n acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)\n });\n let text = '';\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) text += node.data;\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n return (rendered ? el.innerText : hiddenText(el)).replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n}";
42
+ export declare const FILTER_MATCHING_JS = "(shadowRoots) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;\n let text = '';\n for (const node of nodes) {\n if (node.nodeType === 3) text += node.data;\n else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;\n return text.replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n}";
39
43
  /**
40
44
  * Page-side selector search that also reaches into open shadow roots and
41
45
  * same-origin iframes (recursively), like a user sees the page.
@@ -49,14 +53,16 @@ export declare const FILTER_MATCHING_JS = "(shadowRoots) => {\n const skipped =
49
53
  * reached. Cross-origin iframes are separate processes and cannot be
50
54
  * searched. Throws a `SyntaxError` for an invalid selector.
51
55
  */
52
- export declare const DEEP_QUERY_JS = "function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const walker = el.ownerDocument.createTreeWalker(el, 5, {\n acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)\n });\n let text = '';\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) text += node.data;\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n return (rendered ? el.innerText : hiddenText(el)).replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n})(shadowRoots);\n const result = new Set();\n for (const el of found) {\n for (const part of parts) {\n if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;\n for (const match of descend(el, part.steps || [])) result.add(match);\n }\n }\n const rootOrder = new Map(roots.map((root, i) => [root, i]));\n const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;\n return [...result].sort((a, b) =>\n rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)\n );\n}";
56
+ export declare const DEEP_QUERY_JS = "function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;\n let text = '';\n for (const node of nodes) {\n if (node.nodeType === 3) text += node.data;\n else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;\n return text.replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n})(shadowRoots);\n const result = new Set();\n for (const el of found) {\n for (const part of parts) {\n if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;\n for (const match of descend(el, part.steps || [])) result.add(match);\n }\n }\n const rootOrder = new Map(roots.map((root, i) => [root, i]));\n const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;\n return [...result].sort((a, b) =>\n rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)\n );\n}";
53
57
  /**
54
58
  * Page-side element lookup shared by the interaction scripts.
55
59
  *
56
60
  * Returns the bound node (if still in the page) for the placeholder selector,
57
- * otherwise all matches of the selector ({@link DEEP_QUERY_JS}).
61
+ * the matches bound for the selector when the page replaced the search
62
+ * built-ins ({@link bindMatches}), otherwise all matches of the selector
63
+ * ({@link DEEP_QUERY_JS}).
58
64
  */
59
- export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (selector === '__bdg_bound_target__') {\n const el = window.__bdgTarget;\n return el && el.isConnected ? [el] : [];\n }\n return (function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const walker = el.ownerDocument.createTreeWalker(el, 5, {\n acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)\n });\n let text = '';\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) text += node.data;\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n return (rendered ? el.innerText : hiddenText(el)).replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n})(shadowRoots);\n const result = new Set();\n for (const el of found) {\n for (const part of parts) {\n if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;\n for (const match of descend(el, part.steps || [])) result.add(match);\n }\n }\n const rootOrder = new Map(roots.map((root, i) => [root, i]));\n const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;\n return [...result].sort((a, b) =>\n rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)\n );\n})(selector, parts);\n}";
65
+ export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (selector === '__bdg_bound_target__') {\n const el = window.__bdgTarget;\n return el && el.isConnected ? [el] : [];\n }\n const bound = window.__bdgMatches;\n if (bound && bound.selector === selector) {\n const connected = [];\n for (let i = 0; i < bound.nodes.length; i++) {\n if (bound.nodes[i] && bound.nodes[i].isConnected) connected[connected.length] = bound.nodes[i];\n }\n return connected;\n }\n return (function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;\n let text = '';\n for (const node of nodes) {\n if (node.nodeType === 3) text += node.data;\n else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;\n return text.replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n})(shadowRoots);\n const result = new Set();\n for (const el of found) {\n for (const part of parts) {\n if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;\n for (const match of descend(el, part.steps || [])) result.add(match);\n }\n }\n const rootOrder = new Map(roots.map((root, i) => [root, i]));\n const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;\n return [...result].sort((a, b) =>\n rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)\n );\n})(selector, parts);\n}";
60
66
  /**
61
67
  * Page-side check of what selectors cannot search in the top document:
62
68
  * iframes whose document cannot be read (cross-origin) and `<object>`/`<embed>`
@@ -94,23 +100,34 @@ export interface ScriptTarget {
94
100
  index?: number;
95
101
  /** Connection for the page scripts (scoped to the element's frame when the top page cannot reach it) */
96
102
  cdp: CDPConnection;
103
+ /** Built-ins the page replaced that the page scripts use (they run in the page's world) */
104
+ replacedBuiltins?: string[];
105
+ /** The page replaced the selector search, so the matches were found in bdg's world */
106
+ boundInBdgWorld?: boolean;
97
107
  }
98
108
  /**
99
109
  * Run an interaction on the element a request targets. Results and errors
100
110
  * never show the internal placeholder: a bound node the page scripts could
101
111
  * not find is reported as stale (87), and results carry the user's selector.
112
+ * On a page that replaced the selector search, a result warns that the
113
+ * element was found in bdg's world; on a page that replaced built-ins the
114
+ * scripts use, a failure adds that they may be the cause, and a script that
115
+ * threw is reported as broken by the page (90), naming them.
102
116
  *
103
117
  * @param cdp - CDP connection
104
118
  * @param params - Request with a selector (and optional index) or a backend node id
105
119
  * @param work - The interaction, given the script target
106
120
  * @returns The interaction's result
107
- * @throws CommandError (87) when the bound node left the page during the action
121
+ * @throws CommandError (87) when the bound node left the page during the
122
+ * action, (90) when its script threw on a page that replaced built-ins
108
123
  */
109
124
  export declare function onScriptTarget<T extends {
110
125
  selector?: string;
111
126
  error?: string;
112
127
  suggestion?: string;
113
128
  exitCode?: number;
129
+ warning?: string;
130
+ replacedBuiltins?: string[];
114
131
  }>(cdp: CDPConnection, params: {
115
132
  selector?: string;
116
133
  index?: number;