browser-debugger-cli 0.12.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 (180) hide show
  1. package/.claude/skills/bdg/SKILL.md +4 -4
  2. package/README.md +1 -0
  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/dom/DomElementResolver.d.ts +3 -1
  8. package/dist/commands/dom/DomElementResolver.js +10 -3
  9. package/dist/commands/dom/a11y.js +3 -2
  10. package/dist/commands/dom/eval.d.ts +3 -2
  11. package/dist/commands/dom/eval.js +11 -5
  12. package/dist/commands/dom/form.js +10 -9
  13. package/dist/commands/dom/formInteraction.js +8 -7
  14. package/dist/commands/dom/get.js +8 -8
  15. package/dist/commands/dom/helpers/index.d.ts +1 -1
  16. package/dist/commands/dom/helpers/index.js +1 -1
  17. package/dist/commands/dom/helpers/query.d.ts +27 -3
  18. package/dist/commands/dom/helpers/query.js +152 -64
  19. package/dist/commands/dom/helpers/screenshot.js +13 -13
  20. package/dist/commands/dom/index.js +4 -2
  21. package/dist/commands/dom/query.d.ts +19 -2
  22. package/dist/commands/dom/query.js +37 -6
  23. package/dist/commands/dom/screenshot.js +2 -1
  24. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  25. package/dist/commands/dom/semanticUtils.js +40 -9
  26. package/dist/commands/helpJson.d.ts +82 -19
  27. package/dist/commands/helpJson.js +111 -40
  28. package/dist/commands/helpTopic.d.ts +16 -1
  29. package/dist/commands/helpTopic.js +59 -1
  30. package/dist/commands/installSkill.d.ts +15 -5
  31. package/dist/commands/installSkill.js +86 -16
  32. package/dist/commands/network/list.js +22 -12
  33. package/dist/commands/optionBehaviors.js +33 -11
  34. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  35. package/dist/commands/shared/daemonErrorHandler.js +20 -9
  36. package/dist/commands/shared/dataFetcher.d.ts +12 -4
  37. package/dist/commands/shared/dataFetcher.js +12 -4
  38. package/dist/commands/shared/followMode.d.ts +9 -1
  39. package/dist/commands/shared/followMode.js +22 -4
  40. package/dist/commands/shared/optionTypes.d.ts +4 -1
  41. package/dist/commands/shared/outputFile.js +6 -1
  42. package/dist/commands/start.d.ts +7 -5
  43. package/dist/commands/start.js +65 -21
  44. package/dist/commands/stop.d.ts +11 -0
  45. package/dist/commands/stop.js +24 -1
  46. package/dist/commands.js +1 -1
  47. package/dist/connection/cdp.d.ts +7 -0
  48. package/dist/connection/cdp.js +9 -0
  49. package/dist/connection/launcher.js +3 -2
  50. package/dist/daemon/SessionController.js +6 -1
  51. package/dist/daemon/launcher.d.ts +3 -2
  52. package/dist/daemon/launcher.js +47 -3
  53. package/dist/daemon/session/Session.d.ts +4 -1
  54. package/dist/daemon/session/Session.js +33 -2
  55. package/dist/daemon/session/TelemetryStore.d.ts +8 -1
  56. package/dist/daemon/session/TelemetryStore.js +13 -1
  57. package/dist/daemon/session/commandRegistry.js +29 -13
  58. package/dist/daemon/session/interactions.d.ts +2 -1
  59. package/dist/daemon/session/interactions.js +13 -1
  60. package/dist/daemon/session/plugins.js +16 -2
  61. package/dist/daemon/session/teardown.js +1 -1
  62. package/dist/daemon.js +1622 -748
  63. package/dist/errors/messages.d.ts +54 -11
  64. package/dist/errors/messages.js +109 -22
  65. package/dist/index.js +13733 -8796
  66. package/dist/ipc/client.d.ts +18 -2
  67. package/dist/ipc/client.js +26 -5
  68. package/dist/ipc/protocol/auditTypes.d.ts +8 -2
  69. package/dist/ipc/protocol/commands.d.ts +12 -0
  70. package/dist/ipc/protocol/domTypes.d.ts +12 -0
  71. package/dist/ipc/protocol/inspectTypes.d.ts +2 -0
  72. package/dist/ipc/session/types.d.ts +2 -0
  73. package/dist/runtime/dom/actionEffects.d.ts +5 -1
  74. package/dist/runtime/dom/actionEffects.js +26 -14
  75. package/dist/runtime/dom/audit.js +3 -2
  76. package/dist/runtime/dom/auditModel.js +6 -1
  77. package/dist/runtime/dom/auditScripts.d.ts +9 -3
  78. package/dist/runtime/dom/auditScripts.js +41 -5
  79. package/dist/runtime/dom/elementGeometry.d.ts +10 -3
  80. package/dist/runtime/dom/elementGeometry.js +27 -4
  81. package/dist/runtime/dom/elementInfo.d.ts +74 -18
  82. package/dist/runtime/dom/elementInfo.js +187 -40
  83. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  84. package/dist/runtime/dom/evalHelpers.js +67 -7
  85. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  86. package/dist/runtime/dom/formDiscovery.js +20 -3
  87. package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
  88. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  89. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  90. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  91. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  92. package/dist/runtime/dom/frameLayout.js +1 -0
  93. package/dist/runtime/dom/inspect.js +5 -6
  94. package/dist/runtime/dom/inspectAllStyles.js +1 -0
  95. package/dist/runtime/dom/inspectHints.d.ts +1 -1
  96. package/dist/runtime/dom/inspectModel.d.ts +2 -1
  97. package/dist/runtime/dom/inspectModel.js +7 -3
  98. package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
  99. package/dist/runtime/dom/inspectPaintModel.js +3 -1
  100. package/dist/runtime/dom/inspectScripts.d.ts +29 -2
  101. package/dist/runtime/dom/inspectScripts.js +49 -10
  102. package/dist/runtime/dom/layout.js +9 -7
  103. package/dist/runtime/dom/reactEventHelpers.d.ts +14 -4
  104. package/dist/runtime/dom/reactEventHelpers.js +63 -27
  105. package/dist/runtime/dom/targetNode.d.ts +18 -5
  106. package/dist/runtime/dom/targetNode.js +268 -8
  107. package/dist/runtime/dom/wait.js +2 -1
  108. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  109. package/dist/runtime/page/bdgWorld.js +180 -0
  110. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  111. package/dist/runtime/page/replacedBuiltins.js +136 -0
  112. package/dist/session/QueryCacheManager.d.ts +4 -1
  113. package/dist/session/QueryCacheManager.js +5 -2
  114. package/dist/session/chrome.d.ts +4 -1
  115. package/dist/session/chrome.js +7 -1
  116. package/dist/session/cleanup/staleSession.d.ts +21 -4
  117. package/dist/session/cleanup/staleSession.js +79 -9
  118. package/dist/session/cleanup/userCommands.d.ts +4 -1
  119. package/dist/session/cleanup/userCommands.js +10 -5
  120. package/dist/session/daemonSocket.d.ts +10 -0
  121. package/dist/session/daemonSocket.js +22 -0
  122. package/dist/session/lastSession.d.ts +6 -3
  123. package/dist/session/lastSession.js +11 -5
  124. package/dist/session/paths.d.ts +3 -1
  125. package/dist/session/paths.js +5 -5
  126. package/dist/session/portClaims.js +4 -3
  127. package/dist/session/sessionList.d.ts +13 -5
  128. package/dist/session/sessionList.js +31 -7
  129. package/dist/telemetry/a11y.js +2 -2
  130. package/dist/telemetry/console.d.ts +2 -1
  131. package/dist/telemetry/console.js +30 -21
  132. package/dist/telemetry/pageCrash.d.ts +26 -0
  133. package/dist/telemetry/pageCrash.js +53 -0
  134. package/dist/types.d.ts +16 -0
  135. package/dist/ui/formatters/audit.js +14 -5
  136. package/dist/ui/formatters/cdp.d.ts +138 -0
  137. package/dist/ui/formatters/cdp.js +131 -0
  138. package/dist/ui/formatters/console/chronological.js +3 -1
  139. package/dist/ui/formatters/console/follow.d.ts +2 -1
  140. package/dist/ui/formatters/console/follow.js +2 -2
  141. package/dist/ui/formatters/console/json.d.ts +2 -2
  142. package/dist/ui/formatters/console/json.js +11 -5
  143. package/dist/ui/formatters/console/shared.d.ts +30 -0
  144. package/dist/ui/formatters/console/shared.js +16 -0
  145. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  146. package/dist/ui/formatters/console/summarize.js +40 -9
  147. package/dist/ui/formatters/console.d.ts +2 -1
  148. package/dist/ui/formatters/console.js +7 -5
  149. package/dist/ui/formatters/details.js +3 -1
  150. package/dist/ui/formatters/dom.d.ts +1 -1
  151. package/dist/ui/formatters/dom.js +5 -6
  152. package/dist/ui/formatters/helpFormatters.js +1 -1
  153. package/dist/ui/formatters/inspect.js +9 -3
  154. package/dist/ui/formatters/installSkill.d.ts +9 -1
  155. package/dist/ui/formatters/installSkill.js +32 -6
  156. package/dist/ui/formatters/layout.js +2 -1
  157. package/dist/ui/formatters/networkList.d.ts +1 -1
  158. package/dist/ui/formatters/networkList.js +1 -2
  159. package/dist/ui/formatters/preview.d.ts +2 -0
  160. package/dist/ui/formatters/preview.js +17 -7
  161. package/dist/ui/formatters/sessions.d.ts +2 -2
  162. package/dist/ui/formatters/sessions.js +9 -2
  163. package/dist/ui/logging/logger.d.ts +1 -1
  164. package/dist/ui/messages/commands.d.ts +124 -4
  165. package/dist/ui/messages/commands.js +162 -7
  166. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  167. package/dist/ui/messages/consoleMessages.js +32 -0
  168. package/dist/ui/messages/preview.d.ts +6 -0
  169. package/dist/ui/messages/preview.js +9 -1
  170. package/dist/ui/messages/session.d.ts +13 -2
  171. package/dist/ui/messages/session.js +22 -3
  172. package/dist/utils/directories.d.ts +34 -0
  173. package/dist/utils/directories.js +88 -0
  174. package/dist/utils/display.d.ts +16 -0
  175. package/dist/utils/display.js +42 -0
  176. package/dist/utils/exitCodes.d.ts +1 -0
  177. package/dist/utils/exitCodes.js +6 -0
  178. package/dist/utils/process.d.ts +12 -0
  179. package/dist/utils/process.js +25 -0
  180. package/package.json +1 -1
@@ -10,14 +10,15 @@ import { InvalidArgumentError } from 'commander';
10
10
  import { runElementCommand } from './helpers/runElementCommand.js';
11
11
  import { runCommand } from '../shared/CommandRunner.js';
12
12
  import { jsonOption, SELECTOR_OR_INDEX_ARGUMENT } from '../shared/commonOptions.js';
13
+ import { createErrorResult } from '../shared/dataFetcher.js';
13
14
  import { integerOption } from '../shared/validation.js';
14
15
  import { CommandError } from '../../errors/index.js';
15
- import { VIA_LABEL_SUFFIX, conflictingOptionsMessage, hoverOffWithTargetError, missingArgumentError, indexSourceText, internalError, scrollOptionsError, } from '../../errors/messages.js';
16
+ import { VIA_LABEL_SUFFIX, conflictingOptionsError, hoverOffWithTargetError, missingArgumentError, indexSourceText, internalError, scrollOptionsError, } from '../../errors/messages.js';
16
17
  import { callCDP, domClick, domFill, domPressKey, domScroll, domSubmit } from '../../ipc/client.js';
17
18
  import { findUnknownModifiers } from '../../runtime/dom/keyMapping.js';
18
19
  import { formatTriggeredRequestLines, formatTriggeredRequestsTitle, } from '../../ui/formatters/triggeredRequests.js';
19
20
  import { OutputFormatter } from '../../ui/formatting.js';
20
- import { CLICK_RESULT_WAIT_HELP, HOVER_OFF_DONE, HOVER_USAGE, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, actionStatusLine, dialogConsoleText, moreMessagesText, newMessageText, pageNavigationText, shownElementText, stillChangingNote, } from '../../ui/messages/commands.js';
21
+ import { CLICK_RESULT_WAIT_HELP, HOVER_OFF_DONE, HOVER_USAGE, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, pointerScrollText, actionStatusLine, dialogConsoleText, moreMessagesText, newMessageText, pageNavigationText, shownElementText, stillChangingNote, } from '../../ui/messages/commands.js';
21
22
  import { sessionCommand } from '../../ui/messages/sessionCommand.js';
22
23
  import { EXIT_CODES } from '../../utils/exitCodes.js';
23
24
  /** Help of `--strict` on click and hover */
@@ -288,12 +289,9 @@ async function hoverOff(selectorOrIndex, options) {
288
289
  }, options, () => HOVER_OFF_DONE);
289
290
  }
290
291
  async function runPointerCommand(selectorOrIndex, options, action) {
292
+ const conflict = conflictingOptionsError('--double', '--right');
291
293
  await runCommand(() => options.double && options.right
292
- ? Promise.resolve({
293
- success: false,
294
- error: conflictingOptionsMessage('--double', '--right'),
295
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
296
- })
294
+ ? Promise.resolve(createErrorResult(conflict.message, EXIT_CODES.INVALID_ARGUMENTS, conflict.suggestion))
297
295
  : runElementCommand({
298
296
  selectorOrIndex,
299
297
  index: options.index,
@@ -430,6 +428,9 @@ function formatClickOutput(result) {
430
428
  ...selectorRows(result),
431
429
  elementRow(result),
432
430
  ['Method', result.method === 'dom' ? 'DOM events' : 'mouse events'],
431
+ ...(result.scrolledBy
432
+ ? [['Scrolled', pointerScrollText(result.scrolledBy)]]
433
+ : []),
433
434
  ], result, { action: POINTER_ACTION_NOUN[result.action ?? 'click'] }).build();
434
435
  }
435
436
  /**
@@ -10,7 +10,7 @@ import { getDOMElements, getDomContext, selectMatch, } from './helpers/index.js'
10
10
  import { formatSemanticNodeWithContext, resolveNodeWithFallback, } from './semanticUtils.js';
11
11
  import { runCommand } from '../shared/CommandRunner.js';
12
12
  import { CommandError } from '../../errors/index.js';
13
- import { conflictingOptionsMessage, indexWithIndexOptionError, nodeIdNotFoundError, optionRequiresMessage, } from '../../errors/messages.js';
13
+ import { conflictingOptionsError, indexWithIndexOptionError, nodeIdNotFoundError, optionRequiresError, } from '../../errors/messages.js';
14
14
  import { resolveA11yNode } from '../../telemetry/a11y.js';
15
15
  import { formatDomGet } from '../../ui/formatters/dom.js';
16
16
  import { EXIT_CODES } from '../../utils/exitCodes.js';
@@ -99,23 +99,23 @@ function matchIndex(options) {
99
99
  *
100
100
  * @param selectorOrIndex - Selector or index argument
101
101
  * @param options - Command options
102
- * @returns What conflicts, or null
102
+ * @returns What conflicts and how to fix it, or null
103
103
  */
104
104
  function getOptionsConflict(selectorOrIndex, options) {
105
105
  if (options.nodeId !== undefined && selectorOrIndex !== undefined) {
106
- return conflictingOptionsMessage('--node-id', 'a selector or index');
106
+ return conflictingOptionsError('--node-id', 'a selector or index');
107
107
  }
108
108
  if (options.full && (options.raw || options.nodeId !== undefined)) {
109
- return conflictingOptionsMessage('--full', options.raw ? '--raw' : '--node-id');
109
+ return conflictingOptionsError('--full', options.raw ? '--raw' : '--node-id');
110
110
  }
111
111
  if (options.index !== undefined && options.nth !== undefined) {
112
- return conflictingOptionsMessage('--index', '--nth');
112
+ return conflictingOptionsError('--index', '--nth');
113
113
  }
114
114
  if (options.all && matchIndex(options) !== undefined) {
115
- return conflictingOptionsMessage('--all', '--index');
115
+ return conflictingOptionsError('--all', '--index');
116
116
  }
117
117
  if (options.all && !options.raw)
118
- return optionRequiresMessage('--all', '--raw');
118
+ return optionRequiresError('--all', '--raw');
119
119
  return null;
120
120
  }
121
121
  /**
@@ -130,7 +130,7 @@ function getOptionsConflict(selectorOrIndex, options) {
130
130
  export async function handleDomGet(selectorOrIndex, options) {
131
131
  const conflict = getOptionsConflict(selectorOrIndex, options);
132
132
  if (conflict) {
133
- throw new CommandError(conflict, {}, EXIT_CODES.INVALID_ARGUMENTS);
133
+ throw new CommandError(conflict.message, { suggestion: conflict.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
134
134
  }
135
135
  const { nodeId } = options;
136
136
  if (nodeId !== undefined) {
@@ -5,7 +5,7 @@
5
5
  * - `query.ts` — selector → backend node ids (shadow roots, same-origin iframes), DOM.describeNode
6
6
  * - `screenshot.ts` — page / element capture, element bounds, scroll helpers
7
7
  */
8
- export { documentReadyState, noMatchesError, queryDOMElements, getDomContext, getDOMElements, resolveSelector, resolveBackendNodeIds, selectMatch, assertNodeAttached, } from './query.js';
8
+ export { documentReadyState, noMatchesError, queryDOMElements, getDomContext, getDOMElements, resolveSelector, resolveBackendNodeIds, selectMatch, assertNodeAttached, pageDocumentId, } from './query.js';
9
9
  export { capturePageScreenshot, captureElementScreenshot, getElementBounds, } from './screenshot.js';
10
10
  export type { DomQueryResult, DomGetResult, ScreenshotResult, DomGetOptions, ScreenshotOptions, DomContext, ElementBounds, } from '../../../types.js';
11
11
  //# sourceMappingURL=index.d.ts.map
@@ -5,6 +5,6 @@
5
5
  * - `query.ts` — selector → backend node ids (shadow roots, same-origin iframes), DOM.describeNode
6
6
  * - `screenshot.ts` — page / element capture, element bounds, scroll helpers
7
7
  */
8
- export { documentReadyState, noMatchesError, queryDOMElements, getDomContext, getDOMElements, resolveSelector, resolveBackendNodeIds, selectMatch, assertNodeAttached, } from './query.js';
8
+ export { documentReadyState, noMatchesError, queryDOMElements, getDomContext, getDOMElements, resolveSelector, resolveBackendNodeIds, selectMatch, assertNodeAttached, pageDocumentId, } from './query.js';
9
9
  export { capturePageScreenshot, captureElementScreenshot, getElementBounds, } from './screenshot.js';
10
10
  //# sourceMappingURL=index.js.map
@@ -38,13 +38,26 @@ export declare function noMatchContext(selector: string): Promise<NoMatchContext
38
38
  * @returns The state, or undefined when the page did not answer
39
39
  */
40
40
  export declare function documentReadyState(): Promise<string | undefined>;
41
+ /** Matches whose viewport position `dom query` reports (measuring is not free) */
42
+ export declare const VIEWPORT_HINT_LIMIT = 100;
41
43
  /**
42
- * Query elements by selector for `bdg dom query`.
44
+ * Matches `dom query` describes and caches for use by index at least, beyond
45
+ * those it lists: describing costs a round trip per element, so a page with
46
+ * 50000 matches is not described whole unless `--limit 0` asks for it.
47
+ */
48
+ export declare const QUERY_CACHE_LIMIT = 1000;
49
+ /**
50
+ * Query elements by selector for `bdg dom query`: all matches are counted,
51
+ * and the first `limit` (at least {@link QUERY_CACHE_LIMIT}, so indices past
52
+ * the listed ones still work) are described in one page-side pass plus one
53
+ * `DOM.describeNode` each for their backend node id.
43
54
  *
44
55
  * @param selector - CSS selector
45
- * @returns Matches with backend node ids, tags, classes and text previews
56
+ * @param limit - Matches the caller lists (0 = all, so all are described)
57
+ * @returns The match count, and the described matches with backend node
58
+ * ids, tags, key attributes, classes and text previews
46
59
  */
47
- export declare function queryDOMElements(selector: string): Promise<DomQueryResult>;
60
+ export declare function queryDOMElements(selector: string, limit?: number): Promise<DomQueryResult>;
48
61
  /**
49
62
  * Get DOM context (tag, classes, key attributes, text preview) for a node: a one-line
50
63
  * preview, and up to {@link ELEMENT_TEXT_LENGTH} characters of text when it
@@ -102,4 +115,15 @@ export declare function resolveBackendNodeIds(selectors: string[]): Promise<(num
102
115
  * @throws CommandError (87) when the element is gone
103
116
  */
104
117
  export declare function assertNodeAttached(backendNodeId: number, source?: IndexSource): Promise<void>;
118
+ /**
119
+ * Identity of the page's current document: its time origin, which every
120
+ * document load gets anew. Cached backend node ids belong to one document; a
121
+ * page loaded in a new renderer process reuses the same ids for other
122
+ * elements, so an index of an older document must not be used.
123
+ *
124
+ * @returns The identity, or undefined when the page cannot be asked
125
+ * @throws CommandError when the page is busy (102) or crashed (107), as the
126
+ * command itself would
127
+ */
128
+ export declare function pageDocumentId(): Promise<string | undefined>;
105
129
  //# sourceMappingURL=query.d.ts.map
@@ -12,9 +12,9 @@
12
12
  import { keyAttributes } from './keyAttributes.js';
13
13
  import { CommandError } from '../../../errors/index.js';
14
14
  import { noNodesFoundError, indexOutOfRangeError, eitherArgumentRequiredError, invalidSelectorError, nodeIdNotFoundError, operationFailedError, similarSelectorsLine, staleNodeError, } from '../../../errors/messages.js';
15
- import { callCDP } from '../../../ipc/client.js';
15
+ import { callBdgScript, callCDP } from '../../../ipc/client.js';
16
16
  import { ELEMENT_GEOMETRY_JS, VIEWPORT_SIZE_JS, classifyViewportPosition, } from '../../../runtime/dom/elementGeometry.js';
17
- import { ELEMENT_CONTEXT_JS, ELEMENT_STATE_JS, ELEMENT_TEXT_JS, ELEMENT_TEXT_LENGTH, textPreview, } from '../../../runtime/dom/elementInfo.js';
17
+ import { ELEMENT_CONTEXT_JS, ELEMENT_STATE_JS, ELEMENT_TEXT_JS, ELEMENT_TEXT_LENGTH, textPreview, isLabelClass, } from '../../../runtime/dom/elementInfo.js';
18
18
  import { DEEP_QUERY_JS, UNSEARCHED_CONTENT_JS, pageNamesJS, selectorArgsJS, } from '../../../runtime/dom/targetNode.js';
19
19
  import { createLogger } from '../../../ui/logging/index.js';
20
20
  import { sessionCommand } from '../../../ui/messages/sessionCommand.js';
@@ -92,7 +92,7 @@ async function selectAll(selector) {
92
92
  async function withSelection(selector, use) {
93
93
  await callCDP('DOM.enable', {});
94
94
  const objectGroup = `bdg-query-${process.pid}-${++queryCount}`;
95
- const evaluated = await callCDP('Runtime.evaluate', {
95
+ const evaluated = await callBdgScript('Runtime.evaluate', {
96
96
  expression: `(${DEEP_QUERY_JS})(${selectorArgsJS(selector)})`,
97
97
  objectGroup,
98
98
  });
@@ -163,7 +163,7 @@ export async function noMatchContext(selector) {
163
163
  const compounds = leadingCompounds(selector);
164
164
  const shadowHost = compounds.length > 0 ? shadowHostJS(compounds) : '-1';
165
165
  try {
166
- const evaluated = await callCDP('Runtime.evaluate', {
166
+ const evaluated = await callBdgScript('Runtime.evaluate', {
167
167
  expression: `({ hidden: ${hidden}, readyState: document.readyState, unsearched: ${UNSEARCHED_CONTENT_JS}, names: ${names}, shadowHost: ${shadowHost} })`,
168
168
  returnByValue: true,
169
169
  });
@@ -199,7 +199,7 @@ export async function noMatchContext(selector) {
199
199
  */
200
200
  export async function documentReadyState() {
201
201
  try {
202
- const evaluated = await callCDP('Runtime.evaluate', {
202
+ const evaluated = await callBdgScript('Runtime.evaluate', {
203
203
  expression: 'document.readyState',
204
204
  returnByValue: true,
205
205
  });
@@ -212,46 +212,63 @@ export async function documentReadyState() {
212
212
  }
213
213
  }
214
214
  /** Matches whose viewport position `dom query` reports (measuring is not free) */
215
- const VIEWPORT_HINT_LIMIT = 100;
215
+ export const VIEWPORT_HINT_LIMIT = 100;
216
216
  /**
217
- * Where each element of a page-side array lives (an iframe and/or a shadow
218
- * root), its text, its form control state ({@link ELEMENT_STATE_JS}) and, for
219
- * the first {@link VIEWPORT_HINT_LIMIT}, its position
220
- * relative to the viewport, plus the viewport size. An element that cannot be
221
- * read gets empty details instead of failing the whole query.
217
+ * Matches `dom query` describes and caches for use by index at least, beyond
218
+ * those it lists: describing costs a round trip per element, so a page with
219
+ * 50000 matches is not described whole unless `--limit 0` asks for it.
222
220
  */
223
- const ELEMENT_DETAILS_FUNCTION = `function () {
221
+ export const QUERY_CACHE_LIMIT = 1000;
222
+ /**
223
+ * For the first `count` elements of a page-side array: the tag and
224
+ * attributes, where it lives (an iframe and/or a shadow root), its text, its
225
+ * form control state ({@link ELEMENT_STATE_JS}) and, for the first
226
+ * {@link VIEWPORT_HINT_LIMIT}, its position relative to the viewport; plus
227
+ * the viewport size and how many elements the array has. An element that
228
+ * cannot be read gets empty details instead of failing the whole query.
229
+ */
230
+ const ELEMENT_DETAILS_FUNCTION = `function (count) {
224
231
  const contextOf = ${ELEMENT_CONTEXT_JS};
225
232
  const textOf = ${ELEMENT_TEXT_JS};
226
233
  const geometryOf = ${ELEMENT_GEOMETRY_JS};
227
234
  const stateOf = ${ELEMENT_STATE_JS};
228
235
  const read = (el, index) => {
229
236
  try {
230
- return { context: contextOf(el), text: textOf(el), state: stateOf(el), geometry: index < ${VIEWPORT_HINT_LIMIT} ? geometryOf(el) : null };
237
+ const attributes = [];
238
+ for (const attribute of Array.from(el.attributes || [])) attributes.push(attribute.name, attribute.value);
239
+ return { tag: el.nodeName, attributes: attributes, context: contextOf(el), text: textOf(el), state: stateOf(el), geometry: index < ${VIEWPORT_HINT_LIMIT} ? geometryOf(el) : null };
231
240
  } catch (e) {
232
241
  return {};
233
242
  }
234
243
  };
235
- return { viewport: (${VIEWPORT_SIZE_JS})(window), elements: Array.from(this, read) };
244
+ return { total: this.length, viewport: (${VIEWPORT_SIZE_JS})(window), elements: Array.from(Array.prototype.slice.call(this, 0, count), read) };
236
245
  }`;
246
+ /** Page-side: the first `count` elements of the array, as a new array */
247
+ const SLICE_FUNCTION = `function (count) { return Array.prototype.slice.call(this, 0, count); }`;
237
248
  /**
238
- * Backend node ids of the elements in a page-side array, each with where it
239
- * lives (empty for the main document), its text and its viewport position.
249
+ * The first `count` elements of a page-side array with their backend node
250
+ * ids, tag and attributes, where each lives (empty for the main document),
251
+ * its text and its viewport position, and how many elements the array has.
240
252
  *
241
253
  * @param arrayObjectId - Remote object id of the array
242
- * @returns Elements in array order (ones that cannot be described are left out)
254
+ * @param count - Elements to describe (0 = all)
255
+ * @returns Elements in array order (ones that cannot be described are left
256
+ * out) and the array's length
243
257
  * @throws CommandError (91) when the page could not describe the matches
244
258
  */
245
- async function elementsWithDetails(arrayObjectId) {
246
- const { viewport, elements = [] } = await readPageDetails(arrayObjectId);
247
- const ids = await elementBackendNodeIds(arrayObjectId);
248
- return ids.flatMap((backendNodeId, index) => {
259
+ async function elementsWithDetails(arrayObjectId, count) {
260
+ const limit = count === 0 ? Number.MAX_SAFE_INTEGER : count;
261
+ const { total = 0, viewport, elements = [] } = await readPageDetails(arrayObjectId, limit);
262
+ const ids = await elementBackendNodeIds(total > limit ? await sliceArray(arrayObjectId, limit) : arrayObjectId);
263
+ const described = ids.flatMap((backendNodeId, index) => {
249
264
  if (backendNodeId === undefined)
250
265
  return [];
251
266
  const details = elements[index];
252
267
  return [
253
268
  {
254
269
  backendNodeId,
270
+ tag: details?.tag ?? '',
271
+ attributes: details?.attributes ?? [],
255
272
  context: details?.context ?? '',
256
273
  text: details?.text ?? '',
257
274
  state: details?.state ?? {},
@@ -259,18 +276,44 @@ async function elementsWithDetails(arrayObjectId) {
259
276
  },
260
277
  ];
261
278
  });
279
+ return { total, elements: described };
280
+ }
281
+ /**
282
+ * A page-side array of the first elements of another (in the same object
283
+ * group), so only those are described.
284
+ *
285
+ * @param arrayObjectId - Remote object id of the array
286
+ * @param count - Elements to keep
287
+ * @returns Remote object id of the shorter array
288
+ * @throws CommandError (91) when the page could not slice it
289
+ */
290
+ async function sliceArray(arrayObjectId, count) {
291
+ const response = await callCDP('Runtime.callFunctionOn', {
292
+ objectId: arrayObjectId,
293
+ functionDeclaration: SLICE_FUNCTION,
294
+ arguments: [{ value: count }],
295
+ });
296
+ const objectId = response.data?.result
297
+ ?.result?.objectId;
298
+ if (!objectId) {
299
+ const err = operationFailedError('describe the matches', response.error ?? 'no result');
300
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SCRIPT_ERROR);
301
+ }
302
+ return objectId;
262
303
  }
263
304
  /**
264
305
  * Run {@link ELEMENT_DETAILS_FUNCTION} on a page-side array.
265
306
  *
266
307
  * @param arrayObjectId - Remote object id of the array
267
- * @returns Details of each element and the viewport size
308
+ * @param count - Elements to describe, from the start
309
+ * @returns Details of those elements, the array's length and the viewport size
268
310
  * @throws CommandError (91) when the script failed
269
311
  */
270
- async function readPageDetails(arrayObjectId) {
312
+ async function readPageDetails(arrayObjectId, count) {
271
313
  const response = await callCDP('Runtime.callFunctionOn', {
272
314
  objectId: arrayObjectId,
273
315
  functionDeclaration: ELEMENT_DETAILS_FUNCTION,
316
+ arguments: [{ value: count }],
274
317
  returnByValue: true,
275
318
  });
276
319
  const result = response.data?.result;
@@ -359,40 +402,49 @@ function mapConcurrently(items, mapper) {
359
402
  return Promise.all(items.map((item, index) => limiter.run(() => mapper(item, index))));
360
403
  }
361
404
  /**
362
- * Query elements by selector for `bdg dom query`.
405
+ * Query elements by selector for `bdg dom query`: all matches are counted,
406
+ * and the first `limit` (at least {@link QUERY_CACHE_LIMIT}, so indices past
407
+ * the listed ones still work) are described in one page-side pass plus one
408
+ * `DOM.describeNode` each for their backend node id.
363
409
  *
364
410
  * @param selector - CSS selector
365
- * @returns Matches with backend node ids, tags, classes and text previews
411
+ * @param limit - Matches the caller lists (0 = all, so all are described)
412
+ * @returns The match count, and the described matches with backend node
413
+ * ids, tags, key attributes, classes and text previews
366
414
  */
367
- export async function queryDOMElements(selector) {
368
- const elements = await withSelection(selector, elementsWithDetails);
369
- if (elements.length > 20) {
370
- log.debug(`Querying ${elements.length} elements with selector: ${selector}`);
371
- }
372
- const nodes = await mapConcurrently(elements, async (element, index) => {
373
- const { backendNodeId, context, text, state, inViewport, clippedBy } = element;
374
- const desc = await describeNode({ backendNodeId });
375
- if (!desc)
376
- return { index, nodeId: 0 };
377
- const attributes = unpackAttributes(desc.attributes);
378
- const classes = attributes['class']?.split(/\s+/).filter(Boolean);
379
- const preview = textPreview(text);
380
- const tag = desc.nodeName.toLowerCase();
381
- const keys = keyAttributes(tag, attributes, state);
382
- return {
383
- index,
384
- nodeId: desc.backendNodeId,
385
- tag,
386
- ...identifyingAttributes(attributes, desc.nodeName),
387
- ...(keys && { attributes: keys }),
388
- ...(classes && { classes }),
389
- ...(preview && { preview }),
390
- ...(context && { context }),
391
- ...(inViewport && { inViewport }),
392
- ...(clippedBy && { clippedBy }),
393
- };
394
- });
395
- return { selector, count: nodes.length, nodes };
415
+ export async function queryDOMElements(selector, limit = 0) {
416
+ const described = limit === 0 ? 0 : Math.max(limit, QUERY_CACHE_LIMIT);
417
+ const { total, elements } = await withSelection(selector, (arrayObjectId) => elementsWithDetails(arrayObjectId, described));
418
+ if (total > 20)
419
+ log.debug(`Queried ${total} elements with selector: ${selector}`);
420
+ return { selector, count: total, nodes: elements.map(queryNode) };
421
+ }
422
+ /**
423
+ * One described match as `dom query` reports it.
424
+ *
425
+ * @param element - Match with its page-side details
426
+ * @param index - Its position among the matches
427
+ * @returns Query node
428
+ */
429
+ function queryNode(element, index) {
430
+ const { backendNodeId, context, text, state, inViewport, clippedBy } = element;
431
+ const attributes = unpackAttributes(element.attributes);
432
+ const classes = attributes['class']?.split(/\s+/).filter(Boolean);
433
+ const preview = textPreview(text);
434
+ const tag = element.tag.toLowerCase();
435
+ const keys = keyAttributes(tag, attributes, state);
436
+ return {
437
+ index,
438
+ nodeId: backendNodeId,
439
+ tag,
440
+ ...identifyingAttributes(attributes, element.tag),
441
+ ...(keys && { attributes: keys }),
442
+ ...(classes && { classes }),
443
+ ...(preview && { preview }),
444
+ ...(context && { context }),
445
+ ...(inViewport && { inViewport }),
446
+ ...(clippedBy && { clippedBy }),
447
+ };
396
448
  }
397
449
  /**
398
450
  * The attributes that tell similar elements apart (form fields especially).
@@ -420,7 +472,7 @@ function identifyingAttributes(attributes, nodeName) {
420
472
  */
421
473
  async function elementTextAndState(ref, full) {
422
474
  const objectGroup = `bdg-text-${process.pid}-${++queryCount}`;
423
- const resolved = await callCDP('DOM.resolveNode', { ...ref, objectGroup });
475
+ const resolved = await callBdgScript('DOM.resolveNode', { ...ref, objectGroup });
424
476
  const objectId = resolved.data?.result?.object
425
477
  .objectId;
426
478
  if (!objectId)
@@ -478,33 +530,40 @@ export async function getDomContext(ref, options = {}) {
478
530
  }
479
531
  /** Child elements named for an element without text */
480
532
  const CHILDREN_LISTED = 5;
533
+ /** Children that show nothing themselves, left out of the listing */
534
+ const UNSHOWN_CHILD = /^(SCRIPT|STYLE|LINK|TEMPLATE)$/;
481
535
  /**
482
536
  * The child elements of an element (for one without text: a body holding
483
- * only an iframe, an empty app root).
537
+ * only an iframe, an empty app root). A web component is described by its
538
+ * shadow root's children, which is what it shows (an icon button named
539
+ * there); scripts, styles and templates are left out.
484
540
  *
485
541
  * @param ref - Node reference
486
- * @returns The first {@link CHILDREN_LISTED} as `tag#id.class` and how many there are
542
+ * @returns The first {@link CHILDREN_LISTED} as `tag#id.class "label"`, how many there are, and whether they are in its shadow root
487
543
  */
488
544
  async function childElements(ref) {
489
- const response = await callCDP('DOM.describeNode', { ...ref, depth: 1 });
545
+ const response = await callCDP('DOM.describeNode', { ...ref, depth: 2, pierce: true });
490
546
  const node = response.data?.result?.node;
491
- const elements = (node?.children ?? []).filter((child) => child.nodeType === 1);
547
+ const shadowRoot = node?.shadowRoots?.find((root) => root.shadowRootType !== 'user-agent');
548
+ const elements = ((shadowRoot ?? node)?.children ?? []).filter((child) => child.nodeType === 1 && !UNSHOWN_CHILD.test(child.nodeName));
492
549
  return {
493
550
  children: elements.slice(0, CHILDREN_LISTED).map(childLabel),
494
551
  childCount: elements.length,
552
+ ...(shadowRoot && { shadowChildren: true }),
495
553
  };
496
554
  }
497
555
  /**
498
- * A child element in a few characters.
556
+ * A child element in a few characters, with its aria-label when it has one.
499
557
  *
500
558
  * @param node - Child node
501
- * @returns e.g. `iframe#app.full`
559
+ * @returns e.g. `iframe#app.full`, `button.icon "Close"`
502
560
  */
503
561
  function childLabel(node) {
504
562
  const attributes = unpackAttributes(node.attributes);
505
563
  const id = attributes['id'] ? `#${attributes['id']}` : '';
506
- const classes = (attributes['class'] ?? '').split(/\s+/).filter(Boolean).slice(0, 2);
507
- return `${node.nodeName.toLowerCase()}${id}${classes.map((name) => `.${name}`).join('')}`;
564
+ const classes = (attributes['class'] ?? '').split(/\s+/).filter(isLabelClass).slice(0, 2);
565
+ const label = attributes['aria-label']?.trim();
566
+ return `${node.nodeName.toLowerCase()}${id}${classes.map((name) => `.${name}`).join('')}${label ? ` "${label}"` : ''}`;
508
567
  }
509
568
  /**
510
569
  * All matches of a selector.
@@ -629,7 +688,10 @@ export async function resolveBackendNodeIds(selectors) {
629
688
  * @throws CommandError (87) when the element is gone
630
689
  */
631
690
  export async function assertNodeAttached(backendNodeId, source) {
632
- const resolved = await callCDP('DOM.resolveNode', { backendNodeId, objectGroup: 'bdg-check' });
691
+ const resolved = await callBdgScript('DOM.resolveNode', {
692
+ backendNodeId,
693
+ objectGroup: 'bdg-check',
694
+ });
633
695
  const objectId = resolved.data?.result?.object
634
696
  .objectId;
635
697
  let attached = false;
@@ -648,4 +710,30 @@ export async function assertNodeAttached(backendNodeId, source) {
648
710
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
649
711
  }
650
712
  }
713
+ /**
714
+ * Identity of the page's current document: its time origin, which every
715
+ * document load gets anew. Cached backend node ids belong to one document; a
716
+ * page loaded in a new renderer process reuses the same ids for other
717
+ * elements, so an index of an older document must not be used.
718
+ *
719
+ * @returns The identity, or undefined when the page cannot be asked
720
+ * @throws CommandError when the page is busy (102) or crashed (107), as the
721
+ * command itself would
722
+ */
723
+ export async function pageDocumentId() {
724
+ try {
725
+ const evaluated = await callBdgScript('Runtime.evaluate', {
726
+ expression: 'String(performance.timeOrigin)',
727
+ returnByValue: true,
728
+ });
729
+ const { result } = (evaluated.data?.result ?? {});
730
+ return typeof result?.value === 'string' ? result.value : undefined;
731
+ }
732
+ catch (error) {
733
+ if (error instanceof CommandError)
734
+ throw error;
735
+ log.debug(`Page document not identified: ${getErrorMessage(error)}`);
736
+ return undefined;
737
+ }
738
+ }
651
739
  //# sourceMappingURL=query.js.map
@@ -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
  });
@@ -245,7 +245,7 @@ export async function capturePageScreenshot(outputPath, options = {}) {
245
245
  if (options.scroll) {
246
246
  originalScrollPosition = await scrollToElement(options.scroll);
247
247
  }
248
- const dprResponse = await callCDP('Runtime.evaluate', {
248
+ const dprResponse = await callBdgScript('Runtime.evaluate', {
249
249
  expression: 'window.devicePixelRatio',
250
250
  returnByValue: true,
251
251
  });
@@ -266,7 +266,7 @@ export async function capturePageScreenshot(outputPath, options = {}) {
266
266
  const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, viewport);
267
267
  if (devicePixelRatio !== 1) {
268
268
  if (options.scroll) {
269
- await callCDP('Runtime.evaluate', {
269
+ await callBdgScript('Runtime.evaluate', {
270
270
  expression: `(${DEEP_QUERY_JS})(${selectorArgsJS(options.scroll)})[0]?.scrollIntoView({ block: 'center', behavior: 'instant' })`,
271
271
  returnByValue: true,
272
272
  });
@@ -461,7 +461,7 @@ async function measureInView(ref, padding) {
461
461
  const scrolledFrom = await scrollPosition();
462
462
  const dx = bounds.x + bounds.width / 2 - view.width / 2;
463
463
  const dy = bounds.y + bounds.height / 2 - view.height / 2;
464
- await callCDP('Runtime.evaluate', { expression: `window.scrollBy(${dx}, ${dy})` });
464
+ await callBdgScript('Runtime.evaluate', { expression: `window.scrollBy(${dx}, ${dy})` });
465
465
  try {
466
466
  box = await getElementBounds(ref);
467
467
  bounds = await captureArea(ref, box, padding);
@@ -501,7 +501,7 @@ async function restoreViewport() {
501
501
  * @returns Scroll offsets in CSS px
502
502
  */
503
503
  async function scrollPosition() {
504
- const response = await callCDP('Runtime.evaluate', {
504
+ const response = await callBdgScript('Runtime.evaluate', {
505
505
  expression: '[window.scrollX, window.scrollY]',
506
506
  returnByValue: true,
507
507
  });
@@ -544,7 +544,7 @@ async function captureArea(ref, bounds, padding) {
544
544
  async function paintedArea(ref, bounds) {
545
545
  const objectGroup = `bdg-shot-${process.pid}`;
546
546
  try {
547
- const resolved = await callCDP('DOM.resolveNode', { ...ref, objectGroup });
547
+ const resolved = await callBdgScript('DOM.resolveNode', { ...ref, objectGroup });
548
548
  const objectId = resolved.data?.result?.object
549
549
  .objectId;
550
550
  if (!objectId)
@@ -586,7 +586,7 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
586
586
  const format = options.format ?? 'png';
587
587
  const quality = format === 'jpeg' ? (options.quality ?? 90) : undefined;
588
588
  const noResize = options.noResize ?? false;
589
- const dprResponse = await callCDP('Runtime.evaluate', {
589
+ const dprResponse = await callBdgScript('Runtime.evaluate', {
590
590
  expression: 'window.devicePixelRatio',
591
591
  returnByValue: true,
592
592
  });
@@ -17,15 +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
- import { registerAuditCommand } from './audit.js';
26
27
  import { registerLayoutCommand } from './layout.js';
27
28
  import { registerListenersCommand } from './listeners.js';
28
- import { handleDomQuery } from './query.js';
29
+ import { handleDomQuery, QUERY_LIST_LIMIT } from './query.js';
29
30
  import { handleDomScreenshot } from './screenshot.js';
30
31
  import { registerWaitCommand } from './wait.js';
31
32
  import { SELECTOR_OR_INDEX_ARGUMENT, SELECTOR_SCOPE_HELP, } from '../shared/commonOptions.js';
@@ -49,6 +50,7 @@ export function registerDomCommands(program) {
49
50
  .command('query')
50
51
  .description('Find elements by CSS selector')
51
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))
52
54
  .option('-j, --json', 'Output as JSON')
53
55
  .addHelpText('after', SELECTOR_SCOPE_HELP)
54
56
  .action(async (selector, options) => {