browser-debugger-cli 0.13.0 → 0.15.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 (159) hide show
  1. package/.claude/skills/bdg/SKILL.md +101 -187
  2. package/README.md +4 -4
  3. package/dist/commands/cdp.js +1 -0
  4. package/dist/commands/cleanup.js +3 -0
  5. package/dist/commands/console.js +5 -1
  6. package/dist/commands/dom/a11y.d.ts +1 -1
  7. package/dist/commands/dom/a11y.js +20 -20
  8. package/dist/commands/dom/eval.d.ts +3 -1
  9. package/dist/commands/dom/eval.js +8 -5
  10. package/dist/commands/dom/formInteraction.js +1 -1
  11. package/dist/commands/dom/get.js +25 -7
  12. package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
  13. package/dist/commands/dom/helpers/evalResult.js +59 -0
  14. package/dist/commands/dom/index.js +7 -2
  15. package/dist/commands/dom/query.d.ts +2 -1
  16. package/dist/commands/dom/query.js +5 -3
  17. package/dist/commands/dom/screenshot.js +1 -0
  18. package/dist/commands/helpJson.d.ts +1 -1
  19. package/dist/commands/helpJson.js +4 -4
  20. package/dist/commands/helpTopic.js +10 -4
  21. package/dist/commands/network/har.js +18 -14
  22. package/dist/commands/network/list.js +46 -3
  23. package/dist/commands/optionBehaviors.d.ts +25 -2
  24. package/dist/commands/optionBehaviors.js +60 -42
  25. package/dist/commands/peek.js +3 -0
  26. package/dist/commands/shared/CommandRunner.js +13 -13
  27. package/dist/commands/shared/daemonErrorHandler.js +2 -2
  28. package/dist/commands/shared/dataFetcher.d.ts +4 -2
  29. package/dist/commands/shared/dataFetcher.js +11 -3
  30. package/dist/commands/shared/handleValidationError.js +3 -3
  31. package/dist/commands/shared/optionTypes.d.ts +15 -3
  32. package/dist/commands/shared/outputFile.d.ts +2 -1
  33. package/dist/commands/shared/outputFile.js +7 -4
  34. package/dist/commands/shared/startHelpers.js +3 -3
  35. package/dist/commands/status.js +3 -1
  36. package/dist/commands/stop.js +2 -1
  37. package/dist/connection/chromeIdentity.d.ts +8 -2
  38. package/dist/connection/chromeIdentity.js +85 -13
  39. package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
  40. package/dist/connection/launcher/flagsBuilder.js +107 -23
  41. package/dist/connection/launcher.d.ts +1 -1
  42. package/dist/connection/launcher.js +1 -2
  43. package/dist/constants.d.ts +31 -5
  44. package/dist/constants.js +37 -5
  45. package/dist/daemon/SessionController.js +2 -0
  46. package/dist/daemon/launcher.d.ts +17 -3
  47. package/dist/daemon/launcher.js +37 -7
  48. package/dist/daemon/session/Session.d.ts +2 -1
  49. package/dist/daemon/session/Session.js +10 -2
  50. package/dist/daemon/session/TelemetryStore.d.ts +7 -0
  51. package/dist/daemon/session/TelemetryStore.js +6 -0
  52. package/dist/daemon/session/commandRegistry.js +25 -7
  53. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  54. package/dist/daemon/session/matchedStylesReset.js +46 -0
  55. package/dist/daemon/session/plugins.js +1 -0
  56. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  57. package/dist/daemon/session/triggeredRequests.js +13 -7
  58. package/dist/daemon.js +8742 -8315
  59. package/dist/errors/messages.d.ts +31 -0
  60. package/dist/errors/messages.js +96 -6
  61. package/dist/index.js +1129 -548
  62. package/dist/ipc/client.d.ts +6 -1
  63. package/dist/ipc/client.js +11 -2
  64. package/dist/ipc/protocol/commands.d.ts +8 -0
  65. package/dist/ipc/protocol/inspectTypes.d.ts +5 -2
  66. package/dist/ipc/session/types.d.ts +5 -1
  67. package/dist/ipc/transport/index.d.ts +6 -0
  68. package/dist/ipc/transport/index.js +16 -1
  69. package/dist/program.d.ts +14 -0
  70. package/dist/program.js +53 -0
  71. package/dist/runtime/dom/elementGeometry.d.ts +23 -0
  72. package/dist/runtime/dom/elementGeometry.js +17 -15
  73. package/dist/runtime/dom/elementInfo.d.ts +13 -4
  74. package/dist/runtime/dom/elementInfo.js +15 -5
  75. package/dist/runtime/dom/evalHelpers.d.ts +24 -4
  76. package/dist/runtime/dom/evalHelpers.js +40 -12
  77. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  78. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  79. package/dist/runtime/dom/frames.d.ts +2 -1
  80. package/dist/runtime/dom/frames.js +3 -1
  81. package/dist/runtime/dom/inspect.d.ts +17 -3
  82. package/dist/runtime/dom/inspect.js +40 -26
  83. package/dist/runtime/dom/inspectModel.d.ts +3 -3
  84. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  85. package/dist/runtime/dom/inspectRules.js +205 -11
  86. package/dist/runtime/dom/layout.d.ts +0 -2
  87. package/dist/runtime/dom/layout.js +1 -2
  88. package/dist/runtime/dom/reactEventHelpers.d.ts +4 -1
  89. package/dist/runtime/dom/reactEventHelpers.js +9 -2
  90. package/dist/runtime/dom/targetNode.d.ts +10 -6
  91. package/dist/runtime/dom/targetNode.js +15 -8
  92. package/dist/runtime/page/emulation.js +6 -5
  93. package/dist/runtime/page/userAgent.d.ts +86 -2
  94. package/dist/runtime/page/userAgent.js +154 -33
  95. package/dist/session/paths.d.ts +38 -3
  96. package/dist/session/paths.js +154 -7
  97. package/dist/session/portClaims.d.ts +0 -8
  98. package/dist/session/portClaims.js +1 -22
  99. package/dist/session/sessionList.d.ts +5 -1
  100. package/dist/session/sessionList.js +5 -1
  101. package/dist/telemetry/a11y.d.ts +15 -1
  102. package/dist/telemetry/a11y.js +83 -0
  103. package/dist/telemetry/har/builder.d.ts +12 -1
  104. package/dist/telemetry/har/builder.js +11 -3
  105. package/dist/telemetry/har/sanitize.d.ts +24 -0
  106. package/dist/telemetry/har/sanitize.js +138 -0
  107. package/dist/telemetry/har/sanitizeBody.d.ts +38 -0
  108. package/dist/telemetry/har/sanitizeBody.js +168 -0
  109. package/dist/telemetry/network.d.ts +13 -16
  110. package/dist/telemetry/network.js +30 -52
  111. package/dist/telemetry/networkRetention.d.ts +83 -0
  112. package/dist/telemetry/networkRetention.js +117 -0
  113. package/dist/types.d.ts +26 -0
  114. package/dist/ui/OutputBuilder.d.ts +10 -0
  115. package/dist/ui/OutputBuilder.js +12 -0
  116. package/dist/ui/formatters/a11y.d.ts +5 -7
  117. package/dist/ui/formatters/a11y.js +7 -61
  118. package/dist/ui/formatters/console/chronological.js +4 -4
  119. package/dist/ui/formatters/console/follow.d.ts +4 -2
  120. package/dist/ui/formatters/console/follow.js +6 -3
  121. package/dist/ui/formatters/console/json.d.ts +3 -6
  122. package/dist/ui/formatters/console/json.js +9 -13
  123. package/dist/ui/formatters/console/shared.d.ts +17 -2
  124. package/dist/ui/formatters/console/shared.js +17 -0
  125. package/dist/ui/formatters/console/summarize.d.ts +2 -2
  126. package/dist/ui/formatters/console/summarize.js +22 -7
  127. package/dist/ui/formatters/console.d.ts +1 -1
  128. package/dist/ui/formatters/console.js +1 -5
  129. package/dist/ui/formatters/details.js +1 -1
  130. package/dist/ui/formatters/dom.d.ts +13 -4
  131. package/dist/ui/formatters/dom.js +25 -7
  132. package/dist/ui/formatters/layout.js +2 -1
  133. package/dist/ui/formatters/longValues.d.ts +14 -0
  134. package/dist/ui/formatters/longValues.js +23 -0
  135. package/dist/ui/formatters/networkList.d.ts +8 -2
  136. package/dist/ui/formatters/networkList.js +11 -2
  137. package/dist/ui/formatters/preview.d.ts +4 -1
  138. package/dist/ui/formatters/preview.js +55 -13
  139. package/dist/ui/formatters/sessions.d.ts +3 -2
  140. package/dist/ui/formatters/sessions.js +10 -3
  141. package/dist/ui/formatters/status.js +7 -0
  142. package/dist/ui/formatters/triggeredRequests.js +2 -1
  143. package/dist/ui/messages/chrome.d.ts +34 -7
  144. package/dist/ui/messages/chrome.js +81 -15
  145. package/dist/ui/messages/commands.d.ts +29 -8
  146. package/dist/ui/messages/commands.js +36 -8
  147. package/dist/ui/messages/networkMessages.d.ts +50 -0
  148. package/dist/ui/messages/networkMessages.js +66 -0
  149. package/dist/ui/messages/session.d.ts +8 -0
  150. package/dist/ui/messages/session.js +10 -0
  151. package/dist/utils/atomicFile.d.ts +2 -1
  152. package/dist/utils/atomicFile.js +5 -2
  153. package/dist/utils/directories.d.ts +41 -0
  154. package/dist/utils/directories.js +48 -0
  155. package/dist/utils/http.d.ts +9 -2
  156. package/dist/utils/http.js +4 -3
  157. package/dist/utils/strings.d.ts +19 -0
  158. package/dist/utils/strings.js +16 -0
  159. package/package.json +2 -2
@@ -84,11 +84,12 @@ export declare function frameContextLostError(conn: Pick<CDPConnection, 'send'>,
84
84
  * @param script - JavaScript expression
85
85
  * @param query - Requested frame (index, name/id attribute, or part of the name, id or URL)
86
86
  * @param listedIds - Frame id behind each index of the last `dom frames` listing, if any
87
+ * @param full - `--full`: copy the result with every entry
87
88
  * @returns Value, type and the frame's URL
88
89
  * @throws CommandError (81/83) when the frame is ambiguous or missing, (87)
89
90
  * when an index names another frame than when it was listed, (83) when it
90
91
  * navigated or was removed while the script ran, else as evaluateScript
91
92
  */
92
- export declare function evaluateInFrame(page: CDPConnection, wsUrl: string, script: string, query: string, listedIds?: string[]): Promise<DomEvalData>;
93
+ export declare function evaluateInFrame(page: CDPConnection, wsUrl: string, script: string, query: string, listedIds?: string[], full?: boolean): Promise<DomEvalData>;
93
94
  export {};
94
95
  //# sourceMappingURL=frames.d.ts.map
@@ -529,12 +529,13 @@ export async function frameContextLostError(conn, page, frame) {
529
529
  * @param script - JavaScript expression
530
530
  * @param query - Requested frame (index, name/id attribute, or part of the name, id or URL)
531
531
  * @param listedIds - Frame id behind each index of the last `dom frames` listing, if any
532
+ * @param full - `--full`: copy the result with every entry
532
533
  * @returns Value, type and the frame's URL
533
534
  * @throws CommandError (81/83) when the frame is ambiguous or missing, (87)
534
535
  * when an index names another frame than when it was listed, (83) when it
535
536
  * navigated or was removed while the script ran, else as evaluateScript
536
537
  */
537
- export async function evaluateInFrame(page, wsUrl, script, query, listedIds) {
538
+ export async function evaluateInFrame(page, wsUrl, script, query, listedIds, full = false) {
538
539
  return withFrameConnection(page, wsUrl, async (fc) => {
539
540
  const { frame, uniqueContextId } = await resolveFrame(fc, query, listedIds);
540
541
  try {
@@ -542,6 +543,7 @@ export async function evaluateInFrame(page, wsUrl, script, query, listedIds) {
542
543
  ...(frame.sessionId && { sessionId: frame.sessionId }),
543
544
  uniqueContextId,
544
545
  recovery: recoverySender(fc, frame.sessionId),
546
+ full,
545
547
  });
546
548
  return { ...result, frame: frame.info.url };
547
549
  }
@@ -8,9 +8,10 @@
8
8
  * tree), `dom layout`'s measurement (page position, hidden, covered,
9
9
  * offscreen) and, once the nodes are pushed to CDP, `CSS.getComputedStyleForNode`
10
10
  * for the element, its layout parent and its `::before`/`::after`,
11
- * `CSS.getPlatformFontsForNode` for its text and `DOM.getBoxModel`. DOM and
12
- * CSS are enabled on the first inspect and kept on. Matched rules are not
13
- * read (no cascade).
11
+ * `CSS.getPlatformFontsForNode` for its text and `DOM.getBoxModel`. After
12
+ * those, for the hints, `--rules` and `--why`, the matched rules
13
+ * (`CSS.getMatchedStylesForNode`, see {@link matchedStyles}). DOM and CSS are
14
+ * enabled on the first inspect and kept on.
14
15
  */
15
16
  import type { CDPConnection } from '../../connection/cdp.js';
16
17
  import type { DomInspectCommand } from '../../ipc/protocol/commands.js';
@@ -25,6 +26,19 @@ import type { InspectResult } from '../../ipc/protocol/inspectTypes.js';
25
26
  * selector or unknown property, (87) the cached element left the page
26
27
  */
27
28
  export declare function inspectElement(cdp: CDPConnection, params: DomInspectCommand): Promise<InspectResult>;
29
+ /**
30
+ * How a request reads the element's matched rules, when hints, `--rules` or
31
+ * `--why` need them: within {@link HINTS_BUDGET_MS} for the default hints,
32
+ * not waited for at all on a document marked slow; within
33
+ * {@link RULES_BUDGET_MS} when asked for explicitly.
34
+ *
35
+ * @param params - Request
36
+ * @returns Budget and whether to skip on a slow document, or undefined when not needed
37
+ */
38
+ export declare function matchedStylesRead(params: DomInspectCommand): {
39
+ budgetMs: number;
40
+ skipWhenSlow: boolean;
41
+ } | undefined;
28
42
  /**
29
43
  * Enable DOM and CSS once per connection (kept on: CSS.enable replays every
30
44
  * stylesheet, which costs up to a few hundred ms on large sites the first time).
@@ -8,9 +8,10 @@
8
8
  * tree), `dom layout`'s measurement (page position, hidden, covered,
9
9
  * offscreen) and, once the nodes are pushed to CDP, `CSS.getComputedStyleForNode`
10
10
  * for the element, its layout parent and its `::before`/`::after`,
11
- * `CSS.getPlatformFontsForNode` for its text and `DOM.getBoxModel`. DOM and
12
- * CSS are enabled on the first inspect and kept on. Matched rules are not
13
- * read (no cascade).
11
+ * `CSS.getPlatformFontsForNode` for its text and `DOM.getBoxModel`. After
12
+ * those, for the hints, `--rules` and `--why`, the matched rules
13
+ * (`CSS.getMatchedStylesForNode`, see {@link matchedStyles}). DOM and CSS are
14
+ * enabled on the first inspect and kept on.
14
15
  */
15
16
  import { CommandError } from '../../errors/index.js';
16
17
  import { operationFailedError, unknownCssPropertyError, whyAllPropertyError, } from '../../errors/messages.js';
@@ -19,7 +20,7 @@ import { selectedProps } from './inspectAllStyles.js';
19
20
  import { buildCascadeFields } from './inspectCascadeModel.js';
20
21
  import { explainUnsetVariables } from './inspectHints.js';
21
22
  import { buildInspectResult } from './inspectModel.js';
22
- import { matchedStyles, sourceLabel, trackStyleSheets } from './inspectRules.js';
23
+ import { HINTS_BUDGET_MS, matchedStyles, RULES_BUDGET_MS, sourceLabel, trackStyleSheets, } from './inspectRules.js';
23
24
  import { INSPECT_PAGE_JS, RELATED_NODE_JS, VARIABLE_SETTERS_JS, } from './inspectScripts.js';
24
25
  import { DEFAULT_TREE_DEPTH, DEFAULT_TREE_LIMIT } from './inspectTree.js';
25
26
  import { inspectLayout } from './layout.js';
@@ -32,10 +33,6 @@ import { findSimilar } from '../../utils/suggestions.js';
32
33
  const log = createLogger('dom');
33
34
  /** Connections DOM and CSS were enabled on */
34
35
  const stylesEnabled = new WeakSet();
35
- /** Time allowed for the matched rules behind the default hints (large stylesheets take longer) */
36
- const HINTS_BUDGET_MS = 1000;
37
- /** Time allowed for them with --rules or --why */
38
- const RULES_BUDGET_MS = 5000;
39
36
  /** Distinguishes the object groups of concurrent calls */
40
37
  let groupCounter = 0;
41
38
  /**
@@ -171,7 +168,7 @@ function expandCustomPropertyPatterns(names, style) {
171
168
  *
172
169
  * @param cdp - CDP connection (stylesheet headers for the source labels)
173
170
  * @param sources - What was read
174
- * @returns Cascade fields, or `cascade: 'timeout' | 'failed'` when the rules were not read
171
+ * @returns Cascade fields, or `cascade: 'timeout' | 'failed' | 'skipped'` when the rules were not read
175
172
  */
176
173
  function cascadeFields(cdp, sources) {
177
174
  if (!sources.matched)
@@ -305,7 +302,10 @@ async function callOn(cdp, objectId, functionDeclaration, args) {
305
302
  }
306
303
  /**
307
304
  * Read everything about the element: the page-side walk, `dom layout`'s
308
- * measurement and the CDP styles, fonts and box.
305
+ * measurement and the CDP styles, fonts and box, then the matched rules.
306
+ * Chrome answers one request at a time and the rules can take seconds on
307
+ * CSS-heavy pages, so they are asked for last: the other reads do not wait
308
+ * behind them, only their own budget does.
309
309
  *
310
310
  * @param cdp - CDP connection
311
311
  * @param objectId - The element
@@ -315,14 +315,16 @@ async function callOn(cdp, objectId, functionDeclaration, args) {
315
315
  */
316
316
  async function readSources(cdp, objectId, params, objectGroup) {
317
317
  const related = await relatedNodes(cdp, objectId, objectGroup);
318
- const [raw, measured, styles] = await Promise.all([
318
+ const [raw, measured, { nodeId, ...styles }] = await Promise.all([
319
319
  readPage(cdp, objectId, params),
320
320
  measure(cdp, params.selector, related.node),
321
- readStyles(cdp, related, params),
321
+ readStyles(cdp, related),
322
322
  ]);
323
+ const matched = await readMatched(cdp, nodeId, params);
323
324
  return {
324
325
  raw,
325
326
  ...styles,
327
+ ...(matched && { matched }),
326
328
  fonts: raw.textHolder ? styles.fonts.textHolder : styles.fonts.node,
327
329
  ...measured,
328
330
  ...(params.rules && { rules: true }),
@@ -458,20 +460,19 @@ async function nodeIdLookup(cdp, related) {
458
460
  }
459
461
  /**
460
462
  * Computed styles of the element, its parent and pseudo-elements, the
461
- * platform fonts of its text and its border box size.
463
+ * platform fonts of its text, its border box size and its node id.
462
464
  *
463
465
  * @param cdp - CDP connection
464
466
  * @param related - Backend node ids
465
467
  * @returns CDP styles
466
468
  */
467
- async function readStyles(cdp, related, params) {
469
+ async function readStyles(cdp, related) {
468
470
  await enableStyleDomains(cdp);
469
471
  const nodeIdOf = await nodeIdLookup(cdp, related);
470
472
  const optionalStyle = (backendNodeId) => backendNodeId === undefined
471
473
  ? Promise.resolve(undefined)
472
474
  : computedStyle(cdp, nodeIdOf(backendNodeId));
473
- const [matched, style, parentStyle, holderStyle, nodeFonts, holderFonts, size, pseudo] = await Promise.all([
474
- readMatched(cdp, nodeIdOf(related.node), params),
475
+ const [style, parentStyle, holderStyle, nodeFonts, holderFonts, size, pseudo] = await Promise.all([
475
476
  computedStyle(cdp, nodeIdOf(related.node)),
476
477
  optionalStyle(related.parent),
477
478
  optionalStyle(related.textHolder),
@@ -487,26 +488,39 @@ async function readStyles(cdp, related, params) {
487
488
  pseudo,
488
489
  fonts: { node: nodeFonts, textHolder: holderFonts },
489
490
  ...(size && { size }),
490
- ...(matched && { matched }),
491
+ nodeId: nodeIdOf(related.node),
491
492
  };
492
493
  }
493
494
  /**
494
- * The element's matched rules, when hints, `--rules` or `--why` need them:
495
- * within {@link HINTS_BUDGET_MS} for the default hints (skipped on very
496
- * large stylesheets), {@link RULES_BUDGET_MS} when asked for explicitly.
495
+ * How a request reads the element's matched rules, when hints, `--rules` or
496
+ * `--why` need them: within {@link HINTS_BUDGET_MS} for the default hints,
497
+ * not waited for at all on a document marked slow; within
498
+ * {@link RULES_BUDGET_MS} when asked for explicitly.
499
+ *
500
+ * @param params - Request
501
+ * @returns Budget and whether to skip on a slow document, or undefined when not needed
502
+ */
503
+ export function matchedStylesRead(params) {
504
+ if (params.rules === true || params.why !== undefined) {
505
+ return { budgetMs: RULES_BUDGET_MS, skipWhenSlow: false };
506
+ }
507
+ if (params.hints === false || params.props !== undefined || params.all === true)
508
+ return undefined;
509
+ return { budgetMs: HINTS_BUDGET_MS, skipWhenSlow: true };
510
+ }
511
+ /**
512
+ * The element's matched rules, read as {@link matchedStylesRead} says.
497
513
  *
498
514
  * @param cdp - CDP connection
499
515
  * @param nodeId - Node id of the element
500
516
  * @param params - Request
501
- * @returns Matched styles, `timeout`, or undefined when not needed (or no node id)
517
+ * @returns Matched styles or why they are missing, or undefined when not needed (or no node id)
502
518
  */
503
519
  async function readMatched(cdp, nodeId, params) {
504
- const explicit = params.rules === true || params.why !== undefined;
505
- const skipped = !explicit && (params.hints === false || params.props !== undefined || params.all === true);
506
- if (nodeId === undefined || skipped) {
520
+ const read = matchedStylesRead(params);
521
+ if (nodeId === undefined || !read)
507
522
  return undefined;
508
- }
509
- return matchedStyles(cdp, nodeId, explicit ? RULES_BUDGET_MS : HINTS_BUDGET_MS);
523
+ return matchedStyles(cdp, nodeId, read.budgetMs, { skipWhenSlow: read.skipWhenSlow });
510
524
  }
511
525
  /**
512
526
  * A pseudo-element's computed styles and size.
@@ -5,11 +5,11 @@
5
5
  * tree. Pure: every input is plain data, so the rules are tested without a
6
6
  * browser.
7
7
  */
8
- import type { Protocol } from '../../connection/typed-cdp.js';
9
8
  import type { ElementLayout } from '../../ipc/protocol/domTypes.js';
10
9
  import type { InspectResult, InspectVisibility } from '../../ipc/protocol/inspectTypes.js';
11
10
  import { type StyleMap } from './inspectLayoutModel.js';
12
11
  import { type PlatformFont, type PseudoSource } from './inspectPaintModel.js';
12
+ import type { MatchedStyles } from './inspectRules.js';
13
13
  import type { RawInspect } from './inspectScripts.js';
14
14
  /** What one inspect read */
15
15
  export interface InspectSources {
@@ -29,8 +29,8 @@ export interface InspectSources {
29
29
  /** The element as `dom layout` measures it */
30
30
  layout?: ElementLayout;
31
31
  colorScheme?: 'light' | 'dark';
32
- /** Matched rules for the cascade fields; `timeout` or `failed` when they were not read */
33
- matched?: Protocol.CSS.GetMatchedStylesForNodeResponse | 'timeout' | 'failed';
32
+ /** Matched rules for the cascade fields; `timeout`, `failed` or `skipped` when they were not read */
33
+ matched?: MatchedStyles;
34
34
  /** `--rules` was asked for */
35
35
  rules?: boolean;
36
36
  /** `--why` property */
@@ -22,16 +22,42 @@ export declare function trackStyleSheets(cdp: CDPConnection): void;
22
22
  * @returns Headers
23
23
  */
24
24
  export declare function styleSheetHeaders(cdp: CDPConnection): Iterable<Protocol.CSS.CSSStyleSheetHeader>;
25
+ /** Time allowed for the matched rules behind the default hints; a read within it clears the slow mark */
26
+ export declare const HINTS_BUDGET_MS = 1000;
27
+ /** Time allowed for them with --rules or --why */
28
+ export declare const RULES_BUDGET_MS = 5000;
29
+ /** Matched styles of an element, or why they are missing */
30
+ export type MatchedStyles = Protocol.CSS.GetMatchedStylesForNodeResponse | 'timeout' | 'failed' | 'skipped';
31
+ /**
32
+ * Forget the kept answers of a connection (requests still running stay
33
+ * shared). For commands that may change the page in ways CDP reports no
34
+ * event for: clicks, typing, hovering, scripts, emulation.
35
+ *
36
+ * @param cdp - CDP connection
37
+ */
38
+ export declare function resetMatchedStyles(cdp: CDPConnection): void;
25
39
  /**
26
40
  * The rules that match an element, with its inline style and what its
27
- * ancestors pass down, or why they are missing.
41
+ * ancestors pass down, or why they are missing. A request still running for
42
+ * the element is shared; a slow answer (over {@link KEEP_ANSWERS_SLOWER_THAN_MS})
43
+ * is reused for {@link KEPT_ANSWER_TTL_MS} unless the document, its
44
+ * stylesheets or its DOM change or a command may have changed the page.
45
+ * Another element's request is sent only after the one Chrome is working on,
46
+ * within the budget. When the request this call sent or shared outlasts the
47
+ * budget, the document is marked slow: `skipWhenSlow` reads then return
48
+ * `skipped` at once (unless the answer is kept) until a read is fast again,
49
+ * a stylesheet changes or the page navigates.
28
50
  *
29
51
  * @param cdp - CDP connection
30
52
  * @param nodeId - Node id of the element
31
53
  * @param budgetMs - Time allowed
32
- * @returns Matched styles, `timeout` (longer than the budget) or `failed` (CDP error)
54
+ * @param options - `skipWhenSlow`: do not wait on a document marked slow (the default hints)
55
+ * @returns Matched styles, `timeout` (longer than the budget), `failed` (CDP error)
56
+ * or `skipped` (slow document)
33
57
  */
34
- export declare function matchedStyles(cdp: CDPConnection, nodeId: number, budgetMs: number): Promise<Protocol.CSS.GetMatchedStylesForNodeResponse | 'timeout' | 'failed'>;
58
+ export declare function matchedStyles(cdp: CDPConnection, nodeId: number, budgetMs: number, options?: {
59
+ skipWhenSlow?: boolean;
60
+ }): Promise<MatchedStyles>;
35
61
  /**
36
62
  * Where a declaration comes from, for people: the selector and the file
37
63
  * with its line (and column, for single-line minified files), or what kind
@@ -37,24 +37,218 @@ export function trackStyleSheets(cdp) {
37
37
  export function styleSheetHeaders(cdp) {
38
38
  return headersByConnection.get(cdp)?.values() ?? [];
39
39
  }
40
+ /** Time allowed for the matched rules behind the default hints; a read within it clears the slow mark */
41
+ export const HINTS_BUDGET_MS = 1000;
42
+ /** Time allowed for them with --rules or --why */
43
+ export const RULES_BUDGET_MS = 5000;
44
+ /** Answers that came faster are not kept: reading again is cheap and always current */
45
+ const KEEP_ANSWERS_SLOWER_THAN_MS = 300;
46
+ /**
47
+ * How long a kept answer is reused. Page state such as `:checked`, `:hover`
48
+ * or `:focus` changes without any CDP event, so answers are kept only briefly
49
+ * (and dropped by every command that may change the page, see {@link resetMatchedStyles}).
50
+ */
51
+ const KEPT_ANSWER_TTL_MS = 5000;
52
+ /** Answers kept per document (one can be several MB on CSS-heavy pages) */
53
+ const MAX_KEPT_ANSWERS = 4;
54
+ /** Stylesheet events: kept answers are dropped and the slow mark cleared */
55
+ const STYLESHEET_EVENTS = ['CSS.styleSheetAdded', 'CSS.styleSheetChanged', 'CSS.styleSheetRemoved'];
56
+ /** Other events after which kept answers may be stale */
57
+ const STYLE_CHANGE_EVENTS = [
58
+ 'CSS.mediaQueryResultChanged',
59
+ 'DOM.attributeModified',
60
+ 'DOM.attributeRemoved',
61
+ 'DOM.inlineStyleInvalidated',
62
+ 'DOM.childNodeInserted',
63
+ 'DOM.childNodeRemoved',
64
+ 'DOM.childNodeCountUpdated',
65
+ 'DOM.pseudoElementAdded',
66
+ 'DOM.pseudoElementRemoved',
67
+ ];
68
+ /** Matched-styles state per connection */
69
+ const documentStylesByConnection = new WeakMap();
70
+ /**
71
+ * The matched-styles state of a connection, tracked from its first use: a
72
+ * new document (`DOM.documentUpdated`) starts afresh (a request still running
73
+ * for the old one no longer holds back new ones), stylesheet changes drop the
74
+ * kept answers and the slow mark, style and DOM changes the kept answers.
75
+ * Events of other sessions (iframes) are ignored.
76
+ *
77
+ * @param cdp - CDP connection
78
+ * @returns State
79
+ */
80
+ function documentStyles(cdp) {
81
+ const existing = documentStylesByConnection.get(cdp);
82
+ if (existing)
83
+ return existing;
84
+ const state = { requests: new Map(), slow: false, document: 0 };
85
+ documentStylesByConnection.set(cdp, state);
86
+ cdp.on('DOM.documentUpdated', (_params, sessionId) => {
87
+ if (sessionId)
88
+ return;
89
+ state.document++;
90
+ state.requests.clear();
91
+ state.running = undefined;
92
+ state.slow = false;
93
+ });
94
+ for (const event of STYLESHEET_EVENTS) {
95
+ cdp.on(event, (_params, sessionId) => {
96
+ if (sessionId)
97
+ return;
98
+ state.requests.clear();
99
+ state.slow = false;
100
+ });
101
+ }
102
+ for (const event of STYLE_CHANGE_EVENTS) {
103
+ cdp.on(event, (_params, sessionId) => {
104
+ if (!sessionId)
105
+ state.requests.clear();
106
+ });
107
+ }
108
+ return state;
109
+ }
110
+ /**
111
+ * Forget the kept answers of a connection (requests still running stay
112
+ * shared). For commands that may change the page in ways CDP reports no
113
+ * event for: clicks, typing, hovering, scripts, emulation.
114
+ *
115
+ * @param cdp - CDP connection
116
+ */
117
+ export function resetMatchedStyles(cdp) {
118
+ documentStylesByConnection.get(cdp)?.requests.clear();
119
+ }
120
+ /**
121
+ * Record an answer: a fast one clears the slow mark and is dropped, a slow
122
+ * one is kept for {@link KEPT_ANSWER_TTL_MS}, a failed one is dropped.
123
+ *
124
+ * @param state - Document state
125
+ * @param nodeId - Node id of the element
126
+ * @param request - The request
127
+ * @param tookMs - How long Chrome took
128
+ */
129
+ function settleRequest(state, nodeId, request, tookMs) {
130
+ if (state.running === request.promise)
131
+ state.running = undefined;
132
+ if (request.document !== state.document)
133
+ return;
134
+ const answered = request.answer !== 'failed';
135
+ if (answered && tookMs <= HINTS_BUDGET_MS)
136
+ state.slow = false;
137
+ if (answered && tookMs > KEEP_ANSWERS_SLOWER_THAN_MS) {
138
+ request.expiresAt = Date.now() + KEPT_ANSWER_TTL_MS;
139
+ }
140
+ else if (state.requests.get(nodeId) === request) {
141
+ state.requests.delete(nodeId);
142
+ }
143
+ }
144
+ /**
145
+ * Send a matched-styles request and share it while it runs, dropping the
146
+ * least recently used entries beyond {@link MAX_KEPT_ANSWERS}.
147
+ *
148
+ * @param cdp - CDP connection
149
+ * @param state - Document state
150
+ * @param nodeId - Node id of the element
151
+ * @returns The request
152
+ */
153
+ function sendMatchedRequest(cdp, state, nodeId) {
154
+ const sentAt = Date.now();
155
+ const request = {
156
+ document: state.document,
157
+ promise: cdp
158
+ .send('CSS.getMatchedStylesForNode', { nodeId })
159
+ .then((response) => response)
160
+ .catch((error) => {
161
+ log.debug(`CSS.getMatchedStylesForNode failed: ${String(error)}`);
162
+ return 'failed';
163
+ }),
164
+ };
165
+ state.running = request.promise;
166
+ void request.promise.then((answer) => {
167
+ request.answer = answer;
168
+ settleRequest(state, nodeId, request, Date.now() - sentAt);
169
+ });
170
+ state.requests.set(nodeId, request);
171
+ for (const oldest of state.requests.keys()) {
172
+ if (state.requests.size <= MAX_KEPT_ANSWERS)
173
+ break;
174
+ state.requests.delete(oldest);
175
+ }
176
+ return request;
177
+ }
178
+ /**
179
+ * The element's request still running or kept answer, unless expired;
180
+ * marked as the most recently used.
181
+ *
182
+ * @param state - Document state
183
+ * @param nodeId - Node id of the element
184
+ * @returns The request, or undefined
185
+ */
186
+ function keptRequest(state, nodeId) {
187
+ const kept = state.requests.get(nodeId);
188
+ if (!kept)
189
+ return undefined;
190
+ state.requests.delete(nodeId);
191
+ if (kept.expiresAt !== undefined && kept.expiresAt <= Date.now())
192
+ return undefined;
193
+ state.requests.set(nodeId, kept);
194
+ return kept;
195
+ }
196
+ /**
197
+ * The element's request, sent once Chrome has answered the one it is
198
+ * working on (another element's, or this one's sent by a concurrent call).
199
+ *
200
+ * @param cdp - CDP connection
201
+ * @param state - Document state
202
+ * @param nodeId - Node id of the element
203
+ * @param deadline - When to give up waiting
204
+ * @returns The request, or undefined when the running one outlasted the deadline
205
+ */
206
+ async function requestWhenFree(cdp, state, nodeId, deadline) {
207
+ while (state.running) {
208
+ const kept = state.requests.get(nodeId);
209
+ if (kept)
210
+ return kept;
211
+ if ((await raceTimeout(state.running, deadline - Date.now())) === undefined)
212
+ return undefined;
213
+ }
214
+ return state.requests.get(nodeId) ?? sendMatchedRequest(cdp, state, nodeId);
215
+ }
40
216
  /**
41
217
  * The rules that match an element, with its inline style and what its
42
- * ancestors pass down, or why they are missing.
218
+ * ancestors pass down, or why they are missing. A request still running for
219
+ * the element is shared; a slow answer (over {@link KEEP_ANSWERS_SLOWER_THAN_MS})
220
+ * is reused for {@link KEPT_ANSWER_TTL_MS} unless the document, its
221
+ * stylesheets or its DOM change or a command may have changed the page.
222
+ * Another element's request is sent only after the one Chrome is working on,
223
+ * within the budget. When the request this call sent or shared outlasts the
224
+ * budget, the document is marked slow: `skipWhenSlow` reads then return
225
+ * `skipped` at once (unless the answer is kept) until a read is fast again,
226
+ * a stylesheet changes or the page navigates.
43
227
  *
44
228
  * @param cdp - CDP connection
45
229
  * @param nodeId - Node id of the element
46
230
  * @param budgetMs - Time allowed
47
- * @returns Matched styles, `timeout` (longer than the budget) or `failed` (CDP error)
231
+ * @param options - `skipWhenSlow`: do not wait on a document marked slow (the default hints)
232
+ * @returns Matched styles, `timeout` (longer than the budget), `failed` (CDP error)
233
+ * or `skipped` (slow document)
48
234
  */
49
- export async function matchedStyles(cdp, nodeId, budgetMs) {
50
- const request = cdp
51
- .send('CSS.getMatchedStylesForNode', { nodeId })
52
- .then((response) => response)
53
- .catch((error) => {
54
- log.debug(`CSS.getMatchedStylesForNode failed: ${String(error)}`);
55
- return 'failed';
56
- });
57
- return (await raceTimeout(request, budgetMs)) ?? 'timeout';
235
+ export async function matchedStyles(cdp, nodeId, budgetMs, options = {}) {
236
+ const state = documentStyles(cdp);
237
+ const kept = keptRequest(state, nodeId);
238
+ if (kept?.answer)
239
+ return kept.answer;
240
+ if (options.skipWhenSlow && state.slow)
241
+ return 'skipped';
242
+ const deadline = Date.now() + budgetMs;
243
+ const request = kept ?? (await requestWhenFree(cdp, state, nodeId, deadline));
244
+ if (!request)
245
+ return 'timeout';
246
+ const answer = await raceTimeout(request.promise, deadline - Date.now());
247
+ if (answer)
248
+ return answer;
249
+ if (request.document === state.document)
250
+ state.slow = true;
251
+ return 'timeout';
58
252
  }
59
253
  /**
60
254
  * File name of a URL, without query or hash; the host for a site's root.
@@ -11,8 +11,6 @@ import type { CDPConnection } from '../../connection/cdp.js';
11
11
  import type { DomLayoutCommand } from '../../ipc/protocol/commands.js';
12
12
  import type { LayoutComputedStyle, LayoutResult, PageLayout } from '../../ipc/protocol/domTypes.js';
13
13
  import { type ElementGeometry } from './elementGeometry.js';
14
- /** Matches measured per command (the rest are counted as omitted) */
15
- export declare const LAYOUT_ELEMENT_LIMIT = 100;
16
14
  /**
17
15
  * Page-side end of a visible span kept clear of an overlay scrollbar along
18
16
  * an edge (16 CSS px wide): overlay scrollbars (macOS, mobile) show for about a
@@ -7,6 +7,7 @@
7
7
  * shows. The measurements are classified outside the page
8
8
  * ({@link classifyViewportPosition}).
9
9
  */
10
+ import { LAYOUT_ELEMENT_LIMIT } from '../../constants.js';
10
11
  import { CommandError } from '../../errors/index.js';
11
12
  import { operationFailedError } from '../../errors/messages.js';
12
13
  import { ELEMENT_GEOMETRY_JS, FRAME_OFFSET_JS, VIEWPORT_SIZE_JS, classifyViewportPosition, } from './elementGeometry.js';
@@ -20,8 +21,6 @@ import { createLogger } from '../../ui/logging/index.js';
20
21
  import { getErrorMessage } from '../../utils/errors.js';
21
22
  import { EXIT_CODES } from '../../utils/exitCodes.js';
22
23
  const log = createLogger('dom');
23
- /** Matches measured per command (the rest are counted as omitted) */
24
- export const LAYOUT_ELEMENT_LIMIT = 100;
25
24
  /**
26
25
  * Page-side end of a visible span kept clear of an overlay scrollbar along
27
26
  * an edge (16 CSS px wide): overlay scrollbars (macOS, mobile) show for about a
@@ -107,7 +107,10 @@ export declare const FILL_READ_BACK_SCRIPT: string;
107
107
  * dialog or bubble can swallow input while the page looks normal.
108
108
  * Slotted text hit-tests as its shadow host, so an element in a shadow root
109
109
  * that shows slotted content (a button labelled through a `<slot>`) is
110
- * topmost where its host is hit.
110
+ * topmost where its host is hit. An element that is not topmost because an
111
+ * ancestor in the flat tree clips it away (a collapsed
112
+ * `height: 0; overflow: hidden` accordion, {@link ANCESTOR_CLIP_JS}) is
113
+ * reported as hidden by it, not as covered.
111
114
  */
112
115
  export declare const CLICK_ELEMENT_SCRIPT: string;
113
116
  /**
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { FILL_REFUSALS, LABEL_WITHOUT_CONTROL, NAME_QUERY_PLACEHOLDER, VIA_LABEL_SUFFIX, } from '../../errors/messages.js';
9
9
  import { REVEAL_SNAPSHOT_JS } from './actionEffectsScripts.js';
10
+ import { ANCESTOR_CLIP_JS } from './elementGeometry.js';
10
11
  import { DISABLED_CAUSE_JS, ELEMENT_DESCRIPTION_JS, ELEMENT_IDENTITY_JS, } from './elementInfo.js';
11
12
  import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS } from './targetNode.js';
12
13
  /**
@@ -484,7 +485,10 @@ export const FILL_READ_BACK_SCRIPT = `(() => {
484
485
  * dialog or bubble can swallow input while the page looks normal.
485
486
  * Slotted text hit-tests as its shadow host, so an element in a shadow root
486
487
  * that shows slotted content (a button labelled through a `<slot>`) is
487
- * topmost where its host is hit.
488
+ * topmost where its host is hit. An element that is not topmost because an
489
+ * ancestor in the flat tree clips it away (a collapsed
490
+ * `height: 0; overflow: hidden` accordion, {@link ANCESTOR_CLIP_JS}) is
491
+ * reported as hidden by it, not as covered.
488
492
  */
489
493
  export const CLICK_ELEMENT_SCRIPT = `
490
494
  (function(selector, parts, index, action) {
@@ -639,7 +643,10 @@ export const CLICK_ELEMENT_SCRIPT = `
639
643
  else if (el.closest('[inert]')) obstruction = 'inert (the page made it non-interactive)';
640
644
  else if (style.pointerEvents === 'none') obstruction = 'not clickable (pointer-events: none)';
641
645
  else if (!hasSize) obstruction = 'zero-size';
642
- else if (!hittable) obstruction = 'covered by another element' + coveredBy();
646
+ else if (!hittable) {
647
+ const collapsed = (${ANCESTOR_CLIP_JS})(el, rect, describe).collapsed;
648
+ obstruction = collapsed ? 'hidden (' + collapsed + ')' : 'covered by another element' + coveredBy();
649
+ }
643
650
 
644
651
  if (action === 'hover') (${REVEAL_SNAPSHOT_JS})(el);
645
652