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
@@ -6,11 +6,11 @@ import * as fs from 'node:fs';
6
6
  import * as path from 'node:path';
7
7
  import { CommandError } from '../../../errors/index.js';
8
8
  import { fileNotFoundError, uploadDirectoryError, singleFileInputError, fillableElementNotFoundError, clickableElementNotFoundError, clickTargetDetachedError, unexpectedResponseFormatError, operationFailedError, } from '../../../errors/messages.js';
9
- import { escapeValueForJS, formatScriptExecutionError, throwIfInvalidSelector, withMultipleMatchesWarning, } from './shared.js';
10
- import { REACT_FILL_SCRIPT, CLICK_ELEMENT_SCRIPT, isFillResult, isClickResult, } from '../reactEventHelpers.js';
11
- import { FIND_ELEMENTS_JS, selectorArgsJS } from '../targetNode.js';
9
+ import { escapeValueForJS, formatScriptExecutionError, throwIfInvalidSelector, withMultipleMatchesWarning, withValueMismatchWarning, } from './shared.js';
10
+ import { FILL_READ_BACK_SCRIPT, REACT_FILL_SCRIPT, CLICK_ELEMENT_SCRIPT, isFillResult, isClickResult, } from '../reactEventHelpers.js';
11
+ import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS, selectorArgsJS } from '../targetNode.js';
12
12
  import { createLogger } from '../../../ui/logging/index.js';
13
- import { POINTER_ACTION_DONE, domClickFallbackWarning } from '../../../ui/messages/commands.js';
13
+ import { CLICK_NOT_RECEIVED_WARNING, POINTER_ACTION_DONE, domClickFallbackWarning, } from '../../../ui/messages/commands.js';
14
14
  import { getErrorMessage } from '../../../utils/errors.js';
15
15
  import { EXIT_CODES } from '../../../utils/exitCodes.js';
16
16
  const log = createLogger('dom');
@@ -38,7 +38,12 @@ export async function fillElement(cdp, selector, value, options = {}) {
38
38
  }
39
39
  if (cdpResponse.result?.value && isFillResult(cdpResponse.result.value)) {
40
40
  const result = cdpResponse.result.value;
41
- return result.fileInput ? await setFileInput(cdp, selector, value, options) : result;
41
+ if (!result.fileInput)
42
+ return withValueMismatchWarning(await withReadBack(cdp, result));
43
+ const uploaded = await setFileInput(cdp, selector, value, options);
44
+ return uploaded.success && result.elementType
45
+ ? { ...uploaded, elementType: result.elementType }
46
+ : uploaded;
42
47
  }
43
48
  const err = unexpectedResponseFormatError('FillResult');
44
49
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SOFTWARE_ERROR);
@@ -52,6 +57,52 @@ export async function fillElement(cdp, selector, value, options = {}) {
52
57
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SOFTWARE_ERROR);
53
58
  }
54
59
  }
60
+ /** How long reading a filled value back may take before it is skipped */
61
+ const READ_BACK_TIMEOUT_MS = 1000;
62
+ /**
63
+ * Add the read-back of a successful fill ({@link FILL_READ_BACK_SCRIPT}):
64
+ * `valueMismatch` when the field's value is not the one given. The fill
65
+ * stands without it when the read-back fails, takes longer than
66
+ * {@link READ_BACK_TIMEOUT_MS} or finds nothing (a change handler navigated
67
+ * or replaced the page).
68
+ *
69
+ * @param cdp - CDP connection
70
+ * @param result - Fill result
71
+ * @returns The result, with `valueMismatch` when the value differs
72
+ */
73
+ async function withReadBack(cdp, result) {
74
+ if (!result.success)
75
+ return result;
76
+ let timer;
77
+ const timedOut = new Promise((resolve) => {
78
+ timer = setTimeout(() => resolve(undefined), READ_BACK_TIMEOUT_MS);
79
+ });
80
+ const readBack = cdp
81
+ .send('Runtime.evaluate', {
82
+ expression: FILL_READ_BACK_SCRIPT,
83
+ returnByValue: true,
84
+ awaitPromise: true,
85
+ })
86
+ .then((response) => response.result?.value)
87
+ .catch((error) => {
88
+ log.debug(`Filled value not read back: ${getErrorMessage(error)}`);
89
+ return undefined;
90
+ });
91
+ const mismatch = await Promise.race([readBack, timedOut]).finally(() => clearTimeout(timer));
92
+ return isValueMismatch(mismatch) ? { ...result, valueMismatch: mismatch } : result;
93
+ }
94
+ /**
95
+ * Whether a read-back value is a mismatch.
96
+ *
97
+ * @param value - Value from {@link FILL_READ_BACK_SCRIPT}
98
+ * @returns True for `{ expected, actual }`
99
+ */
100
+ function isValueMismatch(value) {
101
+ if (typeof value !== 'object' || value === null)
102
+ return false;
103
+ const obj = value;
104
+ return typeof obj['expected'] === 'string' && typeof obj['actual'] === 'string';
105
+ }
55
106
  /** Empties a file input as a user removing the selection would (with events). */
56
107
  const CLEAR_FILE_INPUT_FUNCTION = `function () {
57
108
  this.value = '';
@@ -125,7 +176,7 @@ async function setFileInput(cdp, selector, value, options) {
125
176
  return problem;
126
177
  try {
127
178
  const located = (await cdp.send('Runtime.evaluate', {
128
- expression: `(${FIND_ELEMENTS_JS})(${selectorArgsJS(selector)})[${options.index ?? 0}]`,
179
+ expression: `((el) => (${LABEL_CONTROL_JS})(el) || el)((${FIND_ELEMENTS_JS})(${selectorArgsJS(selector)})[${options.index ?? 0}])`,
129
180
  objectGroup: UPLOAD_OBJECT_GROUP,
130
181
  }));
131
182
  if (located.exceptionDetails)
@@ -213,6 +264,67 @@ export function mouseEvents(action, x, y) {
213
264
  ];
214
265
  return action === 'double' ? [moved, ...press(1), ...press(2)] : [moved, ...press(1)];
215
266
  }
267
+ /**
268
+ * Page script that stops the press probe installed by CLICK_ELEMENT_SCRIPT
269
+ * and evaluates to whether the mouse press reached the target.
270
+ */
271
+ const PRESS_PROBE_READ_SCRIPT = `(() => {
272
+ const probe = window.__bdgPressProbe;
273
+ delete window.__bdgPressProbe;
274
+ if (!probe) return true;
275
+ probe.stop();
276
+ return probe.reached;
277
+ })()`;
278
+ /**
279
+ * Whether the mouse press just dispatched reached the target element.
280
+ *
281
+ * Any doubt (no probe, evaluation failure) counts as reached, so only a
282
+ * press that the page provably never saw is reported.
283
+ *
284
+ * @param cdp - CDP connection
285
+ * @returns False only when the target received no pointerdown/mousedown
286
+ */
287
+ export async function pressReachedTarget(cdp) {
288
+ try {
289
+ const response = (await cdp.send('Runtime.evaluate', {
290
+ expression: PRESS_PROBE_READ_SCRIPT,
291
+ returnByValue: true,
292
+ }));
293
+ return response.result?.value !== false;
294
+ }
295
+ catch (error) {
296
+ log.debug(`Press probe not read: ${getErrorMessage(error)}`);
297
+ return true;
298
+ }
299
+ }
300
+ /**
301
+ * Dispatch real mouse events for an action, checking after the first press
302
+ * that the target received it. If dispatching fails first, the press probe
303
+ * is still removed from the page.
304
+ *
305
+ * @param cdp - CDP connection
306
+ * @param action - What to do
307
+ * @param x - Page x
308
+ * @param y - Page y
309
+ * @returns False if the press never reached the target
310
+ */
311
+ async function dispatchMouseAction(cdp, action, x, y) {
312
+ let reached;
313
+ try {
314
+ for (const event of mouseEvents(action, x, y)) {
315
+ await cdp.send('Input.dispatchMouseEvent', event);
316
+ if (event['type'] === 'mousePressed' && reached === undefined) {
317
+ reached = await pressReachedTarget(cdp);
318
+ }
319
+ }
320
+ }
321
+ catch (error) {
322
+ if (reached === undefined)
323
+ await pressReachedTarget(cdp);
324
+ throw error;
325
+ }
326
+ return reached ?? true;
327
+ }
216
328
  /**
217
329
  * Drop the located element from the page without waiting: right after a
218
330
  * click on a link, evaluation waits until the new page commits (seconds for a
@@ -244,11 +356,14 @@ async function performClick(cdp, located, action) {
244
356
  if (!result.success)
245
357
  return result;
246
358
  if (hittable && x !== undefined && y !== undefined) {
247
- for (const event of mouseEvents(action, x, y)) {
248
- await cdp.send('Input.dispatchMouseEvent', event);
249
- }
359
+ const reached = await dispatchMouseAction(cdp, action, x, y);
250
360
  releaseClickTarget(cdp);
251
- return withMultipleMatchesWarning({ ...result, action, method: 'mouse' }, result.selectedIndex, `${POINTER_ACTION_DONE[action].toLowerCase()} the first visible one`);
361
+ return withMultipleMatchesWarning({
362
+ ...result,
363
+ action,
364
+ method: 'mouse',
365
+ ...(!reached && { warning: CLICK_NOT_RECEIVED_WARNING }),
366
+ }, result.selectedIndex, `${POINTER_ACTION_DONE[action].toLowerCase()} the first visible one`);
252
367
  }
253
368
  const response = (await cdp.send('Runtime.evaluate', {
254
369
  expression: domFallbackScript(action),
@@ -4,7 +4,7 @@
4
4
  * - `fill.ts` — fillElement, clickElement
5
5
  * - `pressKey.ts` — pressKeyElement + types
6
6
  * - `scroll.ts` — scrollPage + types
7
- * - `stability.ts` — waitForActionStability
7
+ * - `stability.ts` — withActionStability
8
8
  * - `shared.ts` — internal script/error helpers
9
9
  *
10
10
  * Callers keep importing from `@/commands/dom/formFillHelpers.js` which
@@ -13,5 +13,5 @@
13
13
  export { fillElement, clickElement } from './fill.js';
14
14
  export { pressKeyElement, type PressKeyOptions, type PressKeyResult, } from './pressKey.js';
15
15
  export { scrollPage, type ScrollOptions, type ScrollResult, } from './scroll.js';
16
- export { waitForActionStability } from './stability.js';
16
+ export { withActionStability } from './stability.js';
17
17
  //# sourceMappingURL=index.d.ts.map
@@ -4,7 +4,7 @@
4
4
  * - `fill.ts` — fillElement, clickElement
5
5
  * - `pressKey.ts` — pressKeyElement + types
6
6
  * - `scroll.ts` — scrollPage + types
7
- * - `stability.ts` — waitForActionStability
7
+ * - `stability.ts` — withActionStability
8
8
  * - `shared.ts` — internal script/error helpers
9
9
  *
10
10
  * Callers keep importing from `@/commands/dom/formFillHelpers.js` which
@@ -13,5 +13,5 @@
13
13
  export { fillElement, clickElement } from './fill.js';
14
14
  export { pressKeyElement, } from './pressKey.js';
15
15
  export { scrollPage, } from './scroll.js';
16
- export { waitForActionStability } from './stability.js';
16
+ export { withActionStability } from './stability.js';
17
17
  //# sourceMappingURL=index.js.map
@@ -4,10 +4,11 @@
4
4
  * that CDP skips (keypress / input / change / submit).
5
5
  */
6
6
  import { CommandError } from '../../../errors/index.js';
7
- import { keyPressFailedError, operationFailedError, unknownKeyError } from '../../../errors/messages.js';
7
+ import { VIA_LABEL_SUFFIX, keyPressFailedError, operationFailedError, unknownKeyError, } from '../../../errors/messages.js';
8
+ import { ELEMENT_IDENTITY_JS } from '../elementInfo.js';
8
9
  import { throwIfInvalidSelector, withMultipleMatchesWarning, } from './shared.js';
9
10
  import { describeModifiers, getKeyDefinition, impliesShift, MODIFIER_FLAGS, parseModifiers, similarKeyNames, shortcutCommands, } from '../keyMapping.js';
10
- import { FIND_ELEMENTS_JS, selectorArgsJS } from '../targetNode.js';
11
+ import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS, selectorArgsJS } from '../targetNode.js';
11
12
  import { EXIT_CODES } from '../../../utils/exitCodes.js';
12
13
  const FOCUS_ELEMENT_SCRIPT = `
13
14
  (function(selector, parts, index) {
@@ -29,6 +30,9 @@ const FOCUS_ELEMENT_SCRIPT = `
29
30
  } else {
30
31
  el = allMatches[0];
31
32
  }
33
+ const labelControl = (${LABEL_CONTROL_JS})(el);
34
+ const viaLabel = labelControl ? ${JSON.stringify(VIA_LABEL_SUFFIX)} : '';
35
+ if (labelControl) el = labelControl;
32
36
 
33
37
  const pageLevel = el === document.body || el === document.documentElement;
34
38
  if (!pageLevel) {
@@ -61,7 +65,13 @@ const FOCUS_ELEMENT_SCRIPT = `
61
65
  }
62
66
  }
63
67
 
64
- return { success: true, selector: selector, elementType: el.tagName.toLowerCase(), matchCount: allMatches.length };
68
+ return {
69
+ success: true,
70
+ selector: selector,
71
+ element: (${ELEMENT_IDENTITY_JS})(el),
72
+ elementType: el.tagName.toLowerCase() + viaLabel,
73
+ matchCount: allMatches.length
74
+ };
65
75
  })`;
66
76
  /** Exit codes and suggestions for focus failures, by reason. */
67
77
  const FOCUS_FAILURES = {
@@ -135,6 +145,7 @@ export async function pressKeyElement(cdp, selector, keyName, options = {}) {
135
145
  key: keyName,
136
146
  times,
137
147
  ...(modifierFlags > 0 && { modifiers: describeModifiers(modifierFlags) }),
148
+ ...(focusResult.element !== undefined && { element: focusResult.element }),
138
149
  elementType: focusResult.elementType,
139
150
  ...(focusResult.matchCount !== undefined && { matchCount: focusResult.matchCount }),
140
151
  }, options.index, 'pressed the key on the first');
@@ -6,6 +6,9 @@ import type { CDPConnection } from '../../../connection/cdp.js';
6
6
  import type { ScrollResult } from '../../../ipc/protocol/domTypes.js';
7
7
  /**
8
8
  * Scroll options.
9
+ *
10
+ * The reported viewport is the layout viewport without scrollbars and the
11
+ * page size the scrolling element's, as in `bdg dom layout`.
9
12
  */
10
13
  export interface ScrollOptions {
11
14
  down?: number;
@@ -3,6 +3,9 @@
3
3
  * page boundaries.
4
4
  */
5
5
  import { CommandError } from '../../../errors/index.js';
6
+ import { scrollNoEffectWarning } from '../../../errors/messages.js';
7
+ import { SCROLL_RANGE_JS, VIEWPORT_SIZE_JS, } from '../elementGeometry.js';
8
+ import { ELEMENT_IDENTITY_JS } from '../elementInfo.js';
6
9
  import { throwIfInvalidSelector, withMultipleMatchesWarning, } from './shared.js';
7
10
  import { FIND_ELEMENTS_JS, selectorArgsJS } from '../targetNode.js';
8
11
  import { EXIT_CODES } from '../../../utils/exitCodes.js';
@@ -29,26 +32,20 @@ const SCROLL_TO_ELEMENT_SCRIPT = `
29
32
 
30
33
  el.scrollIntoView({ behavior: 'instant', block: 'center' });
31
34
 
32
- const rect = el.getBoundingClientRect();
33
- const scrollX = window.scrollX + rect.left + rect.width / 2 - window.innerWidth / 2;
34
- const scrollY = window.scrollY + rect.top + rect.height / 2 - window.innerHeight / 2;
35
-
36
35
  return {
37
36
  success: true,
38
37
  scrollType: 'element',
39
38
  selector: selector,
39
+ element: (${ELEMENT_IDENTITY_JS})(el),
40
40
  matchCount: allMatches.length,
41
41
  scrolledTo: {
42
42
  x: Math.round(window.scrollX),
43
43
  y: Math.round(window.scrollY)
44
44
  },
45
- viewportSize: {
46
- width: window.innerWidth,
47
- height: window.innerHeight
48
- },
45
+ viewportSize: (${VIEWPORT_SIZE_JS})(window),
49
46
  pageSize: {
50
- width: document.documentElement.scrollWidth,
51
- height: document.documentElement.scrollHeight
47
+ width: (document.scrollingElement || document.documentElement).scrollWidth,
48
+ height: (document.scrollingElement || document.documentElement).scrollHeight
52
49
  }
53
50
  };
54
51
  })`;
@@ -56,6 +53,7 @@ const SCROLL_BY_SCRIPT = `
56
53
  (function(options) {
57
54
  const beforeX = window.scrollX;
58
55
  const beforeY = window.scrollY;
56
+ const range = (${SCROLL_RANGE_JS})(window);
59
57
 
60
58
  if (options.top) {
61
59
  window.scrollTo({ top: 0, behavior: 'instant' });
@@ -81,16 +79,60 @@ const SCROLL_BY_SCRIPT = `
81
79
  scrollType: options.top || options.bottom ? 'position' : 'offset',
82
80
  scrolledTo: { x: Math.round(afterX), y: Math.round(afterY) },
83
81
  scrolledBy: { x: Math.round(afterX - beforeX), y: Math.round(afterY - beforeY) },
84
- viewportSize: {
85
- width: window.innerWidth,
86
- height: window.innerHeight
87
- },
82
+ viewportSize: (${VIEWPORT_SIZE_JS})(window),
88
83
  pageSize: {
89
- width: document.documentElement.scrollWidth,
90
- height: document.documentElement.scrollHeight
91
- }
84
+ width: (document.scrollingElement || document.documentElement).scrollWidth,
85
+ height: (document.scrollingElement || document.documentElement).scrollHeight
86
+ },
87
+ rangeBefore: range,
88
+ readyState: document.readyState
92
89
  };
93
90
  })`;
91
+ /**
92
+ * The direction a page scroll asked for (the options allow one per axis;
93
+ * only one axis is checked, vertical first).
94
+ *
95
+ * @param options - Scroll options
96
+ * @returns Direction, or undefined when no distance was asked for
97
+ */
98
+ function requestedDirection(options) {
99
+ if (options.bottom || (options.down ?? 0) > 0)
100
+ return 'down';
101
+ if (options.top || (options.up ?? 0) > 0)
102
+ return 'up';
103
+ if ((options.right ?? 0) > 0)
104
+ return 'right';
105
+ if ((options.left ?? 0) > 0)
106
+ return 'left';
107
+ return undefined;
108
+ }
109
+ /**
110
+ * Warn when a page scroll moved nothing: the document does not scroll in
111
+ * that direction, the page was already at its edge, or scrolling is locked;
112
+ * while the page is still loading, also that it may grow.
113
+ *
114
+ * @param result - Page script result
115
+ * @param options - Scroll options
116
+ * @returns The scroll result, with `warning` when nothing scrolled
117
+ */
118
+ function withNoEffectWarning(result, options) {
119
+ const { rangeBefore, readyState, ...scroll } = result;
120
+ const direction = requestedDirection(options);
121
+ const moved = scroll.scrolledBy && (scroll.scrolledBy.x !== 0 || scroll.scrolledBy.y !== 0);
122
+ if (!direction || moved || !scroll.viewportSize)
123
+ return scroll;
124
+ const vertical = direction === 'up' || direction === 'down';
125
+ const scrollable = vertical
126
+ ? rangeBefore.up + rangeBefore.down > 0
127
+ : rangeBefore.left + rangeBefore.right > 0;
128
+ const warning = scrollNoEffectWarning({
129
+ direction,
130
+ reason: !scrollable ? 'too-small' : rangeBefore[direction] <= 0 ? 'at-edge' : 'locked',
131
+ viewport: vertical ? scroll.viewportSize.height : scroll.viewportSize.width,
132
+ readyState,
133
+ });
134
+ return { ...scroll, warning };
135
+ }
94
136
  function isScrollResult(value) {
95
137
  if (typeof value !== 'object' || value === null)
96
138
  return false;
@@ -164,7 +206,7 @@ export async function scrollPage(cdp, selector, options = {}) {
164
206
  };
165
207
  }
166
208
  if (cdpResponse.result?.value && isScrollResult(cdpResponse.result.value)) {
167
- return cdpResponse.result.value;
209
+ return withNoEffectWarning(cdpResponse.result.value, options);
168
210
  }
169
211
  return {
170
212
  success: false,
@@ -3,6 +3,7 @@
3
3
  * and JS string escaping.
4
4
  */
5
5
  import type { Protocol } from '../../../connection/typed-cdp.js';
6
+ import type { FillResult } from '../../../ipc/protocol/domTypes.js';
6
7
  /**
7
8
  * Escape a CSS selector for embedding in a single-quoted JS string.
8
9
  */
@@ -20,6 +21,14 @@ export declare function withMultipleMatchesWarning<T extends {
20
21
  matchCount?: number | undefined;
21
22
  warning?: string | undefined;
22
23
  }>(result: T, index: number | undefined, action: string): T;
24
+ /**
25
+ * Put a warning first when the filled field's value is not the one given
26
+ * (see `valueMismatch`). Keeps an existing warning after it.
27
+ *
28
+ * @param result - Fill result
29
+ * @returns The result, with the warning when the value differs
30
+ */
31
+ export declare function withValueMismatchWarning(result: FillResult): FillResult;
23
32
  /**
24
33
  * Escape a value for embedding in a single-quoted JS string.
25
34
  */
@@ -30,6 +39,14 @@ export declare function escapeValueForJS(value: string): string;
30
39
  * query-then-act pattern.
31
40
  */
32
41
  export declare function formatScriptExecutionError(exceptionDetails: Protocol.Runtime.ExceptionDetails, selector: string, operationType?: 'fill' | 'click', expression?: string): string;
42
+ /**
43
+ * What a page script threw, in one line (`TypeError: Cannot read …` rather
44
+ * than CDP's bare "Uncaught").
45
+ *
46
+ * @param details - Exception details from Runtime.evaluate
47
+ * @returns First line of the exception's description
48
+ */
49
+ export declare function exceptionSummary(details: Protocol.Runtime.ExceptionDetails): string;
33
50
  /**
34
51
  * Throw a user error when a page script failed because the CSS selector is
35
52
  * invalid (the browser's `querySelectorAll` rejected it), instead of reporting
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { CommandError } from '../../../errors/index.js';
6
6
  import { invalidSelectorError } from '../../../errors/messages.js';
7
- import { multipleMatchesWarning } from '../../../ui/messages/commands.js';
7
+ import { multipleMatchesWarning, valueMismatchWarning } from '../../../ui/messages/commands.js';
8
8
  import { EXIT_CODES } from '../../../utils/exitCodes.js';
9
9
  import { detectSelectorQuoteDamage } from '../../../utils/shellDetection.js';
10
10
  /**
@@ -29,6 +29,19 @@ export function withMultipleMatchesWarning(result, index, action) {
29
29
  const warning = multipleMatchesWarning(count, action);
30
30
  return { ...result, warning: result.warning ? `${result.warning}; ${warning}` : warning };
31
31
  }
32
+ /**
33
+ * Put a warning first when the filled field's value is not the one given
34
+ * (see `valueMismatch`). Keeps an existing warning after it.
35
+ *
36
+ * @param result - Fill result
37
+ * @returns The result, with the warning when the value differs
38
+ */
39
+ export function withValueMismatchWarning(result) {
40
+ if (!result.valueMismatch)
41
+ return result;
42
+ const warning = valueMismatchWarning(result.valueMismatch);
43
+ return { ...result, warning: result.warning ? `${warning}; ${result.warning}` : warning };
44
+ }
32
45
  /**
33
46
  * Escape a value for embedding in a single-quoted JS string.
34
47
  */
@@ -81,6 +94,17 @@ export function formatScriptExecutionError(exceptionDetails, selector, operation
81
94
  lines.push(` ${troubleshootingSteps.join('\n ')}`);
82
95
  return lines.join('\n');
83
96
  }
97
+ /**
98
+ * What a page script threw, in one line (`TypeError: Cannot read …` rather
99
+ * than CDP's bare "Uncaught").
100
+ *
101
+ * @param details - Exception details from Runtime.evaluate
102
+ * @returns First line of the exception's description
103
+ */
104
+ export function exceptionSummary(details) {
105
+ const description = details.exception?.description ?? details.text;
106
+ return description.split('\n')[0] ?? description;
107
+ }
84
108
  /**
85
109
  * Throw a user error when a page script failed because the CSS selector is
86
110
  * invalid (the browser's `querySelectorAll` rejected it), instead of reporting
@@ -1,14 +1,28 @@
1
1
  /**
2
2
  * Post-action network-stability wait.
3
3
  *
4
- * Lightweight check used after fill/click/pressKey/scroll: subscribes to
5
- * CDP network events, waits until no request has been pending for
6
- * ACTION_NETWORK_IDLE_MS, times out at ACTION_STABILITY_TIMEOUT_MS.
4
+ * Used around fill/click/hover/pressKey/scroll: tracks in-flight requests
5
+ * from before the action runs (so a request its handler sends synchronously
6
+ * is seen), then waits until no request has been pending for
7
+ * ACTION_NETWORK_IDLE_MS, giving up after ACTION_STABILITY_TIMEOUT_MS.
7
8
  */
8
9
  import type { CDPConnection } from '../../../connection/cdp.js';
9
10
  /**
10
- * Wait for the network to settle after an action. Returns when idle for
11
- * the threshold or after the timeout.
11
+ * Run an action and, when it succeeds, wait for the network to settle.
12
+ *
13
+ * Requests are tracked from before the action, so one an event handler
14
+ * starts while the action runs (`onclick = () => fetch(...)`) is waited for.
15
+ * `Network.enable` is sent without waiting for it: the session's network
16
+ * collector has already enabled the domain, and Chrome holds the call until
17
+ * a pending navigation commits, which would otherwise block for as long as
18
+ * the new page's server takes to answer.
19
+ *
20
+ * @param cdp - CDP connection
21
+ * @param action - Action to run (its result's `success` decides whether to wait)
22
+ * @param wait - Wait for the network after the action (false for `--no-wait`)
23
+ * @returns The action's result, once the network is idle or the wait timed out
12
24
  */
13
- export declare function waitForActionStability(cdp: CDPConnection): Promise<void>;
25
+ export declare function withActionStability<T extends {
26
+ success: boolean;
27
+ }>(cdp: CDPConnection, action: () => Promise<T>, wait?: boolean): Promise<T>;
14
28
  //# sourceMappingURL=stability.d.ts.map
@@ -1,40 +1,71 @@
1
1
  /**
2
2
  * Post-action network-stability wait.
3
3
  *
4
- * Lightweight check used after fill/click/pressKey/scroll: subscribes to
5
- * CDP network events, waits until no request has been pending for
6
- * ACTION_NETWORK_IDLE_MS, times out at ACTION_STABILITY_TIMEOUT_MS.
4
+ * Used around fill/click/hover/pressKey/scroll: tracks in-flight requests
5
+ * from before the action runs (so a request its handler sends synchronously
6
+ * is seen), then waits until no request has been pending for
7
+ * ACTION_NETWORK_IDLE_MS, giving up after ACTION_STABILITY_TIMEOUT_MS.
7
8
  */
8
9
  import { trackInFlightRequests } from '../../../connection/inFlightRequests.js';
9
10
  import { createLogger } from '../../../ui/logging/index.js';
10
11
  import { delay } from '../../../utils/async.js';
12
+ import { getErrorMessage } from '../../../utils/errors.js';
11
13
  const log = createLogger('dom');
12
14
  const ACTION_NETWORK_IDLE_MS = 150;
13
15
  const ACTION_STABILITY_TIMEOUT_MS = 2000;
14
16
  const STABILITY_CHECK_INTERVAL_MS = 50;
15
17
  /**
16
- * Wait for the network to settle after an action. Returns when idle for
17
- * the threshold or after the timeout.
18
+ * Run an action and, when it succeeds, wait for the network to settle.
19
+ *
20
+ * Requests are tracked from before the action, so one an event handler
21
+ * starts while the action runs (`onclick = () => fetch(...)`) is waited for.
22
+ * `Network.enable` is sent without waiting for it: the session's network
23
+ * collector has already enabled the domain, and Chrome holds the call until
24
+ * a pending navigation commits, which would otherwise block for as long as
25
+ * the new page's server takes to answer.
26
+ *
27
+ * @param cdp - CDP connection
28
+ * @param action - Action to run (its result's `success` decides whether to wait)
29
+ * @param wait - Wait for the network after the action (false for `--no-wait`)
30
+ * @returns The action's result, once the network is idle or the wait timed out
18
31
  */
19
- export async function waitForActionStability(cdp) {
20
- const deadline = Date.now() + ACTION_STABILITY_TIMEOUT_MS;
21
- await cdp.send('Network.enable');
32
+ export async function withActionStability(cdp, action, wait = true) {
33
+ if (!wait)
34
+ return action();
35
+ void cdp
36
+ .send('Network.enable')
37
+ .catch((error) => log.debug(`Network.enable failed: ${getErrorMessage(error)}`));
22
38
  const requests = trackInFlightRequests(cdp);
23
39
  try {
24
- while (Date.now() < deadline) {
25
- if (requests.count === 0) {
26
- const idleTime = Date.now() - requests.lastActivity;
27
- if (idleTime >= ACTION_NETWORK_IDLE_MS) {
28
- log.debug(`Network stable after ${idleTime}ms idle`);
29
- return;
30
- }
31
- }
32
- await delay(STABILITY_CHECK_INTERVAL_MS);
33
- }
34
- log.debug('Stability timeout reached, proceeding');
40
+ const result = await action();
41
+ if (result.success)
42
+ await waitForNetworkIdle(requests);
43
+ return result;
35
44
  }
36
45
  finally {
37
46
  requests.dispose();
38
47
  }
39
48
  }
49
+ /**
50
+ * Wait until no request has been in flight for ACTION_NETWORK_IDLE_MS since
51
+ * the wait began (a request the action started counts until it ends), or
52
+ * until ACTION_STABILITY_TIMEOUT_MS passed.
53
+ *
54
+ * @param requests - Tracker started before the action
55
+ */
56
+ async function waitForNetworkIdle(requests) {
57
+ const waitStart = Date.now();
58
+ const deadline = waitStart + ACTION_STABILITY_TIMEOUT_MS;
59
+ while (Date.now() < deadline) {
60
+ if (requests.count === 0) {
61
+ const idleTime = Date.now() - Math.max(requests.lastActivity, waitStart);
62
+ if (idleTime >= ACTION_NETWORK_IDLE_MS) {
63
+ log.debug(`Network stable after ${idleTime}ms idle`);
64
+ return;
65
+ }
66
+ }
67
+ await delay(STABILITY_CHECK_INTERVAL_MS);
68
+ }
69
+ log.debug(`Stability timeout reached with ${requests.count} request(s) in flight, proceeding`);
70
+ }
40
71
  //# sourceMappingURL=stability.js.map
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { CDPConnection } from '../../connection/cdp.js';
5
5
  import type { SubmitResult } from '../../ipc/protocol/domTypes.js';
6
+ import type { PendingRequest } from '../../telemetry/network.js';
6
7
  /**
7
8
  * Options for submitting a form.
8
9
  */
@@ -15,6 +16,8 @@ export interface SubmitOptions {
15
16
  waitNetwork?: number;
16
17
  /** Maximum time to wait in milliseconds (default: 10000) */
17
18
  timeout?: number;
19
+ /** Requests still running in the session (named when the wait times out) */
20
+ pendingRequests?: () => Iterable<PendingRequest>;
18
21
  }
19
22
  export type { SubmitResult } from '../../ipc/protocol/domTypes.js';
20
23
  /**