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
@@ -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,5 +1,7 @@
1
+ "use strict";
1
2
  /*
2
3
  © 2024 CVS Health and/or one of its affiliates. All rights reserved.
4
+ © 2026 Jeff Witt.
3
5
  © 2026 Jonathan Robert Pool.
4
6
 
5
7
  Licensed under the MIT License. See LICENSE file at the project root or
@@ -7,79 +9,95 @@
7
9
 
8
10
  SPDX-License-Identifier: MIT
9
11
  */
10
-
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.getXPathCatalogIndex = exports.getAttributeXPath = exports.getNormalizedXPath = void 0;
11
14
  /*
12
- xPath.js
13
- 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.
14
18
  */
15
-
16
19
  // FUNCTIONS
17
-
18
20
  // Normalizes an XPath.
19
- exports.getNormalizedXPath = xPath => {
20
- if (xPath) {
21
- if (xPath === '/') {
22
- 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('/');
23
55
  }
24
- xPath = xPath.replace(/^\.\/\//, '/');
25
- const segments = xPath.split('/');
26
- // Initialize an array of normalized segments.
27
- const normalizedSegments = [];
28
- // For each segment of the XPath:
29
- segments.forEach(segment => {
30
- // If the segment is html[1] or body[1]:
31
- if (/html\[1\]|body\[1\]/.test(segment)) {
32
- // Add it without its subscript to the array.
33
- normalizedSegments.push(segment.replace(/\[1\]/, ''));
34
- }
35
- // Otherwise, if the segment is empty or html or body or ends with a subscript:
36
- else if (segment === '' || ['html', 'body'].includes(segment) || segment.endsWith(']')) {
37
- // Add it to the array.
38
- normalizedSegments.push(segment);
39
- }
40
- // Otherwise, i.e. if the segment is a tag name with no subscript:
41
- else {
42
- // Add it with a subscript 1 to the array.
43
- normalizedSegments.push(`${segment}[1]`);
44
- }
45
- });
46
- // If the final segment contains any nonstandard character:
47
- if (/[[^\]A-Za-z0-9]/.test(segments[segments.length - 1])) {
48
- // Remove it.
49
- normalizedSegments.pop();
56
+ else {
57
+ return '/html';
50
58
  }
51
- // Return the concatenated segments as the normalized XPath.
52
- return normalizedSegments.join('/');
53
- }
54
- else {
55
- return '/html';
56
- }
57
59
  };
60
+ exports.getNormalizedXPath = getNormalizedXPath;
58
61
  // Gets an XPath from a data-xpath attribute in an HTML excerpt.
59
- exports.getAttributeXPath = html => {
60
- const match = html.match(/ data-xpath="([^" ]+)"/);
61
- 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';
62
70
  };
71
+ exports.getAttributeXPath = getAttributeXPath;
63
72
  // Gets a tag name from an XPath.
64
- const getXPathTagName = xPath => {
65
- return xPath.split('/').pop().replace(/\[.+/, '').toUpperCase();
73
+ const getXPathTagName = (xPath) => {
74
+ return xPath.split('/').pop().replace(/\[.+/, '').toUpperCase();
66
75
  };
67
- // Gets a catalog index as a string from an XPath.
68
- exports.getXPathCatalogIndex = (report, xPath) => {
69
- const {catalog} = report;
70
- report.pathIDs ??= {};
71
- // Get the index of the catalog item with the XPath.
72
- const index = report.pathIDs[xPath];
73
- // If no such item exists (an index of '0' is a match, so test for absence, not falsity):
74
- if (index === undefined) {
75
- // Add an item to the catalog, keyed by the count of items already in it.
76
- const newIndex = `${Object.keys(catalog).length}`;
77
- catalog[newIndex] = {
78
- pathID: xPath,
79
- tagName: getXPathTagName(xPath)
80
- };
81
- report.pathIDs[xPath] = newIndex;
82
- return newIndex;
83
- }
84
- 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;
85
102
  };
103
+ exports.getXPathCatalogIndex = getXPathCatalogIndex;