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.
- package/.gitattributes +94 -0
- package/.github/workflows/ci.yml +58 -0
- package/.github/workflows/publish.yml +49 -0
- package/.github/workflows/typescript.yml +35 -0
- package/AGENTS.md +2 -2
- package/CLAUDE.md +24 -18
- package/CONTAINERS.md +9 -6
- package/CONTRIBUTING.md +29 -3
- package/Dockerfile +1 -1
- package/README.md +73 -19
- package/UPGRADES.md +4 -0
- package/actSpecs-doc.md +13 -1
- package/actSpecs.js +29 -5
- package/call.js +6 -5
- package/docker-compose.yml +8 -2
- package/docs/checkpoint-scanning.md +199 -0
- package/docs/standard-result-outcome.md +165 -0
- package/env.example +44 -10
- package/eslint.config.mjs +101 -0
- package/netWatch.js +372 -222
- package/package.json +46 -31
- package/pour/README.md +47 -0
- package/pour/pour.min.js +8 -0
- package/procs/actDo.js +808 -0
- package/procs/catalog.d.ts +17 -0
- package/procs/catalog.js +312 -221
- package/procs/catalog.ts +398 -0
- package/procs/checkpoint.js +110 -0
- package/procs/config.d.ts +35 -0
- package/procs/config.js +62 -0
- package/procs/dateTime.js +2 -1
- package/procs/doActs.js +442 -849
- package/procs/doTestAct.js +27 -121
- package/procs/flow.js +221 -0
- package/procs/generateRuleRegistry.js +94 -0
- package/procs/getSource.d.ts +26 -0
- package/procs/job.js +115 -1
- package/procs/launch.d.ts +40 -0
- package/procs/launch.js +239 -108
- package/procs/nu.d.ts +48 -0
- package/procs/scope.js +196 -0
- package/procs/shoot.d.ts +33 -0
- package/procs/standard.d.ts +18 -0
- package/procs/standard.js +83 -0
- package/procs/standard.ts +126 -0
- package/procs/testAct.js +145 -0
- package/procs/testaro.d.ts +20 -0
- package/procs/testaro.js +236 -189
- package/procs/testaro.ts +321 -0
- package/procs/userPath.js +126 -0
- package/procs/xPath.d.ts +4 -0
- package/procs/xPath.js +81 -69
- package/procs/xPath.ts +106 -0
- package/procs/xPathScript.d.ts +6 -0
- package/procs/xPathScript.js +70 -0
- package/run.js +3 -2
- package/surea11y/README.md +44 -0
- package/surea11y/surea11y.browser.js +14 -0
- package/testaro/adbID.d.ts +3 -0
- package/testaro/adbID.js +38 -41
- package/testaro/adbID.ts +66 -0
- package/testaro/allCapStyle.d.ts +3 -0
- package/testaro/allCapStyle.js +31 -34
- package/testaro/allCapStyle.ts +55 -0
- package/testaro/allCaps.d.ts +16 -0
- package/testaro/allCaps.js +179 -151
- package/testaro/allCaps.ts +228 -0
- package/testaro/allHidden.d.ts +13 -0
- package/testaro/allHidden.js +29 -30
- package/testaro/allHidden.ts +50 -0
- package/testaro/allSlanted.d.ts +3 -0
- package/testaro/allSlanted.js +30 -33
- package/testaro/allSlanted.ts +54 -0
- package/testaro/altScheme.d.ts +3 -0
- package/testaro/altScheme.js +27 -30
- package/testaro/altScheme.ts +50 -0
- package/testaro/attVal.d.ts +3 -0
- package/testaro/attVal.js +20 -21
- package/testaro/attVal.ts +52 -0
- package/testaro/autocomplete.d.ts +3 -0
- package/testaro/autocomplete.js +59 -75
- package/testaro/autocomplete.ts +101 -0
- package/testaro/bulk.d.ts +13 -0
- package/testaro/bulk.js +32 -33
- package/testaro/bulk.ts +55 -0
- package/testaro/buttonMenu.d.ts +9 -0
- package/testaro/buttonMenu.js +317 -319
- package/testaro/buttonMenu.ts +391 -0
- package/testaro/captionLoc.d.ts +3 -0
- package/testaro/captionLoc.js +17 -20
- package/testaro/captionLoc.ts +40 -0
- package/testaro/datalistRef.d.ts +3 -0
- package/testaro/datalistRef.js +33 -36
- package/testaro/datalistRef.ts +55 -0
- package/testaro/distortion.d.ts +3 -0
- package/testaro/distortion.js +57 -26
- package/testaro/distortion.ts +81 -0
- package/testaro/docType.d.ts +15 -0
- package/testaro/docType.js +25 -25
- package/testaro/docType.ts +45 -0
- package/testaro/dupAtt.d.ts +16 -0
- package/testaro/dupAtt.js +113 -104
- package/testaro/dupAtt.ts +144 -0
- package/testaro/elements.d.ts +6 -0
- package/testaro/elements.js +153 -153
- package/testaro/elements.ts +215 -0
- package/testaro/embAc.d.ts +3 -0
- package/testaro/embAc.js +19 -20
- package/testaro/embAc.ts +40 -0
- package/testaro/focAll.d.ts +13 -0
- package/testaro/focAll.js +185 -191
- package/testaro/focAll.ts +217 -0
- package/testaro/focAndOp.d.ts +3 -0
- package/testaro/focAndOp.js +100 -104
- package/testaro/focAndOp.ts +128 -0
- package/testaro/focInd.d.ts +3 -0
- package/testaro/focInd.js +64 -66
- package/testaro/focInd.ts +96 -0
- package/testaro/focVis.d.ts +3 -0
- package/testaro/focVis.js +28 -30
- package/testaro/focVis.ts +52 -0
- package/testaro/headEl.d.ts +10 -0
- package/testaro/headEl.js +61 -62
- package/testaro/headEl.ts +82 -0
- package/testaro/headingAmb.d.ts +3 -0
- package/testaro/headingAmb.js +54 -63
- package/testaro/headingAmb.ts +72 -0
- package/testaro/hovInd.d.ts +12 -0
- package/testaro/hovInd.js +163 -130
- package/testaro/hovInd.ts +199 -0
- package/testaro/hover.d.ts +3 -0
- package/testaro/hover.js +155 -126
- package/testaro/hover.ts +154 -0
- package/testaro/hr.d.ts +3 -0
- package/testaro/hr.js +14 -17
- package/testaro/hr.ts +36 -0
- package/testaro/imageLink.d.ts +3 -0
- package/testaro/imageLink.js +17 -20
- package/testaro/imageLink.ts +42 -0
- package/testaro/labClash.d.ts +3 -0
- package/testaro/labClash.js +31 -33
- package/testaro/labClash.ts +54 -0
- package/testaro/legendLoc.d.ts +3 -0
- package/testaro/legendLoc.js +17 -20
- package/testaro/legendLoc.ts +42 -0
- package/testaro/lineHeight.d.ts +3 -0
- package/testaro/lineHeight.js +43 -48
- package/testaro/lineHeight.ts +70 -0
- package/testaro/linkAmb.d.ts +7 -0
- package/testaro/linkAmb.js +79 -80
- package/testaro/linkAmb.ts +105 -0
- package/testaro/linkExt.d.ts +3 -0
- package/testaro/linkExt.js +13 -16
- package/testaro/linkExt.ts +35 -0
- package/testaro/linkOldAtt.d.ts +3 -0
- package/testaro/linkOldAtt.js +25 -28
- package/testaro/linkOldAtt.ts +48 -0
- package/testaro/linkTo.d.ts +3 -0
- package/testaro/linkTo.js +22 -22
- package/testaro/linkTo.ts +43 -0
- package/testaro/linkUl.d.ts +3 -0
- package/testaro/linkUl.js +30 -34
- package/testaro/linkUl.ts +54 -0
- package/testaro/miniText.d.ts +3 -0
- package/testaro/miniText.js +41 -44
- package/testaro/miniText.ts +68 -0
- package/testaro/motion.d.ts +10 -0
- package/testaro/motion.js +92 -96
- package/testaro/motion.ts +125 -0
- package/testaro/nonTable.d.ts +3 -0
- package/testaro/nonTable.js +39 -45
- package/testaro/nonTable.ts +66 -0
- package/testaro/optRoleSel.d.ts +3 -0
- package/testaro/optRoleSel.js +16 -19
- package/testaro/optRoleSel.ts +41 -0
- package/testaro/phOnly.d.ts +3 -0
- package/testaro/phOnly.js +18 -21
- package/testaro/phOnly.ts +43 -0
- package/testaro/pseudoP.d.ts +3 -0
- package/testaro/pseudoP.js +36 -38
- package/testaro/pseudoP.ts +59 -0
- package/testaro/radioSet.d.ts +3 -0
- package/testaro/radioSet.js +57 -59
- package/testaro/radioSet.ts +79 -0
- package/testaro/registry.d.ts +63 -0
- package/testaro/registry.js +67 -0
- package/testaro/registry.ts +141 -0
- package/testaro/role.d.ts +3 -0
- package/testaro/role.js +27 -29
- package/testaro/role.ts +53 -0
- package/testaro/secHeading.d.ts +3 -0
- package/testaro/secHeading.js +30 -33
- package/testaro/secHeading.ts +53 -0
- package/testaro/styleDiff.d.ts +25 -0
- package/testaro/styleDiff.js +248 -252
- package/testaro/styleDiff.ts +303 -0
- package/testaro/tabNav.d.ts +33 -0
- package/testaro/tabNav.js +272 -342
- package/testaro/tabNav.ts +454 -0
- package/testaro/targetsNear.d.ts +9 -0
- package/testaro/targetsNear.js +130 -132
- package/testaro/targetsNear.ts +160 -0
- package/testaro/textNodes.d.ts +6 -0
- package/testaro/textNodes.js +139 -135
- package/testaro/textNodes.ts +185 -0
- package/testaro/textSem.d.ts +3 -0
- package/testaro/textSem.js +25 -28
- package/testaro/textSem.ts +47 -0
- package/testaro/title.d.ts +9 -0
- package/testaro/title.js +16 -13
- package/testaro/title.ts +31 -0
- package/testaro/titledEl.d.ts +3 -0
- package/testaro/titledEl.js +15 -18
- package/testaro/titledEl.ts +38 -0
- package/testaro/zIndex.d.ts +3 -0
- package/testaro/zIndex.js +19 -22
- package/testaro/zIndex.ts +42 -0
- package/tests/alfa.d.ts +45 -0
- package/tests/alfa.js +138 -141
- package/tests/alfa.ts +214 -0
- package/tests/aslint.d.ts +33 -0
- package/tests/aslint.js +272 -249
- package/tests/aslint.ts +301 -0
- package/tests/axe.d.ts +27 -0
- package/tests/axe.js +199 -200
- package/tests/axe.ts +277 -0
- package/tests/ed11y.d.ts +28 -0
- package/tests/ed11y.js +141 -99
- package/tests/ed11y.ts +178 -0
- package/tests/htmlcs.d.ts +21 -0
- package/tests/htmlcs.js +174 -140
- package/tests/htmlcs.ts +181 -0
- package/tests/ibm.d.ts +52 -0
- package/tests/ibm.js +164 -166
- package/tests/ibm.ts +251 -0
- package/tests/nuVal.d.ts +13 -0
- package/tests/nuVal.js +107 -112
- package/tests/nuVal.ts +145 -0
- package/tests/nuVnu.d.ts +15 -0
- package/tests/nuVnu.js +141 -111
- package/tests/nuVnu.ts +144 -0
- package/tests/pour.d.ts +31 -0
- package/tests/pour.js +242 -0
- package/tests/pour.ts +273 -0
- package/tests/qualWeb.d.ts +39 -0
- package/tests/qualWeb.js +302 -272
- package/tests/qualWeb.ts +415 -0
- package/tests/surea11y.d.ts +33 -0
- package/tests/surea11y.js +288 -0
- package/tests/surea11y.ts +334 -0
- package/tests/testaro.d.ts +25 -0
- package/tests/testaro.js +745 -652
- package/tests/testaro.ts +862 -0
- package/tests/wave.d.ts +46 -0
- package/tests/wave.js +166 -177
- package/tests/wave.ts +252 -0
- package/tsconfig.json +17 -0
- package/types.d.ts +243 -0
- package/types.js +10 -0
- package/types.ts +376 -0
- package/validation/act/README.md +46 -0
- package/validation/act/capture.js +424 -0
- package/validation/act/chromium-issue-draft.md +66 -0
- package/validation/act/fp-triage-2026-08-22.md +86 -0
- package/validation/act/isolation-notes.md +159 -0
- package/validation/act/playwright-issue-draft.md +73 -0
- package/validation/act/propose-mappings.js +0 -0
- package/validation/act/repro-cdp-raw.js +105 -0
- package/validation/act/repro-metarefresh.js +65 -0
- package/validation/act/score.js +184 -0
- package/validation/act/stage3a-stress-report.md +69 -0
- package/validation/act/stage3b-mapping-proposals.md +94 -0
- package/validation/act/stage3b-triage-draft.md +138 -0
- package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
- package/validation/executors/netWatch.js +180 -90
- package/validation/executors/test.js +17 -2
- package/validation/executors/tests.js +118 -10
- package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
- package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
- package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
- package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
- package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
- package/validation/knownFailures.json +1 -0
- package/validation/tests/jobProperties/adbID.json +27 -2
- package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
- package/validation/tests/jobProperties/allCaps.json +25 -0
- package/validation/tests/jobProperties/allHidden.json +133 -13
- package/validation/tests/jobProperties/allSlanted.json +3 -3
- package/validation/tests/jobProperties/altScheme.json +23 -8
- package/validation/tests/jobProperties/attVal.json +57 -57
- package/validation/tests/jobProperties/autocomplete.json +11 -1
- package/validation/tests/jobProperties/bulk.json +6 -1
- package/validation/tests/jobProperties/buttonMenu.json +63 -42
- package/validation/tests/jobProperties/captionLoc.json +1 -6
- package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
- package/validation/tests/jobProperties/checkpoint-page.json +397 -0
- package/validation/tests/jobProperties/checkpoint.json +395 -0
- package/validation/tests/jobProperties/datalistRef.json +20 -5
- package/validation/tests/jobProperties/distortion.json +28 -3
- package/validation/tests/jobProperties/docType.json +2 -2
- package/validation/tests/jobProperties/dupAtt.json +48 -33
- package/validation/tests/jobProperties/elements.json +28 -28
- package/validation/tests/jobProperties/embAc.json +36 -31
- package/validation/tests/jobProperties/focAndOp.json +284 -0
- package/validation/tests/jobProperties/focInd.json +34 -34
- package/validation/tests/jobProperties/focVis.json +1 -1
- package/validation/tests/jobProperties/hover.json +39 -37
- package/validation/tests/jobProperties/hr.json +13 -3
- package/validation/tests/jobProperties/imageLink.json +0 -5
- package/validation/tests/jobProperties/labClash.json +68 -33
- package/validation/tests/jobProperties/legendLoc.json +1 -6
- package/validation/tests/jobProperties/lineHeight.json +2 -2
- package/validation/tests/jobProperties/linkAmb.json +13 -18
- package/validation/tests/jobProperties/linkExt.json +1 -1
- package/validation/tests/jobProperties/linkOldAtt.json +12 -2
- package/validation/tests/jobProperties/linkTo.json +1 -1
- package/validation/tests/jobProperties/linkUl.json +65 -65
- package/validation/tests/jobProperties/miniText.json +7 -2
- package/validation/tests/jobProperties/motion.json +4 -50
- package/validation/tests/jobProperties/nonTable.json +48 -3
- package/validation/tests/jobProperties/optRoleSel.json +12 -2
- package/validation/tests/jobProperties/phOnly.json +6 -16
- package/validation/tests/jobProperties/pseudoP.json +17 -2
- package/validation/tests/jobProperties/radioSet.json +30 -30
- package/validation/tests/jobProperties/role.json +25 -5
- package/validation/tests/jobProperties/secHeading.json +26 -21
- package/validation/tests/jobProperties/styleDiff.json +35 -5
- package/validation/tests/jobProperties/tabNav.json +3 -1
- package/validation/tests/jobProperties/targetsNear.json +237 -0
- package/validation/tests/jobProperties/textNodes.json +29 -29
- package/validation/tests/jobProperties/textSem.json +26 -1
- package/validation/tests/jobProperties/title.json +12 -2
- package/validation/tests/jobProperties/titledEl.json +39 -9
- package/validation/tests/jobProperties/userPath.json +317 -0
- package/validation/tests/jobProperties/zIndex.json +40 -35
- package/validation/tests/targets/allCapStyle/index.html +26 -0
- package/validation/tests/targets/checkpoint/index.html +38 -0
- package/validation/tests/targets/datalistRef/index.html +1 -1
- package/validation/tests/targets/focAndOp/bad.html +29 -0
- package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
- package/validation/tests/targets/focInd/bad.html +2 -1
- package/validation/tests/targets/headEl/index.html +10 -1
- package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
- package/validation/tests/targets/userPath/index.html +42 -0
- package/validation/validateTest.js +61 -9
- package/.claude/settings.local.json +0 -11
- package/.eslintrc.json +0 -41
- package/htmlcs/.eslintrc.json +0 -67
- package/validation/tests/jobProperties/linkTitle.json +0 -127
- package/validation/tests/jobProperties/opFoc.json +0 -164
- package/validation/tests/jobProperties/targetSmall.json +0 -152
- package/validation/tests/jobProperties/targetTiny.json +0 -142
- package/validation/tests/targets/focOp/bad.html +0 -25
- package/validation/tests/targets/linkTitle/index.html +0 -24
- package/validation/tests/targets/opFoc/bad.html +0 -26
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
};
|