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
@@ -178,8 +178,10 @@ export declare function getNetworkHeaders(options?: {
178
178
  *
179
179
  * @param method - CDP method name (e.g., 'Network.getCookies')
180
180
  * @param params - Optional method parameters
181
+ * @param options - `isolated`: a bdg page script, run in bdg's isolated world
182
+ * (`Runtime.evaluate`, `DOM.resolveNode`)
181
183
  * @returns Response with CDP method result
182
- * @throws Error if connection fails; CommandError (102) when the page was busy and its scripts were terminated
184
+ * @throws Error if connection fails; CommandError (102) when the page was busy and its scripts were terminated, (107) when the page crashed
183
185
  *
184
186
  * @example
185
187
  * ```typescript
@@ -189,7 +191,21 @@ export declare function getNetworkHeaders(options?: {
189
191
  * }
190
192
  * ```
191
193
  */
192
- export declare function callCDP(method: string, params?: Record<string, unknown>): Promise<ClientResponse<'cdp_call'>>;
194
+ export declare function callCDP(method: string, params?: Record<string, unknown>, options?: {
195
+ isolated?: boolean;
196
+ }): Promise<ClientResponse<'cdp_call'>>;
197
+ /**
198
+ * Send a CDP call for one of bdg's own page scripts: `Runtime.evaluate` and
199
+ * `DOM.resolveNode` run in bdg's isolated world, so a page that replaced
200
+ * built-ins (`querySelectorAll`, `JSON.stringify`) cannot change what they
201
+ * find or return.
202
+ *
203
+ * @param method - CDP method name
204
+ * @param params - Method parameters
205
+ * @returns Response with the CDP method result
206
+ * @throws Like {@link callCDP}
207
+ */
208
+ export declare function callBdgScript(method: string, params?: Record<string, unknown>): Promise<ClientResponse<'cdp_call'>>;
193
209
  /**
194
210
  * Evaluate a JavaScript expression in the active page (or one of its iframes) via the daemon.
195
211
  */
@@ -251,8 +251,10 @@ export async function getNetworkHeaders(options) {
251
251
  *
252
252
  * @param method - CDP method name (e.g., 'Network.getCookies')
253
253
  * @param params - Optional method parameters
254
+ * @param options - `isolated`: a bdg page script, run in bdg's isolated world
255
+ * (`Runtime.evaluate`, `DOM.resolveNode`)
254
256
  * @returns Response with CDP method result
255
- * @throws Error if connection fails; CommandError (102) when the page was busy and its scripts were terminated
257
+ * @throws Error if connection fails; CommandError (102) when the page was busy and its scripts were terminated, (107) when the page crashed
256
258
  *
257
259
  * @example
258
260
  * ```typescript
@@ -262,13 +264,32 @@ export async function getNetworkHeaders(options) {
262
264
  * }
263
265
  * ```
264
266
  */
265
- export async function callCDP(method, params) {
266
- const response = await sendCommand('cdp_call', { method, ...(params && { params }) });
267
- if (response.status === 'error' && response.exitCode === EXIT_CODES.CDP_TIMEOUT) {
268
- throw new CommandError(response.error ?? `${method} timed out`, response.suggestion ? { suggestion: response.suggestion } : {}, EXIT_CODES.CDP_TIMEOUT);
267
+ export async function callCDP(method, params, options = {}) {
268
+ const response = await sendCommand('cdp_call', {
269
+ method,
270
+ ...(params && { params }),
271
+ ...(options.isolated && { isolated: true }),
272
+ });
273
+ const fatal = [EXIT_CODES.CDP_TIMEOUT, EXIT_CODES.PAGE_CRASHED];
274
+ if (response.status === 'error' && response.exitCode && fatal.includes(response.exitCode)) {
275
+ throw new CommandError(response.error ?? `${method} failed`, response.suggestion ? { suggestion: response.suggestion } : {}, response.exitCode);
269
276
  }
270
277
  return response;
271
278
  }
279
+ /**
280
+ * Send a CDP call for one of bdg's own page scripts: `Runtime.evaluate` and
281
+ * `DOM.resolveNode` run in bdg's isolated world, so a page that replaced
282
+ * built-ins (`querySelectorAll`, `JSON.stringify`) cannot change what they
283
+ * find or return.
284
+ *
285
+ * @param method - CDP method name
286
+ * @param params - Method parameters
287
+ * @returns Response with the CDP method result
288
+ * @throws Like {@link callCDP}
289
+ */
290
+ export function callBdgScript(method, params) {
291
+ return callCDP(method, params, { isolated: true });
292
+ }
272
293
  /**
273
294
  * Evaluate a JavaScript expression in the active page (or one of its iframes) via the daemon.
274
295
  */
@@ -19,7 +19,9 @@ export interface AuditContrastItem {
19
19
  size: number;
20
20
  weight: number;
21
21
  inView: boolean;
22
- /** Why the ratio is approximate (blend modes, filters) */
22
+ /** Opacity of the text and its ancestors, when below 1 (the colors are faded by it) */
23
+ opacity?: number;
24
+ /** Why the ratio is approximate (blend modes, filters, an element behind or on top, out of view) */
23
25
  approximate?: string[];
24
26
  }
25
27
  /** An image drawn larger than its pixels, or with another aspect ratio */
@@ -51,8 +53,10 @@ export interface AuditResult {
51
53
  level: 'AA' | 'AAA';
52
54
  /** Text holders checked */
53
55
  checked: number;
54
- /** How many are below the level */
56
+ /** How many are below the level (measured exactly) */
55
57
  failing: number;
58
+ /** How many more look below the level but cannot be measured exactly (text over images, blend modes, layers): not listed, `dom inspect` them */
59
+ uncertain?: number;
56
60
  /** The weakest ones, at most `--limit` */
57
61
  items: AuditContrastItem[];
58
62
  };
@@ -109,6 +113,8 @@ export interface AuditResult {
109
113
  /** Identical animations this one stands for (2 or more) */
110
114
  count?: number;
111
115
  }>;
116
+ /** Visible `<canvas>` elements: scripts may animate them, which `animations` cannot see */
117
+ canvases?: number;
112
118
  }
113
119
  /** A stylesheet line where `css search` found the text */
114
120
  export interface CssSearchMatch {
@@ -66,10 +66,18 @@ export interface SessionPeekData {
66
66
  }>;
67
67
  /** Navigation id of the page currently loaded. */
68
68
  currentNavigationId: number;
69
+ /** When the page's renderer crashed (epoch ms), while it is not loaded again */
70
+ pageCrashedAt?: number;
69
71
  /** Total number of network requests (for pagination). */
70
72
  totalNetwork: number;
71
73
  /** Total number of console messages (for pagination). */
72
74
  totalConsole: number;
75
+ /** Console messages dropped at the limit (the oldest; indices start after them) */
76
+ droppedConsole?: number;
77
+ /** Finished network requests dropped at the request cap (the oldest) */
78
+ droppedNetwork?: number;
79
+ /** Response bodies evicted at the total body budget (the oldest) */
80
+ evictedNetworkBodies?: number;
73
81
  /** Whether there are more network items available. */
74
82
  hasMoreNetwork?: boolean;
75
83
  /** Whether there are more console items available. */
@@ -99,6 +107,12 @@ export interface CdpCallCommand {
99
107
  method: string;
100
108
  /** Optional parameters for the CDP method. */
101
109
  params?: Record<string, unknown>;
110
+ /**
111
+ * A bdg page script: `Runtime.evaluate` and `DOM.resolveNode` run in bdg's
112
+ * isolated world, out of reach of built-ins the page replaced (not for
113
+ * `bdg cdp`, whose calls stay in the page's world)
114
+ */
115
+ isolated?: boolean;
102
116
  }
103
117
  /**
104
118
  * CDP call command response data.
@@ -183,6 +197,8 @@ export interface DomEvalData {
183
197
  subtype?: string;
184
198
  /** URL of the iframe the script ran in (with `frame`; empty when it has none) */
185
199
  frame?: string;
200
+ /** Set when the page replaced built-ins bdg's copy of the result uses */
201
+ warning?: string;
186
202
  }
187
203
  /** An iframe of the page, as listed by `bdg dom frames` */
188
204
  export interface DomFrame {
@@ -96,6 +96,8 @@ export interface ActionEffects {
96
96
  settled?: false;
97
97
  /** What the page was still working on (with `settled: false`) */
98
98
  pending?: PendingChanges;
99
+ /** All built-ins the page replaced that bdg's action scripts use (when the warning mentions them) */
100
+ replacedBuiltins?: string[];
99
101
  }
100
102
  /** A filled field's value differing from the one given */
101
103
  export interface FillValueMismatch {
@@ -165,6 +167,11 @@ export interface ClickResult extends ActionEffects {
165
167
  method?: 'mouse' | 'dom';
166
168
  /** What was done: click, double (click), right (click) or hover */
167
169
  action?: 'click' | 'double' | 'right' | 'hover';
170
+ /** How far the page scrolled to bring the element into view (CSS px; absent when it did not move) */
171
+ scrolledBy?: {
172
+ x: number;
173
+ y: number;
174
+ };
168
175
  /** Exit code for a failure */
169
176
  exitCode?: number;
170
177
  /** Why the DOM fallback was used (element covered or without size) */
@@ -425,6 +432,11 @@ export interface ElementLayout {
425
432
  * `opacity: 0 on div#menu` (`inViewport` still says where it is)
426
433
  */
427
434
  invisible?: string;
435
+ /**
436
+ * A `mask-image` on it or an ancestor, e.g. `mask-image on div.hero`: part
437
+ * or all of it may not show (how much is not evaluated)
438
+ */
439
+ masked?: string;
428
440
  /** Inside an `inert` element: shown, but a user cannot interact with it */
429
441
  inert?: true;
430
442
  computed: LayoutComputedStyle;
@@ -270,6 +270,8 @@ export interface InspectVisibility {
270
270
  coveredBy?: string;
271
271
  /** The cover paints nothing there: the element shows, but clicks land on the cover */
272
272
  coverTransparent?: true;
273
+ /** A `mask-image` on it or an ancestor, e.g. `mask-image on div.hero` (how much it hides is not evaluated) */
274
+ masked?: string;
273
275
  }
274
276
  /** A property asked for with `--props` */
275
277
  export interface InspectProp {
@@ -445,7 +447,10 @@ export interface InspectResult {
445
447
  rules?: InspectRule[];
446
448
  /** `--why <property>`: one entry, or one per longhand of a shorthand whose sides differ */
447
449
  why?: InspectWhy[];
448
- /** The cascade was not read: Chrome took longer than the time allowed, or failed */
449
- cascade?: 'timeout' | 'failed';
450
+ /**
451
+ * The cascade was not read: Chrome took longer than the time allowed, failed,
452
+ * or (hints only) was not asked because it was too slow earlier on this page
453
+ */
454
+ cascade?: 'timeout' | 'failed' | 'skipped';
450
455
  }
451
456
  //# sourceMappingURL=inspectTypes.d.ts.map
@@ -8,8 +8,12 @@ import type { ColorScheme, ViewportSize } from '../../types.js';
8
8
  * Session activity metrics.
9
9
  */
10
10
  export interface SessionActivity {
11
- /** Total network requests captured. */
11
+ /** Network requests kept (finished ones; the newest at the cap). */
12
12
  networkRequestsCaptured: number;
13
+ /** Oldest finished requests dropped at the request cap (left out when none). */
14
+ networkRequestsDropped?: number;
15
+ /** Oldest response bodies evicted at the total body budget (left out when none). */
16
+ networkBodiesEvicted?: number;
13
17
  /** Total console messages captured. */
14
18
  consoleMessagesCaptured: number;
15
19
  /** Timestamp of last network request. */
@@ -29,5 +33,7 @@ export interface PageState {
29
33
  viewport?: ViewportSize;
30
34
  /** `prefers-color-scheme` the page sees (left out when the page did not answer in time). */
31
35
  colorScheme?: ColorScheme;
36
+ /** When the page's renderer crashed (epoch ms); `bdg page reload` brings it back */
37
+ crashedAt?: number;
32
38
  }
33
39
  //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The bdg command tree: root command, its options and every registered command.
3
+ */
4
+ import { Command, type OutputConfiguration } from 'commander';
5
+ /**
6
+ * Build the bdg program with every command registered, without parsing
7
+ * anything. Commander errors throw instead of exiting; the output
8
+ * configuration is set before commands are registered so they inherit it.
9
+ *
10
+ * @param output - Where Commander writes help and errors
11
+ * @returns Root command
12
+ */
13
+ export declare function buildProgram(output?: OutputConfiguration): Command;
14
+ //# sourceMappingURL=program.d.ts.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The bdg command tree: root command, its options and every registered command.
3
+ */
4
+ import { Command, Option } from 'commander';
5
+ import { commandRegistry } from './commands.js';
6
+ import { VERSION } from './utils/version.js';
7
+ const CLI_NAME = 'bdg';
8
+ const CLI_DESCRIPTION = 'Browser telemetry via Chrome DevTools Protocol';
9
+ const SESSION_OPTION_FLAGS = '--session <name>';
10
+ const SESSION_OPTION_DESCRIPTION = 'Use a named session (own daemon, Chrome and port) instead of the default one; env: BDG_SESSION';
11
+ /**
12
+ * Make `--debug`, `-q` and `--session` accepted after any subcommand (program
13
+ * options are positional).
14
+ *
15
+ * @param command - Command whose subcommands get the hidden global options
16
+ */
17
+ function addGlobalOptions(command) {
18
+ for (const sub of command.commands) {
19
+ if (!sub.options.some((option) => option.long === '--debug')) {
20
+ sub.addOption(new Option('--debug', 'Enable debug logging').hideHelp());
21
+ }
22
+ if (!sub.options.some((option) => option.long === '--quiet')) {
23
+ sub.addOption(new Option('-q, --quiet', 'Hide tips and hints').hideHelp());
24
+ }
25
+ if (!sub.options.some((option) => option.long === '--session')) {
26
+ sub.addOption(new Option(SESSION_OPTION_FLAGS, SESSION_OPTION_DESCRIPTION).hideHelp());
27
+ }
28
+ addGlobalOptions(sub);
29
+ }
30
+ }
31
+ /**
32
+ * Build the bdg program with every command registered, without parsing
33
+ * anything. Commander errors throw instead of exiting; the output
34
+ * configuration is set before commands are registered so they inherit it.
35
+ *
36
+ * @param output - Where Commander writes help and errors
37
+ * @returns Root command
38
+ */
39
+ export function buildProgram(output = {}) {
40
+ const program = new Command()
41
+ .name(CLI_NAME)
42
+ .description(CLI_DESCRIPTION)
43
+ .version(VERSION)
44
+ .option('--debug', 'Enable debug logging (verbose output)')
45
+ .option(SESSION_OPTION_FLAGS, SESSION_OPTION_DESCRIPTION)
46
+ .enablePositionalOptions()
47
+ .exitOverride()
48
+ .configureOutput(output);
49
+ commandRegistry.forEach((register) => register(program));
50
+ addGlobalOptions(program);
51
+ return program;
52
+ }
53
+ //# sourceMappingURL=program.js.map
@@ -65,6 +65,8 @@ export interface OtherActivity {
65
65
  dialogs: number;
66
66
  /** A window, tab or download was opened */
67
67
  opened: boolean;
68
+ /** Console messages logged during the action */
69
+ consoleMessages: number;
68
70
  }
69
71
  /**
70
72
  * Messages that are new after the action: all of them after a new document
@@ -147,7 +149,7 @@ export declare function pageNavigation(startHref: string | undefined, read: Read
147
149
  * Whether an action had no visible effect: the page was read before and
148
150
  * after in the same document, it counted no DOM change, nothing made the
149
151
  * check uncertain, and no navigation, message, shown element, request,
150
- * dialog or new window happened.
152
+ * dialog, new window or console message happened.
151
153
  *
152
154
  * @param read - Page read after the action
153
155
  * @param effects - Navigation and messages found
@@ -159,6 +161,8 @@ export declare function hadNoEffect(read: ReadSnapshot | undefined, effects: Act
159
161
  export interface CollectOptions {
160
162
  /** Dialogs the action opened */
161
163
  dialogs: number;
164
+ /** Console messages logged since the action started, so far */
165
+ consoleMessages: () => number;
162
166
  /** Decide "no effect" (with a second look when nothing seemed to happen) */
163
167
  detectNoEffect: boolean;
164
168
  /** List the elements the action showed */
@@ -208,7 +208,7 @@ export function pageNavigation(startHref, read, events) {
208
208
  * Whether an action had no visible effect: the page was read before and
209
209
  * after in the same document, it counted no DOM change, nothing made the
210
210
  * check uncertain, and no navigation, message, shown element, request,
211
- * dialog or new window happened.
211
+ * dialog, new window or console message happened.
212
212
  *
213
213
  * @param read - Page read after the action
214
214
  * @param effects - Navigation and messages found
@@ -225,7 +225,8 @@ export function hadNoEffect(read, effects, activity) {
225
225
  (effects.shown ?? []).length === 0 &&
226
226
  activity.requests === 0 &&
227
227
  activity.dialogs === 0 &&
228
- !activity.opened);
228
+ !activity.opened &&
229
+ activity.consoleMessages === 0);
229
230
  }
230
231
  /**
231
232
  * Start watching an action's effects: listen for main-frame navigations,
@@ -265,7 +266,11 @@ async function collectEffects(watch, options) {
265
266
  const reportShown = options.reportShown === true;
266
267
  let snapshot = await readPage(watch, { stop: false, reportShown });
267
268
  let effects = effectsOf(start, snapshot, watch.listener.events);
268
- const quiet = () => hadNoEffect(snapshot, effects, { ...watch.listener.activity(), dialogs: options.dialogs });
269
+ const quiet = () => hadNoEffect(snapshot, effects, {
270
+ ...watch.listener.activity(),
271
+ dialogs: options.dialogs,
272
+ consoleMessages: options.consoleMessages(),
273
+ });
269
274
  if (options.detectNoEffect && quiet()) {
270
275
  await delay(NO_EFFECT_RECHECK_MS);
271
276
  snapshot = await readPage(watch, { stop: true, reportShown });
@@ -325,7 +330,8 @@ function pageWork(watch, snapshot, domChanging = false) {
325
330
  }
326
331
  /**
327
332
  * Navigation, new messages and shown elements from the snapshots and CDP
328
- * events.
333
+ * events. Snapshots a page broke (it replaced the built-ins the snapshot
334
+ * script uses) give the navigation only.
329
335
  *
330
336
  * @param start - Snapshot before the action, if taken
331
337
  * @param snapshot - Read after the action, if taken
@@ -334,16 +340,22 @@ function pageWork(watch, snapshot, domChanging = false) {
334
340
  */
335
341
  function effectsOf(start, snapshot, events) {
336
342
  const navigation = pageNavigation(start?.href, snapshot, events);
337
- const { messages, more } = start && snapshot
338
- ? newMessages(start.messages, snapshot.messages, snapshot.fresh)
339
- : { messages: [], more: 0 };
340
- const shown = shownElements(snapshot?.shown ?? [], messages);
341
- return {
342
- ...(navigation && { navigation }),
343
- ...(messages.length > 0 && { messages }),
344
- ...(more > 0 && { moreMessages: more }),
345
- ...(shown.length > 0 && { shown }),
346
- };
343
+ try {
344
+ const { messages, more } = start && snapshot
345
+ ? newMessages(start.messages, snapshot.messages, snapshot.fresh)
346
+ : { messages: [], more: 0 };
347
+ const shown = shownElements(snapshot?.shown ?? [], messages);
348
+ return {
349
+ ...(navigation && { navigation }),
350
+ ...(messages.length > 0 && { messages }),
351
+ ...(more > 0 && { moreMessages: more }),
352
+ ...(shown.length > 0 && { shown }),
353
+ };
354
+ }
355
+ catch (error) {
356
+ log.debug(`Page effects not readable (the page replaced built-ins?): ${getErrorMessage(error)}`);
357
+ return { ...(navigation && { navigation }) };
358
+ }
347
359
  }
348
360
  /**
349
361
  * Read the page after the action, unless a main-frame load is pending (the
@@ -6,6 +6,7 @@ import { CommandError } from '../../errors/index.js';
6
6
  import { operationFailedError } from '../../errors/messages.js';
7
7
  import { buildAudit } from './auditModel.js';
8
8
  import { AUDIT_PAGE_JS } from './auditScripts.js';
9
+ import { evaluateInBdgWorld } from '../page/bdgWorld.js';
9
10
  import { EXIT_CODES } from '../../utils/exitCodes.js';
10
11
  /** Findings listed per check without `--limit` */
11
12
  export const DEFAULT_AUDIT_LIMIT = 20;
@@ -18,10 +19,10 @@ export const DEFAULT_AUDIT_LIMIT = 20;
18
19
  * @throws CommandError (91) when the page script fails
19
20
  */
20
21
  export async function auditPage(cdp, params) {
21
- const response = (await cdp.send('Runtime.evaluate', {
22
+ const response = await evaluateInBdgWorld(cdp, {
22
23
  expression: `(${AUDIT_PAGE_JS})(${JSON.stringify(params.checks)})`,
23
24
  returnByValue: true,
24
- }));
25
+ });
25
26
  const raw = response.result.value;
26
27
  if (response.exceptionDetails || !raw) {
27
28
  const err = operationFailedError('audit the page', response.exceptionDetails?.exception?.description ?? 'the page script failed');
@@ -48,6 +48,7 @@ export function buildAudit(raw, options) {
48
48
  ...(animation.scrollDriven && { scrollDriven: true }),
49
49
  }))).slice(0, options.limit),
50
50
  }),
51
+ ...(raw.canvases && { canvases: raw.canvases }),
51
52
  };
52
53
  }
53
54
  /**
@@ -59,15 +60,18 @@ export function buildAudit(raw, options) {
59
60
  */
60
61
  function contrastFindings(raw, options) {
61
62
  const checked = (raw.texts ?? []).map((text) => contrastItem(text, raw.canvasDark));
62
- const failing = checked
63
+ const below = checked
63
64
  .filter((item) => item !== undefined)
64
65
  .filter((item) => item.ratio < requiredRatio(item, options.level))
65
66
  .sort((a, b) => a.ratio - b.ratio);
67
+ const failing = below.filter((item) => !item.approximate);
68
+ const uncertain = below.length - failing.length;
66
69
  return {
67
70
  level: options.level,
68
71
  checked: checked.length,
69
72
  failing: failing.length,
70
73
  items: failing.slice(0, options.limit),
74
+ ...(uncertain > 0 && { uncertain }),
71
75
  };
72
76
  }
73
77
  /**
@@ -95,6 +99,7 @@ function contrastItem(text, canvasDark) {
95
99
  size: pxNumber(text.fontSize) ?? 16,
96
100
  weight: Number(text.fontWeight) || 400,
97
101
  inView: text.inView,
102
+ ...(contrast.opacity !== undefined && { opacity: contrast.opacity }),
98
103
  ...(contrast.approximate && { approximate: contrast.approximate }),
99
104
  };
100
105
  }
@@ -6,7 +6,7 @@
6
6
  * that collects what each check needs. Colors and contrast are computed in
7
7
  * the daemon ({@link buildAudit}), the same way `dom inspect` computes them.
8
8
  */
9
- import type { RawBackground } from './inspectScripts.js';
9
+ import { type RawBackground } from './inspectScripts.js';
10
10
  /** Elements the walk looks at, at most */
11
11
  export declare const AUDIT_ELEMENT_CAP = 20000;
12
12
  /** A text holder: an element that draws text of its own */
@@ -19,7 +19,11 @@ export interface RawAuditText {
19
19
  /** Backgrounds from the element up to the root, each with its own opacity */
20
20
  backgrounds: RawBackground[];
21
21
  opacity: number;
22
- /** Blend modes and filters on it or an ancestor */
22
+ /**
23
+ * Why its contrast is approximate: blend modes and filters on it or an
24
+ * ancestor, an element painted behind or on top of it (in view), or that
25
+ * only its ancestors were checked (out of view, none of them paints)
26
+ */
23
27
  risks: string[];
24
28
  /** Inside the viewport */
25
29
  inView: boolean;
@@ -98,10 +102,12 @@ export interface RawAudit {
98
102
  }>;
99
103
  layers?: RawLayer[];
100
104
  animations?: RawAnimation[];
105
+ /** Visible `<canvas>` elements (their animations are drawn by scripts, not CSS or Web Animations) */
106
+ canvases?: number;
101
107
  }
102
108
  /**
103
109
  * Page-side audit walk. Arguments: the checks to collect for
104
110
  * (`contrast`, `overflow`, `layers`, `animations`).
105
111
  */
106
- export declare const AUDIT_PAGE_JS = "function (checks) {\n const want = (name) => checks.includes(name);\n const doc = document;\n const view = window;\n const style = (n) => view.getComputedStyle(n);\n const scroller = doc.scrollingElement || doc.documentElement;\n const viewport = { width: doc.documentElement.clientWidth, height: view.innerHeight };\n const label = (n) => n.localName + (n.id ? '#' + n.id : n.classList && n.classList.length ? '.' + n.classList[0] : '');\n const parentOf = (n) => n.assignedSlot || n.parentElement || (n.parentNode && n.parentNode.host) || null;\n const short = (text) => text.replace(/\\s+/g, ' ').trim().slice(0, 60);\n const ownText = (n) => Array.from(n.childNodes).filter((c) => c.nodeType === 3).map((c) => c.data).join(' ').replace(/\\s+/g, ' ').trim();\n const inView = (r) => r.bottom > 0 && r.right > 0 && r.top < viewport.height && r.left < viewport.width;\n const meta = doc.querySelector('meta[name=\"color-scheme\"]');\n const rootScheme = style(doc.documentElement).colorScheme;\n const scheme = rootScheme && rootScheme !== 'normal' ? rootScheme : (meta && meta.content) || '';\n const canvasDark = /dark/.test(scheme) && (!/light/.test(scheme) || view.matchMedia('(prefers-color-scheme: dark)').matches);\n const result = { viewport: viewport, pixelRatio: view.devicePixelRatio || 1, pageWidth: scroller.scrollWidth, canvasDark: canvasDark, walked: 0, capped: false };\n const texts = [], wide = [], truncated = [], images = [], layers = [], scrollers = [];\n const chainOf = (n) => {\n const backgrounds = [];\n const risks = [];\n let opacity = 1;\n for (let p = n; p && backgrounds.length < 60; p = parentOf(p)) {\n const s = style(p);\n const own = Number(s.opacity) || 0;\n const background = { color: s.backgroundColor, image: s.backgroundImage !== 'none' };\n backgrounds.push(own < 1 ? Object.assign(background, { opacity: own }) : background);\n opacity *= own;\n if (s.mixBlendMode !== 'normal') risks.push('mix-blend-mode ' + s.mixBlendMode + ' on ' + label(p));\n if (s.filter !== 'none') risks.push('filter on ' + label(p));\n }\n return { backgrounds: backgrounds, risks: risks, opacity: opacity };\n };\n const visit = (n, fixed) => {\n if (result.walked >= 20000) { result.capped = true; return; }\n if (/^(script|style|template|noscript|head|meta|link|title)$/.test(n.localName)) return;\n const s = style(n);\n if (s.display === 'none') return;\n result.walked++;\n const inFixed = fixed || s.position === 'fixed';\n const kids = n.shadowRoot ? [...Array.from(n.shadowRoot.children), ...Array.from(n.children)] : Array.from(n.children);\n const slot = n.localName === 'slot';\n const box = slot && parentOf(n) ? parentOf(n) : n;\n const r = box.getBoundingClientRect();\n const shown = r.width > 0 && r.height > 0 && s.visibility === 'visible';\n const gradientText = s.backgroundClip === 'text' && /rgba\\(0, 0, 0, 0\\)|transparent/.test(s.webkitTextFillColor);\n if (shown && want('contrast') && !gradientText) {\n const text = slot ? short(n.assignedNodes({ flatten: true }).filter((c) => c.nodeType === 3).map((c) => c.data).join(' ')) : n.shadowRoot ? '' : ownText(n);\n if (text && Number(s.opacity) > 0 && r.width > 2 && r.height > 2) {\n const chain = chainOf(n);\n if (chain.opacity > 0) texts.push(Object.assign({ label: label(n), text: short(text), color: s.color, fontSize: s.fontSize, fontWeight: s.fontWeight, inView: inView(r) }, chain));\n }\n }\n if (shown && want('overflow')) {\n if (r.right > viewport.width + 1 && !inFixed) wide.push({ label: label(n), right: r.right + view.scrollX, width: r.width });\n const cut = s.overflowX !== 'visible' && n.scrollWidth > n.clientWidth + 1 ? (s.textOverflow === 'ellipsis' ? 'ellipsis' : 'clip')\n : s.webkitLineClamp !== 'none' && n.scrollHeight > n.clientHeight + 1 ? 'clamp' : null;\n const textContent = cut && short(n.innerText || '');\n const visuallyHidden = r.width <= 2 || r.height <= 2;\n if (cut && textContent && !visuallyHidden) truncated.push({ label: label(n), text: textContent, kind: cut });\n const vector = /\\.svg([?#]|$)|^data:image\\/svg/i.test(n.currentSrc || '');\n if (n.localName === 'img' && n.naturalWidth > 2 && n.naturalHeight > 2 && n.complete && !vector) images.push({ label: label(n), natural: { w: n.naturalWidth, h: n.naturalHeight }, rendered: { w: r.width, h: r.height }, objectFit: s.objectFit });\n }\n if (shown && want('overflow') && /^(auto|scroll)$/.test(s.overflowX) && n !== doc.documentElement && n !== doc.body && n.scrollWidth > n.clientWidth + 1) {\n scrollers.push({ label: label(n), scrollWidth: n.scrollWidth, width: n.clientWidth });\n }\n if (want('layers') && shown && (s.position === 'fixed' || s.position === 'sticky')) {\n layers.push({ label: label(n), position: s.position, zIndex: s.zIndex, rect: { x: r.left, y: r.top, w: r.width, h: r.height }, inView: inView(r) });\n }\n if (s.overflowX !== 'visible' && s.overflowX !== 'clip' && n !== doc.documentElement && n !== doc.body) {\n const wideBefore = wide.length;\n for (const c of kids) visit(c, inFixed);\n wide.length = wideBefore;\n return;\n }\n for (const c of kids) visit(c, inFixed);\n };\n visit(doc.documentElement, false);\n if (want('contrast')) result.texts = texts;\n if (want('overflow')) Object.assign(result, { wide: wide, truncated: truncated, images: images, scrollers: scrollers });\n if (want('layers')) result.layers = layers;\n if (want('animations')) {\n result.animations = doc.getAnimations().filter((a) => a.playState === 'running').slice(0, 200).map((a) => {\n const timing = a.effect && a.effect.getTiming ? a.effect.getTiming() : {};\n const target = a.effect && a.effect.target;\n return {\n label: target && target.localName ? label(target) : '(none)',\n name: a.animationName || a.transitionProperty || a.id || 'animation',\n type: a.constructor.name,\n duration: typeof timing.duration === 'number' ? timing.duration : String(timing.duration),\n iterations: timing.iterations === Infinity ? 'infinite' : timing.iterations,\n scrollDriven: Boolean(a.timeline) && a.timeline !== doc.timeline\n };\n });\n }\n return result;\n}";
112
+ export declare const AUDIT_PAGE_JS: string;
107
113
  //# sourceMappingURL=auditScripts.d.ts.map
@@ -6,6 +6,9 @@
6
6
  * that collects what each check needs. Colors and contrast are computed in
7
7
  * the daemon ({@link buildAudit}), the same way `dom inspect` computes them.
8
8
  */
9
+ import { LABEL_CLASSES_JS } from './elementInfo.js';
10
+ import { FLAT_TREE_JS, HIT_EVERYTHING_JS, PAINT_RISKS_JS, } from './inspectScripts.js';
11
+ import { AUDIT_OUT_OF_VIEW_RISK } from '../../ui/messages/commands.js';
9
12
  /** Elements the walk looks at, at most */
10
13
  export const AUDIT_ELEMENT_CAP = 20000;
11
14
  /**
@@ -19,7 +22,8 @@ export const AUDIT_PAGE_JS = `function (checks) {
19
22
  const style = (n) => view.getComputedStyle(n);
20
23
  const scroller = doc.scrollingElement || doc.documentElement;
21
24
  const viewport = { width: doc.documentElement.clientWidth, height: view.innerHeight };
22
- const label = (n) => n.localName + (n.id ? '#' + n.id : n.classList && n.classList.length ? '.' + n.classList[0] : '');
25
+ const firstClass = (n) => (${LABEL_CLASSES_JS})(n)[0];
26
+ const label = (n) => n.localName + (n.id ? '#' + n.id : firstClass(n) ? '.' + firstClass(n) : '');
23
27
  const parentOf = (n) => n.assignedSlot || n.parentElement || (n.parentNode && n.parentNode.host) || null;
24
28
  const short = (text) => text.replace(/\\s+/g, ' ').trim().slice(0, 60);
25
29
  const ownText = (n) => Array.from(n.childNodes).filter((c) => c.nodeType === 3).map((c) => c.data).join(' ').replace(/\\s+/g, ' ').trim();
@@ -30,20 +34,42 @@ export const AUDIT_PAGE_JS = `function (checks) {
30
34
  const canvasDark = /dark/.test(scheme) && (!/light/.test(scheme) || view.matchMedia('(prefers-color-scheme: dark)').matches);
31
35
  const result = { viewport: viewport, pixelRatio: view.devicePixelRatio || 1, pageWidth: scroller.scrollWidth, canvasDark: canvasDark, walked: 0, capped: false };
32
36
  const texts = [], wide = [], truncated = [], images = [], layers = [], scrollers = [];
37
+ let canvases = 0;
38
+ const tree = (${FLAT_TREE_JS})(view);
39
+ const paintRisks = ${PAINT_RISKS_JS};
40
+ const clear = (color) => color === 'transparent' || /^rgba\\(.*,\\s*0\\)$/.test(color) || /\\/\\s*0\\)$/.test(color);
33
41
  const chainOf = (n) => {
34
42
  const backgrounds = [];
35
43
  const risks = [];
36
44
  let opacity = 1;
45
+ let painted = false;
46
+ let solid = false;
37
47
  for (let p = n; p && backgrounds.length < 60; p = parentOf(p)) {
38
48
  const s = style(p);
39
49
  const own = Number(s.opacity) || 0;
40
50
  const background = { color: s.backgroundColor, image: s.backgroundImage !== 'none' };
41
51
  backgrounds.push(own < 1 ? Object.assign(background, { opacity: own }) : background);
42
52
  opacity *= own;
53
+ if (p !== doc.documentElement && p !== doc.body && (background.image || !clear(s.backgroundColor))) painted = true;
54
+ if (p !== doc.documentElement && p !== doc.body && !background.image && /^rgb\\(/.test(s.backgroundColor)) solid = true;
43
55
  if (s.mixBlendMode !== 'normal') risks.push('mix-blend-mode ' + s.mixBlendMode + ' on ' + label(p));
44
56
  if (s.filter !== 'none') risks.push('filter on ' + label(p));
45
57
  }
46
- return { backgrounds: backgrounds, risks: risks, opacity: opacity };
58
+ return { backgrounds: backgrounds, risks: risks, opacity: opacity, painted: painted, solid: solid };
59
+ };
60
+ const unhit = [];
61
+ const painters = [];
62
+ const markPaintedBehind = () => {
63
+ for (const text of unhit) {
64
+ const painter = painters.find((p) => p.el !== text.el && !p.el.contains(text.el) && !text.el.contains(p.el) &&
65
+ text.x >= p.left && text.x <= p.right && text.y >= p.top && text.y <= p.bottom);
66
+ if (painter && !text.entry.risks.includes(${JSON.stringify(AUDIT_OUT_OF_VIEW_RISK)})) text.entry.risks.push(label(painter.el) + ' under or over it (out of view, not hit-tested)');
67
+ }
68
+ };
69
+ const risksOf = (n, slot, chain, shown) => {
70
+ if (!shown) return chain.painted ? chain.risks : chain.risks.concat(${JSON.stringify(AUDIT_OUT_OF_VIEW_RISK)});
71
+ const text = slot && n.assignedNodes({ flatten: true }).find((c) => c.nodeType === 3 && c.data.trim() !== '');
72
+ return paintRisks(n, tree, text ? text.parentElement || n : n);
47
73
  };
48
74
  const visit = (n, fixed) => {
49
75
  if (result.walked >= ${AUDIT_ELEMENT_CAP}) { result.capped = true; return; }
@@ -57,14 +83,21 @@ export const AUDIT_PAGE_JS = `function (checks) {
57
83
  const box = slot && parentOf(n) ? parentOf(n) : n;
58
84
  const r = box.getBoundingClientRect();
59
85
  const shown = r.width > 0 && r.height > 0 && s.visibility === 'visible';
60
- const gradientText = s.backgroundClip === 'text' && /rgba\\(0, 0, 0, 0\\)|transparent/.test(s.webkitTextFillColor);
86
+ const gradientText = /rgba\\(0, 0, 0, 0\\)|transparent/.test(s.webkitTextFillColor);
61
87
  if (shown && want('contrast') && !gradientText) {
62
88
  const text = slot ? short(n.assignedNodes({ flatten: true }).filter((c) => c.nodeType === 3).map((c) => c.data).join(' ')) : n.shadowRoot ? '' : ownText(n);
63
89
  if (text && Number(s.opacity) > 0 && r.width > 2 && r.height > 2) {
64
90
  const chain = chainOf(n);
65
- if (chain.opacity > 0) texts.push(Object.assign({ label: label(n), text: short(text), color: s.color, fontSize: s.fontSize, fontWeight: s.fontWeight, inView: inView(r) }, chain));
91
+ const seen = inView(r);
92
+ if (chain.opacity > 0) {
93
+ texts.push({ label: label(n), text: short(text), color: s.color, fontSize: s.fontSize, fontWeight: s.fontWeight, inView: seen, backgrounds: chain.backgrounds, opacity: chain.opacity, risks: risksOf(n, slot, chain, seen) });
94
+ if (!seen && !chain.solid) unhit.push({ entry: texts[texts.length - 1], el: box, x: (r.left + r.right) / 2 + view.scrollX, y: (r.top + r.bottom) / 2 + view.scrollY });
95
+ }
66
96
  }
67
97
  }
98
+ if (shown && want('contrast') && (/^(img|video|canvas|picture|iframe)$/.test(n.localName) || s.backgroundImage !== 'none')) {
99
+ painters.push({ el: n, left: r.left + view.scrollX, top: r.top + view.scrollY, right: r.right + view.scrollX, bottom: r.bottom + view.scrollY });
100
+ }
68
101
  if (shown && want('overflow')) {
69
102
  if (r.right > viewport.width + 1 && !inFixed) wide.push({ label: label(n), right: r.right + view.scrollX, width: r.width });
70
103
  const cut = s.overflowX !== 'visible' && n.scrollWidth > n.clientWidth + 1 ? (s.textOverflow === 'ellipsis' ? 'ellipsis' : 'clip')
@@ -78,6 +111,7 @@ export const AUDIT_PAGE_JS = `function (checks) {
78
111
  if (shown && want('overflow') && /^(auto|scroll)$/.test(s.overflowX) && n !== doc.documentElement && n !== doc.body && n.scrollWidth > n.clientWidth + 1) {
79
112
  scrollers.push({ label: label(n), scrollWidth: n.scrollWidth, width: n.clientWidth });
80
113
  }
114
+ if (want('animations') && shown && n.localName === 'canvas') canvases++;
81
115
  if (want('layers') && shown && (s.position === 'fixed' || s.position === 'sticky')) {
82
116
  layers.push({ label: label(n), position: s.position, zIndex: s.zIndex, rect: { x: r.left, y: r.top, w: r.width, h: r.height }, inView: inView(r) });
83
117
  }
@@ -89,11 +123,13 @@ export const AUDIT_PAGE_JS = `function (checks) {
89
123
  }
90
124
  for (const c of kids) visit(c, inFixed);
91
125
  };
92
- visit(doc.documentElement, false);
126
+ (${HIT_EVERYTHING_JS})([doc], () => visit(doc.documentElement, false));
127
+ markPaintedBehind();
93
128
  if (want('contrast')) result.texts = texts;
94
129
  if (want('overflow')) Object.assign(result, { wide: wide, truncated: truncated, images: images, scrollers: scrollers });
95
130
  if (want('layers')) result.layers = layers;
96
131
  if (want('animations')) {
132
+ result.canvases = canvases;
97
133
  result.animations = doc.getAnimations().filter((a) => a.playState === 'running').slice(0, 200).map((a) => {
98
134
  const timing = a.effect && a.effect.getTiming ? a.effect.getTiming() : {};
99
135
  const target = a.effect && a.effect.target;