eaa-kit 0.5.0 → 0.7.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 +53 -5
- package/dist/astro/index.d.ts +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/audit/runners/worker.js +1 -1
- package/dist/audit-Bwm1xBdJ.js +2 -0
- package/dist/{audit-B282A3EA.js → audit-DRjAY2iY.js} +243 -107
- package/dist/{baseline-CuAS2lMk.js → baseline-BpE7DbDq.js} +36 -9
- package/dist/baseline-rtnj3RHB.js +2 -0
- package/dist/cache-D4dOIuOi.js +219 -0
- package/dist/checklist-DDd9jewi.js +44 -0
- package/dist/cli/index.js +130 -17
- package/dist/{command-C3D7JWn6.js → command-3r4fht5f.js} +102 -10
- package/dist/{component-C3GL1Mnu.js → component-ChisoFyY.js} +1 -1
- package/dist/{component-DKd3EHOg.js → component-DcCn3BJS.js} +1 -1
- package/dist/{coverage-B_Y6l-Ra.js → coverage-cb5kPp48.js} +45 -290
- package/dist/coverage-n1X9CkiG.js +3 -0
- package/dist/{crawl-BUWPgaGW.js → crawl-BHkJmUkA.js} +42 -8
- package/dist/criteria-CnPJKGeb.js +286 -0
- package/dist/eleventy/index.d.ts +1 -1
- package/dist/eleventy/index.js +1 -1
- package/dist/{html-C4vktg0A.js → html-BIxxF3FC.js} +54 -30
- package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
- package/dist/index.d.ts +87 -1
- package/dist/index.js +3 -3
- package/dist/{init-CW7LfGT5.js → init-DrFfzjvm.js} +3 -3
- package/dist/jsdom-BUYuVJJm.js +3 -0
- package/dist/{jsdom-B--cEH-G.js → jsdom-CsW_xVcA.js} +11 -12
- package/dist/{json-DROX33kh.js → json-Bus6YhxB.js} +13 -17
- package/dist/{json-D_Mnnft5.js → json-CrQmoTj6.js} +1 -1
- package/dist/{load-yAR4wzez.js → load-BaWMa-G_.js} +11 -2
- package/dist/load-n62iXDq_.js +2 -0
- package/dist/nuxt/index.d.ts +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/{playwright-BojtYVUa.js → playwright-ukCr_5o3.js} +3 -2
- package/dist/{pool-BO25OIez.js → pool-B4tEUa2S.js} +6 -2
- package/dist/{remediation-CMBIrnpN.js → remediation-C0klJblu.js} +35 -4
- package/dist/{render-DbGOVmhx.js → render-C1sUZZI_.js} +145 -6
- package/dist/{result-BWcYXeRs.js → result-B7_zVxAX.js} +24 -1
- package/dist/result-CRmjjoAA.js +2 -0
- package/dist/review-CdMK2GpQ.js +161 -0
- package/dist/review-DIbOTmOM.js +2 -0
- package/dist/{run-CtcEUhbe.d.ts → run-BK2blxMI.d.ts} +18 -0
- package/dist/{run-C2nKFcb-.js → run-DrS0Komf.js} +6 -2
- package/dist/{sarif-B-UBcVu8.js → sarif-BNCE0PNT.js} +24 -12
- package/dist/statement/templates/at.de.md +14 -0
- package/dist/statement/templates/at.en.md +14 -0
- package/dist/statement/templates/ch.de.md +14 -0
- package/dist/statement/templates/ch.en.md +14 -0
- package/dist/statement/templates/de.de.md +14 -0
- package/dist/statement/templates/de.en.md +14 -0
- package/dist/statement/templates/es.en.md +14 -0
- package/dist/statement/templates/es.es.md +15 -0
- package/dist/statement/templates/fr.en.md +14 -0
- package/dist/statement/templates/fr.fr.md +15 -0
- package/dist/statement/templates/it.en.md +14 -0
- package/dist/statement/templates/it.it.md +15 -0
- package/dist/statement/templates/nl.en.md +14 -0
- package/dist/statement/templates/nl.nl.md +15 -0
- package/dist/{text-CKKpzkYM.js → text-BF1LHMiV.js} +22 -2
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/webpack/index.d.ts +1 -1
- package/dist/webpack/index.js +1 -1
- package/dist/worksheet-BIxMtbUd.js +64 -0
- package/package.json +3 -2
- package/dist/audit-DyHPmGeD.js +0 -2
- package/dist/baseline-0u4df12v.js +0 -2
- package/dist/jsdom-22Bkt65v.js +0 -3
- package/dist/load-CFq2VQtT.js +0 -2
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import pc from "picocolors";
|
|
4
|
+
//#region src/audit/headers.ts
|
|
5
|
+
/**
|
|
6
|
+
* Request headers for the pages this tool fetches.
|
|
7
|
+
*
|
|
8
|
+
* The two places a small agency stages work are the two this tool could not
|
|
9
|
+
* reach: a preview deployment, which every host protects by default, and a CMS
|
|
10
|
+
* staging site behind basic auth or a session cookie. `--url` sent one fixed
|
|
11
|
+
* pair of headers and had no way to add to it, so auditing either meant putting
|
|
12
|
+
* the site on the public internet first.
|
|
13
|
+
*
|
|
14
|
+
* These values are credentials. They are handled on the principle that the tool
|
|
15
|
+
* never writes one down: nothing here reaches a report, a baseline, a SARIF log
|
|
16
|
+
* or the completeness record, and the errors below name the header rather than
|
|
17
|
+
* echoing what was typed, because a malformed `--header` is exactly the case
|
|
18
|
+
* where the value is a token and the terminal is a CI log.
|
|
19
|
+
*
|
|
20
|
+
* For the same reason there is no `headers` key in `eaa.config`. That file is
|
|
21
|
+
* committed; a token in it is a token in the repository, and a tool that
|
|
22
|
+
* offered the field would be inviting that. Pass them as flags — a shell
|
|
23
|
+
* expands `--header "Authorization: Bearer $TOKEN"` — or as the GitHub Action's
|
|
24
|
+
* input, which reads from `secrets`.
|
|
25
|
+
*/
|
|
26
|
+
/** Thrown for a header this cannot parse. Never carries the value. */
|
|
27
|
+
var HeaderError = class extends Error {
|
|
28
|
+
name = "HeaderError";
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* `Name: value` into a pair.
|
|
32
|
+
*
|
|
33
|
+
* Lenient about the space after the colon and about a value containing colons,
|
|
34
|
+
* which `Authorization: Basic …` and a `Cookie` both do. Strict about the name,
|
|
35
|
+
* because a header name is a token by RFC 9110 and anything else is a typo that
|
|
36
|
+
* a server would reject in a way nobody could read.
|
|
37
|
+
*/
|
|
38
|
+
function parseHeader(input) {
|
|
39
|
+
const at = input.indexOf(":");
|
|
40
|
+
if (at <= 0) throw new HeaderError("a header must be written \"Name: value\"");
|
|
41
|
+
const name = input.slice(0, at).trim();
|
|
42
|
+
const value = input.slice(at + 1).trim();
|
|
43
|
+
if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name)) throw new HeaderError(`"${name}" is not a valid header name`);
|
|
44
|
+
if (value === "") throw new HeaderError(`the ${name} header was given no value`);
|
|
45
|
+
if (/[\r\n]/.test(value)) throw new HeaderError(`the ${name} header contains a line break`);
|
|
46
|
+
return [name, value];
|
|
47
|
+
}
|
|
48
|
+
/** Several `Name: value` strings into the record the fetchers take. */
|
|
49
|
+
function parseHeaders(inputs) {
|
|
50
|
+
const headers = {};
|
|
51
|
+
for (const input of inputs) {
|
|
52
|
+
const [name, value] = parseHeader(input);
|
|
53
|
+
headers[name] = value;
|
|
54
|
+
}
|
|
55
|
+
return headers;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* `user:password` into an `Authorization` header.
|
|
59
|
+
*
|
|
60
|
+
* Sugar for the header somebody would otherwise have to base64 by hand, which
|
|
61
|
+
* is the one credential a staging site is most likely to be behind. A password
|
|
62
|
+
* may contain colons; a username may not, which is basic auth's own rule.
|
|
63
|
+
*/
|
|
64
|
+
function basicAuth(input) {
|
|
65
|
+
if (input.indexOf(":") <= 0) throw new HeaderError("basic auth must be written \"user:password\"");
|
|
66
|
+
return ["Authorization", `Basic ${Buffer.from(input, "utf8").toString("base64")}`];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The headers a run will send, from whichever flags were given.
|
|
70
|
+
*
|
|
71
|
+
* `--basic-auth` first, so an explicit `--header "Authorization: …"` beats it:
|
|
72
|
+
* a typed header is the more specific thing to have asked for.
|
|
73
|
+
*/
|
|
74
|
+
function requestHeaders(options) {
|
|
75
|
+
const headers = {};
|
|
76
|
+
if (options.basicAuth !== void 0) {
|
|
77
|
+
const [name, value] = basicAuth(options.basicAuth);
|
|
78
|
+
headers[name] = value;
|
|
79
|
+
}
|
|
80
|
+
Object.assign(headers, parseHeaders(options.header ?? []));
|
|
81
|
+
return Object.keys(headers).length === 0 ? void 0 : headers;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
4
84
|
//#region src/cli/command.ts
|
|
5
85
|
/**
|
|
6
86
|
* What every command does around the audit itself: say what is happening, run
|
|
@@ -42,18 +122,19 @@ async function runEngine(pages, options) {
|
|
|
42
122
|
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
43
123
|
};
|
|
44
124
|
if (!options.browser) {
|
|
45
|
-
const { runPooledAudit } = await import("./pool-
|
|
125
|
+
const { runPooledAudit } = await import("./pool-B4tEUa2S.js");
|
|
46
126
|
return runPooledAudit(pages, {
|
|
47
127
|
...runnerOptions,
|
|
48
128
|
...options.fast ? { fast: true } : {},
|
|
49
129
|
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
|
|
50
130
|
});
|
|
51
131
|
}
|
|
52
|
-
const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-
|
|
132
|
+
const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-ukCr_5o3.js");
|
|
53
133
|
try {
|
|
54
134
|
return await runBrowserAudit(options.directory, pages, {
|
|
55
135
|
...runnerOptions,
|
|
56
|
-
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
|
|
136
|
+
...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
|
|
137
|
+
...options.headers === void 0 ? {} : { headers: options.headers }
|
|
57
138
|
});
|
|
58
139
|
} catch (cause) {
|
|
59
140
|
if (cause instanceof BrowserUnavailableError) {
|
|
@@ -95,7 +176,7 @@ async function emitDocument(body, output, cwd) {
|
|
|
95
176
|
* on different settings than it names would be worse than stopping.
|
|
96
177
|
*/
|
|
97
178
|
async function auditDefaults(options = {}) {
|
|
98
|
-
const { loadAuditConfig } = await import("./load-
|
|
179
|
+
const { loadAuditConfig } = await import("./load-n62iXDq_.js");
|
|
99
180
|
const loaded = await loadAuditConfig({
|
|
100
181
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
101
182
|
...options.config ? { path: options.config } : {}
|
|
@@ -105,14 +186,20 @@ async function auditDefaults(options = {}) {
|
|
|
105
186
|
return loaded.audit;
|
|
106
187
|
}
|
|
107
188
|
function auditInvocation(dir, defaults, flags) {
|
|
108
|
-
const { build, config: _config, ...typed } = flags;
|
|
109
|
-
const { dir: configDir, build: configBuild, ...fromConfig } = defaults;
|
|
189
|
+
const { build, cache, config: _config, header, basicAuth, ...typed } = flags;
|
|
190
|
+
const { dir: configDir, build: configBuild, cache: configCache, headers: _fromFile, ...fromConfig } = defaults;
|
|
191
|
+
const headers = requestHeaders({
|
|
192
|
+
...header ? { header } : {},
|
|
193
|
+
...basicAuth ? { basicAuth } : {}
|
|
194
|
+
});
|
|
110
195
|
return {
|
|
111
196
|
dir: dir ?? configDir,
|
|
112
197
|
options: {
|
|
113
198
|
...fromConfig,
|
|
114
199
|
...typed,
|
|
115
|
-
...build === false || configBuild === false ? { noBuild: true } : {}
|
|
200
|
+
...build === false || configBuild === false ? { noBuild: true } : {},
|
|
201
|
+
...cache === false || configCache === false ? { noCache: true } : {},
|
|
202
|
+
...headers === void 0 ? {} : { headers }
|
|
116
203
|
}
|
|
117
204
|
};
|
|
118
205
|
}
|
|
@@ -126,12 +213,17 @@ function auditInvocation(dir, defaults, flags) {
|
|
|
126
213
|
* and `baseline` all describe a verdict this command does not reach.
|
|
127
214
|
*/
|
|
128
215
|
function baselineInvocation(dir, defaults, flags) {
|
|
129
|
-
const { config: _config, ...typed } = flags;
|
|
216
|
+
const { config: _config, header, basicAuth, ...typed } = flags;
|
|
217
|
+
const headers = requestHeaders({
|
|
218
|
+
...header ? { header } : {},
|
|
219
|
+
...basicAuth ? { basicAuth } : {}
|
|
220
|
+
});
|
|
130
221
|
return {
|
|
131
222
|
dir: dir ?? defaults.dir ?? "./dist",
|
|
132
223
|
options: {
|
|
133
224
|
...baselineDefaults(defaults),
|
|
134
|
-
...typed
|
|
225
|
+
...typed,
|
|
226
|
+
...headers === void 0 ? {} : { headers }
|
|
135
227
|
}
|
|
136
228
|
};
|
|
137
229
|
}
|
|
@@ -157,4 +249,4 @@ function pick(source, keys) {
|
|
|
157
249
|
return out;
|
|
158
250
|
}
|
|
159
251
|
//#endregion
|
|
160
|
-
export { emitDocument as a, runEngine as c, baselineInvocation as i, warn as l, auditDefaults as n, fail as o, auditInvocation as r, note as s, advise as t };
|
|
252
|
+
export { emitDocument as a, runEngine as c, parseHeader as d, baselineInvocation as i, warn as l, auditDefaults as n, fail as o, auditInvocation as r, note as s, advise as t, basicAuth as u };
|
|
@@ -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-BF1LHMiV.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { glob } from "tinyglobby";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as componentFor, t as buildComponentIndex } from "./component-
|
|
1
|
+
import { n as componentFor, t as buildComponentIndex } from "./component-ChisoFyY.js";
|
|
2
2
|
export { buildComponentIndex, componentFor };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as WCAG22_AA_CRITERIA } from "./criteria-CnPJKGeb.js";
|
|
2
|
+
import { n as DEFAULT_TAGS, p as successCriteria, r as ENGINE_BLIND_RULES } from "./result-B7_zVxAX.js";
|
|
3
|
+
import { s as criterionReview } from "./review-CdMK2GpQ.js";
|
|
2
4
|
import axe from "axe-core";
|
|
3
5
|
//#region src/audit/manual.ts
|
|
4
6
|
/**
|
|
@@ -137,289 +139,6 @@ function manualCheckFor(ruleId) {
|
|
|
137
139
|
//#endregion
|
|
138
140
|
//#region src/audit/coverage.ts
|
|
139
141
|
/**
|
|
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
142
|
* Rules axe-core would run under these tags, mapped to the criteria they cover.
|
|
424
143
|
*
|
|
425
144
|
* Experimental rules are excluded, matching `blindRulesInScope`: axe-core leaves
|
|
@@ -463,7 +182,7 @@ function blindedRules(audits) {
|
|
|
463
182
|
* the criterion is met on this site, and saying otherwise would be a coverage
|
|
464
183
|
* claim resting on an empty page.
|
|
465
184
|
*/
|
|
466
|
-
function buildCoverage(audits, tags = DEFAULT_TAGS) {
|
|
185
|
+
function buildCoverage(audits, tags = DEFAULT_TAGS, review) {
|
|
467
186
|
const byCriterion = rulesByCriterion(tags);
|
|
468
187
|
const decided = decidedRules(audits);
|
|
469
188
|
const blinded = blindedRules(audits);
|
|
@@ -496,15 +215,36 @@ function buildCoverage(audits, tags = DEFAULT_TAGS) {
|
|
|
496
215
|
browserWouldAnswer: false
|
|
497
216
|
};
|
|
498
217
|
});
|
|
499
|
-
const
|
|
218
|
+
const reviewed = review === void 0 ? criteria : criteria.map(withReview(review));
|
|
219
|
+
const count = (status) => reviewed.filter((criterion) => criterion.status === status).length;
|
|
220
|
+
const counted = reviewed.filter((criterion) => criterion.review?.counts === true);
|
|
500
221
|
return {
|
|
501
|
-
criteria,
|
|
222
|
+
criteria: reviewed,
|
|
502
223
|
evaluated: count("evaluated"),
|
|
503
224
|
notEvaluated: count("not-evaluated"),
|
|
504
225
|
nothingToCheck: count("nothing-to-check"),
|
|
505
226
|
noAutomatedRule: count("no-automated-rule"),
|
|
506
|
-
browserWouldAnswer:
|
|
507
|
-
total: WCAG22_AA_CRITERIA.length
|
|
227
|
+
browserWouldAnswer: reviewed.filter((criterion) => criterion.browserWouldAnswer).length,
|
|
228
|
+
total: WCAG22_AA_CRITERIA.length,
|
|
229
|
+
reviewed: counted.length,
|
|
230
|
+
reviewedNotMet: counted.filter((criterion) => criterion.review?.result === "not-met").length,
|
|
231
|
+
reviewNotCounted: reviewed.filter((criterion) => criterion.review?.counts === false).length
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Attach what a person recorded, without letting it move what the engine found.
|
|
236
|
+
*
|
|
237
|
+
* `status` is untouched here, deliberately and in every branch: a review adds a
|
|
238
|
+
* second kind of claim beside the run's own, and the moment it could rewrite
|
|
239
|
+
* one the report would stop being a record of what was measured.
|
|
240
|
+
*/
|
|
241
|
+
function withReview(review) {
|
|
242
|
+
return (criterion) => {
|
|
243
|
+
const recorded = criterionReview(criterion.number, review, criterion.status === "evaluated");
|
|
244
|
+
return recorded === void 0 ? criterion : {
|
|
245
|
+
...criterion,
|
|
246
|
+
review: recorded
|
|
247
|
+
};
|
|
508
248
|
};
|
|
509
249
|
}
|
|
510
250
|
/**
|
|
@@ -518,5 +258,20 @@ function coverageSummary(coverage) {
|
|
|
518
258
|
const unautomatable = coverage.noAutomatedRule;
|
|
519
259
|
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
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* What a person added to the run, in one sentence, or nothing when nobody did.
|
|
263
|
+
*
|
|
264
|
+
* Kept out of `coverageSummary` so the sentence about the engine's reach does
|
|
265
|
+
* not change shape depending on whether a review was supplied — and because a
|
|
266
|
+
* reader has to be able to tell the two claims apart at a glance: one is what
|
|
267
|
+
* an engine measured, the other is what somebody says they checked.
|
|
268
|
+
*/
|
|
269
|
+
function reviewSummary(coverage) {
|
|
270
|
+
if (coverage.reviewed === 0 && coverage.reviewNotCounted === 0) return void 0;
|
|
271
|
+
const parts = [`A person recorded a result for ${coverage.reviewed} of the criteria this run did not reach`];
|
|
272
|
+
if (coverage.reviewedNotMet > 0) parts.push(`${coverage.reviewedNotMet} of them as not met`);
|
|
273
|
+
if (coverage.reviewNotCounted > 0) parts.push(`${coverage.reviewNotCounted} further entries were not counted`);
|
|
274
|
+
return `${parts.join("; ")}. This is a claim by a person, not a measurement.`;
|
|
275
|
+
}
|
|
521
276
|
//#endregion
|
|
522
|
-
export {
|
|
277
|
+
export { manualCheckFor as a, rulesByCriterion as i, coverageSummary as n, understandingUrl as o, reviewSummary as r, buildCoverage as t };
|
|
@@ -93,7 +93,7 @@ function urlsFromSitemap(xml, origin) {
|
|
|
93
93
|
return urls;
|
|
94
94
|
}
|
|
95
95
|
/** One request, with a timeout, returning HTML or a reason it is not a page. */
|
|
96
|
-
async function fetchPage(url, impl, timeoutMs, origin, maxBodyBytes) {
|
|
96
|
+
async function fetchPage(url, impl, timeoutMs, origin, maxBodyBytes, headers) {
|
|
97
97
|
const controller = new AbortController();
|
|
98
98
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
99
99
|
try {
|
|
@@ -102,7 +102,8 @@ async function fetchPage(url, impl, timeoutMs, origin, maxBodyBytes) {
|
|
|
102
102
|
redirect: "follow",
|
|
103
103
|
headers: {
|
|
104
104
|
accept: "text/html,application/xhtml+xml",
|
|
105
|
-
"user-agent": "eaa-kit"
|
|
105
|
+
"user-agent": "eaa-kit",
|
|
106
|
+
...headers
|
|
106
107
|
}
|
|
107
108
|
});
|
|
108
109
|
if (!response.ok) return {
|
|
@@ -213,13 +214,14 @@ function disallowedPaths(robots) {
|
|
|
213
214
|
* that is not a page. A site that does not publish one is the ordinary case,
|
|
214
215
|
* not a failure, so it comes back empty either way.
|
|
215
216
|
*/
|
|
216
|
-
async function fetchSiteFile(entry, impl, name, timeoutMs, maxBodyBytes) {
|
|
217
|
+
async function fetchSiteFile(entry, impl, name, timeoutMs, maxBodyBytes, headers) {
|
|
217
218
|
const controller = new AbortController();
|
|
218
219
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
219
220
|
try {
|
|
220
221
|
const response = await impl(new URL(name, entry).href, {
|
|
221
222
|
redirect: "follow",
|
|
222
|
-
signal: controller.signal
|
|
223
|
+
signal: controller.signal,
|
|
224
|
+
...headers === void 0 ? {} : { headers }
|
|
223
225
|
});
|
|
224
226
|
if (!response.ok) return void 0;
|
|
225
227
|
if (new URL(response.url || new URL(name, entry).href).origin !== entry.origin) return void 0;
|
|
@@ -244,7 +246,7 @@ async function crawlSite(entry, options = {}) {
|
|
|
244
246
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
245
247
|
const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
246
248
|
const failures = [];
|
|
247
|
-
const robots = options.ignoreRobots ? void 0 : await fetchSiteFile(entry, impl, "/robots.txt", timeoutMs, maxBodyBytes);
|
|
249
|
+
const robots = options.ignoreRobots ? void 0 : await fetchSiteFile(entry, impl, "/robots.txt", timeoutMs, maxBodyBytes, options.headers);
|
|
248
250
|
const blocked = robots === void 0 ? [] : disallowedPaths(robots);
|
|
249
251
|
const allowed = (url) => !blocked.some((path) => url.pathname.startsWith(path));
|
|
250
252
|
let discovery = "links";
|
|
@@ -258,7 +260,7 @@ async function crawlSite(entry, options = {}) {
|
|
|
258
260
|
depth
|
|
259
261
|
});
|
|
260
262
|
};
|
|
261
|
-
const sitemap = await fetchSiteFile(entry, impl, options.sitemap ?? "/sitemap.xml", timeoutMs, maxBodyBytes);
|
|
263
|
+
const sitemap = await fetchSiteFile(entry, impl, options.sitemap ?? "/sitemap.xml", timeoutMs, maxBodyBytes, options.headers);
|
|
262
264
|
const listed = sitemap === void 0 ? [] : urlsFromSitemap(sitemap, entry);
|
|
263
265
|
if (listed.length > 0) {
|
|
264
266
|
discovery = "sitemap";
|
|
@@ -266,11 +268,12 @@ async function crawlSite(entry, options = {}) {
|
|
|
266
268
|
}
|
|
267
269
|
enqueue(entry, 0);
|
|
268
270
|
const pages = [];
|
|
271
|
+
const redirects = [];
|
|
269
272
|
while (queue.length > 0 && pages.length < maxPages) {
|
|
270
273
|
const batch = queue.splice(0, Math.min(REQUEST_CONCURRENCY, maxPages - pages.length));
|
|
271
274
|
const results = await Promise.all(batch.map(async (item) => ({
|
|
272
275
|
item,
|
|
273
|
-
result: await fetchPage(item.url, impl, timeoutMs, entry.origin, maxBodyBytes)
|
|
276
|
+
result: await fetchPage(item.url, impl, timeoutMs, entry.origin, maxBodyBytes, options.headers)
|
|
274
277
|
})));
|
|
275
278
|
for (const { item, result } of results) {
|
|
276
279
|
if (!result.ok) {
|
|
@@ -281,6 +284,10 @@ async function crawlSite(entry, options = {}) {
|
|
|
281
284
|
continue;
|
|
282
285
|
}
|
|
283
286
|
const { url, html } = result.value;
|
|
287
|
+
if (url.href !== item.url.href) redirects.push({
|
|
288
|
+
requested: item.url.href,
|
|
289
|
+
landedOn: url.href
|
|
290
|
+
});
|
|
284
291
|
pages.push({
|
|
285
292
|
absolutePath: url.href,
|
|
286
293
|
relativePath: pageIdentity(url),
|
|
@@ -294,11 +301,38 @@ async function crawlSite(entry, options = {}) {
|
|
|
294
301
|
pages: byIdentity(pages),
|
|
295
302
|
origin: entry.origin,
|
|
296
303
|
failures,
|
|
304
|
+
redirects,
|
|
297
305
|
truncated: queue.length > 0,
|
|
298
306
|
discovery
|
|
299
307
|
};
|
|
300
308
|
}
|
|
301
309
|
/**
|
|
310
|
+
* The requested URLs that a login wall swallowed, or none.
|
|
311
|
+
*
|
|
312
|
+
* A redirect on its own says nothing: sites normalise trailing slashes and send
|
|
313
|
+
* `/` to `/en/` all day. What is not ordinary is *several* different URLs
|
|
314
|
+
* answering at one address, or a whole crawl coming back as the single page the
|
|
315
|
+
* entry was redirected to. Both are the shape of a sign-in page standing in
|
|
316
|
+
* front of the site, and both otherwise produce a report about a page nobody
|
|
317
|
+
* asked for that says it audited everything it set out to.
|
|
318
|
+
*
|
|
319
|
+
* Returns what was asked for and never reached, which is the one thing the run
|
|
320
|
+
* has to say out loud. Deliberately shy of naming the cause: a single-page site
|
|
321
|
+
* that redirects its entry looks identical from here, and being told where the
|
|
322
|
+
* run actually landed is useful either way.
|
|
323
|
+
*/
|
|
324
|
+
function collapsedOnto(result) {
|
|
325
|
+
if (result.redirects.length === 0) return [];
|
|
326
|
+
const byDestination = /* @__PURE__ */ new Map();
|
|
327
|
+
for (const redirect of result.redirects) byDestination.set(redirect.landedOn, [...byDestination.get(redirect.landedOn) ?? [], redirect.requested]);
|
|
328
|
+
const collapsed = [];
|
|
329
|
+
for (const [landedOn, requested] of byDestination) if (requested.length > 1 || result.pages.length === 1) for (const from of requested) collapsed.push({
|
|
330
|
+
requested: from,
|
|
331
|
+
landedOn
|
|
332
|
+
});
|
|
333
|
+
return collapsed;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
302
336
|
* One page per identity, in a stable order.
|
|
303
337
|
*
|
|
304
338
|
* The queue already refuses a URL it has seen, so this catches the case the
|
|
@@ -315,4 +349,4 @@ function byIdentity(pages) {
|
|
|
315
349
|
}).sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
316
350
|
}
|
|
317
351
|
//#endregion
|
|
318
|
-
export { CrawlError, crawlSite, parseEntryUrl };
|
|
352
|
+
export { CrawlError, collapsedOnto, crawlSite, parseEntryUrl };
|