@monoes/monobrowse 1.0.12 → 1.0.14

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 (271) hide show
  1. package/dist/src/__tests__/bridge-instruments.test.d.ts +2 -0
  2. package/dist/src/__tests__/bridge-instruments.test.d.ts.map +1 -0
  3. package/dist/src/__tests__/bridge-instruments.test.js +132 -0
  4. package/dist/src/__tests__/bridge-instruments.test.js.map +1 -0
  5. package/dist/src/__tests__/bridge-transport.test.d.ts +2 -0
  6. package/dist/src/__tests__/bridge-transport.test.d.ts.map +1 -0
  7. package/dist/src/__tests__/bridge-transport.test.js +287 -0
  8. package/dist/src/__tests__/bridge-transport.test.js.map +1 -0
  9. package/dist/src/__tests__/cdp-transport.test.d.ts +2 -0
  10. package/dist/src/__tests__/cdp-transport.test.d.ts.map +1 -0
  11. package/dist/src/__tests__/cdp-transport.test.js +167 -0
  12. package/dist/src/__tests__/cdp-transport.test.js.map +1 -0
  13. package/dist/src/__tests__/profiler-heap.test.d.ts +2 -0
  14. package/dist/src/__tests__/profiler-heap.test.d.ts.map +1 -0
  15. package/dist/src/__tests__/profiler-heap.test.js +154 -0
  16. package/dist/src/__tests__/profiler-heap.test.js.map +1 -0
  17. package/dist/src/__tests__/report-a11y.test.d.ts +2 -0
  18. package/dist/src/__tests__/report-a11y.test.d.ts.map +1 -0
  19. package/dist/src/__tests__/report-a11y.test.js +147 -0
  20. package/dist/src/__tests__/report-a11y.test.js.map +1 -0
  21. package/dist/src/__tests__/report-analyze.test.d.ts +2 -0
  22. package/dist/src/__tests__/report-analyze.test.d.ts.map +1 -0
  23. package/dist/src/__tests__/report-analyze.test.js +157 -0
  24. package/dist/src/__tests__/report-analyze.test.js.map +1 -0
  25. package/dist/src/__tests__/report-budget.test.d.ts +2 -0
  26. package/dist/src/__tests__/report-budget.test.d.ts.map +1 -0
  27. package/dist/src/__tests__/report-budget.test.js +80 -0
  28. package/dist/src/__tests__/report-budget.test.js.map +1 -0
  29. package/dist/src/__tests__/report-diff.test.d.ts +2 -0
  30. package/dist/src/__tests__/report-diff.test.d.ts.map +1 -0
  31. package/dist/src/__tests__/report-diff.test.js +231 -0
  32. package/dist/src/__tests__/report-diff.test.js.map +1 -0
  33. package/dist/src/__tests__/report-evidence.test.d.ts +2 -0
  34. package/dist/src/__tests__/report-evidence.test.d.ts.map +1 -0
  35. package/dist/src/__tests__/report-evidence.test.js +214 -0
  36. package/dist/src/__tests__/report-evidence.test.js.map +1 -0
  37. package/dist/src/__tests__/report-flake.test.d.ts +2 -0
  38. package/dist/src/__tests__/report-flake.test.d.ts.map +1 -0
  39. package/dist/src/__tests__/report-flake.test.js +182 -0
  40. package/dist/src/__tests__/report-flake.test.js.map +1 -0
  41. package/dist/src/__tests__/report-history.test.d.ts +2 -0
  42. package/dist/src/__tests__/report-history.test.d.ts.map +1 -0
  43. package/dist/src/__tests__/report-history.test.js +229 -0
  44. package/dist/src/__tests__/report-history.test.js.map +1 -0
  45. package/dist/src/__tests__/report-output.test.d.ts +2 -0
  46. package/dist/src/__tests__/report-output.test.d.ts.map +1 -0
  47. package/dist/src/__tests__/report-output.test.js +169 -0
  48. package/dist/src/__tests__/report-output.test.js.map +1 -0
  49. package/dist/src/__tests__/report-png.test.d.ts +2 -0
  50. package/dist/src/__tests__/report-png.test.d.ts.map +1 -0
  51. package/dist/src/__tests__/report-png.test.js +84 -0
  52. package/dist/src/__tests__/report-png.test.js.map +1 -0
  53. package/dist/src/__tests__/report-render-extras.test.d.ts +2 -0
  54. package/dist/src/__tests__/report-render-extras.test.d.ts.map +1 -0
  55. package/dist/src/__tests__/report-render-extras.test.js +285 -0
  56. package/dist/src/__tests__/report-render-extras.test.js.map +1 -0
  57. package/dist/src/__tests__/report-render.test.d.ts +2 -0
  58. package/dist/src/__tests__/report-render.test.d.ts.map +1 -0
  59. package/dist/src/__tests__/report-render.test.js +143 -0
  60. package/dist/src/__tests__/report-render.test.js.map +1 -0
  61. package/dist/src/__tests__/report-run.test.d.ts +2 -0
  62. package/dist/src/__tests__/report-run.test.d.ts.map +1 -0
  63. package/dist/src/__tests__/report-run.test.js +234 -0
  64. package/dist/src/__tests__/report-run.test.js.map +1 -0
  65. package/dist/src/browser/bridge.d.ts +119 -0
  66. package/dist/src/browser/bridge.d.ts.map +1 -0
  67. package/dist/src/browser/bridge.js +264 -0
  68. package/dist/src/browser/bridge.js.map +1 -0
  69. package/dist/src/browser/cdp.d.ts +11 -1
  70. package/dist/src/browser/cdp.d.ts.map +1 -1
  71. package/dist/src/browser/cdp.js +60 -56
  72. package/dist/src/browser/cdp.js.map +1 -1
  73. package/dist/src/browser/index.d.ts +3 -0
  74. package/dist/src/browser/index.d.ts.map +1 -1
  75. package/dist/src/browser/index.js +3 -0
  76. package/dist/src/browser/index.js.map +1 -1
  77. package/dist/src/browser/profiler.d.ts.map +1 -1
  78. package/dist/src/browser/profiler.js +19 -22
  79. package/dist/src/browser/profiler.js.map +1 -1
  80. package/dist/src/browser/transport.d.ts +62 -0
  81. package/dist/src/browser/transport.d.ts.map +1 -0
  82. package/dist/src/browser/transport.js +65 -0
  83. package/dist/src/browser/transport.js.map +1 -0
  84. package/dist/src/cli/commander-adapter.d.ts +16 -0
  85. package/dist/src/cli/commander-adapter.d.ts.map +1 -0
  86. package/dist/src/cli/commander-adapter.js +68 -0
  87. package/dist/src/cli/commander-adapter.js.map +1 -0
  88. package/dist/src/cli/commands-context.d.ts +14 -0
  89. package/dist/src/cli/commands-context.d.ts.map +1 -0
  90. package/dist/src/cli/commands-context.js +351 -0
  91. package/dist/src/cli/commands-context.js.map +1 -0
  92. package/dist/src/cli/commands-data.d.ts +12 -0
  93. package/dist/src/cli/commands-data.d.ts.map +1 -0
  94. package/dist/src/cli/commands-data.js +246 -0
  95. package/dist/src/cli/commands-data.js.map +1 -0
  96. package/dist/src/cli/commands-element.d.ts +15 -0
  97. package/dist/src/cli/commands-element.d.ts.map +1 -0
  98. package/dist/src/cli/commands-element.js +262 -0
  99. package/dist/src/cli/commands-element.js.map +1 -0
  100. package/dist/src/cli/commands-files.d.ts +9 -0
  101. package/dist/src/cli/commands-files.d.ts.map +1 -0
  102. package/dist/src/cli/commands-files.js +207 -0
  103. package/dist/src/cli/commands-files.js.map +1 -0
  104. package/dist/src/cli/commands-input.d.ts +26 -0
  105. package/dist/src/cli/commands-input.d.ts.map +1 -0
  106. package/dist/src/cli/commands-input.js +399 -0
  107. package/dist/src/cli/commands-input.js.map +1 -0
  108. package/dist/src/cli/commands-navigate.d.ts +12 -0
  109. package/dist/src/cli/commands-navigate.d.ts.map +1 -0
  110. package/dist/src/cli/commands-navigate.js +389 -0
  111. package/dist/src/cli/commands-navigate.js.map +1 -0
  112. package/dist/src/cli/commands-page.d.ts +25 -0
  113. package/dist/src/cli/commands-page.d.ts.map +1 -0
  114. package/dist/src/cli/commands-page.js +377 -0
  115. package/dist/src/cli/commands-page.js.map +1 -0
  116. package/dist/src/cli/commands-report.d.ts +8 -0
  117. package/dist/src/cli/commands-report.d.ts.map +1 -0
  118. package/dist/src/cli/commands-report.js +256 -0
  119. package/dist/src/cli/commands-report.js.map +1 -0
  120. package/dist/src/cli/commands-script.d.ts +43 -0
  121. package/dist/src/cli/commands-script.d.ts.map +1 -0
  122. package/dist/src/cli/commands-script.js +168 -0
  123. package/dist/src/cli/commands-script.js.map +1 -0
  124. package/dist/src/cli/commands-session.d.ts +15 -0
  125. package/dist/src/cli/commands-session.d.ts.map +1 -0
  126. package/dist/src/cli/commands-session.js +357 -0
  127. package/dist/src/cli/commands-session.js.map +1 -0
  128. package/dist/src/cli/commands-trace.d.ts +12 -0
  129. package/dist/src/cli/commands-trace.d.ts.map +1 -0
  130. package/dist/src/cli/commands-trace.js +347 -0
  131. package/dist/src/cli/commands-trace.js.map +1 -0
  132. package/dist/src/cli/commands.d.ts +22 -42
  133. package/dist/src/cli/commands.d.ts.map +1 -1
  134. package/dist/src/cli/commands.js +48 -3400
  135. package/dist/src/cli/commands.js.map +1 -1
  136. package/dist/src/cli/session.d.ts +47 -0
  137. package/dist/src/cli/session.d.ts.map +1 -0
  138. package/dist/src/cli/session.js +343 -0
  139. package/dist/src/cli/session.js.map +1 -0
  140. package/dist/src/report/a11y.d.ts +52 -0
  141. package/dist/src/report/a11y.d.ts.map +1 -0
  142. package/dist/src/report/a11y.js +231 -0
  143. package/dist/src/report/a11y.js.map +1 -0
  144. package/dist/src/report/analyze.d.ts +20 -0
  145. package/dist/src/report/analyze.d.ts.map +1 -0
  146. package/dist/src/report/analyze.js +136 -0
  147. package/dist/src/report/analyze.js.map +1 -0
  148. package/dist/src/report/budget.d.ts +27 -0
  149. package/dist/src/report/budget.d.ts.map +1 -0
  150. package/dist/src/report/budget.js +143 -0
  151. package/dist/src/report/budget.js.map +1 -0
  152. package/dist/src/report/collect-a11y.d.ts +25 -0
  153. package/dist/src/report/collect-a11y.d.ts.map +1 -0
  154. package/dist/src/report/collect-a11y.js +154 -0
  155. package/dist/src/report/collect-a11y.js.map +1 -0
  156. package/dist/src/report/collect.d.ts +45 -0
  157. package/dist/src/report/collect.d.ts.map +1 -0
  158. package/dist/src/report/collect.js +275 -0
  159. package/dist/src/report/collect.js.map +1 -0
  160. package/dist/src/report/evidence.d.ts +80 -0
  161. package/dist/src/report/evidence.d.ts.map +1 -0
  162. package/dist/src/report/evidence.js +279 -0
  163. package/dist/src/report/evidence.js.map +1 -0
  164. package/dist/src/report/flake.d.ts +88 -0
  165. package/dist/src/report/flake.d.ts.map +1 -0
  166. package/dist/src/report/flake.js +232 -0
  167. package/dist/src/report/flake.js.map +1 -0
  168. package/dist/src/report/history.d.ts +85 -0
  169. package/dist/src/report/history.d.ts.map +1 -0
  170. package/dist/src/report/history.js +176 -0
  171. package/dist/src/report/history.js.map +1 -0
  172. package/dist/src/report/index.d.ts +102 -0
  173. package/dist/src/report/index.d.ts.map +1 -0
  174. package/dist/src/report/index.js +216 -0
  175. package/dist/src/report/index.js.map +1 -0
  176. package/dist/src/report/pixel-diff.d.ts +30 -0
  177. package/dist/src/report/pixel-diff.d.ts.map +1 -0
  178. package/dist/src/report/pixel-diff.js +175 -0
  179. package/dist/src/report/pixel-diff.js.map +1 -0
  180. package/dist/src/report/png.d.ts +28 -0
  181. package/dist/src/report/png.d.ts.map +1 -0
  182. package/dist/src/report/png.js +259 -0
  183. package/dist/src/report/png.js.map +1 -0
  184. package/dist/src/report/render-evidence.d.ts +16 -0
  185. package/dist/src/report/render-evidence.d.ts.map +1 -0
  186. package/dist/src/report/render-evidence.js +174 -0
  187. package/dist/src/report/render-evidence.js.map +1 -0
  188. package/dist/src/report/render-trend.d.ts +16 -0
  189. package/dist/src/report/render-trend.d.ts.map +1 -0
  190. package/dist/src/report/render-trend.js +194 -0
  191. package/dist/src/report/render-trend.js.map +1 -0
  192. package/dist/src/report/render.d.ts +21 -0
  193. package/dist/src/report/render.d.ts.map +1 -0
  194. package/dist/src/report/render.js +286 -0
  195. package/dist/src/report/render.js.map +1 -0
  196. package/dist/src/report/run-diff.d.ts +18 -0
  197. package/dist/src/report/run-diff.d.ts.map +1 -0
  198. package/dist/src/report/run-diff.js +51 -0
  199. package/dist/src/report/run-diff.js.map +1 -0
  200. package/dist/src/report/structure.d.ts +41 -0
  201. package/dist/src/report/structure.d.ts.map +1 -0
  202. package/dist/src/report/structure.js +210 -0
  203. package/dist/src/report/structure.js.map +1 -0
  204. package/dist/src/report/trend.d.ts +26 -0
  205. package/dist/src/report/trend.d.ts.map +1 -0
  206. package/dist/src/report/trend.js +164 -0
  207. package/dist/src/report/trend.js.map +1 -0
  208. package/dist/src/report/types.d.ts +320 -0
  209. package/dist/src/report/types.d.ts.map +1 -0
  210. package/dist/src/report/types.js +11 -0
  211. package/dist/src/report/types.js.map +1 -0
  212. package/dist/src/report/util.d.ts +17 -0
  213. package/dist/src/report/util.d.ts.map +1 -0
  214. package/dist/src/report/util.js +45 -0
  215. package/dist/src/report/util.js.map +1 -0
  216. package/dist/tsconfig.tsbuildinfo +1 -1
  217. package/package.json +1 -1
  218. package/src/__tests__/bridge-instruments.test.ts +180 -0
  219. package/src/__tests__/bridge-transport.test.ts +365 -0
  220. package/src/__tests__/cdp-transport.test.ts +198 -0
  221. package/src/__tests__/profiler-heap.test.ts +191 -0
  222. package/src/__tests__/report-a11y.test.ts +180 -0
  223. package/src/__tests__/report-analyze.test.ts +202 -0
  224. package/src/__tests__/report-budget.test.ts +92 -0
  225. package/src/__tests__/report-diff.test.ts +272 -0
  226. package/src/__tests__/report-evidence.test.ts +276 -0
  227. package/src/__tests__/report-flake.test.ts +214 -0
  228. package/src/__tests__/report-history.test.ts +293 -0
  229. package/src/__tests__/report-output.test.ts +210 -0
  230. package/src/__tests__/report-png.test.ts +95 -0
  231. package/src/__tests__/report-render-extras.test.ts +315 -0
  232. package/src/__tests__/report-render.test.ts +175 -0
  233. package/src/__tests__/report-run.test.ts +264 -0
  234. package/src/browser/bridge.ts +354 -0
  235. package/src/browser/cdp.ts +57 -52
  236. package/src/browser/index.ts +3 -0
  237. package/src/browser/profiler.ts +28 -21
  238. package/src/browser/transport.ts +111 -0
  239. package/src/cli/commander-adapter.ts +65 -0
  240. package/src/cli/commands-context.ts +367 -0
  241. package/src/cli/commands-data.ts +248 -0
  242. package/src/cli/commands-element.ts +305 -0
  243. package/src/cli/commands-files.ts +248 -0
  244. package/src/cli/commands-input.ts +434 -0
  245. package/src/cli/commands-navigate.ts +442 -0
  246. package/src/cli/commands-page.ts +420 -0
  247. package/src/cli/commands-report.ts +275 -0
  248. package/src/cli/commands-script.ts +183 -0
  249. package/src/cli/commands-session.ts +376 -0
  250. package/src/cli/commands-trace.ts +349 -0
  251. package/src/cli/commands.ts +101 -3677
  252. package/src/cli/session.ts +412 -0
  253. package/src/report/a11y.ts +242 -0
  254. package/src/report/analyze.ts +181 -0
  255. package/src/report/budget.ts +155 -0
  256. package/src/report/collect-a11y.ts +200 -0
  257. package/src/report/collect.ts +408 -0
  258. package/src/report/evidence.ts +379 -0
  259. package/src/report/flake.ts +303 -0
  260. package/src/report/history.ts +219 -0
  261. package/src/report/index.ts +307 -0
  262. package/src/report/pixel-diff.ts +197 -0
  263. package/src/report/png.ts +290 -0
  264. package/src/report/render-evidence.ts +194 -0
  265. package/src/report/render-trend.ts +225 -0
  266. package/src/report/render.ts +314 -0
  267. package/src/report/run-diff.ts +63 -0
  268. package/src/report/structure.ts +212 -0
  269. package/src/report/trend.ts +189 -0
  270. package/src/report/types.ts +356 -0
  271. package/src/report/util.ts +48 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Structural signature of a page, and the diff between two of them (RIG-10).
3
+ *
4
+ * The signature is derived from the accessibility tree we already fetch for
5
+ * the a11y rules, so this costs no extra CDP round-trip. It is deliberately
6
+ * the AX tree and not the DOM: the AX tree is what the page *means* — a
7
+ * className churned by a CSS-in-JS build is invisible to it, while a button
8
+ * losing its label, a heading changing text, or a form field disappearing
9
+ * all show up. That is the signal a "did this deploy change anything real?"
10
+ * diff wants.
11
+ *
12
+ * Everything here is pure, so the tests feed hand-written trees.
13
+ */
14
+ import type { AxNode, StructureDiff, StructureNode } from './types.js';
15
+ /** Bound on signature size: a 40k-node AX tree must not become a 40k-row diff. */
16
+ export declare const MAX_STRUCTURE_NODES = 1500;
17
+ /**
18
+ * Walk the AX tree into a flat, path-addressed signature.
19
+ *
20
+ * Path segments are `role[n]`, n counting siblings of the same role after
21
+ * transparent wrappers are spliced away. Two runs of an unchanged page
22
+ * therefore produce identical paths; an inserted element shifts only its own
23
+ * role's numbering, which the diff's second pass then recovers from.
24
+ */
25
+ export declare function buildStructure(nodes: AxNode[]): StructureNode[];
26
+ /**
27
+ * Diff two signatures.
28
+ *
29
+ * Two passes, because path alone is too brittle to use on its own: inserting
30
+ * one list item renumbers every later sibling, which a naive path diff would
31
+ * report as "everything below here was replaced".
32
+ *
33
+ * 1. Exact path match — same slot. Same name means unchanged, a different
34
+ * name means renamed (the useful case: a label that silently changed).
35
+ * 2. Whatever is left is matched on role+name, which recovers elements that
36
+ * merely moved. Only what survives both passes is genuinely gained/lost.
37
+ */
38
+ export declare function diffStructure(previous: StructureNode[], current: StructureNode[]): StructureDiff;
39
+ /** One line for a CI log: "3 gained, 1 lost, 2 renamed". */
40
+ export declare function summarizeStructureDiff(diff: StructureDiff): string;
41
+ //# sourceMappingURL=structure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structure.d.ts","sourceRoot":"","sources":["../../../src/report/structure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAWvE,kFAAkF;AAClF,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAexC;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,aAAa,EAAE,CAkE/D;AAcD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,aAAa,CA4DhG;AAED,4DAA4D;AAC5D,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAOlE"}
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Structural signature of a page, and the diff between two of them (RIG-10).
3
+ *
4
+ * The signature is derived from the accessibility tree we already fetch for
5
+ * the a11y rules, so this costs no extra CDP round-trip. It is deliberately
6
+ * the AX tree and not the DOM: the AX tree is what the page *means* — a
7
+ * className churned by a CSS-in-JS build is invisible to it, while a button
8
+ * losing its label, a heading changing text, or a form field disappearing
9
+ * all show up. That is the signal a "did this deploy change anything real?"
10
+ * diff wants.
11
+ *
12
+ * Everything here is pure, so the tests feed hand-written trees.
13
+ */
14
+ /** Roles that exist only to hold other nodes — spliced out, children kept. */
15
+ const TRANSPARENT_ROLES = new Set([
16
+ 'none',
17
+ 'generic',
18
+ 'presentation',
19
+ 'inlinetextbox',
20
+ 'genericcontainer',
21
+ ]);
22
+ /** Bound on signature size: a 40k-node AX tree must not become a 40k-row diff. */
23
+ export const MAX_STRUCTURE_NODES = 1500;
24
+ const MAX_DEPTH = 25;
25
+ /** Accessible names are compared verbatim, so trim the essays. */
26
+ const MAX_NAME_LENGTH = 160;
27
+ function strValue(v) {
28
+ return typeof v === 'string' ? v : v == null ? '' : String(v);
29
+ }
30
+ function normalizeName(raw) {
31
+ const flat = raw.replace(/\s+/g, ' ').trim();
32
+ if (!flat)
33
+ return null;
34
+ return flat.length > MAX_NAME_LENGTH ? `${flat.slice(0, MAX_NAME_LENGTH - 1)}…` : flat;
35
+ }
36
+ /**
37
+ * Walk the AX tree into a flat, path-addressed signature.
38
+ *
39
+ * Path segments are `role[n]`, n counting siblings of the same role after
40
+ * transparent wrappers are spliced away. Two runs of an unchanged page
41
+ * therefore produce identical paths; an inserted element shifts only its own
42
+ * role's numbering, which the diff's second pass then recovers from.
43
+ */
44
+ export function buildStructure(nodes) {
45
+ const byId = new Map();
46
+ for (const node of nodes)
47
+ byId.set(String(node.nodeId), node);
48
+ const childIds = (node) => (node.childIds ?? []).map(String);
49
+ /** Visible children, with transparent wrappers replaced by their children. */
50
+ const effectiveChildren = (node, depth) => {
51
+ const out = [];
52
+ const queue = [...childIds(node)];
53
+ let guard = 0;
54
+ while (queue.length && guard++ < 4000) {
55
+ const child = byId.get(queue.shift());
56
+ if (!child)
57
+ continue;
58
+ const role = strValue(child.role?.value).toLowerCase();
59
+ if (child.ignored || TRANSPARENT_ROLES.has(role)) {
60
+ if (depth < MAX_DEPTH)
61
+ queue.unshift(...childIds(child));
62
+ continue;
63
+ }
64
+ out.push(child);
65
+ }
66
+ return out;
67
+ };
68
+ const referenced = new Set();
69
+ for (const node of nodes)
70
+ for (const id of childIds(node))
71
+ referenced.add(id);
72
+ const roots = nodes.filter((n) => !referenced.has(String(n.nodeId)));
73
+ // In a cycle every node has a parent, so there is no unreferenced root and
74
+ // the walk below would visit nothing. Start somewhere rather than silently
75
+ // returning an empty signature that the diff would read as "page is gone".
76
+ if (!roots.length && nodes.length)
77
+ roots.push(nodes[0]);
78
+ const out = [];
79
+ const visited = new Set();
80
+ const counters = new Map();
81
+ const walk = (node, parentPath, depth) => {
82
+ if (out.length >= MAX_STRUCTURE_NODES || depth > MAX_DEPTH)
83
+ return;
84
+ const id = String(node.nodeId);
85
+ if (visited.has(id))
86
+ return; // a malformed tree must not loop us forever
87
+ visited.add(id);
88
+ const role = strValue(node.role?.value).toLowerCase() || 'unknown';
89
+ let siblingsSeen = counters.get(parentPath);
90
+ if (!siblingsSeen) {
91
+ siblingsSeen = new Map();
92
+ counters.set(parentPath, siblingsSeen);
93
+ }
94
+ const n = (siblingsSeen.get(role) ?? 0) + 1;
95
+ siblingsSeen.set(role, n);
96
+ const path = `${parentPath}/${role}[${n}]`;
97
+ out.push({ path, role, name: normalizeName(strValue(node.name?.value)), depth });
98
+ for (const child of effectiveChildren(node, depth))
99
+ walk(child, path, depth + 1);
100
+ };
101
+ for (const root of roots) {
102
+ const role = strValue(root.role?.value).toLowerCase();
103
+ if (root.ignored || TRANSPARENT_ROLES.has(role)) {
104
+ for (const child of effectiveChildren(root, 0))
105
+ walk(child, '', 0);
106
+ }
107
+ else {
108
+ walk(root, '', 0);
109
+ }
110
+ }
111
+ return out;
112
+ }
113
+ // ---------------------------------------------------------------------------
114
+ // Diff
115
+ // ---------------------------------------------------------------------------
116
+ /**
117
+ * Identity for the second matching pass. Names are whitespace-collapsed to a
118
+ * single line by normalizeName, so a newline is a separator no value carries.
119
+ */
120
+ function identityKey(node) {
121
+ return `${node.role}\n${node.name ?? ''}`;
122
+ }
123
+ /**
124
+ * Diff two signatures.
125
+ *
126
+ * Two passes, because path alone is too brittle to use on its own: inserting
127
+ * one list item renumbers every later sibling, which a naive path diff would
128
+ * report as "everything below here was replaced".
129
+ *
130
+ * 1. Exact path match — same slot. Same name means unchanged, a different
131
+ * name means renamed (the useful case: a label that silently changed).
132
+ * 2. Whatever is left is matched on role+name, which recovers elements that
133
+ * merely moved. Only what survives both passes is genuinely gained/lost.
134
+ */
135
+ export function diffStructure(previous, current) {
136
+ const prevByPath = new Map(previous.map((n) => [n.path, n]));
137
+ const currByPath = new Map(current.map((n) => [n.path, n]));
138
+ const renamed = [];
139
+ const unmatchedPrev = [];
140
+ const unmatchedCurr = [];
141
+ let unchanged = 0;
142
+ for (const node of current) {
143
+ const before = prevByPath.get(node.path);
144
+ if (!before) {
145
+ unmatchedCurr.push(node);
146
+ continue;
147
+ }
148
+ if (before.role === node.role && before.name === node.name) {
149
+ unchanged++;
150
+ }
151
+ else if (before.role === node.role) {
152
+ renamed.push({ path: node.path, role: node.role, from: before.name, to: node.name });
153
+ }
154
+ else {
155
+ // Same slot, different role — that is a replacement, not a rename.
156
+ unmatchedCurr.push(node);
157
+ unmatchedPrev.push(before);
158
+ }
159
+ }
160
+ for (const node of previous) {
161
+ if (!currByPath.has(node.path))
162
+ unmatchedPrev.push(node);
163
+ }
164
+ // Pass two: role+name identity, which is what "the same element, elsewhere"
165
+ // actually means to a reader.
166
+ const pool = new Map();
167
+ for (const node of unmatchedPrev) {
168
+ const key = identityKey(node);
169
+ const bucket = pool.get(key);
170
+ if (bucket)
171
+ bucket.push(node);
172
+ else
173
+ pool.set(key, [node]);
174
+ }
175
+ const moved = [];
176
+ const gained = [];
177
+ for (const node of unmatchedCurr) {
178
+ const bucket = pool.get(identityKey(node));
179
+ const match = bucket?.shift();
180
+ if (match) {
181
+ moved.push({ role: node.role, name: node.name, from: match.path, to: node.path });
182
+ }
183
+ else {
184
+ gained.push(node);
185
+ }
186
+ }
187
+ const lost = [...pool.values()].flat();
188
+ return {
189
+ gained,
190
+ lost,
191
+ renamed,
192
+ moved,
193
+ unchanged,
194
+ changed: gained.length + lost.length + renamed.length,
195
+ };
196
+ }
197
+ /** One line for a CI log: "3 gained, 1 lost, 2 renamed". */
198
+ export function summarizeStructureDiff(diff) {
199
+ const parts = [];
200
+ if (diff.gained.length)
201
+ parts.push(`${diff.gained.length} gained`);
202
+ if (diff.lost.length)
203
+ parts.push(`${diff.lost.length} lost`);
204
+ if (diff.renamed.length)
205
+ parts.push(`${diff.renamed.length} renamed`);
206
+ if (diff.moved.length)
207
+ parts.push(`${diff.moved.length} moved`);
208
+ return parts.length ? parts.join(', ') : 'structurally identical';
209
+ }
210
+ //# sourceMappingURL=structure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structure.js","sourceRoot":"","sources":["../../../src/report/structure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Trends across a URL's run history (RIG-13).
3
+ *
4
+ * The point is catching slow decay. A single run that renders in 2.4s looks
5
+ * fine; the same page at 2.0s nine runs ago is a regression nobody filed a
6
+ * bug for, because no individual run ever crossed the budget. So every series
7
+ * reports two deltas — against the previous run (what just changed) and
8
+ * against the oldest run in the window (what has been happening) — and the
9
+ * second one is what produces a headline.
10
+ *
11
+ * Pure: the tests feed arrays of history records.
12
+ */
13
+ import type { HistoryRun } from './history.js';
14
+ import type { Report, TrendReport } from './types.js';
15
+ type Unit = 'ms' | 'score' | 'count';
16
+ export declare function formatTrendValue(unit: Unit, value: number | null): string;
17
+ export declare function formatTrendDelta(unit: Unit, delta: number | null): string;
18
+ /**
19
+ * Compare `current` against prior runs for the same URL.
20
+ *
21
+ * `history` is oldest-first and must NOT include the current run; callers
22
+ * save the run after building the trend so a run never trends against itself.
23
+ */
24
+ export declare function buildTrend(current: Report, history: HistoryRun[]): TrendReport;
25
+ export {};
26
+ //# sourceMappingURL=trend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trend.d.ts","sourceRoot":"","sources":["../../../src/report/trend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,MAAM,EAA8B,WAAW,EAAe,MAAM,YAAY,CAAC;AAE/F,KAAK,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC;AAkDrC,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAOzE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAIzE;AA2FD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,WAAW,CAY9E"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Trends across a URL's run history (RIG-13).
3
+ *
4
+ * The point is catching slow decay. A single run that renders in 2.4s looks
5
+ * fine; the same page at 2.0s nine runs ago is a regression nobody filed a
6
+ * bug for, because no individual run ever crossed the budget. So every series
7
+ * reports two deltas — against the previous run (what just changed) and
8
+ * against the oldest run in the window (what has been happening) — and the
9
+ * second one is what produces a headline.
10
+ *
11
+ * Pure: the tests feed arrays of history records.
12
+ */
13
+ const SERIES = [
14
+ { key: 'lcp', label: 'LCP', unit: 'ms', read: (r) => r.vitals.lcp },
15
+ { key: 'fcp', label: 'FCP', unit: 'ms', read: (r) => r.vitals.fcp },
16
+ { key: 'cls', label: 'CLS', unit: 'score', read: (r) => r.vitals.cls },
17
+ { key: 'inp', label: 'INP', unit: 'ms', read: (r) => r.vitals.inp },
18
+ { key: 'ttfb', label: 'TTFB', unit: 'ms', read: (r) => r.vitals.ttfb },
19
+ {
20
+ key: 'consoleErrors',
21
+ label: 'Console errors',
22
+ unit: 'count',
23
+ read: (r) => r.counts.consoleErrors,
24
+ },
25
+ { key: 'pageErrors', label: 'Page errors', unit: 'count', read: (r) => r.counts.pageErrors },
26
+ {
27
+ key: 'failedRequests',
28
+ label: 'Failed requests',
29
+ unit: 'count',
30
+ read: (r) => r.counts.failedRequests,
31
+ },
32
+ {
33
+ key: 'a11yErrors',
34
+ label: 'Accessibility errors',
35
+ unit: 'count',
36
+ read: (r) => r.counts.a11yErrors,
37
+ },
38
+ ];
39
+ /**
40
+ * Smallest change worth calling a change. Chrome's own run-to-run variance on
41
+ * a paint metric is tens of milliseconds, so without a floor every series
42
+ * would report a direction every single run and the word would stop meaning
43
+ * anything.
44
+ */
45
+ function significant(unit, delta, base) {
46
+ const magnitude = Math.abs(delta);
47
+ if (unit === 'count')
48
+ return magnitude >= 1;
49
+ if (unit === 'score')
50
+ return magnitude >= 0.01;
51
+ const relative = base !== null && base > 0 ? base * 0.05 : 0;
52
+ return magnitude >= Math.max(25, relative);
53
+ }
54
+ export function formatTrendValue(unit, value) {
55
+ if (value === null || !Number.isFinite(value))
56
+ return '—';
57
+ if (unit === 'ms') {
58
+ return value >= 1000 ? `${(value / 1000).toFixed(2)}s` : `${Math.round(value)}ms`;
59
+ }
60
+ if (unit === 'score')
61
+ return value.toFixed(3);
62
+ return String(Math.round(value));
63
+ }
64
+ export function formatTrendDelta(unit, delta) {
65
+ if (delta === null || !Number.isFinite(delta) || delta === 0)
66
+ return 'no change';
67
+ const sign = delta > 0 ? '+' : '−';
68
+ return `${sign}${formatTrendValue(unit, Math.abs(delta))}`;
69
+ }
70
+ function clean(value) {
71
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
72
+ }
73
+ function buildSeries(def, history, current) {
74
+ const points = history.map((run) => ({
75
+ capturedAt: run.capturedAt,
76
+ value: clean(def.read(run)),
77
+ }));
78
+ const currentValue = clean(def.read({ vitals: current.vitals, counts: current.counts }));
79
+ points.push({ capturedAt: current.capturedAt, value: currentValue });
80
+ const priorWithValues = points.slice(0, -1).filter((p) => p.value !== null);
81
+ const previous = priorWithValues.length
82
+ ? priorWithValues[priorWithValues.length - 1].value
83
+ : null;
84
+ const oldest = priorWithValues.length ? priorWithValues[0].value : null;
85
+ const deltaFromPrevious = currentValue !== null && previous !== null ? currentValue - previous : null;
86
+ const deltaFromOldest = currentValue !== null && oldest !== null ? currentValue - oldest : null;
87
+ let direction = 'flat';
88
+ if (currentValue === null || previous === null) {
89
+ direction = 'new';
90
+ }
91
+ else if (deltaFromPrevious !== null && significant(def.unit, deltaFromPrevious, previous)) {
92
+ // Every metric here is lower-is-better, so the sign maps straight across.
93
+ direction = deltaFromPrevious > 0 ? 'regressed' : 'improved';
94
+ }
95
+ const series = {
96
+ key: def.key,
97
+ label: def.label,
98
+ unit: def.unit,
99
+ points,
100
+ current: currentValue,
101
+ previous,
102
+ oldest,
103
+ deltaFromPrevious,
104
+ deltaFromOldest,
105
+ direction,
106
+ };
107
+ // The creep sentence: drift across the whole window, which is invisible run
108
+ // to run. Needs at least three prior points to be worth asserting.
109
+ if (priorWithValues.length >= 3 &&
110
+ deltaFromOldest !== null &&
111
+ oldest !== null &&
112
+ significant(def.unit, deltaFromOldest, oldest)) {
113
+ const verb = deltaFromOldest > 0 ? 'crept up' : 'come down';
114
+ series.creep =
115
+ `${def.label} has ${verb} ${formatTrendValue(def.unit, Math.abs(deltaFromOldest))} ` +
116
+ `over ${points.length} runs (${formatTrendValue(def.unit, oldest)} → ${formatTrendValue(def.unit, currentValue)}).`;
117
+ }
118
+ return series;
119
+ }
120
+ /**
121
+ * Rank the creep sentences so the report leads with the worst. Regressions
122
+ * first, then by how far the metric moved relative to where it started.
123
+ */
124
+ function headlinesFor(series, history, current) {
125
+ const regressions = series
126
+ .filter((s) => s.creep && (s.deltaFromOldest ?? 0) > 0)
127
+ .sort((a, b) => {
128
+ const ratio = (s) => s.oldest && s.oldest > 0 ? Math.abs(s.deltaFromOldest ?? 0) / s.oldest : Infinity;
129
+ return ratio(b) - ratio(a);
130
+ })
131
+ .map((s) => s.creep);
132
+ const headlines = [...regressions];
133
+ // A verdict flip is the loudest thing that can happen, so it goes first.
134
+ const priorVerdicts = history.map((r) => r.verdict);
135
+ const allPassed = priorVerdicts.length > 0 && priorVerdicts.every((v) => v === 'pass');
136
+ const allFailed = priorVerdicts.length > 0 && priorVerdicts.every((v) => v === 'fail');
137
+ if (current.verdict === 'fail' && allPassed) {
138
+ headlines.unshift(`First failure in this window — the previous ${priorVerdicts.length} run(s) all passed.`);
139
+ }
140
+ else if (current.verdict === 'pass' && allFailed) {
141
+ headlines.unshift(`Back to passing — the previous ${priorVerdicts.length} run(s) all failed.`);
142
+ }
143
+ return headlines;
144
+ }
145
+ /**
146
+ * Compare `current` against prior runs for the same URL.
147
+ *
148
+ * `history` is oldest-first and must NOT include the current run; callers
149
+ * save the run after building the trend so a run never trends against itself.
150
+ */
151
+ export function buildTrend(current, history) {
152
+ const series = SERIES.map((def) => buildSeries(def, history, current));
153
+ return {
154
+ runs: history.length,
155
+ windowFrom: history.length ? history[0].capturedAt : null,
156
+ series,
157
+ verdictHistory: [
158
+ ...history.map((r) => ({ capturedAt: r.capturedAt, verdict: r.verdict })),
159
+ { capturedAt: current.capturedAt, verdict: current.verdict },
160
+ ],
161
+ headlines: headlinesFor(series, history, current),
162
+ };
163
+ }
164
+ //# sourceMappingURL=trend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trend.js","sourceRoot":"","sources":["../../../src/report/trend.ts"],"names":[],"mappings":"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"}