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,19 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as pageUrl, d as runOptions, f as shapeResults, i as blindRulesInScope, n as DEFAULT_TAGS, o as failedPage } from "./result-B7_zVxAX.js";
|
|
2
2
|
import axe from "axe-core";
|
|
3
3
|
import { Script } from "node:vm";
|
|
4
4
|
import { JSDOM, VirtualConsole } from "jsdom";
|
|
5
5
|
//#region src/audit/runners/jsdom.ts
|
|
6
6
|
/**
|
|
7
|
-
* Per-page ceiling.
|
|
8
|
-
*
|
|
9
|
-
* A soft one, and the limit is worth stating: this races axe-core against a
|
|
10
|
-
* timer, so it only fires where the work yields to the event loop. Neither
|
|
11
|
-
* jsdom's parse nor axe-core's walk of the tree does, so a document pathological
|
|
12
|
-
* enough to hold the thread runs past this unimpeded. The hard ceiling is the
|
|
13
|
-
* worker pool's, which terminates the thread; see the note on `runWorkers`.
|
|
14
|
-
*/
|
|
15
|
-
const DEFAULT_TIMEOUT_MS = DEFAULT_PAGE_TIMEOUT_MS;
|
|
16
|
-
/**
|
|
17
7
|
* Audit collected pages with axe-core inside jsdom.
|
|
18
8
|
*
|
|
19
9
|
* Pages are processed sequentially: jsdom parsing and axe-core are both
|
|
@@ -40,7 +30,7 @@ async function auditPage(page, options = {}) {
|
|
|
40
30
|
dom = createDom(page.html, url);
|
|
41
31
|
injectAxe(dom);
|
|
42
32
|
const { axe: pageAxe } = dom.window;
|
|
43
|
-
const results = await withTimeout(pageAxe.run(dom.window.document, runOptions(tags, { skipBlindRules: options.fast === true })), options.timeoutMs ??
|
|
33
|
+
const results = await withTimeout(pageAxe.run(dom.window.document, runOptions(tags, { skipBlindRules: options.fast === true })), options.timeoutMs ?? 3e4);
|
|
44
34
|
return shapeResults(results, {
|
|
45
35
|
...identity,
|
|
46
36
|
durationMs: Date.now() - startedAt,
|
|
@@ -74,6 +64,15 @@ function injectAxe(dom) {
|
|
|
74
64
|
axeScript ??= new Script(axe.source, { filename: "axe-core.js" });
|
|
75
65
|
axeScript.runInContext(dom.getInternalVMContext());
|
|
76
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The per-page ceiling, and it is a soft one.
|
|
69
|
+
*
|
|
70
|
+
* This races axe-core against a timer, so it only fires where the work yields to
|
|
71
|
+
* the event loop. Neither jsdom's parse nor axe-core's walk of the tree does, so
|
|
72
|
+
* a document pathological enough to hold the thread runs past this unimpeded.
|
|
73
|
+
* The hard ceiling is the worker pool's, which terminates the thread; see the
|
|
74
|
+
* note on `runWorkers`.
|
|
75
|
+
*/
|
|
77
76
|
async function withTimeout(promise, ms) {
|
|
78
77
|
promise.catch(() => {});
|
|
79
78
|
let timer;
|
|
@@ -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 {
|
|
5
|
-
import { t as buildCoverage } from "./coverage-
|
|
4
|
+
import { m as uniqueRuleOutcomes, u as runEngine } from "./result-B7_zVxAX.js";
|
|
5
|
+
import { t as buildCoverage } from "./coverage-cb5kPp48.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) {
|
|
@@ -109,7 +104,8 @@ function toJsonPage(audit) {
|
|
|
109
104
|
incomplete: [...audit.incomplete].sort(byRuleId).map(toJsonIncomplete),
|
|
110
105
|
passes: [...audit.passes].sort(byRuleId).map((outcome) => outcome.ruleId),
|
|
111
106
|
inapplicable: [...audit.inapplicable].sort(byRuleId).map((outcome) => outcome.ruleId),
|
|
112
|
-
error: audit.error ?? null
|
|
107
|
+
error: audit.error ?? null,
|
|
108
|
+
...audit.reused === void 0 ? {} : { reusedFrom: audit.reused.on }
|
|
113
109
|
};
|
|
114
110
|
}
|
|
115
111
|
function toJsonFinding(finding) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { r as serialiseJsonReport, t as buildJsonReport } from "./json-
|
|
1
|
+
import { r as serialiseJsonReport, t as buildJsonReport } from "./json-Bus6YhxB.js";
|
|
2
2
|
export { buildJsonReport, serialiseJsonReport };
|
|
@@ -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,12 +122,21 @@ 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
|
|
128
132
|
* being described, and because a config file has no flags to negate.
|
|
129
133
|
*/
|
|
130
|
-
build: optional(boolean())
|
|
134
|
+
build: optional(boolean()),
|
|
135
|
+
/**
|
|
136
|
+
* False is `--no-cache`: audit every page, reusing nothing an earlier run
|
|
137
|
+
* measured. Written in the positive for the same reason `build` is.
|
|
138
|
+
*/
|
|
139
|
+
cache: optional(boolean())
|
|
131
140
|
});
|
|
132
141
|
const configSchema = object({
|
|
133
142
|
site: object({
|
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-BK2blxMI.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-DrS0Komf.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 {
|
|
2
|
+
import { c as pageUrl, d as runOptions, f as shapeResults, n as DEFAULT_TAGS, o as failedPage } from "./result-B7_zVxAX.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 {
|
|
2
|
+
import { c as pageUrl, o as failedPage } from "./result-B7_zVxAX.js";
|
|
3
3
|
import { Worker } from "node:worker_threads";
|
|
4
4
|
import { availableParallelism } from "node:os";
|
|
5
5
|
//#region src/audit/runners/pool.ts
|
|
@@ -105,8 +105,12 @@ async function runPooledAudit(pages, options = {}) {
|
|
|
105
105
|
if (!entry) return auditHere(pages, runnerOptions);
|
|
106
106
|
return runWorkers(pages, runnerOptions, Math.min(workers, pages.length), entry);
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Audit in this process: the fallback for a run with no threads, and the sweep
|
|
110
|
+
* for pages the threads never reported on.
|
|
111
|
+
*/
|
|
108
112
|
async function auditHere(pages, options) {
|
|
109
|
-
const { runJsdomAudit } = await import("./jsdom-
|
|
113
|
+
const { runJsdomAudit } = await import("./jsdom-BUYuVJJm.js");
|
|
110
114
|
return runJsdomAudit(pages, options);
|
|
111
115
|
}
|
|
112
116
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { o as impactRank } from "./impact-luGCnN8F.js";
|
|
2
2
|
import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
|
|
3
|
-
import {
|
|
3
|
+
import { s as findingElements } from "./result-B7_zVxAX.js";
|
|
4
4
|
//#region src/audit/completeness.ts
|
|
5
5
|
/**
|
|
6
6
|
* Fold what the collector knew together with what the engine managed.
|
|
@@ -10,14 +10,28 @@ import { o as findingElements } from "./result-BWcYXeRs.js";
|
|
|
10
10
|
*/
|
|
11
11
|
function runCompleteness(audits, collection) {
|
|
12
12
|
const errored = audits.filter((audit) => audit.error).length;
|
|
13
|
+
const reused = audits.filter((audit) => audit.reused !== void 0).length;
|
|
13
14
|
return {
|
|
14
15
|
...collection,
|
|
15
|
-
audited: audits.length - errored,
|
|
16
|
+
audited: audits.length - errored - reused,
|
|
17
|
+
reused,
|
|
16
18
|
errored,
|
|
17
19
|
complete: collection.unreachable.length === 0 && !collection.truncated && errored === 0
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
23
|
+
* What a run reused, in one phrase, or nothing when it audited everything.
|
|
24
|
+
*
|
|
25
|
+
* Its own sentence rather than a clause in `missedParts`: nothing was missed
|
|
26
|
+
* here. The pages have verdicts; they were reached on an earlier day, on markup
|
|
27
|
+
* identical to today's, and a reader is entitled to know which.
|
|
28
|
+
*/
|
|
29
|
+
function reusedPart(completeness) {
|
|
30
|
+
if (completeness.reused === 0) return void 0;
|
|
31
|
+
const pages = completeness.reused === 1 ? "page" : "pages";
|
|
32
|
+
return `${completeness.reused} ${pages} unchanged since the last run, so the earlier result was reused`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
21
35
|
* What was missed, as phrases both reports print.
|
|
22
36
|
*
|
|
23
37
|
* Separate clauses rather than one total, for the same reason the coverage
|
|
@@ -95,6 +109,23 @@ function groupIssues(audits) {
|
|
|
95
109
|
return issues;
|
|
96
110
|
}
|
|
97
111
|
/**
|
|
112
|
+
* What the grouped issues add up to: distinct elements, and the violations they
|
|
113
|
+
* account for. Both reports open their issues section with these two numbers,
|
|
114
|
+
* and say something different when they are equal.
|
|
115
|
+
*/
|
|
116
|
+
function issueTotals(issues) {
|
|
117
|
+
let elements = 0;
|
|
118
|
+
let occurrences = 0;
|
|
119
|
+
for (const issue of issues) {
|
|
120
|
+
elements += issue.elements.length;
|
|
121
|
+
occurrences += issue.occurrences;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
elements,
|
|
125
|
+
occurrences
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
98
129
|
* Unevaluated rules folded across the site, sorted by rule id.
|
|
99
130
|
*
|
|
100
131
|
* Both reports list these once at the end rather than under every page: on a
|
|
@@ -318,4 +349,4 @@ function remediationFor(ruleId, framework) {
|
|
|
318
349
|
};
|
|
319
350
|
}
|
|
320
351
|
//#endregion
|
|
321
|
-
export { isShared as a,
|
|
352
|
+
export { isShared as a, missedParts as c, groupIssues as i, reusedPart as l, blindRules as n, issueTotals as o, coverageParts as r, discoveryLabel as s, remediationFor as t, runCompleteness as u };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as IMPACT_LEVELS, o as impactRank } from "./impact-luGCnN8F.js";
|
|
2
2
|
import { c as nullable, h as string, l as number, m as safeParse, p as record, s as isoDateTime, t as array, u as object, y as withDefault } from "./schema-DJSF4K05.js";
|
|
3
|
-
import { u as STATEMENT_LOCALES } from "./load-
|
|
3
|
+
import { u as STATEMENT_LOCALES } from "./load-BaWMa-G_.js";
|
|
4
4
|
import { n as isDirectory } from "./fs-BmPtmFke.js";
|
|
5
|
-
import { i as escapeText,
|
|
5
|
+
import { a as isoDate, i as escapeText, n as count, r as escapeAttribute, s as standardsReference } from "./text-BF1LHMiV.js";
|
|
6
6
|
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
7
|
+
import { t as WCAG22_AA_CRITERIA } from "./criteria-CnPJKGeb.js";
|
|
7
8
|
import { readFile, readdir } from "node:fs/promises";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -20,6 +21,107 @@ import { fileURLToPath } from "node:url";
|
|
|
20
21
|
var StatementError = class extends Error {
|
|
21
22
|
name = "StatementError";
|
|
22
23
|
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/statement/evidence.ts
|
|
26
|
+
/**
|
|
27
|
+
* How old a claim or a report may be before the document says so.
|
|
28
|
+
*
|
|
29
|
+
* A year, because a statement is a document about a site that changes, and one
|
|
30
|
+
* dated further back than that is describing a site nobody has checked since.
|
|
31
|
+
* It is a warning and never a refusal: how often a statement must be revisited
|
|
32
|
+
* is a question about a legal regime and a rate of change, and this tool is in
|
|
33
|
+
* no position to answer it for somebody.
|
|
34
|
+
*/
|
|
35
|
+
const STALE_DAYS = 365;
|
|
36
|
+
function checkStatementEvidence(input) {
|
|
37
|
+
const today = input.today ?? /* @__PURE__ */ new Date();
|
|
38
|
+
return [
|
|
39
|
+
...claimAgainstAudit(input),
|
|
40
|
+
...claimAgainstReview(input),
|
|
41
|
+
...dates(input, today)
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
/** Whether any problem is bad enough to stop the document being written. */
|
|
45
|
+
function refuses(problems) {
|
|
46
|
+
return problems.some((problem) => problem.severity === "refuses");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A claim of full conformance, against barriers the audit found.
|
|
50
|
+
*
|
|
51
|
+
* Refused rather than warned about. Every other output of this tool is a report
|
|
52
|
+
* somebody reads; this one is a legal document published under their name, and
|
|
53
|
+
* the failure mode is not a wrong number in a terminal but a false statement on
|
|
54
|
+
* a website. The fix is in the message because it is not obvious to somebody
|
|
55
|
+
* filling in a config file for the first time that `partially-compliant` is the
|
|
56
|
+
* ordinary answer rather than an admission of defeat.
|
|
57
|
+
*/
|
|
58
|
+
function claimAgainstAudit(input) {
|
|
59
|
+
const audit = input.audit;
|
|
60
|
+
if (audit === void 0 || input.config.compliance.status !== "compliant") return [];
|
|
61
|
+
if (audit.findings.length === 0) return [];
|
|
62
|
+
const pages = new Set(audit.findings.flatMap((finding) => finding.pages)).size;
|
|
63
|
+
return [{
|
|
64
|
+
severity: "refuses",
|
|
65
|
+
message: `The config claims full conformance, and the audit report lists ${count(audit.findings.length, "barrier")} on ${count(pages, "page")}.\n A statement claiming full conformance for a site that is not conformant is worse\n than no statement at all, so this one was not written.\n Either fix the barriers and audit again, or set compliance.status to\n "partially-compliant", which is the honest answer for most sites and the one that\n carries the obligation to list what is missing.`
|
|
66
|
+
}];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A claim of full conformance, against criteria a person recorded as not met.
|
|
70
|
+
*
|
|
71
|
+
* The review record is the only evidence this tool has about the 34 criteria no
|
|
72
|
+
* engine can reach, and a `not-met` in it is somebody's own finding. Claiming
|
|
73
|
+
* full conformance over the top of it is the same false statement as claiming
|
|
74
|
+
* it over the top of a violation, so it gets the same refusal.
|
|
75
|
+
*/
|
|
76
|
+
function claimAgainstReview(input) {
|
|
77
|
+
const review = input.review;
|
|
78
|
+
if (review === void 0 || input.config.compliance.status !== "compliant") return [];
|
|
79
|
+
const notMet = Object.entries(review.criteria).filter(([, entry]) => entry.result === "not-met").map(([number]) => number);
|
|
80
|
+
if (notMet.length === 0) return [];
|
|
81
|
+
return [{
|
|
82
|
+
severity: "refuses",
|
|
83
|
+
message: `The config claims full conformance, and the review record has ${count(notMet.length, "criterion")} recorded as not met: ${notMet.join(", ")}.\n Somebody checked those and wrote down that the site does not meet them, so this\n statement was not written. Fix them and record the result, or set\n compliance.status to "partially-compliant" and describe them in\n compliance.knownIssues.`
|
|
84
|
+
}];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether the document's dates still describe the site.
|
|
88
|
+
*
|
|
89
|
+
* All warnings. A date being old is not a false claim — it is a document nobody
|
|
90
|
+
* has revisited, which is a different problem with a different fix, and the
|
|
91
|
+
* person publishing it is the one who knows whether the site has moved.
|
|
92
|
+
*/
|
|
93
|
+
function dates(input, today) {
|
|
94
|
+
const problems = [];
|
|
95
|
+
const assessedOn = input.config.compliance.assessedOn;
|
|
96
|
+
const now = isoDate(today);
|
|
97
|
+
if (assessedOn > now) problems.push({
|
|
98
|
+
severity: "warns",
|
|
99
|
+
message: `compliance.assessedOn is ${assessedOn}, which is in the future. A statement dated ahead of its own assessment is a typo somebody will notice.`
|
|
100
|
+
});
|
|
101
|
+
else if (daysBetween(assessedOn, now) > STALE_DAYS) problems.push({
|
|
102
|
+
severity: "warns",
|
|
103
|
+
message: `compliance.assessedOn is ${assessedOn}, ${count(daysBetween(assessedOn, now), "day")} ago. The statement will say so, and it describes a site nobody has assessed since.`
|
|
104
|
+
});
|
|
105
|
+
const audit = input.audit;
|
|
106
|
+
if (audit === void 0) return problems;
|
|
107
|
+
const auditDay = audit.generatedAt.slice(0, 10);
|
|
108
|
+
if (auditDay > assessedOn) problems.push({
|
|
109
|
+
severity: "warns",
|
|
110
|
+
message: `The audit ran on ${auditDay}, after the ${assessedOn} this statement gives as its assessment date. Move compliance.assessedOn forward, or cite the report from that day.`
|
|
111
|
+
});
|
|
112
|
+
if (daysBetween(auditDay, now) > STALE_DAYS) problems.push({
|
|
113
|
+
severity: "warns",
|
|
114
|
+
message: `The audit report is from ${auditDay}, ${count(daysBetween(auditDay, now), "day")} ago. The barriers it lists are the ones the site had then.`
|
|
115
|
+
});
|
|
116
|
+
return problems;
|
|
117
|
+
}
|
|
118
|
+
/** Whole days between two ISO dates, both UTC, without a date library. */
|
|
119
|
+
function daysBetween(from, to) {
|
|
120
|
+
const start = Date.parse(`${from}T00:00:00Z`);
|
|
121
|
+
const end = Date.parse(`${to}T00:00:00Z`);
|
|
122
|
+
if (Number.isNaN(start) || Number.isNaN(end)) return 0;
|
|
123
|
+
return Math.round((end - start) / 864e5);
|
|
124
|
+
}
|
|
23
125
|
/**
|
|
24
126
|
* Only the fields the statement reads. Everything else in the report — node
|
|
25
127
|
* markup, selectors, passes, inapplicable — is audit detail with no place in a
|
|
@@ -412,7 +514,7 @@ async function renderStatement(config, options = {}) {
|
|
|
412
514
|
const country = options.country ?? config.enforcement.country;
|
|
413
515
|
const locale = options.locale ?? defaultLocale(config);
|
|
414
516
|
const template = `${country.toLowerCase()}.${locale}`;
|
|
415
|
-
const markdown = tidy(renderTemplate(await loadTemplate(country, locale), buildScope(config, locale, options
|
|
517
|
+
const markdown = tidy(renderTemplate(await loadTemplate(country, locale), buildScope(config, locale, options)));
|
|
416
518
|
return {
|
|
417
519
|
markdown,
|
|
418
520
|
html: toHtmlDocument(markdown, {
|
|
@@ -442,7 +544,9 @@ function defaultLocale(config) {
|
|
|
442
544
|
* template, which keeps the template language trivial and puts the mapping
|
|
443
545
|
* somewhere that can be typechecked.
|
|
444
546
|
*/
|
|
445
|
-
function buildScope(config, locale,
|
|
547
|
+
function buildScope(config, locale, options) {
|
|
548
|
+
const audit = options.audit;
|
|
549
|
+
const review = options.review ? toReviewScope(options.review, locale) : void 0;
|
|
446
550
|
const issues = [...config.compliance.knownIssues.map((issue) => toIssueScope(issue, locale)), ...(audit?.findings ?? []).map((finding) => toFindingScope(finding, config.compliance.auditReason))];
|
|
447
551
|
return {
|
|
448
552
|
site: { ...config.site },
|
|
@@ -459,6 +563,8 @@ function buildScope(config, locale, audit) {
|
|
|
459
563
|
},
|
|
460
564
|
audit: audit ? toAuditScope(audit, locale) : void 0,
|
|
461
565
|
hasAudit: audit !== void 0,
|
|
566
|
+
review,
|
|
567
|
+
hasReview: review !== void 0,
|
|
462
568
|
hasKnownIssues: issues.length > 0,
|
|
463
569
|
hasNoKnownIssues: issues.length === 0
|
|
464
570
|
};
|
|
@@ -484,6 +590,39 @@ function toAuditScope(audit, locale) {
|
|
|
484
590
|
checkedOnFormatted: formatDate(audit.generatedAt.slice(0, 10), locale)
|
|
485
591
|
};
|
|
486
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* What a person's review contributes to the "preparation" section.
|
|
595
|
+
*
|
|
596
|
+
* One fact: how many of WCAG 2.2's success criteria at A and AA somebody
|
|
597
|
+
* checked by hand, and the last day one of those checks was recorded. It is
|
|
598
|
+
* there because the automated sentences beside it describe a run that cannot
|
|
599
|
+
* reach 34 of those 55 criteria, and a reader with no way to tell an unchecked
|
|
600
|
+
* criterion from an unchecked-by-machine one is being told less than the truth.
|
|
601
|
+
*
|
|
602
|
+
* What it never says is what the review concluded. A recorded result is a claim
|
|
603
|
+
* by a person, and the place for a claim in this document is the barrier list,
|
|
604
|
+
* where a person writes it in their own words. Turning `not-met` into a
|
|
605
|
+
* conformance sentence here would be this tool putting a legal position in
|
|
606
|
+
* somebody's mouth.
|
|
607
|
+
*
|
|
608
|
+
* Returns undefined for a record nobody has answered yet — the ordinary state
|
|
609
|
+
* of the file `eaa-kit checklist` has just written. Generating a worksheet is
|
|
610
|
+
* not doing the review, and the audit report refuses to count it for the same
|
|
611
|
+
* reason.
|
|
612
|
+
*/
|
|
613
|
+
function toReviewScope(record, locale) {
|
|
614
|
+
const answered = Object.values(record.criteria).filter((entry) => entry.result !== "unreviewed");
|
|
615
|
+
if (answered.length === 0) return void 0;
|
|
616
|
+
const latest = answered.map((entry) => entry.reviewedOn).filter((on) => on !== void 0).sort().at(-1);
|
|
617
|
+
return {
|
|
618
|
+
answered: answered.length,
|
|
619
|
+
total: WCAG22_AA_CRITERIA.length,
|
|
620
|
+
isSingle: answered.length === 1,
|
|
621
|
+
isPlural: answered.length > 1,
|
|
622
|
+
hasDate: latest !== void 0,
|
|
623
|
+
checkedOnFormatted: latest === void 0 ? "" : formatDate(latest, locale)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
487
626
|
function toIssueScope(issue, locale) {
|
|
488
627
|
return {
|
|
489
628
|
...reasonScope(issue.reason),
|
|
@@ -614,4 +753,4 @@ async function findTemplateDirectory() {
|
|
|
614
753
|
throw new StatementError(`Could not locate the statement templates. Looked in: ${candidates.join(", ")}`);
|
|
615
754
|
}
|
|
616
755
|
//#endregion
|
|
617
|
-
export { summariseAuditReport as a, readAuditReport as i, toHtmlBody as n,
|
|
756
|
+
export { summariseAuditReport as a, StatementError as c, readAuditReport as i, toHtmlBody as n, checkStatementEvidence as o, toHtmlDocument as r, refuses as s, renderStatement as t };
|
|
@@ -131,6 +131,29 @@ function ruleOutcomes(audit) {
|
|
|
131
131
|
...audit.inapplicable
|
|
132
132
|
];
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Which engine produced this run, as every report format has to label it.
|
|
136
|
+
*
|
|
137
|
+
* Taken from the first page rather than tracked separately: a run uses one
|
|
138
|
+
* engine for all of its pages, and an empty run has none to report, which is
|
|
139
|
+
* the jsdom default the CLI would have used.
|
|
140
|
+
*/
|
|
141
|
+
function runEngine(audits) {
|
|
142
|
+
return audits[0]?.engine ?? "jsdom";
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Every rule the run mentions anywhere, once each, sorted by id.
|
|
146
|
+
*
|
|
147
|
+
* Both machine-readable formats need this and neither can take it from the
|
|
148
|
+
* other: the JSON report keys its `rules` map by id, and SARIF needs the same
|
|
149
|
+
* catalogue as a list its results index into. First mention wins because a
|
|
150
|
+
* rule's help text and criteria do not vary between the pages it appears on.
|
|
151
|
+
*/
|
|
152
|
+
function uniqueRuleOutcomes(audits) {
|
|
153
|
+
const byId = /* @__PURE__ */ new Map();
|
|
154
|
+
for (const audit of audits) for (const outcome of ruleOutcomes(audit)) if (!byId.has(outcome.ruleId)) byId.set(outcome.ruleId, outcome);
|
|
155
|
+
return [...byId.values()].sort((a, b) => a.ruleId.localeCompare(b.ruleId));
|
|
156
|
+
}
|
|
134
157
|
function runOptions(tags, options = {}) {
|
|
135
158
|
return {
|
|
136
159
|
runOnly: {
|
|
@@ -313,4 +336,4 @@ function enClauses(tags) {
|
|
|
313
336
|
return [...clauses].sort();
|
|
314
337
|
}
|
|
315
338
|
//#endregion
|
|
316
|
-
export {
|
|
339
|
+
export { enClauses as a, pageUrl as c, runOptions as d, shapeResults as f, blindRulesInScope as i, ruleOutcomes as l, uniqueRuleOutcomes as m, DEFAULT_TAGS as n, failedPage as o, successCriteria as p, ENGINE_BLIND_RULES as r, findingElements as s, DEFAULT_PAGE_TIMEOUT_MS as t, runEngine as u };
|