browser-debugger-cli 0.9.0 → 0.11.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 (139) hide show
  1. package/.claude/skills/bdg/SKILL.md +268 -0
  2. package/README.md +15 -1
  3. package/dist/commands/dom/a11y.js +2 -1
  4. package/dist/commands/dom/formInteraction.js +56 -25
  5. package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
  6. package/dist/commands/dom/helpers/keyAttributes.js +54 -0
  7. package/dist/commands/dom/helpers/query.d.ts +1 -1
  8. package/dist/commands/dom/helpers/query.js +66 -19
  9. package/dist/commands/dom/helpers/runElementCommand.js +4 -3
  10. package/dist/commands/dom/helpers/screenshot.js +85 -12
  11. package/dist/commands/dom/index.d.ts +1 -0
  12. package/dist/commands/dom/index.js +8 -3
  13. package/dist/commands/dom/inspect.d.ts +15 -0
  14. package/dist/commands/dom/inspect.js +82 -0
  15. package/dist/commands/dom/layout.js +2 -2
  16. package/dist/commands/dom/listeners.js +2 -2
  17. package/dist/commands/dom/semanticUtils.d.ts +14 -1
  18. package/dist/commands/dom/semanticUtils.js +44 -3
  19. package/dist/commands/installSkill.d.ts +20 -0
  20. package/dist/commands/installSkill.js +87 -0
  21. package/dist/commands/network/list.js +13 -2
  22. package/dist/commands/optionBehaviors.js +48 -6
  23. package/dist/commands/page.d.ts +1 -1
  24. package/dist/commands/page.js +62 -3
  25. package/dist/commands/shared/commonOptions.d.ts +4 -0
  26. package/dist/commands/shared/commonOptions.js +9 -0
  27. package/dist/commands/shared/optionTypes.d.ts +21 -0
  28. package/dist/commands/shared/startHelpers.d.ts +66 -0
  29. package/dist/commands/shared/startHelpers.js +91 -10
  30. package/dist/commands/shared/validation.d.ts +11 -0
  31. package/dist/commands/shared/validation.js +16 -0
  32. package/dist/commands.js +3 -0
  33. package/dist/daemon/launcher.d.ts +8 -1
  34. package/dist/daemon/launcher.js +3 -1
  35. package/dist/daemon/session/Session.d.ts +7 -0
  36. package/dist/daemon/session/Session.js +23 -1
  37. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  38. package/dist/daemon/session/commandRegistry.js +65 -9
  39. package/dist/daemon/session/interactions.d.ts +18 -5
  40. package/dist/daemon/session/interactions.js +22 -12
  41. package/dist/daemon.js +3565 -329
  42. package/dist/errors/messages.d.ts +85 -0
  43. package/dist/errors/messages.js +128 -1
  44. package/dist/index.js +2151 -960
  45. package/dist/ipc/client.d.ts +9 -0
  46. package/dist/ipc/client.js +13 -0
  47. package/dist/ipc/protocol/commands.d.ts +56 -1
  48. package/dist/ipc/protocol/commands.js +2 -0
  49. package/dist/ipc/protocol/domTypes.d.ts +35 -2
  50. package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
  51. package/dist/ipc/protocol/inspectTypes.js +10 -0
  52. package/dist/runtime/dom/actionEffects.d.ts +94 -15
  53. package/dist/runtime/dom/actionEffects.js +173 -27
  54. package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -14
  55. package/dist/runtime/dom/actionEffectsScripts.js +224 -32
  56. package/dist/runtime/dom/elementInfo.d.ts +26 -0
  57. package/dist/runtime/dom/elementInfo.js +65 -0
  58. package/dist/runtime/dom/eventListeners.js +14 -4
  59. package/dist/runtime/dom/formFillHelpers/fill.d.ts +3 -4
  60. package/dist/runtime/dom/formFillHelpers/fill.js +77 -28
  61. package/dist/runtime/dom/frameSelection.d.ts +11 -0
  62. package/dist/runtime/dom/frameSelection.js +20 -1
  63. package/dist/runtime/dom/frames.d.ts +38 -5
  64. package/dist/runtime/dom/frames.js +136 -21
  65. package/dist/runtime/dom/inspect.d.ts +28 -0
  66. package/dist/runtime/dom/inspect.js +557 -0
  67. package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
  68. package/dist/runtime/dom/inspectAllStyles.js +385 -0
  69. package/dist/runtime/dom/inspectCascade.d.ts +94 -0
  70. package/dist/runtime/dom/inspectCascade.js +371 -0
  71. package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
  72. package/dist/runtime/dom/inspectCascadeModel.js +232 -0
  73. package/dist/runtime/dom/inspectHints.d.ts +62 -0
  74. package/dist/runtime/dom/inspectHints.js +305 -0
  75. package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
  76. package/dist/runtime/dom/inspectLayoutModel.js +346 -0
  77. package/dist/runtime/dom/inspectModel.d.ts +74 -0
  78. package/dist/runtime/dom/inspectModel.js +184 -0
  79. package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
  80. package/dist/runtime/dom/inspectPaintModel.js +461 -0
  81. package/dist/runtime/dom/inspectRules.d.ts +37 -0
  82. package/dist/runtime/dom/inspectRules.js +101 -0
  83. package/dist/runtime/dom/inspectScripts.d.ts +132 -0
  84. package/dist/runtime/dom/inspectScripts.js +263 -0
  85. package/dist/runtime/dom/inspectTree.d.ts +40 -0
  86. package/dist/runtime/dom/inspectTree.js +134 -0
  87. package/dist/runtime/dom/inspectVariables.d.ts +33 -0
  88. package/dist/runtime/dom/inspectVariables.js +94 -0
  89. package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
  90. package/dist/runtime/dom/inspectWhyModel.js +134 -0
  91. package/dist/runtime/dom/layout.d.ts +5 -1
  92. package/dist/runtime/dom/layout.js +10 -3
  93. package/dist/runtime/dom/listenerPageScripts.d.ts +11 -5
  94. package/dist/runtime/dom/listenerPageScripts.js +95 -9
  95. package/dist/runtime/dom/listenerSummary.d.ts +4 -0
  96. package/dist/runtime/dom/listenerSummary.js +26 -9
  97. package/dist/runtime/dom/reactEventHelpers.d.ts +5 -0
  98. package/dist/runtime/dom/reactEventHelpers.js +12 -4
  99. package/dist/runtime/page/emulation.d.ts +20 -0
  100. package/dist/runtime/page/emulation.js +37 -0
  101. package/dist/telemetry/a11y.d.ts +10 -0
  102. package/dist/telemetry/a11y.js +78 -1
  103. package/dist/telemetry/console.d.ts +1 -0
  104. package/dist/telemetry/console.js +100 -5
  105. package/dist/telemetry/network.js +3 -1
  106. package/dist/types.d.ts +40 -0
  107. package/dist/ui/formatters/details.d.ts +8 -0
  108. package/dist/ui/formatters/details.js +59 -3
  109. package/dist/ui/formatters/dom.d.ts +2 -1
  110. package/dist/ui/formatters/dom.js +25 -9
  111. package/dist/ui/formatters/inspect.d.ts +39 -0
  112. package/dist/ui/formatters/inspect.js +596 -0
  113. package/dist/ui/formatters/installSkill.d.ts +11 -0
  114. package/dist/ui/formatters/installSkill.js +31 -0
  115. package/dist/ui/formatters/keyAttributes.d.ts +19 -0
  116. package/dist/ui/formatters/keyAttributes.js +84 -0
  117. package/dist/ui/formatters/layout.js +2 -2
  118. package/dist/ui/formatters/networkHeaders.d.ts +13 -0
  119. package/dist/ui/formatters/networkHeaders.js +23 -3
  120. package/dist/ui/formatters/networkList.d.ts +29 -1
  121. package/dist/ui/formatters/networkList.js +86 -20
  122. package/dist/ui/formatters/status.js +1 -1
  123. package/dist/ui/formatting.d.ts +9 -0
  124. package/dist/ui/formatting.js +6 -3
  125. package/dist/ui/messages/commands.d.ts +123 -7
  126. package/dist/ui/messages/commands.js +181 -10
  127. package/dist/ui/messages/networkMessages.d.ts +14 -0
  128. package/dist/ui/messages/networkMessages.js +18 -0
  129. package/dist/ui/messages/session.d.ts +14 -0
  130. package/dist/ui/messages/session.js +20 -0
  131. package/dist/utils/async.d.ts +9 -0
  132. package/dist/utils/async.js +17 -0
  133. package/dist/utils/color.d.ts +84 -0
  134. package/dist/utils/color.js +376 -0
  135. package/dist/utils/cssValues.d.ts +109 -0
  136. package/dist/utils/cssValues.js +236 -0
  137. package/dist/utils/selectorFilters.d.ts +12 -0
  138. package/dist/utils/selectorFilters.js +29 -0
  139. package/package.json +2 -1
@@ -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
@@ -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