testaro 78.0.8 → 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 (356) 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 +24 -18
  7. package/CONTAINERS.md +9 -6
  8. package/CONTRIBUTING.md +29 -3
  9. package/Dockerfile +1 -1
  10. package/README.md +73 -19
  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 +372 -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 +312 -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 +62 -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/job.js +115 -1
  38. package/procs/launch.d.ts +40 -0
  39. package/procs/launch.js +239 -108
  40. package/procs/nu.d.ts +48 -0
  41. package/procs/scope.js +196 -0
  42. package/procs/shoot.d.ts +33 -0
  43. package/procs/standard.d.ts +18 -0
  44. package/procs/standard.js +83 -0
  45. package/procs/standard.ts +126 -0
  46. package/procs/testAct.js +145 -0
  47. package/procs/testaro.d.ts +20 -0
  48. package/procs/testaro.js +236 -189
  49. package/procs/testaro.ts +321 -0
  50. package/procs/userPath.js +126 -0
  51. package/procs/xPath.d.ts +4 -0
  52. package/procs/xPath.js +81 -69
  53. package/procs/xPath.ts +106 -0
  54. package/procs/xPathScript.d.ts +6 -0
  55. package/procs/xPathScript.js +70 -0
  56. package/run.js +3 -2
  57. package/surea11y/README.md +44 -0
  58. package/surea11y/surea11y.browser.js +14 -0
  59. package/testaro/adbID.d.ts +3 -0
  60. package/testaro/adbID.js +38 -41
  61. package/testaro/adbID.ts +66 -0
  62. package/testaro/allCapStyle.d.ts +3 -0
  63. package/testaro/allCapStyle.js +31 -34
  64. package/testaro/allCapStyle.ts +55 -0
  65. package/testaro/allCaps.d.ts +16 -0
  66. package/testaro/allCaps.js +179 -151
  67. package/testaro/allCaps.ts +228 -0
  68. package/testaro/allHidden.d.ts +13 -0
  69. package/testaro/allHidden.js +29 -30
  70. package/testaro/allHidden.ts +50 -0
  71. package/testaro/allSlanted.d.ts +3 -0
  72. package/testaro/allSlanted.js +30 -33
  73. package/testaro/allSlanted.ts +54 -0
  74. package/testaro/altScheme.d.ts +3 -0
  75. package/testaro/altScheme.js +27 -30
  76. package/testaro/altScheme.ts +50 -0
  77. package/testaro/attVal.d.ts +3 -0
  78. package/testaro/attVal.js +20 -21
  79. package/testaro/attVal.ts +52 -0
  80. package/testaro/autocomplete.d.ts +3 -0
  81. package/testaro/autocomplete.js +59 -75
  82. package/testaro/autocomplete.ts +101 -0
  83. package/testaro/bulk.d.ts +13 -0
  84. package/testaro/bulk.js +32 -33
  85. package/testaro/bulk.ts +55 -0
  86. package/testaro/buttonMenu.d.ts +9 -0
  87. package/testaro/buttonMenu.js +317 -319
  88. package/testaro/buttonMenu.ts +391 -0
  89. package/testaro/captionLoc.d.ts +3 -0
  90. package/testaro/captionLoc.js +17 -20
  91. package/testaro/captionLoc.ts +40 -0
  92. package/testaro/datalistRef.d.ts +3 -0
  93. package/testaro/datalistRef.js +33 -36
  94. package/testaro/datalistRef.ts +55 -0
  95. package/testaro/distortion.d.ts +3 -0
  96. package/testaro/distortion.js +57 -26
  97. package/testaro/distortion.ts +81 -0
  98. package/testaro/docType.d.ts +15 -0
  99. package/testaro/docType.js +25 -25
  100. package/testaro/docType.ts +45 -0
  101. package/testaro/dupAtt.d.ts +16 -0
  102. package/testaro/dupAtt.js +113 -104
  103. package/testaro/dupAtt.ts +144 -0
  104. package/testaro/elements.d.ts +6 -0
  105. package/testaro/elements.js +153 -153
  106. package/testaro/elements.ts +215 -0
  107. package/testaro/embAc.d.ts +3 -0
  108. package/testaro/embAc.js +19 -20
  109. package/testaro/embAc.ts +40 -0
  110. package/testaro/focAll.d.ts +13 -0
  111. package/testaro/focAll.js +185 -191
  112. package/testaro/focAll.ts +217 -0
  113. package/testaro/focAndOp.d.ts +3 -0
  114. package/testaro/focAndOp.js +100 -104
  115. package/testaro/focAndOp.ts +128 -0
  116. package/testaro/focInd.d.ts +3 -0
  117. package/testaro/focInd.js +64 -66
  118. package/testaro/focInd.ts +96 -0
  119. package/testaro/focVis.d.ts +3 -0
  120. package/testaro/focVis.js +28 -30
  121. package/testaro/focVis.ts +52 -0
  122. package/testaro/headEl.d.ts +10 -0
  123. package/testaro/headEl.js +61 -62
  124. package/testaro/headEl.ts +82 -0
  125. package/testaro/headingAmb.d.ts +3 -0
  126. package/testaro/headingAmb.js +54 -63
  127. package/testaro/headingAmb.ts +72 -0
  128. package/testaro/hovInd.d.ts +12 -0
  129. package/testaro/hovInd.js +163 -130
  130. package/testaro/hovInd.ts +199 -0
  131. package/testaro/hover.d.ts +3 -0
  132. package/testaro/hover.js +155 -126
  133. package/testaro/hover.ts +154 -0
  134. package/testaro/hr.d.ts +3 -0
  135. package/testaro/hr.js +14 -17
  136. package/testaro/hr.ts +36 -0
  137. package/testaro/imageLink.d.ts +3 -0
  138. package/testaro/imageLink.js +17 -20
  139. package/testaro/imageLink.ts +42 -0
  140. package/testaro/labClash.d.ts +3 -0
  141. package/testaro/labClash.js +31 -33
  142. package/testaro/labClash.ts +54 -0
  143. package/testaro/legendLoc.d.ts +3 -0
  144. package/testaro/legendLoc.js +17 -20
  145. package/testaro/legendLoc.ts +42 -0
  146. package/testaro/lineHeight.d.ts +3 -0
  147. package/testaro/lineHeight.js +43 -48
  148. package/testaro/lineHeight.ts +70 -0
  149. package/testaro/linkAmb.d.ts +7 -0
  150. package/testaro/linkAmb.js +79 -80
  151. package/testaro/linkAmb.ts +105 -0
  152. package/testaro/linkExt.d.ts +3 -0
  153. package/testaro/linkExt.js +13 -16
  154. package/testaro/linkExt.ts +35 -0
  155. package/testaro/linkOldAtt.d.ts +3 -0
  156. package/testaro/linkOldAtt.js +25 -28
  157. package/testaro/linkOldAtt.ts +48 -0
  158. package/testaro/linkTo.d.ts +3 -0
  159. package/testaro/linkTo.js +22 -22
  160. package/testaro/linkTo.ts +43 -0
  161. package/testaro/linkUl.d.ts +3 -0
  162. package/testaro/linkUl.js +30 -34
  163. package/testaro/linkUl.ts +54 -0
  164. package/testaro/miniText.d.ts +3 -0
  165. package/testaro/miniText.js +41 -44
  166. package/testaro/miniText.ts +68 -0
  167. package/testaro/motion.d.ts +10 -0
  168. package/testaro/motion.js +92 -96
  169. package/testaro/motion.ts +125 -0
  170. package/testaro/nonTable.d.ts +3 -0
  171. package/testaro/nonTable.js +39 -45
  172. package/testaro/nonTable.ts +66 -0
  173. package/testaro/optRoleSel.d.ts +3 -0
  174. package/testaro/optRoleSel.js +16 -19
  175. package/testaro/optRoleSel.ts +41 -0
  176. package/testaro/phOnly.d.ts +3 -0
  177. package/testaro/phOnly.js +18 -21
  178. package/testaro/phOnly.ts +43 -0
  179. package/testaro/pseudoP.d.ts +3 -0
  180. package/testaro/pseudoP.js +36 -38
  181. package/testaro/pseudoP.ts +59 -0
  182. package/testaro/radioSet.d.ts +3 -0
  183. package/testaro/radioSet.js +57 -59
  184. package/testaro/radioSet.ts +79 -0
  185. package/testaro/registry.d.ts +63 -0
  186. package/testaro/registry.js +67 -0
  187. package/testaro/registry.ts +141 -0
  188. package/testaro/role.d.ts +3 -0
  189. package/testaro/role.js +27 -29
  190. package/testaro/role.ts +53 -0
  191. package/testaro/secHeading.d.ts +3 -0
  192. package/testaro/secHeading.js +30 -33
  193. package/testaro/secHeading.ts +53 -0
  194. package/testaro/styleDiff.d.ts +25 -0
  195. package/testaro/styleDiff.js +248 -252
  196. package/testaro/styleDiff.ts +303 -0
  197. package/testaro/tabNav.d.ts +33 -0
  198. package/testaro/tabNav.js +272 -342
  199. package/testaro/tabNav.ts +454 -0
  200. package/testaro/targetsNear.d.ts +9 -0
  201. package/testaro/targetsNear.js +130 -132
  202. package/testaro/targetsNear.ts +160 -0
  203. package/testaro/textNodes.d.ts +6 -0
  204. package/testaro/textNodes.js +139 -135
  205. package/testaro/textNodes.ts +185 -0
  206. package/testaro/textSem.d.ts +3 -0
  207. package/testaro/textSem.js +25 -28
  208. package/testaro/textSem.ts +47 -0
  209. package/testaro/title.d.ts +9 -0
  210. package/testaro/title.js +16 -13
  211. package/testaro/title.ts +31 -0
  212. package/testaro/titledEl.d.ts +3 -0
  213. package/testaro/titledEl.js +15 -18
  214. package/testaro/titledEl.ts +38 -0
  215. package/testaro/zIndex.d.ts +3 -0
  216. package/testaro/zIndex.js +19 -22
  217. package/testaro/zIndex.ts +42 -0
  218. package/tests/alfa.d.ts +45 -0
  219. package/tests/alfa.js +138 -141
  220. package/tests/alfa.ts +214 -0
  221. package/tests/aslint.d.ts +33 -0
  222. package/tests/aslint.js +272 -249
  223. package/tests/aslint.ts +301 -0
  224. package/tests/axe.d.ts +27 -0
  225. package/tests/axe.js +199 -200
  226. package/tests/axe.ts +277 -0
  227. package/tests/ed11y.d.ts +28 -0
  228. package/tests/ed11y.js +141 -99
  229. package/tests/ed11y.ts +178 -0
  230. package/tests/htmlcs.d.ts +21 -0
  231. package/tests/htmlcs.js +174 -140
  232. package/tests/htmlcs.ts +181 -0
  233. package/tests/ibm.d.ts +52 -0
  234. package/tests/ibm.js +164 -166
  235. package/tests/ibm.ts +251 -0
  236. package/tests/nuVal.d.ts +13 -0
  237. package/tests/nuVal.js +107 -112
  238. package/tests/nuVal.ts +145 -0
  239. package/tests/nuVnu.d.ts +15 -0
  240. package/tests/nuVnu.js +141 -111
  241. package/tests/nuVnu.ts +144 -0
  242. package/tests/pour.d.ts +31 -0
  243. package/tests/pour.js +242 -0
  244. package/tests/pour.ts +273 -0
  245. package/tests/qualWeb.d.ts +39 -0
  246. package/tests/qualWeb.js +302 -272
  247. package/tests/qualWeb.ts +415 -0
  248. package/tests/surea11y.d.ts +33 -0
  249. package/tests/surea11y.js +288 -0
  250. package/tests/surea11y.ts +334 -0
  251. package/tests/testaro.d.ts +25 -0
  252. package/tests/testaro.js +745 -652
  253. package/tests/testaro.ts +862 -0
  254. package/tests/wave.d.ts +46 -0
  255. package/tests/wave.js +166 -177
  256. package/tests/wave.ts +252 -0
  257. package/tsconfig.json +17 -0
  258. package/types.d.ts +243 -0
  259. package/types.js +10 -0
  260. package/types.ts +376 -0
  261. package/validation/act/README.md +46 -0
  262. package/validation/act/capture.js +424 -0
  263. package/validation/act/chromium-issue-draft.md +66 -0
  264. package/validation/act/fp-triage-2026-08-22.md +86 -0
  265. package/validation/act/isolation-notes.md +159 -0
  266. package/validation/act/playwright-issue-draft.md +73 -0
  267. package/validation/act/propose-mappings.js +0 -0
  268. package/validation/act/repro-cdp-raw.js +105 -0
  269. package/validation/act/repro-metarefresh.js +65 -0
  270. package/validation/act/score.js +184 -0
  271. package/validation/act/stage3a-stress-report.md +69 -0
  272. package/validation/act/stage3b-mapping-proposals.md +94 -0
  273. package/validation/act/stage3b-triage-draft.md +138 -0
  274. package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
  275. package/validation/executors/netWatch.js +180 -90
  276. package/validation/executors/test.js +17 -2
  277. package/validation/executors/tests.js +118 -10
  278. package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
  279. package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
  280. package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
  281. package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
  282. package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
  283. package/validation/knownFailures.json +1 -0
  284. package/validation/tests/jobProperties/adbID.json +27 -2
  285. package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
  286. package/validation/tests/jobProperties/allCaps.json +25 -0
  287. package/validation/tests/jobProperties/allHidden.json +133 -13
  288. package/validation/tests/jobProperties/allSlanted.json +3 -3
  289. package/validation/tests/jobProperties/altScheme.json +23 -8
  290. package/validation/tests/jobProperties/attVal.json +57 -57
  291. package/validation/tests/jobProperties/autocomplete.json +11 -1
  292. package/validation/tests/jobProperties/bulk.json +6 -1
  293. package/validation/tests/jobProperties/buttonMenu.json +63 -42
  294. package/validation/tests/jobProperties/captionLoc.json +1 -6
  295. package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
  296. package/validation/tests/jobProperties/checkpoint-page.json +397 -0
  297. package/validation/tests/jobProperties/checkpoint.json +395 -0
  298. package/validation/tests/jobProperties/datalistRef.json +20 -5
  299. package/validation/tests/jobProperties/distortion.json +28 -3
  300. package/validation/tests/jobProperties/docType.json +2 -2
  301. package/validation/tests/jobProperties/dupAtt.json +48 -33
  302. package/validation/tests/jobProperties/elements.json +28 -28
  303. package/validation/tests/jobProperties/embAc.json +36 -31
  304. package/validation/tests/jobProperties/focAndOp.json +284 -0
  305. package/validation/tests/jobProperties/focInd.json +34 -34
  306. package/validation/tests/jobProperties/focVis.json +1 -1
  307. package/validation/tests/jobProperties/hover.json +39 -37
  308. package/validation/tests/jobProperties/hr.json +13 -3
  309. package/validation/tests/jobProperties/imageLink.json +0 -5
  310. package/validation/tests/jobProperties/labClash.json +68 -33
  311. package/validation/tests/jobProperties/legendLoc.json +1 -6
  312. package/validation/tests/jobProperties/lineHeight.json +2 -2
  313. package/validation/tests/jobProperties/linkAmb.json +13 -18
  314. package/validation/tests/jobProperties/linkExt.json +1 -1
  315. package/validation/tests/jobProperties/linkOldAtt.json +12 -2
  316. package/validation/tests/jobProperties/linkTo.json +1 -1
  317. package/validation/tests/jobProperties/linkUl.json +65 -65
  318. package/validation/tests/jobProperties/miniText.json +7 -2
  319. package/validation/tests/jobProperties/motion.json +4 -50
  320. package/validation/tests/jobProperties/nonTable.json +48 -3
  321. package/validation/tests/jobProperties/optRoleSel.json +12 -2
  322. package/validation/tests/jobProperties/phOnly.json +6 -16
  323. package/validation/tests/jobProperties/pseudoP.json +17 -2
  324. package/validation/tests/jobProperties/radioSet.json +30 -30
  325. package/validation/tests/jobProperties/role.json +25 -5
  326. package/validation/tests/jobProperties/secHeading.json +26 -21
  327. package/validation/tests/jobProperties/styleDiff.json +35 -5
  328. package/validation/tests/jobProperties/tabNav.json +3 -1
  329. package/validation/tests/jobProperties/targetsNear.json +237 -0
  330. package/validation/tests/jobProperties/textNodes.json +29 -29
  331. package/validation/tests/jobProperties/textSem.json +26 -1
  332. package/validation/tests/jobProperties/title.json +12 -2
  333. package/validation/tests/jobProperties/titledEl.json +39 -9
  334. package/validation/tests/jobProperties/userPath.json +317 -0
  335. package/validation/tests/jobProperties/zIndex.json +40 -35
  336. package/validation/tests/targets/allCapStyle/index.html +26 -0
  337. package/validation/tests/targets/checkpoint/index.html +38 -0
  338. package/validation/tests/targets/datalistRef/index.html +1 -1
  339. package/validation/tests/targets/focAndOp/bad.html +29 -0
  340. package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
  341. package/validation/tests/targets/focInd/bad.html +2 -1
  342. package/validation/tests/targets/headEl/index.html +10 -1
  343. package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
  344. package/validation/tests/targets/userPath/index.html +42 -0
  345. package/validation/validateTest.js +61 -9
  346. package/.claude/settings.local.json +0 -11
  347. package/.eslintrc.json +0 -41
  348. package/htmlcs/.eslintrc.json +0 -67
  349. package/validation/tests/jobProperties/linkTitle.json +0 -127
  350. package/validation/tests/jobProperties/opFoc.json +0 -164
  351. package/validation/tests/jobProperties/targetSmall.json +0 -152
  352. package/validation/tests/jobProperties/targetTiny.json +0 -142
  353. package/validation/tests/targets/focOp/bad.html +0 -25
  354. package/validation/tests/targets/linkTitle/index.html +0 -24
  355. package/validation/tests/targets/opFoc/bad.html +0 -26
  356. package/validation/tests/targets/opFoc/good.html +0 -23
@@ -0,0 +1,321 @@
1
+ /*
2
+ © 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2026 Jeff Witt.
4
+ © 2025–2026 Jonathan Robert Pool.
5
+
6
+ Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
7
+
8
+ SPDX-License-Identifier: MIT
9
+ */
10
+
11
+ // ########## IMPORTS
12
+
13
+ // Function to get a catalog index from an XPath.
14
+ import {getXPathCatalogIndex} from './xPath';
15
+ // Function to build a standard instance.
16
+ import {getInstance, type InstanceSpec} from './standard';
17
+ import type {Locator, Page} from 'playwright';
18
+ import type {GetBadWhat, Outcome, Report, SeverityTotals, StandardInstance, UncertaintyCode} from '../types';
19
+
20
+ /*
21
+ testaro
22
+ Utilities for Testaro tests. Compiled to testaro.js by tsc (issue #73); edit
23
+ this file, not the emitted one.
24
+ */
25
+
26
+ // ########## TYPES
27
+
28
+ // A violation found in the page before it becomes a standard instance.
29
+ interface ProtoInstance {
30
+ what: string;
31
+ ordinalSeverity: number;
32
+ outcome?: Outcome;
33
+ xPath: string;
34
+ }
35
+ // What doTest and getBasicResult return to the calling rule module.
36
+ export interface RuleResult {
37
+ data: Record<string, unknown>;
38
+ totals: SeverityTotals;
39
+ standardInstances: StandardInstance[];
40
+ }
41
+ // A violation reported by a doTest-ineligible rule module.
42
+ interface BasicViolation {
43
+ loc: Locator;
44
+ what: string;
45
+ // A violation may specify its own certainty.
46
+ outcome?: Outcome;
47
+ uncertainty?: UncertaintyCode;
48
+ needed?: string;
49
+ }
50
+
51
+ // ########## FUNCTIONS
52
+
53
+ // Returns a standard instance, leaving the outcome unset if the violation did not specify one, so
54
+ // that the testaro tool can apply the rule's default outcome.
55
+ const getRuleInstance = (spec: InstanceSpec): StandardInstance => {
56
+ const instance = getInstance(spec);
57
+ if (! spec.outcome) {
58
+ instance.outcome = undefined;
59
+ }
60
+ return instance;
61
+ };
62
+
63
+ // Tests for a testaro rule.
64
+ export const doTest = async (
65
+ page: Page,
66
+ report: Report,
67
+ withItems: boolean,
68
+ ruleID: string,
69
+ candidateSelector: string,
70
+ whats: string,
71
+ severity: number,
72
+ getBadWhatString: string
73
+ ): Promise<RuleResult> => {
74
+ // The subtree roots the rule is restricted to, if the test act has scope changed and the
75
+ // testaro tool judged the rule element-local (set per rule by tests/testaro.ts).
76
+ const scopeRoots = report.ruleScopeRoots ?? null;
77
+ const ruleData = await page.evaluate(async args => {
78
+ // Get the arguments.
79
+ const [
80
+ withItems,
81
+ candidateSelector,
82
+ severity,
83
+ getBadWhatString,
84
+ scopeRoots
85
+ ] = args;
86
+ // Get all violator candidates, within the scope roots if any.
87
+ let candidates = Array.from(document.querySelectorAll(candidateSelector));
88
+ if (scopeRoots && scopeRoots.length) {
89
+ const roots = scopeRoots
90
+ .map(selector => {
91
+ try {
92
+ return document.querySelector(selector);
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ })
98
+ .filter((root): root is Element => Boolean(root));
99
+ candidates = candidates.filter(candidate => roots.some(root => root.contains(candidate)));
100
+ }
101
+ let violationCount = 0;
102
+ // Initialize proto-instances.
103
+ const protoInstances: ProtoInstance[] = [];
104
+ // Initialize counts of cantTell violations by ordinal severity, for summary instances.
105
+ const cantTellTotals = [0, 0, 0, 0];
106
+ /*
107
+ Parse the supplied string to get the classifier. The predicate travels as
108
+ source text (serialized with toString() by the rule module) because it
109
+ must run inside the page; this cast is the single point where its type is
110
+ asserted rather than checked (issue #73, RFC decision 4). The eval input
111
+ is first-party rule-module source from this repository, never target-page
112
+ or user data; replacing the eval pipeline with a bundled artifact is the
113
+ Phase 3 reassessment recorded on issue #73.
114
+ */
115
+ const getBadWhat = eval(`(${getBadWhatString})`) as GetBadWhat;
116
+ // Initialize data on the rule.
117
+ let data: Record<string, unknown> = {};
118
+ const totals = [0, 0, 0, 0];
119
+ // For each candidate:
120
+ for (const candidate of candidates) {
121
+ // Classify it as and get a violation description if a violator or undefined if not.
122
+ const violationWhat = await getBadWhat(candidate);
123
+ // If the candidate violates the rule:
124
+ if (violationWhat) {
125
+ // Increment the violation count.
126
+ violationCount++;
127
+ let ruleWhat;
128
+ const violationType = typeof violationWhat;
129
+ // If data on the violation were provided (unusual):
130
+ if (violationType === 'object') {
131
+ // Get the description and add the data to the rule data.
132
+ ruleWhat = (violationWhat as {description: string; data?: unknown}).description;
133
+ data[violationCount - 1] = (violationWhat as {description: string; data?: unknown}).data;
134
+ }
135
+ // Otherwise, i.e. if only a description of the violation was provided:
136
+ else if (violationType === 'string') {
137
+ // Get it.
138
+ ruleWhat = violationWhat as string;
139
+ }
140
+ let ordinalSeverity = severity;
141
+ let outcome: 'cantTell' | undefined;
142
+ // A predicate that returns a truthy non-object non-string would make this throw,
143
+ // exactly as in the JavaScript original.
144
+ // If this violation has a custom severity or outcome prefix (e.g. 2:, 2?:, or ?:):
145
+ const prefixMatch = (ruleWhat as string).match(/^([0-3])?(\?)?:/);
146
+ if (prefixMatch) {
147
+ // If the prefix has a severity, get it.
148
+ if (prefixMatch[1]) {
149
+ ordinalSeverity = Number(prefixMatch[1]);
150
+ }
151
+ // If the prefix marks the violation as uncertain, record this.
152
+ if (prefixMatch[2]) {
153
+ outcome = 'cantTell';
154
+ cantTellTotals[ordinalSeverity]++;
155
+ }
156
+ // Remove the prefix from the violation description.
157
+ ruleWhat = (ruleWhat as string).slice(prefixMatch[0].length);
158
+ }
159
+ // Increment the applicable rule-violation total.
160
+ totals[ordinalSeverity]++;
161
+ // If itemization is required:
162
+ if (withItems) {
163
+ const protoInstance: ProtoInstance = {
164
+ what: ruleWhat as string,
165
+ ordinalSeverity,
166
+ outcome,
167
+ xPath: window.getXPath(candidate as Element) ?? '/html'
168
+ };
169
+ // Add a proto-instance to the proto-instances.
170
+ protoInstances.push(protoInstance);
171
+ }
172
+ }
173
+ }
174
+ return {
175
+ data,
176
+ totals,
177
+ cantTellTotals,
178
+ protoInstances
179
+ }
180
+ }, [
181
+ withItems,
182
+ candidateSelector,
183
+ severity,
184
+ getBadWhatString,
185
+ scopeRoots
186
+ ] as const
187
+ );
188
+ // Initialize the standard instances. Any instance without an outcome gets the rule's default
189
+ // outcome from the testaro tool.
190
+ let standardInstances: StandardInstance[] = [];
191
+ const {data, totals, cantTellTotals, protoInstances} = ruleData;
192
+ // If itemization is required:
193
+ if (withItems) {
194
+ // For each proto-instance:
195
+ protoInstances.forEach(protoInstance => {
196
+ const {what, ordinalSeverity, outcome, xPath} = protoInstance;
197
+ // Add a standard instance to the standard instances.
198
+ standardInstances.push(getRuleInstance({
199
+ ruleID,
200
+ what,
201
+ ordinalSeverity,
202
+ outcome,
203
+ catalogIndex: xPath ? getXPathCatalogIndex(report, xPath) : undefined
204
+ }));
205
+ });
206
+ }
207
+ // Otherwise, i.e. if itemization is not required:
208
+ else {
209
+ // For each ordinal severity:
210
+ totals.forEach((total, ordinalSeverity) => {
211
+ const cantTellTotal = cantTellTotals[ordinalSeverity];
212
+ // If there were any asserted violations at that severity:
213
+ if (total - cantTellTotal) {
214
+ // Add a summary standard instance to the standard instances.
215
+ standardInstances.push(getRuleInstance({
216
+ ruleID,
217
+ what: whats,
218
+ ordinalSeverity,
219
+ count: total - cantTellTotal
220
+ }));
221
+ }
222
+ // If there were any uncertain violations at that severity:
223
+ if (cantTellTotal) {
224
+ // Add a summary standard instance for them.
225
+ standardInstances.push(getRuleInstance({
226
+ ruleID,
227
+ what: whats,
228
+ ordinalSeverity,
229
+ outcome: 'cantTell',
230
+ count: cantTellTotal
231
+ }));
232
+ }
233
+ });
234
+ }
235
+ // Return the data, totals, and standard instances.
236
+ return {
237
+ data,
238
+ totals: totals as SeverityTotals,
239
+ standardInstances
240
+ };
241
+ };
242
+ // Adds a catalog index or, if necessary, an XPath to a proto-instance.
243
+ const addCatalogIndex = async (
244
+ protoInstance: StandardInstance, locator: Locator, report: Report
245
+ ): Promise<StandardInstance> => {
246
+ // Get the XPath of the element referenced by the locator.
247
+ const xPath = await locator.evaluate(element => window.getXPath(element) ?? '/html');
248
+ // Add a catalog index to the proto-instance.
249
+ protoInstance.catalogIndex = getXPathCatalogIndex(report, xPath);
250
+ // Return the proto-instance with any modification.
251
+ return protoInstance;
252
+ };
253
+ // Tests for a doTest-ineligible Testaro rule.
254
+ export const getBasicResult = async (
255
+ report: Report,
256
+ withItems: boolean,
257
+ ruleID: string,
258
+ ordinalSeverity: StandardInstance['ordinalSeverity'],
259
+ whats: string,
260
+ data: {prevented?: boolean; [key: string]: unknown},
261
+ violations: BasicViolation[]
262
+ ): Promise<RuleResult> => {
263
+ // If the test was prevented:
264
+ if (data.prevented) {
265
+ // Return this.
266
+ return {
267
+ data,
268
+ totals: [0, 0, 0, 0],
269
+ standardInstances: []
270
+ };
271
+ }
272
+ // Otherwise, i.e. if the test was not prevented:
273
+ const totals: SeverityTotals = [0, 0, 0, 0];
274
+ totals[ordinalSeverity] = violations.length;
275
+ const standardInstances: StandardInstance[] = [];
276
+ // If itemization is required:
277
+ if (withItems) {
278
+ // For each violation:
279
+ for (const violation of violations) {
280
+ const {loc, what, outcome, uncertainty, needed} = violation;
281
+ // Initialize a standard instance.
282
+ const protoInstance = getRuleInstance({
283
+ ruleID,
284
+ what,
285
+ ordinalSeverity,
286
+ outcome,
287
+ uncertainty,
288
+ needed
289
+ });
290
+ // Add a catalog index to it, awaited so the index is present before the
291
+ // report can be serialized (issue #100).
292
+ await addCatalogIndex(protoInstance, loc, report);
293
+ // Add the standard instance to the standard instances.
294
+ standardInstances.push(protoInstance);
295
+ }
296
+ }
297
+ // Otherwise, i.e. if itemization is not required:
298
+ else {
299
+ // Add a summary instance per outcome to the instances.
300
+ const outcomeCounts: Partial<Record<Outcome, number>> = {};
301
+ violations.forEach(violation => {
302
+ const outcome = violation.outcome || 'failed';
303
+ outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
304
+ });
305
+ (Object.keys(outcomeCounts) as Outcome[]).forEach(outcome => {
306
+ standardInstances.push(getRuleInstance({
307
+ ruleID,
308
+ what: whats,
309
+ ordinalSeverity,
310
+ outcome,
311
+ count: outcomeCounts[outcome]
312
+ }));
313
+ });
314
+ }
315
+ // Return the result.
316
+ return {
317
+ data,
318
+ totals,
319
+ standardInstances
320
+ };
321
+ };
@@ -0,0 +1,126 @@
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
+ userPath.js
12
+ Converts a recorded user path (a start URL and a sequence of actions of the kinds a
13
+ Playwright codegen recording yields: click, fill, select, navigate, wait, checkpoint) into
14
+ the acts of a Testaro job, so that a job tests the page states the path reaches. Each
15
+ action becomes one act; each checkpoint action becomes a checkpoint act followed by the
16
+ test acts requested. See docs/checkpoint-scanning.md (Phase 4).
17
+
18
+ An action is {type, selector?, value?, url?, timeout?, label?}. Selectors are Playwright
19
+ selectors (CSS, or text=, role=, label= forms), used as the selector property of the act.
20
+ */
21
+
22
+ // CONSTANTS
23
+
24
+ // The action types and the act types they become.
25
+ const actionActTypes = exports.actionActTypes = {
26
+ click: 'button',
27
+ fill: 'text',
28
+ select: 'select',
29
+ navigate: 'url',
30
+ wait: 'state',
31
+ checkpoint: 'checkpoint'
32
+ };
33
+
34
+ // FUNCTIONS
35
+
36
+ // Returns a checkpoint name unique among those used, from a label or an ordinal.
37
+ const getCheckpointName = (label, ordinal, used) => {
38
+ const base = (label || '').trim().replace(/\s+/g, ' ') || `checkpoint${ordinal}`;
39
+ let name = base === 'start' ? `${base}-${ordinal}` : base;
40
+ let suffix = 1;
41
+ while (used.has(name)) {
42
+ name = `${base}-${++suffix}`;
43
+ }
44
+ used.add(name);
45
+ return name;
46
+ };
47
+ /*
48
+ Returns the acts of a job for a user path, or throws if an action is invalid.
49
+ startUrl: the URL the path starts at (the launch act's target).
50
+ actions: the recorded actions.
51
+ testActs: the test acts (without checkpoint-dependent properties) to run at each
52
+ checkpoint, e.g. [{type: 'test', which: 'axe', detailLevel: 0}]; each is copied per
53
+ checkpoint. With scope 'changed', the first checkpoint's copies are given scope 'page',
54
+ since there is no earlier checkpoint to compare.
55
+ scanAtCheckpoints: whether checkpoints get the test acts (default true).
56
+ what: a description of the launch target.
57
+ */
58
+ exports.getUserPathActs = ({startUrl, actions, testActs = [], scanAtCheckpoints = true, what}) => {
59
+ if (! startUrl || typeof startUrl !== 'string') {
60
+ throw new Error('User path has no start URL');
61
+ }
62
+ if (! Array.isArray(actions)) {
63
+ throw new Error('User path actions are not an array');
64
+ }
65
+ const acts = [{
66
+ type: 'launch',
67
+ target: {url: startUrl, what: what || 'start of the user path'}
68
+ }];
69
+ const usedNames = new Set();
70
+ let checkpointCount = 0;
71
+ actions.forEach((action, index) => {
72
+ const {type, selector, value, url, timeout, label} = action || {};
73
+ const comment = label || undefined;
74
+ const need = (condition, message) => {
75
+ if (! condition) {
76
+ throw new Error(`Action ${index} (${type}): ${message}`);
77
+ }
78
+ };
79
+ if (type === 'click') {
80
+ need(selector, 'requires a selector');
81
+ acts.push({type: 'button', selector, what: comment});
82
+ }
83
+ else if (type === 'fill') {
84
+ need(selector, 'requires a selector');
85
+ need(typeof value === 'string' && value.length, 'requires a value');
86
+ acts.push({type: 'text', selector, what: value, clear: true});
87
+ }
88
+ else if (type === 'select') {
89
+ need(selector, 'requires a selector');
90
+ need(typeof value === 'string' && value.length, 'requires a value');
91
+ acts.push({type: 'select', selector, what: value});
92
+ }
93
+ else if (type === 'navigate') {
94
+ need(url, 'requires a url');
95
+ acts.push({type: 'url', which: url, what: comment});
96
+ }
97
+ else if (type === 'wait') {
98
+ // A recorded pause becomes a wait for the page to be idle; the pause length is kept
99
+ // as a comment.
100
+ acts.push({type: 'state', which: 'idle', what: `pause ${timeout ?? 1000} ms`});
101
+ }
102
+ else if (type === 'checkpoint') {
103
+ const name = getCheckpointName(label, ++checkpointCount, usedNames);
104
+ acts.push({type: 'checkpoint', which: name, what: comment});
105
+ if (scanAtCheckpoints) {
106
+ testActs.forEach(testAct => {
107
+ const act = {... testAct};
108
+ if (act.scope === 'changed' && checkpointCount === 1) {
109
+ act.scope = 'page';
110
+ }
111
+ acts.push(act);
112
+ });
113
+ }
114
+ }
115
+ else {
116
+ throw new Error(`Action ${index}: unknown type ${type}`);
117
+ }
118
+ });
119
+ // Remove undefined comments, so the acts serialize cleanly.
120
+ acts.forEach(act => {
121
+ if (act.what === undefined) {
122
+ delete act.what;
123
+ }
124
+ });
125
+ return acts;
126
+ };
@@ -0,0 +1,4 @@
1
+ import type { Report } from '../types';
2
+ export declare const getNormalizedXPath: (xPath: string | null | undefined) => string;
3
+ export declare const getAttributeXPath: (html: string | null | undefined) => string;
4
+ export declare const getXPathCatalogIndex: (report: Report, xPath: string, checkpoint?: number) => string;
package/procs/xPath.js CHANGED
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  /*
2
3
  © 2024 CVS Health and/or one of its affiliates. All rights reserved.
3
4
  © 2026 Jeff Witt.
@@ -8,84 +9,95 @@
8
9
 
9
10
  SPDX-License-Identifier: MIT
10
11
  */
11
-
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.getXPathCatalogIndex = exports.getAttributeXPath = exports.getNormalizedXPath = void 0;
12
14
  /*
13
- xPath.js
14
- Processes element XPaths.
15
+ xPath
16
+ Processes element XPaths. Compiled to xPath.js by tsc (issue #73); edit this
17
+ file, not the emitted one.
15
18
  */
16
-
17
19
  // FUNCTIONS
18
-
19
20
  // Normalizes an XPath.
20
- exports.getNormalizedXPath = xPath => {
21
- if (xPath) {
22
- if (xPath === '/') {
23
- xPath = '/html';
21
+ const getNormalizedXPath = (xPath) => {
22
+ if (xPath) {
23
+ if (xPath === '/') {
24
+ xPath = '/html';
25
+ }
26
+ xPath = xPath.replace(/^\.\/\//, '/');
27
+ const segments = xPath.split('/');
28
+ // Initialize an array of normalized segments.
29
+ const normalizedSegments = [];
30
+ // For each segment of the XPath:
31
+ segments.forEach(segment => {
32
+ // If the segment is html[1] or body[1]:
33
+ if (/html\[1\]|body\[1\]/.test(segment)) {
34
+ // Add it without its subscript to the array.
35
+ normalizedSegments.push(segment.replace(/\[1\]/, ''));
36
+ }
37
+ // Otherwise, if the segment is empty or html or body or ends with a subscript:
38
+ else if (segment === '' || ['html', 'body'].includes(segment) || segment.endsWith(']')) {
39
+ // Add it to the array.
40
+ normalizedSegments.push(segment);
41
+ }
42
+ // Otherwise, i.e. if the segment is a tag name with no subscript:
43
+ else {
44
+ // Add it with a subscript 1 to the array.
45
+ normalizedSegments.push(`${segment}[1]`);
46
+ }
47
+ });
48
+ // If the final segment contains any nonstandard character:
49
+ if (/[[^\]A-Za-z0-9]/.test(segments[segments.length - 1])) {
50
+ // Remove it.
51
+ normalizedSegments.pop();
52
+ }
53
+ // Return the concatenated segments as the normalized XPath.
54
+ return normalizedSegments.join('/');
24
55
  }
25
- xPath = xPath.replace(/^\.\/\//, '/');
26
- const segments = xPath.split('/');
27
- // Initialize an array of normalized segments.
28
- const normalizedSegments = [];
29
- // For each segment of the XPath:
30
- segments.forEach(segment => {
31
- // If the segment is html[1] or body[1]:
32
- if (/html\[1\]|body\[1\]/.test(segment)) {
33
- // Add it without its subscript to the array.
34
- normalizedSegments.push(segment.replace(/\[1\]/, ''));
35
- }
36
- // Otherwise, if the segment is empty or html or body or ends with a subscript:
37
- else if (segment === '' || ['html', 'body'].includes(segment) || segment.endsWith(']')) {
38
- // Add it to the array.
39
- normalizedSegments.push(segment);
40
- }
41
- // Otherwise, i.e. if the segment is a tag name with no subscript:
42
- else {
43
- // Add it with a subscript 1 to the array.
44
- normalizedSegments.push(`${segment}[1]`);
45
- }
46
- });
47
- // If the final segment contains any nonstandard character:
48
- if (/[[^\]A-Za-z0-9]/.test(segments[segments.length - 1])) {
49
- // Remove it.
50
- normalizedSegments.pop();
56
+ else {
57
+ return '/html';
51
58
  }
52
- // Return the concatenated segments as the normalized XPath.
53
- return normalizedSegments.join('/');
54
- }
55
- else {
56
- return '/html';
57
- }
58
59
  };
60
+ exports.getNormalizedXPath = getNormalizedXPath;
59
61
  // Gets an XPath from a data-xpath attribute in an HTML excerpt.
60
- exports.getAttributeXPath = html => {
61
- // If there is no excerpt (e.g. a Nu Html Checker message without an extract):
62
- if (! html) {
63
- // Return the fallback XPath, as for an excerpt without a data-xpath attribute.
64
- return '/html';
65
- }
66
- const match = html.match(/ data-xpath="([^" ]+)"/);
67
- return match ? match[1] : '/html';
62
+ const getAttributeXPath = (html) => {
63
+ // If there is no excerpt (e.g. a Nu Html Checker message without an extract):
64
+ if (!html) {
65
+ // Return the fallback XPath, as for an excerpt without a data-xpath attribute.
66
+ return '/html';
67
+ }
68
+ const match = html.match(/ data-xpath="([^" ]+)"/);
69
+ return match ? match[1] : '/html';
68
70
  };
71
+ exports.getAttributeXPath = getAttributeXPath;
69
72
  // Gets a tag name from an XPath.
70
- const getXPathTagName = xPath => {
71
- return xPath.split('/').pop().replace(/\[.+/, '').toUpperCase();
73
+ const getXPathTagName = (xPath) => {
74
+ return xPath.split('/').pop().replace(/\[.+/, '').toUpperCase();
72
75
  };
73
- // Gets a catalog index as a string from an XPath.
74
- exports.getXPathCatalogIndex = (report, xPath) => {
75
- const {catalog} = report;
76
- report.pathIDs ??= {};
77
- // Get the index of the catalog item with the XPath.
78
- const index = report.pathIDs[xPath];
79
- // If no such item exists (an index of '0' is a match, so test for absence, not falsity):
80
- if (index === undefined) {
81
- // Add an item to the catalog, keyed by the count of items already in it.
82
- const newIndex = `${Object.keys(catalog).length}`;
83
- catalog[newIndex] = {
84
- pathID: xPath,
85
- tagName: getXPathTagName(xPath)
86
- };
87
- report.pathIDs[xPath] = newIndex;
88
- return newIndex;
89
- }
90
- return index;
76
+ // Gets a catalog index as a string from an XPath, within a checkpoint (the active one by
77
+ // default). An XPath the checkpoint's catalog lacks gets a stub entry, keyed from the report's
78
+ // monotonic counter so that entries of different checkpoints never collide.
79
+ const getXPathCatalogIndex = (report, xPath, checkpoint = report.activeCheckpoint ?? 0) => {
80
+ // The catalog always exists by the time tests cite elements (getCatalog ran first).
81
+ const { catalog } = report;
82
+ report.pathIDs ??= {};
83
+ const pathIDs = (report.pathIDs[checkpoint] ??= {});
84
+ // Get the index of the catalog item with the XPath.
85
+ const index = pathIDs[xPath];
86
+ // If no such item exists (an index of '0' is a match, so test for absence, not falsity):
87
+ if (index === undefined) {
88
+ // Add an item to the catalog, keyed by the counter (or, in a report without one, by the
89
+ // count of items already in it).
90
+ const newIndex = report.catalogNextIndex === undefined
91
+ ? `${Object.keys(catalog).length}`
92
+ : `${report.catalogNextIndex++}`;
93
+ catalog[newIndex] = {
94
+ pathID: xPath,
95
+ tagName: getXPathTagName(xPath),
96
+ checkpoint
97
+ };
98
+ pathIDs[xPath] = newIndex;
99
+ return newIndex;
100
+ }
101
+ return index;
91
102
  };
103
+ exports.getXPathCatalogIndex = getXPathCatalogIndex;