testaro 78.0.8 → 78.6.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 (357) hide show
  1. package/.gitattributes +94 -0
  2. package/.github/workflows/ci.yml +58 -0
  3. package/.github/workflows/publish.yml +50 -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 +143 -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/dist/nameComputation.js +1 -0
  16. package/docker-compose.yml +8 -2
  17. package/docs/checkpoint-scanning.md +199 -0
  18. package/docs/standard-result-outcome.md +165 -0
  19. package/env.example +49 -10
  20. package/eslint.config.mjs +101 -0
  21. package/netWatch.js +372 -222
  22. package/package.json +46 -31
  23. package/pour/README.md +47 -0
  24. package/pour/pour.min.js +8 -0
  25. package/procs/actDo.js +808 -0
  26. package/procs/catalog.d.ts +17 -0
  27. package/procs/catalog.js +312 -221
  28. package/procs/catalog.ts +398 -0
  29. package/procs/checkpoint.js +110 -0
  30. package/procs/config.d.ts +41 -0
  31. package/procs/config.js +98 -0
  32. package/procs/dateTime.js +2 -1
  33. package/procs/doActs.js +442 -849
  34. package/procs/doTestAct.js +27 -121
  35. package/procs/flow.js +221 -0
  36. package/procs/generateRuleRegistry.js +94 -0
  37. package/procs/getSource.d.ts +26 -0
  38. package/procs/job.js +115 -1
  39. package/procs/launch.d.ts +40 -0
  40. package/procs/launch.js +243 -108
  41. package/procs/nu.d.ts +48 -0
  42. package/procs/scope.js +196 -0
  43. package/procs/shoot.d.ts +33 -0
  44. package/procs/standard.d.ts +18 -0
  45. package/procs/standard.js +83 -0
  46. package/procs/standard.ts +126 -0
  47. package/procs/testAct.js +145 -0
  48. package/procs/testaro.d.ts +20 -0
  49. package/procs/testaro.js +236 -189
  50. package/procs/testaro.ts +321 -0
  51. package/procs/userPath.js +126 -0
  52. package/procs/xPath.d.ts +4 -0
  53. package/procs/xPath.js +81 -69
  54. package/procs/xPath.ts +106 -0
  55. package/procs/xPathScript.d.ts +6 -0
  56. package/procs/xPathScript.js +70 -0
  57. package/run.js +3 -2
  58. package/surea11y/README.md +44 -0
  59. package/surea11y/surea11y.browser.js +14 -0
  60. package/testaro/adbID.d.ts +3 -0
  61. package/testaro/adbID.js +38 -41
  62. package/testaro/adbID.ts +66 -0
  63. package/testaro/allCapStyle.d.ts +3 -0
  64. package/testaro/allCapStyle.js +31 -34
  65. package/testaro/allCapStyle.ts +55 -0
  66. package/testaro/allCaps.d.ts +16 -0
  67. package/testaro/allCaps.js +179 -151
  68. package/testaro/allCaps.ts +228 -0
  69. package/testaro/allHidden.d.ts +13 -0
  70. package/testaro/allHidden.js +29 -30
  71. package/testaro/allHidden.ts +50 -0
  72. package/testaro/allSlanted.d.ts +3 -0
  73. package/testaro/allSlanted.js +30 -33
  74. package/testaro/allSlanted.ts +54 -0
  75. package/testaro/altScheme.d.ts +3 -0
  76. package/testaro/altScheme.js +27 -30
  77. package/testaro/altScheme.ts +50 -0
  78. package/testaro/attVal.d.ts +3 -0
  79. package/testaro/attVal.js +20 -21
  80. package/testaro/attVal.ts +52 -0
  81. package/testaro/autocomplete.d.ts +3 -0
  82. package/testaro/autocomplete.js +59 -75
  83. package/testaro/autocomplete.ts +101 -0
  84. package/testaro/bulk.d.ts +13 -0
  85. package/testaro/bulk.js +32 -33
  86. package/testaro/bulk.ts +55 -0
  87. package/testaro/buttonMenu.d.ts +9 -0
  88. package/testaro/buttonMenu.js +317 -319
  89. package/testaro/buttonMenu.ts +391 -0
  90. package/testaro/captionLoc.d.ts +3 -0
  91. package/testaro/captionLoc.js +17 -20
  92. package/testaro/captionLoc.ts +40 -0
  93. package/testaro/datalistRef.d.ts +3 -0
  94. package/testaro/datalistRef.js +33 -36
  95. package/testaro/datalistRef.ts +55 -0
  96. package/testaro/distortion.d.ts +3 -0
  97. package/testaro/distortion.js +57 -26
  98. package/testaro/distortion.ts +81 -0
  99. package/testaro/docType.d.ts +15 -0
  100. package/testaro/docType.js +25 -25
  101. package/testaro/docType.ts +45 -0
  102. package/testaro/dupAtt.d.ts +16 -0
  103. package/testaro/dupAtt.js +113 -104
  104. package/testaro/dupAtt.ts +144 -0
  105. package/testaro/elements.d.ts +6 -0
  106. package/testaro/elements.js +153 -153
  107. package/testaro/elements.ts +215 -0
  108. package/testaro/embAc.d.ts +3 -0
  109. package/testaro/embAc.js +19 -20
  110. package/testaro/embAc.ts +40 -0
  111. package/testaro/focAll.d.ts +13 -0
  112. package/testaro/focAll.js +185 -191
  113. package/testaro/focAll.ts +217 -0
  114. package/testaro/focAndOp.d.ts +3 -0
  115. package/testaro/focAndOp.js +100 -104
  116. package/testaro/focAndOp.ts +128 -0
  117. package/testaro/focInd.d.ts +3 -0
  118. package/testaro/focInd.js +64 -66
  119. package/testaro/focInd.ts +96 -0
  120. package/testaro/focVis.d.ts +3 -0
  121. package/testaro/focVis.js +28 -30
  122. package/testaro/focVis.ts +52 -0
  123. package/testaro/headEl.d.ts +10 -0
  124. package/testaro/headEl.js +61 -62
  125. package/testaro/headEl.ts +82 -0
  126. package/testaro/headingAmb.d.ts +3 -0
  127. package/testaro/headingAmb.js +54 -63
  128. package/testaro/headingAmb.ts +72 -0
  129. package/testaro/hovInd.d.ts +12 -0
  130. package/testaro/hovInd.js +163 -130
  131. package/testaro/hovInd.ts +199 -0
  132. package/testaro/hover.d.ts +3 -0
  133. package/testaro/hover.js +155 -126
  134. package/testaro/hover.ts +154 -0
  135. package/testaro/hr.d.ts +3 -0
  136. package/testaro/hr.js +14 -17
  137. package/testaro/hr.ts +36 -0
  138. package/testaro/imageLink.d.ts +3 -0
  139. package/testaro/imageLink.js +17 -20
  140. package/testaro/imageLink.ts +42 -0
  141. package/testaro/labClash.d.ts +3 -0
  142. package/testaro/labClash.js +31 -33
  143. package/testaro/labClash.ts +54 -0
  144. package/testaro/legendLoc.d.ts +3 -0
  145. package/testaro/legendLoc.js +17 -20
  146. package/testaro/legendLoc.ts +42 -0
  147. package/testaro/lineHeight.d.ts +3 -0
  148. package/testaro/lineHeight.js +43 -48
  149. package/testaro/lineHeight.ts +70 -0
  150. package/testaro/linkAmb.d.ts +7 -0
  151. package/testaro/linkAmb.js +79 -80
  152. package/testaro/linkAmb.ts +105 -0
  153. package/testaro/linkExt.d.ts +3 -0
  154. package/testaro/linkExt.js +13 -16
  155. package/testaro/linkExt.ts +35 -0
  156. package/testaro/linkOldAtt.d.ts +3 -0
  157. package/testaro/linkOldAtt.js +25 -28
  158. package/testaro/linkOldAtt.ts +48 -0
  159. package/testaro/linkTo.d.ts +3 -0
  160. package/testaro/linkTo.js +22 -22
  161. package/testaro/linkTo.ts +43 -0
  162. package/testaro/linkUl.d.ts +3 -0
  163. package/testaro/linkUl.js +30 -34
  164. package/testaro/linkUl.ts +54 -0
  165. package/testaro/miniText.d.ts +3 -0
  166. package/testaro/miniText.js +41 -44
  167. package/testaro/miniText.ts +68 -0
  168. package/testaro/motion.d.ts +10 -0
  169. package/testaro/motion.js +92 -96
  170. package/testaro/motion.ts +125 -0
  171. package/testaro/nonTable.d.ts +3 -0
  172. package/testaro/nonTable.js +39 -45
  173. package/testaro/nonTable.ts +66 -0
  174. package/testaro/optRoleSel.d.ts +3 -0
  175. package/testaro/optRoleSel.js +16 -19
  176. package/testaro/optRoleSel.ts +41 -0
  177. package/testaro/phOnly.d.ts +3 -0
  178. package/testaro/phOnly.js +18 -21
  179. package/testaro/phOnly.ts +43 -0
  180. package/testaro/pseudoP.d.ts +3 -0
  181. package/testaro/pseudoP.js +36 -38
  182. package/testaro/pseudoP.ts +59 -0
  183. package/testaro/radioSet.d.ts +3 -0
  184. package/testaro/radioSet.js +57 -59
  185. package/testaro/radioSet.ts +79 -0
  186. package/testaro/registry.d.ts +63 -0
  187. package/testaro/registry.js +67 -0
  188. package/testaro/registry.ts +141 -0
  189. package/testaro/role.d.ts +3 -0
  190. package/testaro/role.js +27 -29
  191. package/testaro/role.ts +53 -0
  192. package/testaro/secHeading.d.ts +3 -0
  193. package/testaro/secHeading.js +30 -33
  194. package/testaro/secHeading.ts +53 -0
  195. package/testaro/styleDiff.d.ts +25 -0
  196. package/testaro/styleDiff.js +248 -252
  197. package/testaro/styleDiff.ts +303 -0
  198. package/testaro/tabNav.d.ts +33 -0
  199. package/testaro/tabNav.js +272 -342
  200. package/testaro/tabNav.ts +454 -0
  201. package/testaro/targetsNear.d.ts +9 -0
  202. package/testaro/targetsNear.js +130 -132
  203. package/testaro/targetsNear.ts +160 -0
  204. package/testaro/textNodes.d.ts +6 -0
  205. package/testaro/textNodes.js +139 -135
  206. package/testaro/textNodes.ts +185 -0
  207. package/testaro/textSem.d.ts +3 -0
  208. package/testaro/textSem.js +25 -28
  209. package/testaro/textSem.ts +47 -0
  210. package/testaro/title.d.ts +9 -0
  211. package/testaro/title.js +16 -13
  212. package/testaro/title.ts +31 -0
  213. package/testaro/titledEl.d.ts +3 -0
  214. package/testaro/titledEl.js +15 -18
  215. package/testaro/titledEl.ts +38 -0
  216. package/testaro/zIndex.d.ts +3 -0
  217. package/testaro/zIndex.js +19 -22
  218. package/testaro/zIndex.ts +42 -0
  219. package/tests/alfa.d.ts +45 -0
  220. package/tests/alfa.js +138 -141
  221. package/tests/alfa.ts +214 -0
  222. package/tests/aslint.d.ts +33 -0
  223. package/tests/aslint.js +272 -249
  224. package/tests/aslint.ts +301 -0
  225. package/tests/axe.d.ts +27 -0
  226. package/tests/axe.js +199 -200
  227. package/tests/axe.ts +277 -0
  228. package/tests/ed11y.d.ts +28 -0
  229. package/tests/ed11y.js +141 -99
  230. package/tests/ed11y.ts +178 -0
  231. package/tests/htmlcs.d.ts +21 -0
  232. package/tests/htmlcs.js +174 -140
  233. package/tests/htmlcs.ts +181 -0
  234. package/tests/ibm.d.ts +57 -0
  235. package/tests/ibm.js +182 -175
  236. package/tests/ibm.ts +287 -0
  237. package/tests/nuVal.d.ts +13 -0
  238. package/tests/nuVal.js +107 -112
  239. package/tests/nuVal.ts +145 -0
  240. package/tests/nuVnu.d.ts +15 -0
  241. package/tests/nuVnu.js +141 -111
  242. package/tests/nuVnu.ts +144 -0
  243. package/tests/pour.d.ts +31 -0
  244. package/tests/pour.js +242 -0
  245. package/tests/pour.ts +273 -0
  246. package/tests/qualWeb.d.ts +39 -0
  247. package/tests/qualWeb.js +262 -273
  248. package/tests/qualWeb.ts +344 -0
  249. package/tests/surea11y.d.ts +33 -0
  250. package/tests/surea11y.js +288 -0
  251. package/tests/surea11y.ts +334 -0
  252. package/tests/testaro.d.ts +25 -0
  253. package/tests/testaro.js +745 -652
  254. package/tests/testaro.ts +862 -0
  255. package/tests/wave.d.ts +46 -0
  256. package/tests/wave.js +166 -177
  257. package/tests/wave.ts +252 -0
  258. package/tsconfig.json +17 -0
  259. package/types.d.ts +243 -0
  260. package/types.js +10 -0
  261. package/types.ts +376 -0
  262. package/validation/act/README.md +46 -0
  263. package/validation/act/capture.js +877 -0
  264. package/validation/act/chromium-issue-draft.md +66 -0
  265. package/validation/act/fp-triage-2026-08-22.md +86 -0
  266. package/validation/act/isolation-notes.md +159 -0
  267. package/validation/act/playwright-issue-draft.md +73 -0
  268. package/validation/act/propose-mappings.js +0 -0
  269. package/validation/act/repro-cdp-raw.js +105 -0
  270. package/validation/act/repro-metarefresh.js +65 -0
  271. package/validation/act/score.js +184 -0
  272. package/validation/act/stage3a-stress-report.md +69 -0
  273. package/validation/act/stage3b-mapping-proposals.md +94 -0
  274. package/validation/act/stage3b-triage-draft.md +138 -0
  275. package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
  276. package/validation/executors/netWatch.js +180 -90
  277. package/validation/executors/test.js +17 -2
  278. package/validation/executors/tests.js +118 -10
  279. package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
  280. package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
  281. package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
  282. package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
  283. package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
  284. package/validation/knownFailures.json +1 -0
  285. package/validation/tests/jobProperties/adbID.json +27 -2
  286. package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
  287. package/validation/tests/jobProperties/allCaps.json +25 -0
  288. package/validation/tests/jobProperties/allHidden.json +133 -13
  289. package/validation/tests/jobProperties/allSlanted.json +3 -3
  290. package/validation/tests/jobProperties/altScheme.json +23 -8
  291. package/validation/tests/jobProperties/attVal.json +57 -57
  292. package/validation/tests/jobProperties/autocomplete.json +11 -1
  293. package/validation/tests/jobProperties/bulk.json +6 -1
  294. package/validation/tests/jobProperties/buttonMenu.json +63 -42
  295. package/validation/tests/jobProperties/captionLoc.json +1 -6
  296. package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
  297. package/validation/tests/jobProperties/checkpoint-page.json +397 -0
  298. package/validation/tests/jobProperties/checkpoint.json +395 -0
  299. package/validation/tests/jobProperties/datalistRef.json +20 -5
  300. package/validation/tests/jobProperties/distortion.json +28 -3
  301. package/validation/tests/jobProperties/docType.json +2 -2
  302. package/validation/tests/jobProperties/dupAtt.json +48 -33
  303. package/validation/tests/jobProperties/elements.json +28 -28
  304. package/validation/tests/jobProperties/embAc.json +36 -31
  305. package/validation/tests/jobProperties/focAndOp.json +284 -0
  306. package/validation/tests/jobProperties/focInd.json +34 -34
  307. package/validation/tests/jobProperties/focVis.json +1 -1
  308. package/validation/tests/jobProperties/hover.json +39 -37
  309. package/validation/tests/jobProperties/hr.json +13 -3
  310. package/validation/tests/jobProperties/imageLink.json +0 -5
  311. package/validation/tests/jobProperties/labClash.json +68 -33
  312. package/validation/tests/jobProperties/legendLoc.json +1 -6
  313. package/validation/tests/jobProperties/lineHeight.json +2 -2
  314. package/validation/tests/jobProperties/linkAmb.json +13 -18
  315. package/validation/tests/jobProperties/linkExt.json +1 -1
  316. package/validation/tests/jobProperties/linkOldAtt.json +12 -2
  317. package/validation/tests/jobProperties/linkTo.json +1 -1
  318. package/validation/tests/jobProperties/linkUl.json +65 -65
  319. package/validation/tests/jobProperties/miniText.json +7 -2
  320. package/validation/tests/jobProperties/motion.json +4 -50
  321. package/validation/tests/jobProperties/nonTable.json +48 -3
  322. package/validation/tests/jobProperties/optRoleSel.json +12 -2
  323. package/validation/tests/jobProperties/phOnly.json +6 -16
  324. package/validation/tests/jobProperties/pseudoP.json +17 -2
  325. package/validation/tests/jobProperties/radioSet.json +30 -30
  326. package/validation/tests/jobProperties/role.json +25 -5
  327. package/validation/tests/jobProperties/secHeading.json +26 -21
  328. package/validation/tests/jobProperties/styleDiff.json +35 -5
  329. package/validation/tests/jobProperties/tabNav.json +3 -1
  330. package/validation/tests/jobProperties/targetsNear.json +237 -0
  331. package/validation/tests/jobProperties/textNodes.json +29 -29
  332. package/validation/tests/jobProperties/textSem.json +26 -1
  333. package/validation/tests/jobProperties/title.json +12 -2
  334. package/validation/tests/jobProperties/titledEl.json +39 -9
  335. package/validation/tests/jobProperties/userPath.json +317 -0
  336. package/validation/tests/jobProperties/zIndex.json +40 -35
  337. package/validation/tests/targets/allCapStyle/index.html +26 -0
  338. package/validation/tests/targets/checkpoint/index.html +38 -0
  339. package/validation/tests/targets/datalistRef/index.html +1 -1
  340. package/validation/tests/targets/focAndOp/bad.html +29 -0
  341. package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
  342. package/validation/tests/targets/focInd/bad.html +2 -1
  343. package/validation/tests/targets/headEl/index.html +10 -1
  344. package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
  345. package/validation/tests/targets/userPath/index.html +42 -0
  346. package/validation/validateTest.js +61 -9
  347. package/.claude/settings.local.json +0 -11
  348. package/.eslintrc.json +0 -41
  349. package/htmlcs/.eslintrc.json +0 -67
  350. package/validation/tests/jobProperties/linkTitle.json +0 -127
  351. package/validation/tests/jobProperties/opFoc.json +0 -164
  352. package/validation/tests/jobProperties/targetSmall.json +0 -152
  353. package/validation/tests/jobProperties/targetTiny.json +0 -142
  354. package/validation/tests/targets/focOp/bad.html +0 -25
  355. package/validation/tests/targets/linkTitle/index.html +0 -24
  356. package/validation/tests/targets/opFoc/bad.html +0 -26
  357. package/validation/tests/targets/opFoc/good.html +0 -23
@@ -0,0 +1,344 @@
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
7
+ https://opensource.org/license/mit/ for details.
8
+
9
+ SPDX-License-Identifier: MIT
10
+ */
11
+
12
+ // IMPORTS
13
+
14
+ import type {Page} from 'playwright';
15
+ import {getAttributeXPath, getXPathCatalogIndex} from '../procs/xPath';
16
+ import {getStandardResult, addInstance} from '../procs/standard';
17
+ import type {Act, Report, StandardInstance, StandardResult} from '../types';
18
+ import {readFileSync} from 'fs';
19
+
20
+ // TYPES
21
+
22
+ // The qualWeb-act properties this reporter consumes.
23
+ interface QualWebAct extends Act {
24
+ rules?: string[];
25
+ }
26
+ // The section names of a QualWeb report.
27
+ type QwSection = 'act-rules' | 'wcag-techniques' | 'best-practices';
28
+ // One element reported by a QualWeb assertion result.
29
+ interface QwElement {
30
+ htmlCode?: string;
31
+ }
32
+ // One result of a QualWeb rule assertion.
33
+ interface QwRaResult {
34
+ verdict: string;
35
+ description?: string;
36
+ elements?: QwElement[];
37
+ }
38
+ // The assertions for one QualWeb rule.
39
+ interface QwRuleAssertions {
40
+ metadata?: {
41
+ warning?: number;
42
+ failed?: number;
43
+ };
44
+ results: QwRaResult[];
45
+ }
46
+ // One module section of a QualWeb report.
47
+ interface QwModuleReport {
48
+ assertions?: Record<string, QwRuleAssertions>;
49
+ }
50
+ // The native result: the module reports, keyed by section, as in QualWeb's own report.
51
+ interface QwNativeResult {
52
+ system?: {url?: string; evaluation?: string};
53
+ modules?: Record<string, QwModuleReport>;
54
+ }
55
+ // The options of one section's runner (QualWeb's EvaluationModule options).
56
+ interface QwSectionOptions {
57
+ include?: string[];
58
+ exclude?: string[];
59
+ levels?: string[];
60
+ principles?: string[];
61
+ }
62
+
63
+ // CONSTANTS
64
+
65
+ /*
66
+ QualWeb runs in the page under test, as @qualweb/core runs it in its own browser: the
67
+ page-side bundles first, then one bundle per section. It used to be given page.content() and
68
+ run by @qualweb/core, which loads that HTML with setContent() into a blank page — so relative
69
+ stylesheet links resolved nowhere, the page it tested was unstyled, and every CSS-dependent
70
+ rule (contrast, visibility, text spacing…) judged a page the user never sees. The bundles are
71
+ evaluated (not added as script tags), so the page's Content Security Policy does not apply.
72
+ */
73
+ const readBundle = (name: string): string => readFileSync(require.resolve(name), 'utf8');
74
+ let pageBundles: string[] | null = null;
75
+ const sectionBundles: Partial<Record<QwSection, string>> = {};
76
+ const sectionPackages: Record<QwSection, string> = {
77
+ 'act-rules': '@qualweb/act-rules',
78
+ 'wcag-techniques': '@qualweb/wcag-techniques',
79
+ 'best-practices': '@qualweb/best-practices'
80
+ };
81
+ // Rules a section may exclude after they crash before the section is given up on.
82
+ const MAX_EXCLUDED = 5;
83
+ // Mapping of QualWeb module violation types to ordinal severities.
84
+ const ordinalSeverities: Record<QwSection, Record<string, StandardInstance['ordinalSeverity']>> = {
85
+ 'act-rules': {
86
+ 'warning': 1,
87
+ 'failed': 3
88
+ },
89
+ 'wcag-techniques': {
90
+ 'warning': 0,
91
+ 'failed': 2
92
+ },
93
+ 'best-practices': {
94
+ 'warning': 0,
95
+ 'failed': 1
96
+ }
97
+ }
98
+
99
+ /*
100
+ qualWeb
101
+ Implements the QualWeb ruleset for accessibility.
102
+ Compiled to qualWeb.js by tsc (issue #73); edit this file, not the emitted one.
103
+ */
104
+
105
+ // FUNCTIONS
106
+
107
+ // Conducts and reports the QualWeb tests.
108
+ export const reporter = async (page: Page, report: Report, actIndex: number, timeLimit: number) => {
109
+ const act = report.acts[actIndex] as QualWebAct;
110
+ const {rules} = act;
111
+ // Initialize the act report.
112
+ const data: {prevented?: boolean; error?: string; rulePreventions?: Record<string, string>} = {};
113
+ const result: {nativeResult: QwNativeResult; standardResult: StandardResult} = {
114
+ nativeResult: {},
115
+ standardResult: {}
116
+ };
117
+ const standard = report.standard !== 'no';
118
+ // If standard results are to be reported:
119
+ if (standard) {
120
+ // Initialize the standard result.
121
+ result.standardResult = getStandardResult();
122
+ }
123
+ /*
124
+ QualWeb tests HTML documents only: on anything else (e.g. an SVG image) its rules report
125
+ false results such as a missing lang on the html element.
126
+ */
127
+ const rootNamespace = await page.evaluate(() => document.documentElement?.namespaceURI ?? null)
128
+ .catch(() => null);
129
+ // If the document is not HTML:
130
+ if (rootNamespace !== 'http://www.w3.org/1999/xhtml') {
131
+ // Report this.
132
+ return {
133
+ data: {
134
+ prevented: true,
135
+ error: `qualWeb tests HTML documents only (root namespace ${rootNamespace})`
136
+ },
137
+ result
138
+ };
139
+ }
140
+ // Specify, per section, whether and what to test (a rules spec of 'act:', 'wcag:' or 'best:'
141
+ // alone skips that section; with numbers, it tests only those rules).
142
+ const allAAA = {
143
+ levels: ['A', 'AA', 'AAA'],
144
+ principles: ['Perceivable', 'Operable', 'Understandable', 'Robust']
145
+ };
146
+ const sectionSpec = (prefix: string, codePrefix: string, all: QwSectionOptions): QwSectionOptions | null => {
147
+ const spec = rules?.find(typeRules => typeRules.startsWith(prefix));
148
+ if (spec === undefined) {
149
+ return all;
150
+ }
151
+ if (spec === prefix) {
152
+ return null;
153
+ }
154
+ return {include: spec.slice(prefix.length).split(',').map(num => `${codePrefix}${num}`)};
155
+ };
156
+ const sections: Partial<Record<QwSection, QwSectionOptions>> = {};
157
+ for (const [section, options] of [
158
+ ['act-rules', sectionSpec('act:', 'QW-ACT-R', allAAA)],
159
+ ['wcag-techniques', sectionSpec('wcag:', 'QW-WCAG-T', allAAA)],
160
+ ['best-practices', sectionSpec('best:', 'QW-BP', {})]
161
+ ] as [QwSection, QwSectionOptions | null][]) {
162
+ if (options) {
163
+ sections[section] = options;
164
+ }
165
+ }
166
+ const deadline = Date.now() + timeLimit * 1000;
167
+ try {
168
+ // Load QualWeb into the page.
169
+ pageBundles ??= ['@qualweb/qw-page', '@qualweb/util', '@qualweb/locale'].map(readBundle);
170
+ for (const source of pageBundles) {
171
+ await page.evaluate(source);
172
+ }
173
+ // The page's own source, which QualWeb's meta-element rules read.
174
+ const sourceHtml = await page.content();
175
+ const modules: Record<string, QwModuleReport> = {};
176
+ const rulePreventions: Record<string, string> = {};
177
+ // For each section to be tested:
178
+ for (const [section, options] of Object.entries(sections) as [QwSection, QwSectionOptions][]) {
179
+ if (Date.now() > deadline) {
180
+ throw new Error(`time limit of ${timeLimit}s reached before ${section}`);
181
+ }
182
+ sectionBundles[section] ??= readBundle(sectionPackages[section]);
183
+ await page.evaluate(sectionBundles[section]!);
184
+ /*
185
+ One rule that throws takes its whole section down with it (e.g. QW-ACT-R76 in 0.8.5 on
186
+ lab(), oklab(), color() or color-mix() colours, qualweb/qualweb#352), so a rule named in
187
+ the stack trace of a crash is excluded and the section rerun; a crash that names no rule
188
+ prevents only its section.
189
+ */
190
+ const outcome = await page.evaluate(({section, options, sourceHtml, maxExcluded}) => {
191
+ // Only this section's bundle has been loaded, so name only its runner.
192
+ const Runner = section === 'act-rules'
193
+ // @ts-expect-error: defined by the section bundle evaluated above.
194
+ ? ACTRulesRunner
195
+ : section === 'wcag-techniques'
196
+ // @ts-expect-error: defined by the section bundle evaluated above.
197
+ ? WCAGTechniquesRunner
198
+ // @ts-expect-error: defined by the section bundle evaluated above.
199
+ : BestPracticesRunner;
200
+ const preventions: Record<string, string> = {};
201
+ const exclude: string[] = [];
202
+ for (;;) {
203
+ try {
204
+ const opts = exclude.length ? {...options, exclude: [...(options.exclude ?? []), ...exclude]} : options;
205
+ const runner = new Runner(opts, 'en').configure(opts).test({sourceHtml, newTabWasOpen: false});
206
+ (window as any).__testaroQwRunners = {...(window as any).__testaroQwRunners, [section]: runner};
207
+ // QualWeb core keeps the act-rules runner here for its special-case pass.
208
+ if (section === 'act-rules') {
209
+ (window as any).act = runner;
210
+ }
211
+ return {ok: true, preventions};
212
+ }
213
+ catch(error) {
214
+ const message = String((error as Error)?.message ?? error).slice(0, 200);
215
+ // Rule classes are named QW_ACT_R76, QW_WCAG_T23, QW_BP1.
216
+ const rule = String((error as Error)?.stack ?? '').match(/QW_(?:ACT_R|WCAG_T|BP)\d+/)?.[0].replace(/_/g, '-');
217
+ if (! rule || exclude.includes(rule) || exclude.length >= maxExcluded) {
218
+ preventions[section] = `section failed: ${message}`;
219
+ return {ok: false, preventions};
220
+ }
221
+ preventions[rule] = message;
222
+ exclude.push(rule);
223
+ }
224
+ }
225
+ }, {section, options, sourceHtml, maxExcluded: MAX_EXCLUDED});
226
+ Object.assign(rulePreventions, outcome.preventions);
227
+ if (! outcome.ok) {
228
+ continue;
229
+ }
230
+ /*
231
+ QualWeb core's special case for act-rules: QW-ACT-R40 (zoomed text not clipped) is
232
+ re-tested at a 640 × 512 viewport, then the viewport is restored.
233
+ */
234
+ const include = options.include;
235
+ if (section === 'act-rules' && (! include || include.includes('QW-ACT-R40'))) {
236
+ const viewport = page.viewportSize();
237
+ if (viewport) {
238
+ try {
239
+ await page.setViewportSize({width: 640, height: 512});
240
+ await page.evaluate(() => (window as any).act?.testSpecial?.());
241
+ }
242
+ catch(error) {
243
+ rulePreventions['QW-ACT-R40'] = `special-case pass failed: ${String((error as Error).message).slice(0, 200)}`;
244
+ }
245
+ finally {
246
+ await page.setViewportSize(viewport);
247
+ }
248
+ }
249
+ }
250
+ modules[section] = await page.evaluate(
251
+ section => JSON.parse(JSON.stringify((window as any).__testaroQwRunners[section].getReport())),
252
+ section
253
+ );
254
+ }
255
+ if (Object.keys(rulePreventions).length) {
256
+ data.rulePreventions = rulePreventions;
257
+ }
258
+ // Add the module reports to the result.
259
+ result.nativeResult = {system: {url: page.url(), evaluation: 'in-page'}, modules};
260
+ const {standardResult} = result;
261
+ // For each section that was to be tested:
262
+ for (const section of Object.keys(sections) as QwSection[]) {
263
+ const moduleReport = modules[section];
264
+ /*
265
+ A section that failed as a whole is recorded in rulePreventions; the act is prevented
266
+ only if no section ran.
267
+ */
268
+ if (! moduleReport) {
269
+ continue;
270
+ }
271
+ const {assertions} = moduleReport;
272
+ // If it contains assertions (test results):
273
+ if (assertions) {
274
+ const ruleIDs = Object.keys(assertions);
275
+ // For each rule:
276
+ for (const ruleID of ruleIDs) {
277
+ const ruleAssertions = assertions[ruleID];
278
+ const {metadata} = ruleAssertions;
279
+ // If there were any warnings or failures:
280
+ if (metadata?.warning || metadata?.failed) {
281
+ // Delete nonviolations from the results.
282
+ ruleAssertions.results = ruleAssertions.results.filter(
283
+ raResult => raResult.verdict !== 'passed'
284
+ );
285
+ // For each test result:
286
+ for (const raResult of ruleAssertions.results) {
287
+ const {elements, verdict} = raResult;
288
+ // If any violations are reported:
289
+ if (elements?.length) {
290
+ // For each violating element:
291
+ for (const element of elements) {
292
+ // Limit the size of its reported excerpt.
293
+ if ((element.htmlCode?.length as number) > 2000) {
294
+ element.htmlCode = `${element.htmlCode!.slice(0, 2000)} …`;
295
+ }
296
+ // If standard results are to be reported:
297
+ if (standard) {
298
+ const what = `[${verdict}] ${raResult.description}`;
299
+ const xPath = getAttributeXPath(element.htmlCode);
300
+ // Add an instance to the standard result.
301
+ addInstance(standardResult, {
302
+ ruleID,
303
+ what,
304
+ ordinalSeverity: ordinalSeverities[section][verdict],
305
+ outcome: verdict === 'warning' ? 'cantTell' : 'failed',
306
+ catalogIndex: getXPathCatalogIndex(report, xPath)
307
+ });
308
+ }
309
+ };
310
+ }
311
+ };
312
+ }
313
+ // Otherwise, i.e. if there were no warnings or failures:
314
+ else {
315
+ // Delete the rule.
316
+ delete assertions[ruleID];
317
+ }
318
+ };
319
+ }
320
+ }
321
+ // If no section ran:
322
+ if (! Object.keys(modules).length) {
323
+ // Report this.
324
+ data.prevented = true;
325
+ data.error = `No qualWeb section ran (${JSON.stringify(rulePreventions).slice(0, 300)})`;
326
+ }
327
+ // Test whether the result is an object.
328
+ try {
329
+ JSON.stringify(result);
330
+ }
331
+ catch(error) {
332
+ data.prevented = true;
333
+ data.error = `QualWeb result cannot be made JSON (${(error as Error).message})`;
334
+ }
335
+ }
336
+ catch(error) {
337
+ data.prevented = true;
338
+ data.error = `QualWeb failed (${(error as Error).message})`;
339
+ }
340
+ return {
341
+ data,
342
+ result
343
+ };
344
+ };
@@ -0,0 +1,33 @@
1
+ import type { Page } from 'playwright';
2
+ import type { Report, StandardResult } from '../types';
3
+ type SureSeverity = 'minor' | 'moderate' | 'serious' | 'critical';
4
+ interface SureFinding {
5
+ ruleID: string;
6
+ what: string;
7
+ wcag: string;
8
+ severity: SureSeverity;
9
+ uncertainty: string;
10
+ needed: string;
11
+ html: string;
12
+ cssPath: string;
13
+ xPath: string;
14
+ }
15
+ interface SureNativeResult {
16
+ engine?: Record<string, unknown>;
17
+ violations?: SureFinding[];
18
+ incomplete?: SureFinding[];
19
+ passRuleCount?: number;
20
+ inapplicableRuleCount?: number;
21
+ manualRuleCount?: number;
22
+ manualOccurrenceCount?: number;
23
+ prevented?: boolean;
24
+ error?: string;
25
+ }
26
+ export declare const reporter: (page: Page, report: Report, actIndex: number) => Promise<{
27
+ data: Record<string, unknown>;
28
+ result: {
29
+ nativeResult: SureNativeResult;
30
+ standardResult: StandardResult;
31
+ };
32
+ }>;
33
+ export {};
@@ -0,0 +1,288 @@
1
+ "use strict";
2
+ /*
3
+ © 2026 Jeff Witt.
4
+
5
+ Licensed under the MIT License. See LICENSE file at the project root or
6
+ https://opensource.org/license/mit/ for details.
7
+
8
+ SPDX-License-Identifier: MIT
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.reporter = void 0;
45
+ // IMPORTS
46
+ const fs = __importStar(require("fs/promises"));
47
+ const xPath_1 = require("../procs/xPath");
48
+ const standard_1 = require("../procs/standard");
49
+ /*
50
+ surea11y
51
+ Implements the SureA11y core engine (https://github.com/SureA11y/core) WCAG ruleset.
52
+ Compiled to surea11y.js by tsc (issue #73); edit this file, not the emitted one.
53
+
54
+ Upstream publishes a self-contained standalone browser bundle, vendored verbatim at
55
+ ../surea11y/surea11y.browser.js (MPL-2.0), exposing an `a11ycore` global with
56
+ `runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly)`. See
57
+ ../surea11y/README.md for the pinned version and verified result shape.
58
+
59
+ Instance policy (engine-candidacy-pipeline.md, Stage 1): `fail`-tier occurrences become
60
+ instances with outcome `failed` at ordinalSeverity 2-3; automatic-rule `cantTell`-tier
61
+ occurrences become instances with outcome `cantTell` at 0-1 (the engine grades per occurrence
62
+ via `occurrenceOutcome`, which takes precedence over the rule-level outcome), carrying the
63
+ engine's `uncertainty` code and `needed` guidance when it supplies them; `pass` and
64
+ `notApplicable` results and all `type: 'manual'` rules are tallied in `data` only and never
65
+ become instances.
66
+ */
67
+ // CONSTANTS
68
+ // Severity weights within a certainty band, mirroring tests/axe.ts.
69
+ const severityWeights = {
70
+ minor: 0,
71
+ moderate: 0,
72
+ serious: 1,
73
+ critical: 1
74
+ };
75
+ // FUNCTIONS
76
+ // Performs and reports the SureA11y tests.
77
+ const reporter = async (page, report, actIndex) => {
78
+ // Get the nonce, if any.
79
+ const act = report.acts[actIndex];
80
+ const { jobData } = report;
81
+ const scriptNonce = (jobData && jobData.lastScriptNonce);
82
+ // Initialize the act report.
83
+ let data = {};
84
+ const result = {
85
+ nativeResult: {},
86
+ standardResult: {}
87
+ };
88
+ const standard = report.standard !== 'no';
89
+ // If standard results are to be reported:
90
+ if (standard) {
91
+ // Initialize the standard result.
92
+ result.standardResult = (0, standard_1.getStandardResult)();
93
+ }
94
+ // Get the vendored tool bundle.
95
+ let script;
96
+ try {
97
+ script = await fs.readFile(`${__dirname}/../surea11y/surea11y.browser.js`, 'utf8');
98
+ }
99
+ catch (error) {
100
+ data.prevented = true;
101
+ data.error = 'ERROR: surea11y bundle missing (see surea11y/README.md for vendoring instructions)';
102
+ return { data, result };
103
+ }
104
+ // If the act is scoped to changed subtrees, the engine tests their nearest common ancestor
105
+ // (its context is one selector, resolved with querySelector).
106
+ const contextSelector = report.scope?.commonRoot || null;
107
+ // Perform the tests and populate the native result.
108
+ result.nativeResult = await page.evaluate(args => new Promise(async (resolve) => {
109
+ const { scriptNonce, script, contextSelector } = args;
110
+ // Detect page-CSP blocking of the injection (the violation event is
111
+ // dispatched as a task, so the check below waits a tick).
112
+ let cspBlocked = false;
113
+ document.addEventListener('securitypolicyviolation', event => {
114
+ if ((event.violatedDirective || '').startsWith('script-src')) {
115
+ cspBlocked = true;
116
+ }
117
+ });
118
+ // Add the tool script to the page.
119
+ const toolScript = document.createElement('script');
120
+ if (scriptNonce) {
121
+ toolScript.nonce = scriptNonce;
122
+ console.log(`Added nonce ${scriptNonce} to tool script`);
123
+ }
124
+ toolScript.textContent = script;
125
+ document.body.insertAdjacentElement('beforeend', toolScript);
126
+ // If the bundle failed to define its global:
127
+ if (typeof a11ycore === 'undefined' || typeof a11ycore.runa11yCoreInPage !== 'function') {
128
+ // Let a pending securitypolicyviolation event fire before diagnosing.
129
+ await new Promise(tick => setTimeout(tick, 0));
130
+ // A CSP block is a per-page condition, not a browser fault — report it
131
+ // distinctly (wedge canaries must not match this message).
132
+ resolve({
133
+ prevented: true,
134
+ error: cspBlocked
135
+ ? 'surea11y injection blocked by page CSP (script-src)'
136
+ : `a11ycore global not defined (contentType ${document.contentType}, `
137
+ + `scriptChars ${script.length}, readyState ${document.readyState})`
138
+ });
139
+ return;
140
+ }
141
+ try {
142
+ // Run the engine on the whole page with default options — every loaded
143
+ // rule at the default WCAG 2.2 target, matching the axe and pour
144
+ // adapters' kitchen-sink default; testilo's taxonomy decides per rule
145
+ // what counts downstream. runa11yCoreInPage is synchronous; await
146
+ // tolerates a future promise-returning version.
147
+ const sureReport = await a11ycore.runa11yCoreInPage(location.href, contextSelector, {}, null);
148
+ // Dotted WCAG criterion (e.g. 1.4.3) from a rule's normative mappings.
149
+ const criterionOf = (meta) => {
150
+ const mappings = meta && meta.normativeMappings || [];
151
+ for (const mapping of mappings) {
152
+ const requirement = String(mapping.requirement || '').replace(/^SC\s*/i, '');
153
+ if (/^\d+\.\d+\.\d+$/.test(requirement)) {
154
+ return requirement;
155
+ }
156
+ }
157
+ return '';
158
+ };
159
+ const violations = [];
160
+ const incomplete = [];
161
+ // Tallied-only buckets.
162
+ let passRuleCount = 0;
163
+ let inapplicableRuleCount = 0;
164
+ let manualRuleCount = 0;
165
+ let manualOccurrenceCount = 0;
166
+ // For each atomic rule that ran:
167
+ (sureReport.checksResults || []).forEach(check => {
168
+ const { ruleId, outcome, type, occurrences } = check;
169
+ // Manual rules are advisory by definition: tally only.
170
+ if (type === 'manual') {
171
+ manualRuleCount++;
172
+ manualOccurrenceCount += (occurrences || []).length;
173
+ return;
174
+ }
175
+ if (outcome === 'pass') {
176
+ passRuleCount++;
177
+ return;
178
+ }
179
+ // A notApplicable result may carry one scan-describing occurrence
180
+ // (empty selector) — never an element finding.
181
+ if (outcome === 'notApplicable') {
182
+ inapplicableRuleCount++;
183
+ return;
184
+ }
185
+ // fail or cantTell: convert occurrences to flat findings, honoring
186
+ // per-occurrence grading when the rule tiered its findings.
187
+ (occurrences || []).forEach(occurrence => {
188
+ const tier = occurrence.occurrenceOutcome || outcome;
189
+ const cssPath = occurrence.selector || '';
190
+ let xPath = '';
191
+ try {
192
+ const element = cssPath && document.querySelector(cssPath);
193
+ if (element && typeof window.getXPath === 'function') {
194
+ xPath = window.getXPath(element) ?? '';
195
+ }
196
+ }
197
+ catch (error) {
198
+ // Unresolvable path: instance falls back to excerpt only.
199
+ }
200
+ // Reason for uncertainty: per occurrence, else per check.
201
+ const uncertainty = occurrence.uncertainty || check.uncertainty || {};
202
+ const finding = {
203
+ ruleID: ruleId || '',
204
+ what: (occurrence.summary || check.title || '').replace(/\s+/g, ' ').trim(),
205
+ wcag: criterionOf(check.meta),
206
+ severity: (check.severity || 'moderate').toLowerCase(),
207
+ uncertainty: tier === 'cantTell' ? (uncertainty.code || '') : '',
208
+ needed: tier === 'cantTell' ? (uncertainty.needed || '') : '',
209
+ html: (occurrence.html || '').slice(0, 500),
210
+ cssPath,
211
+ xPath
212
+ };
213
+ (tier === 'fail' ? violations : incomplete).push(finding);
214
+ });
215
+ });
216
+ resolve({
217
+ engine: sureReport.engine || {},
218
+ violations,
219
+ incomplete,
220
+ passRuleCount,
221
+ inapplicableRuleCount,
222
+ manualRuleCount,
223
+ manualOccurrenceCount
224
+ });
225
+ }
226
+ catch (error) {
227
+ resolve({
228
+ prevented: true,
229
+ error: error.message
230
+ });
231
+ }
232
+ ;
233
+ }), {
234
+ scriptNonce,
235
+ script,
236
+ contextSelector
237
+ });
238
+ const { nativeResult } = result;
239
+ // If the tool ran and rules were selected:
240
+ if (!nativeResult.prevented && act.rules) {
241
+ // Remove results of other rules.
242
+ const { rules } = act;
243
+ nativeResult.violations = (nativeResult.violations || [])
244
+ .filter(finding => rules.includes(finding.ruleID));
245
+ nativeResult.incomplete = (nativeResult.incomplete || [])
246
+ .filter(finding => rules.includes(finding.ruleID));
247
+ }
248
+ // If the tool was prevented from running:
249
+ if (nativeResult.prevented) {
250
+ data.prevented = true;
251
+ data.error = nativeResult.error;
252
+ }
253
+ // Otherwise, if a standard result is to be reported:
254
+ else if (standard) {
255
+ const { standardResult } = result;
256
+ // Record the tallied-only bucket sizes.
257
+ data.passRuleCount = nativeResult.passRuleCount;
258
+ data.inapplicableRuleCount = nativeResult.inapplicableRuleCount;
259
+ data.manualRuleCount = nativeResult.manualRuleCount;
260
+ data.manualOccurrenceCount = nativeResult.manualOccurrenceCount;
261
+ // For each certainty band:
262
+ const bands = [
263
+ [nativeResult.incomplete || [], 0, 'cantTell'],
264
+ [nativeResult.violations || [], 2, 'failed']
265
+ ];
266
+ bands.forEach(([findings, baseSeverity, outcome]) => {
267
+ // For each native-result finding:
268
+ findings.forEach(finding => {
269
+ // Add a standard-result instance, forwarding only a known uncertainty code.
270
+ const { ruleID, what, severity, uncertainty, needed, xPath } = finding;
271
+ (0, standard_1.addInstance)(standardResult, {
272
+ ruleID,
273
+ what,
274
+ ordinalSeverity: baseSeverity + (severityWeights[severity] ?? 0),
275
+ outcome,
276
+ uncertainty: standard_1.UNCERTAINTY_CODES.includes(uncertainty) ? uncertainty : undefined,
277
+ needed,
278
+ catalogIndex: (0, xPath_1.getXPathCatalogIndex)(report, xPath)
279
+ });
280
+ });
281
+ });
282
+ }
283
+ return {
284
+ data,
285
+ result
286
+ };
287
+ };
288
+ exports.reporter = reporter;