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,15 +5,18 @@
5
5
  * Keeps individual command handlers focused on option wiring and output formatting.
6
6
  */
7
7
  import { DomElementResolver } from '../DomElementResolver.js';
8
- import { UNREACHABLE_ELEMENTS_HINT, staleNodeError } from '../../../errors/messages.js';
8
+ import { noMatchContext } from './query.js';
9
+ import { otherIndexSourceNote, staleNodeError, unreachableElementsNote, withLoadingHint, } from '../../../errors/messages.js';
10
+ import { joinLines } from '../../../ui/formatting.js';
9
11
  import { EXIT_CODES } from '../../../utils/exitCodes.js';
10
12
  /**
11
13
  * Resolve an element target, invoke the IPC call, and normalize failures
12
- * into the structured `CommandRunner` result shape.
14
+ * into the structured `CommandRunner` result shape. A numeric index names the
15
+ * list it refers to (`indexSource` in the data, and in errors).
13
16
  */
14
17
  export async function runElementCommand(options) {
15
- const { selectorOrIndex, index, buildRequest, call, action, failureSuggestion } = options;
16
- const target = await DomElementResolver.getInstance().resolve(selectorOrIndex, index);
18
+ const { selectorOrIndex, index, command, buildRequest, call } = options;
19
+ const target = await DomElementResolver.getInstance().resolve(selectorOrIndex, index, command);
17
20
  if (!target.success) {
18
21
  return {
19
22
  success: false,
@@ -28,36 +31,100 @@ export async function runElementCommand(options) {
28
31
  ...(target.backendNodeId !== undefined && { backendNodeId: target.backendNodeId }),
29
32
  });
30
33
  const response = await call(request);
31
- if (response.status === 'error' || !response.data) {
32
- const staleIndex = response.exitCode === EXIT_CODES.STALE_CACHE && /^\d+$/.test(selectorOrIndex)
33
- ? staleNodeError(Number(selectorOrIndex)).message
34
- : undefined;
35
- return {
36
- success: false,
37
- error: staleIndex ?? response.error ?? `Failed to ${action}`,
38
- exitCode: response.exitCode ?? EXIT_CODES.INVALID_ARGUMENTS,
39
- ...(response.suggestion && { errorContext: { suggestion: response.suggestion } }),
40
- };
34
+ const failure = response.status === 'error' || !response.data
35
+ ? errorResponseFailure(response, options)
36
+ : response.data.success
37
+ ? undefined
38
+ : failedResultFailure(response.data, options);
39
+ if (failure && target.source) {
40
+ return indexFailure(failure, target.source, target.preview, response.data);
41
41
  }
42
- const result = response.data;
43
- if (!result.success) {
44
- const exitCode = result.exitCode ??
45
- (result.error?.includes('not found')
46
- ? EXIT_CODES.RESOURCE_NOT_FOUND
47
- : EXIT_CODES.INVALID_ARGUMENTS);
48
- const suggestion = result.suggestion ?? failureSuggestion;
42
+ if (failure)
43
+ return withNotFoundContext(failure, target.selector, response.status !== 'error');
44
+ const { success: _success, ...data } = response.data;
45
+ return { success: true, data: { ...data, ...(target.source && { indexSource: target.source }) } };
46
+ }
47
+ /**
48
+ * A failure on a cached index, told in terms of the index: a stale element
49
+ * (87) names the index and the command that refreshes it, and an element a
50
+ * form command cannot act on, from the results of another command, gets a
51
+ * note on which list the index refers to.
52
+ *
53
+ * @param failure - Failed command result
54
+ * @param source - The index and the list it refers to
55
+ * @param preview - What the cached element was when listed
56
+ * @param result - Action result, when the daemon answered
57
+ * @returns The failure in terms of the index
58
+ */
59
+ function indexFailure(failure, source, preview, result) {
60
+ if (failure.exitCode === EXIT_CODES.STALE_CACHE) {
61
+ const err = staleNodeError(source.index, source);
49
62
  return {
50
63
  success: false,
51
- error: result.error ?? `Failed to ${action}`,
52
- exitCode,
53
- errorContext: {
54
- suggestion: exitCode === EXIT_CODES.RESOURCE_NOT_FOUND
55
- ? `${suggestion} (${UNREACHABLE_ELEMENTS_HINT})`
56
- : suggestion,
57
- },
64
+ error: err.message,
65
+ exitCode: EXIT_CODES.STALE_CACHE,
66
+ errorContext: { suggestion: err.suggestion },
58
67
  };
59
68
  }
60
- const { success: _success, ...data } = result;
61
- return { success: true, data };
69
+ if (!result?.unsuitableElement || source.command === 'dom form')
70
+ return failure;
71
+ const note = otherIndexSourceNote(source, preview);
72
+ return {
73
+ ...failure,
74
+ errorContext: { suggestion: joinLines(failure.errorContext?.suggestion, note) },
75
+ };
76
+ }
77
+ /**
78
+ * Failure for an error response of the daemon.
79
+ *
80
+ * @param response - Error response
81
+ * @param options - Command options (selector or index, action)
82
+ * @returns Failed command result
83
+ */
84
+ function errorResponseFailure(response, options) {
85
+ return {
86
+ success: false,
87
+ error: response.error ?? `Failed to ${options.action}`,
88
+ exitCode: response.exitCode ?? EXIT_CODES.INVALID_ARGUMENTS,
89
+ ...(response.suggestion && { errorContext: { suggestion: response.suggestion } }),
90
+ };
91
+ }
92
+ /**
93
+ * Failure for an action whose page script reported failure.
94
+ *
95
+ * @param result - Action result
96
+ * @param options - Command options (action, fallback suggestion)
97
+ * @returns Failed command result
98
+ */
99
+ function failedResultFailure(result, options) {
100
+ const exitCode = result.exitCode ??
101
+ (result.error?.includes('not found')
102
+ ? EXIT_CODES.RESOURCE_NOT_FOUND
103
+ : EXIT_CODES.INVALID_ARGUMENTS);
104
+ return {
105
+ success: false,
106
+ error: result.error ?? `Failed to ${options.action}`,
107
+ exitCode,
108
+ errorContext: { suggestion: result.suggestion ?? options.failureSuggestion },
109
+ };
110
+ }
111
+ /**
112
+ * Add what the page says to a "not found" failure (one page evaluation, on
113
+ * this failure path only, {@link noMatchContext}): similar ids or classes,
114
+ * the places selectors do not search (for a page script that found nothing)
115
+ * and the still-loading hint while the page loads.
116
+ *
117
+ * @param failure - Failed command result
118
+ * @param selector - Selector that was looked for (the cached query's for an index)
119
+ * @param searched - The page script searched the page (the daemon did not fail first)
120
+ * @returns The failure, with the context in its suggestion
121
+ */
122
+ async function withNotFoundContext(failure, selector, searched) {
123
+ if (failure.exitCode !== EXIT_CODES.RESOURCE_NOT_FOUND)
124
+ return failure;
125
+ const context = await noMatchContext(selector);
126
+ const note = searched ? unreachableElementsNote(selector, context.unsearched) : '';
127
+ const suggestion = withLoadingHint(joinLines(context.similar, failure.errorContext?.suggestion, note ? note : undefined), context.readyState, selector);
128
+ return suggestion ? { ...failure, errorContext: { suggestion } } : failure;
62
129
  }
63
130
  //# sourceMappingURL=runElementCommand.js.map
@@ -20,7 +20,10 @@ export declare function getElementBounds(ref: NodeRef): Promise<ElementBounds>;
20
20
  */
21
21
  export declare function capturePageScreenshot(outputPath: string, options?: ScreenshotOptions): Promise<ScreenshotResult>;
22
22
  /**
23
- * Capture a screenshot of a single element, clipped to its bounding box.
23
+ * Capture a screenshot of a single element: its border box, grown to include
24
+ * content overflowing it ({@link captureArea}). The box model is relative to
25
+ * the viewport and the capture clip to the page, so the page scroll is added
26
+ * (the reported bounds are page coordinates, like `dom layout`'s).
24
27
  */
25
28
  export declare function captureElementScreenshot(outputPath: string, ref: NodeRef, options?: {
26
29
  format?: 'png' | 'jpeg';
@@ -11,6 +11,8 @@ import { CommandError } from '../../../errors/index.js';
11
11
  import { noNodesFoundError, elementNotVisibleError, elementZeroDimensionsError, } from '../../../errors/messages.js';
12
12
  import { callCDP } from '../../../ipc/client.js';
13
13
  import { DEEP_QUERY_JS, selectorArgsJS } from '../../../runtime/dom/targetNode.js';
14
+ import { viewportOverride } from '../../../runtime/page/emulation.js';
15
+ import { readSessionMetadata } from '../../../session/metadata.js';
14
16
  import { createLogger } from '../../../ui/logging/index.js';
15
17
  import { EXIT_CODES } from '../../../utils/exitCodes.js';
16
18
  const log = createLogger('dom');
@@ -136,6 +138,34 @@ async function restoreScrollPosition(position) {
136
138
  returnByValue: true,
137
139
  });
138
140
  }
141
+ /**
142
+ * Capture at a pixel ratio of 1 (CSS px = image px) on a high-DPI display:
143
+ * the viewport is overridden at its size (the session's `--viewport`, else
144
+ * the visible one) until the returned function puts back what was there
145
+ * before, the session's viewport or none.
146
+ *
147
+ * @param devicePixelRatio - Page's pixel ratio
148
+ * @param viewport - Visible viewport size
149
+ * @returns Function restoring the device metrics
150
+ */
151
+ async function useUnitPixelRatio(devicePixelRatio, viewport) {
152
+ if (devicePixelRatio === 1)
153
+ return () => Promise.resolve();
154
+ const sessionViewport = readSessionMetadata()?.viewport;
155
+ const size = sessionViewport ?? {
156
+ width: Math.round(viewport.clientWidth),
157
+ height: Math.round(viewport.clientHeight),
158
+ };
159
+ await callCDP('Emulation.setDeviceMetricsOverride', viewportOverride(size, 1));
160
+ return async () => {
161
+ if (sessionViewport) {
162
+ await callCDP('Emulation.setDeviceMetricsOverride', viewportOverride(sessionViewport));
163
+ }
164
+ else {
165
+ await callCDP('Emulation.clearDeviceMetricsOverride', {});
166
+ }
167
+ };
168
+ }
139
169
  /**
140
170
  * Get the bounding box (border box, so padding and border are included) of an
141
171
  * element via CDP DOM.getBoxModel.
@@ -161,6 +191,20 @@ export async function getElementBounds(ref) {
161
191
  }
162
192
  return { x, y, width, height };
163
193
  }
194
+ /**
195
+ * Page coordinates of the visible area's top-left corner. A capture clip is
196
+ * in page coordinates, so a viewport capture must start at the scroll
197
+ * position, not at the page origin (which shows nothing once scrolled). Read
198
+ * after any metrics override, which can move the scroll position.
199
+ *
200
+ * @returns Scroll offset of the visual viewport in CSS pixels
201
+ */
202
+ async function visibleAreaOrigin() {
203
+ const response = await callCDP('Page.getLayoutMetrics', {});
204
+ const metrics = response.data?.result;
205
+ const viewport = metrics?.cssVisualViewport;
206
+ return { x: viewport?.pageX ?? 0, y: viewport?.pageY ?? 0 };
207
+ }
164
208
  /**
165
209
  * Capture a screenshot of the page. Auto-resizes oversized pages by default
166
210
  * to keep Claude Vision token cost bounded; falls back to viewport capture
@@ -193,13 +237,8 @@ export async function capturePageScreenshot(outputPath, options = {}) {
193
237
  const scale = resized ? calculateResizeScale(captureWidth, captureHeight) : 1;
194
238
  const finalWidth = Math.round(captureWidth * scale);
195
239
  const finalHeight = Math.round(captureHeight * scale);
240
+ const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, viewport);
196
241
  if (devicePixelRatio !== 1) {
197
- await callCDP('Emulation.setDeviceMetricsOverride', {
198
- width: Math.round(viewport.clientWidth),
199
- height: Math.round(viewport.clientHeight),
200
- deviceScaleFactor: 1,
201
- mobile: false,
202
- });
203
242
  if (options.scroll) {
204
243
  await callCDP('Runtime.evaluate', {
205
244
  expression: `(${DEEP_QUERY_JS})(${selectorArgsJS(options.scroll)})[0]?.scrollIntoView({ block: 'center', behavior: 'instant' })`,
@@ -207,18 +246,7 @@ export async function capturePageScreenshot(outputPath, options = {}) {
207
246
  });
208
247
  }
209
248
  }
210
- let clipX = 0;
211
- let clipY = 0;
212
- if (useScroll && !effectiveFullPage) {
213
- const scrollResponse = await callCDP('Runtime.evaluate', {
214
- expression: 'JSON.stringify({ x: window.scrollX, y: window.scrollY })',
215
- returnByValue: true,
216
- });
217
- const scrollPos = JSON.parse(scrollResponse.data?.result?.result?.value ??
218
- '{"x":0,"y":0}');
219
- clipX = scrollPos.x;
220
- clipY = scrollPos.y;
221
- }
249
+ const clipOrigin = effectiveFullPage ? { x: 0, y: 0 } : await visibleAreaOrigin();
222
250
  let screenshotResult;
223
251
  try {
224
252
  const screenshotResponse = await callCDP('Page.captureScreenshot', {
@@ -226,8 +254,8 @@ export async function capturePageScreenshot(outputPath, options = {}) {
226
254
  ...(quality !== undefined && { quality }),
227
255
  captureBeyondViewport: effectiveFullPage,
228
256
  clip: {
229
- x: clipX,
230
- y: clipY,
257
+ x: clipOrigin.x,
258
+ y: clipOrigin.y,
231
259
  width: captureWidth,
232
260
  height: captureHeight,
233
261
  scale,
@@ -236,9 +264,7 @@ export async function capturePageScreenshot(outputPath, options = {}) {
236
264
  screenshotResult = screenshotResponse.data?.result;
237
265
  }
238
266
  finally {
239
- if (devicePixelRatio !== 1) {
240
- await callCDP('Emulation.clearDeviceMetricsOverride', {});
241
- }
267
+ await restoreMetrics();
242
268
  }
243
269
  if (!screenshotResult?.data) {
244
270
  throw new CDPConnectionError('No screenshot data returned', new Error('Empty response'));
@@ -287,11 +313,98 @@ export async function capturePageScreenshot(outputPath, options = {}) {
287
313
  }
288
314
  return result;
289
315
  }
316
+ /** Descendants {@link CONTENT_OVERFLOW_JS} looks at, so a huge element stays cheap */
317
+ const OVERFLOW_SCAN_LIMIT = 2000;
318
+ /**
319
+ * Page-side distances (CSS px, never negative) by which an element's rendered
320
+ * descendants reach beyond its border box on each side: uncleared floats,
321
+ * absolutely positioned and transformed children. Descendants of an element
322
+ * that clips its overflow (`overflow` other than `visible`) are cut off by it
323
+ * and not counted, nor are fixed ones (they belong to the viewport) or what
324
+ * lies outside the document (skip links at -9999px). Zero everywhere when the
325
+ * element clips its own overflow.
326
+ */
327
+ const CONTENT_OVERFLOW_JS = `function () {
328
+ const view = this.ownerDocument.defaultView;
329
+ const scroller = this.ownerDocument.scrollingElement || this.ownerDocument.documentElement;
330
+ const own = this.getBoundingClientRect();
331
+ const reach = { left: own.left, top: own.top, right: own.right, bottom: own.bottom };
332
+ const page = { left: -view.scrollX, top: -view.scrollY, right: scroller.scrollWidth - view.scrollX, bottom: scroller.scrollHeight - view.scrollY };
333
+ const clips = (style) => style.overflowX !== 'visible' || style.overflowY !== 'visible';
334
+ let budget = ${OVERFLOW_SCAN_LIMIT};
335
+ const walk = (el) => {
336
+ for (const child of el.children) {
337
+ if (--budget < 0) return;
338
+ const style = view.getComputedStyle(child);
339
+ if (style.display === 'none' || style.position === 'fixed') continue;
340
+ const r = child.getBoundingClientRect();
341
+ if (r.width > 0 && r.height > 0 && style.visibility === 'visible') {
342
+ reach.left = Math.min(reach.left, Math.max(r.left, page.left));
343
+ reach.top = Math.min(reach.top, Math.max(r.top, page.top));
344
+ reach.right = Math.max(reach.right, Math.min(r.right, page.right));
345
+ reach.bottom = Math.max(reach.bottom, Math.min(r.bottom, page.bottom));
346
+ }
347
+ if (!clips(style)) walk(child);
348
+ }
349
+ };
350
+ if (!clips(view.getComputedStyle(this))) walk(this);
351
+ return { left: own.left - reach.left, top: own.top - reach.top, right: reach.right - own.right, bottom: reach.bottom - own.bottom };
352
+ }`;
353
+ /** Overflow (px) below which the capture keeps to the border box (subpixel rounding) */
354
+ const OVERFLOW_SLACK = 1;
355
+ /**
356
+ * Area an element screenshot captures: the border box, grown to the content
357
+ * that overflows it ({@link CONTENT_OVERFLOW_JS}), so floated children are
358
+ * not cropped away.
359
+ *
360
+ * @param ref - Node reference
361
+ * @param bounds - Border box (DOM.getBoxModel coordinates)
362
+ * @returns The area, or the border box when nothing overflows (or the page cannot be asked)
363
+ */
364
+ async function captureArea(ref, bounds) {
365
+ const objectGroup = `bdg-shot-${process.pid}`;
366
+ try {
367
+ const resolved = await callCDP('DOM.resolveNode', { ...ref, objectGroup });
368
+ const objectId = resolved.data?.result?.object
369
+ .objectId;
370
+ if (!objectId)
371
+ return bounds;
372
+ const response = await callCDP('Runtime.callFunctionOn', {
373
+ objectId,
374
+ functionDeclaration: CONTENT_OVERFLOW_JS,
375
+ returnByValue: true,
376
+ });
377
+ const overflow = response.data?.result
378
+ ?.result?.value;
379
+ if (!overflow)
380
+ return bounds;
381
+ const [left, top, right, bottom] = ['left', 'top', 'right', 'bottom'].map((side) => Math.max(0, overflow[side] ?? 0));
382
+ if (Math.max(left, top, right, bottom) <= OVERFLOW_SLACK)
383
+ return bounds;
384
+ return {
385
+ x: bounds.x - left,
386
+ y: bounds.y - top,
387
+ width: bounds.width + left + right,
388
+ height: bounds.height + top + bottom,
389
+ };
390
+ }
391
+ catch (error) {
392
+ log.debug(`Could not measure overflowing content: ${String(error)}`);
393
+ return bounds;
394
+ }
395
+ finally {
396
+ await callCDP('Runtime.releaseObjectGroup', { objectGroup }).catch(() => undefined);
397
+ }
398
+ }
290
399
  /**
291
- * Capture a screenshot of a single element, clipped to its bounding box.
400
+ * Capture a screenshot of a single element: its border box, grown to include
401
+ * content overflowing it ({@link captureArea}). The box model is relative to
402
+ * the viewport and the capture clip to the page, so the page scroll is added
403
+ * (the reported bounds are page coordinates, like `dom layout`'s).
292
404
  */
293
405
  export async function captureElementScreenshot(outputPath, ref, options = {}) {
294
- const bounds = await getElementBounds(ref);
406
+ const box = await getElementBounds(ref);
407
+ const bounds = await captureArea(ref, box);
295
408
  const format = options.format ?? 'png';
296
409
  const quality = format === 'jpeg' ? (options.quality ?? 90) : undefined;
297
410
  const noResize = options.noResize ?? false;
@@ -309,34 +422,26 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
309
422
  const metricsResponse = await callCDP('Page.getLayoutMetrics', {});
310
423
  const metricsResult = metricsResponse.data?.result;
311
424
  const viewport = metricsResult?.visualViewport ?? { clientWidth: 800, clientHeight: 600 };
312
- if (devicePixelRatio !== 1) {
313
- await callCDP('Emulation.setDeviceMetricsOverride', {
314
- width: Math.round(viewport.clientWidth),
315
- height: Math.round(viewport.clientHeight),
316
- deviceScaleFactor: 1,
317
- mobile: false,
318
- });
319
- }
425
+ const scroll = metricsResult?.cssLayoutViewport ?? { pageX: 0, pageY: 0 };
426
+ const onPage = (area) => ({
427
+ ...area,
428
+ x: area.x + scroll.pageX,
429
+ y: area.y + scroll.pageY,
430
+ });
431
+ const clip = onPage(bounds);
432
+ const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, viewport);
320
433
  let screenshotResult;
321
434
  try {
322
435
  const screenshotResponse = await callCDP('Page.captureScreenshot', {
323
436
  format,
324
437
  ...(quality !== undefined && { quality }),
325
- clip: {
326
- x: bounds.x,
327
- y: bounds.y,
328
- width: bounds.width,
329
- height: bounds.height,
330
- scale,
331
- },
438
+ clip: { ...clip, scale },
332
439
  captureBeyondViewport: true,
333
440
  });
334
441
  screenshotResult = screenshotResponse.data?.result;
335
442
  }
336
443
  finally {
337
- if (devicePixelRatio !== 1) {
338
- await callCDP('Emulation.clearDeviceMetricsOverride', {});
339
- }
444
+ await restoreMetrics();
340
445
  }
341
446
  if (!screenshotResult?.data) {
342
447
  throw new CDPConnectionError('No screenshot data returned', new Error('Empty response'));
@@ -352,12 +457,8 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
352
457
  fullPage: false,
353
458
  finalTokens: calculateImageTokens(finalWidth, finalHeight),
354
459
  element: {
355
- bounds: {
356
- x: Math.round(bounds.x),
357
- y: Math.round(bounds.y),
358
- width: Math.round(bounds.width),
359
- height: Math.round(bounds.height),
360
- },
460
+ bounds: roundBounds(onPage(box)),
461
+ ...(bounds !== box && { captured: roundBounds(clip) }),
361
462
  },
362
463
  };
363
464
  if (quality !== undefined) {
@@ -371,4 +472,18 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
371
472
  }
372
473
  return result;
373
474
  }
475
+ /**
476
+ * Bounds in whole pixels.
477
+ *
478
+ * @param bounds - Bounds
479
+ * @returns Rounded bounds
480
+ */
481
+ function roundBounds(bounds) {
482
+ return {
483
+ x: Math.round(bounds.x),
484
+ y: Math.round(bounds.y),
485
+ width: Math.round(bounds.width),
486
+ height: Math.round(bounds.height),
487
+ };
488
+ }
374
489
  //# sourceMappingURL=screenshot.js.map
@@ -8,11 +8,13 @@
8
8
  * - `eval.ts` — evaluate JavaScript in the page (or an iframe)
9
9
  * - `frames.ts` — list the page's iframes
10
10
  * - `listeners.ts` — list event listeners that run for an element
11
+ * - `layout.ts` — positions, sizes and visibility of elements
12
+ * - `wait.ts` — wait for elements to appear, show, contain a text or go away
11
13
  *
12
14
  * Form-related commands register via `form.ts` and `formInteraction.ts`.
13
15
  * Accessibility commands register via `a11y.ts`.
14
16
  */
15
- import type { Command } from 'commander';
17
+ import { type Command } from 'commander';
16
18
  /**
17
19
  * Register DOM telemetry commands on the root Commander program.
18
20
  */
@@ -8,18 +8,23 @@
8
8
  * - `eval.ts` — evaluate JavaScript in the page (or an iframe)
9
9
  * - `frames.ts` — list the page's iframes
10
10
  * - `listeners.ts` — list event listeners that run for an element
11
+ * - `layout.ts` — positions, sizes and visibility of elements
12
+ * - `wait.ts` — wait for elements to appear, show, contain a text or go away
11
13
  *
12
14
  * Form-related commands register via `form.ts` and `formInteraction.ts`.
13
15
  * Accessibility commands register via `a11y.ts`.
14
16
  */
17
+ import { Option } from 'commander';
15
18
  import { registerA11yCommands } from './a11y.js';
16
19
  import { handleDomEval } from './eval.js';
17
20
  import { registerFormCommand } from './form.js';
18
21
  import { handleDomFrames } from './frames.js';
19
- import { handleDomGet } from './get.js';
22
+ import { DOM_GET_DEFAULT_SELECTOR, handleDomGet } from './get.js';
23
+ import { registerLayoutCommand } from './layout.js';
20
24
  import { registerListenersCommand } from './listeners.js';
21
25
  import { handleDomQuery } from './query.js';
22
26
  import { handleDomScreenshot } from './screenshot.js';
27
+ import { registerWaitCommand } from './wait.js';
23
28
  import { integerOption, screenshotFormatOption } from '../shared/validation.js';
24
29
  /**
25
30
  * Register DOM telemetry commands on the root Commander program.
@@ -32,6 +37,8 @@ export function registerDomCommands(program) {
32
37
  registerA11yCommands(dom);
33
38
  registerFormCommand(dom);
34
39
  registerListenersCommand(dom);
40
+ registerLayoutCommand(dom);
41
+ registerWaitCommand(dom);
35
42
  dom
36
43
  .command('query')
37
44
  .description('Find elements by CSS selector')
@@ -44,7 +51,7 @@ export function registerDomCommands(program) {
44
51
  .command('eval')
45
52
  .description('Evaluate JavaScript expression in the page context')
46
53
  .argument('<script>', 'JavaScript to execute (e.g., "document.title", "window.location.href")')
47
- .option('--frame <frame>', 'Evaluate in an iframe, cross-origin ones included: index, name/id attribute, or part of the URL (see dom frames)')
54
+ .option('--frame <frame>', 'Evaluate in an iframe, cross-origin ones included: index, name/id attribute, or part of the name, id or URL (see dom frames)')
48
55
  .option('-j, --json', 'Output as JSON')
49
56
  .action(async (script, options) => {
50
57
  await handleDomEval(script, options);
@@ -68,10 +75,12 @@ export function registerDomCommands(program) {
68
75
  dom
69
76
  .command('get')
70
77
  .description('Get semantic accessibility structure (default) or raw HTML (--raw)')
71
- .argument('[selector]', 'CSS selector or index from query results (e.g., ".error", "#app", 0); optional with --node-id')
78
+ .argument('[selectorOrIndex]', `CSS selector or numeric index from query results (0-based; e.g. ".error", "#app", 0); default: ${DOM_GET_DEFAULT_SELECTOR}`)
72
79
  .option('--raw', 'Output raw HTML with all filtering options')
80
+ .option('--full', 'Show all of the element text (default: the first 500 characters)')
73
81
  .option('--all', 'Get all matches (only with --raw)')
74
- .option('--nth <n>', 'Get the nth match, 0-based (only with --raw)', integerOption(0))
82
+ .option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
83
+ .addOption(new Option('--nth <n>', 'Alias of --index').argParser(integerOption(0)).hideHelp())
75
84
  .option('--node-id <id>', 'Get the element with this node id (from dom query/get --raw or a11y describe; implies --raw)', integerOption(1))
76
85
  .option('-j, --json', 'Output as JSON')
77
86
  .action(async (selector, options) => {
@@ -81,6 +90,7 @@ export function registerDomCommands(program) {
81
90
  .command('screenshot')
82
91
  .description('Capture page or element screenshot')
83
92
  .argument('<path>', 'Output file path, or directory for --follow mode')
93
+ .argument('[selector]', 'Element to capture: CSS selector or index from a query (same as --selector / --index)')
84
94
  .option('--selector <selector>', 'CSS selector for element capture')
85
95
  .option('--index <number>', 'Cached element index (0-based) from previous query', integerOption(0))
86
96
  .option('--format <format>', 'Image format: png or jpeg/jpg (default: from the file extension, else png)', screenshotFormatOption)
@@ -92,8 +102,8 @@ export function registerDomCommands(program) {
92
102
  .option('--interval <ms>', 'Capture interval for --follow (default: 1000)')
93
103
  .option('--limit <count>', 'Max frames for --follow')
94
104
  .option('-j, --json', 'Output as JSON')
95
- .action(async (path, options) => {
96
- await handleDomScreenshot(path, options);
105
+ .action(async (path, target, options) => {
106
+ await handleDomScreenshot(path, target, options);
97
107
  });
98
108
  }
99
109
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `bdg dom layout <selector|index>` - where elements are on the page and
3
+ * whether a user can see them, without a screenshot: page and viewport
4
+ * coordinates, size, viewport position (visible, partly, above/below the
5
+ * fold, hidden), what covers them and the styles that decide how they show.
6
+ */
7
+ import type { Command } from 'commander';
8
+ /**
9
+ * Register `bdg dom layout`.
10
+ *
11
+ * @param dom - The `dom` command group
12
+ */
13
+ export declare function registerLayoutCommand(dom: Command): void;
14
+ //# sourceMappingURL=layout.d.ts.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `bdg dom layout <selector|index>` - where elements are on the page and
3
+ * whether a user can see them, without a screenshot: page and viewport
4
+ * coordinates, size, viewport position (visible, partly, above/below the
5
+ * fold, hidden), what covers them and the styles that decide how they show.
6
+ */
7
+ import { DomElementResolver } from './DomElementResolver.js';
8
+ import { runElementCommand } from './helpers/runElementCommand.js';
9
+ import { runCommand } from '../shared/CommandRunner.js';
10
+ import { jsonOption } from '../shared/commonOptions.js';
11
+ import { integerOption } from '../shared/validation.js';
12
+ import { domLayout } from '../../ipc/client.js';
13
+ import { formatLayout } from '../../ui/formatters/layout.js';
14
+ /**
15
+ * Register `bdg dom layout`.
16
+ *
17
+ * @param dom - The `dom` command group
18
+ */
19
+ export function registerLayoutCommand(dom) {
20
+ dom
21
+ .command('layout')
22
+ .description('Positions, sizes and visibility of elements (above/below the fold, hidden, covered) without a screenshot')
23
+ .argument('<selectorOrIndex>', 'CSS selector or numeric index from query results (0-based)')
24
+ .option('--index <n>', 'Only this match of the selector (0-based)', integerOption(0))
25
+ .addOption(jsonOption())
26
+ .action(async (selectorOrIndex, options) => {
27
+ await runCommand(() => measureLayout(selectorOrIndex, options), options, formatLayout);
28
+ });
29
+ }
30
+ /**
31
+ * Resolve the target and ask the daemon to measure it.
32
+ *
33
+ * @param selectorOrIndex - CSS selector or cached query index
34
+ * @param options - Command options
35
+ * @returns Command result; an index argument is reported as the element's `index`
36
+ */
37
+ async function measureLayout(selectorOrIndex, options) {
38
+ const result = await runElementCommand({
39
+ selectorOrIndex,
40
+ index: options.index,
41
+ buildRequest: (target) => target,
42
+ call: domLayout,
43
+ command: 'layout',
44
+ action: 'measure the layout',
45
+ failureSuggestion: 'Verify the selector matches an element: bdg dom query "<selector>"',
46
+ });
47
+ if (!result.data || !DomElementResolver.getInstance().isNumericIndex(selectorOrIndex)) {
48
+ return result;
49
+ }
50
+ const index = Number(selectorOrIndex);
51
+ const elements = result.data.elements.map((element) => ({ ...element, index }));
52
+ return { ...result, data: { ...result.data, elements } };
53
+ }
54
+ //# sourceMappingURL=layout.js.map
@@ -4,7 +4,11 @@
4
4
  *
5
5
  * The daemon collects them with `DOMDebugger.getEventListeners`; listeners on
6
6
  * ancestors matter because frameworks (React, jQuery) attach their handlers
7
- * to a root container or the document and dispatch from there.
7
+ * to a root container or the document and dispatch from there. jQuery's
8
+ * dispatcher is replaced by the jQuery handlers it runs for the element,
9
+ * React's `on…` props of the element and its ancestors are listed, and
10
+ * framework roots (React's root container) are summarised per node unless
11
+ * `--all` is given.
8
12
  */
9
13
  import type { Command } from 'commander';
10
14
  /**