browser-debugger-cli 0.8.0 → 0.9.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 (251) hide show
  1. package/README.md +4 -1
  2. package/dist/cdp/schema.d.ts +4 -1
  3. package/dist/cdp/schema.js +48 -7
  4. package/dist/commands/cdp.js +3 -2
  5. package/dist/commands/cleanup.d.ts +11 -0
  6. package/dist/commands/cleanup.js +161 -57
  7. package/dist/commands/console.d.ts +20 -1
  8. package/dist/commands/console.js +57 -17
  9. package/dist/commands/details.js +3 -2
  10. package/dist/commands/dom/DomElementResolver.d.ts +10 -3
  11. package/dist/commands/dom/DomElementResolver.js +35 -17
  12. package/dist/commands/dom/a11y.d.ts +10 -0
  13. package/dist/commands/dom/a11y.js +27 -5
  14. package/dist/commands/dom/eval.d.ts +3 -1
  15. package/dist/commands/dom/eval.js +29 -4
  16. package/dist/commands/dom/form.js +16 -62
  17. package/dist/commands/dom/formInteraction.js +152 -113
  18. package/dist/commands/dom/formSummary.d.ts +49 -0
  19. package/dist/commands/dom/formSummary.js +180 -0
  20. package/dist/commands/dom/frames.d.ts +2 -1
  21. package/dist/commands/dom/frames.js +17 -2
  22. package/dist/commands/dom/get.d.ts +6 -5
  23. package/dist/commands/dom/get.js +92 -82
  24. package/dist/commands/dom/helpers/index.d.ts +1 -1
  25. package/dist/commands/dom/helpers/index.js +1 -1
  26. package/dist/commands/dom/helpers/query.d.ts +44 -17
  27. package/dist/commands/dom/helpers/query.js +244 -97
  28. package/dist/commands/dom/helpers/runElementCommand.d.ts +10 -2
  29. package/dist/commands/dom/helpers/runElementCommand.js +97 -30
  30. package/dist/commands/dom/helpers/screenshot.d.ts +4 -1
  31. package/dist/commands/dom/helpers/screenshot.js +164 -49
  32. package/dist/commands/dom/index.d.ts +3 -1
  33. package/dist/commands/dom/index.js +16 -6
  34. package/dist/commands/dom/layout.d.ts +14 -0
  35. package/dist/commands/dom/layout.js +54 -0
  36. package/dist/commands/dom/listeners.d.ts +5 -1
  37. package/dist/commands/dom/listeners.js +13 -3
  38. package/dist/commands/dom/query.js +2 -3
  39. package/dist/commands/dom/screenshot.d.ts +12 -2
  40. package/dist/commands/dom/screenshot.js +27 -3
  41. package/dist/commands/dom/semanticUtils.d.ts +6 -13
  42. package/dist/commands/dom/semanticUtils.js +15 -19
  43. package/dist/commands/dom/wait.d.ts +13 -0
  44. package/dist/commands/dom/wait.js +83 -0
  45. package/dist/commands/helpJson.js +2 -2
  46. package/dist/commands/network/list.js +4 -11
  47. package/dist/commands/optionBehaviors.js +112 -21
  48. package/dist/commands/page.d.ts +2 -1
  49. package/dist/commands/page.js +41 -5
  50. package/dist/commands/peek.js +4 -11
  51. package/dist/commands/sessions.d.ts +8 -0
  52. package/dist/commands/sessions.js +19 -0
  53. package/dist/commands/shared/CommandRunner.js +4 -4
  54. package/dist/commands/shared/dataFetcher.js +2 -2
  55. package/dist/commands/shared/followMode.d.ts +21 -1
  56. package/dist/commands/shared/followMode.js +29 -2
  57. package/dist/commands/shared/handleValidationError.d.ts +2 -2
  58. package/dist/commands/shared/handleValidationError.js +12 -3
  59. package/dist/commands/shared/optionTypes.d.ts +40 -5
  60. package/dist/commands/shared/startHelpers.js +12 -3
  61. package/dist/commands/shared/validation.d.ts +3 -2
  62. package/dist/commands/shared/validation.js +4 -3
  63. package/dist/commands/start.d.ts +63 -0
  64. package/dist/commands/start.js +115 -15
  65. package/dist/commands/status.js +29 -7
  66. package/dist/commands/stop.js +7 -6
  67. package/dist/commands/tail.js +4 -11
  68. package/dist/commands/types.d.ts +2 -0
  69. package/dist/commands.js +2 -0
  70. package/dist/connection/chromeIdentity.d.ts +65 -0
  71. package/dist/connection/chromeIdentity.js +143 -0
  72. package/dist/connection/launcher/profilePreferences.d.ts +47 -0
  73. package/dist/connection/launcher/profilePreferences.js +151 -0
  74. package/dist/connection/launcher.d.ts +21 -2
  75. package/dist/connection/launcher.js +42 -16
  76. package/dist/connection/portReservation.d.ts +14 -4
  77. package/dist/connection/portReservation.js +21 -6
  78. package/dist/connection/startupExit.d.ts +8 -0
  79. package/dist/connection/startupExit.js +15 -6
  80. package/dist/constants.d.ts +6 -2
  81. package/dist/constants.js +9 -2
  82. package/dist/daemon/SessionController.js +23 -7
  83. package/dist/daemon/errors.d.ts +1 -1
  84. package/dist/daemon/errors.js +1 -1
  85. package/dist/daemon/launcher.d.ts +2 -1
  86. package/dist/daemon/launcher.js +5 -6
  87. package/dist/daemon/server/SocketServer.js +1 -2
  88. package/dist/daemon/session/Session.d.ts +13 -0
  89. package/dist/daemon/session/Session.js +57 -8
  90. package/dist/daemon/session/chromeConnection.d.ts +9 -0
  91. package/dist/daemon/session/chromeConnection.js +45 -8
  92. package/dist/daemon/session/commandRegistry.js +52 -62
  93. package/dist/daemon/session/interactions.d.ts +35 -9
  94. package/dist/daemon/session/interactions.js +36 -9
  95. package/dist/daemon/session/triggeredRequests.d.ts +67 -0
  96. package/dist/daemon/session/triggeredRequests.js +157 -0
  97. package/dist/daemon/session/types.d.ts +5 -1
  98. package/dist/daemon.js +5393 -1600
  99. package/dist/errors/messages.d.ts +387 -24
  100. package/dist/errors/messages.js +761 -67
  101. package/dist/index.js +3976 -1558
  102. package/dist/ipc/client.d.ts +12 -1
  103. package/dist/ipc/client.js +22 -3
  104. package/dist/ipc/protocol/commands.d.ts +89 -4
  105. package/dist/ipc/protocol/commands.js +2 -0
  106. package/dist/ipc/protocol/domTypes.d.ts +258 -7
  107. package/dist/ipc/session/lifecycle.d.ts +8 -1
  108. package/dist/ipc/session/queries.d.ts +5 -1
  109. package/dist/ipc/session/types.d.ts +5 -0
  110. package/dist/ipc/transport/index.d.ts +2 -1
  111. package/dist/ipc/transport/index.js +2 -2
  112. package/dist/runtime/dom/actionEffects.d.ts +106 -0
  113. package/dist/runtime/dom/actionEffects.js +256 -0
  114. package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -0
  115. package/dist/runtime/dom/actionEffectsScripts.js +234 -0
  116. package/dist/runtime/dom/elementGeometry.d.ts +170 -0
  117. package/dist/runtime/dom/elementGeometry.js +553 -0
  118. package/dist/runtime/dom/elementInfo.d.ts +77 -0
  119. package/dist/runtime/dom/elementInfo.js +191 -0
  120. package/dist/runtime/dom/evalHelpers.d.ts +51 -6
  121. package/dist/runtime/dom/evalHelpers.js +136 -26
  122. package/dist/runtime/dom/eventListeners.d.ts +2 -1
  123. package/dist/runtime/dom/eventListeners.js +174 -47
  124. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  125. package/dist/runtime/dom/formDiscovery.js +116 -16
  126. package/dist/runtime/dom/formFillHelpers/fill.d.ts +10 -0
  127. package/dist/runtime/dom/formFillHelpers/fill.js +125 -10
  128. package/dist/runtime/dom/formFillHelpers/index.d.ts +2 -2
  129. package/dist/runtime/dom/formFillHelpers/index.js +2 -2
  130. package/dist/runtime/dom/formFillHelpers/pressKey.js +14 -3
  131. package/dist/runtime/dom/formFillHelpers/scroll.d.ts +3 -0
  132. package/dist/runtime/dom/formFillHelpers/scroll.js +60 -18
  133. package/dist/runtime/dom/formFillHelpers/shared.d.ts +17 -0
  134. package/dist/runtime/dom/formFillHelpers/shared.js +25 -1
  135. package/dist/runtime/dom/formFillHelpers/stability.d.ts +20 -6
  136. package/dist/runtime/dom/formFillHelpers/stability.js +50 -19
  137. package/dist/runtime/dom/formSubmitHelpers.d.ts +3 -0
  138. package/dist/runtime/dom/formSubmitHelpers.js +89 -15
  139. package/dist/runtime/dom/frameLayout.d.ts +60 -0
  140. package/dist/runtime/dom/frameLayout.js +140 -0
  141. package/dist/runtime/dom/frameOrigin.d.ts +50 -0
  142. package/dist/runtime/dom/frameOrigin.js +62 -0
  143. package/dist/runtime/dom/frameScopedConnection.d.ts +92 -0
  144. package/dist/runtime/dom/frameScopedConnection.js +252 -0
  145. package/dist/runtime/dom/frameSelection.d.ts +1 -1
  146. package/dist/runtime/dom/frameSelection.js +2 -2
  147. package/dist/runtime/dom/frames.d.ts +25 -2
  148. package/dist/runtime/dom/frames.js +202 -63
  149. package/dist/runtime/dom/layout.d.ts +67 -0
  150. package/dist/runtime/dom/layout.js +333 -0
  151. package/dist/runtime/dom/listenerPageScripts.d.ts +66 -0
  152. package/dist/runtime/dom/listenerPageScripts.js +279 -0
  153. package/dist/runtime/dom/listenerSummary.d.ts +132 -11
  154. package/dist/runtime/dom/listenerSummary.js +344 -22
  155. package/dist/runtime/dom/pageActivity.d.ts +41 -0
  156. package/dist/runtime/dom/pageActivity.js +123 -0
  157. package/dist/runtime/dom/reactEventHelpers.d.ts +58 -2
  158. package/dist/runtime/dom/reactEventHelpers.js +212 -41
  159. package/dist/runtime/dom/targetNode.d.ts +80 -27
  160. package/dist/runtime/dom/targetNode.js +249 -33
  161. package/dist/runtime/dom/wait.d.ts +25 -0
  162. package/dist/runtime/dom/wait.js +199 -0
  163. package/dist/runtime/dom/waitCondition.d.ts +71 -0
  164. package/dist/runtime/dom/waitCondition.js +75 -0
  165. package/dist/runtime/page/emulation.d.ts +51 -0
  166. package/dist/runtime/page/emulation.js +80 -0
  167. package/dist/runtime/page/loadingState.d.ts +36 -0
  168. package/dist/runtime/page/loadingState.js +86 -0
  169. package/dist/runtime/page/navigation.d.ts +46 -2
  170. package/dist/runtime/page/navigation.js +69 -33
  171. package/dist/session/QueryCacheManager.d.ts +11 -1
  172. package/dist/session/QueryCacheManager.js +25 -3
  173. package/dist/session/chromeOwners.d.ts +34 -0
  174. package/dist/session/chromeOwners.js +51 -0
  175. package/dist/session/cleanup/staleSession.d.ts +11 -1
  176. package/dist/session/cleanup/staleSession.js +17 -6
  177. package/dist/session/cleanup/userCommands.js +2 -4
  178. package/dist/session/metadata.d.ts +5 -1
  179. package/dist/session/metadata.js +2 -1
  180. package/dist/session/paths.d.ts +77 -3
  181. package/dist/session/paths.js +111 -5
  182. package/dist/session/port.d.ts +31 -7
  183. package/dist/session/port.js +50 -43
  184. package/dist/session/portClaims.d.ts +66 -0
  185. package/dist/session/portClaims.js +284 -0
  186. package/dist/session/sessionList.d.ts +58 -0
  187. package/dist/session/sessionList.js +199 -0
  188. package/dist/session/sessionName.d.ts +46 -0
  189. package/dist/session/sessionName.js +97 -0
  190. package/dist/telemetry/a11y.d.ts +8 -3
  191. package/dist/telemetry/a11y.js +92 -28
  192. package/dist/telemetry/requestKinds.d.ts +32 -0
  193. package/dist/telemetry/requestKinds.js +61 -0
  194. package/dist/telemetry/requestState.d.ts +31 -0
  195. package/dist/telemetry/requestState.js +38 -0
  196. package/dist/types.d.ts +80 -3
  197. package/dist/ui/formatters/a11y.js +3 -0
  198. package/dist/ui/formatters/console/chronological.d.ts +8 -0
  199. package/dist/ui/formatters/console/chronological.js +17 -4
  200. package/dist/ui/formatters/console/json.js +3 -4
  201. package/dist/ui/formatters/console/shared.d.ts +12 -0
  202. package/dist/ui/formatters/console.d.ts +2 -2
  203. package/dist/ui/formatters/console.js +1 -1
  204. package/dist/ui/formatters/details.js +2 -1
  205. package/dist/ui/formatters/dom.d.ts +26 -14
  206. package/dist/ui/formatters/dom.js +65 -52
  207. package/dist/ui/formatters/form.js +29 -18
  208. package/dist/ui/formatters/layout.d.ts +31 -0
  209. package/dist/ui/formatters/layout.js +53 -0
  210. package/dist/ui/formatters/listeners.d.ts +3 -2
  211. package/dist/ui/formatters/listeners.js +73 -9
  212. package/dist/ui/formatters/networkHeaders.js +13 -0
  213. package/dist/ui/formatters/preview.js +2 -1
  214. package/dist/ui/formatters/requestStatus.d.ts +1 -17
  215. package/dist/ui/formatters/requestStatus.js +2 -30
  216. package/dist/ui/formatters/sessions.d.ts +12 -0
  217. package/dist/ui/formatters/sessions.js +40 -0
  218. package/dist/ui/formatters/status.d.ts +21 -2
  219. package/dist/ui/formatters/status.js +47 -10
  220. package/dist/ui/formatters/triggeredRequests.d.ts +36 -0
  221. package/dist/ui/formatters/triggeredRequests.js +65 -0
  222. package/dist/ui/formatting.d.ts +10 -0
  223. package/dist/ui/formatting.js +28 -36
  224. package/dist/ui/messages/chrome.d.ts +9 -0
  225. package/dist/ui/messages/chrome.js +17 -5
  226. package/dist/ui/messages/commands.d.ts +388 -14
  227. package/dist/ui/messages/commands.js +664 -21
  228. package/dist/ui/messages/consoleMessages.d.ts +10 -0
  229. package/dist/ui/messages/consoleMessages.js +17 -0
  230. package/dist/ui/messages/hints.js +2 -1
  231. package/dist/ui/messages/preview.js +5 -4
  232. package/dist/ui/messages/session.d.ts +16 -21
  233. package/dist/ui/messages/session.js +28 -26
  234. package/dist/ui/messages/sessionCommand.d.ts +43 -0
  235. package/dist/ui/messages/sessionCommand.js +52 -0
  236. package/dist/utils/async.d.ts +8 -0
  237. package/dist/utils/async.js +19 -0
  238. package/dist/utils/http.d.ts +22 -1
  239. package/dist/utils/http.js +28 -9
  240. package/dist/utils/selectorFilters.d.ts +36 -8
  241. package/dist/utils/selectorFilters.js +267 -53
  242. package/dist/utils/shellDetection.d.ts +8 -2
  243. package/dist/utils/shellDetection.js +120 -33
  244. package/dist/utils/suggestions.d.ts +26 -0
  245. package/dist/utils/suggestions.js +73 -0
  246. package/dist/utils/taskMappings.js +10 -0
  247. package/dist/utils/url.d.ts +12 -2
  248. package/dist/utils/url.js +69 -7
  249. package/package.json +1 -1
  250. package/dist/ui/formatters/sessionFormatters.d.ts +0 -58
  251. package/dist/ui/formatters/sessionFormatters.js +0 -121
@@ -5,7 +5,96 @@
5
5
  * This module provides JavaScript snippets that can be injected via Runtime.evaluate
6
6
  * to properly trigger React's event system.
7
7
  */
8
- import { FIND_ELEMENTS_JS } from './targetNode.js';
8
+ import { FILL_REFUSALS, LABEL_WITHOUT_CONTROL, NAME_QUERY_PLACEHOLDER, VIA_LABEL_SUFFIX, } from '../../errors/messages.js';
9
+ import { ELEMENT_DESCRIPTION_JS, ELEMENT_IDENTITY_JS } from './elementInfo.js';
10
+ import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS } from './targetNode.js';
11
+ /**
12
+ * Page-side read-back of a filled field: a mismatch when its value is not
13
+ * what was asked for (the page rejected, reformatted or moved the input),
14
+ * undefined when it is. Values are compared as the browser normalises them:
15
+ * colors case-insensitively, numbers and ranges as numbers, email trimmed,
16
+ * textarea line endings as `\n`, times and local date-times without zero
17
+ * seconds and with `T` (`2024-01-05 10:00:00` is `2024-01-05T10:00`).
18
+ * Checkboxes and radios compare as `checked`/`unchecked`, a multiple select
19
+ * as its selected values joined by ", ", contenteditable text with
20
+ * whitespace collapsed. A value cut to the field's maxlength sets
21
+ * `truncatedTo`; a password mismatch gives masked values and both lengths.
22
+ */
23
+ export const FILL_VALUE_MISMATCH_JS = `(field, expected) => {
24
+ const type = (field.type || '').toLowerCase();
25
+ const actual = field.isContentEditable
26
+ ? (field.textContent || '')
27
+ : type === 'checkbox' || type === 'radio'
28
+ ? (field.checked ? 'checked' : 'unchecked')
29
+ : field.localName === 'select' && field.multiple
30
+ ? Array.from(field.selectedOptions).map((o) => o.value).join(', ')
31
+ : String(field.value);
32
+ const time = (text) => text.trim().replace(' ', 'T').replace(/(\\d\\d:\\d\\d):00(\\.0+)?$/, '$1');
33
+ const normalize = (text) => {
34
+ text = String(text);
35
+ if (field.isContentEditable) return text.replace(/\\s+/g, ' ').trim();
36
+ if (field.localName === 'textarea') return text.replace(/\\r\\n?/g, '\\n');
37
+ if (type === 'color') return text.trim().toLowerCase();
38
+ if (type === 'email') return text.trim();
39
+ if (type === 'time' || type === 'datetime-local') return time(text);
40
+ return text;
41
+ };
42
+ const numeric = (type === 'number' || type === 'range') && actual.trim() !== '' && String(expected).trim() !== '';
43
+ const same = numeric ? Number(actual) === Number(expected) : normalize(actual) === normalize(expected);
44
+ if (same) return undefined;
45
+ if (type === 'password') {
46
+ const mask = (text) => (text === '' ? '' : '********');
47
+ return { expected: mask(expected), actual: mask(actual), expectedLength: expected.length, actualLength: actual.length };
48
+ }
49
+ const cut = field.maxLength > 0 && actual.length === field.maxLength && expected.length > actual.length &&
50
+ expected.startsWith(actual);
51
+ return cut ? { expected: expected, actual: actual, truncatedTo: actual.length } : { expected: expected, actual: actual };
52
+ }`;
53
+ /**
54
+ * Page-side list of the other text-like fields of a field's form (of the
55
+ * document when it has none), with their values: taken before a fill, so
56
+ * {@link MOVED_VALUE_JS} can tell which one the page changed.
57
+ */
58
+ export const FIELD_VALUES_JS = `(field) => {
59
+ const fields = field.form ? Array.from(field.form.elements) : Array.from(field.ownerDocument.querySelectorAll('input, textarea'));
60
+ return new Map(fields
61
+ .filter((f) => f !== field && /^(input|textarea)$/.test(f.localName) &&
62
+ !/^(checkbox|radio|password|hidden|submit|button|reset|file|image)$/i.test(f.type || ''))
63
+ .map((f) => [f, String(f.value)]));
64
+ }`;
65
+ /**
66
+ * Page-side reason `dom fill` cannot fill an element, or null when it can:
67
+ * a disabled or read-only form control (naming the attribute or the disabled
68
+ * `<fieldset>`), or for any other element the switch that keeps it from being
69
+ * editable: `contenteditable="false"` (on it or the editor around it), an
70
+ * `inert` ancestor, `aria-readonly` or `aria-disabled`; otherwise it is not a
71
+ * fillable kind of element. Messages are {@link FILL_REFUSALS}.
72
+ */
73
+ export const FILL_REFUSAL_JS = `(el) => {
74
+ const describe = ${ELEMENT_DESCRIPTION_JS};
75
+ const refusals = ${JSON.stringify(FILL_REFUSALS)};
76
+ const refuse = (kind, cause) => ({
77
+ kind: kind,
78
+ error: refusals[kind].message + (cause ? ' (' + cause + ')' : ''),
79
+ suggestion: refusals[kind].suggestion
80
+ });
81
+ const tag = el.localName;
82
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') {
83
+ if (el.disabled || el.matches(':disabled')) {
84
+ return refuse('disabled', el.hasAttribute('disabled') ? 'disabled attribute' : el.closest('fieldset[disabled]') ? 'inside a disabled <fieldset>' : '');
85
+ }
86
+ return el.readOnly ? refuse('readOnly', 'readonly attribute') : null;
87
+ }
88
+ if (el.isContentEditable) return null;
89
+ const host = el.closest('[contenteditable]');
90
+ if (host && String(host.getAttribute('contenteditable')).trim().toLowerCase() === 'false') {
91
+ return refuse('readOnly', 'contenteditable="false"' + (host === el ? '' : ' on ' + describe(host)));
92
+ }
93
+ if (el.closest('[inert]')) return refuse('inert', 'inside an inert element');
94
+ if (el.getAttribute('aria-readonly') === 'true') return refuse('readOnly', 'aria-readonly="true"');
95
+ if (el.getAttribute('aria-disabled') === 'true') return refuse('disabled', 'aria-disabled="true"');
96
+ return refuse('notFillable', '<' + tag + '> is not an input, textarea, select or contenteditable element');
97
+ }`;
9
98
  /**
10
99
  * JavaScript function to fill an input element in a React-compatible way.
11
100
  *
@@ -14,6 +103,13 @@ import { FIND_ELEMENTS_JS } from './targetNode.js';
14
103
  * 2. Dispatches input/change events that React listens for
15
104
  * 3. Properly handles focus/blur for form validation
16
105
  *
106
+ * A `<label>` is filled through its control ({@link LABEL_CONTROL_JS}),
107
+ * reported as e.g. `input (via label)`.
108
+ *
109
+ * The result is returned right away (a change handler may navigate). The
110
+ * field and the value to expect are left in `window.__bdgFillCheck` for
111
+ * {@link FILL_READ_BACK_SCRIPT}, which reads the value back a moment later.
112
+ *
17
113
  * @remarks
18
114
  * Works with React, Vue, Angular, and vanilla JS applications.
19
115
  */
@@ -21,11 +117,14 @@ export const REACT_FILL_SCRIPT = `
21
117
  (function(selector, parts, value, options) {
22
118
  const allMatches = (${FIND_ELEMENTS_JS})(selector, parts);
23
119
  const warnings = [];
120
+ let expected = value;
24
121
  // Why a user could not reach the field (the value is still set, so scripted
25
122
  // flows keep working, but the result may not be what a user would see)
26
123
  const unreachableReason = (field) => {
27
124
  if (field.closest('[inert]')) return 'inert';
28
- const modal = field.ownerDocument.querySelector('dialog:modal');
125
+ const shown = (node) => !node.checkVisibility || node.checkVisibility();
126
+ const modal = field.ownerDocument.querySelector('dialog:modal') ||
127
+ Array.from(field.ownerDocument.querySelectorAll('[aria-modal="true"]')).find(shown);
29
128
  if (modal && !modal.contains(field)) return 'behind an open modal dialog';
30
129
  if (field.checkVisibility && !field.checkVisibility({ visibilityProperty: true, opacityProperty: true })) return 'hidden';
31
130
  return null;
@@ -84,40 +183,33 @@ export const REACT_FILL_SCRIPT = `
84
183
  } else {
85
184
  el = allMatches[0];
86
185
  }
87
-
88
- const tagName = el.tagName.toLowerCase();
89
- const inputType = el.type?.toLowerCase();
90
-
91
- const isFillable = (
92
- tagName === 'input' ||
93
- tagName === 'textarea' ||
94
- tagName === 'select' ||
95
- el.isContentEditable
96
- );
97
-
98
- if (!isFillable) {
99
- return {
100
- success: false,
101
- error: 'Element is not fillable',
102
- elementType: tagName,
103
- suggestion: 'Only input, textarea, select, and contenteditable elements can be filled'
104
- };
105
- }
106
186
 
107
- if (el.disabled || el.matches(':disabled')) {
187
+ const labelControl = (${LABEL_CONTROL_JS})(el);
188
+ if (el.localName === 'label' && !labelControl) {
189
+ const quote = (text) => "'" + String(text).split("'").join("'\\\\''") + "'";
190
+ const labelText = (el.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 60);
108
191
  return {
109
192
  success: false,
110
- error: 'Element is disabled',
111
- elementType: tagName,
112
- suggestion: 'Enable the field first (it may depend on another input)'
193
+ error: ${JSON.stringify(LABEL_WITHOUT_CONTROL.message)},
194
+ elementType: 'label',
195
+ unsuitableElement: true,
196
+ suggestion: ${JSON.stringify(LABEL_WITHOUT_CONTROL.suggestion)}.split(${JSON.stringify(NAME_QUERY_PLACEHOLDER)}).join(quote('name=' + labelText))
113
197
  };
114
198
  }
115
- if (el.readOnly) {
199
+ const viaLabel = labelControl ? ${JSON.stringify(VIA_LABEL_SUFFIX)} : '';
200
+ if (labelControl) el = labelControl;
201
+
202
+ const tagName = el.tagName.toLowerCase();
203
+ const inputType = el.type?.toLowerCase();
204
+
205
+ const refusal = (${FILL_REFUSAL_JS})(el);
206
+ if (refusal) {
116
207
  return {
117
208
  success: false,
118
- error: 'Element is read-only',
119
- elementType: tagName,
120
- suggestion: 'Read-only fields cannot be filled'
209
+ error: refusal.error,
210
+ elementType: tagName + viaLabel,
211
+ suggestion: refusal.suggestion,
212
+ unsuitableElement: refusal.kind === 'notFillable'
121
213
  };
122
214
  }
123
215
 
@@ -126,6 +218,7 @@ export const REACT_FILL_SCRIPT = `
126
218
  warnings.push('The field is ' + unreachable + '; a user could not fill it (the value was set anyway)');
127
219
  }
128
220
 
221
+ const fieldsBefore = (${FIELD_VALUES_JS})(el);
129
222
  el.focus();
130
223
 
131
224
  if (tagName === 'select' && el.multiple) {
@@ -143,11 +236,12 @@ export const REACT_FILL_SCRIPT = `
143
236
  success: false,
144
237
  error: 'Option not found: ' + missing,
145
238
  exitCode: 81,
146
- elementType: tagName,
239
+ elementType: tagName + viaLabel,
147
240
  suggestion: 'Available options: ' + options.slice(0, 10).map((o) => o.value || o.text.trim()).join(', ')
148
241
  };
149
242
  }
150
243
  options.forEach((o) => { o.selected = chosen.includes(o); });
244
+ expected = chosen.map((o) => o.value).join(', ');
151
245
  el.dispatchEvent(new Event('input', { bubbles: true }));
152
246
  el.dispatchEvent(new Event('change', { bubbles: true }));
153
247
  } else if (tagName === 'select') {
@@ -161,11 +255,12 @@ export const REACT_FILL_SCRIPT = `
161
255
  success: false,
162
256
  error: 'Option not found: ' + value,
163
257
  exitCode: 81,
164
- elementType: tagName,
258
+ elementType: tagName + viaLabel,
165
259
  suggestion: 'Available options: ' + options.slice(0, 10).map((o) => o.value || o.text.trim()).join(', ')
166
260
  };
167
261
  }
168
262
  el.value = option.value;
263
+ expected = option.value;
169
264
  el.dispatchEvent(new Event('input', { bubbles: true }));
170
265
  el.dispatchEvent(new Event('change', { bubbles: true }));
171
266
  } else if (inputType === 'checkbox' || inputType === 'radio') {
@@ -178,7 +273,7 @@ export const REACT_FILL_SCRIPT = `
178
273
  return {
179
274
  success: false,
180
275
  error: 'Expected true or false for a ' + inputType + ', got "' + value + '"',
181
- elementType: tagName,
276
+ elementType: tagName + viaLabel,
182
277
  inputType: inputType,
183
278
  suggestion: 'Use true/false (also yes/no, on/off, 1/0)'
184
279
  };
@@ -188,11 +283,12 @@ export const REACT_FILL_SCRIPT = `
188
283
  return {
189
284
  success: false,
190
285
  error: 'A radio button cannot be unchecked',
191
- elementType: tagName,
286
+ elementType: tagName + viaLabel,
192
287
  inputType: inputType,
193
288
  suggestion: 'Select another option in the same group instead'
194
289
  };
195
290
  }
291
+ expected = shouldCheck ? 'checked' : 'unchecked';
196
292
  if (el.checked !== shouldCheck) {
197
293
  el.click();
198
294
  }
@@ -200,7 +296,7 @@ export const REACT_FILL_SCRIPT = `
200
296
  return {
201
297
  success: false,
202
298
  fileInput: true,
203
- elementType: tagName,
299
+ elementType: tagName + viaLabel,
204
300
  inputType: inputType,
205
301
  error: 'File input'
206
302
  };
@@ -226,7 +322,7 @@ export const REACT_FILL_SCRIPT = `
226
322
  return {
227
323
  success: false,
228
324
  error: 'Value is ' + value.length + ' characters; the field accepts at most ' + el.maxLength,
229
- elementType: tagName,
325
+ elementType: tagName + viaLabel,
230
326
  inputType: inputType || null,
231
327
  suggestion: 'Shorten the value (a user could not type more than maxlength characters)'
232
328
  };
@@ -242,7 +338,7 @@ export const REACT_FILL_SCRIPT = `
242
338
  return {
243
339
  success: false,
244
340
  error: rejection.error,
245
- elementType: tagName,
341
+ elementType: tagName + viaLabel,
246
342
  inputType: inputType,
247
343
  suggestion: rejection.suggestion
248
344
  };
@@ -258,7 +354,8 @@ export const REACT_FILL_SCRIPT = `
258
354
  if (options.blur !== false) {
259
355
  el.blur();
260
356
  }
261
-
357
+ window.__bdgFillCheck = { el: el, expected: expected, before: fieldsBefore };
358
+
262
359
  return {
263
360
  success: true,
264
361
  selector: selector,
@@ -269,7 +366,8 @@ export const REACT_FILL_SCRIPT = `
269
366
  : tagName === 'select' && el.multiple
270
367
  ? Array.from(el.selectedOptions).map((o) => o.value).join(', ')
271
368
  : el.value,
272
- elementType: tagName,
369
+ element: (${ELEMENT_IDENTITY_JS})(el),
370
+ elementType: tagName + viaLabel,
273
371
  inputType: inputType || null,
274
372
  checked: inputType === 'checkbox' || inputType === 'radio' ? el.checked : undefined,
275
373
  matchCount: allMatches.length,
@@ -279,6 +377,52 @@ export const REACT_FILL_SCRIPT = `
279
377
  };
280
378
  })
281
379
  `;
380
+ /**
381
+ * Page-side search for the field a moved value went to: one of the fields
382
+ * recorded before the fill ({@link FIELD_VALUES_JS}) whose value changed to
383
+ * the one given (trimmed, at least 2 characters). Evaluates to its id or
384
+ * name (`input#first-name`, `input[name="first"]`) or else its
385
+ * description, or undefined. Passwords are never searched for.
386
+ */
387
+ export const MOVED_VALUE_JS = `(field, expected, before) => {
388
+ const wanted = String(expected).trim();
389
+ if (!before || wanted.length < 2 || (field.type || '').toLowerCase() === 'password') return undefined;
390
+ const other = Array.from(before.keys()).find((f) =>
391
+ String(f.value).trim() === wanted && String(before.get(f)).trim() !== wanted);
392
+ if (!other) return undefined;
393
+ if (other.id) return other.localName + '#' + other.id;
394
+ return other.name ? other.localName + '[name="' + other.name + '"]' : (${ELEMENT_DESCRIPTION_JS})(other);
395
+ }`;
396
+ /**
397
+ * Page script reading back the field the last fill left in
398
+ * `window.__bdgFillCheck`, after one macrotask (so frameworks that render
399
+ * asynchronously have updated it). The macrotask comes from a
400
+ * `MessageChannel`, which fake timers and page code rarely replace. Evaluates
401
+ * to the mismatch ({@link FILL_VALUE_MISMATCH_JS}), with `movedTo` when the
402
+ * value turned up in another field ({@link MOVED_VALUE_JS}), or null when the
403
+ * value matches, nothing was left (the page navigated) or the field left the
404
+ * page.
405
+ */
406
+ export const FILL_READ_BACK_SCRIPT = `(() => {
407
+ const check = window.__bdgFillCheck;
408
+ delete window.__bdgFillCheck;
409
+ if (!check) return null;
410
+ const readBack = () => {
411
+ if (!check.el.isConnected) return null;
412
+ const mismatch = (${FILL_VALUE_MISMATCH_JS})(check.el, check.expected);
413
+ if (!mismatch) return null;
414
+ const movedTo = (${MOVED_VALUE_JS})(check.el, check.expected, check.before);
415
+ return movedTo ? Object.assign(mismatch, { movedTo: movedTo }) : mismatch;
416
+ };
417
+ return new Promise((resolve) => {
418
+ const channel = new MessageChannel();
419
+ channel.port1.onmessage = () => {
420
+ channel.port1.close();
421
+ resolve(readBack());
422
+ };
423
+ channel.port2.postMessage(null);
424
+ });
425
+ })()`;
282
426
  /**
283
427
  * JavaScript function to locate an element for clicking.
284
428
  *
@@ -291,9 +435,15 @@ export const REACT_FILL_SCRIPT = `
291
435
  * Handles both direct selector matching and indexed selection.
292
436
  * When index is provided, selects the nth matching element (0-based).
293
437
  * When selector matches multiple elements without index, prioritizes visible ones.
438
+ * A `<label>` is clicked (and double-clicked) through its control when that
439
+ * is visible ({@link LABEL_CONTROL_JS}), reported as e.g. `input (via label)`.
294
440
  */
295
441
  export const CLICK_ELEMENT_SCRIPT = `
296
442
  (function(selector, parts, index, action) {
443
+ if (window.__bdgPressProbe) {
444
+ window.__bdgPressProbe.stop();
445
+ delete window.__bdgPressProbe;
446
+ }
297
447
  const allMatches = (${FIND_ELEMENTS_JS})(selector, parts);
298
448
 
299
449
  if (allMatches.length === 0) {
@@ -343,6 +493,14 @@ export const CLICK_ELEMENT_SCRIPT = `
343
493
  }
344
494
  }
345
495
 
496
+ const labelControl = action === 'click' || action === 'double' ? (${LABEL_CONTROL_JS})(el) : null;
497
+ const viaLabel = labelControl &&
498
+ (typeof labelControl.checkVisibility !== 'function' || labelControl.checkVisibility({ visibilityProperty: true, opacityProperty: true })) &&
499
+ labelControl.getClientRects().length > 0
500
+ ? ${JSON.stringify(VIA_LABEL_SUFFIX)}
501
+ : '';
502
+ if (viaLabel) el = labelControl;
503
+
346
504
  const tagName = el.tagName.toLowerCase();
347
505
  if (tagName === 'option') {
348
506
  const quote = (text) => "'" + String(text).split("'").join("'\\\\''") + "'";
@@ -352,7 +510,7 @@ export const CLICK_ELEMENT_SCRIPT = `
352
510
  success: false,
353
511
  error: 'An <option> is chosen through its <select>, not clicked',
354
512
  selector: selector,
355
- elementType: tagName,
513
+ elementType: tagName + viaLabel,
356
514
  exitCode: 81,
357
515
  suggestion: 'bdg dom fill ' + quote(target) + ' ' + quote(el.value || el.text.trim())
358
516
  };
@@ -363,7 +521,7 @@ export const CLICK_ELEMENT_SCRIPT = `
363
521
  success: false,
364
522
  error: 'Element is disabled',
365
523
  selector: selector,
366
- elementType: tagName,
524
+ elementType: tagName + viaLabel,
367
525
  suggestion: 'A user cannot click a disabled element; enable it first (it may depend on other fields)'
368
526
  };
369
527
  }
@@ -430,10 +588,23 @@ export const CLICK_ELEMENT_SCRIPT = `
430
588
  else if (!hasSize) obstruction = 'zero-size';
431
589
  else if (!hittable) obstruction = 'covered by another element' + coveredBy();
432
590
 
591
+ // Records whether the coming mouse press reaches the element at all; a
592
+ // browser dialog or bubble can swallow input while the page looks normal.
593
+ if (hittable && action !== 'hover') {
594
+ const probe = { reached: false };
595
+ const markReached = (event) => {
596
+ if (event.composedPath().includes(el)) probe.reached = true;
597
+ };
598
+ ['pointerdown', 'mousedown'].forEach((type) => view.addEventListener(type, markReached, true));
599
+ probe.stop = () => ['pointerdown', 'mousedown'].forEach((type) => view.removeEventListener(type, markReached, true));
600
+ window.__bdgPressProbe = probe;
601
+ }
602
+
433
603
  return {
434
604
  success: true,
435
605
  selector: selector,
436
- elementType: tagName,
606
+ element: (${ELEMENT_IDENTITY_JS})(el),
607
+ elementType: tagName + viaLabel,
437
608
  matchCount: allMatches.length,
438
609
  selectedIndex: typeof index === 'number' ? index : undefined,
439
610
  x: x,
@@ -9,27 +9,75 @@
9
9
  * {@link BOUND_TARGET_SELECTOR}.
10
10
  */
11
11
  import type { CDPConnection } from '../../connection/cdp.js';
12
+ import { CommandError } from '../../errors/index.js';
12
13
  /** Selector placeholder that makes page scripts use the bound node. */
13
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";
17
+ /**
18
+ * Page-side matching of filters ({@link SelectorFilter}) and scoped steps
19
+ * ({@link ScopedStep}): called with the open shadow roots of the page, returns
20
+ * `{ passesAll, descend }`.
21
+ *
22
+ * Visible means rendered (`checkVisibility()`: not inside a closed
23
+ * `<details>` or under `content-visibility: hidden`), a non-empty box and
24
+ * `visibility: visible`, as in Playwright (`opacity: 0` still counts as
25
+ * visible). Text is the rendered text (`innerText`) of visible elements and
26
+ * the text nodes of hidden ones (display or visibility; not those of
27
+ * `<script>`, `<style>` or `<noscript>`), so text filters match hidden
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.
32
+ *
33
+ * Steps and `:has()` match CSS relative to an element (`:scope > css`). A
34
+ * descendant step also searches the open shadow roots under the element (the
35
+ * whole step CSS inside one shadow tree); a child step stays in the
36
+ * element's own tree.
37
+ */
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}";
14
39
  /**
15
40
  * Page-side selector search that also reaches into open shadow roots and
16
41
  * same-origin iframes (recursively), like a user sees the page.
17
42
  *
18
43
  * Takes the selector and, when it has text or visibility filters, its parts
19
44
  * from {@link parseSelectorFilters}: their CSS runs as one selector list and
20
- * a match is kept when it passes the filters of a part it matches.
21
- * Matches of the document come first, then those of each shadow root and
22
- * frame in document order. Cross-origin iframes are separate processes and
23
- * cannot be searched. Throws a `SyntaxError` for an invalid selector.
45
+ * a match is kept when it passes the filters of a part it matches; a part
46
+ * with scoped steps contributes the steps' matches under it instead (each
47
+ * element once). Matches come in document order: those of the document
48
+ * first, then those of each shadow root and frame in the order they are
49
+ * reached. Cross-origin iframes are separate processes and cannot be
50
+ * searched. Throws a `SyntaxError` for an invalid selector.
24
51
  */
25
- 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 visit = (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 passes = (el, filter) => {\n if (filter.kind === 'visible') {\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n const text = (typeof el.innerText === 'string' ? el.innerText : el.textContent || '').replace(/\\s+/g, ' ').trim();\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n};\n return found.filter((el) => parts.some((part) => el.matches(part.css) && part.filters.every((filter) => passes(el, filter))));\n}";
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}";
26
53
  /**
27
54
  * Page-side element lookup shared by the interaction scripts.
28
55
  *
29
56
  * Returns the bound node (if still in the page) for the placeholder selector,
30
57
  * otherwise all matches of the selector ({@link DEEP_QUERY_JS}).
31
58
  */
32
- 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 visit = (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 passes = (el, filter) => {\n if (filter.kind === 'visible') {\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n const text = (typeof el.innerText === 'string' ? el.innerText : el.textContent || '').replace(/\\s+/g, ' ').trim();\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n};\n return found.filter((el) => parts.some((part) => el.matches(part.css) && part.filters.every((filter) => passes(el, filter))));\n})(selector, parts);\n}";
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}";
60
+ /**
61
+ * Page-side check of what selectors cannot search in the top document:
62
+ * iframes whose document cannot be read (cross-origin) and `<object>`/`<embed>`
63
+ * elements. Cheap: no frame is entered.
64
+ */
65
+ export declare const UNSEARCHED_CONTENT_JS = "(() => {\n const frames = Array.from(document.querySelectorAll('iframe, frame'));\n const unreadable = (frame) => {\n try { return !frame.contentDocument; } catch (e) { return true; }\n };\n return { crossOriginFrames: frames.some(unreadable), embeds: !!document.querySelector('object, embed') };\n})()";
66
+ /**
67
+ * Page-side ids (`kind` "id") or classes (`kind` "class") in the top
68
+ * document, from the first {@link NAME_SCAN_LIMIT} elements that have one.
69
+ *
70
+ * @param kind - Which names to read
71
+ * @returns Expression evaluating to the distinct names
72
+ */
73
+ export declare function pageNamesJS(kind: 'id' | 'class'): string;
74
+ /**
75
+ * Page-side: the form control a `<label>` stands for (`label.control`: its
76
+ * `for` target or the control inside it), or null for other elements and
77
+ * labels without one. Fill, click and key presses act on that control, like
78
+ * Playwright.
79
+ */
80
+ export declare const LABEL_CONTROL_JS = "(el) => (el && el.localName === 'label' ? el.control : null)";
33
81
  /**
34
82
  * Arguments for {@link DEEP_QUERY_JS} / {@link FIND_ELEMENTS_JS} and the page
35
83
  * scripts built on them: the selector as a JS string literal and its parts
@@ -37,42 +85,47 @@ export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (sel
37
85
  *
38
86
  * @param selector - Selector as the user gave it (or the bound-node placeholder)
39
87
  * @returns JS source of the two arguments, e.g. `"li:visible", [{...}]`
40
- * @throws CommandError (81) for a misplaced or malformed filter
88
+ * @throws CommandError (81) for an empty selector or a misplaced or malformed filter
41
89
  */
42
90
  export declare function selectorArgsJS(selector: string): string;
43
- /** Selector and index the page scripts should use. */
91
+ /** Selector and index the page scripts should use, and the connection to run them on. */
44
92
  export interface ScriptTarget {
45
93
  selector: string;
46
94
  index?: number;
95
+ /** Connection for the page scripts (scoped to the element's frame when the top page cannot reach it) */
96
+ cdp: CDPConnection;
47
97
  }
48
98
  /**
49
- * Make the node with `backendNodeId` the target of the next page script.
50
- *
51
- * @param cdp - CDP connection
52
- * @param backendNodeId - Backend node id from the query cache
53
- * @throws CommandError (exit 87) when the node no longer exists in the page
54
- */
55
- export declare function bindTargetNode(cdp: CDPConnection, backendNodeId: number): Promise<void>;
56
- /**
57
- * Resolve what a page script should target for an interaction request.
99
+ * Run an interaction on the element a request targets. Results and errors
100
+ * never show the internal placeholder: a bound node the page scripts could
101
+ * not find is reported as stale (87), and results carry the user's selector.
58
102
  *
59
103
  * @param cdp - CDP connection
60
104
  * @param params - Request with a selector (and optional index) or a backend node id
61
- * @returns Selector/index for the page script
105
+ * @param work - The interaction, given the script target
106
+ * @returns The interaction's result
107
+ * @throws CommandError (87) when the bound node left the page during the action
62
108
  */
63
- export declare function resolveScriptTarget(cdp: CDPConnection, params: {
109
+ export declare function onScriptTarget<T extends {
110
+ selector?: string;
111
+ error?: string;
112
+ suggestion?: string;
113
+ exitCode?: number;
114
+ }>(cdp: CDPConnection, params: {
64
115
  selector?: string;
65
116
  index?: number;
66
117
  backendNodeId?: number;
67
- }): Promise<ScriptTarget>;
118
+ }, work: (target: ScriptTarget) => Promise<T>): Promise<T>;
68
119
  /**
69
- * Report the user's selector instead of the internal placeholder.
120
+ * Error for an element a command could not find.
70
121
  *
71
- * @param result - Script result
72
- * @param selector - Selector the user gave (or the cached query's selector)
73
- * @returns Result with the placeholder replaced
122
+ * @param target - Selector (and index) or backend node id the command was given
123
+ * @param matchCount - Elements the selector matched
124
+ * @returns Stale (87), out of range (81) or not found (83) error
74
125
  */
75
- export declare function withUserSelector<T extends {
76
- selector?: string;
77
- }>(result: T, selector: string | undefined): T;
126
+ export declare function missingElementError(target: {
127
+ selector: string;
128
+ index?: number;
129
+ backendNodeId?: number;
130
+ }, matchCount: number): CommandError;
78
131
  //# sourceMappingURL=targetNode.d.ts.map