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/tests/testaro.ts
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
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
|
|
7
|
+
https://opensource.org/license/mit/ for details.
|
|
8
|
+
|
|
9
|
+
SPDX-License-Identifier: MIT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
testaro
|
|
14
|
+
Implements the Testaro evaluative rules.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// IMPORTS
|
|
18
|
+
|
|
19
|
+
// Shared configuration for timeout multiplier.
|
|
20
|
+
import type {Page} from 'playwright';
|
|
21
|
+
import {applyMultiplier, ruleLaunchRetries} from '../procs/config';
|
|
22
|
+
import {browserClose, launch} from '../procs/launch';
|
|
23
|
+
// Function to get an empty standard result.
|
|
24
|
+
import {getStandardResult} from '../procs/standard';
|
|
25
|
+
import {ruleModules} from '../testaro/registry';
|
|
26
|
+
import type {RuleID, RuleModule} from '../testaro/registry';
|
|
27
|
+
import type {Act, BrowserID, Outcome, Report, StandardInstance, UncertaintyCode} from '../types';
|
|
28
|
+
|
|
29
|
+
// TYPES
|
|
30
|
+
|
|
31
|
+
// Metadata of one rule.
|
|
32
|
+
interface RuleMeta {
|
|
33
|
+
id: RuleID;
|
|
34
|
+
what: string;
|
|
35
|
+
contaminates: boolean;
|
|
36
|
+
needsAccessibleName: boolean;
|
|
37
|
+
// Default outcome of the rule's violations ('failed' unless specified): 'cantTell' for a
|
|
38
|
+
// heuristic rule whose violations need human confirmation; a violation may override it.
|
|
39
|
+
outcome?: Outcome;
|
|
40
|
+
// Default uncertainty code of the rule's cantTell violations.
|
|
41
|
+
uncertainty?: UncertaintyCode;
|
|
42
|
+
// Whether the rule's verdict on an element depends only on that element's subtree and
|
|
43
|
+
// what it references, so a changed-scope test act may restrict the rule to changed subtrees.
|
|
44
|
+
// Page-level rules (heading order, landmarks, duplicate IDs, focus order, hover, motion, …)
|
|
45
|
+
// are not local: any change can alter their verdicts, so they always test the whole page.
|
|
46
|
+
local: boolean;
|
|
47
|
+
timeOut: number;
|
|
48
|
+
defaultOn: boolean;
|
|
49
|
+
// Read below but defined by no rule, so the temporary-directory branch never runs;
|
|
50
|
+
// verbatim from the original.
|
|
51
|
+
needsTmpDir?: boolean;
|
|
52
|
+
}
|
|
53
|
+
// The testaro-act properties this reporter consumes.
|
|
54
|
+
interface TestaroAct extends Act {
|
|
55
|
+
target?: {url: string; what?: string};
|
|
56
|
+
args?: Record<string, unknown[]>;
|
|
57
|
+
stopOnFail?: boolean;
|
|
58
|
+
withItems?: boolean;
|
|
59
|
+
rules?: string[];
|
|
60
|
+
launch?: {browserID?: BrowserID};
|
|
61
|
+
scope?: 'page' | 'changed';
|
|
62
|
+
// Launch retries per rule (default TESTARO_RULE_RETRIES, else 2).
|
|
63
|
+
retries?: number;
|
|
64
|
+
}
|
|
65
|
+
// The result of one rule test.
|
|
66
|
+
interface RuleTestResult {
|
|
67
|
+
id: RuleID;
|
|
68
|
+
prevented: boolean;
|
|
69
|
+
error: string;
|
|
70
|
+
data: unknown;
|
|
71
|
+
totals: number[];
|
|
72
|
+
instances: StandardInstance[];
|
|
73
|
+
elapsedTime: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// CONSTANTS
|
|
77
|
+
|
|
78
|
+
// Metadata of all rules in default execution order.
|
|
79
|
+
const allRules: RuleMeta[] = [
|
|
80
|
+
{
|
|
81
|
+
id: 'adbID',
|
|
82
|
+
what: 'elements with ambiguous or missing referenced descriptions',
|
|
83
|
+
contaminates: false,
|
|
84
|
+
needsAccessibleName: false,
|
|
85
|
+
local: true,
|
|
86
|
+
timeOut: 5,
|
|
87
|
+
defaultOn: true
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'allCapStyle',
|
|
91
|
+
what: 'elements with all-capital text transformation styles',
|
|
92
|
+
contaminates: false,
|
|
93
|
+
needsAccessibleName: false,
|
|
94
|
+
local: true,
|
|
95
|
+
timeOut: 5,
|
|
96
|
+
defaultOn: false
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'allCaps',
|
|
100
|
+
what: 'elements with unnecessarily all-capital text substrings',
|
|
101
|
+
contaminates: false,
|
|
102
|
+
needsAccessibleName: false,
|
|
103
|
+
local: true,
|
|
104
|
+
outcome: 'cantTell',
|
|
105
|
+
uncertainty: 'judgement-required',
|
|
106
|
+
timeOut: 30,
|
|
107
|
+
defaultOn: true
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'allHidden',
|
|
111
|
+
what: 'page that is entirely or mostly hidden',
|
|
112
|
+
contaminates: false,
|
|
113
|
+
needsAccessibleName: false,
|
|
114
|
+
local: false,
|
|
115
|
+
timeOut: 5,
|
|
116
|
+
defaultOn: true
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'allSlanted',
|
|
120
|
+
what: 'leaf elements with entirely italic or oblique text longer than 39 characters',
|
|
121
|
+
contaminates: false,
|
|
122
|
+
needsAccessibleName: false,
|
|
123
|
+
local: true,
|
|
124
|
+
timeOut: 5,
|
|
125
|
+
defaultOn: true
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'altScheme',
|
|
129
|
+
what: 'img elements with alt attributes having URLs as their entire values',
|
|
130
|
+
contaminates: false,
|
|
131
|
+
needsAccessibleName: false,
|
|
132
|
+
local: true,
|
|
133
|
+
timeOut: 5,
|
|
134
|
+
defaultOn: true
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'attVal',
|
|
138
|
+
what: 'elements with attributes having illicit values',
|
|
139
|
+
contaminates: false,
|
|
140
|
+
needsAccessibleName: false,
|
|
141
|
+
local: true,
|
|
142
|
+
timeOut: 5,
|
|
143
|
+
defaultOn: false
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'bulk',
|
|
147
|
+
what: 'large count of visible elements',
|
|
148
|
+
contaminates: false,
|
|
149
|
+
needsAccessibleName: false,
|
|
150
|
+
local: false,
|
|
151
|
+
timeOut: 5,
|
|
152
|
+
defaultOn: true
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'captionLoc',
|
|
156
|
+
what: 'caption elements that are not first children of table elements',
|
|
157
|
+
contaminates: false,
|
|
158
|
+
needsAccessibleName: false,
|
|
159
|
+
local: true,
|
|
160
|
+
timeOut: 5,
|
|
161
|
+
defaultOn: true
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'datalistRef',
|
|
165
|
+
what: 'elements with ambiguous or missing referenced datalist elements',
|
|
166
|
+
contaminates: false,
|
|
167
|
+
needsAccessibleName: false,
|
|
168
|
+
local: true,
|
|
169
|
+
timeOut: 5,
|
|
170
|
+
defaultOn: true
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'distortion',
|
|
174
|
+
what: 'distorted text',
|
|
175
|
+
contaminates: false,
|
|
176
|
+
needsAccessibleName: false,
|
|
177
|
+
local: true,
|
|
178
|
+
timeOut: 5,
|
|
179
|
+
defaultOn: true
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'docType',
|
|
183
|
+
what: 'document without a doctype property',
|
|
184
|
+
contaminates: false,
|
|
185
|
+
needsAccessibleName: false,
|
|
186
|
+
local: false,
|
|
187
|
+
timeOut: 5,
|
|
188
|
+
defaultOn: true
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'embAc',
|
|
192
|
+
what: 'active elements embedded in links or buttons',
|
|
193
|
+
contaminates: false,
|
|
194
|
+
needsAccessibleName: false,
|
|
195
|
+
local: true,
|
|
196
|
+
timeOut: 5,
|
|
197
|
+
defaultOn: true
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'headEl',
|
|
201
|
+
what: 'invalid elements within the head',
|
|
202
|
+
contaminates: false,
|
|
203
|
+
needsAccessibleName: false,
|
|
204
|
+
local: false,
|
|
205
|
+
timeOut: 5,
|
|
206
|
+
defaultOn: true
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'headingAmb',
|
|
210
|
+
what: 'same-level sibling headings with identical texts',
|
|
211
|
+
contaminates: false,
|
|
212
|
+
needsAccessibleName: false,
|
|
213
|
+
local: false,
|
|
214
|
+
timeOut: 5,
|
|
215
|
+
defaultOn: true
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: 'hr',
|
|
219
|
+
what: 'hr element instead of styles used for vertical segmentation',
|
|
220
|
+
contaminates: false,
|
|
221
|
+
needsAccessibleName: false,
|
|
222
|
+
local: true,
|
|
223
|
+
timeOut: 5,
|
|
224
|
+
defaultOn: true
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'imageLink',
|
|
228
|
+
what: 'links with image files as their destinations',
|
|
229
|
+
contaminates: false,
|
|
230
|
+
needsAccessibleName: false,
|
|
231
|
+
local: true,
|
|
232
|
+
timeOut: 5,
|
|
233
|
+
defaultOn: true
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: 'labClash',
|
|
237
|
+
what: 'labeling inconsistencies',
|
|
238
|
+
contaminates: false,
|
|
239
|
+
needsAccessibleName: false,
|
|
240
|
+
local: false,
|
|
241
|
+
timeOut: 5,
|
|
242
|
+
defaultOn: true
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: 'legendLoc',
|
|
246
|
+
what: 'legend elements that are not first children of fieldset elements',
|
|
247
|
+
contaminates: false,
|
|
248
|
+
needsAccessibleName: false,
|
|
249
|
+
local: true,
|
|
250
|
+
timeOut: 5,
|
|
251
|
+
defaultOn: true
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: 'lineHeight',
|
|
255
|
+
what: 'text with a line height less than 1.5 times its font size',
|
|
256
|
+
contaminates: false,
|
|
257
|
+
needsAccessibleName: false,
|
|
258
|
+
local: true,
|
|
259
|
+
timeOut: 5,
|
|
260
|
+
defaultOn: true
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: 'linkAmb',
|
|
264
|
+
what: 'links with identical texts but different destinations',
|
|
265
|
+
contaminates: false,
|
|
266
|
+
needsAccessibleName: false,
|
|
267
|
+
local: false,
|
|
268
|
+
timeOut: 20,
|
|
269
|
+
defaultOn: true
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: 'linkExt',
|
|
273
|
+
what: 'links that automatically open new windows',
|
|
274
|
+
contaminates: false,
|
|
275
|
+
needsAccessibleName: false,
|
|
276
|
+
local: true,
|
|
277
|
+
timeOut: 5,
|
|
278
|
+
defaultOn: true
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: 'linkOldAtt',
|
|
282
|
+
what: 'links with deprecated attributes',
|
|
283
|
+
contaminates: false,
|
|
284
|
+
needsAccessibleName: false,
|
|
285
|
+
local: true,
|
|
286
|
+
timeOut: 5,
|
|
287
|
+
defaultOn: true
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: 'linkTo',
|
|
291
|
+
what: 'links without destinations',
|
|
292
|
+
contaminates: false,
|
|
293
|
+
needsAccessibleName: false,
|
|
294
|
+
local: true,
|
|
295
|
+
timeOut: 5,
|
|
296
|
+
defaultOn: true
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: 'linkUl',
|
|
300
|
+
what: 'missing underlines on inline links',
|
|
301
|
+
contaminates: false,
|
|
302
|
+
needsAccessibleName: false,
|
|
303
|
+
local: true,
|
|
304
|
+
timeOut: 5,
|
|
305
|
+
defaultOn: true
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 'miniText',
|
|
309
|
+
what: 'text smaller than 11 pixels',
|
|
310
|
+
contaminates: false,
|
|
311
|
+
needsAccessibleName: false,
|
|
312
|
+
local: true,
|
|
313
|
+
timeOut: 5,
|
|
314
|
+
defaultOn: true
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
id: 'nonTable',
|
|
318
|
+
what: 'table elements used for layout',
|
|
319
|
+
contaminates: false,
|
|
320
|
+
needsAccessibleName: false,
|
|
321
|
+
local: true,
|
|
322
|
+
timeOut: 5,
|
|
323
|
+
defaultOn: true
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: 'optRoleSel',
|
|
327
|
+
what: 'Non-option elements with option roles that have no aria-selected attributes',
|
|
328
|
+
contaminates: false,
|
|
329
|
+
needsAccessibleName: false,
|
|
330
|
+
local: true,
|
|
331
|
+
timeOut: 5,
|
|
332
|
+
defaultOn: true
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
id: 'pseudoP',
|
|
336
|
+
what: 'adjacent br elements suspected of nonsemantically simulating p elements',
|
|
337
|
+
contaminates: false,
|
|
338
|
+
needsAccessibleName: false,
|
|
339
|
+
local: true,
|
|
340
|
+
timeOut: 5,
|
|
341
|
+
defaultOn: true
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: 'radioSet',
|
|
345
|
+
what: 'radio buttons not grouped into standard field sets',
|
|
346
|
+
contaminates: false,
|
|
347
|
+
needsAccessibleName: false,
|
|
348
|
+
local: false,
|
|
349
|
+
timeOut: 5,
|
|
350
|
+
defaultOn: true
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: 'role',
|
|
354
|
+
what: 'native-replacing explicit roles',
|
|
355
|
+
contaminates: false,
|
|
356
|
+
needsAccessibleName: false,
|
|
357
|
+
local: false,
|
|
358
|
+
timeOut: 20,
|
|
359
|
+
defaultOn: true
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: 'secHeading',
|
|
363
|
+
what: 'headings that violate the logical level order in their sectioning containers',
|
|
364
|
+
contaminates: false,
|
|
365
|
+
needsAccessibleName: false,
|
|
366
|
+
local: false,
|
|
367
|
+
timeOut: 5,
|
|
368
|
+
defaultOn: true
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
id: 'styleDiff',
|
|
372
|
+
what: 'style inconsistencies',
|
|
373
|
+
contaminates: false,
|
|
374
|
+
needsAccessibleName: false,
|
|
375
|
+
local: false,
|
|
376
|
+
timeOut: 5,
|
|
377
|
+
defaultOn: true
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
id: 'targetsNear',
|
|
381
|
+
what: 'labels, buttons, inputs, and links too near each other',
|
|
382
|
+
contaminates: false,
|
|
383
|
+
needsAccessibleName: false,
|
|
384
|
+
local: false,
|
|
385
|
+
timeOut: 5,
|
|
386
|
+
defaultOn: true
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
id: 'textSem',
|
|
390
|
+
what: 'semantically vague elements i, b, and/or small',
|
|
391
|
+
contaminates: false,
|
|
392
|
+
needsAccessibleName: false,
|
|
393
|
+
local: true,
|
|
394
|
+
timeOut: 5,
|
|
395
|
+
defaultOn: true
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
id: 'title',
|
|
399
|
+
what: 'page title',
|
|
400
|
+
contaminates: false,
|
|
401
|
+
needsAccessibleName: false,
|
|
402
|
+
local: false,
|
|
403
|
+
timeOut: 5,
|
|
404
|
+
defaultOn: false
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
id: 'titledEl',
|
|
408
|
+
what: 'title attributes on inappropriate elements',
|
|
409
|
+
contaminates: false,
|
|
410
|
+
needsAccessibleName: false,
|
|
411
|
+
local: true,
|
|
412
|
+
timeOut: 5,
|
|
413
|
+
defaultOn: true
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: 'zIndex',
|
|
417
|
+
what: 'non-default Z indexes',
|
|
418
|
+
contaminates: false,
|
|
419
|
+
needsAccessibleName: false,
|
|
420
|
+
local: true,
|
|
421
|
+
timeOut: 5,
|
|
422
|
+
defaultOn: true
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
id: 'motion',
|
|
426
|
+
what: 'motion without user request',
|
|
427
|
+
contaminates: false,
|
|
428
|
+
needsAccessibleName: false,
|
|
429
|
+
local: false,
|
|
430
|
+
// The budget must cover a full-page screenshot (itself allowed 4 seconds in
|
|
431
|
+
// procs/shoot.js), decoding two full-page PNGs, and a pixel comparison; 5
|
|
432
|
+
// seconds made the rule time out whenever an initial image existed.
|
|
433
|
+
timeOut: 30,
|
|
434
|
+
defaultOn: true
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
id: 'dupAtt',
|
|
438
|
+
what: 'duplicate attribute values',
|
|
439
|
+
contaminates: false,
|
|
440
|
+
needsAccessibleName: false,
|
|
441
|
+
local: false,
|
|
442
|
+
timeOut: 5,
|
|
443
|
+
defaultOn: true
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: 'autocomplete',
|
|
447
|
+
what: 'name and email inputs without autocomplete attributes',
|
|
448
|
+
contaminates: false,
|
|
449
|
+
needsAccessibleName: true,
|
|
450
|
+
local: true,
|
|
451
|
+
timeOut: 5,
|
|
452
|
+
defaultOn: true
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
id: 'phOnly',
|
|
456
|
+
what: 'input elements with placeholders but no accessible names',
|
|
457
|
+
contaminates: false,
|
|
458
|
+
needsAccessibleName: true,
|
|
459
|
+
local: true,
|
|
460
|
+
timeOut: 5,
|
|
461
|
+
defaultOn: true
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
id: 'buttonMenu',
|
|
465
|
+
what: 'nonstandard keyboard navigation between items of button-controlled menus',
|
|
466
|
+
contaminates: true,
|
|
467
|
+
needsAccessibleName: false,
|
|
468
|
+
local: false,
|
|
469
|
+
timeOut: 15,
|
|
470
|
+
defaultOn: true
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
id: 'elements',
|
|
474
|
+
what: 'data on specified elements',
|
|
475
|
+
contaminates: true,
|
|
476
|
+
needsAccessibleName: false,
|
|
477
|
+
local: false,
|
|
478
|
+
timeOut: 10,
|
|
479
|
+
defaultOn: false
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: 'focAll',
|
|
483
|
+
what: 'discrepancies between focusable and Tab-focused elements',
|
|
484
|
+
contaminates: true,
|
|
485
|
+
needsAccessibleName: false,
|
|
486
|
+
local: false,
|
|
487
|
+
timeOut: 10,
|
|
488
|
+
defaultOn: true
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
id: 'focAndOp',
|
|
492
|
+
what: 'Tab-focusable elements that are not operable or vice versa',
|
|
493
|
+
contaminates: true,
|
|
494
|
+
needsAccessibleName: false,
|
|
495
|
+
local: false,
|
|
496
|
+
timeOut: 5,
|
|
497
|
+
defaultOn: true
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: 'focInd',
|
|
501
|
+
what: 'missing and nonstandard focus indicators',
|
|
502
|
+
contaminates: true,
|
|
503
|
+
needsAccessibleName: false,
|
|
504
|
+
local: false,
|
|
505
|
+
timeOut: 10,
|
|
506
|
+
defaultOn: true
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
id: 'focVis',
|
|
510
|
+
what: 'links that are not entirely visible when focused',
|
|
511
|
+
contaminates: true,
|
|
512
|
+
needsAccessibleName: false,
|
|
513
|
+
local: false,
|
|
514
|
+
timeOut: 10,
|
|
515
|
+
defaultOn: true
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
id: 'hover',
|
|
519
|
+
what: 'hover-caused content changes',
|
|
520
|
+
contaminates: true,
|
|
521
|
+
needsAccessibleName: false,
|
|
522
|
+
local: false,
|
|
523
|
+
timeOut: 20,
|
|
524
|
+
defaultOn: true
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
id: 'hovInd',
|
|
528
|
+
what: 'hover indication nonstandard',
|
|
529
|
+
contaminates: true,
|
|
530
|
+
needsAccessibleName: false,
|
|
531
|
+
local: false,
|
|
532
|
+
timeOut: 10,
|
|
533
|
+
defaultOn: true
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
id: 'tabNav',
|
|
537
|
+
what: 'nonstandard keyboard navigation between elements with the tab role',
|
|
538
|
+
contaminates: true,
|
|
539
|
+
needsAccessibleName: false,
|
|
540
|
+
local: false,
|
|
541
|
+
timeOut: 10,
|
|
542
|
+
defaultOn: true
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
id: 'textNodes',
|
|
546
|
+
what: 'data on specified text nodes',
|
|
547
|
+
contaminates: true,
|
|
548
|
+
needsAccessibleName: false,
|
|
549
|
+
local: false,
|
|
550
|
+
timeOut: 10,
|
|
551
|
+
defaultOn: false
|
|
552
|
+
}
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
// ERROR HANDLER
|
|
556
|
+
process.on('unhandledRejection', reason => {
|
|
557
|
+
console.error(`ERROR: Unhandled Promise Rejection (${reason})`);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// FUNCTIONS
|
|
561
|
+
|
|
562
|
+
// Conducts and reports Testaro tests.
|
|
563
|
+
export const reporter = async (page: Page | undefined, report: Report, actIndex: number) => {
|
|
564
|
+
const act = report.acts[actIndex] as TestaroAct;
|
|
565
|
+
const givenPage = page;
|
|
566
|
+
const {args, stopOnFail, withItems} = act;
|
|
567
|
+
// A testaro act always has a target on itself or the report; verbatim from the original.
|
|
568
|
+
const target = (act.target || report.target)!;
|
|
569
|
+
const url = target.url;
|
|
570
|
+
const browserID = act.launch ? act.launch.browserID || report.browserID : report.browserID;
|
|
571
|
+
const argRules = args ? Object.keys(args) : null;
|
|
572
|
+
// Get the specification of rules to be tested for or, by default, all rules with defaultOn true.
|
|
573
|
+
const ruleSpec = act.rules
|
|
574
|
+
|| ['y', ... allRules.filter(rule => rule.defaultOn).map(rule => rule.id)];
|
|
575
|
+
// Initialize the act data.
|
|
576
|
+
const data: {
|
|
577
|
+
prevented: boolean;
|
|
578
|
+
error: string;
|
|
579
|
+
rulePreventions: Record<string, string>;
|
|
580
|
+
rulesInvalid: string[];
|
|
581
|
+
ruleTestTimes: [string, number][];
|
|
582
|
+
ruleData: Record<string, unknown>;
|
|
583
|
+
scope?: {localRules: string[]; pageRules: string[]};
|
|
584
|
+
} = {
|
|
585
|
+
prevented: false,
|
|
586
|
+
error: '',
|
|
587
|
+
rulePreventions: {},
|
|
588
|
+
rulesInvalid: [],
|
|
589
|
+
ruleTestTimes: [],
|
|
590
|
+
ruleData: {}
|
|
591
|
+
};
|
|
592
|
+
// The changed subtree roots the act is scoped to, if any (report.scope, set by the acts loop).
|
|
593
|
+
const scopeRoots = report.scope?.roots ?? null;
|
|
594
|
+
if (scopeRoots) {
|
|
595
|
+
data.scope = {localRules: [], pageRules: []};
|
|
596
|
+
}
|
|
597
|
+
// Initialize the act result.
|
|
598
|
+
const result: {
|
|
599
|
+
nativeResult: Record<string, unknown>;
|
|
600
|
+
standardResult: {
|
|
601
|
+
prevented: boolean;
|
|
602
|
+
totals: number[];
|
|
603
|
+
outcomeTotals: Record<Outcome, number>;
|
|
604
|
+
instances: StandardInstance[];
|
|
605
|
+
};
|
|
606
|
+
} = {
|
|
607
|
+
nativeResult: {},
|
|
608
|
+
standardResult: getStandardResult() as {
|
|
609
|
+
prevented: boolean;
|
|
610
|
+
totals: number[];
|
|
611
|
+
outcomeTotals: Record<Outcome, number>;
|
|
612
|
+
instances: StandardInstance[];
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
const {standardResult} = result;
|
|
616
|
+
const allRuleIDs = allRules.map(rule => rule.id);
|
|
617
|
+
if (
|
|
618
|
+
// If the rule specification has at least 2 items
|
|
619
|
+
ruleSpec.length > 1
|
|
620
|
+
// and the first item is y or n
|
|
621
|
+
&& ['y', 'n'].includes(ruleSpec[0])
|
|
622
|
+
// and all subsequent items are rule IDs to be included (if y) or excluded (if n):
|
|
623
|
+
&& ruleSpec.slice(1).every(ruleID => (allRuleIDs as string[]).includes(ruleID))
|
|
624
|
+
) {
|
|
625
|
+
// Get the rules to be tested for and their execution order.
|
|
626
|
+
const excludeIDs = ruleSpec.slice(1);
|
|
627
|
+
const jobRuleIDs = ruleSpec[0] === 'y'
|
|
628
|
+
? excludeIDs
|
|
629
|
+
: allRules
|
|
630
|
+
.filter(rule => rule.defaultOn && ! excludeIDs.includes(rule.id))
|
|
631
|
+
.map(rule => rule.id);
|
|
632
|
+
const jobRules = allRules.filter(rule => jobRuleIDs.includes(rule.id));
|
|
633
|
+
let justPrevented = false;
|
|
634
|
+
// For each rule to be tested for:
|
|
635
|
+
for (let ruleIndex = 0; ruleIndex < jobRules.length; ruleIndex++) {
|
|
636
|
+
const rule = jobRules[ruleIndex];
|
|
637
|
+
// Initialize the rule result.
|
|
638
|
+
const ruleResult: RuleTestResult = {
|
|
639
|
+
id: rule.id,
|
|
640
|
+
prevented: false,
|
|
641
|
+
error: '',
|
|
642
|
+
data: {},
|
|
643
|
+
totals: [0, 0, 0, 0],
|
|
644
|
+
instances: [],
|
|
645
|
+
elapsedTime: 0
|
|
646
|
+
};
|
|
647
|
+
console.log(`Starting rule ${ruleResult.id}`);
|
|
648
|
+
// Make the browser emulate headedness in all cases, because performance does not suffer.
|
|
649
|
+
const headEmulation = ruleResult.id.startsWith('shoot') ? 'high' : 'high';
|
|
650
|
+
// Get whether the rule needs a new browser launched. Under page isolation the first rules
|
|
651
|
+
// run on the live page provided; a contaminating rule still gets a fresh page.
|
|
652
|
+
const previousRule = ruleIndex > 0 ? jobRules[ruleIndex - 1] : null;
|
|
653
|
+
const needsLaunch = (ruleIndex === 0 && ! (page && report.jobData?.isolation === 'page'))
|
|
654
|
+
|| justPrevented
|
|
655
|
+
|| Boolean(previousRule?.contaminates)
|
|
656
|
+
|| jobRules[ruleIndex].needsAccessibleName && ! previousRule?.needsAccessibleName
|
|
657
|
+
&& ! (ruleIndex === 0 && page);
|
|
658
|
+
const pageClosed = page && page.isClosed();
|
|
659
|
+
// If it does, or if the page has closed:
|
|
660
|
+
if (needsLaunch || pageClosed) {
|
|
661
|
+
// If the page has closed when it is expected to be open:
|
|
662
|
+
if (pageClosed && ! needsLaunch) {
|
|
663
|
+
// Report this.
|
|
664
|
+
console.log(`WARNING: Relaunching browser for test ${rule} after abnormal closure`);
|
|
665
|
+
}
|
|
666
|
+
// Create a browser, replace the page, and visit the target, retrying twice if necessary.
|
|
667
|
+
page = await launch({
|
|
668
|
+
report,
|
|
669
|
+
actIndex,
|
|
670
|
+
tempBrowserID: browserID,
|
|
671
|
+
tempURL: url,
|
|
672
|
+
headEmulation,
|
|
673
|
+
xPathNeed: 'script',
|
|
674
|
+
needsAccessibleName: jobRules[ruleIndex].needsAccessibleName,
|
|
675
|
+
retries: Number.isInteger(act.retries) && (act.retries as number) >= 0
|
|
676
|
+
? act.retries as number
|
|
677
|
+
: ruleLaunchRetries
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
// If no page exists, the launch (or the replay of a checkpoint's acts) failed: the
|
|
681
|
+
// target is unreachable, so prevent the act and stop testing rules.
|
|
682
|
+
if (! page) {
|
|
683
|
+
const message = String(
|
|
684
|
+
report.jobData?.abortMessage
|
|
685
|
+
|| `Launch or checkpoint replay failed before rule ${ruleResult.id}`
|
|
686
|
+
);
|
|
687
|
+
ruleResult.prevented = true;
|
|
688
|
+
ruleResult.error = message;
|
|
689
|
+
data.rulePreventions[ruleResult.id] = message;
|
|
690
|
+
data.prevented = true;
|
|
691
|
+
data.error = message;
|
|
692
|
+
standardResult.prevented = true;
|
|
693
|
+
console.log(`ERROR: ${message}`);
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
// Report crashes and disconnections during this test.
|
|
697
|
+
let crashHandler: (() => void) | null | undefined;
|
|
698
|
+
let disconnectHandler: (() => void) | null | undefined;
|
|
699
|
+
if (page && ! page.isClosed()) {
|
|
700
|
+
crashHandler = () => {
|
|
701
|
+
console.log(`ERROR: Page crashed during ${rule} test`);
|
|
702
|
+
};
|
|
703
|
+
page.on('crash', crashHandler);
|
|
704
|
+
}
|
|
705
|
+
// The page exists whenever this runs; verbatim from the original.
|
|
706
|
+
const browser = page!.context().browser();
|
|
707
|
+
if (browser) {
|
|
708
|
+
disconnectHandler = () => {
|
|
709
|
+
console.log(`ERROR: Browser disconnected during ${rule} test`);
|
|
710
|
+
};
|
|
711
|
+
browser.on('disconnected', disconnectHandler);
|
|
712
|
+
}
|
|
713
|
+
// Restrict an element-local rule to the scope roots; a page-level rule tests the page.
|
|
714
|
+
if (scopeRoots) {
|
|
715
|
+
report.ruleScopeRoots = rule.local ? scopeRoots : null;
|
|
716
|
+
(rule.local ? data.scope!.localRules : data.scope!.pageRules).push(rule.id);
|
|
717
|
+
}
|
|
718
|
+
// Initialize an argument array for the reporter.
|
|
719
|
+
const ruleArgs: unknown[] = [page, report, actIndex, withItems];
|
|
720
|
+
// If the rule needs a temporary directory:
|
|
721
|
+
if (rule.needsTmpDir) {
|
|
722
|
+
// Add its path to the argument array.
|
|
723
|
+
ruleArgs.push(report.jobData!.tmpDir);
|
|
724
|
+
}
|
|
725
|
+
// If the testaro test act specifies extra arguments for this rule:
|
|
726
|
+
if (argRules?.includes(ruleResult.id)) {
|
|
727
|
+
// Add them to the argument array.
|
|
728
|
+
ruleArgs.push(... args![ruleResult.id]);
|
|
729
|
+
}
|
|
730
|
+
const startTime = Date.now();
|
|
731
|
+
let timer: Promise<{timedOut: true}>;
|
|
732
|
+
try {
|
|
733
|
+
// Apply a time limit to the test.
|
|
734
|
+
const timeLimit = applyMultiplier(1000 * rule.timeOut);
|
|
735
|
+
let timeout: NodeJS.Timeout;
|
|
736
|
+
// If the time limit expires during the test:
|
|
737
|
+
timer = new Promise(resolve => {
|
|
738
|
+
timeout = setTimeout(() => {
|
|
739
|
+
// Add data about the timeout to the rule result.
|
|
740
|
+
justPrevented = true;
|
|
741
|
+
ruleResult.prevented = true;
|
|
742
|
+
ruleResult.error = 'Timeout';
|
|
743
|
+
console.log(`ERROR: Test of testaro rule ${ruleResult.id} timed out`);
|
|
744
|
+
resolve({timedOut: true});
|
|
745
|
+
}, timeLimit);
|
|
746
|
+
});
|
|
747
|
+
// Try to perform the test and get a test report, loading the rule via the registry.
|
|
748
|
+
// The RuleModule cast widens the per-rule signature union to the registry contract.
|
|
749
|
+
const testReport = (ruleModules[ruleResult.id]() as RuleModule).reporter(... ruleArgs);
|
|
750
|
+
// Get a test or timeout report.
|
|
751
|
+
const ruleReport: {
|
|
752
|
+
timedOut?: boolean;
|
|
753
|
+
data?: unknown;
|
|
754
|
+
totals?: number[];
|
|
755
|
+
standardInstances?: unknown[];
|
|
756
|
+
} = await Promise.race([timer, testReport]);
|
|
757
|
+
clearTimeout(timeout!);
|
|
758
|
+
// If it was a test report:
|
|
759
|
+
if (! ruleReport.timedOut) {
|
|
760
|
+
// Add the rule-report properties to the rule result.
|
|
761
|
+
ruleResult.data = ruleReport.data;
|
|
762
|
+
ruleResult.totals = ruleReport.totals as number[];
|
|
763
|
+
ruleResult.instances = ruleReport.standardInstances as StandardInstance[];
|
|
764
|
+
// Add the rule-result properties to the result.
|
|
765
|
+
if (Object.keys(ruleReport.data as Record<string, unknown>).length) {
|
|
766
|
+
data.ruleData[ruleResult.id] = ruleResult.data;
|
|
767
|
+
}
|
|
768
|
+
if (ruleResult.totals) {
|
|
769
|
+
ruleResult.totals.forEach((total, index) => {
|
|
770
|
+
standardResult.totals[index] += Math.round(total);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
if (ruleResult.instances?.length) {
|
|
774
|
+
// Apply the rule's default outcome to instances without their own.
|
|
775
|
+
ruleResult.instances.forEach(instance => {
|
|
776
|
+
instance.outcome ??= rule.outcome ?? 'failed';
|
|
777
|
+
if (instance.outcome === 'cantTell' && rule.uncertainty) {
|
|
778
|
+
instance.uncertainty ??= rule.uncertainty;
|
|
779
|
+
}
|
|
780
|
+
standardResult.outcomeTotals[instance.outcome] += instance.count || 1;
|
|
781
|
+
});
|
|
782
|
+
standardResult.instances.push(... ruleResult.instances);
|
|
783
|
+
}
|
|
784
|
+
justPrevented = false;
|
|
785
|
+
// If testing is to stop after a failure and the page failed the test:
|
|
786
|
+
if (stopOnFail && ruleReport.totals?.some(total => total)) {
|
|
787
|
+
// Test for no more rules.
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// If an error is thrown by the test:
|
|
793
|
+
catch(error) {
|
|
794
|
+
ruleResult.prevented = true;
|
|
795
|
+
justPrevented = true;
|
|
796
|
+
const isPageClosed = ['closed', 'Protocol error', 'Target page'].some(phrase =>
|
|
797
|
+
(error as Error).message.includes(phrase)
|
|
798
|
+
);
|
|
799
|
+
// If the page has closed:
|
|
800
|
+
if (isPageClosed) {
|
|
801
|
+
// Report this.
|
|
802
|
+
console.log(`ERROR: Test ${ruleResult.id} failed because page closed`);
|
|
803
|
+
}
|
|
804
|
+
// Otherwise, i.e. if the page is open:
|
|
805
|
+
else {
|
|
806
|
+
// Add this to the rule result.
|
|
807
|
+
ruleResult.error = (error as Error).message;
|
|
808
|
+
console.log(
|
|
809
|
+
`ERROR: Test of testaro rule ${ruleResult.id} prevented (${(error as Error).message})`
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
finally {
|
|
814
|
+
// Add the elapsed time to the rule result.
|
|
815
|
+
ruleResult.elapsedTime = Math.round((Date.now() - startTime) / 1000);
|
|
816
|
+
// Add the elapsed time to the data.
|
|
817
|
+
data.ruleTestTimes.push([ruleResult.id, ruleResult.elapsedTime]);
|
|
818
|
+
// If the test timed out or otherwise failed:
|
|
819
|
+
if (ruleResult.prevented) {
|
|
820
|
+
// Add this and the error to the data.
|
|
821
|
+
data.rulePreventions[ruleResult.id] = ruleResult.error;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
// Sort the rule test times.
|
|
825
|
+
data.ruleTestTimes.sort((a, b) => b[1] - a[1]);
|
|
826
|
+
// Clear the error listeners.
|
|
827
|
+
if (page && ! page.isClosed() && crashHandler) {
|
|
828
|
+
page.off('crash', crashHandler);
|
|
829
|
+
crashHandler = null;
|
|
830
|
+
}
|
|
831
|
+
if (browser && disconnectHandler) {
|
|
832
|
+
browser.off('disconnected', disconnectHandler);
|
|
833
|
+
disconnectHandler = null;
|
|
834
|
+
}
|
|
835
|
+
// Force a garbage collection.
|
|
836
|
+
try {
|
|
837
|
+
if (global.gc) {
|
|
838
|
+
global.gc();
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
catch(error) {}
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
// Otherwise, i.e. if the rule specification is invalid:
|
|
845
|
+
else {
|
|
846
|
+
// Report this and stop testing.
|
|
847
|
+
standardResult.prevented = true;
|
|
848
|
+
data.prevented = true;
|
|
849
|
+
const message = 'ERROR: Testaro rule specification invalid';
|
|
850
|
+
data.error = message;
|
|
851
|
+
console.log(message);
|
|
852
|
+
}
|
|
853
|
+
// Close the last page the tool launched, unless it is the live page it was given.
|
|
854
|
+
if (page && page !== givenPage) {
|
|
855
|
+
await browserClose(page);
|
|
856
|
+
}
|
|
857
|
+
delete report.ruleScopeRoots;
|
|
858
|
+
return {
|
|
859
|
+
data,
|
|
860
|
+
result
|
|
861
|
+
};
|
|
862
|
+
};
|