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/procs/testaro.ts
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/*
|
|
2
|
+
© 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
|
+
© 2026 Jeff Witt.
|
|
4
|
+
© 2025–2026 Jonathan Robert Pool.
|
|
5
|
+
|
|
6
|
+
Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
|
|
7
|
+
|
|
8
|
+
SPDX-License-Identifier: MIT
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ########## IMPORTS
|
|
12
|
+
|
|
13
|
+
// Function to get a catalog index from an XPath.
|
|
14
|
+
import {getXPathCatalogIndex} from './xPath';
|
|
15
|
+
// Function to build a standard instance.
|
|
16
|
+
import {getInstance, type InstanceSpec} from './standard';
|
|
17
|
+
import type {Locator, Page} from 'playwright';
|
|
18
|
+
import type {GetBadWhat, Outcome, Report, SeverityTotals, StandardInstance, UncertaintyCode} from '../types';
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
testaro
|
|
22
|
+
Utilities for Testaro tests. Compiled to testaro.js by tsc (issue #73); edit
|
|
23
|
+
this file, not the emitted one.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// ########## TYPES
|
|
27
|
+
|
|
28
|
+
// A violation found in the page before it becomes a standard instance.
|
|
29
|
+
interface ProtoInstance {
|
|
30
|
+
what: string;
|
|
31
|
+
ordinalSeverity: number;
|
|
32
|
+
outcome?: Outcome;
|
|
33
|
+
xPath: string;
|
|
34
|
+
}
|
|
35
|
+
// What doTest and getBasicResult return to the calling rule module.
|
|
36
|
+
export interface RuleResult {
|
|
37
|
+
data: Record<string, unknown>;
|
|
38
|
+
totals: SeverityTotals;
|
|
39
|
+
standardInstances: StandardInstance[];
|
|
40
|
+
}
|
|
41
|
+
// A violation reported by a doTest-ineligible rule module.
|
|
42
|
+
interface BasicViolation {
|
|
43
|
+
loc: Locator;
|
|
44
|
+
what: string;
|
|
45
|
+
// A violation may specify its own certainty.
|
|
46
|
+
outcome?: Outcome;
|
|
47
|
+
uncertainty?: UncertaintyCode;
|
|
48
|
+
needed?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ########## FUNCTIONS
|
|
52
|
+
|
|
53
|
+
// Returns a standard instance, leaving the outcome unset if the violation did not specify one, so
|
|
54
|
+
// that the testaro tool can apply the rule's default outcome.
|
|
55
|
+
const getRuleInstance = (spec: InstanceSpec): StandardInstance => {
|
|
56
|
+
const instance = getInstance(spec);
|
|
57
|
+
if (! spec.outcome) {
|
|
58
|
+
instance.outcome = undefined;
|
|
59
|
+
}
|
|
60
|
+
return instance;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Tests for a testaro rule.
|
|
64
|
+
export const doTest = async (
|
|
65
|
+
page: Page,
|
|
66
|
+
report: Report,
|
|
67
|
+
withItems: boolean,
|
|
68
|
+
ruleID: string,
|
|
69
|
+
candidateSelector: string,
|
|
70
|
+
whats: string,
|
|
71
|
+
severity: number,
|
|
72
|
+
getBadWhatString: string
|
|
73
|
+
): Promise<RuleResult> => {
|
|
74
|
+
// The subtree roots the rule is restricted to, if the test act has scope changed and the
|
|
75
|
+
// testaro tool judged the rule element-local (set per rule by tests/testaro.ts).
|
|
76
|
+
const scopeRoots = report.ruleScopeRoots ?? null;
|
|
77
|
+
const ruleData = await page.evaluate(async args => {
|
|
78
|
+
// Get the arguments.
|
|
79
|
+
const [
|
|
80
|
+
withItems,
|
|
81
|
+
candidateSelector,
|
|
82
|
+
severity,
|
|
83
|
+
getBadWhatString,
|
|
84
|
+
scopeRoots
|
|
85
|
+
] = args;
|
|
86
|
+
// Get all violator candidates, within the scope roots if any.
|
|
87
|
+
let candidates = Array.from(document.querySelectorAll(candidateSelector));
|
|
88
|
+
if (scopeRoots && scopeRoots.length) {
|
|
89
|
+
const roots = scopeRoots
|
|
90
|
+
.map(selector => {
|
|
91
|
+
try {
|
|
92
|
+
return document.querySelector(selector);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
.filter((root): root is Element => Boolean(root));
|
|
99
|
+
candidates = candidates.filter(candidate => roots.some(root => root.contains(candidate)));
|
|
100
|
+
}
|
|
101
|
+
let violationCount = 0;
|
|
102
|
+
// Initialize proto-instances.
|
|
103
|
+
const protoInstances: ProtoInstance[] = [];
|
|
104
|
+
// Initialize counts of cantTell violations by ordinal severity, for summary instances.
|
|
105
|
+
const cantTellTotals = [0, 0, 0, 0];
|
|
106
|
+
/*
|
|
107
|
+
Parse the supplied string to get the classifier. The predicate travels as
|
|
108
|
+
source text (serialized with toString() by the rule module) because it
|
|
109
|
+
must run inside the page; this cast is the single point where its type is
|
|
110
|
+
asserted rather than checked (issue #73, RFC decision 4). The eval input
|
|
111
|
+
is first-party rule-module source from this repository, never target-page
|
|
112
|
+
or user data; replacing the eval pipeline with a bundled artifact is the
|
|
113
|
+
Phase 3 reassessment recorded on issue #73.
|
|
114
|
+
*/
|
|
115
|
+
const getBadWhat = eval(`(${getBadWhatString})`) as GetBadWhat;
|
|
116
|
+
// Initialize data on the rule.
|
|
117
|
+
let data: Record<string, unknown> = {};
|
|
118
|
+
const totals = [0, 0, 0, 0];
|
|
119
|
+
// For each candidate:
|
|
120
|
+
for (const candidate of candidates) {
|
|
121
|
+
// Classify it as and get a violation description if a violator or undefined if not.
|
|
122
|
+
const violationWhat = await getBadWhat(candidate);
|
|
123
|
+
// If the candidate violates the rule:
|
|
124
|
+
if (violationWhat) {
|
|
125
|
+
// Increment the violation count.
|
|
126
|
+
violationCount++;
|
|
127
|
+
let ruleWhat;
|
|
128
|
+
const violationType = typeof violationWhat;
|
|
129
|
+
// If data on the violation were provided (unusual):
|
|
130
|
+
if (violationType === 'object') {
|
|
131
|
+
// Get the description and add the data to the rule data.
|
|
132
|
+
ruleWhat = (violationWhat as {description: string; data?: unknown}).description;
|
|
133
|
+
data[violationCount - 1] = (violationWhat as {description: string; data?: unknown}).data;
|
|
134
|
+
}
|
|
135
|
+
// Otherwise, i.e. if only a description of the violation was provided:
|
|
136
|
+
else if (violationType === 'string') {
|
|
137
|
+
// Get it.
|
|
138
|
+
ruleWhat = violationWhat as string;
|
|
139
|
+
}
|
|
140
|
+
let ordinalSeverity = severity;
|
|
141
|
+
let outcome: 'cantTell' | undefined;
|
|
142
|
+
// A predicate that returns a truthy non-object non-string would make this throw,
|
|
143
|
+
// exactly as in the JavaScript original.
|
|
144
|
+
// If this violation has a custom severity or outcome prefix (e.g. 2:, 2?:, or ?:):
|
|
145
|
+
const prefixMatch = (ruleWhat as string).match(/^([0-3])?(\?)?:/);
|
|
146
|
+
if (prefixMatch) {
|
|
147
|
+
// If the prefix has a severity, get it.
|
|
148
|
+
if (prefixMatch[1]) {
|
|
149
|
+
ordinalSeverity = Number(prefixMatch[1]);
|
|
150
|
+
}
|
|
151
|
+
// If the prefix marks the violation as uncertain, record this.
|
|
152
|
+
if (prefixMatch[2]) {
|
|
153
|
+
outcome = 'cantTell';
|
|
154
|
+
cantTellTotals[ordinalSeverity]++;
|
|
155
|
+
}
|
|
156
|
+
// Remove the prefix from the violation description.
|
|
157
|
+
ruleWhat = (ruleWhat as string).slice(prefixMatch[0].length);
|
|
158
|
+
}
|
|
159
|
+
// Increment the applicable rule-violation total.
|
|
160
|
+
totals[ordinalSeverity]++;
|
|
161
|
+
// If itemization is required:
|
|
162
|
+
if (withItems) {
|
|
163
|
+
const protoInstance: ProtoInstance = {
|
|
164
|
+
what: ruleWhat as string,
|
|
165
|
+
ordinalSeverity,
|
|
166
|
+
outcome,
|
|
167
|
+
xPath: window.getXPath(candidate as Element) ?? '/html'
|
|
168
|
+
};
|
|
169
|
+
// Add a proto-instance to the proto-instances.
|
|
170
|
+
protoInstances.push(protoInstance);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
data,
|
|
176
|
+
totals,
|
|
177
|
+
cantTellTotals,
|
|
178
|
+
protoInstances
|
|
179
|
+
}
|
|
180
|
+
}, [
|
|
181
|
+
withItems,
|
|
182
|
+
candidateSelector,
|
|
183
|
+
severity,
|
|
184
|
+
getBadWhatString,
|
|
185
|
+
scopeRoots
|
|
186
|
+
] as const
|
|
187
|
+
);
|
|
188
|
+
// Initialize the standard instances. Any instance without an outcome gets the rule's default
|
|
189
|
+
// outcome from the testaro tool.
|
|
190
|
+
let standardInstances: StandardInstance[] = [];
|
|
191
|
+
const {data, totals, cantTellTotals, protoInstances} = ruleData;
|
|
192
|
+
// If itemization is required:
|
|
193
|
+
if (withItems) {
|
|
194
|
+
// For each proto-instance:
|
|
195
|
+
protoInstances.forEach(protoInstance => {
|
|
196
|
+
const {what, ordinalSeverity, outcome, xPath} = protoInstance;
|
|
197
|
+
// Add a standard instance to the standard instances.
|
|
198
|
+
standardInstances.push(getRuleInstance({
|
|
199
|
+
ruleID,
|
|
200
|
+
what,
|
|
201
|
+
ordinalSeverity,
|
|
202
|
+
outcome,
|
|
203
|
+
catalogIndex: xPath ? getXPathCatalogIndex(report, xPath) : undefined
|
|
204
|
+
}));
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// Otherwise, i.e. if itemization is not required:
|
|
208
|
+
else {
|
|
209
|
+
// For each ordinal severity:
|
|
210
|
+
totals.forEach((total, ordinalSeverity) => {
|
|
211
|
+
const cantTellTotal = cantTellTotals[ordinalSeverity];
|
|
212
|
+
// If there were any asserted violations at that severity:
|
|
213
|
+
if (total - cantTellTotal) {
|
|
214
|
+
// Add a summary standard instance to the standard instances.
|
|
215
|
+
standardInstances.push(getRuleInstance({
|
|
216
|
+
ruleID,
|
|
217
|
+
what: whats,
|
|
218
|
+
ordinalSeverity,
|
|
219
|
+
count: total - cantTellTotal
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
// If there were any uncertain violations at that severity:
|
|
223
|
+
if (cantTellTotal) {
|
|
224
|
+
// Add a summary standard instance for them.
|
|
225
|
+
standardInstances.push(getRuleInstance({
|
|
226
|
+
ruleID,
|
|
227
|
+
what: whats,
|
|
228
|
+
ordinalSeverity,
|
|
229
|
+
outcome: 'cantTell',
|
|
230
|
+
count: cantTellTotal
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// Return the data, totals, and standard instances.
|
|
236
|
+
return {
|
|
237
|
+
data,
|
|
238
|
+
totals: totals as SeverityTotals,
|
|
239
|
+
standardInstances
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
// Adds a catalog index or, if necessary, an XPath to a proto-instance.
|
|
243
|
+
const addCatalogIndex = async (
|
|
244
|
+
protoInstance: StandardInstance, locator: Locator, report: Report
|
|
245
|
+
): Promise<StandardInstance> => {
|
|
246
|
+
// Get the XPath of the element referenced by the locator.
|
|
247
|
+
const xPath = await locator.evaluate(element => window.getXPath(element) ?? '/html');
|
|
248
|
+
// Add a catalog index to the proto-instance.
|
|
249
|
+
protoInstance.catalogIndex = getXPathCatalogIndex(report, xPath);
|
|
250
|
+
// Return the proto-instance with any modification.
|
|
251
|
+
return protoInstance;
|
|
252
|
+
};
|
|
253
|
+
// Tests for a doTest-ineligible Testaro rule.
|
|
254
|
+
export const getBasicResult = async (
|
|
255
|
+
report: Report,
|
|
256
|
+
withItems: boolean,
|
|
257
|
+
ruleID: string,
|
|
258
|
+
ordinalSeverity: StandardInstance['ordinalSeverity'],
|
|
259
|
+
whats: string,
|
|
260
|
+
data: {prevented?: boolean; [key: string]: unknown},
|
|
261
|
+
violations: BasicViolation[]
|
|
262
|
+
): Promise<RuleResult> => {
|
|
263
|
+
// If the test was prevented:
|
|
264
|
+
if (data.prevented) {
|
|
265
|
+
// Return this.
|
|
266
|
+
return {
|
|
267
|
+
data,
|
|
268
|
+
totals: [0, 0, 0, 0],
|
|
269
|
+
standardInstances: []
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
// Otherwise, i.e. if the test was not prevented:
|
|
273
|
+
const totals: SeverityTotals = [0, 0, 0, 0];
|
|
274
|
+
totals[ordinalSeverity] = violations.length;
|
|
275
|
+
const standardInstances: StandardInstance[] = [];
|
|
276
|
+
// If itemization is required:
|
|
277
|
+
if (withItems) {
|
|
278
|
+
// For each violation:
|
|
279
|
+
for (const violation of violations) {
|
|
280
|
+
const {loc, what, outcome, uncertainty, needed} = violation;
|
|
281
|
+
// Initialize a standard instance.
|
|
282
|
+
const protoInstance = getRuleInstance({
|
|
283
|
+
ruleID,
|
|
284
|
+
what,
|
|
285
|
+
ordinalSeverity,
|
|
286
|
+
outcome,
|
|
287
|
+
uncertainty,
|
|
288
|
+
needed
|
|
289
|
+
});
|
|
290
|
+
// Add a catalog index to it, awaited so the index is present before the
|
|
291
|
+
// report can be serialized (issue #100).
|
|
292
|
+
await addCatalogIndex(protoInstance, loc, report);
|
|
293
|
+
// Add the standard instance to the standard instances.
|
|
294
|
+
standardInstances.push(protoInstance);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Otherwise, i.e. if itemization is not required:
|
|
298
|
+
else {
|
|
299
|
+
// Add a summary instance per outcome to the instances.
|
|
300
|
+
const outcomeCounts: Partial<Record<Outcome, number>> = {};
|
|
301
|
+
violations.forEach(violation => {
|
|
302
|
+
const outcome = violation.outcome || 'failed';
|
|
303
|
+
outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
|
|
304
|
+
});
|
|
305
|
+
(Object.keys(outcomeCounts) as Outcome[]).forEach(outcome => {
|
|
306
|
+
standardInstances.push(getRuleInstance({
|
|
307
|
+
ruleID,
|
|
308
|
+
what: whats,
|
|
309
|
+
ordinalSeverity,
|
|
310
|
+
outcome,
|
|
311
|
+
count: outcomeCounts[outcome]
|
|
312
|
+
}));
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// Return the result.
|
|
316
|
+
return {
|
|
317
|
+
data,
|
|
318
|
+
totals,
|
|
319
|
+
standardInstances
|
|
320
|
+
};
|
|
321
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
© 2026 Jeff Witt.
|
|
3
|
+
|
|
4
|
+
Licensed under the MIT License. See LICENSE file at the project root or
|
|
5
|
+
https://opensource.org/license/mit/ for details.
|
|
6
|
+
|
|
7
|
+
SPDX-License-Identifier: MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
userPath.js
|
|
12
|
+
Converts a recorded user path (a start URL and a sequence of actions of the kinds a
|
|
13
|
+
Playwright codegen recording yields: click, fill, select, navigate, wait, checkpoint) into
|
|
14
|
+
the acts of a Testaro job, so that a job tests the page states the path reaches. Each
|
|
15
|
+
action becomes one act; each checkpoint action becomes a checkpoint act followed by the
|
|
16
|
+
test acts requested. See docs/checkpoint-scanning.md (Phase 4).
|
|
17
|
+
|
|
18
|
+
An action is {type, selector?, value?, url?, timeout?, label?}. Selectors are Playwright
|
|
19
|
+
selectors (CSS, or text=, role=, label= forms), used as the selector property of the act.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// CONSTANTS
|
|
23
|
+
|
|
24
|
+
// The action types and the act types they become.
|
|
25
|
+
const actionActTypes = exports.actionActTypes = {
|
|
26
|
+
click: 'button',
|
|
27
|
+
fill: 'text',
|
|
28
|
+
select: 'select',
|
|
29
|
+
navigate: 'url',
|
|
30
|
+
wait: 'state',
|
|
31
|
+
checkpoint: 'checkpoint'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// FUNCTIONS
|
|
35
|
+
|
|
36
|
+
// Returns a checkpoint name unique among those used, from a label or an ordinal.
|
|
37
|
+
const getCheckpointName = (label, ordinal, used) => {
|
|
38
|
+
const base = (label || '').trim().replace(/\s+/g, ' ') || `checkpoint${ordinal}`;
|
|
39
|
+
let name = base === 'start' ? `${base}-${ordinal}` : base;
|
|
40
|
+
let suffix = 1;
|
|
41
|
+
while (used.has(name)) {
|
|
42
|
+
name = `${base}-${++suffix}`;
|
|
43
|
+
}
|
|
44
|
+
used.add(name);
|
|
45
|
+
return name;
|
|
46
|
+
};
|
|
47
|
+
/*
|
|
48
|
+
Returns the acts of a job for a user path, or throws if an action is invalid.
|
|
49
|
+
startUrl: the URL the path starts at (the launch act's target).
|
|
50
|
+
actions: the recorded actions.
|
|
51
|
+
testActs: the test acts (without checkpoint-dependent properties) to run at each
|
|
52
|
+
checkpoint, e.g. [{type: 'test', which: 'axe', detailLevel: 0}]; each is copied per
|
|
53
|
+
checkpoint. With scope 'changed', the first checkpoint's copies are given scope 'page',
|
|
54
|
+
since there is no earlier checkpoint to compare.
|
|
55
|
+
scanAtCheckpoints: whether checkpoints get the test acts (default true).
|
|
56
|
+
what: a description of the launch target.
|
|
57
|
+
*/
|
|
58
|
+
exports.getUserPathActs = ({startUrl, actions, testActs = [], scanAtCheckpoints = true, what}) => {
|
|
59
|
+
if (! startUrl || typeof startUrl !== 'string') {
|
|
60
|
+
throw new Error('User path has no start URL');
|
|
61
|
+
}
|
|
62
|
+
if (! Array.isArray(actions)) {
|
|
63
|
+
throw new Error('User path actions are not an array');
|
|
64
|
+
}
|
|
65
|
+
const acts = [{
|
|
66
|
+
type: 'launch',
|
|
67
|
+
target: {url: startUrl, what: what || 'start of the user path'}
|
|
68
|
+
}];
|
|
69
|
+
const usedNames = new Set();
|
|
70
|
+
let checkpointCount = 0;
|
|
71
|
+
actions.forEach((action, index) => {
|
|
72
|
+
const {type, selector, value, url, timeout, label} = action || {};
|
|
73
|
+
const comment = label || undefined;
|
|
74
|
+
const need = (condition, message) => {
|
|
75
|
+
if (! condition) {
|
|
76
|
+
throw new Error(`Action ${index} (${type}): ${message}`);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
if (type === 'click') {
|
|
80
|
+
need(selector, 'requires a selector');
|
|
81
|
+
acts.push({type: 'button', selector, what: comment});
|
|
82
|
+
}
|
|
83
|
+
else if (type === 'fill') {
|
|
84
|
+
need(selector, 'requires a selector');
|
|
85
|
+
need(typeof value === 'string' && value.length, 'requires a value');
|
|
86
|
+
acts.push({type: 'text', selector, what: value, clear: true});
|
|
87
|
+
}
|
|
88
|
+
else if (type === 'select') {
|
|
89
|
+
need(selector, 'requires a selector');
|
|
90
|
+
need(typeof value === 'string' && value.length, 'requires a value');
|
|
91
|
+
acts.push({type: 'select', selector, what: value});
|
|
92
|
+
}
|
|
93
|
+
else if (type === 'navigate') {
|
|
94
|
+
need(url, 'requires a url');
|
|
95
|
+
acts.push({type: 'url', which: url, what: comment});
|
|
96
|
+
}
|
|
97
|
+
else if (type === 'wait') {
|
|
98
|
+
// A recorded pause becomes a wait for the page to be idle; the pause length is kept
|
|
99
|
+
// as a comment.
|
|
100
|
+
acts.push({type: 'state', which: 'idle', what: `pause ${timeout ?? 1000} ms`});
|
|
101
|
+
}
|
|
102
|
+
else if (type === 'checkpoint') {
|
|
103
|
+
const name = getCheckpointName(label, ++checkpointCount, usedNames);
|
|
104
|
+
acts.push({type: 'checkpoint', which: name, what: comment});
|
|
105
|
+
if (scanAtCheckpoints) {
|
|
106
|
+
testActs.forEach(testAct => {
|
|
107
|
+
const act = {... testAct};
|
|
108
|
+
if (act.scope === 'changed' && checkpointCount === 1) {
|
|
109
|
+
act.scope = 'page';
|
|
110
|
+
}
|
|
111
|
+
acts.push(act);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
throw new Error(`Action ${index}: unknown type ${type}`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// Remove undefined comments, so the acts serialize cleanly.
|
|
120
|
+
acts.forEach(act => {
|
|
121
|
+
if (act.what === undefined) {
|
|
122
|
+
delete act.what;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
return acts;
|
|
126
|
+
};
|
package/procs/xPath.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Report } from '../types';
|
|
2
|
+
export declare const getNormalizedXPath: (xPath: string | null | undefined) => string;
|
|
3
|
+
export declare const getAttributeXPath: (html: string | null | undefined) => string;
|
|
4
|
+
export declare const getXPathCatalogIndex: (report: Report, xPath: string, checkpoint?: number) => string;
|
package/procs/xPath.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/*
|
|
2
3
|
© 2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
4
|
© 2026 Jeff Witt.
|
|
@@ -8,84 +9,95 @@
|
|
|
8
9
|
|
|
9
10
|
SPDX-License-Identifier: MIT
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.getXPathCatalogIndex = exports.getAttributeXPath = exports.getNormalizedXPath = void 0;
|
|
12
14
|
/*
|
|
13
|
-
xPath
|
|
14
|
-
Processes element XPaths.
|
|
15
|
+
xPath
|
|
16
|
+
Processes element XPaths. Compiled to xPath.js by tsc (issue #73); edit this
|
|
17
|
+
file, not the emitted one.
|
|
15
18
|
*/
|
|
16
|
-
|
|
17
19
|
// FUNCTIONS
|
|
18
|
-
|
|
19
20
|
// Normalizes an XPath.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const getNormalizedXPath = (xPath) => {
|
|
22
|
+
if (xPath) {
|
|
23
|
+
if (xPath === '/') {
|
|
24
|
+
xPath = '/html';
|
|
25
|
+
}
|
|
26
|
+
xPath = xPath.replace(/^\.\/\//, '/');
|
|
27
|
+
const segments = xPath.split('/');
|
|
28
|
+
// Initialize an array of normalized segments.
|
|
29
|
+
const normalizedSegments = [];
|
|
30
|
+
// For each segment of the XPath:
|
|
31
|
+
segments.forEach(segment => {
|
|
32
|
+
// If the segment is html[1] or body[1]:
|
|
33
|
+
if (/html\[1\]|body\[1\]/.test(segment)) {
|
|
34
|
+
// Add it without its subscript to the array.
|
|
35
|
+
normalizedSegments.push(segment.replace(/\[1\]/, ''));
|
|
36
|
+
}
|
|
37
|
+
// Otherwise, if the segment is empty or html or body or ends with a subscript:
|
|
38
|
+
else if (segment === '' || ['html', 'body'].includes(segment) || segment.endsWith(']')) {
|
|
39
|
+
// Add it to the array.
|
|
40
|
+
normalizedSegments.push(segment);
|
|
41
|
+
}
|
|
42
|
+
// Otherwise, i.e. if the segment is a tag name with no subscript:
|
|
43
|
+
else {
|
|
44
|
+
// Add it with a subscript 1 to the array.
|
|
45
|
+
normalizedSegments.push(`${segment}[1]`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// If the final segment contains any nonstandard character:
|
|
49
|
+
if (/[[^\]A-Za-z0-9]/.test(segments[segments.length - 1])) {
|
|
50
|
+
// Remove it.
|
|
51
|
+
normalizedSegments.pop();
|
|
52
|
+
}
|
|
53
|
+
// Return the concatenated segments as the normalized XPath.
|
|
54
|
+
return normalizedSegments.join('/');
|
|
24
55
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Initialize an array of normalized segments.
|
|
28
|
-
const normalizedSegments = [];
|
|
29
|
-
// For each segment of the XPath:
|
|
30
|
-
segments.forEach(segment => {
|
|
31
|
-
// If the segment is html[1] or body[1]:
|
|
32
|
-
if (/html\[1\]|body\[1\]/.test(segment)) {
|
|
33
|
-
// Add it without its subscript to the array.
|
|
34
|
-
normalizedSegments.push(segment.replace(/\[1\]/, ''));
|
|
35
|
-
}
|
|
36
|
-
// Otherwise, if the segment is empty or html or body or ends with a subscript:
|
|
37
|
-
else if (segment === '' || ['html', 'body'].includes(segment) || segment.endsWith(']')) {
|
|
38
|
-
// Add it to the array.
|
|
39
|
-
normalizedSegments.push(segment);
|
|
40
|
-
}
|
|
41
|
-
// Otherwise, i.e. if the segment is a tag name with no subscript:
|
|
42
|
-
else {
|
|
43
|
-
// Add it with a subscript 1 to the array.
|
|
44
|
-
normalizedSegments.push(`${segment}[1]`);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
// If the final segment contains any nonstandard character:
|
|
48
|
-
if (/[[^\]A-Za-z0-9]/.test(segments[segments.length - 1])) {
|
|
49
|
-
// Remove it.
|
|
50
|
-
normalizedSegments.pop();
|
|
56
|
+
else {
|
|
57
|
+
return '/html';
|
|
51
58
|
}
|
|
52
|
-
// Return the concatenated segments as the normalized XPath.
|
|
53
|
-
return normalizedSegments.join('/');
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
return '/html';
|
|
57
|
-
}
|
|
58
59
|
};
|
|
60
|
+
exports.getNormalizedXPath = getNormalizedXPath;
|
|
59
61
|
// Gets an XPath from a data-xpath attribute in an HTML excerpt.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
const getAttributeXPath = (html) => {
|
|
63
|
+
// If there is no excerpt (e.g. a Nu Html Checker message without an extract):
|
|
64
|
+
if (!html) {
|
|
65
|
+
// Return the fallback XPath, as for an excerpt without a data-xpath attribute.
|
|
66
|
+
return '/html';
|
|
67
|
+
}
|
|
68
|
+
const match = html.match(/ data-xpath="([^" ]+)"/);
|
|
69
|
+
return match ? match[1] : '/html';
|
|
68
70
|
};
|
|
71
|
+
exports.getAttributeXPath = getAttributeXPath;
|
|
69
72
|
// Gets a tag name from an XPath.
|
|
70
|
-
const getXPathTagName = xPath => {
|
|
71
|
-
|
|
73
|
+
const getXPathTagName = (xPath) => {
|
|
74
|
+
return xPath.split('/').pop().replace(/\[.+/, '').toUpperCase();
|
|
72
75
|
};
|
|
73
|
-
// Gets a catalog index as a string from an XPath
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
76
|
+
// Gets a catalog index as a string from an XPath, within a checkpoint (the active one by
|
|
77
|
+
// default). An XPath the checkpoint's catalog lacks gets a stub entry, keyed from the report's
|
|
78
|
+
// monotonic counter so that entries of different checkpoints never collide.
|
|
79
|
+
const getXPathCatalogIndex = (report, xPath, checkpoint = report.activeCheckpoint ?? 0) => {
|
|
80
|
+
// The catalog always exists by the time tests cite elements (getCatalog ran first).
|
|
81
|
+
const { catalog } = report;
|
|
82
|
+
report.pathIDs ??= {};
|
|
83
|
+
const pathIDs = (report.pathIDs[checkpoint] ??= {});
|
|
84
|
+
// Get the index of the catalog item with the XPath.
|
|
85
|
+
const index = pathIDs[xPath];
|
|
86
|
+
// If no such item exists (an index of '0' is a match, so test for absence, not falsity):
|
|
87
|
+
if (index === undefined) {
|
|
88
|
+
// Add an item to the catalog, keyed by the counter (or, in a report without one, by the
|
|
89
|
+
// count of items already in it).
|
|
90
|
+
const newIndex = report.catalogNextIndex === undefined
|
|
91
|
+
? `${Object.keys(catalog).length}`
|
|
92
|
+
: `${report.catalogNextIndex++}`;
|
|
93
|
+
catalog[newIndex] = {
|
|
94
|
+
pathID: xPath,
|
|
95
|
+
tagName: getXPathTagName(xPath),
|
|
96
|
+
checkpoint
|
|
97
|
+
};
|
|
98
|
+
pathIDs[xPath] = newIndex;
|
|
99
|
+
return newIndex;
|
|
100
|
+
}
|
|
101
|
+
return index;
|
|
91
102
|
};
|
|
103
|
+
exports.getXPathCatalogIndex = getXPathCatalogIndex;
|