eaa-kit 0.3.0 → 0.5.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/README.md +82 -14
- package/dist/astro/index.d.ts +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/audit/runners/worker.js +5 -1
- package/dist/{audit-DcL73mOC.js → audit-B282A3EA.js} +207 -41
- package/dist/audit-DyHPmGeD.js +2 -0
- package/dist/{baseline-CuKFq4IF.js → baseline-0u4df12v.js} +1 -1
- package/dist/{baseline-s9F3fXTN.js → baseline-CuAS2lMk.js} +6 -5
- package/dist/cli/index.js +306 -18
- package/dist/{collect-BkAQ0viT.js → collect-Cu1q9NTB.js} +53 -11
- package/dist/command-C3D7JWn6.js +160 -0
- package/dist/{component-7kEBjv_y.js → component-C3GL1Mnu.js} +41 -6
- package/dist/component-DKd3EHOg.js +2 -0
- package/dist/coverage-B_Y6l-Ra.js +522 -0
- package/dist/{crawl-Oxt2Gaqo.js → crawl-BUWPgaGW.js} +83 -9
- package/dist/eleventy/index.d.ts +28 -0
- package/dist/eleventy/index.js +19 -0
- package/dist/fingerprint-BjYV_0F7.js +67 -0
- package/dist/{frameworks-BYa3tULg.js → frameworks-B4ClIJgE.js} +71 -0
- package/dist/{frameworks-DaDqrJOw.js → frameworks-etFg_O8K.js} +1 -1
- package/dist/{html-DKiI_3gs.js → html-C4vktg0A.js} +130 -5
- package/dist/{impact-YdoOtFqm.js → impact-DZt2oBCP.js} +1 -1
- package/dist/index.d.ts +119 -4
- package/dist/index.js +2 -2
- package/dist/{init-DiLiYvN1.js → init-CW7LfGT5.js} +10 -4
- package/dist/jsdom-22Bkt65v.js +3 -0
- package/dist/jsdom-B--cEH-G.js +89 -0
- package/dist/{json-Cnv9nd6U.js → json-DROX33kh.js} +9 -4
- package/dist/{json-B0Y7rNjt.js → json-D_Mnnft5.js} +1 -1
- package/dist/load-CFq2VQtT.js +2 -0
- package/dist/{load-UYXLqGV9.js → load-yAR4wzez.js} +133 -8
- package/dist/nuxt/index.d.ts +35 -0
- package/dist/nuxt/index.js +23 -0
- package/dist/{playwright-DYFsGUNd.js → playwright-BojtYVUa.js} +30 -6
- package/dist/{pool-BWkWZiJW.js → pool-BO25OIez.js} +52 -2
- package/dist/{project-DW08TseF.js → project-CzOnkLH6.js} +8 -2
- package/dist/project-MFrXcw1M.js +2 -0
- package/dist/remediation-CMBIrnpN.js +321 -0
- package/dist/{render-DI_aCnAZ.js → render-DbGOVmhx.js} +48 -10
- package/dist/{result-DLxd2Eip.js → result-BWcYXeRs.js} +114 -3
- package/dist/routes-CmdRUuOs.js +265 -0
- package/dist/{run-BW6CVuND.js → run-C2nKFcb-.js} +19 -2
- package/dist/{run-BMASMmwO.d.ts → run-CtcEUhbe.d.ts} +7 -0
- package/dist/{sarif-DB3WG7T9.js → sarif-B-UBcVu8.js} +16 -8
- package/dist/{schema-CMZ8ItGk.js → schema-DJSF4K05.js} +18 -1
- package/dist/statement/templates/es.en.md +125 -0
- package/dist/statement/templates/es.es.md +127 -0
- package/dist/statement/templates/fr.en.md +128 -0
- package/dist/statement/templates/fr.fr.md +131 -0
- package/dist/statement/templates/it.en.md +127 -0
- package/dist/statement/templates/it.it.md +130 -0
- package/dist/statement/templates/nl.en.md +125 -0
- package/dist/statement/templates/nl.nl.md +127 -0
- package/dist/{text-BFmNtMsV.js → text-CKKpzkYM.js} +1 -1
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/webpack/index.d.ts +33 -0
- package/dist/webpack/index.js +20 -0
- package/package.json +35 -9
- package/dist/audit-CuG2hYyo.js +0 -2
- package/dist/command-Dxpa00Ha.js +0 -77
- package/dist/fingerprint-DRoneAjj.js +0 -20
- package/dist/jsdom-BjpF-2V-.js +0 -158
- package/dist/jsdom-X4KYfTp8.js +0 -3
- package/dist/manual-Vz-oX1I_.js +0 -239
- package/dist/routes-C2Cgf6Ko.js +0 -119
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as toPosix } from "./fs-BmPtmFke.js";
|
|
2
|
-
import { t as collapse } from "./text-
|
|
2
|
+
import { t as collapse } from "./text-CKKpzkYM.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { glob } from "tinyglobby";
|
|
@@ -96,16 +96,51 @@ function searchTermsFor(html) {
|
|
|
96
96
|
* Returns undefined when nothing matches and when more than one does. The
|
|
97
97
|
* second case is the important one: naming a file that merely happens to
|
|
98
98
|
* contain the same path sends somebody to edit the wrong component.
|
|
99
|
+
*
|
|
100
|
+
* The position comes free. The search already has to find the literal to know
|
|
101
|
+
* the file contains it, so recording where it found it costs one more scan of
|
|
102
|
+
* one string — and a report that says `Header.astro:12` opens an editor where a
|
|
103
|
+
* report that says `Header.astro` starts a search.
|
|
99
104
|
*/
|
|
100
105
|
function componentFor(index, html) {
|
|
101
106
|
for (const term of searchTermsFor(html)) {
|
|
102
107
|
const matches = [];
|
|
103
|
-
for (const [file, source] of index.files)
|
|
104
|
-
|
|
105
|
-
if (
|
|
108
|
+
for (const [file, source] of index.files) {
|
|
109
|
+
const offset = source.indexOf(term);
|
|
110
|
+
if (offset !== -1) {
|
|
111
|
+
matches.push({
|
|
112
|
+
file,
|
|
113
|
+
offset
|
|
114
|
+
});
|
|
115
|
+
if (matches.length > 1) break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const only = matches.length === 1 ? matches[0] : void 0;
|
|
119
|
+
if (only !== void 0) {
|
|
120
|
+
const source = index.files.get(only.file);
|
|
121
|
+
return {
|
|
122
|
+
file: only.file,
|
|
123
|
+
...positionOf(source, only.offset)
|
|
124
|
+
};
|
|
106
125
|
}
|
|
107
|
-
if (matches.length === 1) return matches[0];
|
|
108
126
|
}
|
|
109
127
|
}
|
|
128
|
+
/** 1-based line and column of a byte offset in a source file. */
|
|
129
|
+
function positionOf(source, offset) {
|
|
130
|
+
let line = 1;
|
|
131
|
+
let lineStart = 0;
|
|
132
|
+
for (let i = 0; i < offset; i += 1) if (source[i] === "\n") {
|
|
133
|
+
line += 1;
|
|
134
|
+
lineStart = i + 1;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
line,
|
|
138
|
+
column: offset - lineStart + 1
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** `components/Header.astro:12`, as both reports name a component. */
|
|
142
|
+
function componentPath(location) {
|
|
143
|
+
return `${location.file}:${location.line}`;
|
|
144
|
+
}
|
|
110
145
|
//#endregion
|
|
111
|
-
export {
|
|
146
|
+
export { searchTermsFor as i, componentFor as n, componentPath as r, buildComponentIndex as t };
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { d as successCriteria, n as DEFAULT_TAGS, r as ENGINE_BLIND_RULES } from "./result-BWcYXeRs.js";
|
|
2
|
+
import axe from "axe-core";
|
|
3
|
+
//#region src/audit/manual.ts
|
|
4
|
+
/**
|
|
5
|
+
* What a person has to check, and where to read about it.
|
|
6
|
+
*
|
|
7
|
+
* Automated testing finds a minority of accessibility barriers. This tool says
|
|
8
|
+
* so everywhere, and saying so is not much use on its own: a reader told that
|
|
9
|
+
* six rules "could not be evaluated" is left knowing there is a gap and not
|
|
10
|
+
* what to do about it.
|
|
11
|
+
*
|
|
12
|
+
* So each rule the browserless engine cannot decide carries the check somebody
|
|
13
|
+
* would do by hand, and each success criterion carries a link to what it
|
|
14
|
+
* actually requires. The point is to turn the honest disclaimer into the part
|
|
15
|
+
* of the report with the most work in it.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* WCAG 2.2 Understanding pages, by success criterion.
|
|
19
|
+
*
|
|
20
|
+
* The Understanding document rather than the criterion text: the text says what
|
|
21
|
+
* conformance is, and the Understanding page says what it is for and how to
|
|
22
|
+
* meet it, which is what somebody reading a report needs.
|
|
23
|
+
*
|
|
24
|
+
* Level A and AA only, since that is what this tool audits against.
|
|
25
|
+
*/
|
|
26
|
+
const UNDERSTANDING = {
|
|
27
|
+
"1.1.1": "non-text-content",
|
|
28
|
+
"1.2.1": "audio-only-and-video-only-prerecorded",
|
|
29
|
+
"1.2.2": "captions-prerecorded",
|
|
30
|
+
"1.2.3": "audio-description-or-media-alternative-prerecorded",
|
|
31
|
+
"1.2.4": "captions-live",
|
|
32
|
+
"1.2.5": "audio-description-prerecorded",
|
|
33
|
+
"1.3.1": "info-and-relationships",
|
|
34
|
+
"1.3.2": "meaningful-sequence",
|
|
35
|
+
"1.3.3": "sensory-characteristics",
|
|
36
|
+
"1.3.4": "orientation",
|
|
37
|
+
"1.3.5": "identify-input-purpose",
|
|
38
|
+
"1.4.1": "use-of-color",
|
|
39
|
+
"1.4.2": "audio-control",
|
|
40
|
+
"1.4.3": "contrast-minimum",
|
|
41
|
+
"1.4.4": "resize-text",
|
|
42
|
+
"1.4.5": "images-of-text",
|
|
43
|
+
"1.4.10": "reflow",
|
|
44
|
+
"1.4.11": "non-text-contrast",
|
|
45
|
+
"1.4.12": "text-spacing",
|
|
46
|
+
"1.4.13": "content-on-hover-or-focus",
|
|
47
|
+
"2.1.1": "keyboard",
|
|
48
|
+
"2.1.2": "no-keyboard-trap",
|
|
49
|
+
"2.1.4": "character-key-shortcuts",
|
|
50
|
+
"2.2.1": "timing-adjustable",
|
|
51
|
+
"2.2.2": "pause-stop-hide",
|
|
52
|
+
"2.3.1": "three-flashes-or-below-threshold",
|
|
53
|
+
"2.4.1": "bypass-blocks",
|
|
54
|
+
"2.4.2": "page-titled",
|
|
55
|
+
"2.4.3": "focus-order",
|
|
56
|
+
"2.4.4": "link-purpose-in-context",
|
|
57
|
+
"2.4.5": "multiple-ways",
|
|
58
|
+
"2.4.6": "headings-and-labels",
|
|
59
|
+
"2.4.7": "focus-visible",
|
|
60
|
+
"2.4.11": "focus-not-obscured-minimum",
|
|
61
|
+
"2.5.1": "pointer-gestures",
|
|
62
|
+
"2.5.2": "pointer-cancellation",
|
|
63
|
+
"2.5.3": "label-in-name",
|
|
64
|
+
"2.5.4": "motion-actuation",
|
|
65
|
+
"2.5.7": "dragging-movements",
|
|
66
|
+
"2.5.8": "target-size-minimum",
|
|
67
|
+
"3.1.1": "language-of-page",
|
|
68
|
+
"3.1.2": "language-of-parts",
|
|
69
|
+
"3.2.1": "on-focus",
|
|
70
|
+
"3.2.2": "on-input",
|
|
71
|
+
"3.2.3": "consistent-navigation",
|
|
72
|
+
"3.2.4": "consistent-identification",
|
|
73
|
+
"3.2.6": "consistent-help",
|
|
74
|
+
"3.3.1": "error-identification",
|
|
75
|
+
"3.3.2": "labels-or-instructions",
|
|
76
|
+
"3.3.3": "error-suggestion",
|
|
77
|
+
"3.3.4": "error-prevention-legal-financial-data",
|
|
78
|
+
"3.3.7": "redundant-entry",
|
|
79
|
+
"3.3.8": "accessible-authentication-minimum",
|
|
80
|
+
"4.1.2": "name-role-value",
|
|
81
|
+
"4.1.3": "status-messages"
|
|
82
|
+
};
|
|
83
|
+
/** Where to read what a success criterion requires, or undefined if unknown. */
|
|
84
|
+
function understandingUrl(criterion) {
|
|
85
|
+
const slug = UNDERSTANDING[criterion];
|
|
86
|
+
return slug === void 0 ? void 0 : `https://www.w3.org/WAI/WCAG22/Understanding/${slug}.html`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The check a person does for a rule this engine could not decide.
|
|
90
|
+
*
|
|
91
|
+
* Written as an action rather than a restatement of the criterion. "Ensure
|
|
92
|
+
* sufficient contrast" tells somebody nothing they did not already know; the
|
|
93
|
+
* useful sentence names what to open and what to look at.
|
|
94
|
+
*/
|
|
95
|
+
const MANUAL_CHECKS = {
|
|
96
|
+
"color-contrast": {
|
|
97
|
+
check: "Open the page and check text against its background with a contrast checker. Body text needs 4.5:1, and large or bold text 3:1. Check the states too — hover, focus, visited, disabled and placeholder text are the ones usually missed.",
|
|
98
|
+
browserAnswers: true
|
|
99
|
+
},
|
|
100
|
+
"color-contrast-enhanced": {
|
|
101
|
+
check: "Only needed if you are claiming AAA. Body text needs 7:1 and large text 4.5:1.",
|
|
102
|
+
browserAnswers: true
|
|
103
|
+
},
|
|
104
|
+
"target-size": {
|
|
105
|
+
check: "Measure the clickable area of buttons, icon links and form controls: 24×24 CSS pixels at minimum, unless they are inline in a sentence or have that much clear space around them. Icon-only controls in a header or a media player are where this usually fails.",
|
|
106
|
+
browserAnswers: true
|
|
107
|
+
},
|
|
108
|
+
"scrollable-region-focusable": {
|
|
109
|
+
check: "Find anything that scrolls inside the page — a code block, a wide table, a long list — and try reaching it with the Tab key alone. If it cannot take focus, a keyboard user cannot scroll it.",
|
|
110
|
+
browserAnswers: true
|
|
111
|
+
},
|
|
112
|
+
"link-in-text-block": {
|
|
113
|
+
check: "Look at links inside paragraphs. If the only thing distinguishing them from the surrounding text is colour, they need an underline or a 3:1 contrast difference against that text as well.",
|
|
114
|
+
browserAnswers: true
|
|
115
|
+
},
|
|
116
|
+
"no-autoplay-audio": {
|
|
117
|
+
check: "Load the page and listen. Anything that plays for more than three seconds on its own needs a pause or stop control, or a volume control independent of the system volume.",
|
|
118
|
+
browserAnswers: false
|
|
119
|
+
},
|
|
120
|
+
"avoid-inline-spacing": {
|
|
121
|
+
check: "Override line height to 1.5×, paragraph spacing to 2×, letter spacing to 0.12× and word spacing to 0.16× the font size, then check nothing is clipped or overlapping. Inline styles that set spacing with !important are what break this.",
|
|
122
|
+
browserAnswers: true
|
|
123
|
+
},
|
|
124
|
+
"p-as-heading": {
|
|
125
|
+
check: "Look for paragraphs styled to look like headings. A screen reader reads them as body text, so they are invisible to anyone navigating by heading.",
|
|
126
|
+
browserAnswers: false
|
|
127
|
+
},
|
|
128
|
+
"css-orientation-lock": {
|
|
129
|
+
check: "Rotate a phone or tablet. The content should work in both orientations unless one is essential, which is rare outside games and instruments.",
|
|
130
|
+
browserAnswers: false
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
/** The check for a rule, if there is one written for it. */
|
|
134
|
+
function manualCheckFor(ruleId) {
|
|
135
|
+
return MANUAL_CHECKS[ruleId];
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/audit/coverage.ts
|
|
139
|
+
/**
|
|
140
|
+
* WCAG 2.2 at Levels A and AA, which is what this tool audits against.
|
|
141
|
+
*
|
|
142
|
+
* 4.1.1 Parsing is deliberately absent: it was removed in WCAG 2.2 and counting
|
|
143
|
+
* it would inflate the denominator with a criterion nobody has to meet.
|
|
144
|
+
*/
|
|
145
|
+
const WCAG22_AA_CRITERIA = [
|
|
146
|
+
{
|
|
147
|
+
number: "1.1.1",
|
|
148
|
+
title: "Non-text Content",
|
|
149
|
+
level: "A"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
number: "1.2.1",
|
|
153
|
+
title: "Audio-only and Video-only (Prerecorded)",
|
|
154
|
+
level: "A"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
number: "1.2.2",
|
|
158
|
+
title: "Captions (Prerecorded)",
|
|
159
|
+
level: "A"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
number: "1.2.3",
|
|
163
|
+
title: "Audio Description or Media Alternative (Prerecorded)",
|
|
164
|
+
level: "A"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
number: "1.2.4",
|
|
168
|
+
title: "Captions (Live)",
|
|
169
|
+
level: "AA"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
number: "1.2.5",
|
|
173
|
+
title: "Audio Description (Prerecorded)",
|
|
174
|
+
level: "AA"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
number: "1.3.1",
|
|
178
|
+
title: "Info and Relationships",
|
|
179
|
+
level: "A"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
number: "1.3.2",
|
|
183
|
+
title: "Meaningful Sequence",
|
|
184
|
+
level: "A"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
number: "1.3.3",
|
|
188
|
+
title: "Sensory Characteristics",
|
|
189
|
+
level: "A"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
number: "1.3.4",
|
|
193
|
+
title: "Orientation",
|
|
194
|
+
level: "AA"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
number: "1.3.5",
|
|
198
|
+
title: "Identify Input Purpose",
|
|
199
|
+
level: "AA"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
number: "1.4.1",
|
|
203
|
+
title: "Use of Color",
|
|
204
|
+
level: "A"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
number: "1.4.2",
|
|
208
|
+
title: "Audio Control",
|
|
209
|
+
level: "A"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
number: "1.4.3",
|
|
213
|
+
title: "Contrast (Minimum)",
|
|
214
|
+
level: "AA"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
number: "1.4.4",
|
|
218
|
+
title: "Resize Text",
|
|
219
|
+
level: "AA"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
number: "1.4.5",
|
|
223
|
+
title: "Images of Text",
|
|
224
|
+
level: "AA"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
number: "1.4.10",
|
|
228
|
+
title: "Reflow",
|
|
229
|
+
level: "AA"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
number: "1.4.11",
|
|
233
|
+
title: "Non-text Contrast",
|
|
234
|
+
level: "AA"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
number: "1.4.12",
|
|
238
|
+
title: "Text Spacing",
|
|
239
|
+
level: "AA"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
number: "1.4.13",
|
|
243
|
+
title: "Content on Hover or Focus",
|
|
244
|
+
level: "AA"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
number: "2.1.1",
|
|
248
|
+
title: "Keyboard",
|
|
249
|
+
level: "A"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
number: "2.1.2",
|
|
253
|
+
title: "No Keyboard Trap",
|
|
254
|
+
level: "A"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
number: "2.1.4",
|
|
258
|
+
title: "Character Key Shortcuts",
|
|
259
|
+
level: "A"
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
number: "2.2.1",
|
|
263
|
+
title: "Timing Adjustable",
|
|
264
|
+
level: "A"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
number: "2.2.2",
|
|
268
|
+
title: "Pause, Stop, Hide",
|
|
269
|
+
level: "A"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
number: "2.3.1",
|
|
273
|
+
title: "Three Flashes or Below Threshold",
|
|
274
|
+
level: "A"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
number: "2.4.1",
|
|
278
|
+
title: "Bypass Blocks",
|
|
279
|
+
level: "A"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
number: "2.4.2",
|
|
283
|
+
title: "Page Titled",
|
|
284
|
+
level: "A"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
number: "2.4.3",
|
|
288
|
+
title: "Focus Order",
|
|
289
|
+
level: "A"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
number: "2.4.4",
|
|
293
|
+
title: "Link Purpose (In Context)",
|
|
294
|
+
level: "A"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
number: "2.4.5",
|
|
298
|
+
title: "Multiple Ways",
|
|
299
|
+
level: "AA"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
number: "2.4.6",
|
|
303
|
+
title: "Headings and Labels",
|
|
304
|
+
level: "AA"
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
number: "2.4.7",
|
|
308
|
+
title: "Focus Visible",
|
|
309
|
+
level: "AA"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
number: "2.4.11",
|
|
313
|
+
title: "Focus Not Obscured (Minimum)",
|
|
314
|
+
level: "AA"
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
number: "2.5.1",
|
|
318
|
+
title: "Pointer Gestures",
|
|
319
|
+
level: "A"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
number: "2.5.2",
|
|
323
|
+
title: "Pointer Cancellation",
|
|
324
|
+
level: "A"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
number: "2.5.3",
|
|
328
|
+
title: "Label in Name",
|
|
329
|
+
level: "A"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
number: "2.5.4",
|
|
333
|
+
title: "Motion Actuation",
|
|
334
|
+
level: "A"
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
number: "2.5.7",
|
|
338
|
+
title: "Dragging Movements",
|
|
339
|
+
level: "AA"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
number: "2.5.8",
|
|
343
|
+
title: "Target Size (Minimum)",
|
|
344
|
+
level: "AA"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
number: "3.1.1",
|
|
348
|
+
title: "Language of Page",
|
|
349
|
+
level: "A"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
number: "3.1.2",
|
|
353
|
+
title: "Language of Parts",
|
|
354
|
+
level: "AA"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
number: "3.2.1",
|
|
358
|
+
title: "On Focus",
|
|
359
|
+
level: "A"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
number: "3.2.2",
|
|
363
|
+
title: "On Input",
|
|
364
|
+
level: "A"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
number: "3.2.3",
|
|
368
|
+
title: "Consistent Navigation",
|
|
369
|
+
level: "AA"
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
number: "3.2.4",
|
|
373
|
+
title: "Consistent Identification",
|
|
374
|
+
level: "AA"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
number: "3.2.6",
|
|
378
|
+
title: "Consistent Help",
|
|
379
|
+
level: "A"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
number: "3.3.1",
|
|
383
|
+
title: "Error Identification",
|
|
384
|
+
level: "A"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
number: "3.3.2",
|
|
388
|
+
title: "Labels or Instructions",
|
|
389
|
+
level: "A"
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
number: "3.3.3",
|
|
393
|
+
title: "Error Suggestion",
|
|
394
|
+
level: "AA"
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
number: "3.3.4",
|
|
398
|
+
title: "Error Prevention (Legal, Financial, Data)",
|
|
399
|
+
level: "AA"
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
number: "3.3.7",
|
|
403
|
+
title: "Redundant Entry",
|
|
404
|
+
level: "A"
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
number: "3.3.8",
|
|
408
|
+
title: "Accessible Authentication (Minimum)",
|
|
409
|
+
level: "AA"
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
number: "4.1.2",
|
|
413
|
+
title: "Name, Role, Value",
|
|
414
|
+
level: "A"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
number: "4.1.3",
|
|
418
|
+
title: "Status Messages",
|
|
419
|
+
level: "AA"
|
|
420
|
+
}
|
|
421
|
+
];
|
|
422
|
+
/**
|
|
423
|
+
* Rules axe-core would run under these tags, mapped to the criteria they cover.
|
|
424
|
+
*
|
|
425
|
+
* Experimental rules are excluded, matching `blindRulesInScope`: axe-core leaves
|
|
426
|
+
* them off by default, so counting them as coverage would promise checks that
|
|
427
|
+
* never run.
|
|
428
|
+
*/
|
|
429
|
+
function rulesByCriterion(tags) {
|
|
430
|
+
const byCriterion = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const rule of axe.getRules([...tags])) {
|
|
432
|
+
if (rule.tags.includes("experimental")) continue;
|
|
433
|
+
for (const criterion of successCriteria(rule.tags)) {
|
|
434
|
+
const rules = byCriterion.get(criterion);
|
|
435
|
+
if (rules) rules.push(rule.ruleId);
|
|
436
|
+
else byCriterion.set(criterion, [rule.ruleId]);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
for (const rules of byCriterion.values()) rules.sort();
|
|
440
|
+
return byCriterion;
|
|
441
|
+
}
|
|
442
|
+
/** Rule ids this run reached a pass or a violation on, across every page. */
|
|
443
|
+
function decidedRules(audits) {
|
|
444
|
+
const decided = /* @__PURE__ */ new Set();
|
|
445
|
+
for (const audit of audits) {
|
|
446
|
+
for (const finding of audit.violations) decided.add(finding.ruleId);
|
|
447
|
+
for (const finding of audit.accepted ?? []) decided.add(finding.ruleId);
|
|
448
|
+
for (const outcome of audit.passes) decided.add(outcome.ruleId);
|
|
449
|
+
}
|
|
450
|
+
return decided;
|
|
451
|
+
}
|
|
452
|
+
/** Rule ids this run reached no verdict on because the engine cannot see them. */
|
|
453
|
+
function blindedRules(audits) {
|
|
454
|
+
const blind = /* @__PURE__ */ new Set();
|
|
455
|
+
for (const audit of audits) for (const finding of audit.incomplete) if (finding.reason === "engine-limitation") blind.add(finding.ruleId);
|
|
456
|
+
return blind;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* What this run reached, criterion by criterion.
|
|
460
|
+
*
|
|
461
|
+
* `audits` decides `evaluated` rather than the engine's capabilities alone: a
|
|
462
|
+
* rule that could have run and never matched anything has not established that
|
|
463
|
+
* the criterion is met on this site, and saying otherwise would be a coverage
|
|
464
|
+
* claim resting on an empty page.
|
|
465
|
+
*/
|
|
466
|
+
function buildCoverage(audits, tags = DEFAULT_TAGS) {
|
|
467
|
+
const byCriterion = rulesByCriterion(tags);
|
|
468
|
+
const decided = decidedRules(audits);
|
|
469
|
+
const blinded = blindedRules(audits);
|
|
470
|
+
const jsdomBlindApplies = audits.length === 0 || audits.some((audit) => audit.engine !== "browser");
|
|
471
|
+
const criteria = WCAG22_AA_CRITERIA.map((criterion) => {
|
|
472
|
+
const rules = byCriterion.get(criterion.number) ?? [];
|
|
473
|
+
if (rules.length === 0) return {
|
|
474
|
+
...criterion,
|
|
475
|
+
status: "no-automated-rule",
|
|
476
|
+
rules,
|
|
477
|
+
browserWouldAnswer: false
|
|
478
|
+
};
|
|
479
|
+
if (rules.some((rule) => decided.has(rule))) return {
|
|
480
|
+
...criterion,
|
|
481
|
+
status: "evaluated",
|
|
482
|
+
rules,
|
|
483
|
+
browserWouldAnswer: false
|
|
484
|
+
};
|
|
485
|
+
const engineBlind = rules.filter((rule) => blinded.has(rule) || jsdomBlindApplies && ENGINE_BLIND_RULES[rule] !== void 0);
|
|
486
|
+
if (engineBlind.length > 0) return {
|
|
487
|
+
...criterion,
|
|
488
|
+
status: "not-evaluated",
|
|
489
|
+
rules,
|
|
490
|
+
browserWouldAnswer: engineBlind.every((rule) => manualCheckFor(rule)?.browserAnswers === true)
|
|
491
|
+
};
|
|
492
|
+
return {
|
|
493
|
+
...criterion,
|
|
494
|
+
status: "nothing-to-check",
|
|
495
|
+
rules,
|
|
496
|
+
browserWouldAnswer: false
|
|
497
|
+
};
|
|
498
|
+
});
|
|
499
|
+
const count = (status) => criteria.filter((criterion) => criterion.status === status).length;
|
|
500
|
+
return {
|
|
501
|
+
criteria,
|
|
502
|
+
evaluated: count("evaluated"),
|
|
503
|
+
notEvaluated: count("not-evaluated"),
|
|
504
|
+
nothingToCheck: count("nothing-to-check"),
|
|
505
|
+
noAutomatedRule: count("no-automated-rule"),
|
|
506
|
+
browserWouldAnswer: criteria.filter((criterion) => criterion.browserWouldAnswer).length,
|
|
507
|
+
total: WCAG22_AA_CRITERIA.length
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* The sentence a reader needs before drawing anything from a clean report.
|
|
512
|
+
*
|
|
513
|
+
* Deliberately leads with what was not checked. A report that opened with "23
|
|
514
|
+
* criteria evaluated" would read as a score, which is precisely what this
|
|
515
|
+
* refuses to be.
|
|
516
|
+
*/
|
|
517
|
+
function coverageSummary(coverage) {
|
|
518
|
+
const unautomatable = coverage.noAutomatedRule;
|
|
519
|
+
return `Of the ${coverage.total} WCAG 2.2 A and AA success criteria, ${unautomatable} cannot be checked by any automated engine and need a person. This run reached a verdict on ${coverage.evaluated}.`;
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
522
|
+
export { understandingUrl as i, coverageSummary as n, manualCheckFor as r, buildCoverage as t };
|