browser-debugger-cli 0.9.0 → 0.10.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 (133) hide show
  1. package/README.md +4 -1
  2. package/dist/commands/dom/a11y.js +2 -1
  3. package/dist/commands/dom/formInteraction.js +56 -25
  4. package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
  5. package/dist/commands/dom/helpers/keyAttributes.js +54 -0
  6. package/dist/commands/dom/helpers/query.d.ts +1 -1
  7. package/dist/commands/dom/helpers/query.js +66 -19
  8. package/dist/commands/dom/helpers/runElementCommand.js +4 -3
  9. package/dist/commands/dom/helpers/screenshot.js +85 -12
  10. package/dist/commands/dom/index.d.ts +1 -0
  11. package/dist/commands/dom/index.js +8 -3
  12. package/dist/commands/dom/inspect.d.ts +15 -0
  13. package/dist/commands/dom/inspect.js +82 -0
  14. package/dist/commands/dom/layout.js +2 -2
  15. package/dist/commands/dom/listeners.js +2 -2
  16. package/dist/commands/dom/semanticUtils.d.ts +14 -1
  17. package/dist/commands/dom/semanticUtils.js +44 -3
  18. package/dist/commands/network/list.js +13 -2
  19. package/dist/commands/optionBehaviors.js +48 -6
  20. package/dist/commands/page.d.ts +1 -1
  21. package/dist/commands/page.js +62 -3
  22. package/dist/commands/shared/commonOptions.d.ts +4 -0
  23. package/dist/commands/shared/commonOptions.js +9 -0
  24. package/dist/commands/shared/optionTypes.d.ts +21 -0
  25. package/dist/commands/shared/startHelpers.d.ts +66 -0
  26. package/dist/commands/shared/startHelpers.js +91 -10
  27. package/dist/commands/shared/validation.d.ts +11 -0
  28. package/dist/commands/shared/validation.js +16 -0
  29. package/dist/daemon/launcher.d.ts +8 -1
  30. package/dist/daemon/launcher.js +3 -1
  31. package/dist/daemon/session/Session.d.ts +7 -0
  32. package/dist/daemon/session/Session.js +23 -1
  33. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  34. package/dist/daemon/session/commandRegistry.js +65 -9
  35. package/dist/daemon/session/interactions.d.ts +18 -5
  36. package/dist/daemon/session/interactions.js +22 -12
  37. package/dist/daemon.js +3565 -329
  38. package/dist/errors/messages.d.ts +69 -0
  39. package/dist/errors/messages.js +102 -1
  40. package/dist/index.js +2416 -1320
  41. package/dist/ipc/client.d.ts +9 -0
  42. package/dist/ipc/client.js +13 -0
  43. package/dist/ipc/protocol/commands.d.ts +56 -1
  44. package/dist/ipc/protocol/commands.js +2 -0
  45. package/dist/ipc/protocol/domTypes.d.ts +35 -2
  46. package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
  47. package/dist/ipc/protocol/inspectTypes.js +10 -0
  48. package/dist/runtime/dom/actionEffects.d.ts +94 -15
  49. package/dist/runtime/dom/actionEffects.js +173 -27
  50. package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -14
  51. package/dist/runtime/dom/actionEffectsScripts.js +224 -32
  52. package/dist/runtime/dom/elementInfo.d.ts +26 -0
  53. package/dist/runtime/dom/elementInfo.js +65 -0
  54. package/dist/runtime/dom/eventListeners.js +14 -4
  55. package/dist/runtime/dom/formFillHelpers/fill.d.ts +3 -4
  56. package/dist/runtime/dom/formFillHelpers/fill.js +77 -28
  57. package/dist/runtime/dom/frameSelection.d.ts +11 -0
  58. package/dist/runtime/dom/frameSelection.js +20 -1
  59. package/dist/runtime/dom/frames.d.ts +38 -5
  60. package/dist/runtime/dom/frames.js +136 -21
  61. package/dist/runtime/dom/inspect.d.ts +28 -0
  62. package/dist/runtime/dom/inspect.js +557 -0
  63. package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
  64. package/dist/runtime/dom/inspectAllStyles.js +385 -0
  65. package/dist/runtime/dom/inspectCascade.d.ts +94 -0
  66. package/dist/runtime/dom/inspectCascade.js +371 -0
  67. package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
  68. package/dist/runtime/dom/inspectCascadeModel.js +232 -0
  69. package/dist/runtime/dom/inspectHints.d.ts +62 -0
  70. package/dist/runtime/dom/inspectHints.js +305 -0
  71. package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
  72. package/dist/runtime/dom/inspectLayoutModel.js +346 -0
  73. package/dist/runtime/dom/inspectModel.d.ts +74 -0
  74. package/dist/runtime/dom/inspectModel.js +184 -0
  75. package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
  76. package/dist/runtime/dom/inspectPaintModel.js +461 -0
  77. package/dist/runtime/dom/inspectRules.d.ts +37 -0
  78. package/dist/runtime/dom/inspectRules.js +101 -0
  79. package/dist/runtime/dom/inspectScripts.d.ts +132 -0
  80. package/dist/runtime/dom/inspectScripts.js +263 -0
  81. package/dist/runtime/dom/inspectTree.d.ts +40 -0
  82. package/dist/runtime/dom/inspectTree.js +134 -0
  83. package/dist/runtime/dom/inspectVariables.d.ts +33 -0
  84. package/dist/runtime/dom/inspectVariables.js +94 -0
  85. package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
  86. package/dist/runtime/dom/inspectWhyModel.js +134 -0
  87. package/dist/runtime/dom/layout.d.ts +5 -1
  88. package/dist/runtime/dom/layout.js +10 -3
  89. package/dist/runtime/dom/listenerPageScripts.d.ts +11 -5
  90. package/dist/runtime/dom/listenerPageScripts.js +95 -9
  91. package/dist/runtime/dom/listenerSummary.d.ts +4 -0
  92. package/dist/runtime/dom/listenerSummary.js +26 -9
  93. package/dist/runtime/dom/reactEventHelpers.d.ts +5 -0
  94. package/dist/runtime/dom/reactEventHelpers.js +12 -4
  95. package/dist/runtime/page/emulation.d.ts +20 -0
  96. package/dist/runtime/page/emulation.js +37 -0
  97. package/dist/telemetry/a11y.d.ts +10 -0
  98. package/dist/telemetry/a11y.js +78 -1
  99. package/dist/telemetry/console.d.ts +1 -0
  100. package/dist/telemetry/console.js +100 -5
  101. package/dist/telemetry/network.js +3 -1
  102. package/dist/types.d.ts +32 -0
  103. package/dist/ui/formatters/details.d.ts +8 -0
  104. package/dist/ui/formatters/details.js +59 -3
  105. package/dist/ui/formatters/dom.d.ts +2 -1
  106. package/dist/ui/formatters/dom.js +25 -9
  107. package/dist/ui/formatters/inspect.d.ts +39 -0
  108. package/dist/ui/formatters/inspect.js +596 -0
  109. package/dist/ui/formatters/keyAttributes.d.ts +19 -0
  110. package/dist/ui/formatters/keyAttributes.js +84 -0
  111. package/dist/ui/formatters/layout.js +2 -2
  112. package/dist/ui/formatters/networkHeaders.d.ts +13 -0
  113. package/dist/ui/formatters/networkHeaders.js +23 -3
  114. package/dist/ui/formatters/networkList.d.ts +29 -1
  115. package/dist/ui/formatters/networkList.js +86 -20
  116. package/dist/ui/formatters/status.js +1 -1
  117. package/dist/ui/formatting.d.ts +9 -0
  118. package/dist/ui/formatting.js +6 -3
  119. package/dist/ui/messages/commands.d.ts +123 -7
  120. package/dist/ui/messages/commands.js +181 -10
  121. package/dist/ui/messages/networkMessages.d.ts +14 -0
  122. package/dist/ui/messages/networkMessages.js +18 -0
  123. package/dist/ui/messages/session.d.ts +14 -0
  124. package/dist/ui/messages/session.js +20 -0
  125. package/dist/utils/async.d.ts +9 -0
  126. package/dist/utils/async.js +17 -0
  127. package/dist/utils/color.d.ts +84 -0
  128. package/dist/utils/color.js +376 -0
  129. package/dist/utils/cssValues.d.ts +109 -0
  130. package/dist/utils/cssValues.js +236 -0
  131. package/dist/utils/selectorFilters.d.ts +12 -0
  132. package/dist/utils/selectorFilters.js +29 -0
  133. package/package.json +1 -1
package/README.md CHANGED
@@ -59,6 +59,9 @@ bdg eval "document.title" # Run JavaScript in the page (--frame for ifr
59
59
  bdg network list --preset errors # Network requests, console: bdg console
60
60
  bdg dom listeners "#save" # Which event listeners run for an element
61
61
  bdg dom layout "#save" # Where it is, whether it is visible or covered
62
+ bdg dom inspect "#save" # What it looks like (Figma-like styles), no screenshot
63
+ bdg dom inspect "#save" --why color # Which CSS rule sets a value, and what it beats
64
+ bdg page emulate --viewport 900x700 # Responsive check mid-session (or --color-scheme)
62
65
  bdg dom wait "#result" --visible # Wait for an element instead of sleeping
63
66
  bdg example.com --session agent2 --viewport 1280x800 # A second, independent session
64
67
  bdg stop # End session
@@ -66,7 +69,7 @@ bdg stop # End session
66
69
 
67
70
  ## Current State
68
71
 
69
- **Raw CDP access is complete.** Every protocol method works now. High-level commands cover the common work: page navigation, DOM queries and interaction (click, fill, hover, keys, forms, shadow DOM and iframes), accessibility tree, screenshots, network requests and HAR export, console messages and event listeners. Actions report what they changed (navigation, new messages, requests, or no visible effect), and several named sessions can run side by side. See the [CLI reference](docs/CLI_REFERENCE.md) for every command, and `bdg --help --json` for the machine-readable version.
72
+ **Raw CDP access is complete.** Every protocol method works now. High-level commands cover the common work: page navigation, DOM queries and interaction (click, fill, hover, keys, forms, shadow DOM and iframes), accessibility tree, screenshots, element styles with the CSS cascade (`dom inspect`: what an element looks like, which rule sets each value, and declarations that have no effect), network requests and HAR export, console messages and event listeners. Actions report what they changed (navigation, new messages, requests, or no visible effect), and several named sessions can run side by side. See the [CLI reference](docs/CLI_REFERENCE.md) for every command, and `bdg --help --json` for the machine-readable version.
70
73
 
71
74
  ## Agent Discovery Pattern
72
75
 
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { DomElementResolver } from './DomElementResolver.js';
12
12
  import { getDomContext, resolveBackendNodeIds } from './helpers/index.js';
13
+ import { withSecretMasked } from './semanticUtils.js';
13
14
  import { runCommand, runJsonCommand } from '../shared/CommandRunner.js';
14
15
  import { jsonOption } from '../shared/commonOptions.js';
15
16
  import { integerOption } from '../shared/validation.js';
@@ -164,7 +165,7 @@ async function handleA11yDescribe(selectorOrIndex, options) {
164
165
  if (domNodeId) {
165
166
  domContext = await getDomContext({ backendNodeId: domNodeId });
166
167
  }
167
- return { node, domContext };
168
+ return { node: withSecretMasked(node, domContext), domContext };
168
169
  }
169
170
  if (options.json) {
170
171
  await runJsonCommand(fetchA11yNodeData);
@@ -9,7 +9,7 @@
9
9
  import { InvalidArgumentError } from 'commander';
10
10
  import { runElementCommand } from './helpers/runElementCommand.js';
11
11
  import { runCommand } from '../shared/CommandRunner.js';
12
- import { jsonOption } from '../shared/commonOptions.js';
12
+ import { jsonOption, SELECTOR_OR_INDEX_ARGUMENT } from '../shared/commonOptions.js';
13
13
  import { integerOption } from '../shared/validation.js';
14
14
  import { CommandError } from '../../errors/index.js';
15
15
  import { VIA_LABEL_SUFFIX, conflictingOptionsMessage, indexSourceText, internalError, scrollOptionsError, } from '../../errors/messages.js';
@@ -17,9 +17,11 @@ import { domClick, domFill, domPressKey, domScroll, domSubmit } from '../../ipc/
17
17
  import { findUnknownModifiers } from '../../runtime/dom/keyMapping.js';
18
18
  import { formatTriggeredRequestLines, formatTriggeredRequestsTitle, } from '../../ui/formatters/triggeredRequests.js';
19
19
  import { OutputFormatter } from '../../ui/formatting.js';
20
- import { CLICK_RESULT_WAIT_HELP, POINTER_ACTION_DONE, actionStatusLine, dialogConsoleText, newMessageText, pageNavigationText, } from '../../ui/messages/commands.js';
20
+ import { CLICK_RESULT_WAIT_HELP, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, actionStatusLine, dialogConsoleText, newMessageText, pageNavigationText, shownElementText, stillChangingNote, } from '../../ui/messages/commands.js';
21
21
  import { sessionCommand } from '../../ui/messages/sessionCommand.js';
22
22
  import { EXIT_CODES } from '../../utils/exitCodes.js';
23
+ /** Help of `--strict` on click and hover */
24
+ const STRICT_OPTION_HELP = 'Fail (exit 90) instead of using DOM events when a real mouse cannot reach the element (covered, hidden, zero-size)';
23
25
  /**
24
26
  * Commander parser for `--modifiers`: rejects unknown names instead of
25
27
  * silently pressing the bare key.
@@ -54,7 +56,7 @@ export function registerFormInteractionCommands(program) {
54
56
  domCommand
55
57
  .command('fill')
56
58
  .description('Fill a form field with a value (React-compatible, waits for stability)')
57
- .argument('<selectorOrIndex>', 'CSS selector or numeric index from query results (0-based)')
59
+ .argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
58
60
  .argument('<value>', 'Value to fill (file inputs: paths separated by commas, "" clears)')
59
61
  .option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
60
62
  .option('--no-blur', 'Do not blur after filling (keeps focus on element)')
@@ -80,10 +82,11 @@ export function registerFormInteractionCommands(program) {
80
82
  domCommand
81
83
  .command('click')
82
84
  .description('Click an element and wait for stability (accepts selector or index)')
83
- .argument('<selectorOrIndex>', 'CSS selector or numeric index from query results (0-based)')
85
+ .argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
84
86
  .option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
85
87
  .option('--double', 'Double-click')
86
88
  .option('--right', 'Right-click (opens the context menu)')
89
+ .option('--strict', STRICT_OPTION_HELP)
87
90
  .option('--no-wait', 'Skip waiting for network stability after click')
88
91
  .addOption(jsonOption())
89
92
  .addHelpText('after', CLICK_RESULT_WAIT_HELP)
@@ -94,8 +97,9 @@ export function registerFormInteractionCommands(program) {
94
97
  domCommand
95
98
  .command('hover')
96
99
  .description('Move the mouse over an element (shows hover menus and tooltips)')
97
- .argument('<selectorOrIndex>', 'CSS selector or numeric index from query results (0-based)')
100
+ .argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
98
101
  .option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
102
+ .option('--strict', STRICT_OPTION_HELP)
99
103
  .option('--no-wait', 'Skip waiting for network stability after hovering')
100
104
  .addOption(jsonOption())
101
105
  .action(async (selectorOrIndex, options) => {
@@ -104,7 +108,7 @@ export function registerFormInteractionCommands(program) {
104
108
  domCommand
105
109
  .command('submit')
106
110
  .description('Submit a form by clicking submit button and waiting for completion')
107
- .argument('<selectorOrIndex>', 'CSS selector or numeric index from query results (0-based)')
111
+ .argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
108
112
  .option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
109
113
  .option('--wait-navigation', 'Wait for page navigation after submit')
110
114
  .option('--wait-network <ms>', 'Wait for network idle after submit (milliseconds)', integerOption(0), 1000)
@@ -132,7 +136,7 @@ export function registerFormInteractionCommands(program) {
132
136
  domCommand
133
137
  .command('pressKey')
134
138
  .description('Press a key on an element (for Enter-to-submit, keyboard navigation)')
135
- .argument('<selectorOrIndex>', 'CSS selector or numeric index from query results (0-based)')
139
+ .argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
136
140
  .argument('<key>', 'Key to press (Enter, Tab, Escape, Space, ArrowUp, etc.)')
137
141
  .option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
138
142
  .option('--times <n>', 'Press key multiple times (default: 1)', integerOption(1, 1000))
@@ -270,6 +274,7 @@ async function runPointerCommand(selectorOrIndex, options, action) {
270
274
  ...target,
271
275
  wait: options.wait !== false,
272
276
  ...(action !== 'click' && { action }),
277
+ ...(options.strict && { strict: true }),
273
278
  }),
274
279
  call: domClick,
275
280
  command: action === 'hover' ? 'hover' : 'click',
@@ -279,34 +284,40 @@ async function runPointerCommand(selectorOrIndex, options, action) {
279
284
  }
280
285
  /**
281
286
  * Build an action's output: the status line ("✓ Element Clicked",
282
- * "⚠ Element Clicked (with warnings)" with the warning right below it, or
283
- * "⚠ Element Clicked (no visible effect: …)"), the details, what changed on
284
- * the page (`Page:` navigation, `New text:` messages), then the network
285
- * requests it triggered and the dialogs it caused. No request list is shown
286
- * when there were none (JSON has an empty `triggeredRequests` then).
287
+ * "⚠ Element Clicked (with warnings)" with the warning right below it,
288
+ * "⚠ Element Clicked (page still changing)" with what it was still working
289
+ * on, or "⚠ Element Clicked (no visible effect: …)"), the details, what
290
+ * changed on the page (`Page:` navigation, `New text:` messages, `Shown:`
291
+ * elements), then the network requests it triggered and the dialogs it
292
+ * caused. No request list is shown when there were none (JSON has an empty
293
+ * `triggeredRequests` then).
287
294
  *
288
295
  * @param done - What was done, e.g. "Element Clicked"
289
296
  * @param details - Label/value rows
290
297
  * @param result - Action result
291
- * @param keyWidth - Width of the labels
298
+ * @param options - Width of the labels; what the action is called in notes (e.g. "click")
292
299
  * @returns Output being built (more can be appended)
293
300
  */
294
- function formatActionOutput(done, details, result, keyWidth = 15) {
301
+ function formatActionOutput(done, details, result, options = {}) {
302
+ const keyWidth = options.keyWidth ?? 15;
295
303
  const fmt = new OutputFormatter();
296
- fmt.text(actionStatusLine(done, result.warning !== undefined, result.effect === 'none'));
304
+ const stillChanging = result.settled === false && result.pending !== undefined;
305
+ fmt.text(actionStatusLine(done, {
306
+ warned: result.warning !== undefined,
307
+ noEffect: result.effect === 'none',
308
+ stillChanging,
309
+ }));
297
310
  if (result.warning)
298
311
  fmt.text(`⚠ Warning: ${result.warning}`);
312
+ if (stillChanging && result.pending) {
313
+ fmt.text(`⚠ ${stillChangingNote(options.action ?? 'action', result.pending)}`);
314
+ }
299
315
  fmt.blank();
300
316
  fmt.keyValueList(details, keyWidth);
301
317
  if (result.navigation)
302
318
  fmt.keyValue('Page', pageNavigationText(result.navigation), keyWidth);
303
- (result.messages ?? []).forEach((message, index) => {
304
- const text = newMessageText(message);
305
- if (index === 0)
306
- fmt.keyValue('New text', text, keyWidth);
307
- else
308
- fmt.text(' '.repeat(keyWidth) + text);
309
- });
319
+ listRows(fmt, 'New text', (result.messages ?? []).map(newMessageText), keyWidth);
320
+ listRows(fmt, 'Shown', (result.shown ?? []).map(shownElementText), keyWidth);
310
321
  const omitted = result.triggeredRequestsOmitted;
311
322
  const requests = formatTriggeredRequestLines(result.triggeredRequests ?? [], omitted);
312
323
  if (requests.length > 0) {
@@ -320,6 +331,23 @@ function formatActionOutput(done, details, result, keyWidth = 15) {
320
331
  }
321
332
  return fmt;
322
333
  }
334
+ /**
335
+ * Rows of a list under one label: the label on the first row, the rest
336
+ * indented below it.
337
+ *
338
+ * @param fmt - Output being built
339
+ * @param label - Label, e.g. "New text"
340
+ * @param texts - One text per row
341
+ * @param keyWidth - Width of the labels
342
+ */
343
+ function listRows(fmt, label, texts, keyWidth) {
344
+ texts.forEach((text, index) => {
345
+ if (index === 0)
346
+ fmt.keyValue(label, text, keyWidth);
347
+ else
348
+ fmt.text(' '.repeat(keyWidth) + text);
349
+ });
350
+ }
323
351
  /**
324
352
  * Row naming the element an action hit, e.g.
325
353
  * `Element: input.toggle in div.view "Write report"` (just its tag when the
@@ -372,7 +400,7 @@ function formatClickOutput(result) {
372
400
  ...selectorRows(result),
373
401
  elementRow(result),
374
402
  ['Method', result.method === 'dom' ? 'DOM events' : 'mouse events'],
375
- ], result).build();
403
+ ], result, { action: POINTER_ACTION_NOUN[result.action ?? 'click'] }).build();
376
404
  }
377
405
  /**
378
406
  * Format submit command output for human-readable display.
@@ -391,7 +419,10 @@ function formatSubmitOutput(result) {
391
419
  details.push(['Navigation', 'yes']);
392
420
  if (result.waitTimeMs !== undefined)
393
421
  details.push(['Wait Time', `${result.waitTimeMs}ms`]);
394
- const fmt = formatActionOutput('Form Submitted', details, result, 20);
422
+ const fmt = formatActionOutput('Form Submitted', details, result, {
423
+ keyWidth: 20,
424
+ action: 'submit',
425
+ });
395
426
  fmt.hints('Next steps:', [
396
427
  `${sessionCommand('bdg network list --last 10').padEnd(32)} Check network requests`,
397
428
  `${sessionCommand('bdg console --last 5').padEnd(32)} Check console messages`,
@@ -412,7 +443,7 @@ function formatPressKeyOutput(result) {
412
443
  details.push(['Times', result.times.toString()]);
413
444
  if (result.modifiers?.length)
414
445
  details.push(['Modifiers', result.modifiers.join('+')]);
415
- return formatActionOutput('Key Pressed', details, result).build();
446
+ return formatActionOutput('Key Pressed', details, result, { action: 'key press' }).build();
416
447
  }
417
448
  /**
418
449
  * Format scroll command output for human-readable display.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The attributes that identify an element by its type (an image's `src`, a
3
+ * link's `href`, a field's name and value), shown by `dom query`, `dom get`
4
+ * and `dom a11y describe`.
5
+ */
6
+ import type { ElementState, KeyAttributes } from '../../../types.js';
7
+ /**
8
+ * The key attributes of an element: the identifying attributes of its type
9
+ * that are set (empty ones left out) and the live state of a form control
10
+ * (type, current value, checked, selected options). Values arrive masked
11
+ * from the page (`ELEMENT_STATE_JS`); a hidden input's value is never read,
12
+ * and a sensitive field's value is masked here again in case it was not.
13
+ *
14
+ * @param tag - Lower-case tag name
15
+ * @param attributes - Element attributes
16
+ * @param state - Live state read in the page (`ELEMENT_STATE_JS`)
17
+ * @returns Key attributes, or undefined for an element type without any
18
+ */
19
+ export declare function keyAttributes(tag: string, attributes: Record<string, string>, state?: ElementState): KeyAttributes | undefined;
20
+ //# sourceMappingURL=keyAttributes.d.ts.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The attributes that identify an element by its type (an image's `src`, a
3
+ * link's `href`, a field's name and value), shown by `dom query`, `dom get`
4
+ * and `dom a11y describe`.
5
+ */
6
+ import { MASKED_VALUE } from '../../../runtime/dom/elementInfo.js';
7
+ /** Attributes read for each element type (live state is added for form controls) */
8
+ const ATTRIBUTES_BY_TAG = {
9
+ img: ['src', 'alt'],
10
+ a: ['href'],
11
+ input: ['name', 'placeholder'],
12
+ textarea: ['name', 'placeholder'],
13
+ button: ['name'],
14
+ select: ['name'],
15
+ iframe: ['src'],
16
+ form: ['action', 'method'],
17
+ };
18
+ /**
19
+ * The key attributes of an element: the identifying attributes of its type
20
+ * that are set (empty ones left out) and the live state of a form control
21
+ * (type, current value, checked, selected options). Values arrive masked
22
+ * from the page (`ELEMENT_STATE_JS`); a hidden input's value is never read,
23
+ * and a sensitive field's value is masked here again in case it was not.
24
+ *
25
+ * @param tag - Lower-case tag name
26
+ * @param attributes - Element attributes
27
+ * @param state - Live state read in the page (`ELEMENT_STATE_JS`)
28
+ * @returns Key attributes, or undefined for an element type without any
29
+ */
30
+ export function keyAttributes(tag, attributes, state = {}) {
31
+ const names = ATTRIBUTES_BY_TAG[tag];
32
+ if (!names)
33
+ return undefined;
34
+ const result = {};
35
+ const type = state.type ?? attributes['type'];
36
+ if (type)
37
+ result['type'] = type;
38
+ for (const name of names) {
39
+ const value = attributes[name];
40
+ if (value)
41
+ result[name] = value;
42
+ }
43
+ if (type === 'hidden')
44
+ return result;
45
+ const secret = state.sensitive === true || type === 'password';
46
+ if (state.value)
47
+ result['value'] = secret ? MASKED_VALUE : state.value;
48
+ if (state.checked !== undefined)
49
+ result['checked'] = state.checked;
50
+ if (state.selected)
51
+ result['selected'] = secret ? MASKED_VALUE : state.selected;
52
+ return Object.keys(result).length > 0 ? result : undefined;
53
+ }
54
+ //# sourceMappingURL=keyAttributes.js.map
@@ -46,7 +46,7 @@ export declare function documentReadyState(): Promise<string | undefined>;
46
46
  */
47
47
  export declare function queryDOMElements(selector: string): Promise<DomQueryResult>;
48
48
  /**
49
- * Get DOM context (tag, classes, text preview) for a node: a one-line
49
+ * Get DOM context (tag, classes, key attributes, text preview) for a node: a one-line
50
50
  * preview, and up to {@link ELEMENT_TEXT_LENGTH} characters of text when it
51
51
  * is longer (all of it with `full`).
52
52
  *
@@ -9,18 +9,19 @@
9
9
  * bdg invocations. Selectors are matched in the page, including open shadow
10
10
  * roots and same-origin iframes, like a user sees the page.
11
11
  */
12
+ import { keyAttributes } from './keyAttributes.js';
12
13
  import { CommandError } from '../../../errors/index.js';
13
14
  import { noNodesFoundError, indexOutOfRangeError, eitherArgumentRequiredError, invalidSelectorError, nodeIdNotFoundError, operationFailedError, similarSelectorsLine, staleNodeError, } from '../../../errors/messages.js';
14
15
  import { callCDP } from '../../../ipc/client.js';
15
16
  import { ELEMENT_GEOMETRY_JS, VIEWPORT_SIZE_JS, classifyViewportPosition, } from '../../../runtime/dom/elementGeometry.js';
16
- import { ELEMENT_CONTEXT_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, } from '../../../runtime/dom/elementInfo.js';
17
18
  import { DEEP_QUERY_JS, UNSEARCHED_CONTENT_JS, pageNamesJS, selectorArgsJS, } from '../../../runtime/dom/targetNode.js';
18
19
  import { createLogger } from '../../../ui/logging/index.js';
19
20
  import { sessionCommand } from '../../../ui/messages/sessionCommand.js';
20
21
  import { ConcurrencyLimiter } from '../../../utils/concurrency.js';
21
22
  import { getErrorMessage } from '../../../utils/errors.js';
22
23
  import { EXIT_CODES } from '../../../utils/exitCodes.js';
23
- import { parseSelectorFilters, withoutVisibleFilters } from '../../../utils/selectorFilters.js';
24
+ import { leadingCompounds, parseSelectorFilters, withoutVisibleFilters, } from '../../../utils/selectorFilters.js';
24
25
  import { findSimilarNames, parseSingleNameSelector } from '../../../utils/suggestions.js';
25
26
  const log = createLogger('dom');
26
27
  /** Maximum concurrent CDP calls to avoid overwhelming the connection. */
@@ -113,6 +114,34 @@ async function withSelection(selector, use) {
113
114
  export async function noMatchesError(selector) {
114
115
  return noNodesFoundError(selector, await noMatchContext(selector));
115
116
  }
117
+ /**
118
+ * Page-side index of the first leading compound that matches a shadow host
119
+ * (an element with an open shadow root, anywhere a selector reaches) while
120
+ * the selector after it finds something: then the selector only failed by
121
+ * crossing into the shadow root.
122
+ *
123
+ * @param compounds - Leading compounds of the selector
124
+ * @returns Expression evaluating to the index, or -1
125
+ */
126
+ function shadowHostJS(compounds) {
127
+ const plain = compounds
128
+ .slice(0, MAX_HOST_COMPOUNDS)
129
+ .map((entry) => [withoutFilters(entry.compound), withoutFilters(entry.rest)]);
130
+ return `${JSON.stringify(plain)}.findIndex(([host, rest]) => { try { const deep = ${DEEP_QUERY_JS}; return deep(host, null).some((el) => el.shadowRoot) && deep(rest, null).length > 0; } catch (e) { return false; } })`;
131
+ }
132
+ /** Leading compounds checked for a shadow host (each check walks the page) */
133
+ const MAX_HOST_COMPOUNDS = 4;
134
+ /**
135
+ * A selector without bdg's filters (`:visible`, `:has-text()`, `:text-is()`),
136
+ * which plain CSS does not know.
137
+ *
138
+ * @param selector - Selector
139
+ * @returns Plain CSS
140
+ */
141
+ function withoutFilters(selector) {
142
+ return (selector.replace(/:visible\b|:(has-text|text-is)\((?:"[^"]*"|'[^']*'|[^)"'])*\)/g, '').trim() ||
143
+ '*');
144
+ }
116
145
  /**
117
146
  * What the page says about a selector that matched nothing, in one
118
147
  * evaluation: whether it is still loading, how many elements match with the
@@ -131,9 +160,11 @@ export async function noMatchContext(selector) {
131
160
  ? `(() => { try { return (${DEEP_QUERY_JS})(${JSON.stringify(selector)}, ${JSON.stringify(unfiltered)}).length; } catch (e) { return 0; } })()`
132
161
  : '0';
133
162
  const names = single ? pageNamesJS(single.kind) : '[]';
163
+ const compounds = leadingCompounds(selector);
164
+ const shadowHost = compounds.length > 0 ? shadowHostJS(compounds) : '-1';
134
165
  try {
135
166
  const evaluated = await callCDP('Runtime.evaluate', {
136
- expression: `({ hidden: ${hidden}, readyState: document.readyState, unsearched: ${UNSEARCHED_CONTENT_JS}, names: ${names} })`,
167
+ expression: `({ hidden: ${hidden}, readyState: document.readyState, unsearched: ${UNSEARCHED_CONTENT_JS}, names: ${names}, shadowHost: ${shadowHost} })`,
137
168
  returnByValue: true,
138
169
  });
139
170
  const { result } = (evaluated.data?.result ?? {});
@@ -151,6 +182,8 @@ export async function noMatchContext(selector) {
151
182
  },
152
183
  }),
153
184
  ...(similar && { similar }),
185
+ ...(typeof value.shadowHost === 'number' &&
186
+ compounds[value.shadowHost] && { shadowHost: compounds[value.shadowHost] }),
154
187
  };
155
188
  }
156
189
  catch (error) {
@@ -182,7 +215,8 @@ export async function documentReadyState() {
182
215
  const VIEWPORT_HINT_LIMIT = 100;
183
216
  /**
184
217
  * Where each element of a page-side array lives (an iframe and/or a shadow
185
- * root), its text and, for the first {@link VIEWPORT_HINT_LIMIT}, its position
218
+ * root), its text, its form control state ({@link ELEMENT_STATE_JS}) and, for
219
+ * the first {@link VIEWPORT_HINT_LIMIT}, its position
186
220
  * relative to the viewport, plus the viewport size. An element that cannot be
187
221
  * read gets empty details instead of failing the whole query.
188
222
  */
@@ -190,9 +224,10 @@ const ELEMENT_DETAILS_FUNCTION = `function () {
190
224
  const contextOf = ${ELEMENT_CONTEXT_JS};
191
225
  const textOf = ${ELEMENT_TEXT_JS};
192
226
  const geometryOf = ${ELEMENT_GEOMETRY_JS};
227
+ const stateOf = ${ELEMENT_STATE_JS};
193
228
  const read = (el, index) => {
194
229
  try {
195
- return { context: contextOf(el), text: textOf(el), geometry: index < ${VIEWPORT_HINT_LIMIT} ? geometryOf(el) : null };
230
+ return { context: contextOf(el), text: textOf(el), state: stateOf(el), geometry: index < ${VIEWPORT_HINT_LIMIT} ? geometryOf(el) : null };
196
231
  } catch (e) {
197
232
  return {};
198
233
  }
@@ -219,6 +254,7 @@ async function elementsWithDetails(arrayObjectId) {
219
254
  backendNodeId,
220
255
  context: details?.context ?? '',
221
256
  text: details?.text ?? '',
257
+ state: details?.state ?? {},
222
258
  ...viewportHint(details?.geometry, viewport),
223
259
  },
224
260
  ];
@@ -334,18 +370,21 @@ export async function queryDOMElements(selector) {
334
370
  log.debug(`Querying ${elements.length} elements with selector: ${selector}`);
335
371
  }
336
372
  const nodes = await mapConcurrently(elements, async (element, index) => {
337
- const { backendNodeId, context, text, inViewport, clippedBy } = element;
373
+ const { backendNodeId, context, text, state, inViewport, clippedBy } = element;
338
374
  const desc = await describeNode({ backendNodeId });
339
375
  if (!desc)
340
376
  return { index, nodeId: 0 };
341
377
  const attributes = unpackAttributes(desc.attributes);
342
378
  const classes = attributes['class']?.split(/\s+/).filter(Boolean);
343
379
  const preview = textPreview(text);
380
+ const tag = desc.nodeName.toLowerCase();
381
+ const keys = keyAttributes(tag, attributes, state);
344
382
  return {
345
383
  index,
346
384
  nodeId: desc.backendNodeId,
347
- tag: desc.nodeName.toLowerCase(),
385
+ tag,
348
386
  ...identifyingAttributes(attributes, desc.nodeName),
387
+ ...(keys && { attributes: keys }),
349
388
  ...(classes && { classes }),
350
389
  ...(preview && { preview }),
351
390
  ...(context && { context }),
@@ -372,36 +411,39 @@ function identifyingAttributes(attributes, nodeName) {
372
411
  };
373
412
  }
374
413
  /**
375
- * The text of one element as the page renders it ({@link ELEMENT_TEXT_JS}).
414
+ * The text of one element as the page renders it ({@link ELEMENT_TEXT_JS})
415
+ * and its form control state ({@link ELEMENT_STATE_JS}).
376
416
  *
377
417
  * @param ref - Node reference
378
418
  * @param full - Read all of a large container's text, not just its start
379
- * @returns Element text, or empty when the node cannot be read
419
+ * @returns Element text and state, empty when the node cannot be read
380
420
  */
381
- async function elementText(ref, full) {
421
+ async function elementTextAndState(ref, full) {
382
422
  const objectGroup = `bdg-text-${process.pid}-${++queryCount}`;
383
423
  const resolved = await callCDP('DOM.resolveNode', { ...ref, objectGroup });
384
424
  const objectId = resolved.data?.result?.object
385
425
  .objectId;
386
426
  if (!objectId)
387
- return '';
427
+ return { text: '', state: {} };
388
428
  try {
389
429
  const response = await callCDP('Runtime.callFunctionOn', {
390
430
  objectId,
391
- functionDeclaration: `function (full) { return (${ELEMENT_TEXT_JS})(this, full); }`,
431
+ functionDeclaration: `function (full) { return { text: (${ELEMENT_TEXT_JS})(this, full), state: (${ELEMENT_STATE_JS})(this) }; }`,
392
432
  arguments: [{ value: full }],
393
433
  returnByValue: true,
394
434
  });
395
- const value = response.data?.result?.result
396
- ?.value;
397
- return typeof value === 'string' ? value : '';
435
+ const value = response.data?.result?.result?.value;
436
+ return {
437
+ text: typeof value?.text === 'string' ? value.text : '',
438
+ state: value?.state ?? {},
439
+ };
398
440
  }
399
441
  finally {
400
442
  await callCDP('Runtime.releaseObjectGroup', { objectGroup });
401
443
  }
402
444
  }
403
445
  /**
404
- * Get DOM context (tag, classes, text preview) for a node: a one-line
446
+ * Get DOM context (tag, classes, key attributes, text preview) for a node: a one-line
405
447
  * preview, and up to {@link ELEMENT_TEXT_LENGTH} characters of text when it
406
448
  * is longer (all of it with `full`).
407
449
  *
@@ -417,13 +459,18 @@ export async function getDomContext(ref, options = {}) {
417
459
  return null;
418
460
  }
419
461
  const full = options.full === true;
420
- const classes = unpackAttributes(desc.attributes)['class']?.split(/\s+/).filter(Boolean);
421
- const text = await elementText(ref, full);
462
+ const attributes = unpackAttributes(desc.attributes);
463
+ const classes = attributes['class']?.split(/\s+/).filter(Boolean);
464
+ const { text, state } = await elementTextAndState(ref, full);
422
465
  const preview = textPreview(text);
423
466
  const longer = textPreview(text, full ? Number.POSITIVE_INFINITY : ELEMENT_TEXT_LENGTH);
467
+ const tag = desc.nodeName.toLowerCase();
468
+ const keys = keyAttributes(tag, attributes, state);
424
469
  return {
425
- tag: desc.nodeName.toLowerCase(),
470
+ tag,
426
471
  ...(classes && classes.length > 0 && { classes }),
472
+ ...(keys && { attributes: keys }),
473
+ ...(state.sensitive && { sensitive: true }),
427
474
  ...(preview && { preview }),
428
475
  ...(longer !== preview && { text: longer }),
429
476
  ...(!preview && (await childElements(ref))),
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { DomElementResolver } from '../DomElementResolver.js';
8
8
  import { noMatchContext } from './query.js';
9
- import { otherIndexSourceNote, staleNodeError, unreachableElementsNote, withLoadingHint, } from '../../../errors/messages.js';
9
+ import { otherIndexSourceNote, staleNodeError, shadowBoundaryLine, unreachableElementsNote, withLoadingHint, } from '../../../errors/messages.js';
10
10
  import { joinLines } from '../../../ui/formatting.js';
11
11
  import { EXIT_CODES } from '../../../utils/exitCodes.js';
12
12
  /**
@@ -110,7 +110,8 @@ function failedResultFailure(result, options) {
110
110
  }
111
111
  /**
112
112
  * Add what the page says to a "not found" failure (one page evaluation, on
113
- * this failure path only, {@link noMatchContext}): similar ids or classes,
113
+ * this failure path only, {@link noMatchContext}): similar ids or classes, a
114
+ * shadow host the selector tries to cross,
114
115
  * the places selectors do not search (for a page script that found nothing)
115
116
  * and the still-loading hint while the page loads.
116
117
  *
@@ -124,7 +125,7 @@ async function withNotFoundContext(failure, selector, searched) {
124
125
  return failure;
125
126
  const context = await noMatchContext(selector);
126
127
  const note = searched ? unreachableElementsNote(selector, context.unsearched) : '';
127
- const suggestion = withLoadingHint(joinLines(context.similar, failure.errorContext?.suggestion, note ? note : undefined), context.readyState, selector);
128
+ const suggestion = withLoadingHint(joinLines(context.similar, context.shadowHost && shadowBoundaryLine(context.shadowHost), failure.errorContext?.suggestion, note ? note : undefined), context.readyState, selector);
128
129
  return suggestion ? { ...failure, errorContext: { suggestion } } : failure;
129
130
  }
130
131
  //# sourceMappingURL=runElementCommand.js.map