eaa-kit 0.4.0 → 0.6.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 +66 -11
- 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-DK2StT1_.js +2 -0
- package/dist/{audit-CPoZMXGM.js → audit-e_uixhPG.js} +123 -54
- package/dist/baseline-BscHfFSW.js +2 -0
- package/dist/{baseline-DB9CZGnV.js → baseline-D2qWmUC6.js} +40 -12
- package/dist/checklist-BtgkxMEa.js +44 -0
- package/dist/cli/index.js +153 -25
- package/dist/{collect-CFM8gEVv.js → collect-Cu1q9NTB.js} +34 -3
- package/dist/command-CZKp9Wi_.js +251 -0
- package/dist/{remediation-Dtowi2EC.js → completeness-Dwq3CPB3.js} +61 -43
- package/dist/{component-C3GL1Mnu.js → component-ChisoFyY.js} +1 -1
- package/dist/{component-DKd3EHOg.js → component-DcCn3BJS.js} +1 -1
- package/dist/coverage-7bQqWNfN.js +2 -0
- package/dist/{coverage-B4IBKMO8.js → coverage-BD2_CYns.js} +46 -8
- package/dist/{crawl-BiI1Lau_.js → crawl-BHkJmUkA.js} +119 -11
- package/dist/eleventy/index.d.ts +1 -1
- package/dist/eleventy/index.js +1 -1
- package/dist/fingerprint-BjYV_0F7.js +67 -0
- package/dist/{html-BGTO3ypW.js → html-Bmh7X_sM.js} +52 -30
- package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
- package/dist/index.d.ts +187 -5
- package/dist/index.js +3 -3
- package/dist/{init-DIWDE35F.js → init-BjKYttxc.js} +10 -4
- package/dist/{jsdom-DCpGSLfW.js → jsdom-CQar6OQS.js} +12 -4
- package/dist/jsdom-DMvU2h-f.js +3 -0
- package/dist/{json-QQuFIw1W.js → json-HJDgJp09.js} +1 -1
- package/dist/{json-DjEvy1nX.js → json-PNg6BeXp.js} +13 -18
- package/dist/load-Bc_xDGAh.js +2 -0
- package/dist/{load-5wRGLvub.js → load-vZdV_fD1.js} +137 -8
- package/dist/nuxt/index.d.ts +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/{playwright-BWniOain.js → playwright-C-miJP2b.js} +32 -7
- package/dist/{pool-BMevaLWD.js → pool-dkexq5xo.js} +52 -2
- package/dist/{render-DrvXRCEn.js → render-CtsqR0k-.js} +150 -11
- package/dist/{result-DoamKFsp.js → result-DfUru6y_.js} +60 -3
- package/dist/review-CdMK2GpQ.js +161 -0
- package/dist/review-DIbOTmOM.js +2 -0
- package/dist/{run-DB34BSOZ.js → run-D7hdVXLA.js} +1 -1
- package/dist/{run-BMASMmwO.d.ts → run-YiqyQA2b.d.ts} +17 -0
- package/dist/{sarif-SR3_lLYd.js → sarif-BKvq_AUm.js} +25 -14
- package/dist/{schema-is6CGX2D.js → schema-DJSF4K05.js} +15 -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-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-eUjwYLxC.js +63 -0
- package/package.json +7 -3
- package/dist/audit-CpXH2Mk8.js +0 -2
- package/dist/baseline-22Y1NWxM.js +0 -2
- package/dist/command-D8l_oYbV.js +0 -77
- package/dist/fingerprint-DRoneAjj.js +0 -20
- package/dist/jsdom-4IMzv0eE.js +0 -3
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as stripBom } from "./collect-Cu1q9NTB.js";
|
|
2
2
|
/** Requests in flight at once. Politeness, not throughput. */
|
|
3
3
|
const REQUEST_CONCURRENCY = 4;
|
|
4
|
+
/**
|
|
5
|
+
* Largest response body this will read, in bytes.
|
|
6
|
+
*
|
|
7
|
+
* A crawl reads whatever the server sends, and nothing obliges a server to send
|
|
8
|
+
* something reasonable — a misconfigured export endpoint, a log streamed as
|
|
9
|
+
* text/html, or a host that simply does not stop. `response.text()` buffers all
|
|
10
|
+
* of it before anyone can object, so the ceiling has to be applied while the
|
|
11
|
+
* body is still arriving.
|
|
12
|
+
*
|
|
13
|
+
* Matches the on-disk page limit: the two paths audit the same kind of
|
|
14
|
+
* document, and a page that would be declined off disk should not be accepted
|
|
15
|
+
* because it arrived over HTTP instead.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_BODY_BYTES = 33554432;
|
|
4
18
|
var CrawlError = class extends Error {
|
|
5
19
|
name = "CrawlError";
|
|
6
20
|
};
|
|
@@ -79,7 +93,7 @@ function urlsFromSitemap(xml, origin) {
|
|
|
79
93
|
return urls;
|
|
80
94
|
}
|
|
81
95
|
/** One request, with a timeout, returning HTML or a reason it is not a page. */
|
|
82
|
-
async function fetchPage(url, impl, timeoutMs, origin) {
|
|
96
|
+
async function fetchPage(url, impl, timeoutMs, origin, maxBodyBytes, headers) {
|
|
83
97
|
const controller = new AbortController();
|
|
84
98
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
85
99
|
try {
|
|
@@ -88,7 +102,8 @@ async function fetchPage(url, impl, timeoutMs, origin) {
|
|
|
88
102
|
redirect: "follow",
|
|
89
103
|
headers: {
|
|
90
104
|
accept: "text/html,application/xhtml+xml",
|
|
91
|
-
"user-agent": "eaa-kit"
|
|
105
|
+
"user-agent": "eaa-kit",
|
|
106
|
+
...headers
|
|
92
107
|
}
|
|
93
108
|
});
|
|
94
109
|
if (!response.ok) return {
|
|
@@ -106,11 +121,16 @@ async function fetchPage(url, impl, timeoutMs, origin) {
|
|
|
106
121
|
ok: false,
|
|
107
122
|
reason: `not HTML (${type.split(";")[0] || "no content-type"})`
|
|
108
123
|
};
|
|
124
|
+
const body = await readCapped(response, maxBodyBytes);
|
|
125
|
+
if (body === void 0) return {
|
|
126
|
+
ok: false,
|
|
127
|
+
reason: `larger than the ${maxBodyBytes} byte limit for one page`
|
|
128
|
+
};
|
|
109
129
|
return {
|
|
110
130
|
ok: true,
|
|
111
131
|
value: {
|
|
112
132
|
url: finalUrl,
|
|
113
|
-
html:
|
|
133
|
+
html: body
|
|
114
134
|
}
|
|
115
135
|
};
|
|
116
136
|
} catch (cause) {
|
|
@@ -123,6 +143,51 @@ async function fetchPage(url, impl, timeoutMs, origin) {
|
|
|
123
143
|
clearTimeout(timer);
|
|
124
144
|
}
|
|
125
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* A response body, up to a limit, or undefined when it runs past it.
|
|
148
|
+
*
|
|
149
|
+
* Streamed rather than buffered, because `response.text()` has already read
|
|
150
|
+
* everything by the time it could be checked, and a content-length header is
|
|
151
|
+
* both optional and unverified — a chunked response carries no length at all,
|
|
152
|
+
* and one that carries a length is not obliged to tell the truth. Counting the
|
|
153
|
+
* bytes as they arrive is the only check that holds either way, and the body is
|
|
154
|
+
* cancelled the moment it goes over so nothing keeps arriving.
|
|
155
|
+
*/
|
|
156
|
+
async function readCapped(response, limit) {
|
|
157
|
+
const body = response.body;
|
|
158
|
+
if (!body) {
|
|
159
|
+
const text = await response.text();
|
|
160
|
+
return text.length > limit ? void 0 : text;
|
|
161
|
+
}
|
|
162
|
+
const reader = body.getReader();
|
|
163
|
+
const chunks = [];
|
|
164
|
+
let total = 0;
|
|
165
|
+
try {
|
|
166
|
+
while (true) {
|
|
167
|
+
const { done, value } = await reader.read();
|
|
168
|
+
if (done) break;
|
|
169
|
+
if (value === void 0) continue;
|
|
170
|
+
total += value.byteLength;
|
|
171
|
+
if (total > limit) {
|
|
172
|
+
await reader.cancel();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
chunks.push(value);
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
reader.releaseLock();
|
|
179
|
+
}
|
|
180
|
+
return new TextDecoder().decode(concat(chunks, total));
|
|
181
|
+
}
|
|
182
|
+
function concat(chunks, total) {
|
|
183
|
+
const joined = new Uint8Array(total);
|
|
184
|
+
let at = 0;
|
|
185
|
+
for (const chunk of chunks) {
|
|
186
|
+
joined.set(chunk, at);
|
|
187
|
+
at += chunk.byteLength;
|
|
188
|
+
}
|
|
189
|
+
return joined;
|
|
190
|
+
}
|
|
126
191
|
/** Paths robots.txt disallows for us. Only the wildcard group is read. */
|
|
127
192
|
function disallowedPaths(robots) {
|
|
128
193
|
const lines = robots.split(/\r?\n/).map((line) => line.replace(/#.*$/, "").trim());
|
|
@@ -149,12 +214,22 @@ function disallowedPaths(robots) {
|
|
|
149
214
|
* that is not a page. A site that does not publish one is the ordinary case,
|
|
150
215
|
* not a failure, so it comes back empty either way.
|
|
151
216
|
*/
|
|
152
|
-
async function fetchSiteFile(entry, impl, name) {
|
|
217
|
+
async function fetchSiteFile(entry, impl, name, timeoutMs, maxBodyBytes, headers) {
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
153
220
|
try {
|
|
154
|
-
const response = await impl(new URL(name, entry).href, {
|
|
155
|
-
|
|
221
|
+
const response = await impl(new URL(name, entry).href, {
|
|
222
|
+
redirect: "follow",
|
|
223
|
+
signal: controller.signal,
|
|
224
|
+
...headers === void 0 ? {} : { headers }
|
|
225
|
+
});
|
|
226
|
+
if (!response.ok) return void 0;
|
|
227
|
+
if (new URL(response.url || new URL(name, entry).href).origin !== entry.origin) return void 0;
|
|
228
|
+
return await readCapped(response, maxBodyBytes);
|
|
156
229
|
} catch {
|
|
157
230
|
return;
|
|
231
|
+
} finally {
|
|
232
|
+
clearTimeout(timer);
|
|
158
233
|
}
|
|
159
234
|
}
|
|
160
235
|
/**
|
|
@@ -169,8 +244,9 @@ async function crawlSite(entry, options = {}) {
|
|
|
169
244
|
const maxPages = options.maxPages ?? 200;
|
|
170
245
|
const maxDepth = options.maxDepth ?? 3;
|
|
171
246
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
247
|
+
const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
172
248
|
const failures = [];
|
|
173
|
-
const robots = options.ignoreRobots ? void 0 : await fetchSiteFile(entry, impl, "/robots.txt");
|
|
249
|
+
const robots = options.ignoreRobots ? void 0 : await fetchSiteFile(entry, impl, "/robots.txt", timeoutMs, maxBodyBytes, options.headers);
|
|
174
250
|
const blocked = robots === void 0 ? [] : disallowedPaths(robots);
|
|
175
251
|
const allowed = (url) => !blocked.some((path) => url.pathname.startsWith(path));
|
|
176
252
|
let discovery = "links";
|
|
@@ -184,7 +260,7 @@ async function crawlSite(entry, options = {}) {
|
|
|
184
260
|
depth
|
|
185
261
|
});
|
|
186
262
|
};
|
|
187
|
-
const sitemap = await fetchSiteFile(entry, impl, options.sitemap ?? "/sitemap.xml");
|
|
263
|
+
const sitemap = await fetchSiteFile(entry, impl, options.sitemap ?? "/sitemap.xml", timeoutMs, maxBodyBytes, options.headers);
|
|
188
264
|
const listed = sitemap === void 0 ? [] : urlsFromSitemap(sitemap, entry);
|
|
189
265
|
if (listed.length > 0) {
|
|
190
266
|
discovery = "sitemap";
|
|
@@ -192,11 +268,12 @@ async function crawlSite(entry, options = {}) {
|
|
|
192
268
|
}
|
|
193
269
|
enqueue(entry, 0);
|
|
194
270
|
const pages = [];
|
|
271
|
+
const redirects = [];
|
|
195
272
|
while (queue.length > 0 && pages.length < maxPages) {
|
|
196
273
|
const batch = queue.splice(0, Math.min(REQUEST_CONCURRENCY, maxPages - pages.length));
|
|
197
274
|
const results = await Promise.all(batch.map(async (item) => ({
|
|
198
275
|
item,
|
|
199
|
-
result: await fetchPage(item.url, impl, timeoutMs, entry.origin)
|
|
276
|
+
result: await fetchPage(item.url, impl, timeoutMs, entry.origin, maxBodyBytes, options.headers)
|
|
200
277
|
})));
|
|
201
278
|
for (const { item, result } of results) {
|
|
202
279
|
if (!result.ok) {
|
|
@@ -207,6 +284,10 @@ async function crawlSite(entry, options = {}) {
|
|
|
207
284
|
continue;
|
|
208
285
|
}
|
|
209
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
|
+
});
|
|
210
291
|
pages.push({
|
|
211
292
|
absolutePath: url.href,
|
|
212
293
|
relativePath: pageIdentity(url),
|
|
@@ -220,11 +301,38 @@ async function crawlSite(entry, options = {}) {
|
|
|
220
301
|
pages: byIdentity(pages),
|
|
221
302
|
origin: entry.origin,
|
|
222
303
|
failures,
|
|
304
|
+
redirects,
|
|
223
305
|
truncated: queue.length > 0,
|
|
224
306
|
discovery
|
|
225
307
|
};
|
|
226
308
|
}
|
|
227
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
|
+
/**
|
|
228
336
|
* One page per identity, in a stable order.
|
|
229
337
|
*
|
|
230
338
|
* The queue already refuses a URL it has seen, so this catches the case the
|
|
@@ -241,4 +349,4 @@ function byIdentity(pages) {
|
|
|
241
349
|
}).sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
242
350
|
}
|
|
243
351
|
//#endregion
|
|
244
|
-
export { CrawlError, crawlSite, parseEntryUrl };
|
|
352
|
+
export { CrawlError, collapsedOnto, crawlSite, parseEntryUrl };
|
package/dist/eleventy/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as IntegrationOptions, t as BuildAuditError } from "../run-
|
|
1
|
+
import { n as IntegrationOptions, t as BuildAuditError } from "../run-YiqyQA2b.js";
|
|
2
2
|
//#region src/eleventy/index.d.ts
|
|
3
3
|
/** What `eleventy.after` is given. Only two of its fields are read. */
|
|
4
4
|
interface EleventyAfterEvent {
|
package/dist/eleventy/index.js
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
//#region src/audit/fingerprint.ts
|
|
3
|
+
/**
|
|
4
|
+
* A stable identity for one violating element.
|
|
5
|
+
*
|
|
6
|
+
* Derived from the rule, the selector and the element's own opening tag, and
|
|
7
|
+
* deliberately not from the file it was found in. Two consumers need this and
|
|
8
|
+
* they need it to agree: SARIF, so that moving a page does not close one code
|
|
9
|
+
* scanning alert and open an identical one, and the baseline, so that an
|
|
10
|
+
* accepted violation stays accepted when the surrounding page changes.
|
|
11
|
+
*
|
|
12
|
+
* Sixteen hex characters. This identifies a defect for humans and tooling, not
|
|
13
|
+
* a secret, and a full digest in every entry would make a baseline file for a
|
|
14
|
+
* large site unreadable.
|
|
15
|
+
*/
|
|
16
|
+
function elementFingerprint(ruleId, selector, html) {
|
|
17
|
+
return createHash("sha256").update(`${ruleId}\n${selector}\n${openingTag(html)}`).digest("hex").slice(0, 16);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The element's own tag, without anything nested inside it.
|
|
21
|
+
*
|
|
22
|
+
* axe-core hands back the failing element's outerHTML, which for a leaf like
|
|
23
|
+
* `<img>` is the element and for a container is the element and every
|
|
24
|
+
* descendant it has. Hashing all of that made the identity of a container
|
|
25
|
+
* depend on its contents — and the container that matters here is `<html>`,
|
|
26
|
+
* which every document-level rule fails against: `html-has-lang`,
|
|
27
|
+
* `document-title`, `landmark-one-main`, `page-has-heading-one`.
|
|
28
|
+
*
|
|
29
|
+
* Their outerHTML is the whole page. So adding one paragraph anywhere changed
|
|
30
|
+
* the fingerprint of every document-level violation on that page, and all three
|
|
31
|
+
* consumers believed it:
|
|
32
|
+
*
|
|
33
|
+
* - the baseline stopped suppressing barriers it had accepted, and the build
|
|
34
|
+
* went red on a page whose only change was a typo fix;
|
|
35
|
+
* - `diff` reported the same untouched barrier as both new and **fixed**, which
|
|
36
|
+
* is the one thing it exists to refuse to do;
|
|
37
|
+
* - SARIF churned its partialFingerprints, so code scanning closed an alert and
|
|
38
|
+
* opened an identical one on every edit.
|
|
39
|
+
*
|
|
40
|
+
* The identity of an element is its own tag and attributes. Where two elements
|
|
41
|
+
* share those, axe-core's selector already tells them apart — it qualifies
|
|
42
|
+
* ambiguous matches with `:nth-child(…)` — so nothing that was distinguishable
|
|
43
|
+
* before stops being distinguishable now.
|
|
44
|
+
*
|
|
45
|
+
* Anything that is not an element — the empty string a rule with no attached
|
|
46
|
+
* node carries — is returned unchanged, so those keep the identity they had.
|
|
47
|
+
*/
|
|
48
|
+
function openingTag(html) {
|
|
49
|
+
const trimmed = html.trim();
|
|
50
|
+
if (!trimmed.startsWith("<")) return trimmed;
|
|
51
|
+
let quote;
|
|
52
|
+
for (let i = 1; i < trimmed.length; i += 1) {
|
|
53
|
+
const char = trimmed[i];
|
|
54
|
+
if (quote !== void 0) {
|
|
55
|
+
if (char === quote) quote = void 0;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (char === "\"" || char === "'") {
|
|
59
|
+
quote = char;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (char === ">") return trimmed.slice(0, i + 1);
|
|
63
|
+
}
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { elementFingerprint as t };
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { i as escapeText, n as count,
|
|
1
|
+
import { a as impactLabel, r as byImpactThenRule } from "./impact-luGCnN8F.js";
|
|
2
|
+
import { i as escapeText, n as count, r as escapeAttribute, s as standardsReference, t as collapse } from "./text-BF1LHMiV.js";
|
|
3
3
|
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { n as
|
|
4
|
+
import { c as runEngine } from "./result-DfUru6y_.js";
|
|
5
|
+
import { l as reviewSentence } from "./review-CdMK2GpQ.js";
|
|
6
|
+
import { r as componentPath } from "./component-ChisoFyY.js";
|
|
7
|
+
import { i as reviewSummary, n as buildCoverage, o as manualCheckFor, s as understandingUrl } from "./coverage-BD2_CYns.js";
|
|
8
|
+
import { a as blindRules, c as isShared, i as remediationFor, l as issueTotals, n as missedParts, o as coverageParts, s as groupIssues, t as discoveryLabel } from "./completeness-Dwq3CPB3.js";
|
|
9
|
+
import { n as buildSummary } from "./json-PNg6BeXp.js";
|
|
8
10
|
import axe from "axe-core";
|
|
9
11
|
//#region src/audit/report/html.ts
|
|
10
12
|
/**
|
|
@@ -36,8 +38,8 @@ const MAX_NODES = 5;
|
|
|
36
38
|
/** Longest element markup shown before it is truncated. */
|
|
37
39
|
const MAX_SNIPPET = 200;
|
|
38
40
|
function buildHtmlReport(audits, options) {
|
|
39
|
-
const engine = audits
|
|
40
|
-
const
|
|
41
|
+
const engine = runEngine(audits);
|
|
42
|
+
const totals = buildSummary(audits, options.failOn);
|
|
41
43
|
const generatedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
42
44
|
const title = `Accessibility audit · ${options.directory}`;
|
|
43
45
|
return `<!doctype html>
|
|
@@ -54,15 +56,15 @@ ${STYLES}
|
|
|
54
56
|
<body>
|
|
55
57
|
<main>
|
|
56
58
|
<h1>Accessibility audit</h1>
|
|
57
|
-
${verdict(
|
|
59
|
+
${verdict(totals, options)}
|
|
58
60
|
${scoreboard(audits)}
|
|
59
61
|
${issues(audits, options)}
|
|
60
62
|
${runDetails(audits, engine, generatedAt, options)}
|
|
61
63
|
${notMeasured(options)}
|
|
62
|
-
${summary(
|
|
64
|
+
${summary(totals, options)}
|
|
63
65
|
${pages(audits)}
|
|
64
66
|
${notEvaluated(audits)}
|
|
65
|
-
${coverageSection(audits)}
|
|
67
|
+
${coverageSection(audits, options)}
|
|
66
68
|
${footer()}
|
|
67
69
|
</main>
|
|
68
70
|
</body>
|
|
@@ -77,11 +79,11 @@ ${footer()}
|
|
|
77
79
|
* which would be an embarrassing thing for this document in particular to get
|
|
78
80
|
* wrong.
|
|
79
81
|
*/
|
|
80
|
-
function verdict(
|
|
81
|
-
const unaudited =
|
|
82
|
+
function verdict(totals, options) {
|
|
83
|
+
const unaudited = totals.pagesNotAudited;
|
|
82
84
|
if (unaudited > 0) return banner("broken", "Could not finish", `${count(unaudited, "page")} could not be audited, so this run reached no verdict.`);
|
|
83
|
-
if (failing > 0) return banner("fail", "Violations found", `${count(failing, "violation")} at or above ${escapeText(options.failOn)}.`);
|
|
84
|
-
const below =
|
|
85
|
+
if (totals.failing > 0) return banner("fail", "Violations found", `${count(totals.failing, "violation")} at or above ${escapeText(options.failOn)}.`);
|
|
86
|
+
const below = totals.violations - totals.failing;
|
|
85
87
|
if (below > 0) return banner("pass", "No violations at the threshold", `${count(below, "violation")} below ${escapeText(options.failOn)}, which do not fail the run.`);
|
|
86
88
|
const completeness = options.completeness;
|
|
87
89
|
if (completeness && !completeness.complete) return banner("partial", "No violations found, but the whole site was not measured", `${escapeText(missedParts(completeness).join(", "))}. This report describes ${count(completeness.audited, "page")}, not the site.`);
|
|
@@ -137,13 +139,12 @@ function notMeasured(options) {
|
|
|
137
139
|
* the kind of bug nobody notices until a client does, so there is one place
|
|
138
140
|
* that counts.
|
|
139
141
|
*/
|
|
140
|
-
function summary(
|
|
141
|
-
const totals = buildSummary(audits, options.failOn);
|
|
142
|
+
function summary(totals, options) {
|
|
142
143
|
const impacts = IMPACT_ORDER.filter((impact) => totals.byImpact[impact] > 0).map((impact) => ` <li><span class="badge ${escapeAttribute(impact)}">${escapeText(impact)}</span> ${totals.byImpact[impact]}</li>`).join("\n");
|
|
143
144
|
return `<h2>Summary</h2>
|
|
144
145
|
<ul class="counts">
|
|
145
146
|
<li><strong>${count(totals.violations, "violation")}</strong> on ${totals.pagesWithViolations} of ${count(totals.pages, "page")}, across ${count(totals.violatingElements, "element")}</li>
|
|
146
|
-
<li><strong>${failing}</strong> at or above ${escapeText(options.failOn)}</li>
|
|
147
|
+
<li><strong>${totals.failing}</strong> at or above ${escapeText(options.failOn)}</li>
|
|
147
148
|
<li><strong>${totals.needsReview}</strong> ${totals.needsReview === 1 ? "rule needs" : "rules need"} manual review</li>
|
|
148
149
|
<li><strong>${totals.notEvaluated}</strong> ${totals.notEvaluated === 1 ? "rule was" : "rules were"} not evaluated by this engine</li>
|
|
149
150
|
${totals.accepted > 0 ? ` <li><strong>${totals.accepted}</strong> ${totals.accepted === 1 ? "element is" : "elements are"} accepted by the baseline, and not counted above</li>` : ""}
|
|
@@ -236,8 +237,8 @@ ${manualBlock(ruleId, finding)}
|
|
|
236
237
|
* majority of WCAG, and a percentage would present a limit of automated testing
|
|
237
238
|
* as a property of this site.
|
|
238
239
|
*/
|
|
239
|
-
function coverageSection(audits) {
|
|
240
|
-
const coverage = buildCoverage(audits);
|
|
240
|
+
function coverageSection(audits, options) {
|
|
241
|
+
const coverage = buildCoverage(audits, void 0, options.review);
|
|
241
242
|
const rows = coverage.criteria.map((criterion) => {
|
|
242
243
|
const url = understandingUrl(criterion.number);
|
|
243
244
|
const name = escapeText(`${criterion.number} ${criterion.title}`);
|
|
@@ -245,7 +246,7 @@ function coverageSection(audits) {
|
|
|
245
246
|
return ` <tr class="${escapeAttribute(criterion.status)}">
|
|
246
247
|
<td>${linked}</td>
|
|
247
248
|
<td>${escapeText(criterion.level)}</td>
|
|
248
|
-
<td>${statusText(criterion)}</td>
|
|
249
|
+
<td>${statusText(criterion)}${reviewCell(criterion)}</td>
|
|
249
250
|
</tr>`;
|
|
250
251
|
}).join("\n");
|
|
251
252
|
return `<h2>Coverage of WCAG 2.2 AA</h2>
|
|
@@ -263,6 +264,7 @@ function coverageSection(audits) {
|
|
|
263
264
|
of WCAG cannot be automated, and a percentage here would present that limit of automated
|
|
264
265
|
testing as though it were a measurement of this site.
|
|
265
266
|
</p>
|
|
267
|
+
${reviewParagraph(coverage)}
|
|
266
268
|
<div class="scroll">
|
|
267
269
|
<table class="coverage">
|
|
268
270
|
<thead><tr><th>Success criterion</th><th>Level</th><th>This run</th></tr></thead>
|
|
@@ -272,6 +274,20 @@ ${rows}
|
|
|
272
274
|
</table>
|
|
273
275
|
</div>`;
|
|
274
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* What a person recorded, where a review was supplied.
|
|
279
|
+
*
|
|
280
|
+
* Its own paragraph, under the four counts and visibly apart from them: the
|
|
281
|
+
* counts are what an engine measured and this is what somebody says they
|
|
282
|
+
* checked. This document is the one that leaves the building, so the difference
|
|
283
|
+
* between the two has to survive being quoted.
|
|
284
|
+
*/
|
|
285
|
+
function reviewParagraph(coverage) {
|
|
286
|
+
const summary = reviewSummary(coverage);
|
|
287
|
+
if (summary === void 0) return "";
|
|
288
|
+
return `<p class="note review">${escapeText(summary)} eaa-kit cannot check that anything
|
|
289
|
+
recorded in a review is true; it reports it as the claim it is.</p>`;
|
|
290
|
+
}
|
|
275
291
|
function statusText(criterion) {
|
|
276
292
|
const browser = criterion.browserWouldAnswer ? " <em>(a real browser would answer this)</em>" : "";
|
|
277
293
|
switch (criterion.status) {
|
|
@@ -281,6 +297,16 @@ function statusText(criterion) {
|
|
|
281
297
|
case "no-automated-rule": return "No automated rule exists; a person must check it";
|
|
282
298
|
}
|
|
283
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* What a person recorded about this criterion, in the cell that says what the
|
|
302
|
+
* run reached. The same sentence the console report prints, from the same
|
|
303
|
+
* function, so two documents of one run cannot word it differently.
|
|
304
|
+
*/
|
|
305
|
+
function reviewCell(criterion) {
|
|
306
|
+
const review = criterion.review;
|
|
307
|
+
if (review === void 0) return "";
|
|
308
|
+
return `<br><span class="reviewed">${escapeText(reviewSentence(review))}</span>`;
|
|
309
|
+
}
|
|
284
310
|
function footer() {
|
|
285
311
|
return `<hr>
|
|
286
312
|
<footer>
|
|
@@ -303,12 +329,6 @@ function standards(finding) {
|
|
|
303
329
|
function standardsText(finding) {
|
|
304
330
|
return escapeText(standardsReference(finding.successCriteria, finding.enClauses));
|
|
305
331
|
}
|
|
306
|
-
function totalViolations(audits) {
|
|
307
|
-
return audits.reduce((total, audit) => total + audit.violations.length, 0);
|
|
308
|
-
}
|
|
309
|
-
function byImpactThenRule(a, b) {
|
|
310
|
-
return impactRank(a.impact) - impactRank(b.impact) || a.ruleId.localeCompare(b.ruleId);
|
|
311
|
-
}
|
|
312
332
|
const STYLES = `:root { color-scheme: light dark; }
|
|
313
333
|
body { margin: 0; background: #ffffff; color: #1a1a1a;
|
|
314
334
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; line-height: 1.6; }
|
|
@@ -362,6 +382,8 @@ table.coverage tr.evaluated td:last-child { color: #216e39; }
|
|
|
362
382
|
table.coverage em { font-style: normal; opacity: 0.75; }
|
|
363
383
|
ul.unreachable li { margin-bottom: 0.2rem; overflow-wrap: anywhere; }
|
|
364
384
|
code.selector { color: #4a4a4a; }
|
|
385
|
+
.reviewed { color: #4a4a4a; font-size: 0.95em; }
|
|
386
|
+
p.note.review { border-left: 3px solid #c9c9c9; padding-left: 0.75rem; }
|
|
365
387
|
p.clean { color: #216e39; }
|
|
366
388
|
p.note { font-size: 0.95rem; }
|
|
367
389
|
hr { border: 0; border-top: 1px solid #d4d4d4; margin: 3rem 0 1.5rem; }
|
|
@@ -401,7 +423,8 @@ ul.page-list { margin: 0.35rem 0 0; padding-left: 1.25rem; }
|
|
|
401
423
|
pre { background: #1e1e1e; }
|
|
402
424
|
li.finding { border-color: #3a3a3a; }
|
|
403
425
|
p.standards, p.coverage, .reason, li.more, code.selector, footer,
|
|
404
|
-
p.accepted-heading, ul.accepted { color: #b6b6b6; }
|
|
426
|
+
p.accepted-heading, ul.accepted, .reviewed { color: #b6b6b6; }
|
|
427
|
+
p.note.review { border-left-color: #555; }
|
|
405
428
|
p.clean { color: #7ee2a8; }
|
|
406
429
|
.verdict.pass { background: #10240f; border-color: #7ee2a8; }
|
|
407
430
|
.verdict.fail { background: #2b1111; border-color: #ff9d9d; }
|
|
@@ -467,8 +490,7 @@ function scoreboard(audits) {
|
|
|
467
490
|
function issues(audits, options) {
|
|
468
491
|
const found = groupIssues(audits);
|
|
469
492
|
if (found.length === 0) return "";
|
|
470
|
-
const elements
|
|
471
|
-
const occurrences = found.reduce((total, issue) => total + issue.occurrences, 0);
|
|
493
|
+
const { elements, occurrences } = issueTotals(found);
|
|
472
494
|
const intro = occurrences === elements ? `${count(elements, "distinct element")} to fix.` : `${count(occurrences, "violation")} across the site, from ${count(elements, "distinct element")}.`;
|
|
473
495
|
return `<h2 id="issues">What to fix</h2>
|
|
474
496
|
<p class="intro">${escapeText(intro)}</p>
|
|
@@ -36,6 +36,16 @@ function impactRank(impact) {
|
|
|
36
36
|
function impactLabel(impact) {
|
|
37
37
|
return impact !== null && isImpactLevel(impact) ? impact : "unclassified";
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Worst first, then by rule id so two runs of the same build agree.
|
|
41
|
+
*
|
|
42
|
+
* The order every format lists a page's findings in. Issues are sorted by
|
|
43
|
+
* `bySeverityThenReach` instead, which adds the reach a grouped issue has and a
|
|
44
|
+
* single page's findings do not.
|
|
45
|
+
*/
|
|
46
|
+
function byImpactThenRule(a, b) {
|
|
47
|
+
return impactRank(a.impact) - impactRank(b.impact) || a.ruleId.localeCompare(b.ruleId);
|
|
48
|
+
}
|
|
39
49
|
/** Violations at or above `threshold`, counted per rule per page. */
|
|
40
50
|
function countAtOrAbove(audits, threshold) {
|
|
41
51
|
let total = 0;
|
|
@@ -43,4 +53,4 @@ function countAtOrAbove(audits, threshold) {
|
|
|
43
53
|
return total;
|
|
44
54
|
}
|
|
45
55
|
//#endregion
|
|
46
|
-
export {
|
|
56
|
+
export { impactLabel as a, meetsThreshold as c, countAtOrAbove as i, IMPACT_LEVELS as n, impactRank as o, byImpactThenRule as r, isImpactLevel as s, DEFAULT_FAIL_ON as t };
|