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/wave.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
import type { Report, StandardResult } from '../types';
|
|
3
|
+
interface WaveItem {
|
|
4
|
+
count: number;
|
|
5
|
+
description: string;
|
|
6
|
+
selectors: (string | [string, string])[];
|
|
7
|
+
}
|
|
8
|
+
interface WaveCategory {
|
|
9
|
+
count: number;
|
|
10
|
+
items?: Record<string, WaveItem>;
|
|
11
|
+
}
|
|
12
|
+
interface WaveResult {
|
|
13
|
+
categories?: Record<string, WaveCategory>;
|
|
14
|
+
status: {
|
|
15
|
+
success: boolean;
|
|
16
|
+
error?: string;
|
|
17
|
+
};
|
|
18
|
+
statistics?: {
|
|
19
|
+
pagetitle?: string;
|
|
20
|
+
pageurl?: string;
|
|
21
|
+
time?: number;
|
|
22
|
+
creditsremaining?: number;
|
|
23
|
+
allitemcount?: number;
|
|
24
|
+
totalelements?: number;
|
|
25
|
+
waveurl?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
interface WaveData {
|
|
29
|
+
prevented?: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
pageTitle?: string;
|
|
32
|
+
pageURL?: string;
|
|
33
|
+
elapsedSeconds?: number | null;
|
|
34
|
+
creditsRemaining?: number | null;
|
|
35
|
+
allItemCount?: number | null;
|
|
36
|
+
totalElements?: number | null;
|
|
37
|
+
waveURL?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare const reporter: (page: Page, report: Report, actIndex: number) => Promise<{
|
|
40
|
+
data: WaveData;
|
|
41
|
+
result: {
|
|
42
|
+
nativeResult: WaveResult;
|
|
43
|
+
standardResult: StandardResult;
|
|
44
|
+
};
|
|
45
|
+
}>;
|
|
46
|
+
export {};
|
package/tests/wave.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/*
|
|
2
3
|
© 2021–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
4
|
© 2026 Jeff Witt.
|
|
@@ -8,194 +9,182 @@
|
|
|
8
9
|
|
|
9
10
|
SPDX-License-Identifier: MIT
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.reporter = void 0;
|
|
14
|
+
const xPath_1 = require("../procs/xPath");
|
|
15
|
+
const standard_1 = require("../procs/standard");
|
|
16
|
+
// CONSTANTS
|
|
17
|
+
const https = require('https');
|
|
12
18
|
/*
|
|
13
19
|
wave
|
|
14
20
|
Implements the WebAIM WAVE ruleset for accessibility. The 'reportType' argument
|
|
15
21
|
specifies a WAVE report type: 1, 2, 3, or 4.
|
|
22
|
+
Compiled to wave.js by tsc (issue #73); edit this file, not the emitted one.
|
|
16
23
|
*/
|
|
17
|
-
|
|
18
|
-
// IMPORTS
|
|
19
|
-
|
|
20
|
-
const {getXPathCatalogIndex} = require('../procs/xPath');
|
|
21
|
-
|
|
22
|
-
// CONSTANTS
|
|
23
|
-
|
|
24
|
-
const https = require('https');
|
|
25
|
-
|
|
26
24
|
// FUNCTIONS
|
|
27
|
-
|
|
28
25
|
// Conducts and reports the WAVE tests.
|
|
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
|
-
const standard = report.standard !== 'no';
|
|
58
|
-
// If standard results are to be reported:
|
|
59
|
-
if (standard) {
|
|
60
|
-
// Initialize the standard result.
|
|
61
|
-
result.standardResult = {
|
|
62
|
-
prevented: false,
|
|
63
|
-
totals: [0, 0, 0, 0],
|
|
64
|
-
instances: []
|
|
26
|
+
const reporter = async (page, report, actIndex) => {
|
|
27
|
+
// Create a host and a path for a request to the WAVE API.
|
|
28
|
+
const act = report.acts[actIndex];
|
|
29
|
+
const { reportType, url, prescript, postscript, rules } = act;
|
|
30
|
+
const waveKey = process.env.WAVE_KEY;
|
|
31
|
+
const waveKeyParam = waveKey ? `key=${waveKey}` : '';
|
|
32
|
+
let host = 'wave.webaim.org';
|
|
33
|
+
if (url && url.startsWith('http')) {
|
|
34
|
+
host = url.replace(/^https?:\/\//, '');
|
|
35
|
+
}
|
|
36
|
+
let prescriptParam = prescript ? `prescript=${prescript}` : '';
|
|
37
|
+
let postscriptParam = postscript ? `postscript=${postscript}` : '';
|
|
38
|
+
const wavePath = '/api/request';
|
|
39
|
+
const queryParams = [
|
|
40
|
+
waveKeyParam,
|
|
41
|
+
`url=${page.url()}`,
|
|
42
|
+
`reporttype=${reportType}`,
|
|
43
|
+
prescriptParam,
|
|
44
|
+
postscriptParam
|
|
45
|
+
];
|
|
46
|
+
const query = queryParams.filter(param => param).join('&');
|
|
47
|
+
const path = [wavePath, query].join('?');
|
|
48
|
+
// Initialize the act report.
|
|
49
|
+
const data = {};
|
|
50
|
+
const result = {
|
|
51
|
+
nativeResult: {},
|
|
52
|
+
standardResult: {}
|
|
65
53
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
54
|
+
const standard = report.standard !== 'no';
|
|
55
|
+
// If standard results are to be reported:
|
|
56
|
+
if (standard) {
|
|
57
|
+
// Initialize the standard result.
|
|
58
|
+
result.standardResult = (0, standard_1.getStandardResult)();
|
|
59
|
+
}
|
|
60
|
+
// Get and process a WAVE API report and return the results.
|
|
61
|
+
return await new Promise(resolve => https.get({
|
|
62
|
+
host,
|
|
63
|
+
path
|
|
64
|
+
}, response => {
|
|
65
|
+
let rawReport = '';
|
|
66
|
+
response.on('data', chunk => {
|
|
67
|
+
rawReport += chunk;
|
|
68
|
+
});
|
|
69
|
+
// When the response arrives:
|
|
70
|
+
response.on('end', async () => {
|
|
71
|
+
try {
|
|
72
|
+
// Parse it as JSON.
|
|
73
|
+
result.nativeResult = JSON.parse(rawReport);
|
|
74
|
+
}
|
|
75
|
+
// If it was not parsable:
|
|
76
|
+
catch (error) {
|
|
77
|
+
// Report this.
|
|
78
|
+
data.prevented = true;
|
|
79
|
+
data.error = error.message;
|
|
80
|
+
result.nativeResult = {};
|
|
81
|
+
}
|
|
82
|
+
// If the response was parsed:
|
|
83
|
+
if (!data.prevented) {
|
|
84
|
+
const { categories, status } = result.nativeResult;
|
|
85
|
+
// If the request succeeded and produced categories:
|
|
86
|
+
if (status.success && categories) {
|
|
87
|
+
// Delete the unnecessary properties of the categories.
|
|
88
|
+
delete categories.feature;
|
|
89
|
+
delete categories.structure;
|
|
90
|
+
delete categories.aria;
|
|
91
|
+
// For each WAVE rule category:
|
|
92
|
+
for (const categoryName of ['error', 'contrast', 'alert']) {
|
|
93
|
+
const category = categories[categoryName];
|
|
94
|
+
const ordinalSeverity = categoryName === 'alert' ? 0 : 3;
|
|
95
|
+
// If any violated rules (named items by WAVE) were reported:
|
|
96
|
+
if (category?.items
|
|
97
|
+
&& Object.keys(category.items).length) {
|
|
98
|
+
const { items } = category;
|
|
99
|
+
// If rules to be tested for were specified:
|
|
100
|
+
if (rules && rules.length) {
|
|
101
|
+
// For each rule violated:
|
|
102
|
+
Object.keys(items).forEach(ruleID => {
|
|
103
|
+
// If it was not a specified rule:
|
|
104
|
+
if (!rules.includes(ruleID)) {
|
|
105
|
+
// Decrease the category violation count by the count of its violations.
|
|
106
|
+
category.count -= items[ruleID].count;
|
|
107
|
+
// Remove its violations from the native result.
|
|
108
|
+
delete items[ruleID];
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// If standard results are to be reported:
|
|
113
|
+
if (standard) {
|
|
114
|
+
const { standardResult } = result;
|
|
115
|
+
const { totals } = standardResult;
|
|
116
|
+
// Add the category violation count to the standard-result totals.
|
|
117
|
+
totals[ordinalSeverity] += category.count;
|
|
118
|
+
const annotatedItems = await page.evaluate(items => {
|
|
119
|
+
const ruleIDs = Object.keys(items);
|
|
120
|
+
// For each rule of the category with any violations:
|
|
121
|
+
ruleIDs.forEach(ruleID => {
|
|
122
|
+
const { selectors } = items[ruleID];
|
|
123
|
+
// For each of those violations:
|
|
124
|
+
for (const index in selectors) {
|
|
125
|
+
const selector = selectors[index];
|
|
126
|
+
// Get the violator.
|
|
127
|
+
let violator;
|
|
128
|
+
try {
|
|
129
|
+
violator = document.querySelector(selector);
|
|
130
|
+
// Concatenate its selector with its XPath in the native result.
|
|
131
|
+
selectors[index] = [
|
|
132
|
+
selector, window.getXPath(violator) ?? ''
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error(`ERROR: Invalid selector: ${selector} (${error.message})`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return items;
|
|
141
|
+
}, items);
|
|
142
|
+
const ruleIDs = Object.keys(annotatedItems);
|
|
143
|
+
// For each rule of the category with any violations:
|
|
144
|
+
for (const ruleID of ruleIDs) {
|
|
145
|
+
const { description, selectors } = annotatedItems[ruleID];
|
|
146
|
+
// For each violation of the rule:
|
|
147
|
+
for (const violation of selectors) {
|
|
148
|
+
const xPath = violation[1];
|
|
149
|
+
// Add an instance to the standard result. Alerts are uncertainty.
|
|
150
|
+
(0, standard_1.pushInstance)(standardResult, {
|
|
151
|
+
ruleID,
|
|
152
|
+
what: description,
|
|
153
|
+
ordinalSeverity,
|
|
154
|
+
outcome: categoryName === 'alert' ? 'cantTell' : 'failed',
|
|
155
|
+
catalogIndex: (0, xPath_1.getXPathCatalogIndex)(report, xPath)
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
145
160
|
}
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
return items;
|
|
149
|
-
}, items);
|
|
150
|
-
const ruleIDs = Object.keys(annotatedItems);
|
|
151
|
-
// For each rule of the category with any violations:
|
|
152
|
-
for (const ruleID of ruleIDs) {
|
|
153
|
-
const {description, selectors} = annotatedItems[ruleID];
|
|
154
|
-
// For each violation of the rule:
|
|
155
|
-
for (const violation of selectors) {
|
|
156
|
-
// Initialize a standard instance.
|
|
157
|
-
const instance = {
|
|
158
|
-
ruleID,
|
|
159
|
-
what: description,
|
|
160
|
-
ordinalSeverity,
|
|
161
|
-
count: 1
|
|
162
|
-
};
|
|
163
|
-
const xPath = violation[1];
|
|
164
|
-
// Add the catalog index to the instance.
|
|
165
|
-
instance.catalogIndex = getXPathCatalogIndex(report, xPath);
|
|
166
|
-
// Add the instance to the standard result.
|
|
167
|
-
instances.push(instance);
|
|
168
161
|
}
|
|
169
|
-
}
|
|
170
162
|
}
|
|
171
|
-
|
|
163
|
+
// Otherwise, if the request failed:
|
|
164
|
+
else if (!status.success) {
|
|
165
|
+
// Report this.
|
|
166
|
+
data.prevented = true;
|
|
167
|
+
data.error = status.error || 'Unknown error';
|
|
168
|
+
}
|
|
169
|
+
const { statistics } = result.nativeResult;
|
|
170
|
+
if (statistics) {
|
|
171
|
+
// Copy important data from the native result to the result.
|
|
172
|
+
data.pageTitle = statistics.pagetitle || '';
|
|
173
|
+
data.pageURL = statistics.pageurl || '';
|
|
174
|
+
data.elapsedSeconds = statistics.time || null;
|
|
175
|
+
data.creditsRemaining = statistics.creditsremaining || null;
|
|
176
|
+
console.log(`WAVE credits remaining: ${data.creditsRemaining}`);
|
|
177
|
+
data.allItemCount = statistics.allitemcount || null;
|
|
178
|
+
data.totalElements = statistics.totalelements || null;
|
|
179
|
+
data.waveURL = statistics.waveurl || '';
|
|
180
|
+
}
|
|
172
181
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
data.error = status.error || 'Unknown error';
|
|
179
|
-
}
|
|
180
|
-
const {statistics} = result.nativeResult;
|
|
181
|
-
if (statistics) {
|
|
182
|
-
// Copy important data from the native result to the result.
|
|
183
|
-
data.pageTitle = statistics.pagetitle || '';
|
|
184
|
-
data.pageURL = statistics.pageurl || '';
|
|
185
|
-
data.elapsedSeconds = statistics.time || null;
|
|
186
|
-
data.creditsRemaining = statistics.creditsremaining || null;
|
|
187
|
-
console.log(`WAVE credits remaining: ${data.creditsRemaining}`);
|
|
188
|
-
data.allItemCount = statistics.allitemcount || null;
|
|
189
|
-
data.totalElements = statistics.totalelements || null;
|
|
190
|
-
data.waveURL = statistics.waveurl || '';
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// Return the result.
|
|
194
|
-
resolve({
|
|
195
|
-
data,
|
|
196
|
-
result
|
|
182
|
+
// Return the result.
|
|
183
|
+
resolve({
|
|
184
|
+
data,
|
|
185
|
+
result
|
|
186
|
+
});
|
|
197
187
|
});
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
));
|
|
188
|
+
}));
|
|
201
189
|
};
|
|
190
|
+
exports.reporter = reporter;
|
package/tests/wave.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/*
|
|
2
|
+
© 2021–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
|
+
© 2026 Jeff Witt.
|
|
4
|
+
© 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
|
+
// IMPORTS
|
|
13
|
+
|
|
14
|
+
import type {Page} from 'playwright';
|
|
15
|
+
import {getXPathCatalogIndex} from '../procs/xPath';
|
|
16
|
+
import {getStandardResult, pushInstance} from '../procs/standard';
|
|
17
|
+
import type {Act, Report, StandardInstance, StandardResult} from '../types';
|
|
18
|
+
|
|
19
|
+
// TYPES
|
|
20
|
+
|
|
21
|
+
// The wave-act properties this reporter consumes.
|
|
22
|
+
interface WaveAct extends Act {
|
|
23
|
+
reportType?: number;
|
|
24
|
+
url?: string;
|
|
25
|
+
prescript?: string;
|
|
26
|
+
postscript?: string;
|
|
27
|
+
rules?: string[];
|
|
28
|
+
}
|
|
29
|
+
// One violated rule (item) of a WAVE category; the annotation step replaces
|
|
30
|
+
// each selector with a [selector, xPath] pair.
|
|
31
|
+
interface WaveItem {
|
|
32
|
+
count: number;
|
|
33
|
+
description: string;
|
|
34
|
+
selectors: (string | [string, string])[];
|
|
35
|
+
}
|
|
36
|
+
// One WAVE rule category.
|
|
37
|
+
interface WaveCategory {
|
|
38
|
+
count: number;
|
|
39
|
+
items?: Record<string, WaveItem>;
|
|
40
|
+
}
|
|
41
|
+
// The native result: the parsed WAVE API response.
|
|
42
|
+
interface WaveResult {
|
|
43
|
+
categories?: Record<string, WaveCategory>;
|
|
44
|
+
status: {
|
|
45
|
+
success: boolean;
|
|
46
|
+
error?: string;
|
|
47
|
+
};
|
|
48
|
+
statistics?: {
|
|
49
|
+
pagetitle?: string;
|
|
50
|
+
pageurl?: string;
|
|
51
|
+
time?: number;
|
|
52
|
+
creditsremaining?: number;
|
|
53
|
+
allitemcount?: number;
|
|
54
|
+
totalelements?: number;
|
|
55
|
+
waveurl?: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// The data of the act report.
|
|
59
|
+
interface WaveData {
|
|
60
|
+
prevented?: boolean;
|
|
61
|
+
error?: string;
|
|
62
|
+
pageTitle?: string;
|
|
63
|
+
pageURL?: string;
|
|
64
|
+
elapsedSeconds?: number | null;
|
|
65
|
+
creditsRemaining?: number | null;
|
|
66
|
+
allItemCount?: number | null;
|
|
67
|
+
totalElements?: number | null;
|
|
68
|
+
waveURL?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// CONSTANTS
|
|
72
|
+
|
|
73
|
+
const https = require('https') as typeof import('https');
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
wave
|
|
77
|
+
Implements the WebAIM WAVE ruleset for accessibility. The 'reportType' argument
|
|
78
|
+
specifies a WAVE report type: 1, 2, 3, or 4.
|
|
79
|
+
Compiled to wave.js by tsc (issue #73); edit this file, not the emitted one.
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
// FUNCTIONS
|
|
83
|
+
|
|
84
|
+
// Conducts and reports the WAVE tests.
|
|
85
|
+
export const reporter = async (page: Page, report: Report, actIndex: number) => {
|
|
86
|
+
// Create a host and a path for a request to the WAVE API.
|
|
87
|
+
const act = report.acts[actIndex] as WaveAct;
|
|
88
|
+
const {reportType, url, prescript, postscript, rules} = act;
|
|
89
|
+
const waveKey = process.env.WAVE_KEY;
|
|
90
|
+
const waveKeyParam = waveKey ? `key=${waveKey}` : '';
|
|
91
|
+
let host = 'wave.webaim.org';
|
|
92
|
+
if (url && url.startsWith('http')) {
|
|
93
|
+
host = url.replace(/^https?:\/\//, '');
|
|
94
|
+
}
|
|
95
|
+
let prescriptParam = prescript ? `prescript=${prescript}` : '';
|
|
96
|
+
let postscriptParam = postscript ? `postscript=${postscript}` : '';
|
|
97
|
+
const wavePath = '/api/request';
|
|
98
|
+
const queryParams = [
|
|
99
|
+
waveKeyParam,
|
|
100
|
+
`url=${page.url()}`,
|
|
101
|
+
`reporttype=${reportType}`,
|
|
102
|
+
prescriptParam,
|
|
103
|
+
postscriptParam
|
|
104
|
+
];
|
|
105
|
+
const query = queryParams.filter(param => param).join('&');
|
|
106
|
+
const path = [wavePath, query].join('?');
|
|
107
|
+
// Initialize the act report.
|
|
108
|
+
const data: WaveData = {};
|
|
109
|
+
const result: {nativeResult: WaveResult; standardResult: StandardResult} = {
|
|
110
|
+
nativeResult: {} as WaveResult,
|
|
111
|
+
standardResult: {}
|
|
112
|
+
};
|
|
113
|
+
const standard = report.standard !== 'no';
|
|
114
|
+
// If standard results are to be reported:
|
|
115
|
+
if (standard) {
|
|
116
|
+
// Initialize the standard result.
|
|
117
|
+
result.standardResult = getStandardResult();
|
|
118
|
+
}
|
|
119
|
+
// Get and process a WAVE API report and return the results.
|
|
120
|
+
return await new Promise<{data: WaveData; result: typeof result}>(resolve => https.get(
|
|
121
|
+
{
|
|
122
|
+
host,
|
|
123
|
+
path
|
|
124
|
+
},
|
|
125
|
+
response => {
|
|
126
|
+
let rawReport = '';
|
|
127
|
+
response.on('data', chunk => {
|
|
128
|
+
rawReport += chunk;
|
|
129
|
+
});
|
|
130
|
+
// When the response arrives:
|
|
131
|
+
response.on('end', async () => {
|
|
132
|
+
try {
|
|
133
|
+
// Parse it as JSON.
|
|
134
|
+
result.nativeResult = JSON.parse(rawReport);
|
|
135
|
+
}
|
|
136
|
+
// If it was not parsable:
|
|
137
|
+
catch (error) {
|
|
138
|
+
// Report this.
|
|
139
|
+
data.prevented = true;
|
|
140
|
+
data.error = (error as Error).message;
|
|
141
|
+
result.nativeResult = {} as WaveResult;
|
|
142
|
+
}
|
|
143
|
+
// If the response was parsed:
|
|
144
|
+
if (! data.prevented) {
|
|
145
|
+
const {categories, status} = result.nativeResult;
|
|
146
|
+
// If the request succeeded and produced categories:
|
|
147
|
+
if(status.success && categories) {
|
|
148
|
+
// Delete the unnecessary properties of the categories.
|
|
149
|
+
delete categories.feature;
|
|
150
|
+
delete categories.structure;
|
|
151
|
+
delete categories.aria;
|
|
152
|
+
// For each WAVE rule category:
|
|
153
|
+
for (const categoryName of ['error', 'contrast', 'alert']) {
|
|
154
|
+
const category = categories[categoryName];
|
|
155
|
+
const ordinalSeverity = categoryName === 'alert' ? 0 : 3;
|
|
156
|
+
// If any violated rules (named items by WAVE) were reported:
|
|
157
|
+
if (
|
|
158
|
+
category?.items
|
|
159
|
+
&& Object.keys(category.items).length
|
|
160
|
+
) {
|
|
161
|
+
const {items} = category as Required<WaveCategory>;
|
|
162
|
+
// If rules to be tested for were specified:
|
|
163
|
+
if (rules && rules.length) {
|
|
164
|
+
// For each rule violated:
|
|
165
|
+
Object.keys(items).forEach(ruleID => {
|
|
166
|
+
// If it was not a specified rule:
|
|
167
|
+
if (! rules.includes(ruleID)) {
|
|
168
|
+
// Decrease the category violation count by the count of its violations.
|
|
169
|
+
category.count -= items[ruleID].count;
|
|
170
|
+
// Remove its violations from the native result.
|
|
171
|
+
delete items[ruleID];
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// If standard results are to be reported:
|
|
176
|
+
if (standard) {
|
|
177
|
+
const {standardResult} = result;
|
|
178
|
+
const {totals} = standardResult as Required<StandardResult>;
|
|
179
|
+
// Add the category violation count to the standard-result totals.
|
|
180
|
+
totals[ordinalSeverity] += category.count;
|
|
181
|
+
const annotatedItems = await page.evaluate(items => {
|
|
182
|
+
const ruleIDs = Object.keys(items);
|
|
183
|
+
// For each rule of the category with any violations:
|
|
184
|
+
ruleIDs.forEach(ruleID => {
|
|
185
|
+
const {selectors} = items[ruleID];
|
|
186
|
+
// For each of those violations:
|
|
187
|
+
for (const index in selectors) {
|
|
188
|
+
const selector = selectors[index as unknown as number] as string;
|
|
189
|
+
// Get the violator.
|
|
190
|
+
let violator: Element | null | undefined;
|
|
191
|
+
try {
|
|
192
|
+
violator = document.querySelector(selector);
|
|
193
|
+
// Concatenate its selector with its XPath in the native result.
|
|
194
|
+
selectors[index as unknown as number] = [
|
|
195
|
+
selector, window.getXPath(violator as Element) ?? ''
|
|
196
|
+
];
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(`ERROR: Invalid selector: ${selector} (${(error as Error).message})`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
return items;
|
|
203
|
+
}, items);
|
|
204
|
+
const ruleIDs = Object.keys(annotatedItems);
|
|
205
|
+
// For each rule of the category with any violations:
|
|
206
|
+
for (const ruleID of ruleIDs) {
|
|
207
|
+
const {description, selectors} = annotatedItems[ruleID];
|
|
208
|
+
// For each violation of the rule:
|
|
209
|
+
for (const violation of selectors) {
|
|
210
|
+
const xPath = violation[1];
|
|
211
|
+
// Add an instance to the standard result. Alerts are uncertainty.
|
|
212
|
+
pushInstance(standardResult, {
|
|
213
|
+
ruleID,
|
|
214
|
+
what: description,
|
|
215
|
+
ordinalSeverity,
|
|
216
|
+
outcome: categoryName === 'alert' ? 'cantTell' : 'failed',
|
|
217
|
+
catalogIndex: getXPathCatalogIndex(report, xPath)
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Otherwise, if the request failed:
|
|
226
|
+
else if (! status.success) {
|
|
227
|
+
// Report this.
|
|
228
|
+
data.prevented = true;
|
|
229
|
+
data.error = status.error || 'Unknown error';
|
|
230
|
+
}
|
|
231
|
+
const {statistics} = result.nativeResult;
|
|
232
|
+
if (statistics) {
|
|
233
|
+
// Copy important data from the native result to the result.
|
|
234
|
+
data.pageTitle = statistics.pagetitle || '';
|
|
235
|
+
data.pageURL = statistics.pageurl || '';
|
|
236
|
+
data.elapsedSeconds = statistics.time || null;
|
|
237
|
+
data.creditsRemaining = statistics.creditsremaining || null;
|
|
238
|
+
console.log(`WAVE credits remaining: ${data.creditsRemaining}`);
|
|
239
|
+
data.allItemCount = statistics.allitemcount || null;
|
|
240
|
+
data.totalElements = statistics.totalelements || null;
|
|
241
|
+
data.waveURL = statistics.waveurl || '';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Return the result.
|
|
245
|
+
resolve({
|
|
246
|
+
data,
|
|
247
|
+
result
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
));
|
|
252
|
+
};
|