testaro 78.0.6 → 78.4.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 (361) hide show
  1. package/.gitattributes +94 -0
  2. package/.github/workflows/ci.yml +58 -0
  3. package/.github/workflows/publish.yml +49 -0
  4. package/.github/workflows/typescript.yml +35 -0
  5. package/AGENTS.md +2 -2
  6. package/CLAUDE.md +25 -19
  7. package/CONTAINERS.md +10 -6
  8. package/CONTRIBUTING.md +29 -3
  9. package/Dockerfile +1 -1
  10. package/README.md +75 -20
  11. package/UPGRADES.md +4 -0
  12. package/actSpecs-doc.md +13 -1
  13. package/actSpecs.js +29 -5
  14. package/call.js +6 -5
  15. package/docker-compose.yml +8 -2
  16. package/docs/checkpoint-scanning.md +199 -0
  17. package/docs/standard-result-outcome.md +165 -0
  18. package/env.example +44 -10
  19. package/eslint.config.mjs +101 -0
  20. package/netWatch.js +373 -222
  21. package/package.json +46 -31
  22. package/pour/README.md +47 -0
  23. package/pour/pour.min.js +8 -0
  24. package/procs/actDo.js +808 -0
  25. package/procs/catalog.d.ts +17 -0
  26. package/procs/catalog.js +313 -221
  27. package/procs/catalog.ts +398 -0
  28. package/procs/checkpoint.js +110 -0
  29. package/procs/config.d.ts +35 -0
  30. package/procs/config.js +63 -0
  31. package/procs/dateTime.js +2 -1
  32. package/procs/doActs.js +442 -849
  33. package/procs/doTestAct.js +27 -121
  34. package/procs/flow.js +221 -0
  35. package/procs/generateRuleRegistry.js +94 -0
  36. package/procs/getSource.d.ts +26 -0
  37. package/procs/getSource.js +1 -0
  38. package/procs/job.js +116 -1
  39. package/procs/launch.d.ts +40 -0
  40. package/procs/launch.js +239 -108
  41. package/procs/nu.d.ts +48 -0
  42. package/procs/nu.js +1 -0
  43. package/procs/scope.js +196 -0
  44. package/procs/shoot.d.ts +33 -0
  45. package/procs/shoot.js +3 -1
  46. package/procs/standard.d.ts +18 -0
  47. package/procs/standard.js +83 -0
  48. package/procs/standard.ts +126 -0
  49. package/procs/testAct.js +145 -0
  50. package/procs/testaro.d.ts +20 -0
  51. package/procs/testaro.js +237 -189
  52. package/procs/testaro.ts +321 -0
  53. package/procs/userPath.js +126 -0
  54. package/procs/xPath.d.ts +4 -0
  55. package/procs/xPath.js +82 -64
  56. package/procs/xPath.ts +106 -0
  57. package/procs/xPathScript.d.ts +6 -0
  58. package/procs/xPathScript.js +70 -0
  59. package/run.js +3 -2
  60. package/surea11y/README.md +44 -0
  61. package/surea11y/surea11y.browser.js +14 -0
  62. package/testaro/adbID.d.ts +3 -0
  63. package/testaro/adbID.js +39 -41
  64. package/testaro/adbID.ts +66 -0
  65. package/testaro/allCapStyle.d.ts +3 -0
  66. package/testaro/allCapStyle.js +32 -34
  67. package/testaro/allCapStyle.ts +55 -0
  68. package/testaro/allCaps.d.ts +16 -0
  69. package/testaro/allCaps.js +179 -151
  70. package/testaro/allCaps.ts +228 -0
  71. package/testaro/allHidden.d.ts +13 -0
  72. package/testaro/allHidden.js +30 -30
  73. package/testaro/allHidden.ts +50 -0
  74. package/testaro/allSlanted.d.ts +3 -0
  75. package/testaro/allSlanted.js +31 -33
  76. package/testaro/allSlanted.ts +54 -0
  77. package/testaro/altScheme.d.ts +3 -0
  78. package/testaro/altScheme.js +28 -30
  79. package/testaro/altScheme.ts +50 -0
  80. package/testaro/attVal.d.ts +3 -0
  81. package/testaro/attVal.js +21 -21
  82. package/testaro/attVal.ts +52 -0
  83. package/testaro/autocomplete.d.ts +3 -0
  84. package/testaro/autocomplete.js +60 -75
  85. package/testaro/autocomplete.ts +101 -0
  86. package/testaro/bulk.d.ts +13 -0
  87. package/testaro/bulk.js +33 -33
  88. package/testaro/bulk.ts +55 -0
  89. package/testaro/buttonMenu.d.ts +9 -0
  90. package/testaro/buttonMenu.js +318 -319
  91. package/testaro/buttonMenu.ts +391 -0
  92. package/testaro/captionLoc.d.ts +3 -0
  93. package/testaro/captionLoc.js +18 -20
  94. package/testaro/captionLoc.ts +40 -0
  95. package/testaro/datalistRef.d.ts +3 -0
  96. package/testaro/datalistRef.js +34 -36
  97. package/testaro/datalistRef.ts +55 -0
  98. package/testaro/distortion.d.ts +3 -0
  99. package/testaro/distortion.js +58 -26
  100. package/testaro/distortion.ts +81 -0
  101. package/testaro/docType.d.ts +15 -0
  102. package/testaro/docType.js +26 -25
  103. package/testaro/docType.ts +45 -0
  104. package/testaro/dupAtt.d.ts +16 -0
  105. package/testaro/dupAtt.js +114 -104
  106. package/testaro/dupAtt.ts +144 -0
  107. package/testaro/elements.d.ts +6 -0
  108. package/testaro/elements.js +153 -153
  109. package/testaro/elements.ts +215 -0
  110. package/testaro/embAc.d.ts +3 -0
  111. package/testaro/embAc.js +20 -20
  112. package/testaro/embAc.ts +40 -0
  113. package/testaro/focAll.d.ts +13 -0
  114. package/testaro/focAll.js +186 -191
  115. package/testaro/focAll.ts +217 -0
  116. package/testaro/focAndOp.d.ts +3 -0
  117. package/testaro/focAndOp.js +101 -104
  118. package/testaro/focAndOp.ts +128 -0
  119. package/testaro/focInd.d.ts +3 -0
  120. package/testaro/focInd.js +65 -66
  121. package/testaro/focInd.ts +96 -0
  122. package/testaro/focVis.d.ts +3 -0
  123. package/testaro/focVis.js +29 -30
  124. package/testaro/focVis.ts +52 -0
  125. package/testaro/headEl.d.ts +10 -0
  126. package/testaro/headEl.js +62 -62
  127. package/testaro/headEl.ts +82 -0
  128. package/testaro/headingAmb.d.ts +3 -0
  129. package/testaro/headingAmb.js +55 -63
  130. package/testaro/headingAmb.ts +72 -0
  131. package/testaro/hovInd.d.ts +12 -0
  132. package/testaro/hovInd.js +164 -130
  133. package/testaro/hovInd.ts +199 -0
  134. package/testaro/hover.d.ts +3 -0
  135. package/testaro/hover.js +156 -126
  136. package/testaro/hover.ts +154 -0
  137. package/testaro/hr.d.ts +3 -0
  138. package/testaro/hr.js +15 -17
  139. package/testaro/hr.ts +36 -0
  140. package/testaro/imageLink.d.ts +3 -0
  141. package/testaro/imageLink.js +18 -20
  142. package/testaro/imageLink.ts +42 -0
  143. package/testaro/labClash.d.ts +3 -0
  144. package/testaro/labClash.js +32 -33
  145. package/testaro/labClash.ts +54 -0
  146. package/testaro/legendLoc.d.ts +3 -0
  147. package/testaro/legendLoc.js +18 -20
  148. package/testaro/legendLoc.ts +42 -0
  149. package/testaro/lineHeight.d.ts +3 -0
  150. package/testaro/lineHeight.js +44 -48
  151. package/testaro/lineHeight.ts +70 -0
  152. package/testaro/linkAmb.d.ts +7 -0
  153. package/testaro/linkAmb.js +80 -80
  154. package/testaro/linkAmb.ts +105 -0
  155. package/testaro/linkExt.d.ts +3 -0
  156. package/testaro/linkExt.js +14 -16
  157. package/testaro/linkExt.ts +35 -0
  158. package/testaro/linkOldAtt.d.ts +3 -0
  159. package/testaro/linkOldAtt.js +26 -28
  160. package/testaro/linkOldAtt.ts +48 -0
  161. package/testaro/linkTo.d.ts +3 -0
  162. package/testaro/linkTo.js +23 -22
  163. package/testaro/linkTo.ts +43 -0
  164. package/testaro/linkUl.d.ts +3 -0
  165. package/testaro/linkUl.js +31 -34
  166. package/testaro/linkUl.ts +54 -0
  167. package/testaro/miniText.d.ts +3 -0
  168. package/testaro/miniText.js +42 -44
  169. package/testaro/miniText.ts +68 -0
  170. package/testaro/motion.d.ts +10 -0
  171. package/testaro/motion.js +93 -96
  172. package/testaro/motion.ts +125 -0
  173. package/testaro/nonTable.d.ts +3 -0
  174. package/testaro/nonTable.js +40 -45
  175. package/testaro/nonTable.ts +66 -0
  176. package/testaro/optRoleSel.d.ts +3 -0
  177. package/testaro/optRoleSel.js +17 -19
  178. package/testaro/optRoleSel.ts +41 -0
  179. package/testaro/phOnly.d.ts +3 -0
  180. package/testaro/phOnly.js +19 -21
  181. package/testaro/phOnly.ts +43 -0
  182. package/testaro/pseudoP.d.ts +3 -0
  183. package/testaro/pseudoP.js +37 -38
  184. package/testaro/pseudoP.ts +59 -0
  185. package/testaro/radioSet.d.ts +3 -0
  186. package/testaro/radioSet.js +58 -59
  187. package/testaro/radioSet.ts +79 -0
  188. package/testaro/registry.d.ts +63 -0
  189. package/testaro/registry.js +67 -0
  190. package/testaro/registry.ts +141 -0
  191. package/testaro/role.d.ts +3 -0
  192. package/testaro/role.js +28 -29
  193. package/testaro/role.ts +53 -0
  194. package/testaro/secHeading.d.ts +3 -0
  195. package/testaro/secHeading.js +31 -33
  196. package/testaro/secHeading.ts +53 -0
  197. package/testaro/styleDiff.d.ts +25 -0
  198. package/testaro/styleDiff.js +249 -252
  199. package/testaro/styleDiff.ts +303 -0
  200. package/testaro/tabNav.d.ts +33 -0
  201. package/testaro/tabNav.js +273 -342
  202. package/testaro/tabNav.ts +454 -0
  203. package/testaro/targetsNear.d.ts +9 -0
  204. package/testaro/targetsNear.js +131 -132
  205. package/testaro/targetsNear.ts +160 -0
  206. package/testaro/textNodes.d.ts +6 -0
  207. package/testaro/textNodes.js +139 -135
  208. package/testaro/textNodes.ts +185 -0
  209. package/testaro/textSem.d.ts +3 -0
  210. package/testaro/textSem.js +26 -28
  211. package/testaro/textSem.ts +47 -0
  212. package/testaro/title.d.ts +9 -0
  213. package/testaro/title.js +16 -13
  214. package/testaro/title.ts +31 -0
  215. package/testaro/titledEl.d.ts +3 -0
  216. package/testaro/titledEl.js +16 -18
  217. package/testaro/titledEl.ts +38 -0
  218. package/testaro/zIndex.d.ts +3 -0
  219. package/testaro/zIndex.js +20 -22
  220. package/testaro/zIndex.ts +42 -0
  221. package/tests/alfa.d.ts +45 -0
  222. package/tests/alfa.js +139 -141
  223. package/tests/alfa.ts +214 -0
  224. package/tests/aslint.d.ts +33 -0
  225. package/tests/aslint.js +273 -249
  226. package/tests/aslint.ts +301 -0
  227. package/tests/axe.d.ts +27 -0
  228. package/tests/axe.js +200 -200
  229. package/tests/axe.ts +277 -0
  230. package/tests/ed11y.d.ts +28 -0
  231. package/tests/ed11y.js +142 -99
  232. package/tests/ed11y.ts +178 -0
  233. package/tests/htmlcs.d.ts +21 -0
  234. package/tests/htmlcs.js +175 -140
  235. package/tests/htmlcs.ts +181 -0
  236. package/tests/ibm.d.ts +52 -0
  237. package/tests/ibm.js +165 -166
  238. package/tests/ibm.ts +251 -0
  239. package/tests/nuVal.d.ts +13 -0
  240. package/tests/nuVal.js +107 -112
  241. package/tests/nuVal.ts +145 -0
  242. package/tests/nuVnu.d.ts +15 -0
  243. package/tests/nuVnu.js +142 -111
  244. package/tests/nuVnu.ts +144 -0
  245. package/tests/pour.d.ts +31 -0
  246. package/tests/pour.js +242 -0
  247. package/tests/pour.ts +273 -0
  248. package/tests/qualWeb.d.ts +39 -0
  249. package/tests/qualWeb.js +303 -272
  250. package/tests/qualWeb.ts +415 -0
  251. package/tests/surea11y.d.ts +33 -0
  252. package/tests/surea11y.js +288 -0
  253. package/tests/surea11y.ts +334 -0
  254. package/tests/testaro.d.ts +25 -0
  255. package/tests/testaro.js +746 -652
  256. package/tests/testaro.ts +862 -0
  257. package/tests/wave.d.ts +46 -0
  258. package/tests/wave.js +167 -177
  259. package/tests/wave.ts +252 -0
  260. package/tsconfig.json +17 -0
  261. package/types.d.ts +243 -0
  262. package/types.js +10 -0
  263. package/types.ts +376 -0
  264. package/validation/act/README.md +46 -0
  265. package/validation/act/capture.js +424 -0
  266. package/validation/act/chromium-issue-draft.md +66 -0
  267. package/validation/act/fp-triage-2026-08-22.md +86 -0
  268. package/validation/act/isolation-notes.md +159 -0
  269. package/validation/act/playwright-issue-draft.md +73 -0
  270. package/validation/act/propose-mappings.js +0 -0
  271. package/validation/act/repro-cdp-raw.js +105 -0
  272. package/validation/act/repro-metarefresh.js +65 -0
  273. package/validation/act/score.js +184 -0
  274. package/validation/act/stage3a-stress-report.md +69 -0
  275. package/validation/act/stage3b-mapping-proposals.md +94 -0
  276. package/validation/act/stage3b-triage-draft.md +138 -0
  277. package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
  278. package/validation/executors/netWatch.js +180 -90
  279. package/validation/executors/test.js +17 -2
  280. package/validation/executors/tests.js +118 -10
  281. package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
  282. package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
  283. package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
  284. package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
  285. package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
  286. package/validation/knownFailures.json +1 -0
  287. package/validation/tests/jobProperties/adbID.json +27 -2
  288. package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
  289. package/validation/tests/jobProperties/allCaps.json +25 -0
  290. package/validation/tests/jobProperties/allHidden.json +133 -13
  291. package/validation/tests/jobProperties/allSlanted.json +3 -3
  292. package/validation/tests/jobProperties/altScheme.json +23 -8
  293. package/validation/tests/jobProperties/attVal.json +57 -57
  294. package/validation/tests/jobProperties/autocomplete.json +11 -1
  295. package/validation/tests/jobProperties/bulk.json +6 -1
  296. package/validation/tests/jobProperties/buttonMenu.json +63 -42
  297. package/validation/tests/jobProperties/captionLoc.json +1 -6
  298. package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
  299. package/validation/tests/jobProperties/checkpoint-page.json +397 -0
  300. package/validation/tests/jobProperties/checkpoint.json +395 -0
  301. package/validation/tests/jobProperties/datalistRef.json +20 -5
  302. package/validation/tests/jobProperties/distortion.json +28 -3
  303. package/validation/tests/jobProperties/docType.json +2 -2
  304. package/validation/tests/jobProperties/dupAtt.json +48 -33
  305. package/validation/tests/jobProperties/elements.json +28 -28
  306. package/validation/tests/jobProperties/embAc.json +36 -31
  307. package/validation/tests/jobProperties/focAndOp.json +284 -0
  308. package/validation/tests/jobProperties/focInd.json +34 -34
  309. package/validation/tests/jobProperties/focVis.json +1 -1
  310. package/validation/tests/jobProperties/hover.json +39 -37
  311. package/validation/tests/jobProperties/hr.json +13 -3
  312. package/validation/tests/jobProperties/imageLink.json +0 -5
  313. package/validation/tests/jobProperties/labClash.json +68 -33
  314. package/validation/tests/jobProperties/legendLoc.json +1 -6
  315. package/validation/tests/jobProperties/lineHeight.json +2 -2
  316. package/validation/tests/jobProperties/linkAmb.json +13 -18
  317. package/validation/tests/jobProperties/linkExt.json +1 -1
  318. package/validation/tests/jobProperties/linkOldAtt.json +12 -2
  319. package/validation/tests/jobProperties/linkTo.json +1 -1
  320. package/validation/tests/jobProperties/linkUl.json +65 -65
  321. package/validation/tests/jobProperties/miniText.json +7 -2
  322. package/validation/tests/jobProperties/motion.json +4 -50
  323. package/validation/tests/jobProperties/nonTable.json +48 -3
  324. package/validation/tests/jobProperties/optRoleSel.json +12 -2
  325. package/validation/tests/jobProperties/phOnly.json +6 -16
  326. package/validation/tests/jobProperties/pseudoP.json +17 -2
  327. package/validation/tests/jobProperties/radioSet.json +30 -30
  328. package/validation/tests/jobProperties/role.json +25 -5
  329. package/validation/tests/jobProperties/secHeading.json +26 -21
  330. package/validation/tests/jobProperties/styleDiff.json +35 -5
  331. package/validation/tests/jobProperties/tabNav.json +3 -1
  332. package/validation/tests/jobProperties/targetsNear.json +237 -0
  333. package/validation/tests/jobProperties/textNodes.json +29 -29
  334. package/validation/tests/jobProperties/textSem.json +26 -1
  335. package/validation/tests/jobProperties/title.json +12 -2
  336. package/validation/tests/jobProperties/titledEl.json +39 -9
  337. package/validation/tests/jobProperties/userPath.json +317 -0
  338. package/validation/tests/jobProperties/zIndex.json +40 -35
  339. package/validation/tests/targets/allCapStyle/index.html +26 -0
  340. package/validation/tests/targets/checkpoint/index.html +38 -0
  341. package/validation/tests/targets/datalistRef/index.html +1 -1
  342. package/validation/tests/targets/focAndOp/bad.html +29 -0
  343. package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
  344. package/validation/tests/targets/focInd/bad.html +2 -1
  345. package/validation/tests/targets/headEl/index.html +10 -1
  346. package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
  347. package/validation/tests/targets/userPath/index.html +42 -0
  348. package/validation/validateTest.js +62 -10
  349. package/.claude/settings.local.json +0 -11
  350. package/.eslintrc.json +0 -41
  351. package/htmlcs/.eslintrc.json +0 -67
  352. package/memory/MEMORY.md +0 -3
  353. package/memory/project_validation_pause.md +0 -10
  354. package/validation/tests/jobProperties/linkTitle.json +0 -127
  355. package/validation/tests/jobProperties/opFoc.json +0 -164
  356. package/validation/tests/jobProperties/targetSmall.json +0 -152
  357. package/validation/tests/jobProperties/targetTiny.json +0 -142
  358. package/validation/tests/targets/focOp/bad.html +0 -25
  359. package/validation/tests/targets/linkTitle/index.html +0 -24
  360. package/validation/tests/targets/opFoc/bad.html +0 -26
  361. package/validation/tests/targets/opFoc/good.html +0 -23
package/procs/scope.js ADDED
@@ -0,0 +1,196 @@
1
+ /*
2
+ © 2026 Jeff Witt.
3
+
4
+ Licensed under the MIT License. See LICENSE file at the project root or
5
+ https://opensource.org/license/mit/ for details.
6
+
7
+ SPDX-License-Identifier: MIT
8
+ */
9
+
10
+ /*
11
+ scope.js
12
+ Finds what changed between two checkpoints of a report, by comparing their catalog entries,
13
+ and reduces the changes to the smallest set of subtree roots that contains them. The roots
14
+ serve test acts with scope: 'changed' (docs/checkpoint-scanning.md, Phase 3) and the
15
+ structure diffs of report.flow (Phase 2).
16
+ */
17
+
18
+ // CONSTANTS
19
+
20
+ // The most roots a changed-scope test act may be given; above this, the whole page is tested.
21
+ const maxRoots = exports.maxRoots = 50;
22
+
23
+ // FUNCTIONS
24
+
25
+ // Returns whether one XPath (as the catalog records them) is a strict descendant of another.
26
+ const isDescendant = (pathID, ancestorPathID) => pathID.startsWith(`${ancestorPathID}/`);
27
+ // Returns the XPath of the parent of an element, or '' for the root.
28
+ const parentOf = pathID => pathID.slice(0, pathID.lastIndexOf('/')) || '';
29
+ // Returns the catalog entries of a checkpoint, keyed by XPath.
30
+ const getEntriesByPath = (report, checkpointIndex) => {
31
+ const entries = {};
32
+ const checkpoint = (report.checkpoints ?? [])[checkpointIndex];
33
+ const range = checkpoint && checkpoint.catalogRange;
34
+ const catalog = report.catalog ?? {};
35
+ // Read the checkpoint's index range if recorded, else every entry stamped with the checkpoint.
36
+ const keys = range
37
+ ? Array.from({length: range[1] - range[0] + 1}, (_, offset) => String(range[0] + offset))
38
+ : Object.keys(catalog).filter(key => catalog[key].checkpoint === checkpointIndex);
39
+ keys.forEach(key => {
40
+ const entry = catalog[key];
41
+ if (entry && entry.pathID && entry.checkpoint === checkpointIndex) {
42
+ entries[entry.pathID] = entry;
43
+ }
44
+ });
45
+ return entries;
46
+ };
47
+ /*
48
+ Converts an XPath of the form the catalog records (/html/body/main[1]/div[2]) to a CSS
49
+ selector (html > body > main:nth-of-type(1) > div:nth-of-type(2)). The conversion is exact
50
+ because window.getXPath subscripts each element among its same-tag siblings, which is what
51
+ nth-of-type counts. Returns '' if the XPath has an unexpected shape.
52
+ */
53
+ const xPathToSelector = exports.xPathToSelector = pathID => {
54
+ if (typeof pathID !== 'string' || ! pathID.startsWith('/')) {
55
+ return '';
56
+ }
57
+ const segments = pathID.slice(1).split('/');
58
+ const parts = [];
59
+ for (const segment of segments) {
60
+ const match = segment.match(/^([a-z][a-z0-9-]*)(?:\[(\d+)\])?$/);
61
+ if (! match) {
62
+ return '';
63
+ }
64
+ parts.push(match[2] ? `${match[1]}:nth-of-type(${match[2]})` : match[1]);
65
+ }
66
+ return parts.join(' > ');
67
+ };
68
+ /*
69
+ Compares the catalog entries of two checkpoints and returns the difference:
70
+ added: XPaths present only in the later checkpoint
71
+ removed: XPaths present only in the earlier one
72
+ changed: XPaths present in both whose start tags differ
73
+ textChanged: XPaths present in both, with the same start tag, whose text differs
74
+ roots: the smallest set of XPaths, in the later checkpoint, whose subtrees contain every
75
+ change (the parent of a removed element stands for it; a text change counts only where
76
+ no descendant changed, because the catalog text of an element includes its descendants')
77
+ counts: the sizes of the above
78
+ */
79
+ const getStructureDiff = exports.getStructureDiff = (report, fromIndex, toIndex) => {
80
+ const before = getEntriesByPath(report, fromIndex);
81
+ const after = getEntriesByPath(report, toIndex);
82
+ const added = [];
83
+ const removed = [];
84
+ const changed = [];
85
+ const textChanged = [];
86
+ Object.keys(after).forEach(pathID => {
87
+ const entry = after[pathID];
88
+ const priorEntry = before[pathID];
89
+ if (! priorEntry) {
90
+ added.push(pathID);
91
+ }
92
+ else if ((priorEntry.startTag ?? '') !== (entry.startTag ?? '')) {
93
+ changed.push(pathID);
94
+ }
95
+ else if ((priorEntry.text ?? '') !== (entry.text ?? '')) {
96
+ textChanged.push(pathID);
97
+ }
98
+ });
99
+ Object.keys(before).forEach(pathID => {
100
+ if (! after[pathID]) {
101
+ removed.push(pathID);
102
+ }
103
+ });
104
+ // Elements whose own change (not their descendants') marks them as changed.
105
+ const structural = new Set([... added, ... changed]);
106
+ removed.forEach(pathID => {
107
+ // The nearest surviving ancestor stands for a removed element.
108
+ let ancestor = parentOf(pathID);
109
+ while (ancestor && ! after[ancestor]) {
110
+ ancestor = parentOf(ancestor);
111
+ }
112
+ if (ancestor) {
113
+ structural.add(ancestor);
114
+ }
115
+ });
116
+ const allChanged = new Set([... structural, ... textChanged]);
117
+ // A text change counts only where no descendant changed at all.
118
+ textChanged.forEach(pathID => {
119
+ const hasChangedDescendant = Array.from(allChanged).some(other => isDescendant(other, pathID));
120
+ if (! hasChangedDescendant) {
121
+ structural.add(pathID);
122
+ }
123
+ });
124
+ // Collapse the changed elements to their outermost members.
125
+ const roots = Array.from(structural)
126
+ .filter(pathID => ! Array.from(structural).some(other => other !== pathID && isDescendant(pathID, other)))
127
+ .sort();
128
+ return {
129
+ added,
130
+ removed,
131
+ changed,
132
+ textChanged,
133
+ roots,
134
+ counts: {
135
+ before: Object.keys(before).length,
136
+ after: Object.keys(after).length,
137
+ added: added.length,
138
+ removed: removed.length,
139
+ changed: changed.length,
140
+ textChanged: textChanged.length,
141
+ roots: roots.length
142
+ }
143
+ };
144
+ };
145
+ // Returns the XPath of the nearest common ancestor-or-self of XPaths.
146
+ const getCommonAncestor = exports.getCommonAncestor = pathIDs => {
147
+ if (! pathIDs.length) {
148
+ return '';
149
+ }
150
+ let common = pathIDs[0].split('/');
151
+ pathIDs.slice(1).forEach(pathID => {
152
+ const segments = pathID.split('/');
153
+ let length = 0;
154
+ while (length < common.length && length < segments.length && common[length] === segments[length]) {
155
+ length++;
156
+ }
157
+ common = common.slice(0, length);
158
+ });
159
+ return common.join('/') || '/html';
160
+ };
161
+ /*
162
+ Returns the subtree roots a changed-scope test act at a checkpoint is to test: the CSS
163
+ selectors of the roots of the structure diff between the checkpoint and the previous one.
164
+ applied is false, with a reason, when the act cannot be scoped: no previous checkpoint, no
165
+ change at all, too many roots (maxRoots), or a root whose XPath cannot become a selector.
166
+ */
167
+ exports.getChangedRoots = (report, checkpointIndex) => {
168
+ if (! Number.isInteger(checkpointIndex) || checkpointIndex < 1) {
169
+ return {applied: false, reason: 'no previous checkpoint to compare', roots: [], pathIDs: []};
170
+ }
171
+ // Use the diff recorded when the checkpoint was made (the previous checkpoint's uncited
172
+ // entries were pruned then), else compute it.
173
+ const checkpoint = (report.checkpoints ?? [])[checkpointIndex];
174
+ const diff = checkpoint && checkpoint.structure
175
+ ? checkpoint.structure
176
+ : getStructureDiff(report, checkpointIndex - 1, checkpointIndex);
177
+ const {roots: pathIDs} = diff;
178
+ if (! pathIDs.length) {
179
+ return {applied: false, reason: 'no change since the previous checkpoint', roots: [], pathIDs};
180
+ }
181
+ if (pathIDs.length > maxRoots) {
182
+ return {
183
+ applied: false,
184
+ reason: `${pathIDs.length} changed subtrees exceed the limit of ${maxRoots}`,
185
+ roots: [],
186
+ pathIDs
187
+ };
188
+ }
189
+ const roots = pathIDs.map(xPathToSelector);
190
+ if (roots.some(root => ! root)) {
191
+ return {applied: false, reason: 'a changed subtree has no CSS selector', roots: [], pathIDs};
192
+ }
193
+ // The nearest ancestor of all the roots, for tools that take one root only.
194
+ const commonPathID = getCommonAncestor(pathIDs);
195
+ return {applied: true, reason: '', roots, pathIDs, commonRoot: xPathToSelector(commonPathID)};
196
+ };
@@ -0,0 +1,33 @@
1
+ /*
2
+ © 2026 Jeff Witt.
3
+
4
+ Licensed under the MIT License. See LICENSE file at the project root or
5
+ https://opensource.org/license/mit/ for details.
6
+
7
+ SPDX-License-Identifier: MIT
8
+ */
9
+
10
+ /*
11
+ shoot (hand-written declarations)
12
+ Minimal typings for the surface of procs/shoot.js that converted TypeScript
13
+ files use. Delete this file when shoot.js itself is converted (issue #73).
14
+ */
15
+
16
+ import type {Page} from 'playwright';
17
+ import type {Report} from '../types';
18
+
19
+ export interface ShootOptions {
20
+ exclusionSelector?: string | null;
21
+ // Color fidelity: 0 (grayscale), 2 (RGB), 4 (grayscale alpha), 6 (RGBA).
22
+ colorType?: number;
23
+ // Disposition: return, report, file.
24
+ action?: 'return' | 'report' | 'file';
25
+ // Screenshot scale.
26
+ scale?: 'css' | 'device';
27
+ }
28
+
29
+ // Makes a screenshot of a page and returns, reports, or files it. With the return action,
30
+ // the resolution is the base64 encoding of the screenshot, or a falsy value on failure.
31
+ export function shoot(
32
+ page: Page, report: Report, options?: ShootOptions
33
+ ): Promise<string | null | undefined>;
package/procs/shoot.js CHANGED
@@ -57,7 +57,9 @@ exports.shoot = async (page, report, {
57
57
  scale = 'css'
58
58
  } = {}) => {
59
59
  // Make and get a screenshot as a buffer.
60
- let shot = await screenShot(page, exclusionSelector ? page.locator(exclusionSelector) : null, scale);
60
+ let shot = await screenShot(
61
+ page, exclusionSelector ? page.locator(exclusionSelector) : null, scale
62
+ );
61
63
  // If it succeeded:
62
64
  if (shot.length) {
63
65
  // Get the screenshot as an object representation of a PNG image.
@@ -0,0 +1,18 @@
1
+ import type { Outcome, StandardInstance, StandardResult, UncertaintyCode } from '../types';
2
+ export interface InstanceSpec {
3
+ ruleID: string;
4
+ what: string;
5
+ ordinalSeverity: number;
6
+ outcome?: Outcome;
7
+ uncertainty?: string;
8
+ needed?: string;
9
+ count?: number;
10
+ catalogIndex?: string | number;
11
+ pathID?: string;
12
+ }
13
+ export declare const OUTCOMES: Outcome[];
14
+ export declare const UNCERTAINTY_CODES: UncertaintyCode[];
15
+ export declare const getStandardResult: () => StandardResult;
16
+ export declare const getInstance: (spec: InstanceSpec) => StandardInstance;
17
+ export declare const pushInstance: (standardResult: StandardResult, spec: InstanceSpec) => StandardInstance;
18
+ export declare const addInstance: (standardResult: StandardResult, spec: InstanceSpec) => StandardInstance;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ /*
3
+ © 2026 Jeff Witt.
4
+
5
+ Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
6
+
7
+ SPDX-License-Identifier: MIT
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.addInstance = exports.pushInstance = exports.getInstance = exports.getStandardResult = exports.UNCERTAINTY_CODES = exports.OUTCOMES = void 0;
11
+ // ########## CONSTANTS
12
+ // Outcomes of instances.
13
+ exports.OUTCOMES = ['failed', 'cantTell'];
14
+ // Reasons for cantTell outcomes.
15
+ exports.UNCERTAINTY_CODES = [
16
+ 'not-computable',
17
+ 'judgement-required',
18
+ 'runtime-dependent',
19
+ 'spec-only',
20
+ 'equivalence-unknown',
21
+ 'out-of-scope'
22
+ ];
23
+ // Maximum length of a needed description.
24
+ const NEEDED_MAX_LENGTH = 300;
25
+ // ########## FUNCTIONS
26
+ // Returns an empty standard result.
27
+ const getStandardResult = () => ({
28
+ prevented: false,
29
+ totals: [0, 0, 0, 0],
30
+ outcomeTotals: { failed: 0, cantTell: 0 },
31
+ instances: []
32
+ });
33
+ exports.getStandardResult = getStandardResult;
34
+ // Returns a standard instance, validating its certainty properties.
35
+ const getInstance = (spec) => {
36
+ const { ruleID, what, ordinalSeverity, outcome = 'failed', uncertainty, needed, count = 1, catalogIndex, pathID } = spec;
37
+ if (!exports.OUTCOMES.includes(outcome)) {
38
+ throw new Error(`Invalid outcome ${outcome} for rule ${ruleID}`);
39
+ }
40
+ const instance = {
41
+ ruleID,
42
+ what,
43
+ ordinalSeverity: Number(ordinalSeverity),
44
+ outcome
45
+ };
46
+ // If the outcome is cantTell, add any reason and reviewer guidance.
47
+ if (outcome === 'cantTell') {
48
+ if (uncertainty) {
49
+ if (!exports.UNCERTAINTY_CODES.includes(uncertainty)) {
50
+ throw new Error(`Invalid uncertainty ${uncertainty} for rule ${ruleID}`);
51
+ }
52
+ instance.uncertainty = uncertainty;
53
+ }
54
+ if (needed) {
55
+ instance.needed = String(needed).replace(/\s+/g, ' ').trim().slice(0, NEEDED_MAX_LENGTH);
56
+ }
57
+ }
58
+ instance.count = count;
59
+ if (catalogIndex !== undefined) {
60
+ instance.catalogIndex = catalogIndex;
61
+ }
62
+ else if (pathID) {
63
+ instance.pathID = pathID;
64
+ }
65
+ return instance;
66
+ };
67
+ exports.getInstance = getInstance;
68
+ // Adds an instance to a standard result and increments its outcome total.
69
+ const pushInstance = (standardResult, spec) => {
70
+ const instance = (0, exports.getInstance)(spec);
71
+ standardResult.outcomeTotals ??= { failed: 0, cantTell: 0 };
72
+ standardResult.outcomeTotals[instance.outcome] += instance.count;
73
+ standardResult.instances.push(instance);
74
+ return instance;
75
+ };
76
+ exports.pushInstance = pushInstance;
77
+ // Adds an instance to a standard result and increments its severity and outcome totals.
78
+ const addInstance = (standardResult, spec) => {
79
+ const instance = (0, exports.pushInstance)(standardResult, spec);
80
+ standardResult.totals[instance.ordinalSeverity] += instance.count;
81
+ return instance;
82
+ };
83
+ exports.addInstance = addInstance;
@@ -0,0 +1,126 @@
1
+ /*
2
+ © 2026 Jeff Witt.
3
+
4
+ Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
5
+
6
+ SPDX-License-Identifier: MIT
7
+ */
8
+
9
+ // ########## IMPORTS
10
+
11
+ import type {Outcome, StandardInstance, StandardResult, UncertaintyCode} from '../types';
12
+
13
+ /*
14
+ standard
15
+ Utilities for building standard results. Compiled to standard.js by tsc (issue #73); edit
16
+ this file, not the emitted one.
17
+
18
+ A standard result has these properties:
19
+ prevented: whether the rule engine was prevented from performing the act
20
+ totals: array of 4 integers, counting instances at ordinal severities 0 through 3
21
+ outcomeTotals: object counting instances by outcome ({failed, cantTell})
22
+ instances: array of standard instances
23
+
24
+ A standard instance has these properties:
25
+ ruleID: ID of the violated rule
26
+ what: description of the rule or of the violation
27
+ ordinalSeverity: severity of the violation (0 to 3)
28
+ outcome: 'failed' if the rule engine asserted a violation, or 'cantTell' if it reported that it could not determine whether the rule was violated (ACT Rules Format vocabulary)
29
+ uncertainty?: reason for a cantTell outcome, if the rule engine gave one (one of UNCERTAINTY_CODES)
30
+ needed?: what a reviewer must determine to resolve a cantTell outcome, if the rule engine said
31
+ count: how many violations of the rule the instance reports
32
+ catalogIndex?: key of the violating element in the catalog
33
+ pathID?: normalized XPath of the violating element, if it has no catalog index
34
+
35
+ The outcome is the authoritative certainty signal. Until ordinalSeverity is redefined as impact only, each tool's ordinalSeverity conventions are unchanged, so consumers must not infer certainty from ordinalSeverity.
36
+ */
37
+
38
+ // ########## TYPES
39
+
40
+ // The properties from which a standard instance is built.
41
+ export interface InstanceSpec {
42
+ ruleID: string;
43
+ what: string;
44
+ ordinalSeverity: number;
45
+ outcome?: Outcome;
46
+ uncertainty?: string;
47
+ needed?: string;
48
+ count?: number;
49
+ catalogIndex?: string | number;
50
+ pathID?: string;
51
+ }
52
+
53
+ // ########## CONSTANTS
54
+
55
+ // Outcomes of instances.
56
+ export const OUTCOMES: Outcome[] = ['failed', 'cantTell'];
57
+ // Reasons for cantTell outcomes.
58
+ export const UNCERTAINTY_CODES: UncertaintyCode[] = [
59
+ 'not-computable',
60
+ 'judgement-required',
61
+ 'runtime-dependent',
62
+ 'spec-only',
63
+ 'equivalence-unknown',
64
+ 'out-of-scope'
65
+ ];
66
+ // Maximum length of a needed description.
67
+ const NEEDED_MAX_LENGTH = 300;
68
+
69
+ // ########## FUNCTIONS
70
+
71
+ // Returns an empty standard result.
72
+ export const getStandardResult = (): StandardResult => ({
73
+ prevented: false,
74
+ totals: [0, 0, 0, 0],
75
+ outcomeTotals: {failed: 0, cantTell: 0},
76
+ instances: []
77
+ });
78
+ // Returns a standard instance, validating its certainty properties.
79
+ export const getInstance = (spec: InstanceSpec): StandardInstance => {
80
+ const {
81
+ ruleID, what, ordinalSeverity, outcome = 'failed', uncertainty, needed, count = 1, catalogIndex, pathID
82
+ } = spec;
83
+ if (! OUTCOMES.includes(outcome)) {
84
+ throw new Error(`Invalid outcome ${outcome} for rule ${ruleID}`);
85
+ }
86
+ const instance: StandardInstance = {
87
+ ruleID,
88
+ what,
89
+ ordinalSeverity: Number(ordinalSeverity) as StandardInstance['ordinalSeverity'],
90
+ outcome
91
+ };
92
+ // If the outcome is cantTell, add any reason and reviewer guidance.
93
+ if (outcome === 'cantTell') {
94
+ if (uncertainty) {
95
+ if (! (UNCERTAINTY_CODES as string[]).includes(uncertainty)) {
96
+ throw new Error(`Invalid uncertainty ${uncertainty} for rule ${ruleID}`);
97
+ }
98
+ instance.uncertainty = uncertainty as UncertaintyCode;
99
+ }
100
+ if (needed) {
101
+ instance.needed = String(needed).replace(/\s+/g, ' ').trim().slice(0, NEEDED_MAX_LENGTH);
102
+ }
103
+ }
104
+ instance.count = count;
105
+ if (catalogIndex !== undefined) {
106
+ instance.catalogIndex = catalogIndex;
107
+ }
108
+ else if (pathID) {
109
+ instance.pathID = pathID;
110
+ }
111
+ return instance;
112
+ };
113
+ // Adds an instance to a standard result and increments its outcome total.
114
+ export const pushInstance = (standardResult: StandardResult, spec: InstanceSpec): StandardInstance => {
115
+ const instance = getInstance(spec);
116
+ standardResult.outcomeTotals ??= {failed: 0, cantTell: 0};
117
+ standardResult.outcomeTotals[instance.outcome!] += instance.count!;
118
+ standardResult.instances!.push(instance);
119
+ return instance;
120
+ };
121
+ // Adds an instance to a standard result and increments its severity and outcome totals.
122
+ export const addInstance = (standardResult: StandardResult, spec: InstanceSpec): StandardInstance => {
123
+ const instance = pushInstance(standardResult, spec);
124
+ standardResult.totals![instance.ordinalSeverity] += instance.count!;
125
+ return instance;
126
+ };
@@ -0,0 +1,145 @@
1
+ /*
2
+ © 2026 Jeff Witt.
3
+
4
+ Licensed under the MIT License. See LICENSE file at the project root or
5
+ https://opensource.org/license/mit/ for details.
6
+
7
+ SPDX-License-Identifier: MIT
8
+ */
9
+
10
+ /*
11
+ testAct.js
12
+ Performs a test act: launches (or prepares) a page for the tool, runs the tool's reporter,
13
+ and records the act's data and result in the report. Used by the child process of process
14
+ isolation (doTestAct.js) and directly by the acts loop under browser and page isolation.
15
+ */
16
+
17
+ // IMPORTS
18
+
19
+ const {browserClose, launch, preparePage} = require('./launch');
20
+
21
+ // CONSTANTS
22
+
23
+ /*
24
+ Tool XPath requirements.
25
+ none: Needs no script or extra load time.
26
+ own: Needs extra load time for its own XPath computations.
27
+ script: Needs the window.getXPath script.
28
+ attribute: Needs data-xpath attributes made with window.getXPath.
29
+ */
30
+ const xPathNeeds = exports.xPathNeeds = {
31
+ alfa: 'own',
32
+ aslint: 'own',
33
+ axe: 'attribute',
34
+ ed11y: 'script',
35
+ htmlcs: 'attribute',
36
+ ibm: 'attribute',
37
+ nuVal: 'attribute',
38
+ nuVnu: 'attribute',
39
+ pour: 'script',
40
+ qualWeb: 'attribute',
41
+ surea11y: 'script',
42
+ wave: 'script'
43
+ };
44
+ const accessibleNameNeeders = ['testaro'];
45
+
46
+ // FUNCTIONS
47
+
48
+ /*
49
+ Performs the test act at an index of a report and revises the report. Under page isolation,
50
+ livePage is the live page of the current checkpoint, which tools that take a page use as it
51
+ is (after preparation); otherwise each tool gets a page launched for it. The testaro tool
52
+ launches its own pages per rule (starting from livePage if given).
53
+ */
54
+ exports.performTestAct = async ({report, actIndex, livePage = null}) => {
55
+ const act = report.acts[actIndex];
56
+ const {which} = act;
57
+ const useLivePage = Boolean(livePage) && report.jobData?.isolation === 'page';
58
+ let page;
59
+ // If the tool is not Testaro:
60
+ if (which !== 'testaro') {
61
+ // If the tool is to use the live page, prepare it for the tool.
62
+ if (useLivePage) {
63
+ page = livePage;
64
+ await preparePage(page, {
65
+ xPathNeed: xPathNeeds[which] ?? 'none',
66
+ needsAccessibleName: accessibleNameNeeders.includes(which)
67
+ });
68
+ }
69
+ // Otherwise, launch a browser and navigate to the URL (replaying the checkpoint's acts).
70
+ else {
71
+ const browserID = act.launch && act.launch.browserID || report.browserID;
72
+ const targetURL = act.launch && act.launch.target && act.launch.target.url || report.target.url;
73
+ page = await launch({
74
+ report,
75
+ actIndex,
76
+ tempBrowserID: browserID,
77
+ tempURL: targetURL,
78
+ xPathNeed: xPathNeeds[which] ?? 'none',
79
+ needsAccessibleName: accessibleNameNeeders.includes(which)
80
+ });
81
+ // If the launch aborted the job, stop.
82
+ if (report.jobData?.aborted) {
83
+ await browserClose(page);
84
+ return;
85
+ }
86
+ }
87
+ }
88
+ // If the page exists after the launch, or if the tool is Testaro:
89
+ if (page || which === 'testaro') {
90
+ try {
91
+ // Make the act reporter perform the specified tests of the tool.
92
+ const actReport = await require(`../tests/${which}`)
93
+ .reporter(useLivePage && which === 'testaro' ? livePage : page, report, actIndex, 65);
94
+ // Add the data and result to the act, keeping any checkpoint replay record the launch
95
+ // added to the act's data and any scope record the acts loop added (which the tool may
96
+ // have extended).
97
+ const {replay, scope} = act.data ?? {};
98
+ const toolScope = actReport.data && actReport.data.scope;
99
+ act.data = actReport.data ?? {};
100
+ if (replay) {
101
+ act.data.replay = replay;
102
+ }
103
+ if (scope) {
104
+ act.data.scope = {... scope, ... (toolScope ?? {})};
105
+ }
106
+ act.result = actReport.result;
107
+ // If the tool reported that the page prevented testing:
108
+ if (act.data && act.data.prevented) {
109
+ const {standardResult} = act.result;
110
+ // Add this to any standard result.
111
+ if (standardResult) {
112
+ standardResult.prevented = true;
113
+ }
114
+ // Add prevention data to the job data.
115
+ report.jobData.preventions[which] = act.data.error;
116
+ }
117
+ }
118
+ // If the tool invocation failed:
119
+ catch(error) {
120
+ const message = error.message.slice(0, 400);
121
+ console.log(`ERROR: Test act ${act.which} failed (${message})`);
122
+ act.data ??= {};
123
+ act.data.prevented = true;
124
+ act.data.error = `ERROR performing the act (${message})`;
125
+ report.jobData.preventions[which] = act.data.error;
126
+ throw error;
127
+ }
128
+ finally {
129
+ // Close the page and its context, unless it is the live page.
130
+ if (page && page !== livePage) {
131
+ await browserClose(page);
132
+ }
133
+ }
134
+ }
135
+ // Otherwise, i.e. if the page does not exist after the launch:
136
+ else {
137
+ // Add this to the act.
138
+ act.data ??= {};
139
+ act.data.prevented = true;
140
+ act.data.error = report.jobData?.abortMessage || 'No page';
141
+ // Add the prevention to the job data.
142
+ report.jobData.preventions[which] = act.data.error;
143
+ console.log('ERROR: No page');
144
+ }
145
+ };
@@ -0,0 +1,20 @@
1
+ import type { Locator, Page } from 'playwright';
2
+ import type { Outcome, Report, SeverityTotals, StandardInstance, UncertaintyCode } from '../types';
3
+ export interface RuleResult {
4
+ data: Record<string, unknown>;
5
+ totals: SeverityTotals;
6
+ standardInstances: StandardInstance[];
7
+ }
8
+ interface BasicViolation {
9
+ loc: Locator;
10
+ what: string;
11
+ outcome?: Outcome;
12
+ uncertainty?: UncertaintyCode;
13
+ needed?: string;
14
+ }
15
+ export declare const doTest: (page: Page, report: Report, withItems: boolean, ruleID: string, candidateSelector: string, whats: string, severity: number, getBadWhatString: string) => Promise<RuleResult>;
16
+ export declare const getBasicResult: (report: Report, withItems: boolean, ruleID: string, ordinalSeverity: StandardInstance["ordinalSeverity"], whats: string, data: {
17
+ prevented?: boolean;
18
+ [key: string]: unknown;
19
+ }, violations: BasicViolation[]) => Promise<RuleResult>;
20
+ export {};