browser-debugger-cli 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +5 -4
  3. package/dist/commands/cdp.d.ts +22 -1
  4. package/dist/commands/cdp.js +100 -43
  5. package/dist/commands/console.d.ts +12 -0
  6. package/dist/commands/console.js +67 -13
  7. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  8. package/dist/commands/dom/DomElementResolver.js +10 -3
  9. package/dist/commands/dom/a11y.d.ts +1 -1
  10. package/dist/commands/dom/a11y.js +23 -22
  11. package/dist/commands/dom/eval.d.ts +4 -2
  12. package/dist/commands/dom/eval.js +31 -7
  13. package/dist/commands/dom/form.js +10 -9
  14. package/dist/commands/dom/formInteraction.js +9 -8
  15. package/dist/commands/dom/get.js +32 -14
  16. package/dist/commands/dom/helpers/index.d.ts +1 -1
  17. package/dist/commands/dom/helpers/index.js +1 -1
  18. package/dist/commands/dom/helpers/query.d.ts +27 -3
  19. package/dist/commands/dom/helpers/query.js +152 -64
  20. package/dist/commands/dom/helpers/screenshot.js +13 -13
  21. package/dist/commands/dom/index.js +10 -3
  22. package/dist/commands/dom/query.d.ts +20 -2
  23. package/dist/commands/dom/query.js +39 -6
  24. package/dist/commands/dom/screenshot.js +3 -1
  25. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  26. package/dist/commands/dom/semanticUtils.js +40 -9
  27. package/dist/commands/helpJson.d.ts +82 -19
  28. package/dist/commands/helpJson.js +112 -41
  29. package/dist/commands/helpTopic.d.ts +16 -1
  30. package/dist/commands/helpTopic.js +59 -1
  31. package/dist/commands/installSkill.d.ts +15 -5
  32. package/dist/commands/installSkill.js +86 -16
  33. package/dist/commands/network/list.js +65 -12
  34. package/dist/commands/optionBehaviors.d.ts +25 -2
  35. package/dist/commands/optionBehaviors.js +81 -46
  36. package/dist/commands/peek.js +3 -0
  37. package/dist/commands/shared/CommandRunner.js +13 -13
  38. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  39. package/dist/commands/shared/daemonErrorHandler.js +21 -10
  40. package/dist/commands/shared/dataFetcher.d.ts +14 -4
  41. package/dist/commands/shared/dataFetcher.js +20 -4
  42. package/dist/commands/shared/followMode.d.ts +9 -1
  43. package/dist/commands/shared/followMode.js +22 -4
  44. package/dist/commands/shared/handleValidationError.js +3 -3
  45. package/dist/commands/shared/optionTypes.d.ts +17 -3
  46. package/dist/commands/shared/outputFile.js +6 -1
  47. package/dist/commands/shared/startHelpers.js +3 -3
  48. package/dist/commands/start.d.ts +7 -5
  49. package/dist/commands/start.js +65 -21
  50. package/dist/commands/stop.d.ts +11 -0
  51. package/dist/commands/stop.js +24 -1
  52. package/dist/commands.js +1 -1
  53. package/dist/connection/cdp.d.ts +7 -0
  54. package/dist/connection/cdp.js +9 -0
  55. package/dist/connection/chromeIdentity.d.ts +8 -2
  56. package/dist/connection/chromeIdentity.js +85 -13
  57. package/dist/connection/launcher.js +3 -2
  58. package/dist/constants.d.ts +29 -1
  59. package/dist/constants.js +35 -1
  60. package/dist/daemon/SessionController.js +8 -1
  61. package/dist/daemon/launcher.d.ts +3 -2
  62. package/dist/daemon/launcher.js +47 -3
  63. package/dist/daemon/session/Session.d.ts +5 -1
  64. package/dist/daemon/session/Session.js +42 -3
  65. package/dist/daemon/session/TelemetryStore.d.ts +15 -1
  66. package/dist/daemon/session/TelemetryStore.js +19 -1
  67. package/dist/daemon/session/commandRegistry.js +52 -18
  68. package/dist/daemon/session/interactions.d.ts +2 -1
  69. package/dist/daemon/session/interactions.js +13 -1
  70. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  71. package/dist/daemon/session/matchedStylesReset.js +46 -0
  72. package/dist/daemon/session/plugins.js +17 -2
  73. package/dist/daemon/session/teardown.js +1 -1
  74. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  75. package/dist/daemon/session/triggeredRequests.js +13 -7
  76. package/dist/daemon.js +2385 -1229
  77. package/dist/errors/messages.d.ts +62 -11
  78. package/dist/errors/messages.js +119 -22
  79. package/dist/index.js +14995 -9866
  80. package/dist/ipc/client.d.ts +18 -2
  81. package/dist/ipc/client.js +26 -5
  82. package/dist/ipc/protocol/auditTypes.d.ts +8 -2
  83. package/dist/ipc/protocol/commands.d.ts +16 -0
  84. package/dist/ipc/protocol/domTypes.d.ts +12 -0
  85. package/dist/ipc/protocol/inspectTypes.d.ts +7 -2
  86. package/dist/ipc/session/types.d.ts +7 -1
  87. package/dist/program.d.ts +14 -0
  88. package/dist/program.js +53 -0
  89. package/dist/runtime/dom/actionEffects.d.ts +5 -1
  90. package/dist/runtime/dom/actionEffects.js +26 -14
  91. package/dist/runtime/dom/audit.js +3 -2
  92. package/dist/runtime/dom/auditModel.js +6 -1
  93. package/dist/runtime/dom/auditScripts.d.ts +9 -3
  94. package/dist/runtime/dom/auditScripts.js +41 -5
  95. package/dist/runtime/dom/elementGeometry.d.ts +33 -3
  96. package/dist/runtime/dom/elementGeometry.js +44 -19
  97. package/dist/runtime/dom/elementInfo.d.ts +76 -18
  98. package/dist/runtime/dom/elementInfo.js +190 -40
  99. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  100. package/dist/runtime/dom/evalHelpers.js +67 -7
  101. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  102. package/dist/runtime/dom/formDiscovery.js +20 -3
  103. package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
  104. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  105. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  106. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  107. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  108. package/dist/runtime/dom/frameLayout.js +1 -0
  109. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  110. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  111. package/dist/runtime/dom/inspect.d.ts +17 -3
  112. package/dist/runtime/dom/inspect.js +45 -32
  113. package/dist/runtime/dom/inspectAllStyles.js +1 -0
  114. package/dist/runtime/dom/inspectHints.d.ts +1 -1
  115. package/dist/runtime/dom/inspectModel.d.ts +5 -4
  116. package/dist/runtime/dom/inspectModel.js +7 -3
  117. package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
  118. package/dist/runtime/dom/inspectPaintModel.js +3 -1
  119. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  120. package/dist/runtime/dom/inspectRules.js +205 -11
  121. package/dist/runtime/dom/inspectScripts.d.ts +29 -2
  122. package/dist/runtime/dom/inspectScripts.js +49 -10
  123. package/dist/runtime/dom/layout.d.ts +0 -2
  124. package/dist/runtime/dom/layout.js +10 -9
  125. package/dist/runtime/dom/reactEventHelpers.d.ts +17 -4
  126. package/dist/runtime/dom/reactEventHelpers.js +71 -28
  127. package/dist/runtime/dom/targetNode.d.ts +27 -10
  128. package/dist/runtime/dom/targetNode.js +283 -16
  129. package/dist/runtime/dom/wait.js +2 -1
  130. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  131. package/dist/runtime/page/bdgWorld.js +180 -0
  132. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  133. package/dist/runtime/page/replacedBuiltins.js +136 -0
  134. package/dist/session/QueryCacheManager.d.ts +4 -1
  135. package/dist/session/QueryCacheManager.js +5 -2
  136. package/dist/session/chrome.d.ts +4 -1
  137. package/dist/session/chrome.js +7 -1
  138. package/dist/session/cleanup/staleSession.d.ts +21 -4
  139. package/dist/session/cleanup/staleSession.js +79 -9
  140. package/dist/session/cleanup/userCommands.d.ts +4 -1
  141. package/dist/session/cleanup/userCommands.js +10 -5
  142. package/dist/session/daemonSocket.d.ts +10 -0
  143. package/dist/session/daemonSocket.js +22 -0
  144. package/dist/session/lastSession.d.ts +6 -3
  145. package/dist/session/lastSession.js +11 -5
  146. package/dist/session/paths.d.ts +3 -1
  147. package/dist/session/paths.js +5 -5
  148. package/dist/session/portClaims.js +4 -3
  149. package/dist/session/sessionList.d.ts +13 -5
  150. package/dist/session/sessionList.js +31 -7
  151. package/dist/telemetry/a11y.d.ts +15 -1
  152. package/dist/telemetry/a11y.js +85 -2
  153. package/dist/telemetry/console.d.ts +2 -1
  154. package/dist/telemetry/console.js +30 -21
  155. package/dist/telemetry/har/builder.js +1 -1
  156. package/dist/telemetry/network.d.ts +13 -16
  157. package/dist/telemetry/network.js +30 -52
  158. package/dist/telemetry/networkRetention.d.ts +83 -0
  159. package/dist/telemetry/networkRetention.js +117 -0
  160. package/dist/telemetry/pageCrash.d.ts +26 -0
  161. package/dist/telemetry/pageCrash.js +53 -0
  162. package/dist/types.d.ts +42 -0
  163. package/dist/ui/OutputBuilder.d.ts +10 -0
  164. package/dist/ui/OutputBuilder.js +12 -0
  165. package/dist/ui/formatters/a11y.d.ts +5 -7
  166. package/dist/ui/formatters/a11y.js +7 -61
  167. package/dist/ui/formatters/audit.js +14 -5
  168. package/dist/ui/formatters/cdp.d.ts +138 -0
  169. package/dist/ui/formatters/cdp.js +131 -0
  170. package/dist/ui/formatters/console/chronological.js +7 -5
  171. package/dist/ui/formatters/console/follow.d.ts +5 -2
  172. package/dist/ui/formatters/console/follow.js +7 -4
  173. package/dist/ui/formatters/console/json.d.ts +4 -7
  174. package/dist/ui/formatters/console/json.js +16 -14
  175. package/dist/ui/formatters/console/shared.d.ts +47 -2
  176. package/dist/ui/formatters/console/shared.js +33 -0
  177. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  178. package/dist/ui/formatters/console/summarize.js +57 -11
  179. package/dist/ui/formatters/console.d.ts +3 -2
  180. package/dist/ui/formatters/console.js +8 -10
  181. package/dist/ui/formatters/details.js +4 -2
  182. package/dist/ui/formatters/dom.d.ts +14 -5
  183. package/dist/ui/formatters/dom.js +30 -13
  184. package/dist/ui/formatters/helpFormatters.js +1 -1
  185. package/dist/ui/formatters/inspect.js +9 -3
  186. package/dist/ui/formatters/installSkill.d.ts +9 -1
  187. package/dist/ui/formatters/installSkill.js +32 -6
  188. package/dist/ui/formatters/layout.js +4 -2
  189. package/dist/ui/formatters/longValues.d.ts +14 -0
  190. package/dist/ui/formatters/longValues.js +23 -0
  191. package/dist/ui/formatters/networkList.d.ts +8 -2
  192. package/dist/ui/formatters/networkList.js +11 -3
  193. package/dist/ui/formatters/preview.d.ts +6 -1
  194. package/dist/ui/formatters/preview.js +67 -15
  195. package/dist/ui/formatters/sessions.d.ts +2 -2
  196. package/dist/ui/formatters/sessions.js +9 -2
  197. package/dist/ui/formatters/status.js +7 -0
  198. package/dist/ui/formatters/triggeredRequests.js +2 -1
  199. package/dist/ui/logging/logger.d.ts +1 -1
  200. package/dist/ui/messages/chrome.d.ts +20 -1
  201. package/dist/ui/messages/chrome.js +29 -3
  202. package/dist/ui/messages/commands.d.ts +153 -12
  203. package/dist/ui/messages/commands.js +198 -15
  204. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  205. package/dist/ui/messages/consoleMessages.js +32 -0
  206. package/dist/ui/messages/networkMessages.d.ts +24 -0
  207. package/dist/ui/messages/networkMessages.js +45 -0
  208. package/dist/ui/messages/preview.d.ts +6 -0
  209. package/dist/ui/messages/preview.js +9 -1
  210. package/dist/ui/messages/session.d.ts +13 -2
  211. package/dist/ui/messages/session.js +22 -3
  212. package/dist/utils/directories.d.ts +34 -0
  213. package/dist/utils/directories.js +88 -0
  214. package/dist/utils/display.d.ts +16 -0
  215. package/dist/utils/display.js +42 -0
  216. package/dist/utils/exitCodes.d.ts +1 -0
  217. package/dist/utils/exitCodes.js +6 -0
  218. package/dist/utils/http.d.ts +9 -2
  219. package/dist/utils/http.js +4 -3
  220. package/dist/utils/process.d.ts +12 -0
  221. package/dist/utils/process.js +25 -0
  222. package/dist/utils/strings.d.ts +19 -0
  223. package/dist/utils/strings.js +16 -0
  224. package/package.json +2 -2
@@ -8,22 +8,24 @@
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';
17
18
  import { throwIfInvalidSelector } from './formFillHelpers/shared.js';
18
19
  import { selectedProps } from './inspectAllStyles.js';
19
20
  import { buildCascadeFields } from './inspectCascadeModel.js';
20
- import { buildInspectResult } from './inspectModel.js';
21
- import { matchedStyles, sourceLabel, trackStyleSheets } from './inspectRules.js';
22
21
  import { explainUnsetVariables } from './inspectHints.js';
22
+ import { buildInspectResult } from './inspectModel.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';
26
27
  import { DEEP_QUERY_JS, missingElementError, selectorArgsJS } from './targetNode.js';
28
+ import { evaluateInBdgWorld, resolveNodeInBdgWorld } from '../page/bdgWorld.js';
27
29
  import { createLogger } from '../../ui/logging/index.js';
28
30
  import { getErrorMessage } from '../../utils/errors.js';
29
31
  import { EXIT_CODES } from '../../utils/exitCodes.js';
@@ -31,10 +33,6 @@ import { findSimilar } from '../../utils/suggestions.js';
31
33
  const log = createLogger('dom');
32
34
  /** Connections DOM and CSS were enabled on */
33
35
  const stylesEnabled = new WeakSet();
34
- /** Time allowed for the matched rules behind the default hints (large stylesheets take longer) */
35
- const HINTS_BUDGET_MS = 1000;
36
- /** Time allowed for them with --rules or --why */
37
- const RULES_BUDGET_MS = 5000;
38
36
  /** Distinguishes the object groups of concurrent calls */
39
37
  let groupCounter = 0;
40
38
  /**
@@ -170,7 +168,7 @@ function expandCustomPropertyPatterns(names, style) {
170
168
  *
171
169
  * @param cdp - CDP connection (stylesheet headers for the source labels)
172
170
  * @param sources - What was read
173
- * @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
174
172
  */
175
173
  function cascadeFields(cdp, sources) {
176
174
  if (!sources.matched)
@@ -248,9 +246,7 @@ function pickedHow(params, count, index) {
248
246
  * @returns Remote object id, or undefined when the node left the page
249
247
  */
250
248
  async function resolveCachedNode(cdp, backendNodeId, objectGroup) {
251
- const resolved = (await cdp
252
- .send('DOM.resolveNode', { backendNodeId, objectGroup })
253
- .catch((error) => {
249
+ const resolved = (await resolveNodeInBdgWorld(cdp, { backendNodeId, objectGroup }).catch((error) => {
254
250
  log.debug(`Node ${backendNodeId} not resolved: ${getErrorMessage(error)}`);
255
251
  return {};
256
252
  }));
@@ -270,10 +266,10 @@ async function resolveCachedNode(cdp, backendNodeId, objectGroup) {
270
266
  * @throws CommandError (81) invalid selector, (91) page script failure
271
267
  */
272
268
  async function querySelector(cdp, selector, objectGroup) {
273
- const response = (await cdp.send('Runtime.evaluate', {
269
+ const response = await evaluateInBdgWorld(cdp, {
274
270
  expression: `(${DEEP_QUERY_JS})(${selectorArgsJS(selector)})`,
275
271
  objectGroup,
276
- }));
272
+ });
277
273
  if (response.exceptionDetails || !response.result.objectId) {
278
274
  if (response.exceptionDetails)
279
275
  throwIfInvalidSelector(response.exceptionDetails, selector);
@@ -306,7 +302,10 @@ async function callOn(cdp, objectId, functionDeclaration, args) {
306
302
  }
307
303
  /**
308
304
  * Read everything about the element: the page-side walk, `dom layout`'s
309
- * 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.
310
309
  *
311
310
  * @param cdp - CDP connection
312
311
  * @param objectId - The element
@@ -316,14 +315,16 @@ async function callOn(cdp, objectId, functionDeclaration, args) {
316
315
  */
317
316
  async function readSources(cdp, objectId, params, objectGroup) {
318
317
  const related = await relatedNodes(cdp, objectId, objectGroup);
319
- const [raw, measured, styles] = await Promise.all([
318
+ const [raw, measured, { nodeId, ...styles }] = await Promise.all([
320
319
  readPage(cdp, objectId, params),
321
320
  measure(cdp, params.selector, related.node),
322
- readStyles(cdp, related, params),
321
+ readStyles(cdp, related),
323
322
  ]);
323
+ const matched = await readMatched(cdp, nodeId, params);
324
324
  return {
325
325
  raw,
326
326
  ...styles,
327
+ ...(matched && { matched }),
327
328
  fonts: raw.textHolder ? styles.fonts.textHolder : styles.fonts.node,
328
329
  ...measured,
329
330
  ...(params.rules && { rules: true }),
@@ -459,20 +460,19 @@ async function nodeIdLookup(cdp, related) {
459
460
  }
460
461
  /**
461
462
  * Computed styles of the element, its parent and pseudo-elements, the
462
- * platform fonts of its text and its border box size.
463
+ * platform fonts of its text, its border box size and its node id.
463
464
  *
464
465
  * @param cdp - CDP connection
465
466
  * @param related - Backend node ids
466
467
  * @returns CDP styles
467
468
  */
468
- async function readStyles(cdp, related, params) {
469
+ async function readStyles(cdp, related) {
469
470
  await enableStyleDomains(cdp);
470
471
  const nodeIdOf = await nodeIdLookup(cdp, related);
471
472
  const optionalStyle = (backendNodeId) => backendNodeId === undefined
472
473
  ? Promise.resolve(undefined)
473
474
  : computedStyle(cdp, nodeIdOf(backendNodeId));
474
- const [matched, style, parentStyle, holderStyle, nodeFonts, holderFonts, size, pseudo] = await Promise.all([
475
- readMatched(cdp, nodeIdOf(related.node), params),
475
+ const [style, parentStyle, holderStyle, nodeFonts, holderFonts, size, pseudo] = await Promise.all([
476
476
  computedStyle(cdp, nodeIdOf(related.node)),
477
477
  optionalStyle(related.parent),
478
478
  optionalStyle(related.textHolder),
@@ -488,26 +488,39 @@ async function readStyles(cdp, related, params) {
488
488
  pseudo,
489
489
  fonts: { node: nodeFonts, textHolder: holderFonts },
490
490
  ...(size && { size }),
491
- ...(matched && { matched }),
491
+ nodeId: nodeIdOf(related.node),
492
492
  };
493
493
  }
494
494
  /**
495
- * The element's matched rules, when hints, `--rules` or `--why` need them:
496
- * within {@link HINTS_BUDGET_MS} for the default hints (skipped on very
497
- * 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.
498
513
  *
499
514
  * @param cdp - CDP connection
500
515
  * @param nodeId - Node id of the element
501
516
  * @param params - Request
502
- * @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)
503
518
  */
504
519
  async function readMatched(cdp, nodeId, params) {
505
- const explicit = params.rules === true || params.why !== undefined;
506
- const skipped = !explicit && (params.hints === false || params.props !== undefined || params.all === true);
507
- if (nodeId === undefined || skipped) {
520
+ const read = matchedStylesRead(params);
521
+ if (nodeId === undefined || !read)
508
522
  return undefined;
509
- }
510
- return matchedStyles(cdp, nodeId, explicit ? RULES_BUDGET_MS : HINTS_BUDGET_MS);
523
+ return matchedStyles(cdp, nodeId, read.budgetMs, { skipWhenSlow: read.skipWhenSlow });
511
524
  }
512
525
  /**
513
526
  * A pseudo-element's computed styles and size.
@@ -170,6 +170,7 @@ const PREFIXED_KEPT = new Set([
170
170
  '-webkit-line-clamp',
171
171
  '-webkit-text-stroke-width',
172
172
  '-webkit-text-security',
173
+ '-webkit-text-fill-color',
173
174
  ]);
174
175
  /** SVG paint and geometry properties (noise on HTML elements) */
175
176
  const SVG_ONLY = /^(fill|fill-opacity|fill-rule|stroke|stroke-.*|stop-.*|flood-.*|lighting-color|marker-.*|clip-rule|color-interpolation.*|color-rendering|shape-rendering|text-anchor|dominant-baseline|alignment-baseline|baseline-shift|vector-effect|buffered-rendering|paint-order|mask-type)$/;
@@ -4,9 +4,9 @@
4
4
  * checked against authored declarations only (never the browser's own
5
5
  * styles), and `var()` references to custom properties that are not set.
6
6
  */
7
+ import type { InspectHint } from '../../ipc/protocol/inspectTypes.js';
7
8
  import type { Declaration, Resolution } from './inspectCascade.js';
8
9
  import type { StyleMap } from './inspectLayoutModel.js';
9
- import type { InspectHint } from '../../ipc/protocol/inspectTypes.js';
10
10
  import type { VariableSetter } from './inspectScripts.js';
11
11
  /** A declaration that has no effect */
12
12
  export interface CssHint {
@@ -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 */
@@ -52,7 +52,8 @@ export interface InspectRequest {
52
52
  propValues?: InspectResult['props'];
53
53
  }
54
54
  /**
55
- * The element's label: tag, id and the first classes, with a count of the rest.
55
+ * The element's label: tag, id and the first classes ({@link isLabelClass}),
56
+ * with a count of the rest.
56
57
  *
57
58
  * @param raw - Tag, id and classes
58
59
  * @returns e.g. `a.z-1.max-sm:hidden(+11)`, `input#user-name`
@@ -5,6 +5,7 @@
5
5
  * tree. Pure: every input is plain data, so the rules are tested without a
6
6
  * browser.
7
7
  */
8
+ import { isLabelClass } from './elementInfo.js';
8
9
  import { allStyles } from './inspectAllStyles.js';
9
10
  import { buildBox, buildLayout } from './inspectLayoutModel.js';
10
11
  import { buildEffects, buildFills, buildSvgPaint, buildFx, buildOutline, buildPseudo, buildRadius, buildState, buildStrokes, buildText, effectiveBackground, } from './inspectPaintModel.js';
@@ -14,17 +15,19 @@ import { normalizeCssValue, round1 } from '../../utils/cssValues.js';
14
15
  /** Classes shown in the label (the rest are counted) */
15
16
  const LABEL_CLASSES = 2;
16
17
  /**
17
- * The element's label: tag, id and the first classes, with a count of the rest.
18
+ * The element's label: tag, id and the first classes ({@link isLabelClass}),
19
+ * with a count of the rest.
18
20
  *
19
21
  * @param raw - Tag, id and classes
20
22
  * @returns e.g. `a.z-1.max-sm:hidden(+11)`, `input#user-name`
21
23
  */
22
24
  export function elementLabel(raw) {
23
- const shown = raw.classes
25
+ const classes = raw.classes.filter(isLabelClass);
26
+ const shown = classes
24
27
  .slice(0, LABEL_CLASSES)
25
28
  .map((name) => `.${name}`)
26
29
  .join('');
27
- const more = raw.classes.length - LABEL_CLASSES;
30
+ const more = classes.length - LABEL_CLASSES;
28
31
  return `${raw.tag}${raw.id ? `#${raw.id}` : ''}${shown}${more > 0 ? `(+${more})` : ''}`;
29
32
  }
30
33
  /**
@@ -48,6 +51,7 @@ export function visibilityOf(layout, rendered) {
48
51
  ...(offscreen && { offscreen }),
49
52
  ...(layout.coveredBy && { coveredBy: layout.coveredBy }),
50
53
  ...(layout.coverTransparent && { coverTransparent: true }),
54
+ ...(layout.masked && { masked: layout.masked }),
51
55
  };
52
56
  }
53
57
  /**
@@ -46,6 +46,8 @@ export declare function effectiveBackground(backgrounds: readonly RawBackground[
46
46
  inherited: boolean;
47
47
  overImage: boolean;
48
48
  };
49
+ /** Why a contrast over a background image or gradient is approximate (its pixels are unknown) */
50
+ export declare const OVER_IMAGE_RISK = "background image or gradient behind";
49
51
  /**
50
52
  * Contrast of the text color with the background behind it, both painted
51
53
  * as the browser composites them ({@link paintOver}), with what makes the
@@ -128,6 +128,8 @@ export function effectiveBackground(backgrounds, canvasDark) {
128
128
  overImage: painted.overImage,
129
129
  };
130
130
  }
131
+ /** Why a contrast over a background image or gradient is approximate (its pixels are unknown) */
132
+ export const OVER_IMAGE_RISK = 'background image or gradient behind';
131
133
  /**
132
134
  * Contrast of the text color with the background behind it, both painted
133
135
  * as the browser composites them ({@link paintOver}), with what makes the
@@ -146,7 +148,7 @@ export function textContrast(style, raw) {
146
148
  const ratio = Math.floor(contrastRatio(painted.text, painted.background) * 100) / 100;
147
149
  const size = pxNumber(style['font-size']) ?? 16;
148
150
  const weight = Number(style['font-weight'] ?? 400);
149
- const approximate = raw.paintRisks ?? [];
151
+ const approximate = [...(raw.paintRisks ?? []), ...(painted.overImage ? [OVER_IMAGE_RISK] : [])];
150
152
  return {
151
153
  ratio,
152
154
  level: contrastLevel(ratio, size, weight),
@@ -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.