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
package/testaro/hovInd.js CHANGED
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  /*
2
3
  © 2023–2025 CVS Health and/or one of its affiliates. All rights reserved.
3
4
  © 2026 Jeff Witt.
@@ -7,139 +8,171 @@
7
8
 
8
9
  SPDX-License-Identifier: MIT
9
10
  */
10
-
11
- /*
12
- hovInd
13
- This test reports confusing hover indication.
14
- */
15
-
16
- // IMPORTS
17
-
18
- const {doTest} = require('../procs/testaro');
19
-
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.reporter = void 0;
46
+ const playwright = __importStar(require("playwright"));
47
+ const testaro_1 = require("../procs/testaro");
48
+ const config_1 = require("../procs/config");
49
+ // CONSTANTS
50
+ // Standard non-default hover cursors of inputs, by type.
51
+ const standardInputCursors = {
52
+ email: 'text',
53
+ image: 'pointer',
54
+ number: 'text',
55
+ password: 'text',
56
+ search: 'text',
57
+ tel: 'text',
58
+ text: 'text',
59
+ url: 'text'
60
+ };
61
+ // Style properties compared between states.
62
+ const comparedStyles = ['backgroundColor', 'border', 'color', 'outline'];
63
+ // Maximum count of triggers inspected, because each inspection performs 3 browser interactions.
64
+ const MAX_TRIGGERS = 60;
20
65
  // FUNCTIONS
21
-
66
+ // Returns the hover-related style properties of a trigger in its current state.
67
+ const getStyles = async (loc) => await loc.evaluate(element => {
68
+ const { cursor, borderColor, borderStyle, borderWidth, outlineColor, outlineStyle, outlineWidth, outlineOffset, color, backgroundColor } = window.getComputedStyle(element);
69
+ return {
70
+ tagName: element.tagName,
71
+ inputType: element.tagName === 'INPUT' ? element.getAttribute('type') || 'text' : null,
72
+ // Ignore any custom cursor images and keep only the fallback keyword.
73
+ cursor: cursor.replace(/^.+, */, ''),
74
+ border: `${borderColor} ${borderStyle} ${borderWidth}`,
75
+ outline: `${outlineColor} ${outlineStyle} ${outlineWidth} ${outlineOffset}`,
76
+ color,
77
+ backgroundColor
78
+ };
79
+ });
80
+ // Returns whether the hover cursor of a trigger is standard. A computed value of auto is standard, because browsers render it as the type-appropriate cursor.
81
+ const cursorIsStandard = (styles) => {
82
+ const { tagName, inputType, cursor } = styles;
83
+ if (tagName === 'A') {
84
+ return ['pointer', 'auto'].includes(cursor);
85
+ }
86
+ if (tagName === 'INPUT') {
87
+ return [standardInputCursors[inputType || 'text'], 'default', 'auto'].includes(cursor);
88
+ }
89
+ if (tagName === 'BUTTON') {
90
+ return ['default', 'auto'].includes(cursor);
91
+ }
92
+ // Any other trigger is one with a hover listener, and its cursor is assumed standard.
93
+ return true;
94
+ };
95
+ // Returns whether the compared styles of two states are identical.
96
+ const areAlike = (styles0, styles1) => comparedStyles
97
+ .every(style => styles0[style] === styles1[style]);
22
98
  // Runs the test and returns the result.
23
- exports.reporter = async (page, report, _, withItems) => {
24
- const getBadWhat = element => {
25
- const violationTypes = [];
26
- const isVisible = element.checkVisibility({
27
- contentVisibilityAuto: true,
28
- opacityProperty: true,
29
- visibilityProperty: true
30
- });
31
- // If the element is visible:
32
- if (isVisible) {
33
- // Get its live style declaration.
34
- const styleDec = window.getComputedStyle(element);
35
- // FUNCTION DEFINITIONS START
36
- // Returns hover-related style data on a trigger.
37
- const getStyleData = () => {
38
- const {
39
- cursor,
40
- borderColor,
41
- borderStyle,
42
- borderWidth,
43
- outlineColor,
44
- outlineStyle,
45
- outlineWidth,
46
- outlineOffset,
47
- color,
48
- backgroundColor
49
- } = styleDec;
50
- return {
51
- tagName: element.tagName,
52
- inputType: element.tagName === 'INPUT' ? element.getAttribute('type') || 'text' : null,
53
- cursor: cursor.replace(/^.+, */, ''),
54
- border: `${borderColor} ${borderStyle} ${borderWidth}`,
55
- outline: `${outlineColor} ${outlineStyle} ${outlineWidth} ${outlineOffset}`,
56
- color,
57
- backgroundColor
58
- };
59
- };
60
- // Returns whether the cursor is bad when the element is hovered over.
61
- const cursorIsBad = hoverCursor => {
62
- const {tagName, type} = element;
63
- if (tagName === 'A' || tagName === 'INPUT' && type === 'image') {
64
- return hoverCursor !== 'pointer';
65
- }
66
- if (tagName === 'INPUT') {
67
- if (['button', 'radio', 'reset', 'submit'].some(typeName => type === typeName)) {
68
- return hoverCursor !== 'default';
69
- }
70
- return hoverCursor !== 'text';
71
- }
72
- return ! ['auto', 'default'].includes(hoverCursor);
73
- };
74
- // Returns whether two hover styles are effectively identical.
75
- const areAlike = (styles0, styles1) => {
76
- // Return whether they are effectively identical.
77
- const areAlike = ['cursor', 'backgroundColor', 'border', 'color', 'outline']
78
- .every(style => styles1[style] === styles0[style]);
79
- return areAlike;
80
- };
81
- // FUNCTION DEFINITIONS END
82
- // Get its style data when neither focused nor hovered over.
83
- const defaultStyleData = getStyleData();
84
- // Correct the cursor value.
85
- defaultStyleData.cursor = 'default';
86
- // Get its style data when only focused.
87
- element.focus();
88
- const focusStyleData = getStyleData();
89
- // Correct the cursor value.
90
- focusStyleData.cursor = 'default';
91
- // Get its style data when only hovered over.
92
- element.blur();
93
- element.dispatchEvent(new MouseEvent('mouseenter'));
94
- const hoverStyleData = getStyleData();
95
- const data = {};
96
- // If the cursor is confusing when the element is only hovered over:
97
- if (cursorIsBad(hoverStyleData.cursor)) {
98
- // Add this to the violation types.
99
- violationTypes.push(
100
- `nonstandard mouse cursor (${hoverStyleData.cursor}) when hovered over`
101
- );
102
- }
103
- // If the neutral and hover styles are indistinguishable:
104
- if (areAlike(defaultStyleData, hoverStyleData)) {
105
- // Add this to the violation types.
106
- violationTypes.push('normal and hover styles are indistinguishable');
107
- // Add the details to the data.
108
- data.n_h = {
109
- neutral: defaultStyleData,
110
- hover: hoverStyleData
111
- };
112
- }
113
- // If the focus and hoverstyles are indistinguishable:
114
- if (areAlike(focusStyleData, hoverStyleData)) {
115
- // Add this to the violation types.
116
- violationTypes.push('focus and hover styles are indistinguishable');
117
- // Add the details to the data.
118
- data.f_h = {
119
- focus: focusStyleData,
120
- hover: hoverStyleData
121
- };
122
- }
123
- // If any violations occurred:
124
- if (violationTypes.length) {
125
- const description = `Element styles do not clearly indicate hovering: ${violationTypes.join('; ')}`;
126
- // If there are additional data:
127
- if (Object.keys(data).length) {
128
- // Return the violation description and data.
129
- return {
130
- description,
131
- data
132
- };
99
+ const reporter = async (page, report, _, withItems) => {
100
+ // Get locators for the visible triggers.
101
+ const selector = ['a', 'button', 'input', '[onmouseenter]', '[onmouseover]']
102
+ .map(tagSelector => `body ${tagSelector}:visible`)
103
+ .join(', ');
104
+ const allLocs = await page.locator(selector).all();
105
+ // Inspect at most MAX_TRIGGERS of them, evenly spaced through the page.
106
+ const step = Math.max(1, Math.ceil(allLocs.length / MAX_TRIGGERS));
107
+ const locs = allLocs.filter((loc, index) => index % step === 0).slice(0, MAX_TRIGGERS);
108
+ const data = {
109
+ triggerCount: allLocs.length,
110
+ inspectedCount: locs.length
111
+ };
112
+ // Initialize the violations, by severity.
113
+ const cursorViolations = [];
114
+ const styleViolations = [];
115
+ const timeout = (0, config_1.applyMultiplier)(500);
116
+ // For each trigger:
117
+ for (const loc of locs) {
118
+ try {
119
+ // Move the mouse away from any previously hovered trigger.
120
+ await page.mouse.move(0, 0);
121
+ // Get its styles when neither focused nor hovered over.
122
+ const neutralStyles = await getStyles(loc);
123
+ // Get its styles when only focused.
124
+ await loc.focus({ timeout });
125
+ const focusStyles = await getStyles(loc);
126
+ await loc.blur({ timeout });
127
+ // Get its styles when only hovered over.
128
+ await loc.hover({ timeout });
129
+ const hoverStyles = await getStyles(loc);
130
+ await page.mouse.move(0, 0);
131
+ // If its hover cursor is nonstandard:
132
+ if (!cursorIsStandard(hoverStyles)) {
133
+ // Add a violation.
134
+ cursorViolations.push({
135
+ loc,
136
+ what: `Element has a nonstandard hover cursor (${hoverStyles.cursor})`
137
+ });
138
+ }
139
+ // If it is a button and its hover styles are indistinguishable from its neutral styles:
140
+ if (hoverStyles.tagName === 'BUTTON' && areAlike(neutralStyles, hoverStyles)) {
141
+ // Add a violation.
142
+ styleViolations.push({
143
+ loc,
144
+ what: 'Element border, outline, color, and background color do not change when hovered over'
145
+ });
146
+ }
147
+ // Otherwise, if its hover styles differ from its neutral styles but are indistinguishable from its focus styles:
148
+ else if (!areAlike(neutralStyles, hoverStyles) && areAlike(focusStyles, hoverStyles)) {
149
+ // Add a violation.
150
+ styleViolations.push({
151
+ loc,
152
+ what: 'Element border, outline, color, and background color are alike on hover and focus'
153
+ });
154
+ }
133
155
  }
134
- // Otherwise, i.e. if there are no additional data:
135
- else {
136
- // Return the violation description.
137
- return description;
156
+ // If focusing, blurring, or hovering throws an error:
157
+ catch (error) {
158
+ // If the error is a timeout, skip the trigger.
159
+ if (error instanceof playwright.errors.TimeoutError) {
160
+ continue;
161
+ }
162
+ // Otherwise, report this and quit.
163
+ data.prevented = true;
164
+ data.error = `ERROR inspecting a trigger (${error.message.slice(0, 200)})`;
165
+ break;
138
166
  }
139
- }
140
167
  }
141
- };
142
- const selector = 'body a, body button, body input, body [onmouseenter], body [onmouseover]';
143
- const whats = 'elements have confusing hover indicators';
144
- return await doTest(page, report, withItems, 'hovInd', selector, whats, 1, getBadWhat.toString());
168
+ // Get results for the two severities and merge them.
169
+ const cursorResult = await (0, testaro_1.getBasicResult)(report, withItems, 'hovInd', 2, 'Elements have nonstandard hover cursors', data, cursorViolations);
170
+ const styleResult = await (0, testaro_1.getBasicResult)(report, withItems, 'hovInd', 1, 'Element styles do not distinguish hovering from the neutral or focus state', data, styleViolations);
171
+ const totals = cursorResult.totals.map((total, index) => total + styleResult.totals[index]);
172
+ return {
173
+ data,
174
+ totals,
175
+ standardInstances: [...cursorResult.standardInstances, ...styleResult.standardInstances]
176
+ };
145
177
  };
178
+ exports.reporter = reporter;
@@ -0,0 +1,199 @@
1
+ /*
2
+ © 2023–2025 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
+ import type {Locator, Page} from 'playwright';
14
+ import * as playwright from 'playwright';
15
+ import {getBasicResult} from '../procs/testaro';
16
+ import {applyMultiplier} from '../procs/config';
17
+ import type {Report, SeverityTotals} from '../types';
18
+
19
+ /*
20
+ hovInd
21
+ This test reports confusing hover indication. The triggers are visible links, buttons, inputs, and elements with hover listeners. Each trigger is inspected in three states: neutral, focused, and hovered over. Hovering is performed by Playwright, because CSS :hover styles cannot be activated by synthetic mouse events dispatched inside the page. A trigger violates the rule if its hover cursor is nonstandard for its type (severity 2), if it is a button whose border, outline, color, and background color do not change when it is hovered over (severity 1), or if those styles change on hover but are indistinguishable from its focus styles (severity 1). A trigger may produce more than one instance.
22
+ Compiled to hovInd.js by tsc (issue #73); edit this file, not the emitted one.
23
+ */
24
+
25
+ // TYPES
26
+
27
+ // The hover-related style properties of a trigger in one state.
28
+ interface TriggerStyles {
29
+ tagName: string;
30
+ inputType: string | null;
31
+ cursor: string;
32
+ border: string;
33
+ outline: string;
34
+ color: string;
35
+ backgroundColor: string;
36
+ }
37
+ // A violation to be reported.
38
+ interface Violation {
39
+ loc: Locator;
40
+ what: string;
41
+ }
42
+
43
+ // CONSTANTS
44
+
45
+ // Standard non-default hover cursors of inputs, by type.
46
+ const standardInputCursors: Record<string, string> = {
47
+ email: 'text',
48
+ image: 'pointer',
49
+ number: 'text',
50
+ password: 'text',
51
+ search: 'text',
52
+ tel: 'text',
53
+ text: 'text',
54
+ url: 'text'
55
+ };
56
+ // Style properties compared between states.
57
+ const comparedStyles: (keyof TriggerStyles)[] = ['backgroundColor', 'border', 'color', 'outline'];
58
+ // Maximum count of triggers inspected, because each inspection performs 3 browser interactions.
59
+ const MAX_TRIGGERS = 60;
60
+
61
+ // FUNCTIONS
62
+
63
+ // Returns the hover-related style properties of a trigger in its current state.
64
+ const getStyles = async (loc: Locator): Promise<TriggerStyles> => await loc.evaluate(element => {
65
+ const {
66
+ cursor,
67
+ borderColor,
68
+ borderStyle,
69
+ borderWidth,
70
+ outlineColor,
71
+ outlineStyle,
72
+ outlineWidth,
73
+ outlineOffset,
74
+ color,
75
+ backgroundColor
76
+ } = window.getComputedStyle(element);
77
+ return {
78
+ tagName: element.tagName,
79
+ inputType: element.tagName === 'INPUT' ? element.getAttribute('type') || 'text' : null,
80
+ // Ignore any custom cursor images and keep only the fallback keyword.
81
+ cursor: cursor.replace(/^.+, */, ''),
82
+ border: `${borderColor} ${borderStyle} ${borderWidth}`,
83
+ outline: `${outlineColor} ${outlineStyle} ${outlineWidth} ${outlineOffset}`,
84
+ color,
85
+ backgroundColor
86
+ };
87
+ });
88
+ // Returns whether the hover cursor of a trigger is standard. A computed value of auto is standard, because browsers render it as the type-appropriate cursor.
89
+ const cursorIsStandard = (styles: TriggerStyles) => {
90
+ const {tagName, inputType, cursor} = styles;
91
+ if (tagName === 'A') {
92
+ return ['pointer', 'auto'].includes(cursor);
93
+ }
94
+ if (tagName === 'INPUT') {
95
+ return [standardInputCursors[inputType || 'text'], 'default', 'auto'].includes(cursor);
96
+ }
97
+ if (tagName === 'BUTTON') {
98
+ return ['default', 'auto'].includes(cursor);
99
+ }
100
+ // Any other trigger is one with a hover listener, and its cursor is assumed standard.
101
+ return true;
102
+ };
103
+ // Returns whether the compared styles of two states are identical.
104
+ const areAlike = (styles0: TriggerStyles, styles1: TriggerStyles) => comparedStyles
105
+ .every(style => styles0[style] === styles1[style]);
106
+ // Runs the test and returns the result.
107
+ export const reporter = async (page: Page, report: Report, _: unknown, withItems: boolean) => {
108
+ // Get locators for the visible triggers.
109
+ const selector = ['a', 'button', 'input', '[onmouseenter]', '[onmouseover]']
110
+ .map(tagSelector => `body ${tagSelector}:visible`)
111
+ .join(', ');
112
+ const allLocs = await page.locator(selector).all();
113
+ // Inspect at most MAX_TRIGGERS of them, evenly spaced through the page.
114
+ const step = Math.max(1, Math.ceil(allLocs.length / MAX_TRIGGERS));
115
+ const locs = allLocs.filter((loc, index) => index % step === 0).slice(0, MAX_TRIGGERS);
116
+ const data: {
117
+ triggerCount: number; inspectedCount: number; prevented?: boolean; error?: string;
118
+ } = {
119
+ triggerCount: allLocs.length,
120
+ inspectedCount: locs.length
121
+ };
122
+ // Initialize the violations, by severity.
123
+ const cursorViolations: Violation[] = [];
124
+ const styleViolations: Violation[] = [];
125
+ const timeout = applyMultiplier(500);
126
+ // For each trigger:
127
+ for (const loc of locs) {
128
+ try {
129
+ // Move the mouse away from any previously hovered trigger.
130
+ await page.mouse.move(0, 0);
131
+ // Get its styles when neither focused nor hovered over.
132
+ const neutralStyles = await getStyles(loc);
133
+ // Get its styles when only focused.
134
+ await loc.focus({timeout});
135
+ const focusStyles = await getStyles(loc);
136
+ await loc.blur({timeout});
137
+ // Get its styles when only hovered over.
138
+ await loc.hover({timeout});
139
+ const hoverStyles = await getStyles(loc);
140
+ await page.mouse.move(0, 0);
141
+ // If its hover cursor is nonstandard:
142
+ if (! cursorIsStandard(hoverStyles)) {
143
+ // Add a violation.
144
+ cursorViolations.push({
145
+ loc,
146
+ what: `Element has a nonstandard hover cursor (${hoverStyles.cursor})`
147
+ });
148
+ }
149
+ // If it is a button and its hover styles are indistinguishable from its neutral styles:
150
+ if (hoverStyles.tagName === 'BUTTON' && areAlike(neutralStyles, hoverStyles)) {
151
+ // Add a violation.
152
+ styleViolations.push({
153
+ loc,
154
+ what: 'Element border, outline, color, and background color do not change when hovered over'
155
+ });
156
+ }
157
+ // Otherwise, if its hover styles differ from its neutral styles but are indistinguishable from its focus styles:
158
+ else if (! areAlike(neutralStyles, hoverStyles) && areAlike(focusStyles, hoverStyles)) {
159
+ // Add a violation.
160
+ styleViolations.push({
161
+ loc,
162
+ what: 'Element border, outline, color, and background color are alike on hover and focus'
163
+ });
164
+ }
165
+ }
166
+ // If focusing, blurring, or hovering throws an error:
167
+ catch(error) {
168
+ // If the error is a timeout, skip the trigger.
169
+ if (error instanceof playwright.errors.TimeoutError) {
170
+ continue;
171
+ }
172
+ // Otherwise, report this and quit.
173
+ data.prevented = true;
174
+ data.error = `ERROR inspecting a trigger (${(error as Error).message.slice(0, 200)})`;
175
+ break;
176
+ }
177
+ }
178
+ // Get results for the two severities and merge them.
179
+ const cursorResult = await getBasicResult(
180
+ report, withItems, 'hovInd', 2, 'Elements have nonstandard hover cursors', data, cursorViolations
181
+ );
182
+ const styleResult = await getBasicResult(
183
+ report,
184
+ withItems,
185
+ 'hovInd',
186
+ 1,
187
+ 'Element styles do not distinguish hovering from the neutral or focus state',
188
+ data,
189
+ styleViolations
190
+ );
191
+ const totals = cursorResult.totals.map(
192
+ (total, index) => total + styleResult.totals[index]
193
+ ) as SeverityTotals;
194
+ return {
195
+ data,
196
+ totals,
197
+ standardInstances: [... cursorResult.standardInstances, ... styleResult.standardInstances]
198
+ };
199
+ };
@@ -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>;