testaro 78.0.6 → 78.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/.gitattributes +94 -0
  2. package/.github/workflows/ci.yml +58 -0
  3. package/.github/workflows/publish.yml +49 -0
  4. package/.github/workflows/typescript.yml +35 -0
  5. package/AGENTS.md +2 -2
  6. package/CLAUDE.md +25 -19
  7. package/CONTAINERS.md +10 -6
  8. package/CONTRIBUTING.md +29 -3
  9. package/Dockerfile +1 -1
  10. package/README.md +75 -20
  11. package/UPGRADES.md +4 -0
  12. package/actSpecs-doc.md +13 -1
  13. package/actSpecs.js +29 -5
  14. package/call.js +6 -5
  15. package/docker-compose.yml +8 -2
  16. package/docs/checkpoint-scanning.md +199 -0
  17. package/docs/standard-result-outcome.md +165 -0
  18. package/env.example +44 -10
  19. package/eslint.config.mjs +101 -0
  20. package/netWatch.js +373 -222
  21. package/package.json +46 -31
  22. package/pour/README.md +47 -0
  23. package/pour/pour.min.js +8 -0
  24. package/procs/actDo.js +808 -0
  25. package/procs/catalog.d.ts +17 -0
  26. package/procs/catalog.js +313 -221
  27. package/procs/catalog.ts +398 -0
  28. package/procs/checkpoint.js +110 -0
  29. package/procs/config.d.ts +35 -0
  30. package/procs/config.js +63 -0
  31. package/procs/dateTime.js +2 -1
  32. package/procs/doActs.js +442 -849
  33. package/procs/doTestAct.js +27 -121
  34. package/procs/flow.js +221 -0
  35. package/procs/generateRuleRegistry.js +94 -0
  36. package/procs/getSource.d.ts +26 -0
  37. package/procs/getSource.js +1 -0
  38. package/procs/job.js +116 -1
  39. package/procs/launch.d.ts +40 -0
  40. package/procs/launch.js +239 -108
  41. package/procs/nu.d.ts +48 -0
  42. package/procs/nu.js +1 -0
  43. package/procs/scope.js +196 -0
  44. package/procs/shoot.d.ts +33 -0
  45. package/procs/shoot.js +3 -1
  46. package/procs/standard.d.ts +18 -0
  47. package/procs/standard.js +83 -0
  48. package/procs/standard.ts +126 -0
  49. package/procs/testAct.js +145 -0
  50. package/procs/testaro.d.ts +20 -0
  51. package/procs/testaro.js +237 -189
  52. package/procs/testaro.ts +321 -0
  53. package/procs/userPath.js +126 -0
  54. package/procs/xPath.d.ts +4 -0
  55. package/procs/xPath.js +82 -64
  56. package/procs/xPath.ts +106 -0
  57. package/procs/xPathScript.d.ts +6 -0
  58. package/procs/xPathScript.js +70 -0
  59. package/run.js +3 -2
  60. package/surea11y/README.md +44 -0
  61. package/surea11y/surea11y.browser.js +14 -0
  62. package/testaro/adbID.d.ts +3 -0
  63. package/testaro/adbID.js +39 -41
  64. package/testaro/adbID.ts +66 -0
  65. package/testaro/allCapStyle.d.ts +3 -0
  66. package/testaro/allCapStyle.js +32 -34
  67. package/testaro/allCapStyle.ts +55 -0
  68. package/testaro/allCaps.d.ts +16 -0
  69. package/testaro/allCaps.js +179 -151
  70. package/testaro/allCaps.ts +228 -0
  71. package/testaro/allHidden.d.ts +13 -0
  72. package/testaro/allHidden.js +30 -30
  73. package/testaro/allHidden.ts +50 -0
  74. package/testaro/allSlanted.d.ts +3 -0
  75. package/testaro/allSlanted.js +31 -33
  76. package/testaro/allSlanted.ts +54 -0
  77. package/testaro/altScheme.d.ts +3 -0
  78. package/testaro/altScheme.js +28 -30
  79. package/testaro/altScheme.ts +50 -0
  80. package/testaro/attVal.d.ts +3 -0
  81. package/testaro/attVal.js +21 -21
  82. package/testaro/attVal.ts +52 -0
  83. package/testaro/autocomplete.d.ts +3 -0
  84. package/testaro/autocomplete.js +60 -75
  85. package/testaro/autocomplete.ts +101 -0
  86. package/testaro/bulk.d.ts +13 -0
  87. package/testaro/bulk.js +33 -33
  88. package/testaro/bulk.ts +55 -0
  89. package/testaro/buttonMenu.d.ts +9 -0
  90. package/testaro/buttonMenu.js +318 -319
  91. package/testaro/buttonMenu.ts +391 -0
  92. package/testaro/captionLoc.d.ts +3 -0
  93. package/testaro/captionLoc.js +18 -20
  94. package/testaro/captionLoc.ts +40 -0
  95. package/testaro/datalistRef.d.ts +3 -0
  96. package/testaro/datalistRef.js +34 -36
  97. package/testaro/datalistRef.ts +55 -0
  98. package/testaro/distortion.d.ts +3 -0
  99. package/testaro/distortion.js +58 -26
  100. package/testaro/distortion.ts +81 -0
  101. package/testaro/docType.d.ts +15 -0
  102. package/testaro/docType.js +26 -25
  103. package/testaro/docType.ts +45 -0
  104. package/testaro/dupAtt.d.ts +16 -0
  105. package/testaro/dupAtt.js +114 -104
  106. package/testaro/dupAtt.ts +144 -0
  107. package/testaro/elements.d.ts +6 -0
  108. package/testaro/elements.js +153 -153
  109. package/testaro/elements.ts +215 -0
  110. package/testaro/embAc.d.ts +3 -0
  111. package/testaro/embAc.js +20 -20
  112. package/testaro/embAc.ts +40 -0
  113. package/testaro/focAll.d.ts +13 -0
  114. package/testaro/focAll.js +186 -191
  115. package/testaro/focAll.ts +217 -0
  116. package/testaro/focAndOp.d.ts +3 -0
  117. package/testaro/focAndOp.js +101 -104
  118. package/testaro/focAndOp.ts +128 -0
  119. package/testaro/focInd.d.ts +3 -0
  120. package/testaro/focInd.js +65 -66
  121. package/testaro/focInd.ts +96 -0
  122. package/testaro/focVis.d.ts +3 -0
  123. package/testaro/focVis.js +29 -30
  124. package/testaro/focVis.ts +52 -0
  125. package/testaro/headEl.d.ts +10 -0
  126. package/testaro/headEl.js +62 -62
  127. package/testaro/headEl.ts +82 -0
  128. package/testaro/headingAmb.d.ts +3 -0
  129. package/testaro/headingAmb.js +55 -63
  130. package/testaro/headingAmb.ts +72 -0
  131. package/testaro/hovInd.d.ts +12 -0
  132. package/testaro/hovInd.js +164 -130
  133. package/testaro/hovInd.ts +199 -0
  134. package/testaro/hover.d.ts +3 -0
  135. package/testaro/hover.js +156 -126
  136. package/testaro/hover.ts +154 -0
  137. package/testaro/hr.d.ts +3 -0
  138. package/testaro/hr.js +15 -17
  139. package/testaro/hr.ts +36 -0
  140. package/testaro/imageLink.d.ts +3 -0
  141. package/testaro/imageLink.js +18 -20
  142. package/testaro/imageLink.ts +42 -0
  143. package/testaro/labClash.d.ts +3 -0
  144. package/testaro/labClash.js +32 -33
  145. package/testaro/labClash.ts +54 -0
  146. package/testaro/legendLoc.d.ts +3 -0
  147. package/testaro/legendLoc.js +18 -20
  148. package/testaro/legendLoc.ts +42 -0
  149. package/testaro/lineHeight.d.ts +3 -0
  150. package/testaro/lineHeight.js +44 -48
  151. package/testaro/lineHeight.ts +70 -0
  152. package/testaro/linkAmb.d.ts +7 -0
  153. package/testaro/linkAmb.js +80 -80
  154. package/testaro/linkAmb.ts +105 -0
  155. package/testaro/linkExt.d.ts +3 -0
  156. package/testaro/linkExt.js +14 -16
  157. package/testaro/linkExt.ts +35 -0
  158. package/testaro/linkOldAtt.d.ts +3 -0
  159. package/testaro/linkOldAtt.js +26 -28
  160. package/testaro/linkOldAtt.ts +48 -0
  161. package/testaro/linkTo.d.ts +3 -0
  162. package/testaro/linkTo.js +23 -22
  163. package/testaro/linkTo.ts +43 -0
  164. package/testaro/linkUl.d.ts +3 -0
  165. package/testaro/linkUl.js +31 -34
  166. package/testaro/linkUl.ts +54 -0
  167. package/testaro/miniText.d.ts +3 -0
  168. package/testaro/miniText.js +42 -44
  169. package/testaro/miniText.ts +68 -0
  170. package/testaro/motion.d.ts +10 -0
  171. package/testaro/motion.js +93 -96
  172. package/testaro/motion.ts +125 -0
  173. package/testaro/nonTable.d.ts +3 -0
  174. package/testaro/nonTable.js +40 -45
  175. package/testaro/nonTable.ts +66 -0
  176. package/testaro/optRoleSel.d.ts +3 -0
  177. package/testaro/optRoleSel.js +17 -19
  178. package/testaro/optRoleSel.ts +41 -0
  179. package/testaro/phOnly.d.ts +3 -0
  180. package/testaro/phOnly.js +19 -21
  181. package/testaro/phOnly.ts +43 -0
  182. package/testaro/pseudoP.d.ts +3 -0
  183. package/testaro/pseudoP.js +37 -38
  184. package/testaro/pseudoP.ts +59 -0
  185. package/testaro/radioSet.d.ts +3 -0
  186. package/testaro/radioSet.js +58 -59
  187. package/testaro/radioSet.ts +79 -0
  188. package/testaro/registry.d.ts +63 -0
  189. package/testaro/registry.js +67 -0
  190. package/testaro/registry.ts +141 -0
  191. package/testaro/role.d.ts +3 -0
  192. package/testaro/role.js +28 -29
  193. package/testaro/role.ts +53 -0
  194. package/testaro/secHeading.d.ts +3 -0
  195. package/testaro/secHeading.js +31 -33
  196. package/testaro/secHeading.ts +53 -0
  197. package/testaro/styleDiff.d.ts +25 -0
  198. package/testaro/styleDiff.js +249 -252
  199. package/testaro/styleDiff.ts +303 -0
  200. package/testaro/tabNav.d.ts +33 -0
  201. package/testaro/tabNav.js +273 -342
  202. package/testaro/tabNav.ts +454 -0
  203. package/testaro/targetsNear.d.ts +9 -0
  204. package/testaro/targetsNear.js +131 -132
  205. package/testaro/targetsNear.ts +160 -0
  206. package/testaro/textNodes.d.ts +6 -0
  207. package/testaro/textNodes.js +139 -135
  208. package/testaro/textNodes.ts +185 -0
  209. package/testaro/textSem.d.ts +3 -0
  210. package/testaro/textSem.js +26 -28
  211. package/testaro/textSem.ts +47 -0
  212. package/testaro/title.d.ts +9 -0
  213. package/testaro/title.js +16 -13
  214. package/testaro/title.ts +31 -0
  215. package/testaro/titledEl.d.ts +3 -0
  216. package/testaro/titledEl.js +16 -18
  217. package/testaro/titledEl.ts +38 -0
  218. package/testaro/zIndex.d.ts +3 -0
  219. package/testaro/zIndex.js +20 -22
  220. package/testaro/zIndex.ts +42 -0
  221. package/tests/alfa.d.ts +45 -0
  222. package/tests/alfa.js +139 -141
  223. package/tests/alfa.ts +214 -0
  224. package/tests/aslint.d.ts +33 -0
  225. package/tests/aslint.js +273 -249
  226. package/tests/aslint.ts +301 -0
  227. package/tests/axe.d.ts +27 -0
  228. package/tests/axe.js +200 -200
  229. package/tests/axe.ts +277 -0
  230. package/tests/ed11y.d.ts +28 -0
  231. package/tests/ed11y.js +142 -99
  232. package/tests/ed11y.ts +178 -0
  233. package/tests/htmlcs.d.ts +21 -0
  234. package/tests/htmlcs.js +175 -140
  235. package/tests/htmlcs.ts +181 -0
  236. package/tests/ibm.d.ts +52 -0
  237. package/tests/ibm.js +165 -166
  238. package/tests/ibm.ts +251 -0
  239. package/tests/nuVal.d.ts +13 -0
  240. package/tests/nuVal.js +107 -112
  241. package/tests/nuVal.ts +145 -0
  242. package/tests/nuVnu.d.ts +15 -0
  243. package/tests/nuVnu.js +142 -111
  244. package/tests/nuVnu.ts +144 -0
  245. package/tests/pour.d.ts +31 -0
  246. package/tests/pour.js +242 -0
  247. package/tests/pour.ts +273 -0
  248. package/tests/qualWeb.d.ts +39 -0
  249. package/tests/qualWeb.js +303 -272
  250. package/tests/qualWeb.ts +415 -0
  251. package/tests/surea11y.d.ts +33 -0
  252. package/tests/surea11y.js +288 -0
  253. package/tests/surea11y.ts +334 -0
  254. package/tests/testaro.d.ts +25 -0
  255. package/tests/testaro.js +746 -652
  256. package/tests/testaro.ts +862 -0
  257. package/tests/wave.d.ts +46 -0
  258. package/tests/wave.js +167 -177
  259. package/tests/wave.ts +252 -0
  260. package/tsconfig.json +17 -0
  261. package/types.d.ts +243 -0
  262. package/types.js +10 -0
  263. package/types.ts +376 -0
  264. package/validation/act/README.md +46 -0
  265. package/validation/act/capture.js +424 -0
  266. package/validation/act/chromium-issue-draft.md +66 -0
  267. package/validation/act/fp-triage-2026-08-22.md +86 -0
  268. package/validation/act/isolation-notes.md +159 -0
  269. package/validation/act/playwright-issue-draft.md +73 -0
  270. package/validation/act/propose-mappings.js +0 -0
  271. package/validation/act/repro-cdp-raw.js +105 -0
  272. package/validation/act/repro-metarefresh.js +65 -0
  273. package/validation/act/score.js +184 -0
  274. package/validation/act/stage3a-stress-report.md +69 -0
  275. package/validation/act/stage3b-mapping-proposals.md +94 -0
  276. package/validation/act/stage3b-triage-draft.md +138 -0
  277. package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
  278. package/validation/executors/netWatch.js +180 -90
  279. package/validation/executors/test.js +17 -2
  280. package/validation/executors/tests.js +118 -10
  281. package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
  282. package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
  283. package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
  284. package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
  285. package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
  286. package/validation/knownFailures.json +1 -0
  287. package/validation/tests/jobProperties/adbID.json +27 -2
  288. package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
  289. package/validation/tests/jobProperties/allCaps.json +25 -0
  290. package/validation/tests/jobProperties/allHidden.json +133 -13
  291. package/validation/tests/jobProperties/allSlanted.json +3 -3
  292. package/validation/tests/jobProperties/altScheme.json +23 -8
  293. package/validation/tests/jobProperties/attVal.json +57 -57
  294. package/validation/tests/jobProperties/autocomplete.json +11 -1
  295. package/validation/tests/jobProperties/bulk.json +6 -1
  296. package/validation/tests/jobProperties/buttonMenu.json +63 -42
  297. package/validation/tests/jobProperties/captionLoc.json +1 -6
  298. package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
  299. package/validation/tests/jobProperties/checkpoint-page.json +397 -0
  300. package/validation/tests/jobProperties/checkpoint.json +395 -0
  301. package/validation/tests/jobProperties/datalistRef.json +20 -5
  302. package/validation/tests/jobProperties/distortion.json +28 -3
  303. package/validation/tests/jobProperties/docType.json +2 -2
  304. package/validation/tests/jobProperties/dupAtt.json +48 -33
  305. package/validation/tests/jobProperties/elements.json +28 -28
  306. package/validation/tests/jobProperties/embAc.json +36 -31
  307. package/validation/tests/jobProperties/focAndOp.json +284 -0
  308. package/validation/tests/jobProperties/focInd.json +34 -34
  309. package/validation/tests/jobProperties/focVis.json +1 -1
  310. package/validation/tests/jobProperties/hover.json +39 -37
  311. package/validation/tests/jobProperties/hr.json +13 -3
  312. package/validation/tests/jobProperties/imageLink.json +0 -5
  313. package/validation/tests/jobProperties/labClash.json +68 -33
  314. package/validation/tests/jobProperties/legendLoc.json +1 -6
  315. package/validation/tests/jobProperties/lineHeight.json +2 -2
  316. package/validation/tests/jobProperties/linkAmb.json +13 -18
  317. package/validation/tests/jobProperties/linkExt.json +1 -1
  318. package/validation/tests/jobProperties/linkOldAtt.json +12 -2
  319. package/validation/tests/jobProperties/linkTo.json +1 -1
  320. package/validation/tests/jobProperties/linkUl.json +65 -65
  321. package/validation/tests/jobProperties/miniText.json +7 -2
  322. package/validation/tests/jobProperties/motion.json +4 -50
  323. package/validation/tests/jobProperties/nonTable.json +48 -3
  324. package/validation/tests/jobProperties/optRoleSel.json +12 -2
  325. package/validation/tests/jobProperties/phOnly.json +6 -16
  326. package/validation/tests/jobProperties/pseudoP.json +17 -2
  327. package/validation/tests/jobProperties/radioSet.json +30 -30
  328. package/validation/tests/jobProperties/role.json +25 -5
  329. package/validation/tests/jobProperties/secHeading.json +26 -21
  330. package/validation/tests/jobProperties/styleDiff.json +35 -5
  331. package/validation/tests/jobProperties/tabNav.json +3 -1
  332. package/validation/tests/jobProperties/targetsNear.json +237 -0
  333. package/validation/tests/jobProperties/textNodes.json +29 -29
  334. package/validation/tests/jobProperties/textSem.json +26 -1
  335. package/validation/tests/jobProperties/title.json +12 -2
  336. package/validation/tests/jobProperties/titledEl.json +39 -9
  337. package/validation/tests/jobProperties/userPath.json +317 -0
  338. package/validation/tests/jobProperties/zIndex.json +40 -35
  339. package/validation/tests/targets/allCapStyle/index.html +26 -0
  340. package/validation/tests/targets/checkpoint/index.html +38 -0
  341. package/validation/tests/targets/datalistRef/index.html +1 -1
  342. package/validation/tests/targets/focAndOp/bad.html +29 -0
  343. package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
  344. package/validation/tests/targets/focInd/bad.html +2 -1
  345. package/validation/tests/targets/headEl/index.html +10 -1
  346. package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
  347. package/validation/tests/targets/userPath/index.html +42 -0
  348. package/validation/validateTest.js +62 -10
  349. package/.claude/settings.local.json +0 -11
  350. package/.eslintrc.json +0 -41
  351. package/htmlcs/.eslintrc.json +0 -67
  352. package/memory/MEMORY.md +0 -3
  353. package/memory/project_validation_pause.md +0 -10
  354. package/validation/tests/jobProperties/linkTitle.json +0 -127
  355. package/validation/tests/jobProperties/opFoc.json +0 -164
  356. package/validation/tests/jobProperties/targetSmall.json +0 -152
  357. package/validation/tests/jobProperties/targetTiny.json +0 -142
  358. package/validation/tests/targets/focOp/bad.html +0 -25
  359. package/validation/tests/targets/linkTitle/index.html +0 -24
  360. package/validation/tests/targets/opFoc/bad.html +0 -26
  361. package/validation/tests/targets/opFoc/good.html +0 -23
package/testaro/hovInd.js CHANGED
@@ -1,144 +1,178 @@
1
+ "use strict";
1
2
  /*
2
3
  © 2023–2025 CVS Health and/or one of its affiliates. All rights reserved.
4
+ © 2026 Jeff Witt.
3
5
  © 2025–2026 Jonathan Robert Pool.
4
6
 
5
7
  Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
6
8
 
7
9
  SPDX-License-Identifier: MIT
8
10
  */
9
-
10
- /*
11
- hovInd
12
- This test reports confusing hover indication.
13
- */
14
-
15
- // IMPORTS
16
-
17
- const {doTest} = require('../procs/testaro');
18
-
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;
19
65
  // FUNCTIONS
20
-
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]);
21
98
  // Runs the test and returns the result.
22
- exports.reporter = async (page, report, _, withItems) => {
23
- const getBadWhat = element => {
24
- const violationTypes = [];
25
- const isVisible = element.checkVisibility({
26
- contentVisibilityAuto: true,
27
- opacityProperty: true,
28
- visibilityProperty: true
29
- });
30
- // If the element is visible:
31
- if (isVisible) {
32
- // Get its live style declaration.
33
- const styleDec = window.getComputedStyle(element);
34
- // FUNCTION DEFINITIONS START
35
- // Returns hover-related style data on a trigger.
36
- const getStyleData = () => {
37
- const {
38
- cursor,
39
- borderColor,
40
- borderStyle,
41
- borderWidth,
42
- outlineColor,
43
- outlineStyle,
44
- outlineWidth,
45
- outlineOffset,
46
- color,
47
- backgroundColor
48
- } = styleDec;
49
- return {
50
- tagName: element.tagName,
51
- inputType: element.tagName === 'INPUT' ? element.getAttribute('type') || 'text' : null,
52
- cursor: cursor.replace(/^.+, */, ''),
53
- border: `${borderColor} ${borderStyle} ${borderWidth}`,
54
- outline: `${outlineColor} ${outlineStyle} ${outlineWidth} ${outlineOffset}`,
55
- color,
56
- backgroundColor
57
- };
58
- };
59
- // Returns whether the cursor is bad when the element is hovered over.
60
- const cursorIsBad = hoverCursor => {
61
- const {tagName, type} = element;
62
- if (tagName === 'A' || tagName === 'INPUT' && type === 'image') {
63
- return hoverCursor !== 'pointer';
64
- }
65
- if (tagName === 'INPUT') {
66
- if (['button', 'radio', 'reset', 'submit'].some(typeName => type === typeName)) {
67
- return hoverCursor !== 'default';
68
- }
69
- return hoverCursor !== 'text';
70
- }
71
- return ! ['auto', 'default'].includes(hoverCursor);
72
- };
73
- // Returns whether two hover styles are effectively identical.
74
- const areAlike = (styles0, styles1) => {
75
- // Return whether they are effectively identical.
76
- const areAlike = ['cursor', 'backgroundColor', 'border', 'color', 'outline']
77
- .every(style => styles1[style] === styles0[style]);
78
- return areAlike;
79
- };
80
- // FUNCTION DEFINITIONS END
81
- // Get its style data when neither focused nor hovered over.
82
- const defaultStyleData = getStyleData();
83
- // Correct the cursor value.
84
- defaultStyleData.cursor = 'default';
85
- // Get its style data when only focused.
86
- element.focus();
87
- const focusStyleData = getStyleData();
88
- // Correct the cursor value.
89
- focusStyleData.cursor = 'default';
90
- // Get its style data when only hovered over.
91
- element.blur();
92
- element.dispatchEvent(new MouseEvent('mouseenter'));
93
- const hoverStyleData = getStyleData();
94
- const data = {};
95
- // If the cursor is confusing when the element is only hovered over:
96
- if (cursorIsBad(hoverStyleData.cursor)) {
97
- // Add this to the violation types.
98
- violationTypes.push(
99
- `nonstandard mouse cursor (${hoverStyleData.cursor}) when hovered over`
100
- );
101
- }
102
- // If the neutral and hover styles are indistinguishable:
103
- if (areAlike(defaultStyleData, hoverStyleData)) {
104
- // Add this to the violation types.
105
- violationTypes.push('normal and hover styles are indistinguishable');
106
- // Add the details to the data.
107
- data.n_h = {
108
- neutral: defaultStyleData,
109
- hover: hoverStyleData
110
- };
111
- }
112
- // If the focus and hoverstyles are indistinguishable:
113
- if (areAlike(focusStyleData, hoverStyleData)) {
114
- // Add this to the violation types.
115
- violationTypes.push('focus and hover styles are indistinguishable');
116
- // Add the details to the data.
117
- data.f_h = {
118
- focus: focusStyleData,
119
- hover: hoverStyleData
120
- };
121
- }
122
- // If any violations occurred:
123
- if (violationTypes.length) {
124
- const description = `Element styles do not clearly indicate hovering: ${violationTypes.join('; ')}`;
125
- // If there are additional data:
126
- if (Object.keys(data).length) {
127
- // Return the violation description and data.
128
- return {
129
- description,
130
- data
131
- };
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
+ }
132
155
  }
133
- // Otherwise, i.e. if there are no additional data:
134
- else {
135
- // Return the violation description.
136
- 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;
137
166
  }
138
- }
139
167
  }
140
- };
141
- const selector = 'body a, body button, body input, body [onmouseenter], body [onmouseover]';
142
- const whats = 'elements have confusing hover indicators';
143
- 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
+ };
144
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>;