browser-debugger-cli 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/.claude/skills/bdg/SKILL.md +4 -4
  2. package/README.md +143 -79
  3. package/dist/commands/cdp.d.ts +22 -1
  4. package/dist/commands/cdp.js +100 -43
  5. package/dist/commands/console.d.ts +12 -0
  6. package/dist/commands/console.js +62 -12
  7. package/dist/commands/css.d.ts +13 -0
  8. package/dist/commands/css.js +53 -0
  9. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  10. package/dist/commands/dom/DomElementResolver.js +10 -3
  11. package/dist/commands/dom/a11y.js +3 -2
  12. package/dist/commands/dom/audit.d.ts +14 -0
  13. package/dist/commands/dom/audit.js +87 -0
  14. package/dist/commands/dom/eval.d.ts +3 -2
  15. package/dist/commands/dom/eval.js +11 -5
  16. package/dist/commands/dom/form.js +10 -9
  17. package/dist/commands/dom/formInteraction.js +42 -11
  18. package/dist/commands/dom/get.js +8 -8
  19. package/dist/commands/dom/helpers/index.d.ts +1 -1
  20. package/dist/commands/dom/helpers/index.js +1 -1
  21. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  22. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  23. package/dist/commands/dom/helpers/query.d.ts +27 -3
  24. package/dist/commands/dom/helpers/query.js +152 -64
  25. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  26. package/dist/commands/dom/helpers/screenshot.js +169 -49
  27. package/dist/commands/dom/index.js +7 -2
  28. package/dist/commands/dom/query.d.ts +19 -2
  29. package/dist/commands/dom/query.js +37 -6
  30. package/dist/commands/dom/screenshot.js +12 -7
  31. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  32. package/dist/commands/dom/semanticUtils.js +40 -9
  33. package/dist/commands/dom/wait.js +5 -3
  34. package/dist/commands/helpJson.d.ts +82 -19
  35. package/dist/commands/helpJson.js +112 -41
  36. package/dist/commands/helpTopic.d.ts +16 -1
  37. package/dist/commands/helpTopic.js +59 -1
  38. package/dist/commands/installSkill.d.ts +15 -5
  39. package/dist/commands/installSkill.js +86 -16
  40. package/dist/commands/network/list.js +22 -12
  41. package/dist/commands/optionBehaviors.js +53 -16
  42. package/dist/commands/page.js +7 -4
  43. package/dist/commands/peek.d.ts +7 -0
  44. package/dist/commands/peek.js +65 -23
  45. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  46. package/dist/commands/shared/daemonErrorHandler.js +20 -9
  47. package/dist/commands/shared/dataFetcher.d.ts +12 -4
  48. package/dist/commands/shared/dataFetcher.js +12 -4
  49. package/dist/commands/shared/followMode.d.ts +9 -1
  50. package/dist/commands/shared/followMode.js +22 -4
  51. package/dist/commands/shared/optionTypes.d.ts +9 -2
  52. package/dist/commands/shared/outputFile.js +6 -1
  53. package/dist/commands/start.d.ts +20 -5
  54. package/dist/commands/start.js +84 -23
  55. package/dist/commands/stop.d.ts +11 -0
  56. package/dist/commands/stop.js +24 -1
  57. package/dist/commands/tail.d.ts +7 -1
  58. package/dist/commands/tail.js +13 -62
  59. package/dist/commands.js +2 -0
  60. package/dist/connection/cdp.d.ts +7 -0
  61. package/dist/connection/cdp.js +9 -0
  62. package/dist/connection/launcher.js +3 -2
  63. package/dist/daemon/SessionController.js +6 -1
  64. package/dist/daemon/launcher.d.ts +3 -2
  65. package/dist/daemon/launcher.js +47 -3
  66. package/dist/daemon/session/Session.d.ts +4 -1
  67. package/dist/daemon/session/Session.js +33 -2
  68. package/dist/daemon/session/TelemetryStore.d.ts +8 -1
  69. package/dist/daemon/session/TelemetryStore.js +13 -1
  70. package/dist/daemon/session/commandRegistry.js +36 -14
  71. package/dist/daemon/session/interactions.d.ts +2 -1
  72. package/dist/daemon/session/interactions.js +13 -1
  73. package/dist/daemon/session/plugins.js +19 -53
  74. package/dist/daemon/session/teardown.js +1 -1
  75. package/dist/daemon.js +9234 -7222
  76. package/dist/errors/messages.d.ts +88 -15
  77. package/dist/errors/messages.js +177 -27
  78. package/dist/index.js +19322 -13961
  79. package/dist/ipc/client.d.ts +22 -2
  80. package/dist/ipc/client.js +34 -5
  81. package/dist/ipc/protocol/auditTypes.d.ts +135 -0
  82. package/dist/ipc/protocol/auditTypes.js +6 -0
  83. package/dist/ipc/protocol/commands.d.ts +35 -0
  84. package/dist/ipc/protocol/commands.js +2 -0
  85. package/dist/ipc/protocol/domTypes.d.ts +16 -0
  86. package/dist/ipc/protocol/inspectTypes.d.ts +73 -8
  87. package/dist/ipc/session/types.d.ts +2 -0
  88. package/dist/runtime/css/search.d.ts +39 -0
  89. package/dist/runtime/css/search.js +122 -0
  90. package/dist/runtime/dom/actionEffects.d.ts +9 -2
  91. package/dist/runtime/dom/actionEffects.js +30 -14
  92. package/dist/runtime/dom/audit.d.ts +19 -0
  93. package/dist/runtime/dom/audit.js +37 -0
  94. package/dist/runtime/dom/auditModel.d.ts +45 -0
  95. package/dist/runtime/dom/auditModel.js +220 -0
  96. package/dist/runtime/dom/auditScripts.d.ts +113 -0
  97. package/dist/runtime/dom/auditScripts.js +148 -0
  98. package/dist/runtime/dom/elementGeometry.d.ts +16 -3
  99. package/dist/runtime/dom/elementGeometry.js +49 -10
  100. package/dist/runtime/dom/elementInfo.d.ts +74 -17
  101. package/dist/runtime/dom/elementInfo.js +187 -34
  102. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  103. package/dist/runtime/dom/evalHelpers.js +67 -7
  104. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  105. package/dist/runtime/dom/formDiscovery.js +20 -3
  106. package/dist/runtime/dom/formFillHelpers/fill.js +8 -12
  107. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  108. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  109. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  110. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  111. package/dist/runtime/dom/frameLayout.js +1 -0
  112. package/dist/runtime/dom/inspect.d.ts +7 -0
  113. package/dist/runtime/dom/inspect.js +92 -28
  114. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  115. package/dist/runtime/dom/inspectAllStyles.js +90 -7
  116. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  117. package/dist/runtime/dom/inspectCascade.js +214 -44
  118. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  119. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  120. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  121. package/dist/runtime/dom/inspectHints.js +125 -9
  122. package/dist/runtime/dom/inspectModel.d.ts +5 -1
  123. package/dist/runtime/dom/inspectModel.js +37 -10
  124. package/dist/runtime/dom/inspectPaintModel.d.ts +50 -22
  125. package/dist/runtime/dom/inspectPaintModel.js +182 -68
  126. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  127. package/dist/runtime/dom/inspectRules.js +21 -5
  128. package/dist/runtime/dom/inspectScripts.d.ts +112 -12
  129. package/dist/runtime/dom/inspectScripts.js +357 -32
  130. package/dist/runtime/dom/inspectTree.js +10 -2
  131. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  132. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  133. package/dist/runtime/dom/layout.js +40 -16
  134. package/dist/runtime/dom/reactEventHelpers.d.ts +21 -4
  135. package/dist/runtime/dom/reactEventHelpers.js +90 -36
  136. package/dist/runtime/dom/targetNode.d.ts +18 -5
  137. package/dist/runtime/dom/targetNode.js +268 -8
  138. package/dist/runtime/dom/wait.js +2 -1
  139. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  140. package/dist/runtime/page/bdgWorld.js +180 -0
  141. package/dist/runtime/page/emulation.d.ts +13 -4
  142. package/dist/runtime/page/emulation.js +69 -4
  143. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  144. package/dist/runtime/page/replacedBuiltins.js +136 -0
  145. package/dist/runtime/page/userAgent.d.ts +17 -0
  146. package/dist/runtime/page/userAgent.js +57 -0
  147. package/dist/session/QueryCacheManager.d.ts +4 -1
  148. package/dist/session/QueryCacheManager.js +5 -2
  149. package/dist/session/chrome.d.ts +4 -1
  150. package/dist/session/chrome.js +7 -1
  151. package/dist/session/cleanup/staleSession.d.ts +21 -4
  152. package/dist/session/cleanup/staleSession.js +79 -9
  153. package/dist/session/cleanup/userCommands.d.ts +4 -1
  154. package/dist/session/cleanup/userCommands.js +10 -5
  155. package/dist/session/daemonSocket.d.ts +10 -0
  156. package/dist/session/daemonSocket.js +22 -0
  157. package/dist/session/lastSession.d.ts +6 -3
  158. package/dist/session/lastSession.js +11 -5
  159. package/dist/session/paths.d.ts +3 -1
  160. package/dist/session/paths.js +5 -5
  161. package/dist/session/portClaims.js +4 -3
  162. package/dist/session/sessionList.d.ts +13 -5
  163. package/dist/session/sessionList.js +31 -7
  164. package/dist/telemetry/a11y.js +2 -2
  165. package/dist/telemetry/console.d.ts +2 -1
  166. package/dist/telemetry/console.js +30 -21
  167. package/dist/telemetry/pageCrash.d.ts +26 -0
  168. package/dist/telemetry/pageCrash.js +53 -0
  169. package/dist/types.d.ts +20 -0
  170. package/dist/ui/formatters/audit.d.ts +19 -0
  171. package/dist/ui/formatters/audit.js +115 -0
  172. package/dist/ui/formatters/cdp.d.ts +138 -0
  173. package/dist/ui/formatters/cdp.js +131 -0
  174. package/dist/ui/formatters/console/chronological.js +3 -1
  175. package/dist/ui/formatters/console/follow.d.ts +2 -1
  176. package/dist/ui/formatters/console/follow.js +2 -2
  177. package/dist/ui/formatters/console/json.d.ts +2 -2
  178. package/dist/ui/formatters/console/json.js +11 -5
  179. package/dist/ui/formatters/console/shared.d.ts +30 -0
  180. package/dist/ui/formatters/console/shared.js +16 -0
  181. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  182. package/dist/ui/formatters/console/summarize.js +40 -9
  183. package/dist/ui/formatters/console.d.ts +2 -1
  184. package/dist/ui/formatters/console.js +7 -5
  185. package/dist/ui/formatters/details.js +3 -1
  186. package/dist/ui/formatters/dom.d.ts +2 -2
  187. package/dist/ui/formatters/dom.js +10 -8
  188. package/dist/ui/formatters/helpFormatters.js +1 -1
  189. package/dist/ui/formatters/inspect.js +50 -17
  190. package/dist/ui/formatters/installSkill.d.ts +9 -1
  191. package/dist/ui/formatters/installSkill.js +32 -6
  192. package/dist/ui/formatters/layout.js +2 -1
  193. package/dist/ui/formatters/networkList.d.ts +1 -1
  194. package/dist/ui/formatters/networkList.js +1 -2
  195. package/dist/ui/formatters/preview.d.ts +2 -0
  196. package/dist/ui/formatters/preview.js +17 -7
  197. package/dist/ui/formatters/sessions.d.ts +2 -2
  198. package/dist/ui/formatters/sessions.js +9 -2
  199. package/dist/ui/formatters/status.js +1 -1
  200. package/dist/ui/logging/logger.d.ts +1 -1
  201. package/dist/ui/messages/commands.d.ts +168 -11
  202. package/dist/ui/messages/commands.js +245 -18
  203. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  204. package/dist/ui/messages/consoleMessages.js +32 -0
  205. package/dist/ui/messages/preview.d.ts +12 -0
  206. package/dist/ui/messages/preview.js +18 -2
  207. package/dist/ui/messages/session.d.ts +13 -2
  208. package/dist/ui/messages/session.js +22 -3
  209. package/dist/utils/cssValues.js +36 -4
  210. package/dist/utils/decisionTrees.js +0 -5
  211. package/dist/utils/directories.d.ts +34 -0
  212. package/dist/utils/directories.js +88 -0
  213. package/dist/utils/display.d.ts +16 -0
  214. package/dist/utils/display.js +42 -0
  215. package/dist/utils/exitCodes.d.ts +1 -0
  216. package/dist/utils/exitCodes.js +6 -0
  217. package/dist/utils/process.d.ts +12 -0
  218. package/dist/utils/process.js +25 -0
  219. package/dist/utils/suggestions.d.ts +4 -2
  220. package/dist/utils/suggestions.js +7 -5
  221. package/dist/utils/taskMappings.js +1 -1
  222. package/package.json +3 -2
@@ -9,7 +9,7 @@ import { writeOutputFile } from '../../shared/outputFile.js';
9
9
  import { CDPConnectionError } from '../../../connection/errors.js';
10
10
  import { CommandError } from '../../../errors/index.js';
11
11
  import { noNodesFoundError, elementNotVisibleError, elementZeroDimensionsError, } from '../../../errors/messages.js';
12
- import { callCDP } from '../../../ipc/client.js';
12
+ import { callBdgScript, callCDP } from '../../../ipc/client.js';
13
13
  import { DEEP_QUERY_JS, selectorArgsJS } from '../../../runtime/dom/targetNode.js';
14
14
  import { viewportOverride } from '../../../runtime/page/emulation.js';
15
15
  import { readSessionMetadata } from '../../../session/metadata.js';
@@ -26,7 +26,7 @@ const STABILITY_CHECK_INTERVAL_MS = 50;
26
26
  */
27
27
  async function waitForPostScrollStability() {
28
28
  const deadline = Date.now() + POST_SCROLL_MAX_WAIT_MS;
29
- await callCDP('Runtime.evaluate', {
29
+ await callBdgScript('Runtime.evaluate', {
30
30
  expression: `
31
31
  (() => {
32
32
  window.__bdg_scrollStability = {
@@ -66,7 +66,7 @@ async function waitForPostScrollStability() {
66
66
  });
67
67
  try {
68
68
  while (Date.now() < deadline) {
69
- const checkResult = await callCDP('Runtime.evaluate', {
69
+ const checkResult = await callBdgScript('Runtime.evaluate', {
70
70
  expression: `
71
71
  (() => {
72
72
  const state = window.__bdg_scrollStability;
@@ -91,7 +91,7 @@ async function waitForPostScrollStability() {
91
91
  log.debug('Post-scroll stability timeout, proceeding anyway');
92
92
  }
93
93
  finally {
94
- await callCDP('Runtime.evaluate', {
94
+ await callBdgScript('Runtime.evaluate', {
95
95
  expression: `
96
96
  (() => {
97
97
  const state = window.__bdg_scrollStability;
@@ -111,7 +111,7 @@ async function waitForPostScrollStability() {
111
111
  * position so it can be restored afterwards.
112
112
  */
113
113
  async function scrollToElement(selector) {
114
- const result = await callCDP('Runtime.evaluate', {
114
+ const result = await callBdgScript('Runtime.evaluate', {
115
115
  expression: `
116
116
  (() => {
117
117
  const el = (${DEEP_QUERY_JS})(${selectorArgsJS(selector)})[0];
@@ -133,7 +133,7 @@ async function scrollToElement(selector) {
133
133
  return { x: value.originalX ?? 0, y: value.originalY ?? 0 };
134
134
  }
135
135
  async function restoreScrollPosition(position) {
136
- await callCDP('Runtime.evaluate', {
136
+ await callBdgScript('Runtime.evaluate', {
137
137
  expression: `window.scrollTo(${position.x}, ${position.y})`,
138
138
  returnByValue: true,
139
139
  });
@@ -147,7 +147,7 @@ async function restoreScrollPosition(position) {
147
147
  * @returns Width and height in CSS px
148
148
  */
149
149
  async function windowSize(viewport) {
150
- const response = await callCDP('Runtime.evaluate', {
150
+ const response = await callBdgScript('Runtime.evaluate', {
151
151
  expression: '[window.innerWidth, window.innerHeight]',
152
152
  returnByValue: true,
153
153
  });
@@ -175,14 +175,22 @@ async function useUnitPixelRatio(devicePixelRatio, viewport) {
175
175
  const sessionViewport = readSessionMetadata()?.viewport;
176
176
  const size = sessionViewport ?? (await windowSize(viewport));
177
177
  await callCDP('Emulation.setDeviceMetricsOverride', viewportOverride(size, 1));
178
- return async () => {
179
- if (sessionViewport) {
180
- await callCDP('Emulation.setDeviceMetricsOverride', viewportOverride(sessionViewport));
181
- }
182
- else {
183
- await callCDP('Emulation.clearDeviceMetricsOverride', {});
184
- }
185
- };
178
+ return restoreSessionMetrics;
179
+ }
180
+ /**
181
+ * Put back the session's device metrics: its `--viewport` (and a phone's
182
+ * touch input, which a capture beyond the viewport turns off), else none.
183
+ */
184
+ async function restoreSessionMetrics() {
185
+ const sessionViewport = readSessionMetadata()?.viewport;
186
+ if (!sessionViewport) {
187
+ await callCDP('Emulation.clearDeviceMetricsOverride', {});
188
+ return;
189
+ }
190
+ await callCDP('Emulation.setDeviceMetricsOverride', viewportOverride(sessionViewport));
191
+ if (sessionViewport.mobile) {
192
+ await callCDP('Emulation.setTouchEmulationEnabled', { enabled: true, maxTouchPoints: 5 });
193
+ }
186
194
  }
187
195
  /**
188
196
  * Get the bounding box (border box, so padding and border are included) of an
@@ -237,7 +245,7 @@ export async function capturePageScreenshot(outputPath, options = {}) {
237
245
  if (options.scroll) {
238
246
  originalScrollPosition = await scrollToElement(options.scroll);
239
247
  }
240
- const dprResponse = await callCDP('Runtime.evaluate', {
248
+ const dprResponse = await callBdgScript('Runtime.evaluate', {
241
249
  expression: 'window.devicePixelRatio',
242
250
  returnByValue: true,
243
251
  });
@@ -258,7 +266,7 @@ export async function capturePageScreenshot(outputPath, options = {}) {
258
266
  const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, viewport);
259
267
  if (devicePixelRatio !== 1) {
260
268
  if (options.scroll) {
261
- await callCDP('Runtime.evaluate', {
269
+ await callBdgScript('Runtime.evaluate', {
262
270
  expression: `(${DEEP_QUERY_JS})(${selectorArgsJS(options.scroll)})[0]?.scrollIntoView({ block: 'center', behavior: 'instant' })`,
263
271
  returnByValue: true,
264
272
  });
@@ -334,13 +342,17 @@ export async function capturePageScreenshot(outputPath, options = {}) {
334
342
  /** Descendants {@link CONTENT_OVERFLOW_JS} looks at, so a huge element stays cheap */
335
343
  const OVERFLOW_SCAN_LIMIT = 2000;
336
344
  /**
337
- * Page-side distances (CSS px, never negative) by which an element's rendered
338
- * descendants reach beyond its border box on each side: uncleared floats,
339
- * absolutely positioned and transformed children. Descendants of an element
340
- * that clips its overflow (`overflow` other than `visible`) are cut off by it
341
- * and not counted, nor are fixed ones (they belong to the viewport) or what
342
- * lies outside the document (skip links at -9999px). Zero everywhere when the
343
- * element clips its own overflow.
345
+ * Page-side distances (CSS px, never negative) by which what an element
346
+ * paints reaches beyond its border box on each side: its rendered
347
+ * descendants (uncleared floats, absolutely positioned and transformed
348
+ * children), its text (descenders past a tight line height, read from its
349
+ * scroll size beyond its client size, in transformed px, to the left on an
350
+ * RTL element), and its own outer box shadows and outline (a focus ring).
351
+ * Descendants of an element that clips its overflow (`overflow` other than
352
+ * `visible`) are cut off by it and not counted, nor are fixed ones (they
353
+ * belong to the viewport) or what lies outside the document (skip links at
354
+ * -9999px). Only the shadows and outline count when the element clips its
355
+ * own overflow.
344
356
  */
345
357
  const CONTENT_OVERFLOW_JS = `function () {
346
358
  const view = this.ownerDocument.defaultView;
@@ -365,8 +377,37 @@ const CONTENT_OVERFLOW_JS = `function () {
365
377
  if (!clips(style)) walk(child);
366
378
  }
367
379
  };
368
- if (!clips(view.getComputedStyle(this))) walk(this);
369
- return { left: own.left - reach.left, top: own.top - reach.top, right: reach.right - own.right, bottom: reach.bottom - own.bottom };
380
+ const ownStyle = view.getComputedStyle(this);
381
+ if (!clips(ownStyle)) {
382
+ walk(this);
383
+ const scaleX = this.offsetWidth ? own.width / this.offsetWidth : 1;
384
+ const scaleY = this.offsetHeight ? own.height / this.offsetHeight : 1;
385
+ const wider = Math.max(0, this.scrollWidth - this.clientWidth) * scaleX;
386
+ const taller = Math.max(0, this.scrollHeight - this.clientHeight) * scaleY;
387
+ if (ownStyle.direction === 'rtl') reach.left = Math.min(reach.left, own.left - wider);
388
+ else reach.right = Math.max(reach.right, own.right + wider);
389
+ reach.bottom = Math.max(reach.bottom, own.bottom + taller);
390
+ }
391
+ const ink = { left: 0, top: 0, right: 0, bottom: 0 };
392
+ const grow = (side, amount) => { ink[side] = Math.max(ink[side], amount); };
393
+ for (const layer of ownStyle.boxShadow === 'none' ? [] : ownStyle.boxShadow.split(/,(?![^(]*\\))/)) {
394
+ if (/\\binset\\b/.test(layer)) continue;
395
+ const [x = 0, y = 0, blur = 0, spread = 0] = (layer.replace(/(rgba?|hsla?|color|oklch|lab|lch)\\([^)]*\\)/g, '').match(/-?[\\d.]+px/g) || []).map(parseFloat);
396
+ grow('left', blur + spread - x);
397
+ grow('right', blur + spread + x);
398
+ grow('top', blur + spread - y);
399
+ grow('bottom', blur + spread + y);
400
+ }
401
+ if (ownStyle.outlineStyle !== 'none') {
402
+ const outline = parseFloat(ownStyle.outlineWidth) + parseFloat(ownStyle.outlineOffset);
403
+ ['left', 'top', 'right', 'bottom'].forEach((side) => grow(side, outline));
404
+ }
405
+ return {
406
+ left: Math.max(own.left - reach.left, ink.left),
407
+ top: Math.max(own.top - reach.top, ink.top),
408
+ right: Math.max(reach.right - own.right, ink.right),
409
+ bottom: Math.max(reach.bottom - own.bottom, ink.bottom)
410
+ };
370
411
  }`;
371
412
  /**
372
413
  * The visible viewport (without scrollbars) in CSS px.
@@ -393,27 +434,80 @@ function insideView(area, view) {
393
434
  }
394
435
  /**
395
436
  * Measure the area to capture and, when it fits in the viewport but is not
396
- * in view, scroll it to the middle first. A capture inside the viewport
397
- * keeps the page as it is; one beyond it makes Chrome lay the page out
398
- * without its scrollbar, which moves centered content by half the
399
- * scrollbar's width, so it is used only for areas larger than the viewport.
437
+ * in view, scroll it to the middle first (the returned position puts the
438
+ * page back). A capture inside the viewport keeps the page as it is; one
439
+ * beyond it makes Chrome lay the page out without its scrollbar, so for an
440
+ * area larger than the viewport the scrollbars are hidden first and the
441
+ * area measured in that layout (centered content would else move by half
442
+ * the scrollbar's width).
400
443
  *
401
444
  * @param ref - Node reference
402
- * @returns Border box, area to capture (viewport coordinates) and whether it is in view
445
+ * @param padding - Extra space around the area (CSS px)
446
+ * @returns Border box, area to capture (viewport coordinates), whether it is
447
+ * in view, and the scroll position to restore when it scrolled
403
448
  */
404
- async function measureInView(ref) {
449
+ async function measureInView(ref, padding) {
405
450
  const view = await visibleViewport();
406
451
  let box = await getElementBounds(ref);
407
- let bounds = await captureArea(ref, box);
452
+ let bounds = await captureArea(ref, box, padding);
408
453
  const fits = bounds.width <= view.width && bounds.height <= view.height;
409
- if (fits && !insideView(bounds, view)) {
410
- const dx = bounds.x + bounds.width / 2 - view.width / 2;
411
- const dy = bounds.y + bounds.height / 2 - view.height / 2;
412
- await callCDP('Runtime.evaluate', { expression: `window.scrollBy(${dx}, ${dy})` });
454
+ if (!fits) {
455
+ await keepLayoutWithoutScrollbars(view);
413
456
  box = await getElementBounds(ref);
414
- bounds = await captureArea(ref, box);
457
+ return { box, bounds: await captureArea(ref, box, padding), inView: false };
415
458
  }
416
- return { box, bounds, inView: insideView(bounds, view) };
459
+ if (insideView(bounds, view))
460
+ return { box, bounds, inView: true };
461
+ const scrolledFrom = await scrollPosition();
462
+ const dx = bounds.x + bounds.width / 2 - view.width / 2;
463
+ const dy = bounds.y + bounds.height / 2 - view.height / 2;
464
+ await callBdgScript('Runtime.evaluate', { expression: `window.scrollBy(${dx}, ${dy})` });
465
+ try {
466
+ box = await getElementBounds(ref);
467
+ bounds = await captureArea(ref, box, padding);
468
+ }
469
+ catch (error) {
470
+ await restoreScrollPosition(scrolledFrom);
471
+ throw error;
472
+ }
473
+ return { box, bounds, inView: insideView(bounds, view), scrolledFrom };
474
+ }
475
+ /**
476
+ * Lay the page out at its current width without scrollbars: a capture
477
+ * beyond the viewport hides them, and without this the page would widen by
478
+ * the scrollbar and centered content move after it was measured. The
479
+ * viewport is overridden at the visible width (CSS px, pixel ratio 1; still
480
+ * a phone in a `--mobile` session) until
481
+ * {@link restoreViewport}.
482
+ *
483
+ * @param view - Visible viewport size
484
+ */
485
+ async function keepLayoutWithoutScrollbars(view) {
486
+ const phone = readSessionMetadata()?.viewport?.mobile;
487
+ await callCDP('Emulation.setScrollbarsHidden', { hidden: true });
488
+ await callCDP('Emulation.setDeviceMetricsOverride', viewportOverride({ ...view, ...(phone && { mobile: true }) }, 1));
489
+ }
490
+ /**
491
+ * Put back the viewport a capture changed: the session's `--viewport`, else
492
+ * none, with scrollbars shown.
493
+ */
494
+ async function restoreViewport() {
495
+ await callCDP('Emulation.setScrollbarsHidden', { hidden: false });
496
+ await restoreSessionMetrics();
497
+ }
498
+ /**
499
+ * The page's scroll position.
500
+ *
501
+ * @returns Scroll offsets in CSS px
502
+ */
503
+ async function scrollPosition() {
504
+ const response = await callBdgScript('Runtime.evaluate', {
505
+ expression: '[window.scrollX, window.scrollY]',
506
+ returnByValue: true,
507
+ });
508
+ const value = response.data?.result?.result?.value;
509
+ const [x, y] = Array.isArray(value) ? value : [];
510
+ return { x: x ?? 0, y: y ?? 0 };
417
511
  }
418
512
  /** Overflow (px) below which the capture keeps to the border box (subpixel rounding) */
419
513
  const OVERFLOW_SLACK = 1;
@@ -424,12 +518,33 @@ const OVERFLOW_SLACK = 1;
424
518
  *
425
519
  * @param ref - Node reference
426
520
  * @param bounds - Border box (DOM.getBoxModel coordinates)
427
- * @returns The area, or the border box when nothing overflows (or the page cannot be asked)
521
+ * @param padding - Extra space around it (CSS px)
522
+ * @returns The area, or the border box (with the padding) when nothing
523
+ * overflows (or the page cannot be asked)
428
524
  */
429
- async function captureArea(ref, bounds) {
525
+ async function captureArea(ref, bounds, padding) {
526
+ const area = await paintedArea(ref, bounds);
527
+ return padding > 0
528
+ ? {
529
+ x: area.x - padding,
530
+ y: area.y - padding,
531
+ width: area.width + 2 * padding,
532
+ height: area.height + 2 * padding,
533
+ }
534
+ : area;
535
+ }
536
+ /**
537
+ * The border box grown to what the element paints beyond it
538
+ * ({@link CONTENT_OVERFLOW_JS}).
539
+ *
540
+ * @param ref - Node reference
541
+ * @param bounds - Border box
542
+ * @returns The area
543
+ */
544
+ async function paintedArea(ref, bounds) {
430
545
  const objectGroup = `bdg-shot-${process.pid}`;
431
546
  try {
432
- const resolved = await callCDP('DOM.resolveNode', { ...ref, objectGroup });
547
+ const resolved = await callBdgScript('DOM.resolveNode', { ...ref, objectGroup });
433
548
  const objectId = resolved.data?.result?.object
434
549
  .objectId;
435
550
  if (!objectId)
@@ -471,23 +586,27 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
471
586
  const format = options.format ?? 'png';
472
587
  const quality = format === 'jpeg' ? (options.quality ?? 90) : undefined;
473
588
  const noResize = options.noResize ?? false;
474
- const dprResponse = await callCDP('Runtime.evaluate', {
589
+ const dprResponse = await callBdgScript('Runtime.evaluate', {
475
590
  expression: 'window.devicePixelRatio',
476
591
  returnByValue: true,
477
592
  });
478
593
  const devicePixelRatio = dprResponse.data?.result?.result?.value ?? 1;
479
594
  const before = (await callCDP('Page.getLayoutMetrics', {})).data?.result;
480
595
  const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, before?.visualViewport ?? { clientWidth: 800, clientHeight: 600 });
481
- let box;
482
- let bounds;
483
- let inView;
596
+ const restore = async (wide, scrolledFrom) => {
597
+ await (wide ? restoreViewport() : restoreMetrics());
598
+ if (scrolledFrom)
599
+ await restoreScrollPosition(scrolledFrom);
600
+ };
601
+ let measured;
484
602
  try {
485
- ({ box, bounds, inView } = await measureInView(ref));
603
+ measured = await measureInView(ref, options.padding ?? 0);
486
604
  }
487
605
  catch (error) {
488
- await restoreMetrics();
606
+ await restoreViewport();
489
607
  throw error;
490
608
  }
609
+ const { box, bounds, inView, scrolledFrom } = measured;
491
610
  const originalWidth = bounds.width;
492
611
  const originalHeight = bounds.height;
493
612
  const resized = shouldResize(originalWidth, originalHeight, noResize);
@@ -514,7 +633,7 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
514
633
  screenshotResult = screenshotResponse.data?.result;
515
634
  }
516
635
  finally {
517
- await restoreMetrics();
636
+ await restore(!inView, scrolledFrom);
518
637
  }
519
638
  if (!screenshotResult?.data) {
520
639
  throw new CDPConnectionError('No screenshot data returned', new Error('Empty response'));
@@ -532,6 +651,7 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
532
651
  element: {
533
652
  bounds: roundBounds(onPage(box)),
534
653
  ...(bounds !== box && { captured: roundBounds(clip) }),
654
+ ...(options.padding && { padding: options.padding }),
535
655
  },
536
656
  };
537
657
  if (quality !== undefined) {
@@ -17,14 +17,16 @@
17
17
  */
18
18
  import { Option } from 'commander';
19
19
  import { registerA11yCommands } from './a11y.js';
20
+ import { registerAuditCommand } from './audit.js';
20
21
  import { handleDomEval } from './eval.js';
21
22
  import { registerFormCommand } from './form.js';
22
23
  import { handleDomFrames } from './frames.js';
23
24
  import { DOM_GET_DEFAULT_SELECTOR, handleDomGet } from './get.js';
25
+ import { QUERY_CACHE_LIMIT } from './helpers/query.js';
24
26
  import { registerInspectCommand } from './inspect.js';
25
27
  import { registerLayoutCommand } from './layout.js';
26
28
  import { registerListenersCommand } from './listeners.js';
27
- import { handleDomQuery } from './query.js';
29
+ import { handleDomQuery, QUERY_LIST_LIMIT } from './query.js';
28
30
  import { handleDomScreenshot } from './screenshot.js';
29
31
  import { registerWaitCommand } from './wait.js';
30
32
  import { SELECTOR_OR_INDEX_ARGUMENT, SELECTOR_SCOPE_HELP, } from '../shared/commonOptions.js';
@@ -41,12 +43,14 @@ export function registerDomCommands(program) {
41
43
  registerFormCommand(dom);
42
44
  registerListenersCommand(dom);
43
45
  registerLayoutCommand(dom);
46
+ registerAuditCommand(dom);
44
47
  registerInspectCommand(dom);
45
48
  registerWaitCommand(dom);
46
49
  dom
47
50
  .command('query')
48
51
  .description('Find elements by CSS selector')
49
52
  .argument('<selector>', 'CSS selector (e.g., ".error", "#app", "button")')
53
+ .option('--limit <n>', `Matches to list (default: ${QUERY_LIST_LIMIT}, ${QUERY_CACHE_LIMIT} with --json; 0 = all); the first ${QUERY_CACHE_LIMIT} (or more with a higher limit) are indexed`, integerOption(0))
50
54
  .option('-j, --json', 'Output as JSON')
51
55
  .addHelpText('after', SELECTOR_SCOPE_HELP)
52
56
  .action(async (selector, options) => {
@@ -97,7 +101,8 @@ export function registerDomCommands(program) {
97
101
  .argument('<path>', 'Output file path, or directory for --follow mode')
98
102
  .argument('[selector]', 'Element to capture: CSS selector or index from a query (same as --selector / --index)')
99
103
  .option('--selector <selector>', 'CSS selector for element capture')
100
- .option('--index <number>', 'Cached element index (0-based) from previous query', integerOption(0))
104
+ .option('--index <number>', 'Cached element index (0-based) from a previous query; with --selector, which match', integerOption(0))
105
+ .option('--padding <px>', 'Element capture: extra space around it (shadows and focus rings are included anyway)', integerOption(0, 500))
101
106
  .option('--format <format>', 'Image format: png or jpeg/jpg (default: from the file extension, else png)', screenshotFormatOption)
102
107
  .option('--quality <number>', 'JPEG quality 0-100 (default: 90)', integerOption(0, 100))
103
108
  .option('--no-full-page', 'Capture viewport only (default: full page)')
@@ -2,13 +2,30 @@
2
2
  * `bdg dom query` — find elements by CSS selector and populate the query cache.
3
3
  */
4
4
  import type { DomQueryCommandOptions } from '../shared/optionTypes.js';
5
+ import type { DomQueryResult } from '../../types.js';
6
+ /** Matches `dom query` lists without `--limit` (human output) */
7
+ export declare const QUERY_LIST_LIMIT = 50;
5
8
  /**
6
9
  * Handle `bdg dom query <selector>`.
7
10
  *
8
- * Runs the selector, caches the result set so later commands can reference
9
- * elements by index, and renders the result either as JSON or human output.
11
+ * Runs the selector, caches the described matches so later commands can
12
+ * reference elements by index, and lists the first `--limit` of them (50,
13
+ * or {@link QUERY_CACHE_LIMIT} with `--json`; 0 = all) with the total count.
10
14
  * No match exits 83, like `dom get` and `dom a11y`, and clears the cache so
11
15
  * indices of an earlier query are not used by mistake.
12
16
  */
13
17
  export declare function handleDomQuery(selector: string, options: DomQueryCommandOptions): Promise<void>;
18
+ /**
19
+ * The matches to list: the first `limit` (all with 0), with how many were
20
+ * left out and, when not every match was described, how many can be used by
21
+ * index. Nothing is added when the limit cut nothing (a match that could not
22
+ * be described is just missing, as before). When more than
23
+ * {@link VIEWPORT_HINT_LIMIT} are listed, `viewportChecked` says that only
24
+ * the first ones have a viewport position.
25
+ *
26
+ * @param result - Query result with every described match
27
+ * @param limit - Matches to list (0 = all)
28
+ * @returns Result to output
29
+ */
30
+ export declare function listedMatches(result: DomQueryResult, limit: number): DomQueryResult;
14
31
  //# sourceMappingURL=query.d.ts.map
@@ -1,22 +1,28 @@
1
1
  /**
2
2
  * `bdg dom query` — find elements by CSS selector and populate the query cache.
3
3
  */
4
- import { noMatchesError, queryDOMElements } from './helpers/index.js';
4
+ import { noMatchesError, pageDocumentId, queryDOMElements } from './helpers/index.js';
5
+ import { QUERY_CACHE_LIMIT, VIEWPORT_HINT_LIMIT } from './helpers/query.js';
5
6
  import { runCommand } from '../shared/CommandRunner.js';
6
7
  import { QueryCacheManager } from '../../session/QueryCacheManager.js';
7
8
  import { formatDomQuery } from '../../ui/formatters/dom.js';
8
9
  import { EXIT_CODES } from '../../utils/exitCodes.js';
10
+ /** Matches `dom query` lists without `--limit` (human output) */
11
+ export const QUERY_LIST_LIMIT = 50;
9
12
  /**
10
13
  * Handle `bdg dom query <selector>`.
11
14
  *
12
- * Runs the selector, caches the result set so later commands can reference
13
- * elements by index, and renders the result either as JSON or human output.
15
+ * Runs the selector, caches the described matches so later commands can
16
+ * reference elements by index, and lists the first `--limit` of them (50,
17
+ * or {@link QUERY_CACHE_LIMIT} with `--json`; 0 = all) with the total count.
14
18
  * No match exits 83, like `dom get` and `dom a11y`, and clears the cache so
15
19
  * indices of an earlier query are not used by mistake.
16
20
  */
17
21
  export async function handleDomQuery(selector, options) {
22
+ const limit = options.limit ?? (options.json ? QUERY_CACHE_LIMIT : QUERY_LIST_LIMIT);
18
23
  await runCommand(async () => {
19
- const result = await queryDOMElements(selector);
24
+ const document = await pageDocumentId();
25
+ const result = await queryDOMElements(selector, limit);
20
26
  const cache = QueryCacheManager.getInstance();
21
27
  if (result.count === 0) {
22
28
  await cache.clear();
@@ -28,8 +34,33 @@ export async function handleDomQuery(selector, options) {
28
34
  errorContext: { suggestion: err.suggestion },
29
35
  };
30
36
  }
31
- await cache.set(result);
32
- return { success: true, data: result };
37
+ await cache.set(result, document);
38
+ return { success: true, data: listedMatches(result, limit) };
33
39
  }, options, formatDomQuery);
34
40
  }
41
+ /**
42
+ * The matches to list: the first `limit` (all with 0), with how many were
43
+ * left out and, when not every match was described, how many can be used by
44
+ * index. Nothing is added when the limit cut nothing (a match that could not
45
+ * be described is just missing, as before). When more than
46
+ * {@link VIEWPORT_HINT_LIMIT} are listed, `viewportChecked` says that only
47
+ * the first ones have a viewport position.
48
+ *
49
+ * @param result - Query result with every described match
50
+ * @param limit - Matches to list (0 = all)
51
+ * @returns Result to output
52
+ */
53
+ export function listedMatches(result, limit) {
54
+ const listed = limit === 0 || result.count <= limit
55
+ ? result
56
+ : {
57
+ ...result,
58
+ nodes: result.nodes.slice(0, limit),
59
+ omitted: result.count - Math.min(limit, result.nodes.length),
60
+ ...(result.nodes.length < result.count && { indexed: result.nodes.length }),
61
+ };
62
+ return listed.nodes.length > VIEWPORT_HINT_LIMIT
63
+ ? { ...listed, viewportChecked: VIEWPORT_HINT_LIMIT }
64
+ : listed;
65
+ }
35
66
  //# sourceMappingURL=query.js.map
@@ -3,17 +3,18 @@
3
3
  */
4
4
  import { extname } from 'path';
5
5
  import { DomElementResolver } from './DomElementResolver.js';
6
- import { capturePageScreenshot, captureElementScreenshot, resolveSelector, } from './helpers/index.js';
6
+ import { capturePageScreenshot, captureElementScreenshot, resolveSelector, selectMatch, } from './helpers/index.js';
7
7
  import { runCommand } from '../shared/CommandRunner.js';
8
8
  import { assertFilePath, outputPathError } from '../shared/outputFile.js';
9
9
  import { positiveIntRule } from '../shared/validation.js';
10
10
  import { CommandError } from '../../errors/index.js';
11
- import { conflictingOptionsMessage, conflictingTargetError, genericError, } from '../../errors/messages.js';
11
+ import { conflictingTargetError, genericError } from '../../errors/messages.js';
12
12
  import { missingArgumentError } from '../../errors/messages.js';
13
13
  import { OutputBuilder, buildSuccessResponse } from '../../ui/OutputBuilder.js';
14
14
  import { formatDomScreenshot } from '../../ui/formatters/dom.js';
15
15
  import { createLogger } from '../../ui/logging/index.js';
16
16
  import { delay } from '../../utils/async.js';
17
+ import { makeDirectory } from '../../utils/directories.js';
17
18
  import { getErrorMessage } from '../../utils/errors.js';
18
19
  import { EXIT_CODES } from '../../utils/exitCodes.js';
19
20
  import { filterDefined } from '../../utils/objects.js';
@@ -60,12 +61,16 @@ function buildElementScreenshotOptions(options) {
60
61
  format: options.format,
61
62
  quality: options.quality,
62
63
  noResize: options.resize === false,
64
+ padding: options.padding,
63
65
  });
64
66
  }
65
67
  function hasElementTarget(options) {
66
68
  return options.selector !== undefined || options.index !== undefined;
67
69
  }
68
70
  async function resolveElementNodeId(options) {
71
+ if (options.selector !== undefined && options.index !== undefined) {
72
+ return { backendNodeId: await selectMatch(options.selector, options.index) };
73
+ }
69
74
  if (options.index !== undefined) {
70
75
  const resolver = DomElementResolver.getInstance();
71
76
  const node = await resolver.getNodeIdForIndex(options.index);
@@ -106,7 +111,7 @@ function ensureDirectory(dirPath, fs) {
106
111
  throw new CommandError(`--follow needs a directory, but ${dirPath} is a file`, { suggestion: 'Give a directory for the frames, e.g. bdg dom screenshot ./frames --follow' }, EXIT_CODES.INVALID_ARGUMENTS);
107
112
  }
108
113
  try {
109
- fs.mkdirSync(dirPath, { recursive: true });
114
+ makeDirectory(dirPath);
110
115
  }
111
116
  catch (error) {
112
117
  throw outputPathError(dirPath, error);
@@ -207,8 +212,8 @@ function reportSequenceError(error, captured, json) {
207
212
  process.exit(exitCode);
208
213
  }
209
214
  /**
210
- * Reject options that would be ignored: `--selector` with `--index` (the
211
- * index already names an element), and `--quality` for a PNG.
215
+ * Reject options that would be ignored: `--quality` for a PNG, and
216
+ * `--padding` without an element.
212
217
  *
213
218
  * @param outputPath - File to write
214
219
  * @param options - Command options
@@ -216,8 +221,8 @@ function reportSequenceError(error, captured, json) {
216
221
  */
217
222
  function assertScreenshotOptions(outputPath, options) {
218
223
  let message;
219
- if (options.selector !== undefined && options.index !== undefined) {
220
- message = conflictingOptionsMessage('--selector', '--index');
224
+ if (options.padding !== undefined && !hasElementTarget(options)) {
225
+ message = '--padding applies to element captures; name an element (selector or index)';
221
226
  }
222
227
  else if (options.quality !== undefined &&
223
228
  !options.follow &&
@@ -21,10 +21,11 @@ export interface SemanticNodeWithContext {
21
21
  * link's href, a field's type and name), like `dom query` does, and is
22
22
  * followed by up to 500 characters of the element's text
23
23
  * (all of it with `dom get --full`) when it is longer than the one-line
24
- * preview, or, for an element without text or name, what it holds.
24
+ * preview or the role line shows an accessible name other than the text,
25
+ * or, for an element without text or name, what it holds.
25
26
  *
26
27
  * @param data - Accessibility node and optional DOM context
27
- * @returns Role line, plus a text line for elements with longer text
28
+ * @returns Role line, plus a text line for elements with longer or differently named text
28
29
  */
29
30
  export declare function formatSemanticNodeWithContext(data: SemanticNodeWithContext): string;
30
31
  /**