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
@@ -138,11 +138,32 @@ async function restoreScrollPosition(position) {
138
138
  returnByValue: true,
139
139
  });
140
140
  }
141
+ /**
142
+ * The window's size with its scrollbars (`innerWidth`/`innerHeight`): an
143
+ * override at this size keeps the page's layout, where the visible size
144
+ * (without scrollbars) would narrow it and move centered content.
145
+ *
146
+ * @param viewport - Visible viewport size, used when the page does not answer
147
+ * @returns Width and height in CSS px
148
+ */
149
+ async function windowSize(viewport) {
150
+ const response = await callCDP('Runtime.evaluate', {
151
+ expression: '[window.innerWidth, window.innerHeight]',
152
+ returnByValue: true,
153
+ });
154
+ const value = response.data?.result?.result?.value;
155
+ const [width, height] = Array.isArray(value) ? value : [];
156
+ return {
157
+ width: Math.round(width ?? viewport.clientWidth),
158
+ height: Math.round(height ?? viewport.clientHeight),
159
+ };
160
+ }
141
161
  /**
142
162
  * Capture at a pixel ratio of 1 (CSS px = image px) on a high-DPI display:
143
- * the viewport is overridden at its size (the session's `--viewport`, else
144
- * the visible one) until the returned function puts back what was there
145
- * before, the session's viewport or none.
163
+ * the viewport is overridden at the window's size (the session's
164
+ * `--viewport`, else the window with its scrollbars, so the layout does not
165
+ * change) until the returned function puts back what was there before, the
166
+ * session's viewport or none.
146
167
  *
147
168
  * @param devicePixelRatio - Page's pixel ratio
148
169
  * @param viewport - Visible viewport size
@@ -152,10 +173,7 @@ async function useUnitPixelRatio(devicePixelRatio, viewport) {
152
173
  if (devicePixelRatio === 1)
153
174
  return () => Promise.resolve();
154
175
  const sessionViewport = readSessionMetadata()?.viewport;
155
- const size = sessionViewport ?? {
156
- width: Math.round(viewport.clientWidth),
157
- height: Math.round(viewport.clientHeight),
158
- };
176
+ const size = sessionViewport ?? (await windowSize(viewport));
159
177
  await callCDP('Emulation.setDeviceMetricsOverride', viewportOverride(size, 1));
160
178
  return async () => {
161
179
  if (sessionViewport) {
@@ -350,6 +368,53 @@ const CONTENT_OVERFLOW_JS = `function () {
350
368
  if (!clips(view.getComputedStyle(this))) walk(this);
351
369
  return { left: own.left - reach.left, top: own.top - reach.top, right: reach.right - own.right, bottom: reach.bottom - own.bottom };
352
370
  }`;
371
+ /**
372
+ * The visible viewport (without scrollbars) in CSS px.
373
+ *
374
+ * @returns Width and height
375
+ */
376
+ async function visibleViewport() {
377
+ const metrics = (await callCDP('Page.getLayoutMetrics', {})).data?.result;
378
+ const view = metrics?.cssVisualViewport ?? metrics?.visualViewport;
379
+ return { width: view?.clientWidth ?? 0, height: view?.clientHeight ?? 0 };
380
+ }
381
+ /**
382
+ * Whether an area (viewport coordinates) lies inside the viewport.
383
+ *
384
+ * @param area - Area
385
+ * @param view - Viewport size
386
+ * @returns True when fully inside
387
+ */
388
+ function insideView(area, view) {
389
+ return (area.x >= 0 &&
390
+ area.y >= 0 &&
391
+ area.x + area.width <= view.width &&
392
+ area.y + area.height <= view.height);
393
+ }
394
+ /**
395
+ * Measure the area to capture and, when it fits in the viewport but is not
396
+ * in view, scroll it to the middle first. A capture inside the viewport
397
+ * keeps the page as it is; one beyond it makes Chrome lay the page out
398
+ * without its scrollbar, which moves centered content by half the
399
+ * scrollbar's width, so it is used only for areas larger than the viewport.
400
+ *
401
+ * @param ref - Node reference
402
+ * @returns Border box, area to capture (viewport coordinates) and whether it is in view
403
+ */
404
+ async function measureInView(ref) {
405
+ const view = await visibleViewport();
406
+ let box = await getElementBounds(ref);
407
+ let bounds = await captureArea(ref, box);
408
+ const fits = bounds.width <= view.width && bounds.height <= view.height;
409
+ if (fits && !insideView(bounds, view)) {
410
+ const dx = bounds.x + bounds.width / 2 - view.width / 2;
411
+ const dy = bounds.y + bounds.height / 2 - view.height / 2;
412
+ await callCDP('Runtime.evaluate', { expression: `window.scrollBy(${dx}, ${dy})` });
413
+ box = await getElementBounds(ref);
414
+ bounds = await captureArea(ref, box);
415
+ }
416
+ return { box, bounds, inView: insideView(bounds, view) };
417
+ }
353
418
  /** Overflow (px) below which the capture keeps to the border box (subpixel rounding) */
354
419
  const OVERFLOW_SLACK = 1;
355
420
  /**
@@ -403,8 +468,6 @@ async function captureArea(ref, bounds) {
403
468
  * (the reported bounds are page coordinates, like `dom layout`'s).
404
469
  */
405
470
  export async function captureElementScreenshot(outputPath, ref, options = {}) {
406
- const box = await getElementBounds(ref);
407
- const bounds = await captureArea(ref, box);
408
471
  const format = options.format ?? 'png';
409
472
  const quality = format === 'jpeg' ? (options.quality ?? 90) : undefined;
410
473
  const noResize = options.noResize ?? false;
@@ -413,6 +476,18 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
413
476
  returnByValue: true,
414
477
  });
415
478
  const devicePixelRatio = dprResponse.data?.result?.result?.value ?? 1;
479
+ const before = (await callCDP('Page.getLayoutMetrics', {})).data?.result;
480
+ const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, before?.visualViewport ?? { clientWidth: 800, clientHeight: 600 });
481
+ let box;
482
+ let bounds;
483
+ let inView;
484
+ try {
485
+ ({ box, bounds, inView } = await measureInView(ref));
486
+ }
487
+ catch (error) {
488
+ await restoreMetrics();
489
+ throw error;
490
+ }
416
491
  const originalWidth = bounds.width;
417
492
  const originalHeight = bounds.height;
418
493
  const resized = shouldResize(originalWidth, originalHeight, noResize);
@@ -421,7 +496,6 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
421
496
  const finalHeight = Math.round(originalHeight * scale);
422
497
  const metricsResponse = await callCDP('Page.getLayoutMetrics', {});
423
498
  const metricsResult = metricsResponse.data?.result;
424
- const viewport = metricsResult?.visualViewport ?? { clientWidth: 800, clientHeight: 600 };
425
499
  const scroll = metricsResult?.cssLayoutViewport ?? { pageX: 0, pageY: 0 };
426
500
  const onPage = (area) => ({
427
501
  ...area,
@@ -429,14 +503,13 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
429
503
  y: area.y + scroll.pageY,
430
504
  });
431
505
  const clip = onPage(bounds);
432
- const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, viewport);
433
506
  let screenshotResult;
434
507
  try {
435
508
  const screenshotResponse = await callCDP('Page.captureScreenshot', {
436
509
  format,
437
510
  ...(quality !== undefined && { quality }),
438
511
  clip: { ...clip, scale },
439
- captureBeyondViewport: true,
512
+ captureBeyondViewport: !inView,
440
513
  });
441
514
  screenshotResult = screenshotResponse.data?.result;
442
515
  }
@@ -9,6 +9,7 @@
9
9
  * - `frames.ts` — list the page's iframes
10
10
  * - `listeners.ts` — list event listeners that run for an element
11
11
  * - `layout.ts` — positions, sizes and visibility of elements
12
+ * - `inspect.ts` — what one element looks like (styles, box, layout, child tree)
12
13
  * - `wait.ts` — wait for elements to appear, show, contain a text or go away
13
14
  *
14
15
  * Form-related commands register via `form.ts` and `formInteraction.ts`.
@@ -9,6 +9,7 @@
9
9
  * - `frames.ts` — list the page's iframes
10
10
  * - `listeners.ts` — list event listeners that run for an element
11
11
  * - `layout.ts` — positions, sizes and visibility of elements
12
+ * - `inspect.ts` — what one element looks like (styles, box, layout, child tree)
12
13
  * - `wait.ts` — wait for elements to appear, show, contain a text or go away
13
14
  *
14
15
  * Form-related commands register via `form.ts` and `formInteraction.ts`.
@@ -20,11 +21,13 @@ import { handleDomEval } from './eval.js';
20
21
  import { registerFormCommand } from './form.js';
21
22
  import { handleDomFrames } from './frames.js';
22
23
  import { DOM_GET_DEFAULT_SELECTOR, handleDomGet } from './get.js';
24
+ import { registerInspectCommand } from './inspect.js';
23
25
  import { registerLayoutCommand } from './layout.js';
24
26
  import { registerListenersCommand } from './listeners.js';
25
27
  import { handleDomQuery } from './query.js';
26
28
  import { handleDomScreenshot } from './screenshot.js';
27
29
  import { registerWaitCommand } from './wait.js';
30
+ import { SELECTOR_OR_INDEX_ARGUMENT, SELECTOR_SCOPE_HELP, } from '../shared/commonOptions.js';
28
31
  import { integerOption, screenshotFormatOption } from '../shared/validation.js';
29
32
  /**
30
33
  * Register DOM telemetry commands on the root Commander program.
@@ -38,12 +41,14 @@ export function registerDomCommands(program) {
38
41
  registerFormCommand(dom);
39
42
  registerListenersCommand(dom);
40
43
  registerLayoutCommand(dom);
44
+ registerInspectCommand(dom);
41
45
  registerWaitCommand(dom);
42
46
  dom
43
47
  .command('query')
44
48
  .description('Find elements by CSS selector')
45
49
  .argument('<selector>', 'CSS selector (e.g., ".error", "#app", "button")')
46
50
  .option('-j, --json', 'Output as JSON')
51
+ .addHelpText('after', SELECTOR_SCOPE_HELP)
47
52
  .action(async (selector, options) => {
48
53
  await handleDomQuery(selector, options);
49
54
  });
@@ -51,7 +56,7 @@ export function registerDomCommands(program) {
51
56
  .command('eval')
52
57
  .description('Evaluate JavaScript expression in the page context')
53
58
  .argument('<script>', 'JavaScript to execute (e.g., "document.title", "window.location.href")')
54
- .option('--frame <frame>', 'Evaluate in an iframe, cross-origin ones included: index, name/id attribute, or part of the name, id or URL (see dom frames)')
59
+ .option('--frame <frame>', 'Evaluate in an iframe, cross-origin ones included: index (from dom frames; 87 when stale), name/id attribute, or part of the name, id or URL')
55
60
  .option('-j, --json', 'Output as JSON')
56
61
  .action(async (script, options) => {
57
62
  await handleDomEval(script, options);
@@ -67,7 +72,7 @@ export function registerDomCommands(program) {
67
72
  });
68
73
  dom
69
74
  .command('frames')
70
- .description("List the page's iframes (nested and cross-origin ones included) for eval --frame")
75
+ .description("List the page's iframes in document order (nested and cross-origin ones included) for eval --frame")
71
76
  .option('-j, --json', 'Output as JSON')
72
77
  .action(async (options) => {
73
78
  await handleDomFrames(options);
@@ -75,7 +80,7 @@ export function registerDomCommands(program) {
75
80
  dom
76
81
  .command('get')
77
82
  .description('Get semantic accessibility structure (default) or raw HTML (--raw)')
78
- .argument('[selectorOrIndex]', `CSS selector or numeric index from query results (0-based; e.g. ".error", "#app", 0); default: ${DOM_GET_DEFAULT_SELECTOR}`)
83
+ .argument('[selectorOrIndex]', `${SELECTOR_OR_INDEX_ARGUMENT} (e.g. ".error", "#app", 0); default: ${DOM_GET_DEFAULT_SELECTOR}`)
79
84
  .option('--raw', 'Output raw HTML with all filtering options')
80
85
  .option('--full', 'Show all of the element text (default: the first 500 characters)')
81
86
  .option('--all', 'Get all matches (only with --raw)')
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `bdg dom inspect <selector|index>` - what one element looks like, without a
3
+ * screenshot: its box, layout (and its place in the parent), typography with
4
+ * the rendered font and contrast, fills, borders, effects, CSS state,
5
+ * pseudo-elements and a compact child tree, as grouped lines (`--json`:
6
+ * Figma-aligned fields).
7
+ */
8
+ import { type Command } from 'commander';
9
+ /**
10
+ * Register `bdg dom inspect`.
11
+ *
12
+ * @param dom - The `dom` command group
13
+ */
14
+ export declare function registerInspectCommand(dom: Command): void;
15
+ //# sourceMappingURL=inspect.d.ts.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `bdg dom inspect <selector|index>` - what one element looks like, without a
3
+ * screenshot: its box, layout (and its place in the parent), typography with
4
+ * the rendered font and contrast, fills, borders, effects, CSS state,
5
+ * pseudo-elements and a compact child tree, as grouped lines (`--json`:
6
+ * Figma-aligned fields).
7
+ */
8
+ import { Option } from 'commander';
9
+ import { runElementCommand } from './helpers/runElementCommand.js';
10
+ import { runCommand } from '../shared/CommandRunner.js';
11
+ import { jsonOption, SELECTOR_OR_INDEX_ARGUMENT } from '../shared/commonOptions.js';
12
+ import { cssPropertiesOption, integerOption } from '../shared/validation.js';
13
+ import { domInspect } from '../../ipc/client.js';
14
+ import { DEFAULT_TREE_DEPTH, DEFAULT_TREE_LIMIT } from '../../runtime/dom/inspectTree.js';
15
+ import { formatInspect } from '../../ui/formatters/inspect.js';
16
+ import { INSPECT_OUTPUT_LEGEND } from '../../ui/messages/commands.js';
17
+ import { filterDefined } from '../../utils/objects.js';
18
+ /**
19
+ * Register `bdg dom inspect`.
20
+ *
21
+ * @param dom - The `dom` command group
22
+ */
23
+ export function registerInspectCommand(dom) {
24
+ dom
25
+ .command('inspect')
26
+ .description('What one element looks like without a screenshot: box, layout, font (rendered, contrast), colors, borders, effects, pseudo-elements and child tree')
27
+ .argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
28
+ .option('--index <n>', 'Which match to inspect (0-based; default: the first rendered one)', integerOption(0))
29
+ .option('--tree <depth>', `Child tree depth (default ${DEFAULT_TREE_DEPTH}; 0 for none)`, integerOption(0, 10))
30
+ .option('--tree-limit <n>', `Child tree rows at most (default ${DEFAULT_TREE_LIMIT})`, integerOption(1, 500))
31
+ .addOption(new Option('--all', 'Every computed property that is not its default, collapsed into shorthands, instead of the groups').conflicts('props'))
32
+ .addHelpText('after', INSPECT_OUTPUT_LEGEND)
33
+ .option('--props <names>', "Only these properties, computed and normalized (comma-separated, e.g. padding,color,--brand; '--*' or '--bs-btn-*' lists custom properties)", cssPropertiesOption)
34
+ .addOption(new Option('--rules', 'Also show which CSS rule sets each shown property (selector, file:line, what it overrides); with --props, those properties').conflicts('all'))
35
+ .addOption(new Option('--why <property>', 'Every declaration of one property: the one that applies and those it overrides (e.g. --why color)')
36
+ .conflicts('all')
37
+ .argParser(propertyOption))
38
+ .option('--no-hints', 'Skip the check for declarations that have no effect')
39
+ .addOption(jsonOption())
40
+ .action(async (selectorOrIndex, options) => {
41
+ await runCommand(() => inspectTarget(selectorOrIndex, options), options, formatInspect);
42
+ });
43
+ }
44
+ /**
45
+ * Parse `--why`: a property name, lowercased (custom properties as given).
46
+ *
47
+ * @param value - Property name
48
+ * @returns Name
49
+ */
50
+ function propertyOption(value) {
51
+ return value.startsWith('--') ? value.trim() : value.trim().toLowerCase();
52
+ }
53
+ /**
54
+ * Resolve the target and ask the daemon to inspect it.
55
+ *
56
+ * @param selectorOrIndex - CSS selector or cached query index
57
+ * @param options - Command options
58
+ * @returns Command result
59
+ */
60
+ async function inspectTarget(selectorOrIndex, options) {
61
+ return runElementCommand({
62
+ selectorOrIndex,
63
+ index: options.index,
64
+ buildRequest: (target) => ({
65
+ ...target,
66
+ ...filterDefined({
67
+ tree: options.tree,
68
+ treeLimit: options.treeLimit,
69
+ all: options.all,
70
+ props: options.props,
71
+ rules: options.rules,
72
+ why: options.why,
73
+ ...(options.hints === false && { hints: false }),
74
+ }),
75
+ }),
76
+ call: domInspect,
77
+ command: 'inspect',
78
+ action: 'inspect the element',
79
+ failureSuggestion: 'Verify the selector matches an element: bdg dom query "<selector>"',
80
+ });
81
+ }
82
+ //# sourceMappingURL=inspect.js.map
@@ -7,7 +7,7 @@
7
7
  import { DomElementResolver } from './DomElementResolver.js';
8
8
  import { runElementCommand } from './helpers/runElementCommand.js';
9
9
  import { runCommand } from '../shared/CommandRunner.js';
10
- import { jsonOption } from '../shared/commonOptions.js';
10
+ import { jsonOption, SELECTOR_OR_INDEX_ARGUMENT } from '../shared/commonOptions.js';
11
11
  import { integerOption } from '../shared/validation.js';
12
12
  import { domLayout } from '../../ipc/client.js';
13
13
  import { formatLayout } from '../../ui/formatters/layout.js';
@@ -20,7 +20,7 @@ export function registerLayoutCommand(dom) {
20
20
  dom
21
21
  .command('layout')
22
22
  .description('Positions, sizes and visibility of elements (above/below the fold, hidden, covered) without a screenshot')
23
- .argument('<selectorOrIndex>', 'CSS selector or numeric index from query results (0-based)')
23
+ .argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
24
24
  .option('--index <n>', 'Only this match of the selector (0-based)', integerOption(0))
25
25
  .addOption(jsonOption())
26
26
  .action(async (selectorOrIndex, options) => {
@@ -13,7 +13,7 @@
13
13
  import { DomElementResolver } from './DomElementResolver.js';
14
14
  import { runElementCommand } from './helpers/runElementCommand.js';
15
15
  import { runCommand } from '../shared/CommandRunner.js';
16
- import { jsonOption } from '../shared/commonOptions.js';
16
+ import { jsonOption, SELECTOR_OR_INDEX_ARGUMENT } from '../shared/commonOptions.js';
17
17
  import { eventTypesOption, integerOption } from '../shared/validation.js';
18
18
  import { domListeners } from '../../ipc/client.js';
19
19
  import { formatListeners } from '../../ui/formatters/listeners.js';
@@ -26,7 +26,7 @@ export function registerListenersCommand(dom) {
26
26
  dom
27
27
  .command('listeners')
28
28
  .description('List event listeners that run for an element (incl. delegated ones on ancestors)')
29
- .argument('<selectorOrIndex>', 'CSS selector or numeric index from query results (0-based)')
29
+ .argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
30
30
  .option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
31
31
  .option('--type <types>', 'Only these event types (comma-separated, e.g. click,keydown; repeatable)', eventTypesOption)
32
32
  .option('--all', 'List every listener of framework roots (React) instead of one line per node')
@@ -17,7 +17,9 @@ export interface SemanticNodeWithContext {
17
17
  /**
18
18
  * Format a semantic node together with DOM context for human-readable output.
19
19
  *
20
- * The role line is followed by up to 500 characters of the element's text
20
+ * The role line names the element's key attributes (an image's file name, a
21
+ * link's href, a field's type and name), like `dom query` does, and is
22
+ * followed by up to 500 characters of the element's text
21
23
  * (all of it with `dom get --full`) when it is longer than the one-line
22
24
  * preview, or, for an element without text or name, what it holds.
23
25
  *
@@ -34,4 +36,15 @@ export declare function formatSemanticNodeWithContext(data: SemanticNodeWithCont
34
36
  * @param nodeId - CDP nodeId for synthesis
35
37
  */
36
38
  export declare function resolveNodeWithFallback(a11yNode: A11yNode | null, domContext: DomContext | null, nodeId: number | undefined): A11yNode | null;
39
+ /**
40
+ * The accessibility node with its value masked when the element holds a
41
+ * secret (`domContext.sensitive`): Chrome reports a password field's value
42
+ * as one bullet per character, and a field switched to text by a "show
43
+ * password" button in clear.
44
+ *
45
+ * @param node - Accessibility node
46
+ * @param domContext - DOM context of the same element
47
+ * @returns The node, with {@link MASKED_VALUE} as its value for a secret
48
+ */
49
+ export declare function withSecretMasked(node: A11yNode, domContext: DomContext | null): A11yNode;
37
50
  //# sourceMappingURL=semanticUtils.d.ts.map
@@ -5,7 +5,9 @@
5
5
  * their surrounding DOM context and to fall back gracefully when only one
6
6
  * of the two data sources is available.
7
7
  */
8
+ import { MASKED_VALUE } from '../../runtime/dom/elementInfo.js';
8
9
  import { synthesizeA11yNode } from '../../telemetry/roleInference.js';
10
+ import { keyAttributeItems } from '../../ui/formatters/keyAttributes.js';
9
11
  import { joinLines } from '../../ui/formatting.js';
10
12
  import { elementTextLine, emptyElementLine } from '../../ui/messages/commands.js';
11
13
  function capitalize(str) {
@@ -35,6 +37,27 @@ function buildContextText(node, domContext) {
35
37
  }
36
38
  return '';
37
39
  }
40
+ /**
41
+ * Key attributes of the element (src, href, a field's type and name, ...),
42
+ * leaving out what the role line already shows: values equal to the
43
+ * accessible name (an image's alt, a field's placeholder) and the value
44
+ * when the accessibility node has one.
45
+ *
46
+ * @param node - Accessibility node
47
+ * @param domContext - DOM context with the key attributes
48
+ * @returns ` src="…/logo.png"`-style text, or empty
49
+ */
50
+ function buildKeyAttributesText(node, domContext) {
51
+ if (!domContext?.attributes)
52
+ return '';
53
+ const shown = Object.entries(domContext.attributes)
54
+ .filter(([, value]) => Boolean(node.name) && value === node.name)
55
+ .map(([name]) => name);
56
+ if (node.value !== undefined && node.value !== '')
57
+ shown.push('value');
58
+ const items = keyAttributeItems(domContext.tag, domContext.attributes, new Set(shown));
59
+ return items.length > 0 ? ` ${items.join(' ')}` : '';
60
+ }
38
61
  function buildPropertiesText(node) {
39
62
  const props = [];
40
63
  if (node.value !== undefined && node.value !== '')
@@ -59,7 +82,9 @@ function buildPropertiesText(node) {
59
82
  /**
60
83
  * Format a semantic node together with DOM context for human-readable output.
61
84
  *
62
- * The role line is followed by up to 500 characters of the element's text
85
+ * The role line names the element's key attributes (an image's file name, a
86
+ * link's href, a field's type and name), like `dom query` does, and is
87
+ * followed by up to 500 characters of the element's text
63
88
  * (all of it with `dom get --full`) when it is longer than the one-line
64
89
  * preview, or, for an element without text or name, what it holds.
65
90
  *
@@ -70,9 +95,10 @@ export function formatSemanticNodeWithContext(data) {
70
95
  const { node, domContext } = data;
71
96
  const roleText = buildRoleText(node);
72
97
  const contextText = buildContextText(node, domContext);
98
+ const keysText = buildKeyAttributesText(node, domContext);
73
99
  const propsText = buildPropertiesText(node);
74
100
  const inferredText = node.inferred ? ' (inferred from DOM)' : '';
75
- const line = `${roleText}${contextText}${propsText}${inferredText}`;
101
+ const line = `${roleText}${contextText}${keysText}${propsText}${inferredText}`;
76
102
  if (domContext?.text)
77
103
  return joinLines(line, elementTextLine(domContext.text));
78
104
  if (domContext?.childCount !== undefined && !node.name) {
@@ -90,9 +116,24 @@ export function formatSemanticNodeWithContext(data) {
90
116
  */
91
117
  export function resolveNodeWithFallback(a11yNode, domContext, nodeId) {
92
118
  if (a11yNode)
93
- return a11yNode;
119
+ return withSecretMasked(a11yNode, domContext);
94
120
  if (domContext && nodeId)
95
121
  return synthesizeA11yNode(domContext, nodeId);
96
122
  return null;
97
123
  }
124
+ /**
125
+ * The accessibility node with its value masked when the element holds a
126
+ * secret (`domContext.sensitive`): Chrome reports a password field's value
127
+ * as one bullet per character, and a field switched to text by a "show
128
+ * password" button in clear.
129
+ *
130
+ * @param node - Accessibility node
131
+ * @param domContext - DOM context of the same element
132
+ * @returns The node, with {@link MASKED_VALUE} as its value for a secret
133
+ */
134
+ export function withSecretMasked(node, domContext) {
135
+ if (!domContext?.sensitive || !node.value)
136
+ return node;
137
+ return { ...node, value: MASKED_VALUE };
138
+ }
98
139
  //# sourceMappingURL=semanticUtils.js.map
@@ -13,7 +13,7 @@ import { applyFilters, getFilterHelpText, validateFilterString } from '../../tel
13
13
  import { resolvePreset, FILTER_PRESETS } from '../../telemetry/filterPresets.js';
14
14
  import { filterByResourceType } from '../../telemetry/filters.js';
15
15
  import { buildSuccessResponse } from '../../ui/OutputBuilder.js';
16
- import { formatNetworkFollowRows, formatNetworkList, } from '../../ui/formatters/networkList.js';
16
+ import { formatNetworkFollowRows, formatNetworkList, pageStartOf, } from '../../ui/formatters/networkList.js';
17
17
  import { followingNetworkMessage, stoppedFollowingNetworkMessage, } from '../../ui/messages/networkMessages.js';
18
18
  import { EXIT_CODES } from '../../utils/exitCodes.js';
19
19
  import { validateFilterOption } from './shared.js';
@@ -110,6 +110,7 @@ function buildFormatOptions(options, result, lastLimit) {
110
110
  last: lastLimit,
111
111
  totalCount: result.totalCount,
112
112
  filteredCount: result.filteredCount,
113
+ ...(result.pageStart && { pageStart: result.pageStart }),
113
114
  };
114
115
  }
115
116
  /**
@@ -148,9 +149,11 @@ async function runFollowMode(options, resourceTypes, lastN) {
148
149
  }
149
150
  }
150
151
  else {
152
+ const pageStart = pageStartOf(result.data);
151
153
  const text = formatNetworkFollowRows(fresh, {
152
154
  header: !started,
153
155
  verbose: options.verbose ?? false,
156
+ ...(pageStart && { pageStart }),
154
157
  });
155
158
  if (text)
156
159
  console.log(text);
@@ -164,6 +167,12 @@ async function runFollowMode(options, resourceTypes, lastN) {
164
167
  intervalMs: FOLLOW_INTERVAL,
165
168
  });
166
169
  }
170
+ /** What the less obvious columns of the list mean */
171
+ const COLUMNS_HELP = `Columns:
172
+ START When the request started, from the start of the current page (its document
173
+ request): +1.2s. Requests of earlier pages are negative. --json has the
174
+ absolute time (timestamp, epoch ms) and data.pageStart.
175
+ TIME How long it took (to its last byte or failure); - while pending`;
167
176
  function formatPresetHelp() {
168
177
  return Object.entries(FILTER_PRESETS)
169
178
  .map(([name, preset]) => ` ${name.padEnd(12)} ${preset.description}`)
@@ -180,7 +189,7 @@ export function registerListCommand(networkCmd) {
180
189
  .addOption(networkLastOption)
181
190
  .addOption(new Option('-f, --follow', 'Stream network requests in real-time').default(false))
182
191
  .addOption(new Option('-v, --verbose', 'Show full URLs and additional details').default(false))
183
- .addHelpText('after', `\n${getFilterHelpText()}\n\nPresets:\n${formatPresetHelp()}`)
192
+ .addHelpText('after', `\n${COLUMNS_HELP}\n\n${getFilterHelpText()}\n\nPresets:\n${formatPresetHelp()}`)
184
193
  .action(async (options) => {
185
194
  let resourceTypes;
186
195
  let lastN;
@@ -211,12 +220,14 @@ export function registerListCommand(networkCmd) {
211
220
  return createErrorResult(result.error, result.exitCode, result.suggestion);
212
221
  }
213
222
  const filtered = filterRequests(result.data, options, resourceTypes);
223
+ const pageStart = pageStartOf(result.data);
214
224
  return {
215
225
  success: true,
216
226
  data: {
217
227
  requests: lastN === 0 ? filtered : filtered.slice(-lastN),
218
228
  totalCount: result.data.length,
219
229
  filteredCount: filtered.length,
230
+ ...(pageStart && { pageStart }),
220
231
  },
221
232
  };
222
233
  }, options, (data) => formatNetworkList(data.requests, buildFormatOptions(options, data, lastN)));