browser-debugger-cli 0.11.0 → 0.13.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 (222) hide show
  1. package/.claude/skills/bdg/SKILL.md +4 -4
  2. package/README.md +143 -79
  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 +62 -12
  7. package/dist/commands/css.d.ts +13 -0
  8. package/dist/commands/css.js +53 -0
  9. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  10. package/dist/commands/dom/DomElementResolver.js +10 -3
  11. package/dist/commands/dom/a11y.js +3 -2
  12. package/dist/commands/dom/audit.d.ts +14 -0
  13. package/dist/commands/dom/audit.js +87 -0
  14. package/dist/commands/dom/eval.d.ts +3 -2
  15. package/dist/commands/dom/eval.js +11 -5
  16. package/dist/commands/dom/form.js +10 -9
  17. package/dist/commands/dom/formInteraction.js +42 -11
  18. package/dist/commands/dom/get.js +8 -8
  19. package/dist/commands/dom/helpers/index.d.ts +1 -1
  20. package/dist/commands/dom/helpers/index.js +1 -1
  21. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  22. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  23. package/dist/commands/dom/helpers/query.d.ts +27 -3
  24. package/dist/commands/dom/helpers/query.js +152 -64
  25. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  26. package/dist/commands/dom/helpers/screenshot.js +169 -49
  27. package/dist/commands/dom/index.js +7 -2
  28. package/dist/commands/dom/query.d.ts +19 -2
  29. package/dist/commands/dom/query.js +37 -6
  30. package/dist/commands/dom/screenshot.js +12 -7
  31. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  32. package/dist/commands/dom/semanticUtils.js +40 -9
  33. package/dist/commands/dom/wait.js +5 -3
  34. package/dist/commands/helpJson.d.ts +82 -19
  35. package/dist/commands/helpJson.js +112 -41
  36. package/dist/commands/helpTopic.d.ts +16 -1
  37. package/dist/commands/helpTopic.js +59 -1
  38. package/dist/commands/installSkill.d.ts +15 -5
  39. package/dist/commands/installSkill.js +86 -16
  40. package/dist/commands/network/list.js +22 -12
  41. package/dist/commands/optionBehaviors.js +53 -16
  42. package/dist/commands/page.js +7 -4
  43. package/dist/commands/peek.d.ts +7 -0
  44. package/dist/commands/peek.js +65 -23
  45. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  46. package/dist/commands/shared/daemonErrorHandler.js +20 -9
  47. package/dist/commands/shared/dataFetcher.d.ts +12 -4
  48. package/dist/commands/shared/dataFetcher.js +12 -4
  49. package/dist/commands/shared/followMode.d.ts +9 -1
  50. package/dist/commands/shared/followMode.js +22 -4
  51. package/dist/commands/shared/optionTypes.d.ts +9 -2
  52. package/dist/commands/shared/outputFile.js +6 -1
  53. package/dist/commands/start.d.ts +20 -5
  54. package/dist/commands/start.js +84 -23
  55. package/dist/commands/stop.d.ts +11 -0
  56. package/dist/commands/stop.js +24 -1
  57. package/dist/commands/tail.d.ts +7 -1
  58. package/dist/commands/tail.js +13 -62
  59. package/dist/commands.js +2 -0
  60. package/dist/connection/cdp.d.ts +7 -0
  61. package/dist/connection/cdp.js +9 -0
  62. package/dist/connection/launcher.js +3 -2
  63. package/dist/daemon/SessionController.js +6 -1
  64. package/dist/daemon/launcher.d.ts +3 -2
  65. package/dist/daemon/launcher.js +47 -3
  66. package/dist/daemon/session/Session.d.ts +4 -1
  67. package/dist/daemon/session/Session.js +33 -2
  68. package/dist/daemon/session/TelemetryStore.d.ts +8 -1
  69. package/dist/daemon/session/TelemetryStore.js +13 -1
  70. package/dist/daemon/session/commandRegistry.js +36 -14
  71. package/dist/daemon/session/interactions.d.ts +2 -1
  72. package/dist/daemon/session/interactions.js +13 -1
  73. package/dist/daemon/session/plugins.js +19 -53
  74. package/dist/daemon/session/teardown.js +1 -1
  75. package/dist/daemon.js +9234 -7222
  76. package/dist/errors/messages.d.ts +88 -15
  77. package/dist/errors/messages.js +177 -27
  78. package/dist/index.js +19322 -13961
  79. package/dist/ipc/client.d.ts +22 -2
  80. package/dist/ipc/client.js +34 -5
  81. package/dist/ipc/protocol/auditTypes.d.ts +135 -0
  82. package/dist/ipc/protocol/auditTypes.js +6 -0
  83. package/dist/ipc/protocol/commands.d.ts +35 -0
  84. package/dist/ipc/protocol/commands.js +2 -0
  85. package/dist/ipc/protocol/domTypes.d.ts +16 -0
  86. package/dist/ipc/protocol/inspectTypes.d.ts +73 -8
  87. package/dist/ipc/session/types.d.ts +2 -0
  88. package/dist/runtime/css/search.d.ts +39 -0
  89. package/dist/runtime/css/search.js +122 -0
  90. package/dist/runtime/dom/actionEffects.d.ts +9 -2
  91. package/dist/runtime/dom/actionEffects.js +30 -14
  92. package/dist/runtime/dom/audit.d.ts +19 -0
  93. package/dist/runtime/dom/audit.js +37 -0
  94. package/dist/runtime/dom/auditModel.d.ts +45 -0
  95. package/dist/runtime/dom/auditModel.js +220 -0
  96. package/dist/runtime/dom/auditScripts.d.ts +113 -0
  97. package/dist/runtime/dom/auditScripts.js +148 -0
  98. package/dist/runtime/dom/elementGeometry.d.ts +16 -3
  99. package/dist/runtime/dom/elementGeometry.js +49 -10
  100. package/dist/runtime/dom/elementInfo.d.ts +74 -17
  101. package/dist/runtime/dom/elementInfo.js +187 -34
  102. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  103. package/dist/runtime/dom/evalHelpers.js +67 -7
  104. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  105. package/dist/runtime/dom/formDiscovery.js +20 -3
  106. package/dist/runtime/dom/formFillHelpers/fill.js +8 -12
  107. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  108. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  109. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  110. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  111. package/dist/runtime/dom/frameLayout.js +1 -0
  112. package/dist/runtime/dom/inspect.d.ts +7 -0
  113. package/dist/runtime/dom/inspect.js +92 -28
  114. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  115. package/dist/runtime/dom/inspectAllStyles.js +90 -7
  116. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  117. package/dist/runtime/dom/inspectCascade.js +214 -44
  118. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  119. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  120. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  121. package/dist/runtime/dom/inspectHints.js +125 -9
  122. package/dist/runtime/dom/inspectModel.d.ts +5 -1
  123. package/dist/runtime/dom/inspectModel.js +37 -10
  124. package/dist/runtime/dom/inspectPaintModel.d.ts +50 -22
  125. package/dist/runtime/dom/inspectPaintModel.js +182 -68
  126. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  127. package/dist/runtime/dom/inspectRules.js +21 -5
  128. package/dist/runtime/dom/inspectScripts.d.ts +112 -12
  129. package/dist/runtime/dom/inspectScripts.js +357 -32
  130. package/dist/runtime/dom/inspectTree.js +10 -2
  131. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  132. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  133. package/dist/runtime/dom/layout.js +40 -16
  134. package/dist/runtime/dom/reactEventHelpers.d.ts +21 -4
  135. package/dist/runtime/dom/reactEventHelpers.js +90 -36
  136. package/dist/runtime/dom/targetNode.d.ts +18 -5
  137. package/dist/runtime/dom/targetNode.js +268 -8
  138. package/dist/runtime/dom/wait.js +2 -1
  139. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  140. package/dist/runtime/page/bdgWorld.js +180 -0
  141. package/dist/runtime/page/emulation.d.ts +13 -4
  142. package/dist/runtime/page/emulation.js +69 -4
  143. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  144. package/dist/runtime/page/replacedBuiltins.js +136 -0
  145. package/dist/runtime/page/userAgent.d.ts +17 -0
  146. package/dist/runtime/page/userAgent.js +57 -0
  147. package/dist/session/QueryCacheManager.d.ts +4 -1
  148. package/dist/session/QueryCacheManager.js +5 -2
  149. package/dist/session/chrome.d.ts +4 -1
  150. package/dist/session/chrome.js +7 -1
  151. package/dist/session/cleanup/staleSession.d.ts +21 -4
  152. package/dist/session/cleanup/staleSession.js +79 -9
  153. package/dist/session/cleanup/userCommands.d.ts +4 -1
  154. package/dist/session/cleanup/userCommands.js +10 -5
  155. package/dist/session/daemonSocket.d.ts +10 -0
  156. package/dist/session/daemonSocket.js +22 -0
  157. package/dist/session/lastSession.d.ts +6 -3
  158. package/dist/session/lastSession.js +11 -5
  159. package/dist/session/paths.d.ts +3 -1
  160. package/dist/session/paths.js +5 -5
  161. package/dist/session/portClaims.js +4 -3
  162. package/dist/session/sessionList.d.ts +13 -5
  163. package/dist/session/sessionList.js +31 -7
  164. package/dist/telemetry/a11y.js +2 -2
  165. package/dist/telemetry/console.d.ts +2 -1
  166. package/dist/telemetry/console.js +30 -21
  167. package/dist/telemetry/pageCrash.d.ts +26 -0
  168. package/dist/telemetry/pageCrash.js +53 -0
  169. package/dist/types.d.ts +20 -0
  170. package/dist/ui/formatters/audit.d.ts +19 -0
  171. package/dist/ui/formatters/audit.js +115 -0
  172. package/dist/ui/formatters/cdp.d.ts +138 -0
  173. package/dist/ui/formatters/cdp.js +131 -0
  174. package/dist/ui/formatters/console/chronological.js +3 -1
  175. package/dist/ui/formatters/console/follow.d.ts +2 -1
  176. package/dist/ui/formatters/console/follow.js +2 -2
  177. package/dist/ui/formatters/console/json.d.ts +2 -2
  178. package/dist/ui/formatters/console/json.js +11 -5
  179. package/dist/ui/formatters/console/shared.d.ts +30 -0
  180. package/dist/ui/formatters/console/shared.js +16 -0
  181. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  182. package/dist/ui/formatters/console/summarize.js +40 -9
  183. package/dist/ui/formatters/console.d.ts +2 -1
  184. package/dist/ui/formatters/console.js +7 -5
  185. package/dist/ui/formatters/details.js +3 -1
  186. package/dist/ui/formatters/dom.d.ts +2 -2
  187. package/dist/ui/formatters/dom.js +10 -8
  188. package/dist/ui/formatters/helpFormatters.js +1 -1
  189. package/dist/ui/formatters/inspect.js +50 -17
  190. package/dist/ui/formatters/installSkill.d.ts +9 -1
  191. package/dist/ui/formatters/installSkill.js +32 -6
  192. package/dist/ui/formatters/layout.js +2 -1
  193. package/dist/ui/formatters/networkList.d.ts +1 -1
  194. package/dist/ui/formatters/networkList.js +1 -2
  195. package/dist/ui/formatters/preview.d.ts +2 -0
  196. package/dist/ui/formatters/preview.js +17 -7
  197. package/dist/ui/formatters/sessions.d.ts +2 -2
  198. package/dist/ui/formatters/sessions.js +9 -2
  199. package/dist/ui/formatters/status.js +1 -1
  200. package/dist/ui/logging/logger.d.ts +1 -1
  201. package/dist/ui/messages/commands.d.ts +168 -11
  202. package/dist/ui/messages/commands.js +245 -18
  203. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  204. package/dist/ui/messages/consoleMessages.js +32 -0
  205. package/dist/ui/messages/preview.d.ts +12 -0
  206. package/dist/ui/messages/preview.js +18 -2
  207. package/dist/ui/messages/session.d.ts +13 -2
  208. package/dist/ui/messages/session.js +22 -3
  209. package/dist/utils/cssValues.js +36 -4
  210. package/dist/utils/decisionTrees.js +0 -5
  211. package/dist/utils/directories.d.ts +34 -0
  212. package/dist/utils/directories.js +88 -0
  213. package/dist/utils/display.d.ts +16 -0
  214. package/dist/utils/display.js +42 -0
  215. package/dist/utils/exitCodes.d.ts +1 -0
  216. package/dist/utils/exitCodes.js +6 -0
  217. package/dist/utils/process.d.ts +12 -0
  218. package/dist/utils/process.js +25 -0
  219. package/dist/utils/suggestions.d.ts +4 -2
  220. package/dist/utils/suggestions.js +7 -5
  221. package/dist/utils/taskMappings.js +1 -1
  222. package/package.json +3 -2
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `bdg css search <text>`: find text in the page's stylesheets, cross-origin
3
+ * ones included (CDP reads every stylesheet's text), and show the rule
4
+ * around each match with its `file:line`.
5
+ */
6
+ import { enableStyleDomains } from '../dom/inspect.js';
7
+ import { stylesheetPositionLabel, styleSheetHeaders } from '../dom/inspectRules.js';
8
+ import { createLogger } from '../../ui/logging/index.js';
9
+ import { getErrorMessage } from '../../utils/errors.js';
10
+ const log = createLogger('dom');
11
+ /** Matches listed without `--limit` */
12
+ export const DEFAULT_CSS_SEARCH_LIMIT = 20;
13
+ /** Characters of a rule shown around a match */
14
+ const RULE_CONTEXT = 240;
15
+ /**
16
+ * Search the stylesheets for a text (case-insensitive).
17
+ *
18
+ * @param cdp - CDP connection
19
+ * @param params - Text and limit
20
+ * @returns Matches with their place
21
+ */
22
+ export async function searchStyleSheets(cdp, params) {
23
+ await enableStyleDomains(cdp);
24
+ const headers = [...styleSheetHeaders(cdp)].filter((header) => header.origin !== 'user-agent');
25
+ const limit = params.limit ?? DEFAULT_CSS_SEARCH_LIMIT;
26
+ const texts = await Promise.all(headers.map((header) => sheetText(cdp, header.styleSheetId)));
27
+ const matches = [];
28
+ let total = 0;
29
+ headers.forEach((header, i) => {
30
+ const found = findInSheet(texts[i] ?? '', params.query, Number.POSITIVE_INFINITY);
31
+ const seen = new Set();
32
+ for (const match of found.matches) {
33
+ if (seen.has(match.rule))
34
+ continue;
35
+ seen.add(match.rule);
36
+ total++;
37
+ if (matches.length >= limit)
38
+ continue;
39
+ matches.push({
40
+ source: stylesheetPositionLabel(header, match.line, match.column),
41
+ text: match.rule,
42
+ });
43
+ }
44
+ });
45
+ return { query: params.query, sheets: headers.length, total, matches };
46
+ }
47
+ /**
48
+ * A stylesheet's text; empty when Chrome cannot give it.
49
+ *
50
+ * @param cdp - CDP connection
51
+ * @param styleSheetId - Stylesheet
52
+ * @returns Text
53
+ */
54
+ async function sheetText(cdp, styleSheetId) {
55
+ try {
56
+ const response = (await cdp.send('CSS.getStyleSheetText', {
57
+ styleSheetId,
58
+ }));
59
+ return response.text;
60
+ }
61
+ catch (error) {
62
+ log.debug(`No text for stylesheet ${styleSheetId}: ${getErrorMessage(error)}`);
63
+ return '';
64
+ }
65
+ }
66
+ /**
67
+ * Where a text occurs in a stylesheet (case-insensitive): how many times,
68
+ * and for the first `limit` matches the 0-based line and column and the
69
+ * rule around it (from the end of the previous rule to the end of this one,
70
+ * whitespace collapsed, at most {@link RULE_CONTEXT} characters each side).
71
+ * Lines are counted as the search moves on, so a big sheet is read once.
72
+ *
73
+ * @param text - Stylesheet text
74
+ * @param query - Text to find
75
+ * @param limit - Matches described at most
76
+ * @returns Match count and the described matches
77
+ */
78
+ export function findInSheet(text, query, limit = Number.POSITIVE_INFINITY) {
79
+ const matches = [];
80
+ const haystack = text.toLowerCase();
81
+ const needle = query.toLowerCase();
82
+ if (needle === '')
83
+ return { total: 0, matches };
84
+ let total = 0;
85
+ let line = 0;
86
+ let lineStart = 0;
87
+ let scanned = 0;
88
+ for (let at = haystack.indexOf(needle); at >= 0; at = haystack.indexOf(needle, at + needle.length)) {
89
+ total++;
90
+ if (matches.length >= limit)
91
+ continue;
92
+ for (let i = text.indexOf('\n', scanned); i >= 0 && i < at; i = text.indexOf('\n', i + 1)) {
93
+ line++;
94
+ lineStart = i + 1;
95
+ }
96
+ scanned = at;
97
+ matches.push({ line, column: at - lineStart, rule: ruleAround(text, at) });
98
+ }
99
+ return { total, matches };
100
+ }
101
+ /**
102
+ * The rule around a position: whole when short, else its selector and the
103
+ * declarations near the position (`.btn { … color: var(--brand); … }`),
104
+ * whitespace collapsed.
105
+ *
106
+ * @param text - Stylesheet text
107
+ * @param at - Position of the match
108
+ * @returns Rule text
109
+ */
110
+ function ruleAround(text, at) {
111
+ const ruleStart = text.lastIndexOf('}', at) + 1;
112
+ const open = text.indexOf('{', ruleStart);
113
+ const close = text.indexOf('}', at);
114
+ const ruleEnd = close < 0 ? text.length : close + 1;
115
+ const collapse = (part) => part.replace(/\s+/g, ' ').trim();
116
+ if (ruleEnd - ruleStart <= 2 * RULE_CONTEXT || open < 0 || open > at) {
117
+ return collapse(text.slice(Math.max(ruleStart, at - RULE_CONTEXT), Math.min(ruleEnd, at + RULE_CONTEXT)));
118
+ }
119
+ const near = collapse(text.slice(Math.max(open + 1, at - RULE_CONTEXT / 2), Math.min(ruleEnd - 1, at + RULE_CONTEXT / 2)));
120
+ return `${collapse(text.slice(ruleStart, open))} { … ${near} … }`;
121
+ }
122
+ //# sourceMappingURL=search.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
@@ -81,7 +83,10 @@ export interface OtherActivity {
81
83
  * @param newDocument - Whether a new document loaded
82
84
  * @returns New messages
83
85
  */
84
- export declare function newMessages(before: SeenMessage[], after: SeenMessage[], newDocument: boolean): NewMessage[];
86
+ export declare function newMessages(before: SeenMessage[], after: SeenMessage[], newDocument: boolean): {
87
+ messages: NewMessage[];
88
+ more: number;
89
+ };
85
90
  /**
86
91
  * Elements to report as shown: those whose text is not already reported as
87
92
  * a new message, at most {@link MAX_SHOWN_ELEMENTS}, texts cut to
@@ -144,7 +149,7 @@ export declare function pageNavigation(startHref: string | undefined, read: Read
144
149
  * Whether an action had no visible effect: the page was read before and
145
150
  * after in the same document, it counted no DOM change, nothing made the
146
151
  * check uncertain, and no navigation, message, shown element, request,
147
- * dialog or new window happened.
152
+ * dialog, new window or console message happened.
148
153
  *
149
154
  * @param read - Page read after the action
150
155
  * @param effects - Navigation and messages found
@@ -156,6 +161,8 @@ export declare function hadNoEffect(read: ReadSnapshot | undefined, effects: Act
156
161
  export interface CollectOptions {
157
162
  /** Dialogs the action opened */
158
163
  dialogs: number;
164
+ /** Console messages logged since the action started, so far */
165
+ consoleMessages: () => number;
159
166
  /** Decide "no effect" (with a second look when nothing seemed to happen) */
160
167
  detectNoEffect: boolean;
161
168
  /** List the elements the action showed */
@@ -81,8 +81,6 @@ export function newMessages(before, after, newDocument) {
81
81
  const reported = new Set();
82
82
  const result = [];
83
83
  for (const message of after) {
84
- if (result.length >= MAX_NEW_MESSAGES)
85
- break;
86
84
  if (reported.has(message.text) || TICKING_TEXT.test(message.text))
87
85
  continue;
88
86
  if (!isNew(message))
@@ -90,7 +88,10 @@ export function newMessages(before, after, newDocument) {
90
88
  reported.add(message.text);
91
89
  result.push({ text: cutText(message.text), element: message.element });
92
90
  }
93
- return result;
91
+ return {
92
+ messages: result.slice(0, MAX_NEW_MESSAGES),
93
+ more: Math.max(0, result.length - MAX_NEW_MESSAGES),
94
+ };
94
95
  }
95
96
  /**
96
97
  * Cut a text to {@link MAX_MESSAGE_LENGTH} characters, marking the cut.
@@ -207,7 +208,7 @@ export function pageNavigation(startHref, read, events) {
207
208
  * Whether an action had no visible effect: the page was read before and
208
209
  * after in the same document, it counted no DOM change, nothing made the
209
210
  * check uncertain, and no navigation, message, shown element, request,
210
- * dialog or new window happened.
211
+ * dialog, new window or console message happened.
211
212
  *
212
213
  * @param read - Page read after the action
213
214
  * @param effects - Navigation and messages found
@@ -224,7 +225,8 @@ export function hadNoEffect(read, effects, activity) {
224
225
  (effects.shown ?? []).length === 0 &&
225
226
  activity.requests === 0 &&
226
227
  activity.dialogs === 0 &&
227
- !activity.opened);
228
+ !activity.opened &&
229
+ activity.consoleMessages === 0);
228
230
  }
229
231
  /**
230
232
  * Start watching an action's effects: listen for main-frame navigations,
@@ -264,7 +266,11 @@ async function collectEffects(watch, options) {
264
266
  const reportShown = options.reportShown === true;
265
267
  let snapshot = await readPage(watch, { stop: false, reportShown });
266
268
  let effects = effectsOf(start, snapshot, watch.listener.events);
267
- 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
+ });
268
274
  if (options.detectNoEffect && quiet()) {
269
275
  await delay(NO_EFFECT_RECHECK_MS);
270
276
  snapshot = await readPage(watch, { stop: true, reportShown });
@@ -324,7 +330,8 @@ function pageWork(watch, snapshot, domChanging = false) {
324
330
  }
325
331
  /**
326
332
  * Navigation, new messages and shown elements from the snapshots and CDP
327
- * events.
333
+ * events. Snapshots a page broke (it replaced the built-ins the snapshot
334
+ * script uses) give the navigation only.
328
335
  *
329
336
  * @param start - Snapshot before the action, if taken
330
337
  * @param snapshot - Read after the action, if taken
@@ -333,13 +340,22 @@ function pageWork(watch, snapshot, domChanging = false) {
333
340
  */
334
341
  function effectsOf(start, snapshot, events) {
335
342
  const navigation = pageNavigation(start?.href, snapshot, events);
336
- const messages = start && snapshot ? newMessages(start.messages, snapshot.messages, snapshot.fresh) : [];
337
- const shown = shownElements(snapshot?.shown ?? [], messages);
338
- return {
339
- ...(navigation && { navigation }),
340
- ...(messages.length > 0 && { messages }),
341
- ...(shown.length > 0 && { shown }),
342
- };
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
+ }
343
359
  }
344
360
  /**
345
361
  * Read the page after the action, unless a main-frame load is pending (the
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `bdg dom audit`: page-wide checks in one page walk
3
+ * ({@link AUDIT_PAGE_JS}), turned into findings by {@link buildAudit}.
4
+ */
5
+ import type { CDPConnection } from '../../connection/cdp.js';
6
+ import type { AuditResult } from '../../ipc/protocol/auditTypes.js';
7
+ import type { DomAuditCommand } from '../../ipc/protocol/commands.js';
8
+ /** Findings listed per check without `--limit` */
9
+ export declare const DEFAULT_AUDIT_LIMIT = 20;
10
+ /**
11
+ * Run the page-wide checks.
12
+ *
13
+ * @param cdp - CDP connection
14
+ * @param params - Checks, WCAG level and limit
15
+ * @returns Findings
16
+ * @throws CommandError (91) when the page script fails
17
+ */
18
+ export declare function auditPage(cdp: CDPConnection, params: DomAuditCommand): Promise<AuditResult>;
19
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `bdg dom audit`: page-wide checks in one page walk
3
+ * ({@link AUDIT_PAGE_JS}), turned into findings by {@link buildAudit}.
4
+ */
5
+ import { CommandError } from '../../errors/index.js';
6
+ import { operationFailedError } from '../../errors/messages.js';
7
+ import { buildAudit } from './auditModel.js';
8
+ import { AUDIT_PAGE_JS } from './auditScripts.js';
9
+ import { evaluateInBdgWorld } from '../page/bdgWorld.js';
10
+ import { EXIT_CODES } from '../../utils/exitCodes.js';
11
+ /** Findings listed per check without `--limit` */
12
+ export const DEFAULT_AUDIT_LIMIT = 20;
13
+ /**
14
+ * Run the page-wide checks.
15
+ *
16
+ * @param cdp - CDP connection
17
+ * @param params - Checks, WCAG level and limit
18
+ * @returns Findings
19
+ * @throws CommandError (91) when the page script fails
20
+ */
21
+ export async function auditPage(cdp, params) {
22
+ const response = await evaluateInBdgWorld(cdp, {
23
+ expression: `(${AUDIT_PAGE_JS})(${JSON.stringify(params.checks)})`,
24
+ returnByValue: true,
25
+ });
26
+ const raw = response.result.value;
27
+ if (response.exceptionDetails || !raw) {
28
+ const err = operationFailedError('audit the page', response.exceptionDetails?.exception?.description ?? 'the page script failed');
29
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SCRIPT_ERROR);
30
+ }
31
+ return buildAudit(raw, {
32
+ checks: params.checks,
33
+ level: params.level ?? 'AA',
34
+ limit: params.limit ?? DEFAULT_AUDIT_LIMIT,
35
+ });
36
+ }
37
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `bdg dom audit` results from the page-side walk ({@link AUDIT_PAGE_JS}):
3
+ * text below a WCAG contrast level (composited like `dom inspect`), what
4
+ * makes the page scroll sideways, cut-off text, scaled or distorted images,
5
+ * fixed and sticky layers and running animations. Pure: tested without a
6
+ * browser.
7
+ */
8
+ import type { AuditContrastItem, AuditImage, AuditResult, AuditCheck } from '../../ipc/protocol/auditTypes.js';
9
+ import type { RawAudit, RawImage } from './auditScripts.js';
10
+ /** What `dom audit` was asked for */
11
+ export interface AuditOptions {
12
+ checks: AuditCheck[];
13
+ /** WCAG level text must reach */
14
+ level: 'AA' | 'AAA';
15
+ /** Findings listed per check */
16
+ limit: number;
17
+ }
18
+ /**
19
+ * The audit result.
20
+ *
21
+ * @param raw - Page-side walk
22
+ * @param options - Checks, level and limit
23
+ * @returns Result
24
+ */
25
+ export declare function buildAudit(raw: RawAudit, options: AuditOptions): AuditResult;
26
+ /**
27
+ * The ratio WCAG asks of a text at a level: large text (24px, or 18.66px
28
+ * bold) needs less.
29
+ *
30
+ * @param item - Text size and weight
31
+ * @param level - AA or AAA
32
+ * @returns Minimum ratio
33
+ */
34
+ export declare function requiredRatio(item: Pick<AuditContrastItem, 'size' | 'weight'>, level: 'AA' | 'AAA'): number;
35
+ /**
36
+ * An image drawn with fewer pixels than the screen needs (upscaled, blurry:
37
+ * rendered size × pixel ratio over its own pixels) or with another aspect
38
+ * ratio (distorted, unless `object-fit` keeps the ratio).
39
+ *
40
+ * @param image - Page-side image
41
+ * @param pixelRatio - Device pixel ratio
42
+ * @returns Finding, or undefined when it is sharp and keeps its ratio
43
+ */
44
+ export declare function imageFinding(image: RawImage, pixelRatio?: number): AuditImage | undefined;
45
+ //# sourceMappingURL=auditModel.d.ts.map
@@ -0,0 +1,220 @@
1
+ /**
2
+ * `bdg dom audit` results from the page-side walk ({@link AUDIT_PAGE_JS}):
3
+ * text below a WCAG contrast level (composited like `dom inspect`), what
4
+ * makes the page scroll sideways, cut-off text, scaled or distorted images,
5
+ * fixed and sticky layers and running animations. Pure: tested without a
6
+ * browser.
7
+ */
8
+ import { textContrast } from './inspectPaintModel.js';
9
+ import { hexColor } from '../../utils/color.js';
10
+ import { pxNumber, round1 } from '../../utils/cssValues.js';
11
+ /** Scale or aspect change (fraction) below which an image counts as drawn at its size */
12
+ const IMAGE_TOLERANCE = 0.05;
13
+ /**
14
+ * The audit result.
15
+ *
16
+ * @param raw - Page-side walk
17
+ * @param options - Checks, level and limit
18
+ * @returns Result
19
+ */
20
+ export function buildAudit(raw, options) {
21
+ return {
22
+ checks: options.checks,
23
+ walked: raw.walked,
24
+ ...(raw.capped && { capped: true }),
25
+ ...(raw.texts && { contrast: contrastFindings(raw, options) }),
26
+ ...(raw.wide && { overflow: overflowFindings(raw, options.limit) }),
27
+ ...(raw.layers && {
28
+ layers: raw.layers.slice(0, options.limit).map((layer) => ({
29
+ element: layer.label,
30
+ position: layer.position,
31
+ zIndex: layer.zIndex,
32
+ rect: {
33
+ x: round1(layer.rect.x),
34
+ y: round1(layer.rect.y),
35
+ w: round1(layer.rect.w),
36
+ h: round1(layer.rect.h),
37
+ },
38
+ inView: layer.inView,
39
+ })),
40
+ }),
41
+ ...(raw.animations && {
42
+ animations: grouped(raw.animations.map((animation) => ({
43
+ element: animation.label,
44
+ name: animation.name,
45
+ type: animation.type,
46
+ duration: animation.duration,
47
+ iterations: animation.iterations,
48
+ ...(animation.scrollDriven && { scrollDriven: true }),
49
+ }))).slice(0, options.limit),
50
+ }),
51
+ ...(raw.canvases && { canvases: raw.canvases }),
52
+ };
53
+ }
54
+ /**
55
+ * Text below the level, weakest first.
56
+ *
57
+ * @param raw - Page-side walk
58
+ * @param options - Level and limit
59
+ * @returns How many were checked and failed, and the weakest
60
+ */
61
+ function contrastFindings(raw, options) {
62
+ const checked = (raw.texts ?? []).map((text) => contrastItem(text, raw.canvasDark));
63
+ const below = checked
64
+ .filter((item) => item !== undefined)
65
+ .filter((item) => item.ratio < requiredRatio(item, options.level))
66
+ .sort((a, b) => a.ratio - b.ratio);
67
+ const failing = below.filter((item) => !item.approximate);
68
+ const uncertain = below.length - failing.length;
69
+ return {
70
+ level: options.level,
71
+ checked: checked.length,
72
+ failing: failing.length,
73
+ items: failing.slice(0, options.limit),
74
+ ...(uncertain > 0 && { uncertain }),
75
+ };
76
+ }
77
+ /**
78
+ * One text holder's contrast.
79
+ *
80
+ * @param text - Page-side text holder
81
+ * @param canvasDark - The page canvas is dark
82
+ * @returns Finding, or undefined when its color cannot be read
83
+ */
84
+ function contrastItem(text, canvasDark) {
85
+ const contrast = textContrast({ color: text.color, 'font-size': text.fontSize, 'font-weight': text.fontWeight }, {
86
+ backgrounds: text.backgrounds,
87
+ canvasDark,
88
+ opacity: text.opacity,
89
+ paintRisks: text.risks,
90
+ });
91
+ if (!contrast)
92
+ return undefined;
93
+ return {
94
+ element: text.label,
95
+ text: text.text,
96
+ ratio: contrast.ratio,
97
+ color: hexColor(text.color),
98
+ background: contrast.background,
99
+ size: pxNumber(text.fontSize) ?? 16,
100
+ weight: Number(text.fontWeight) || 400,
101
+ inView: text.inView,
102
+ ...(contrast.opacity !== undefined && { opacity: contrast.opacity }),
103
+ ...(contrast.approximate && { approximate: contrast.approximate }),
104
+ };
105
+ }
106
+ /**
107
+ * The ratio WCAG asks of a text at a level: large text (24px, or 18.66px
108
+ * bold) needs less.
109
+ *
110
+ * @param item - Text size and weight
111
+ * @param level - AA or AAA
112
+ * @returns Minimum ratio
113
+ */
114
+ export function requiredRatio(item, level) {
115
+ const large = item.size >= 24 || (item.size >= 18.66 && item.weight >= 700);
116
+ if (level === 'AAA')
117
+ return large ? 4.5 : 7;
118
+ return large ? 3 : 4.5;
119
+ }
120
+ /**
121
+ * What makes the page scroll sideways, cut-off text and scaled images.
122
+ *
123
+ * @param raw - Page-side walk
124
+ * @param limit - Findings per list
125
+ * @returns Overflow findings
126
+ */
127
+ function overflowFindings(raw, limit) {
128
+ const wide = [...(raw.wide ?? [])].sort((a, b) => b.right - a.right);
129
+ const images = grouped((raw.images ?? [])
130
+ .map((image) => imageFinding(image, raw.pixelRatio))
131
+ .filter((image) => image !== undefined));
132
+ return {
133
+ pageWidth: raw.pageWidth,
134
+ viewportWidth: raw.viewport.width,
135
+ scrollsSideways: raw.pageWidth > raw.viewport.width + 1,
136
+ wide: wide.slice(0, limit).map((element) => ({
137
+ element: element.label,
138
+ right: round1(element.right),
139
+ width: round1(element.width),
140
+ })),
141
+ truncated: grouped((raw.truncated ?? []).map((text) => ({
142
+ element: text.label,
143
+ text: text.text,
144
+ kind: text.kind,
145
+ }))).slice(0, limit),
146
+ images: images.slice(0, limit),
147
+ pixelRatio: raw.pixelRatio,
148
+ scrollers: (raw.scrollers ?? []).slice(0, limit).map((scroller) => ({
149
+ element: scroller.label,
150
+ scrollWidth: scroller.scrollWidth,
151
+ width: scroller.width,
152
+ })),
153
+ };
154
+ }
155
+ /**
156
+ * How much an image's pixels are stretched on screen, by its `object-fit`:
157
+ * `fill` and `cover` stretch to the larger ratio, `contain` to the smaller,
158
+ * `none` not at all, `scale-down` at most to its own size.
159
+ *
160
+ * @param image - Page-side image
161
+ * @param pixelRatio - Device pixel ratio
162
+ * @returns Screen pixels per image pixel
163
+ */
164
+ function drawnScale(image, pixelRatio) {
165
+ const across = (image.rendered.w * pixelRatio) / image.natural.w;
166
+ const down = (image.rendered.h * pixelRatio) / image.natural.h;
167
+ if (image.objectFit === 'none')
168
+ return pixelRatio;
169
+ if (image.objectFit === 'contain')
170
+ return Math.min(across, down);
171
+ if (image.objectFit === 'scale-down')
172
+ return Math.min(pixelRatio, Math.min(across, down));
173
+ return Math.max(across, down);
174
+ }
175
+ /**
176
+ * Findings with identical ones merged into the first, with a `count`.
177
+ *
178
+ * @param findings - Findings in page order
179
+ * @returns Findings, each distinct one once
180
+ */
181
+ function grouped(findings) {
182
+ const byKey = new Map();
183
+ for (const finding of findings) {
184
+ const key = JSON.stringify(finding);
185
+ const seen = byKey.get(key);
186
+ if (seen)
187
+ seen.count = (seen.count ?? 1) + 1;
188
+ else
189
+ byKey.set(key, { ...finding });
190
+ }
191
+ return [...byKey.values()];
192
+ }
193
+ /**
194
+ * An image drawn with fewer pixels than the screen needs (upscaled, blurry:
195
+ * rendered size × pixel ratio over its own pixels) or with another aspect
196
+ * ratio (distorted, unless `object-fit` keeps the ratio).
197
+ *
198
+ * @param image - Page-side image
199
+ * @param pixelRatio - Device pixel ratio
200
+ * @returns Finding, or undefined when it is sharp and keeps its ratio
201
+ */
202
+ export function imageFinding(image, pixelRatio = 1) {
203
+ const scale = drawnScale(image, pixelRatio);
204
+ const naturalRatio = image.natural.w / image.natural.h;
205
+ const renderedRatio = image.rendered.w / image.rendered.h;
206
+ const distorted = image.objectFit === 'fill' &&
207
+ Math.abs(renderedRatio - naturalRatio) / naturalRatio > IMAGE_TOLERANCE;
208
+ const upscaled = scale > 1 + IMAGE_TOLERANCE;
209
+ if (!distorted && !upscaled)
210
+ return undefined;
211
+ return {
212
+ element: image.label,
213
+ natural: image.natural,
214
+ rendered: { w: round1(image.rendered.w), h: round1(image.rendered.h) },
215
+ scale: Math.round(scale * 100) / 100,
216
+ ...(upscaled && { upscaled: true }),
217
+ ...(distorted && { distorted: true }),
218
+ };
219
+ }
220
+ //# sourceMappingURL=auditModel.js.map
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Page-side part of `bdg dom audit`: one walk over the rendered elements of
3
+ * the page (open shadow roots and the light DOM slotted into them, at most
4
+ * {@link AUDIT_ELEMENT_CAP}; nothing inside a fixed element counts as making
5
+ * the page wider; SVG images and placeholders of 2px or less are skipped)
6
+ * that collects what each check needs. Colors and contrast are computed in
7
+ * the daemon ({@link buildAudit}), the same way `dom inspect` computes them.
8
+ */
9
+ import { type RawBackground } from './inspectScripts.js';
10
+ /** Elements the walk looks at, at most */
11
+ export declare const AUDIT_ELEMENT_CAP = 20000;
12
+ /** A text holder: an element that draws text of its own */
13
+ export interface RawAuditText {
14
+ label: string;
15
+ text: string;
16
+ color: string;
17
+ fontSize: string;
18
+ fontWeight: string;
19
+ /** Backgrounds from the element up to the root, each with its own opacity */
20
+ backgrounds: RawBackground[];
21
+ opacity: number;
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
+ */
27
+ risks: string[];
28
+ /** Inside the viewport */
29
+ inView: boolean;
30
+ }
31
+ /** An element that reaches past the right edge of the page's viewport */
32
+ export interface RawWideElement {
33
+ label: string;
34
+ right: number;
35
+ width: number;
36
+ }
37
+ /** Text cut off by its box */
38
+ export interface RawTruncated {
39
+ label: string;
40
+ text: string;
41
+ /** `ellipsis`, `clamp` or `clip` */
42
+ kind: string;
43
+ }
44
+ /** An image drawn at another size than its own */
45
+ export interface RawImage {
46
+ label: string;
47
+ natural: {
48
+ w: number;
49
+ h: number;
50
+ };
51
+ rendered: {
52
+ w: number;
53
+ h: number;
54
+ };
55
+ objectFit: string;
56
+ }
57
+ /** A fixed or sticky element */
58
+ export interface RawLayer {
59
+ label: string;
60
+ position: string;
61
+ zIndex: string;
62
+ rect: {
63
+ x: number;
64
+ y: number;
65
+ w: number;
66
+ h: number;
67
+ };
68
+ inView: boolean;
69
+ }
70
+ /** A running animation or transition */
71
+ export interface RawAnimation {
72
+ label: string;
73
+ /** Animation name, transitioned property, or `animation` for a Web Animation */
74
+ name: string;
75
+ type: string;
76
+ duration: number | string;
77
+ iterations: number | string;
78
+ scrollDriven: boolean;
79
+ }
80
+ /** What {@link AUDIT_PAGE_JS} returns */
81
+ export interface RawAudit {
82
+ viewport: {
83
+ width: number;
84
+ height: number;
85
+ };
86
+ /** Device pixel ratio: an image needs that many pixels per CSS px to look sharp */
87
+ pixelRatio: number;
88
+ pageWidth: number;
89
+ canvasDark: boolean;
90
+ /** Elements walked, and whether the walk stopped at the cap */
91
+ walked: number;
92
+ capped: boolean;
93
+ texts?: RawAuditText[];
94
+ wide?: RawWideElement[];
95
+ truncated?: RawTruncated[];
96
+ images?: RawImage[];
97
+ /** Elements whose content scrolls sideways inside them (carousels, tab strips) */
98
+ scrollers?: Array<{
99
+ label: string;
100
+ scrollWidth: number;
101
+ width: number;
102
+ }>;
103
+ layers?: RawLayer[];
104
+ animations?: RawAnimation[];
105
+ /** Visible `<canvas>` elements (their animations are drawn by scripts, not CSS or Web Animations) */
106
+ canvases?: number;
107
+ }
108
+ /**
109
+ * Page-side audit walk. Arguments: the checks to collect for
110
+ * (`contrast`, `overflow`, `layers`, `animations`).
111
+ */
112
+ export declare const AUDIT_PAGE_JS: string;
113
+ //# sourceMappingURL=auditScripts.d.ts.map