eaa-kit 0.5.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 +35 -2
- 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-DK2StT1_.js +2 -0
- package/dist/{audit-B282A3EA.js → audit-e_uixhPG.js} +95 -48
- package/dist/baseline-BscHfFSW.js +2 -0
- package/dist/{baseline-CuAS2lMk.js → baseline-D2qWmUC6.js} +36 -9
- package/dist/checklist-BtgkxMEa.js +44 -0
- package/dist/cli/index.js +111 -14
- package/dist/{command-C3D7JWn6.js → command-CZKp9Wi_.js} +101 -10
- package/dist/{remediation-CMBIrnpN.js → completeness-Dwq3CPB3.js} +60 -42
- 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-B_Y6l-Ra.js → coverage-BD2_CYns.js} +44 -7
- package/dist/{crawl-BUWPgaGW.js → crawl-BHkJmUkA.js} +42 -8
- package/dist/eleventy/index.d.ts +1 -1
- package/dist/eleventy/index.js +1 -1
- package/dist/{html-C4vktg0A.js → html-Bmh7X_sM.js} +52 -30
- package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.js +3 -3
- package/dist/{init-CW7LfGT5.js → init-BjKYttxc.js} +3 -3
- package/dist/{jsdom-B--cEH-G.js → jsdom-CQar6OQS.js} +1 -1
- package/dist/jsdom-DMvU2h-f.js +3 -0
- package/dist/{json-D_Mnnft5.js → json-HJDgJp09.js} +1 -1
- package/dist/{json-DROX33kh.js → json-PNg6BeXp.js} +11 -16
- package/dist/load-Bc_xDGAh.js +2 -0
- package/dist/{load-yAR4wzez.js → load-vZdV_fD1.js} +5 -1
- package/dist/nuxt/index.d.ts +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/{playwright-BojtYVUa.js → playwright-C-miJP2b.js} +3 -2
- package/dist/{pool-BO25OIez.js → pool-dkexq5xo.js} +2 -2
- package/dist/{render-DbGOVmhx.js → render-CtsqR0k-.js} +105 -4
- package/dist/{result-BWcYXeRs.js → result-DfUru6y_.js} +24 -1
- package/dist/review-CdMK2GpQ.js +161 -0
- package/dist/review-DIbOTmOM.js +2 -0
- package/dist/{run-C2nKFcb-.js → run-D7hdVXLA.js} +1 -1
- package/dist/{run-CtcEUhbe.d.ts → run-YiqyQA2b.d.ts} +10 -0
- package/dist/{sarif-B-UBcVu8.js → sarif-BKvq_AUm.js} +23 -12
- 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 +2 -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
|
@@ -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 };
|
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
|
@@ -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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -136,6 +136,10 @@ declare const auditSchema: Schema<ObjectOf<{
|
|
|
136
136
|
manual: OptionalSchema<boolean>;
|
|
137
137
|
/** List every WCAG 2.2 A/AA criterion and what the run reached on it. */
|
|
138
138
|
coverage: OptionalSchema<boolean>;
|
|
139
|
+
/** Path to a review record: what a person checked, for the criteria no engine reaches. */
|
|
140
|
+
review: OptionalSchema<string>;
|
|
141
|
+
/** Days after which a recorded review stops counting. */
|
|
142
|
+
reviewMaxAge: OptionalSchema<number>;
|
|
139
143
|
/**
|
|
140
144
|
* False is `--no-build`: never run the project's build or start its server to
|
|
141
145
|
* find something to audit. Written in the positive because that is the state
|
|
@@ -232,6 +236,10 @@ declare const configSchema: Schema<ObjectOf<{
|
|
|
232
236
|
manual: OptionalSchema<boolean>;
|
|
233
237
|
/** List every WCAG 2.2 A/AA criterion and what the run reached on it. */
|
|
234
238
|
coverage: OptionalSchema<boolean>;
|
|
239
|
+
/** Path to a review record: what a person checked, for the criteria no engine reaches. */
|
|
240
|
+
review: OptionalSchema<string>;
|
|
241
|
+
/** Days after which a recorded review stops counting. */
|
|
242
|
+
reviewMaxAge: OptionalSchema<number>;
|
|
235
243
|
/**
|
|
236
244
|
* False is `--no-build`: never run the project's build or start its server to
|
|
237
245
|
* find something to audit. Written in the positive because that is the state
|
|
@@ -349,6 +357,24 @@ declare class StatementError extends Error {
|
|
|
349
357
|
readonly name = "StatementError";
|
|
350
358
|
}
|
|
351
359
|
//#endregion
|
|
360
|
+
//#region src/audit/review.d.ts
|
|
361
|
+
declare const reviewSchema: Schema<ObjectOf<{
|
|
362
|
+
schemaVersion: Schema<number>;
|
|
363
|
+
/** Who carried out the review, for the record as a whole. */
|
|
364
|
+
reviewedBy: OptionalSchema<string>;
|
|
365
|
+
/** Keyed by success criterion number, e.g. '1.2.1'. */
|
|
366
|
+
criteria: DefaultedSchema<Record<string, ObjectOf<{
|
|
367
|
+
result: DefaultedSchema<"met" | "not-applicable" | "not-met" | "unreviewed">;
|
|
368
|
+
/** ISO date the check was done. Without it the entry cannot be aged. */
|
|
369
|
+
reviewedOn: OptionalSchema<string>;
|
|
370
|
+
/** Who did it, when that is not the person named on the record. */
|
|
371
|
+
reviewedBy: OptionalSchema<string>;
|
|
372
|
+
/** What was checked and what was found. Free text, for whoever reads it. */
|
|
373
|
+
note: OptionalSchema<string>;
|
|
374
|
+
}>>>;
|
|
375
|
+
}>>;
|
|
376
|
+
type ReviewRecord = Infer<typeof reviewSchema>;
|
|
377
|
+
//#endregion
|
|
352
378
|
//#region src/statement/findings.d.ts
|
|
353
379
|
/** One rule that failed, folded across every page it failed on. */
|
|
354
380
|
interface AuditFinding {
|
|
@@ -394,6 +420,47 @@ declare function summariseAuditReport(value: unknown, source?: string): AuditSum
|
|
|
394
420
|
/** Read and summarise a report written by `eaa-kit audit --format json`. */
|
|
395
421
|
declare function readAuditReport(file: string, cwd?: string): Promise<AuditSummary>;
|
|
396
422
|
//#endregion
|
|
423
|
+
//#region src/statement/evidence.d.ts
|
|
424
|
+
/**
|
|
425
|
+
* Whether the statement's claim survives contact with the evidence beside it.
|
|
426
|
+
*
|
|
427
|
+
* The README has said since 0.1 that "a statement claiming full conformance for
|
|
428
|
+
* a site that is not conformant is worse than no statement at all", and until
|
|
429
|
+
* now the tool did nothing about it: `compliance.status` came out of the config
|
|
430
|
+
* file, the audit report came in through `--audit`, and nothing ever put the
|
|
431
|
+
* two in the same room. A run could print a document claiming full conformance
|
|
432
|
+
* directly above the list of barriers that disproves it.
|
|
433
|
+
*
|
|
434
|
+
* This is the check that was missing. It is deliberately narrow: it does not
|
|
435
|
+
* decide whether a site is accessible, and it cannot — most of WCAG is not
|
|
436
|
+
* automatable and the whole tool is built around saying so. It refuses only
|
|
437
|
+
* claims the evidence in hand already contradicts, and warns where a document
|
|
438
|
+
* is citing evidence that no longer describes the site.
|
|
439
|
+
*
|
|
440
|
+
* Nothing here needs an audit report. Given none, only the dates are checked,
|
|
441
|
+
* and a project that has never run one gets exactly the statement it always
|
|
442
|
+
* got — the tool cannot audit what it was not shown.
|
|
443
|
+
*/
|
|
444
|
+
/** How much a problem matters. `refuses` stops the statement being written. */
|
|
445
|
+
type EvidenceSeverity = 'refuses' | 'warns';
|
|
446
|
+
interface EvidenceProblem {
|
|
447
|
+
severity: EvidenceSeverity;
|
|
448
|
+
/** One sentence stating the contradiction, and one naming the fix. */
|
|
449
|
+
message: string;
|
|
450
|
+
}
|
|
451
|
+
interface EvidenceInput {
|
|
452
|
+
config: EaaConfig;
|
|
453
|
+
/** From `--audit`. Without it only the dates can be checked. */
|
|
454
|
+
audit?: AuditSummary;
|
|
455
|
+
/** From `--review`. Read for this check alone; it does not reach the text. */
|
|
456
|
+
review?: ReviewRecord;
|
|
457
|
+
/** Injectable so the ageing checks can be tested without waiting. */
|
|
458
|
+
today?: Date;
|
|
459
|
+
}
|
|
460
|
+
declare function checkStatementEvidence(input: EvidenceInput): EvidenceProblem[];
|
|
461
|
+
/** Whether any problem is bad enough to stop the document being written. */
|
|
462
|
+
declare function refuses(problems: readonly EvidenceProblem[]): boolean;
|
|
463
|
+
//#endregion
|
|
397
464
|
//#region src/statement/html.d.ts
|
|
398
465
|
/**
|
|
399
466
|
* Markdown-to-HTML for exactly the subset the statement templates emit, and
|
|
@@ -466,4 +533,4 @@ interface RenderedStatement {
|
|
|
466
533
|
*/
|
|
467
534
|
declare function renderStatement(config: EaaConfig, options?: RenderStatementOptions): Promise<RenderedStatement>;
|
|
468
535
|
//#endregion
|
|
469
|
-
export { ASSESSMENT_METHODS, type AssessmentMethod, type AuditFinding, type AuditSummary, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, type ComplianceStatus, ConfigError, type Country, type EaaConfig, type EaaConfigInput, type HtmlDocumentOptions, ISSUE_REASONS, type IssueReason, type KnownIssue, type LoadConfigOptions, type LoadedConfig, type RenderStatementOptions, type RenderedStatement, STATEMENT_LOCALES, StatementError, type StatementLocale, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
|
|
536
|
+
export { ASSESSMENT_METHODS, type AssessmentMethod, type AuditFinding, type AuditSummary, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, type ComplianceStatus, ConfigError, type Country, type EaaConfig, type EaaConfigInput, type EvidenceInput, type EvidenceProblem, type EvidenceSeverity, type HtmlDocumentOptions, ISSUE_REASONS, type IssueReason, type KnownIssue, type LoadConfigOptions, type LoadedConfig, type RenderStatementOptions, type RenderedStatement, STATEMENT_LOCALES, StatementError, type StatementLocale, checkStatementEvidence, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, refuses, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as ASSESSMENT_METHODS, c as ConfigError, d as configSchema, f as defineConfig, i as loadConfig, l as ISSUE_REASONS, n as findConfigFile, o as COMPLIANCE_STATUSES, p as parseConfig, s as COUNTRIES, t as CONFIG_FILENAMES, u as STATEMENT_LOCALES } from "./load-
|
|
2
|
-
import { a as summariseAuditReport, i as readAuditReport, n as toHtmlBody, o as
|
|
3
|
-
export { ASSESSMENT_METHODS, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, ConfigError, ISSUE_REASONS, STATEMENT_LOCALES, StatementError, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
|
|
1
|
+
import { a as ASSESSMENT_METHODS, c as ConfigError, d as configSchema, f as defineConfig, i as loadConfig, l as ISSUE_REASONS, n as findConfigFile, o as COMPLIANCE_STATUSES, p as parseConfig, s as COUNTRIES, t as CONFIG_FILENAMES, u as STATEMENT_LOCALES } from "./load-vZdV_fD1.js";
|
|
2
|
+
import { a as summariseAuditReport, c as StatementError, i as readAuditReport, n as toHtmlBody, o as checkStatementEvidence, r as toHtmlDocument, s as refuses, t as renderStatement } from "./render-CtsqR0k-.js";
|
|
3
|
+
export { ASSESSMENT_METHODS, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, ConfigError, ISSUE_REASONS, STATEMENT_LOCALES, StatementError, checkStatementEvidence, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, refuses, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { t as DEFAULT_FAIL_ON } from "./impact-
|
|
2
|
-
import { s as COUNTRIES, t as CONFIG_FILENAMES } from "./load-
|
|
1
|
+
import { t as DEFAULT_FAIL_ON } from "./impact-luGCnN8F.js";
|
|
2
|
+
import { s as COUNTRIES, t as CONFIG_FILENAMES } from "./load-vZdV_fD1.js";
|
|
3
3
|
import { t as exists } from "./fs-BmPtmFke.js";
|
|
4
|
-
import { l as warn, o as fail, s as note } from "./command-
|
|
4
|
+
import { l as warn, o as fail, s as note } from "./command-CZKp9Wi_.js";
|
|
5
5
|
import { readFile, writeFile } from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import pc from "picocolors";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as failedPage, i as blindRulesInScope, l as runOptions, n as DEFAULT_TAGS, s as pageUrl, t as DEFAULT_PAGE_TIMEOUT_MS, u as shapeResults } from "./result-
|
|
1
|
+
import { a as failedPage, i as blindRulesInScope, l as runOptions, n as DEFAULT_TAGS, s as pageUrl, t as DEFAULT_PAGE_TIMEOUT_MS, u as shapeResults } from "./result-DfUru6y_.js";
|
|
2
2
|
import axe from "axe-core";
|
|
3
3
|
import { Script } from "node:vm";
|
|
4
4
|
import { JSDOM, VirtualConsole } from "jsdom";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { r as serialiseJsonReport, t as buildJsonReport } from "./json-
|
|
1
|
+
import { r as serialiseJsonReport, t as buildJsonReport } from "./json-PNg6BeXp.js";
|
|
2
2
|
export { buildJsonReport, serialiseJsonReport };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as impactLabel, i as countAtOrAbove, s as isImpactLevel } from "./impact-luGCnN8F.js";
|
|
2
2
|
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
3
3
|
import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
|
|
4
|
-
import { c as
|
|
5
|
-
import {
|
|
4
|
+
import { c as runEngine, f as uniqueRuleOutcomes } from "./result-DfUru6y_.js";
|
|
5
|
+
import { n as buildCoverage } from "./coverage-BD2_CYns.js";
|
|
6
6
|
import axe from "axe-core";
|
|
7
7
|
/**
|
|
8
8
|
* Build the machine-readable report.
|
|
@@ -24,7 +24,7 @@ function buildJsonReport(audits, options) {
|
|
|
24
24
|
axeCore: axe.version
|
|
25
25
|
},
|
|
26
26
|
generatedAt,
|
|
27
|
-
engine: audits
|
|
27
|
+
engine: runEngine(audits),
|
|
28
28
|
target: {
|
|
29
29
|
source: options.directory,
|
|
30
30
|
kind: options.sourceKind ?? "directory",
|
|
@@ -33,24 +33,19 @@ function buildJsonReport(audits, options) {
|
|
|
33
33
|
},
|
|
34
34
|
summary: buildSummary(audits, options.failOn),
|
|
35
35
|
completeness: options.completeness,
|
|
36
|
-
coverage: buildCoverage(audits),
|
|
36
|
+
coverage: buildCoverage(audits, void 0, options.review),
|
|
37
37
|
rules: buildRuleIndex(audits),
|
|
38
38
|
pages: audits.map(toJsonPage)
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
/** Every rule mentioned by any page, keyed by id and sorted for stable diffs. */
|
|
42
42
|
function buildRuleIndex(audits) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
successCriteria: outcome.successCriteria,
|
|
50
|
-
en301549: outcome.enClauses
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
return Object.fromEntries([...index].sort(([a], [b]) => a.localeCompare(b)));
|
|
43
|
+
return Object.fromEntries(uniqueRuleOutcomes(audits).map((outcome) => [outcome.ruleId, {
|
|
44
|
+
help: outcome.help,
|
|
45
|
+
helpUrl: outcome.helpUrl,
|
|
46
|
+
successCriteria: outcome.successCriteria,
|
|
47
|
+
en301549: outcome.enClauses
|
|
48
|
+
}]));
|
|
54
49
|
}
|
|
55
50
|
/** Serialised form written to stdout or to --output, with a trailing newline. */
|
|
56
51
|
function serialiseJsonReport(report) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as IMPACT_LEVELS } from "./impact-
|
|
1
|
+
import { n as IMPACT_LEVELS } from "./impact-luGCnN8F.js";
|
|
2
2
|
import { _ as union, a as integer, d as optional, f as pipe, g as transform, h as string, i as enumeration, m as safeParse, n as boolean, o as isoDate, r as email, t as array, u as object, v as url, y as withDefault } from "./schema-DJSF4K05.js";
|
|
3
3
|
import { r as isFile } from "./fs-BmPtmFke.js";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
@@ -122,6 +122,10 @@ const auditSchema = object({
|
|
|
122
122
|
manual: optional(boolean()),
|
|
123
123
|
/** List every WCAG 2.2 A/AA criterion and what the run reached on it. */
|
|
124
124
|
coverage: optional(boolean()),
|
|
125
|
+
/** Path to a review record: what a person checked, for the criteria no engine reaches. */
|
|
126
|
+
review: optional(string({ min: 1 })),
|
|
127
|
+
/** Days after which a recorded review stops counting. */
|
|
128
|
+
reviewMaxAge: optional(integer({ min: 0 })),
|
|
125
129
|
/**
|
|
126
130
|
* False is `--no-build`: never run the project's build or start its server to
|
|
127
131
|
* find something to audit. Written in the positive because that is the state
|
package/dist/nuxt/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/nuxt/index.d.ts
|
|
3
3
|
/** The part of the Nitro instance this reads, handed over by `nitro:init`. */
|
|
4
4
|
interface NitroLike {
|
package/dist/nuxt/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-
|
|
1
|
+
import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-D7hdVXLA.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
//#region src/nuxt/index.ts
|
|
4
4
|
function eaaKitModule(options = {}, nuxt) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as isFile } from "./fs-BmPtmFke.js";
|
|
2
|
-
import { a as failedPage, l as runOptions, n as DEFAULT_TAGS, s as pageUrl, u as shapeResults } from "./result-
|
|
2
|
+
import { a as failedPage, l as runOptions, n as DEFAULT_TAGS, s as pageUrl, u as shapeResults } from "./result-DfUru6y_.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
@@ -165,7 +165,8 @@ async function runBrowserAudit(directory, pages, options = {}) {
|
|
|
165
165
|
try {
|
|
166
166
|
const context = await browser.newContext({
|
|
167
167
|
viewport,
|
|
168
|
-
bypassCSP: true
|
|
168
|
+
bypassCSP: true,
|
|
169
|
+
...options.headers === void 0 ? {} : { extraHTTPHeaders: options.headers }
|
|
169
170
|
});
|
|
170
171
|
const audits = Array.from({ length: pages.length });
|
|
171
172
|
const lanes = Math.max(1, Math.min(options.concurrency ?? DEFAULT_CONCURRENCY, pages.length));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as isFile } from "./fs-BmPtmFke.js";
|
|
2
|
-
import { a as failedPage, s as pageUrl } from "./result-
|
|
2
|
+
import { a as failedPage, s as pageUrl } from "./result-DfUru6y_.js";
|
|
3
3
|
import { Worker } from "node:worker_threads";
|
|
4
4
|
import { availableParallelism } from "node:os";
|
|
5
5
|
//#region src/audit/runners/pool.ts
|
|
@@ -106,7 +106,7 @@ async function runPooledAudit(pages, options = {}) {
|
|
|
106
106
|
return runWorkers(pages, runnerOptions, Math.min(workers, pages.length), entry);
|
|
107
107
|
}
|
|
108
108
|
async function auditHere(pages, options) {
|
|
109
|
-
const { runJsdomAudit } = await import("./jsdom-
|
|
109
|
+
const { runJsdomAudit } = await import("./jsdom-DMvU2h-f.js");
|
|
110
110
|
return runJsdomAudit(pages, options);
|
|
111
111
|
}
|
|
112
112
|
/**
|