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,391 @@
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 {Locator, Page} from 'playwright';
15
+ import {getXPathCatalogIndex} from '../procs/xPath';
16
+ import type {Report, StandardInstance} from '../types';
17
+
18
+ // TYPES
19
+
20
+ // Location of an element, as recorded in locator data.
21
+ interface LocatorLocation {
22
+ doc: string;
23
+ type: string;
24
+ spec: string | Record<string, number>;
25
+ }
26
+ // Data on the element referenced by a locator.
27
+ interface LocatorData {
28
+ tagName: string;
29
+ id: string;
30
+ location: LocatorLocation;
31
+ excerpt: string;
32
+ }
33
+
34
+ /*
35
+ buttonMenu
36
+ This test reports nonstandard navigation among menu items of button-controlled menus. Standards are based on https://www.w3.org/TR/wai-aria-practices-1.1/#menu. The trialKeys argument is an array of strings, each of which may be 'Home', 'End', '+', or '-'. The '+' string represents the ArrowDown or ArrowRight key, and the '-' string represents the ArrowUp or ArrowLeft key, depending on the orientation of the current menu. When the trialKeys argument is missing or is an empty array, 12 keys are selected at random.
37
+ Compiled to buttonMenu.js by tsc (issue #73); edit this file, not the emitted one.
38
+ */
39
+
40
+ // ########## FUNCTIONS
41
+
42
+ // Returns data about the element referenced by a locator.
43
+ const getLocatorData = async (loc: Locator): Promise<LocatorData | null> => {
44
+ const locCount = await loc.count();
45
+ // If the locator identifies exactly 1 element:
46
+ if (locCount === 1) {
47
+ // Get the facts obtainable from the browser.
48
+ const data = await loc.evaluate((element): LocatorData => {
49
+ // Tag name.
50
+ const tagName = element.tagName;
51
+ // ID.
52
+ const id = element.id || '';
53
+ // Texts.
54
+ const {textContent} = element;
55
+ const alts = Array.from(element.querySelectorAll('img[alt]:not([alt=""])'));
56
+ const altTexts = alts.map(alt => alt.getAttribute('alt'));
57
+ const altsText = altTexts.join(' ');
58
+ const ariaLabelText = element.ariaLabel || '';
59
+ const refLabelID = element.getAttribute('aria-labelledby');
60
+ const refLabel = refLabelID ? document.getElementById(refLabelID) : '';
61
+ const refLabelText = refLabel ? refLabel.textContent : '';
62
+ let labelsText = '';
63
+ if (tagName === 'INPUT') {
64
+ const labels = (element as HTMLInputElement).labels || [];
65
+ const labelTexts: (string | null)[] = [];
66
+ labels.forEach(label => {
67
+ labelTexts.push(label.textContent);
68
+ });
69
+ labelsText = labelTexts.join(' ');
70
+ }
71
+ let text = [textContent, altsText, ariaLabelText, refLabelText, labelsText]
72
+ .join(' ')
73
+ .replace(/\s+/g, ' ')
74
+ .trim();
75
+ if (! text) {
76
+ text = element.outerHTML.replace(/\s+/g, ' ').trim();
77
+ }
78
+ if (/^<[^<>]+>$/.test(text)) {
79
+ text = element.parentElement!.outerHTML.replace(/\s+/g, ' ').trim();
80
+ }
81
+ // Location.
82
+ let location: LocatorLocation = {
83
+ doc: 'dom',
84
+ type: 'box',
85
+ spec: {}
86
+ };
87
+ if (id) {
88
+ location.type = 'selector';
89
+ location.spec = `#${id}`;
90
+ }
91
+ // Return the data.
92
+ return {
93
+ tagName,
94
+ id,
95
+ location,
96
+ excerpt: text
97
+ };
98
+ });
99
+ // If an ID-based selector could not be defined:
100
+ if (data.location.type === 'box') {
101
+ // Define a bounding-box-based location.
102
+ const rawSpec = await loc.boundingBox();
103
+ // If there is a bounding box (i.e. the element is visible):
104
+ if (rawSpec) {
105
+ // Populate the location.
106
+ Object.keys(rawSpec).forEach(specName => {
107
+ (data.location.spec as Record<string, number>)[specName] = Math.round(
108
+ rawSpec[specName as keyof typeof rawSpec]
109
+ );
110
+ });
111
+ }
112
+ // Otherwise, i.e. if there is no bounding box:
113
+ else {
114
+ // Empty the location.
115
+ data.location.doc = '';
116
+ data.location.type = '';
117
+ data.location.spec = '';
118
+ }
119
+ }
120
+ // If the text is long:
121
+ if (data.excerpt.length > 400) {
122
+ // Truncate its middle.
123
+ data.excerpt = `${data.excerpt.slice(0, 200)} … ${data.excerpt.slice(-200)}`;
124
+ }
125
+ // Return the data.
126
+ return data;
127
+ }
128
+ // Otherwise, i.e. if it does not identify exactly 1 element:
129
+ else {
130
+ // Report this.
131
+ console.log(`ERROR: Locator count to get data from is ${locCount} instead of 1`);
132
+ return null;
133
+ }
134
+ };
135
+ // Returns an adjacent index, with wrapping.
136
+ const getAdjacentIndexWithWrap = (groupSize: number, startIndex: number, increment: number) => {
137
+ let newIndex = startIndex + increment;
138
+ if (newIndex < 0) {
139
+ newIndex = groupSize - 1;
140
+ }
141
+ else if (newIndex > groupSize - 1) {
142
+ newIndex = 0;
143
+ }
144
+ return newIndex;
145
+ };
146
+ // Returns whether a key created the expected effective focus.
147
+ const focusSuccess = async (
148
+ miLocsDir: Locator[], priorIndex: number, key: string, isPseudo: boolean
149
+ ) => {
150
+ // Initialize the result.
151
+ const result: {isOK: boolean; newFocIndex: number | null} = {
152
+ isOK: false,
153
+ newFocIndex: null
154
+ };
155
+ // For each of the menu items directly descending from the menu:
156
+ for (const miLocDir of miLocsDir) {
157
+ // Get whether it is effectively focused.
158
+ const hasFocus = await miLocDir.evaluate((el, isPseudo) => {
159
+ if (isPseudo) {
160
+ // The page has an active element whenever this runs; verbatim from the original.
161
+ const effectiveID = document.activeElement!.getAttribute('aria-activeDescendant');
162
+ if (effectiveID) {
163
+ const effectiveEl = document.getElementById(effectiveID);
164
+ return effectiveEl === el;
165
+ }
166
+ else {
167
+ return false;
168
+ }
169
+ }
170
+ else {
171
+ return document.activeElement === el;
172
+ }
173
+ }, isPseudo);
174
+ // If it is:
175
+ if (hasFocus) {
176
+ // Get whether the effective focus is as expected and update the result.
177
+ const miFocIndex = miLocsDir.indexOf(miLocDir);
178
+ const miCount = miLocsDir.length;
179
+ if (key === 'Home' && miFocIndex === 0) {
180
+ result.isOK = true;
181
+ result.newFocIndex = 0;
182
+ }
183
+ else if (key === 'End' && miFocIndex === miCount - 1) {
184
+ result.isOK = true;
185
+ result.newFocIndex = miCount - 1;
186
+ }
187
+ else if (['ArrowLeft', 'ArrowUp'].includes(key)) {
188
+ const expectedIndex = getAdjacentIndexWithWrap(miCount, priorIndex, -1);
189
+ if (miFocIndex === expectedIndex) {
190
+ result.isOK = true;
191
+ result.newFocIndex = expectedIndex;
192
+ }
193
+ }
194
+ else if (['ArrowRight', 'ArrowDown'].includes(key)) {
195
+ const expectedIndex = getAdjacentIndexWithWrap(miCount, priorIndex, 1);
196
+ if (miFocIndex === expectedIndex) {
197
+ result.isOK = true;
198
+ result.newFocIndex = expectedIndex;
199
+ }
200
+ }
201
+ break;
202
+ }
203
+ }
204
+ // Return the result.
205
+ return result;
206
+ };
207
+ // Performs the test and reports the result.
208
+ export const reporter = async (
209
+ page: Page, report: Report, _0: unknown, withItems: boolean, trialKeySpecs: string[] = []
210
+ ) => {
211
+ // Initialize the result.
212
+ const data: {trialKeys?: string[]} = {};
213
+ const totals = [0, 0, 0, 0];
214
+ const standardInstances: StandardInstance[] = [];
215
+ // Get locators for all menu buttons.
216
+ const mbLocAll = page.locator(
217
+ 'button[aria-controls][aria-expanded][aria-haspopup=true], button[aria-controls][aria-expanded][aria-haspopup=menu]'
218
+ );
219
+ const mbLocsAll = await mbLocAll.all();
220
+ // For each menu button:
221
+ for (const mbLoc of mbLocsAll) {
222
+ // Get a locator for its menu.
223
+ const menuID = await mbLoc.getAttribute('aria-controls');
224
+ const menuLoc = page.locator(`[id=${menuID}][role=menu], [id=${menuID}][role=menubar]`);
225
+ // If the button controls exactly 1 menu:
226
+ if (menuLoc && await menuLoc.count() === 1) {
227
+ // Get data on the menu.
228
+ const elData = await getLocatorData(menuLoc);
229
+ // If data were obtained:
230
+ if (elData) {
231
+ // Get the orientation and focus-management type of the menu.
232
+ const extraData = await menuLoc.evaluate(element => {
233
+ const extraData: {orientation?: string | null; isPseudo?: boolean} = {};
234
+ let orientation = element.getAttribute('aria-orientation');
235
+ if (! orientation) {
236
+ const role = element.getAttribute('role');
237
+ if (role === 'menubar') {
238
+ orientation = 'horizontal';
239
+ }
240
+ else if (role === 'menu') {
241
+ orientation = 'vertical';
242
+ }
243
+ else {
244
+ orientation = null;
245
+ }
246
+ }
247
+ extraData.orientation = orientation;
248
+ const isPseudo = !! element.getAttribute('aria-activedescendant');
249
+ extraData.isPseudo = isPseudo;
250
+ return extraData;
251
+ });
252
+ // If they were obtained:
253
+ if (extraData) {
254
+ // Get locators for its descendant non-menu menu items.
255
+ const miLocAll = menuLoc.locator('[role=menuitem]:not([role=menu], [role=menubar])');
256
+ // Get which of them are direct descendants.
257
+ const areDirect = await miLocAll.evaluateAll((els, menuID) => {
258
+ return els.map(el => {
259
+ // Menu items are inside a menu by definition; verbatim from the original.
260
+ const itsMenu = el.closest('[role=menu], [role=menubar]')!;
261
+ return itsMenu.id && itsMenu.id === menuID;
262
+ });
263
+ }, menuID);
264
+ const miLocsAll = await miLocAll.all();
265
+ const miLocsDir = miLocsAll.filter((loc, index) => areDirect[index]);
266
+ // If there are at least 2 of them:
267
+ if (miLocsDir.length > 1) {
268
+ // Ensure that the menu is collapsed.
269
+ await menuLoc.evaluate(element => {
270
+ // The boolean is coerced to 'false' at runtime; verbatim from the original.
271
+ element.setAttribute('aria-expanded', false as unknown as string);
272
+ });
273
+ // Focus the menu button and press the key that opens its menu from the top.
274
+ await mbLoc.press(extraData.orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown');
275
+ // If trial keys have been specified:
276
+ const trialKeys: string[] = [];
277
+ if (trialKeySpecs.length) {
278
+ // Implement them.
279
+ trialKeySpecs.forEach(spec => {
280
+ if (['Home', 'End'].includes(spec)) {
281
+ trialKeys.push(spec);
282
+ }
283
+ else if (spec === '-') {
284
+ trialKeys.push(extraData.orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp');
285
+ }
286
+ else if (spec === '+') {
287
+ trialKeys.push(
288
+ extraData.orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'
289
+ );
290
+ }
291
+ });
292
+ }
293
+ // Otherwise, i.e. if trial keys have not been specified:
294
+ else {
295
+ // Randomly draw 12 keys from Home, End, and the applicable arrow keys.
296
+ const keys = ['Home', 'End'];
297
+ if (extraData.orientation === 'horizontal') {
298
+ keys.push('ArrowLeft', 'ArrowRight');
299
+ }
300
+ else {
301
+ keys.push('ArrowUp', 'ArrowDown');
302
+ }
303
+ let trialKeyCount = 0;
304
+ while (trialKeyCount++ < 12) {
305
+ trialKeys.push(keys[Math.floor(4 * Math.random())]);
306
+ }
307
+ }
308
+ // Add the list of trial keys to the result.
309
+ data.trialKeys = trialKeys;
310
+ let focIndex = 0;
311
+ // For each key in the trial:
312
+ for (const key of trialKeys) {
313
+ // Press it.
314
+ await page.keyboard.press(key);
315
+ // Get whether the expected focus occurred.
316
+ const focData = await focusSuccess(miLocsDir, focIndex, key, extraData.isPseudo!);
317
+ // If so:
318
+ if (focData.isOK) {
319
+ // Update the index of the effective focus.
320
+ focIndex = focData.newFocIndex as number;
321
+ }
322
+ // Otherwise, i.e. if the expected focus did not occur:
323
+ else {
324
+ // Add to the totals.
325
+ totals[2]++;
326
+ // If itemization is required:
327
+ if (withItems) {
328
+ // Get the XPath of the menu button.
329
+ const mbXPath = await mbLoc.evaluate(element => window.getXPath(element));
330
+ // Add an instance to the standard instances.
331
+ standardInstances.push({
332
+ ruleID: 'buttonMenu',
333
+ what: `Menu responds nonstandardly to the ${key} key`,
334
+ ordinalSeverity: 2,
335
+ count: 1,
336
+ catalogIndex: getXPathCatalogIndex(report, mbXPath as string)
337
+ });
338
+ }
339
+ // Stop testing the menu button.
340
+ break;
341
+ }
342
+ }
343
+ }
344
+ }
345
+ // Otherwise, i.e. if the orientation and focus-management type were not obtained:
346
+ else {
347
+ // Report this.
348
+ console.log('ERROR: Menu orientation and focus-management type not obtained');
349
+ }
350
+ }
351
+ // Otherwise, i.e. if the data were not obtained:
352
+ else {
353
+ // Report this.
354
+ console.log('ERROR: Menu data not obtained');
355
+ }
356
+ }
357
+ // Otherwise, i.e. if it does not control exactly 1 menu:
358
+ else {
359
+ // Add to the totals.
360
+ totals[2]++;
361
+ // If itemization is required:
362
+ if (withItems) {
363
+ // Get the XPath of the menu button.
364
+ const mbXPath = await mbLoc.evaluate(element => window.getXPath(element));
365
+ // Add an instance to the standard instances.
366
+ standardInstances.push({
367
+ ruleID: 'buttonMenu',
368
+ what: 'Menu button does not control exactly 1 menu',
369
+ ordinalSeverity: 2,
370
+ count: 1,
371
+ catalogIndex: getXPathCatalogIndex(report, mbXPath as string)
372
+ });
373
+ }
374
+ }
375
+ }
376
+ // If itemization is not required and there are any instances:
377
+ if (! withItems && totals[2]) {
378
+ // Add a summary instance to the result.
379
+ standardInstances.push({
380
+ ruleID: 'buttonMenu',
381
+ what: 'Menu buttons and menus behave nonstandardly',
382
+ count: totals[2],
383
+ ordinalSeverity: 2
384
+ });
385
+ }
386
+ return {
387
+ data,
388
+ totals,
389
+ standardInstances
390
+ };
391
+ };
@@ -0,0 +1,3 @@
1
+ import type { Page } from 'playwright';
2
+ import type { Report } from '../types';
3
+ export declare const reporter: (page: Page, report: Report, _: unknown, withItems: boolean) => Promise<import("../procs/testaro").RuleResult>;
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  /*
2
3
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
4
  © 2025 Juan S. Casado.
@@ -9,29 +10,25 @@
9
10
 
10
11
  SPDX-License-Identifier: MIT
11
12
  */
12
-
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.reporter = void 0;
15
+ const testaro_1 = require("../procs/testaro");
13
16
  /*
14
17
  captionLoc
15
18
  This test reports caption elements that are not the first children of table elements.
19
+ Compiled to captionLoc.js by tsc (issue #73); edit this file, not the emitted one.
16
20
  */
17
-
18
- // IMPORTS
19
-
20
- const {doTest} = require('../procs/testaro');
21
-
22
21
  // FUNCTIONS
23
-
24
- exports.reporter = async (page, report, _, withItems) => {
25
- const getBadWhat = element => {
26
- const parent = element.parentElement;
27
- // If the element is not the first child of a table element:
28
- if (! parent || parent.tagName !== 'TABLE' || parent.firstElementChild !== element) {
29
- // Return a violation description.
30
- return 'caption element is not the first child of a table element';
31
- }
32
- };
33
- const whats = 'caption elements are not the first children of table elements';
34
- return await doTest(
35
- page, report, withItems, 'captionLoc', 'body caption', whats, 3, getBadWhat.toString()
36
- );
22
+ const reporter = async (page, report, _, withItems) => {
23
+ const getBadWhat = element => {
24
+ const parent = element.parentElement;
25
+ // If the element is not the first child of a table element:
26
+ if (!parent || parent.tagName !== 'TABLE' || parent.firstElementChild !== element) {
27
+ // Return a violation description.
28
+ return 'caption element is not the first child of a table element';
29
+ }
30
+ };
31
+ const whats = 'caption elements are not the first children of table elements';
32
+ return await (0, testaro_1.doTest)(page, report, withItems, 'captionLoc', 'body caption', whats, 3, getBadWhat.toString());
37
33
  };
34
+ exports.reporter = reporter;
@@ -0,0 +1,40 @@
1
+ /*
2
+ © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2025 Juan S. Casado.
4
+ © 2026 Jeff Witt.
5
+ © 2025–2026 Jonathan Robert Pool.
6
+
7
+ Licensed under the MIT License. See LICENSE file at the project root or
8
+ https://opensource.org/license/mit/ for details.
9
+
10
+ SPDX-License-Identifier: MIT
11
+ */
12
+
13
+ // IMPORTS
14
+
15
+ import type {Page} from 'playwright';
16
+ import {doTest} from '../procs/testaro';
17
+ import type {GetBadWhat, Report} from '../types';
18
+
19
+ /*
20
+ captionLoc
21
+ This test reports caption elements that are not the first children of table elements.
22
+ Compiled to captionLoc.js by tsc (issue #73); edit this file, not the emitted one.
23
+ */
24
+
25
+ // FUNCTIONS
26
+
27
+ export const reporter = async (page: Page, report: Report, _: unknown, withItems: boolean) => {
28
+ const getBadWhat: GetBadWhat = element => {
29
+ const parent = element.parentElement;
30
+ // If the element is not the first child of a table element:
31
+ if (! parent || parent.tagName !== 'TABLE' || parent.firstElementChild !== element) {
32
+ // Return a violation description.
33
+ return 'caption element is not the first child of a table element';
34
+ }
35
+ };
36
+ const whats = 'caption elements are not the first children of table elements';
37
+ return await doTest(
38
+ page, report, withItems, 'captionLoc', 'body caption', whats, 3, getBadWhat.toString()
39
+ );
40
+ };
@@ -0,0 +1,3 @@
1
+ import type { Page } from 'playwright';
2
+ import type { Report } from '../types';
3
+ export declare const reporter: (page: Page, report: Report, _: unknown, withItems: boolean) => Promise<import("../procs/testaro").RuleResult>;
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  /*
2
3
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
4
  © 2025 Juan S. Casado.
@@ -8,45 +9,41 @@
8
9
 
9
10
  SPDX-License-Identifier: MIT
10
11
  */
11
-
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.reporter = void 0;
14
+ const testaro_1 = require("../procs/testaro");
12
15
  /*
13
16
  datalistRef
14
17
  This test reports inputs whose list attributes reference missing or ambiguous datalists.
18
+ Compiled to datalistRef.js by tsc (issue #73); edit this file, not the emitted one.
15
19
  */
16
-
17
- // IMPORTS
18
-
19
- const {doTest} = require('../procs/testaro');
20
-
21
20
  // FUNCTIONS
22
-
23
- exports.reporter = async (page, report, _, withItems) => {
24
- const getBadWhat = element => {
25
- // Get the ID of the datalist element referenced by the list attribute of the element.
26
- const listID = element.getAttribute('list');
27
- // If the element has a list attribute with a non-empty value:
28
- if (listID) {
29
- // Get the element it references.
30
- const listElement = document.getElementById(listID);
31
- // If no such element exists:
32
- if (! listElement) {
33
- // Return a violation description.
34
- return 'input element list attribute references a missing element';
35
- }
36
- // Otherwise, if the element it references is not a datalist:
37
- if (listElement.tagName.toLowerCase() !== 'datalist') {
38
- // Return a violation description.
39
- return 'input element list attribute references a non-datalist element';
40
- }
41
- }
42
- // Otherwise, i.e. if it has no list attribute with a non-empty value:
43
- else {
44
- // Return a violation description.
45
- return 'input element list attribute is empty';
46
- }
47
- };
48
- const whats = 'list attributes of input elements are empty or IDs of no or non-datalist elements';
49
- return await doTest(
50
- page, report, withItems, 'datalistRef', 'body input[list]', whats, 3, getBadWhat.toString()
51
- );
21
+ const reporter = async (page, report, _, withItems) => {
22
+ const getBadWhat = element => {
23
+ // Get the ID of the datalist element referenced by the list attribute of the element.
24
+ const listID = element.getAttribute('list');
25
+ // If the element has a list attribute with a non-empty value:
26
+ if (listID) {
27
+ // Get the element it references.
28
+ const listElement = document.getElementById(listID);
29
+ // If no such element exists:
30
+ if (!listElement) {
31
+ // Return a violation description.
32
+ return 'input element list attribute references a missing element';
33
+ }
34
+ // Otherwise, if the element it references is not a datalist:
35
+ if (listElement.tagName.toLowerCase() !== 'datalist') {
36
+ // Return a violation description.
37
+ return 'input element list attribute references a non-datalist element';
38
+ }
39
+ }
40
+ // Otherwise, i.e. if it has no list attribute with a non-empty value:
41
+ else {
42
+ // Return a violation description.
43
+ return 'input element list attribute is empty';
44
+ }
45
+ };
46
+ const whats = 'list attributes of input elements are empty or IDs of no or non-datalist elements';
47
+ return await (0, testaro_1.doTest)(page, report, withItems, 'datalistRef', 'body input[list]', whats, 3, getBadWhat.toString());
52
48
  };
49
+ exports.reporter = reporter;
@@ -0,0 +1,55 @@
1
+ /*
2
+ © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2025 Juan S. Casado.
4
+ © 2026 Jeff Witt.
5
+ © 2025–2026 Jonathan Robert Pool.
6
+
7
+ Licensed under the MIT License. See LICENSE file at the project root or 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 {doTest} from '../procs/testaro';
16
+ import type {GetBadWhat, Report} from '../types';
17
+
18
+ /*
19
+ datalistRef
20
+ This test reports inputs whose list attributes reference missing or ambiguous datalists.
21
+ Compiled to datalistRef.js by tsc (issue #73); edit this file, not the emitted one.
22
+ */
23
+
24
+ // FUNCTIONS
25
+
26
+ export const reporter = async (page: Page, report: Report, _: unknown, withItems: boolean) => {
27
+ const getBadWhat: GetBadWhat = element => {
28
+ // Get the ID of the datalist element referenced by the list attribute of the element.
29
+ const listID = element.getAttribute('list');
30
+ // If the element has a list attribute with a non-empty value:
31
+ if (listID) {
32
+ // Get the element it references.
33
+ const listElement = document.getElementById(listID);
34
+ // If no such element exists:
35
+ if (! listElement) {
36
+ // Return a violation description.
37
+ return 'input element list attribute references a missing element';
38
+ }
39
+ // Otherwise, if the element it references is not a datalist:
40
+ if (listElement.tagName.toLowerCase() !== 'datalist') {
41
+ // Return a violation description.
42
+ return 'input element list attribute references a non-datalist element';
43
+ }
44
+ }
45
+ // Otherwise, i.e. if it has no list attribute with a non-empty value:
46
+ else {
47
+ // Return a violation description.
48
+ return 'input element list attribute is empty';
49
+ }
50
+ };
51
+ const whats = 'list attributes of input elements are empty or IDs of no or non-datalist elements';
52
+ return await doTest(
53
+ page, report, withItems, 'datalistRef', 'body input[list]', whats, 3, getBadWhat.toString()
54
+ );
55
+ };
@@ -0,0 +1,3 @@
1
+ import type { Page } from 'playwright';
2
+ import type { Report } from '../types';
3
+ export declare const reporter: (page: Page, report: Report, _: unknown, withItems: boolean) => Promise<import("../procs/testaro").RuleResult>;