browser-debugger-cli 0.12.0 → 0.14.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 (224) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +5 -4
  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 +67 -13
  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.d.ts +1 -1
  10. package/dist/commands/dom/a11y.js +23 -22
  11. package/dist/commands/dom/eval.d.ts +4 -2
  12. package/dist/commands/dom/eval.js +31 -7
  13. package/dist/commands/dom/form.js +10 -9
  14. package/dist/commands/dom/formInteraction.js +9 -8
  15. package/dist/commands/dom/get.js +32 -14
  16. package/dist/commands/dom/helpers/index.d.ts +1 -1
  17. package/dist/commands/dom/helpers/index.js +1 -1
  18. package/dist/commands/dom/helpers/query.d.ts +27 -3
  19. package/dist/commands/dom/helpers/query.js +152 -64
  20. package/dist/commands/dom/helpers/screenshot.js +13 -13
  21. package/dist/commands/dom/index.js +10 -3
  22. package/dist/commands/dom/query.d.ts +20 -2
  23. package/dist/commands/dom/query.js +39 -6
  24. package/dist/commands/dom/screenshot.js +3 -1
  25. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  26. package/dist/commands/dom/semanticUtils.js +40 -9
  27. package/dist/commands/helpJson.d.ts +82 -19
  28. package/dist/commands/helpJson.js +112 -41
  29. package/dist/commands/helpTopic.d.ts +16 -1
  30. package/dist/commands/helpTopic.js +59 -1
  31. package/dist/commands/installSkill.d.ts +15 -5
  32. package/dist/commands/installSkill.js +86 -16
  33. package/dist/commands/network/list.js +65 -12
  34. package/dist/commands/optionBehaviors.d.ts +25 -2
  35. package/dist/commands/optionBehaviors.js +81 -46
  36. package/dist/commands/peek.js +3 -0
  37. package/dist/commands/shared/CommandRunner.js +13 -13
  38. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  39. package/dist/commands/shared/daemonErrorHandler.js +21 -10
  40. package/dist/commands/shared/dataFetcher.d.ts +14 -4
  41. package/dist/commands/shared/dataFetcher.js +20 -4
  42. package/dist/commands/shared/followMode.d.ts +9 -1
  43. package/dist/commands/shared/followMode.js +22 -4
  44. package/dist/commands/shared/handleValidationError.js +3 -3
  45. package/dist/commands/shared/optionTypes.d.ts +17 -3
  46. package/dist/commands/shared/outputFile.js +6 -1
  47. package/dist/commands/shared/startHelpers.js +3 -3
  48. package/dist/commands/start.d.ts +7 -5
  49. package/dist/commands/start.js +65 -21
  50. package/dist/commands/stop.d.ts +11 -0
  51. package/dist/commands/stop.js +24 -1
  52. package/dist/commands.js +1 -1
  53. package/dist/connection/cdp.d.ts +7 -0
  54. package/dist/connection/cdp.js +9 -0
  55. package/dist/connection/chromeIdentity.d.ts +8 -2
  56. package/dist/connection/chromeIdentity.js +85 -13
  57. package/dist/connection/launcher.js +3 -2
  58. package/dist/constants.d.ts +29 -1
  59. package/dist/constants.js +35 -1
  60. package/dist/daemon/SessionController.js +8 -1
  61. package/dist/daemon/launcher.d.ts +3 -2
  62. package/dist/daemon/launcher.js +47 -3
  63. package/dist/daemon/session/Session.d.ts +5 -1
  64. package/dist/daemon/session/Session.js +42 -3
  65. package/dist/daemon/session/TelemetryStore.d.ts +15 -1
  66. package/dist/daemon/session/TelemetryStore.js +19 -1
  67. package/dist/daemon/session/commandRegistry.js +52 -18
  68. package/dist/daemon/session/interactions.d.ts +2 -1
  69. package/dist/daemon/session/interactions.js +13 -1
  70. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  71. package/dist/daemon/session/matchedStylesReset.js +46 -0
  72. package/dist/daemon/session/plugins.js +17 -2
  73. package/dist/daemon/session/teardown.js +1 -1
  74. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  75. package/dist/daemon/session/triggeredRequests.js +13 -7
  76. package/dist/daemon.js +2385 -1229
  77. package/dist/errors/messages.d.ts +62 -11
  78. package/dist/errors/messages.js +119 -22
  79. package/dist/index.js +14995 -9866
  80. package/dist/ipc/client.d.ts +18 -2
  81. package/dist/ipc/client.js +26 -5
  82. package/dist/ipc/protocol/auditTypes.d.ts +8 -2
  83. package/dist/ipc/protocol/commands.d.ts +16 -0
  84. package/dist/ipc/protocol/domTypes.d.ts +12 -0
  85. package/dist/ipc/protocol/inspectTypes.d.ts +7 -2
  86. package/dist/ipc/session/types.d.ts +7 -1
  87. package/dist/program.d.ts +14 -0
  88. package/dist/program.js +53 -0
  89. package/dist/runtime/dom/actionEffects.d.ts +5 -1
  90. package/dist/runtime/dom/actionEffects.js +26 -14
  91. package/dist/runtime/dom/audit.js +3 -2
  92. package/dist/runtime/dom/auditModel.js +6 -1
  93. package/dist/runtime/dom/auditScripts.d.ts +9 -3
  94. package/dist/runtime/dom/auditScripts.js +41 -5
  95. package/dist/runtime/dom/elementGeometry.d.ts +33 -3
  96. package/dist/runtime/dom/elementGeometry.js +44 -19
  97. package/dist/runtime/dom/elementInfo.d.ts +76 -18
  98. package/dist/runtime/dom/elementInfo.js +190 -40
  99. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  100. package/dist/runtime/dom/evalHelpers.js +67 -7
  101. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  102. package/dist/runtime/dom/formDiscovery.js +20 -3
  103. package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
  104. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  105. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  106. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  107. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  108. package/dist/runtime/dom/frameLayout.js +1 -0
  109. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  110. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  111. package/dist/runtime/dom/inspect.d.ts +17 -3
  112. package/dist/runtime/dom/inspect.js +45 -32
  113. package/dist/runtime/dom/inspectAllStyles.js +1 -0
  114. package/dist/runtime/dom/inspectHints.d.ts +1 -1
  115. package/dist/runtime/dom/inspectModel.d.ts +5 -4
  116. package/dist/runtime/dom/inspectModel.js +7 -3
  117. package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
  118. package/dist/runtime/dom/inspectPaintModel.js +3 -1
  119. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  120. package/dist/runtime/dom/inspectRules.js +205 -11
  121. package/dist/runtime/dom/inspectScripts.d.ts +29 -2
  122. package/dist/runtime/dom/inspectScripts.js +49 -10
  123. package/dist/runtime/dom/layout.d.ts +0 -2
  124. package/dist/runtime/dom/layout.js +10 -9
  125. package/dist/runtime/dom/reactEventHelpers.d.ts +17 -4
  126. package/dist/runtime/dom/reactEventHelpers.js +71 -28
  127. package/dist/runtime/dom/targetNode.d.ts +27 -10
  128. package/dist/runtime/dom/targetNode.js +283 -16
  129. package/dist/runtime/dom/wait.js +2 -1
  130. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  131. package/dist/runtime/page/bdgWorld.js +180 -0
  132. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  133. package/dist/runtime/page/replacedBuiltins.js +136 -0
  134. package/dist/session/QueryCacheManager.d.ts +4 -1
  135. package/dist/session/QueryCacheManager.js +5 -2
  136. package/dist/session/chrome.d.ts +4 -1
  137. package/dist/session/chrome.js +7 -1
  138. package/dist/session/cleanup/staleSession.d.ts +21 -4
  139. package/dist/session/cleanup/staleSession.js +79 -9
  140. package/dist/session/cleanup/userCommands.d.ts +4 -1
  141. package/dist/session/cleanup/userCommands.js +10 -5
  142. package/dist/session/daemonSocket.d.ts +10 -0
  143. package/dist/session/daemonSocket.js +22 -0
  144. package/dist/session/lastSession.d.ts +6 -3
  145. package/dist/session/lastSession.js +11 -5
  146. package/dist/session/paths.d.ts +3 -1
  147. package/dist/session/paths.js +5 -5
  148. package/dist/session/portClaims.js +4 -3
  149. package/dist/session/sessionList.d.ts +13 -5
  150. package/dist/session/sessionList.js +31 -7
  151. package/dist/telemetry/a11y.d.ts +15 -1
  152. package/dist/telemetry/a11y.js +85 -2
  153. package/dist/telemetry/console.d.ts +2 -1
  154. package/dist/telemetry/console.js +30 -21
  155. package/dist/telemetry/har/builder.js +1 -1
  156. package/dist/telemetry/network.d.ts +13 -16
  157. package/dist/telemetry/network.js +30 -52
  158. package/dist/telemetry/networkRetention.d.ts +83 -0
  159. package/dist/telemetry/networkRetention.js +117 -0
  160. package/dist/telemetry/pageCrash.d.ts +26 -0
  161. package/dist/telemetry/pageCrash.js +53 -0
  162. package/dist/types.d.ts +42 -0
  163. package/dist/ui/OutputBuilder.d.ts +10 -0
  164. package/dist/ui/OutputBuilder.js +12 -0
  165. package/dist/ui/formatters/a11y.d.ts +5 -7
  166. package/dist/ui/formatters/a11y.js +7 -61
  167. package/dist/ui/formatters/audit.js +14 -5
  168. package/dist/ui/formatters/cdp.d.ts +138 -0
  169. package/dist/ui/formatters/cdp.js +131 -0
  170. package/dist/ui/formatters/console/chronological.js +7 -5
  171. package/dist/ui/formatters/console/follow.d.ts +5 -2
  172. package/dist/ui/formatters/console/follow.js +7 -4
  173. package/dist/ui/formatters/console/json.d.ts +4 -7
  174. package/dist/ui/formatters/console/json.js +16 -14
  175. package/dist/ui/formatters/console/shared.d.ts +47 -2
  176. package/dist/ui/formatters/console/shared.js +33 -0
  177. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  178. package/dist/ui/formatters/console/summarize.js +57 -11
  179. package/dist/ui/formatters/console.d.ts +3 -2
  180. package/dist/ui/formatters/console.js +8 -10
  181. package/dist/ui/formatters/details.js +4 -2
  182. package/dist/ui/formatters/dom.d.ts +14 -5
  183. package/dist/ui/formatters/dom.js +30 -13
  184. package/dist/ui/formatters/helpFormatters.js +1 -1
  185. package/dist/ui/formatters/inspect.js +9 -3
  186. package/dist/ui/formatters/installSkill.d.ts +9 -1
  187. package/dist/ui/formatters/installSkill.js +32 -6
  188. package/dist/ui/formatters/layout.js +4 -2
  189. package/dist/ui/formatters/longValues.d.ts +14 -0
  190. package/dist/ui/formatters/longValues.js +23 -0
  191. package/dist/ui/formatters/networkList.d.ts +8 -2
  192. package/dist/ui/formatters/networkList.js +11 -3
  193. package/dist/ui/formatters/preview.d.ts +6 -1
  194. package/dist/ui/formatters/preview.js +67 -15
  195. package/dist/ui/formatters/sessions.d.ts +2 -2
  196. package/dist/ui/formatters/sessions.js +9 -2
  197. package/dist/ui/formatters/status.js +7 -0
  198. package/dist/ui/formatters/triggeredRequests.js +2 -1
  199. package/dist/ui/logging/logger.d.ts +1 -1
  200. package/dist/ui/messages/chrome.d.ts +20 -1
  201. package/dist/ui/messages/chrome.js +29 -3
  202. package/dist/ui/messages/commands.d.ts +153 -12
  203. package/dist/ui/messages/commands.js +198 -15
  204. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  205. package/dist/ui/messages/consoleMessages.js +32 -0
  206. package/dist/ui/messages/networkMessages.d.ts +24 -0
  207. package/dist/ui/messages/networkMessages.js +45 -0
  208. package/dist/ui/messages/preview.d.ts +6 -0
  209. package/dist/ui/messages/preview.js +9 -1
  210. package/dist/ui/messages/session.d.ts +13 -2
  211. package/dist/ui/messages/session.js +22 -3
  212. package/dist/utils/directories.d.ts +34 -0
  213. package/dist/utils/directories.js +88 -0
  214. package/dist/utils/display.d.ts +16 -0
  215. package/dist/utils/display.js +42 -0
  216. package/dist/utils/exitCodes.d.ts +1 -0
  217. package/dist/utils/exitCodes.js +6 -0
  218. package/dist/utils/http.d.ts +9 -2
  219. package/dist/utils/http.js +4 -3
  220. package/dist/utils/process.d.ts +12 -0
  221. package/dist/utils/process.js +25 -0
  222. package/dist/utils/strings.d.ts +19 -0
  223. package/dist/utils/strings.js +16 -0
  224. package/package.json +2 -2
@@ -5,11 +5,12 @@
5
5
  * validation state, and suggested commands for agent consumption.
6
6
  */
7
7
  import { calculateSummary, orderForms, primaryButtonIndex } from './formSummary.js';
8
- import { resolveBackendNodeIds } from './helpers/index.js';
8
+ import { pageDocumentId, resolveBackendNodeIds } from './helpers/index.js';
9
9
  import { runCommand } from '../shared/CommandRunner.js';
10
10
  import { jsonOption } from '../shared/commonOptions.js';
11
11
  import { noFormsFoundError, formInIframeError } from '../../errors/messages.js';
12
12
  import { domFormDiscover } from '../../ipc/client.js';
13
+ import { MASKED_VALUE } from '../../runtime/dom/elementInfo.js';
13
14
  import { FORM_DISCOVERY_CACHE_SELECTOR, QueryCacheManager } from '../../session/QueryCacheManager.js';
14
15
  import { formatFormDiscovery } from '../../ui/formatters/form.js';
15
16
  import { createLogger } from '../../ui/logging/index.js';
@@ -84,16 +85,14 @@ function buildFieldState(raw) {
84
85
  return 'empty';
85
86
  }
86
87
  /**
87
- * Build masked value for password fields.
88
+ * The masked value of a sensitive field (the page script never sends its
89
+ * real value).
88
90
  *
89
91
  * @param raw - Raw field data
90
92
  * @returns Masked value string
91
93
  */
92
94
  function buildMaskedValue(raw) {
93
- if (raw.inputType === 'password' && typeof raw.value === 'string' && raw.value.length > 0) {
94
- return '•'.repeat(Math.min(raw.value.length, 8));
95
- }
96
- return undefined;
95
+ return raw.value === MASKED_VALUE ? MASKED_VALUE : undefined;
97
96
  }
98
97
  /**
99
98
  * Build interaction warning for non-native fields.
@@ -244,8 +243,9 @@ function transformForm(raw) {
244
243
  * the selector later.
245
244
  *
246
245
  * @param forms - Discovered forms
246
+ * @param document - Identity of the page document they were found in, read before discovery
247
247
  */
248
- async function cacheFormElements(forms) {
248
+ async function cacheFormElements(forms, document) {
249
249
  const elements = forms.flatMap((form) => [...form.fields, ...form.buttons]);
250
250
  const backendNodeIds = await resolveBackendNodeIds(elements.map((el) => el.selector)).catch((error) => {
251
251
  log.debug(`Form fields not cached by node: ${getErrorMessage(error)}`);
@@ -259,7 +259,7 @@ async function cacheFormElements(forms) {
259
259
  nodeId: backendNodeIds[i] ?? 0,
260
260
  selector: el.selector,
261
261
  })),
262
- });
262
+ }, document);
263
263
  log.debug(`Cached ${elements.length} form elements`);
264
264
  }
265
265
  /**
@@ -269,6 +269,7 @@ async function cacheFormElements(forms) {
269
269
  */
270
270
  async function handleFormCommand(options) {
271
271
  await runCommand(async () => {
272
+ const document = await pageDocumentId();
272
273
  const response = await domFormDiscover();
273
274
  if (response.status === 'error' || !response.data) {
274
275
  return {
@@ -311,7 +312,7 @@ async function handleFormCommand(options) {
311
312
  const allForms = orderForms(rawData.forms).map(transformForm);
312
313
  const forms = options.all ? allForms : [allForms[0]];
313
314
  // Cache ALL forms so global indices work with bdg dom fill/click
314
- await cacheFormElements(allForms);
315
+ await cacheFormElements(allForms, document);
315
316
  const result = {
316
317
  formCount: rawData.forms.length,
317
318
  selectedForm: 0,
@@ -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 */
@@ -57,7 +58,7 @@ export function registerFormInteractionCommands(program) {
57
58
  .command('fill')
58
59
  .description('Fill a form field with a value (React-compatible, waits for stability)')
59
60
  .argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
60
- .argument('<value>', 'Value to fill (file inputs: paths separated by commas, "" clears)')
61
+ .argument('<value>', 'Value to fill (file inputs: local file paths, separated by commas, that are uploaded to the page; "" clears)')
61
62
  .option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
62
63
  .option('--no-blur', 'Do not blur after filling (keeps focus on element)')
63
64
  .option('--no-wait', 'Skip waiting for network stability after fill')
@@ -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
  /**
@@ -9,12 +9,14 @@ import { DomElementResolver } from './DomElementResolver.js';
9
9
  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
+ import { MAX_VALUE_LENGTH } from '../../constants.js';
12
13
  import { CommandError } from '../../errors/index.js';
13
- import { conflictingOptionsMessage, indexWithIndexOptionError, nodeIdNotFoundError, optionRequiresMessage, } from '../../errors/messages.js';
14
+ import { conflictingOptionsError, indexWithIndexOptionError, nodeIdNotFoundError, optionRequiresError, } from '../../errors/messages.js';
14
15
  import { resolveA11yNode } from '../../telemetry/a11y.js';
15
16
  import { formatDomGet } from '../../ui/formatters/dom.js';
16
17
  import { EXIT_CODES } from '../../utils/exitCodes.js';
17
18
  import { filterDefined } from '../../utils/objects.js';
19
+ import { capLength } from '../../utils/strings.js';
18
20
  /** What `bdg dom get` reads without a selector */
19
21
  export const DOM_GET_DEFAULT_SELECTOR = 'body';
20
22
  /**
@@ -58,7 +60,7 @@ async function semanticElement(backendNodeId, full) {
58
60
  * Raw view: the element(s) with attributes and outer HTML.
59
61
  *
60
62
  * @param selectorOrIndex - CSS selector or cached index
61
- * @param options - Command options (`--all`, `--index`)
63
+ * @param options - Command options (`--all`, `--index`, `--full`)
62
64
  */
63
65
  async function handleRawGet(selectorOrIndex, options) {
64
66
  await runCommand(async () => {
@@ -70,8 +72,27 @@ async function handleRawGet(selectorOrIndex, options) {
70
72
  all: options.all,
71
73
  nth: matchIndex(options),
72
74
  });
73
- return { success: true, data: await getDOMElements(getOptions) };
74
- }, options, formatDomGet);
75
+ return { success: true, data: rawGetData(await getDOMElements(getOptions), options) };
76
+ }, options, (data) => formatDomGet(data, { full: options.full }));
77
+ }
78
+ /**
79
+ * Raw elements for the output: in JSON each outer HTML is cut to
80
+ * {@link MAX_VALUE_LENGTH} characters with `truncatedFrom`, unless `--full`
81
+ * (human output is cut when formatted).
82
+ *
83
+ * @param result - Elements read
84
+ * @param options - `--json`, `--full`
85
+ * @returns Elements to output
86
+ */
87
+ function rawGetData(result, options) {
88
+ if (!options.json || options.full)
89
+ return result;
90
+ return {
91
+ nodes: result.nodes.map((node) => {
92
+ const { text, truncatedFrom } = capLength(node.outerHTML ?? '', MAX_VALUE_LENGTH);
93
+ return truncatedFrom === undefined ? node : { ...node, outerHTML: text, truncatedFrom };
94
+ }),
95
+ };
75
96
  }
76
97
  /**
77
98
  * Semantic view of the target.
@@ -99,23 +120,20 @@ function matchIndex(options) {
99
120
  *
100
121
  * @param selectorOrIndex - Selector or index argument
101
122
  * @param options - Command options
102
- * @returns What conflicts, or null
123
+ * @returns What conflicts and how to fix it, or null
103
124
  */
104
125
  function getOptionsConflict(selectorOrIndex, options) {
105
126
  if (options.nodeId !== undefined && selectorOrIndex !== undefined) {
106
- return conflictingOptionsMessage('--node-id', 'a selector or index');
107
- }
108
- if (options.full && (options.raw || options.nodeId !== undefined)) {
109
- return conflictingOptionsMessage('--full', options.raw ? '--raw' : '--node-id');
127
+ return conflictingOptionsError('--node-id', 'a selector or index');
110
128
  }
111
129
  if (options.index !== undefined && options.nth !== undefined) {
112
- return conflictingOptionsMessage('--index', '--nth');
130
+ return conflictingOptionsError('--index', '--nth');
113
131
  }
114
132
  if (options.all && matchIndex(options) !== undefined) {
115
- return conflictingOptionsMessage('--all', '--index');
133
+ return conflictingOptionsError('--all', '--index');
116
134
  }
117
135
  if (options.all && !options.raw)
118
- return optionRequiresMessage('--all', '--raw');
136
+ return optionRequiresError('--all', '--raw');
119
137
  return null;
120
138
  }
121
139
  /**
@@ -130,11 +148,11 @@ function getOptionsConflict(selectorOrIndex, options) {
130
148
  export async function handleDomGet(selectorOrIndex, options) {
131
149
  const conflict = getOptionsConflict(selectorOrIndex, options);
132
150
  if (conflict) {
133
- throw new CommandError(conflict, {}, EXIT_CODES.INVALID_ARGUMENTS);
151
+ throw new CommandError(conflict.message, { suggestion: conflict.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
134
152
  }
135
153
  const { nodeId } = options;
136
154
  if (nodeId !== undefined) {
137
- await runCommand(async () => ({ success: true, data: await getDOMElements({ nodeId }) }), options, formatDomGet);
155
+ await runCommand(async () => ({ success: true, data: rawGetData(await getDOMElements({ nodeId }), options) }), options, (data) => formatDomGet(data, { full: options.full }));
138
156
  return;
139
157
  }
140
158
  const target = selectorOrIndex ?? DOM_GET_DEFAULT_SELECTOR;
@@ -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