eaa-kit 0.2.1 → 0.3.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 +2 -1
- package/dist/astro/index.d.ts +7 -34
- package/dist/astro/index.js +6 -26
- package/dist/audit/runners/worker.js +1 -1
- package/dist/audit-CuG2hYyo.js +2 -0
- package/dist/audit-DcL73mOC.js +635 -0
- package/dist/{baseline-CV_3lbER.js → baseline-CuKFq4IF.js} +1 -1
- package/dist/{baseline-CgBmzFTr.js → baseline-s9F3fXTN.js} +16 -26
- package/dist/cli/index.js +80 -143
- package/dist/collect-BkAQ0viT.js +123 -0
- package/dist/command-Dxpa00Ha.js +77 -0
- package/dist/component-7kEBjv_y.js +111 -0
- package/dist/{crawl-CtJbMNNb.js → crawl-Oxt2Gaqo.js} +13 -23
- package/dist/frameworks-BYa3tULg.js +243 -0
- package/dist/frameworks-DaDqrJOw.js +2 -0
- package/dist/fs-BmPtmFke.js +40 -0
- package/dist/{html-BLEuzep6.js → html-DKiI_3gs.js} +167 -86
- package/dist/{impact-DvgBjupx.js → impact-YdoOtFqm.js} +15 -1
- package/dist/index.js +2 -2
- package/dist/{init-DRKIdpK1.js → init-DiLiYvN1.js} +13 -10
- package/dist/{jsdom-BEu6Ra_2.js → jsdom-BjpF-2V-.js} +2 -7
- package/dist/jsdom-X4KYfTp8.js +3 -0
- package/dist/json-B0Y7rNjt.js +2 -0
- package/dist/{json-C9xS1PNC.js → json-Cnv9nd6U.js} +13 -21
- package/dist/{load-sCkKsvGQ.js → load-UYXLqGV9.js} +2 -8
- package/dist/manual-Vz-oX1I_.js +239 -0
- package/dist/{playwright-DSRnXmcd.js → playwright-DYFsGUNd.js} +70 -23
- package/dist/{pool-DixLeu8L.js → pool-BWkWZiJW.js} +4 -12
- package/dist/{project-CufCqIE2.js → project-DW08TseF.js} +8 -21
- package/dist/{render-BO0nVrrZ.js → render-DI_aCnAZ.js} +7 -15
- package/dist/{result-2aZPfM8w.js → result-DLxd2Eip.js} +38 -1
- package/dist/{routes-BxbSZKXC.js → routes-C2Cgf6Ko.js} +4 -8
- package/dist/run-BMASMmwO.d.ts +45 -0
- package/dist/run-BW6CVuND.js +36 -0
- package/dist/{sarif-eSCuI0eX.js → sarif-DB3WG7T9.js} +11 -33
- package/dist/text-BFmNtMsV.js +43 -0
- package/dist/vite/index.d.ts +39 -0
- package/dist/vite/index.js +40 -0
- package/package.json +8 -3
- package/dist/audit-B2dIKpJ5.js +0 -2
- package/dist/audit-DXpKkXsC.js +0 -950
- package/dist/escape-Dm1o_RAk.js +0 -21
- package/dist/jsdom-C6dIyaxN.js +0 -3
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { i as
|
|
2
|
-
import {
|
|
1
|
+
import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-YdoOtFqm.js";
|
|
2
|
+
import { a as standardsReference, i as escapeText, n as count, r as escapeAttribute, t as collapse } from "./text-BFmNtMsV.js";
|
|
3
3
|
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
4
|
+
import { a as groupIssues, i as coverageParts, n as understandingUrl, o as isShared, r as blindRules, t as manualCheckFor } from "./manual-Vz-oX1I_.js";
|
|
5
|
+
import { n as buildSummary } from "./json-Cnv9nd6U.js";
|
|
4
6
|
import axe from "axe-core";
|
|
5
7
|
//#region src/audit/report/html.ts
|
|
6
8
|
/**
|
|
@@ -19,6 +21,14 @@ import axe from "axe-core";
|
|
|
19
21
|
* found, which is not the same as a site being accessible, and the document
|
|
20
22
|
* says so in its own footer rather than leaving the reader to infer it.
|
|
21
23
|
*/
|
|
24
|
+
/** Severity levels, worst first, as both the scoreboard and the summary list them. */
|
|
25
|
+
const IMPACT_ORDER = [
|
|
26
|
+
"critical",
|
|
27
|
+
"serious",
|
|
28
|
+
"moderate",
|
|
29
|
+
"minor",
|
|
30
|
+
"unclassified"
|
|
31
|
+
];
|
|
22
32
|
/** Elements listed per rule before the rest are summarised. */
|
|
23
33
|
const MAX_NODES = 5;
|
|
24
34
|
/** Longest element markup shown before it is truncated. */
|
|
@@ -43,6 +53,8 @@ ${STYLES}
|
|
|
43
53
|
<main>
|
|
44
54
|
<h1>Accessibility audit</h1>
|
|
45
55
|
${verdict(audits, failing, options)}
|
|
56
|
+
${scoreboard(audits)}
|
|
57
|
+
${issues(audits, options)}
|
|
46
58
|
${runDetails(audits, engine, generatedAt, options)}
|
|
47
59
|
${summary(audits, failing, options)}
|
|
48
60
|
${pages(audits)}
|
|
@@ -84,45 +96,27 @@ function runDetails(audits, engine, generatedAt, options) {
|
|
|
84
96
|
if (options.baseUrl) rows.splice(1, 0, ["Base URL", options.baseUrl]);
|
|
85
97
|
return `<h2>The run</h2>\n<dl class="run">\n${rows.map(([term, value]) => ` <div><dt>${escapeText(term)}</dt><dd>${escapeText(value)}</dd></div>`).join("\n")}\n</dl>`;
|
|
86
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* The counts, taken from the JSON report's own tally rather than recomputed.
|
|
101
|
+
*
|
|
102
|
+
* Two reports of one run disagreeing about how many violations there were is
|
|
103
|
+
* the kind of bug nobody notices until a client does, so there is one place
|
|
104
|
+
* that counts.
|
|
105
|
+
*/
|
|
87
106
|
function summary(audits, failing, options) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
let blind = 0;
|
|
91
|
-
let passes = 0;
|
|
92
|
-
let inapplicable = 0;
|
|
93
|
-
let elements = 0;
|
|
94
|
-
let accepted = 0;
|
|
95
|
-
for (const audit of audits) {
|
|
96
|
-
for (const finding of audit.accepted ?? []) accepted += finding.nodes.length;
|
|
97
|
-
for (const finding of audit.violations) {
|
|
98
|
-
const impact = finding.impact && isImpactLevel(finding.impact) ? finding.impact : "unclassified";
|
|
99
|
-
byImpact.set(impact, (byImpact.get(impact) ?? 0) + 1);
|
|
100
|
-
elements += finding.nodes.length;
|
|
101
|
-
}
|
|
102
|
-
for (const finding of audit.incomplete) if (finding.reason === "engine-limitation") blind += 1;
|
|
103
|
-
else needsReview += 1;
|
|
104
|
-
passes += audit.passes.length;
|
|
105
|
-
inapplicable += audit.inapplicable.length;
|
|
106
|
-
}
|
|
107
|
-
const impacts = [
|
|
108
|
-
"critical",
|
|
109
|
-
"serious",
|
|
110
|
-
"moderate",
|
|
111
|
-
"minor",
|
|
112
|
-
"unclassified"
|
|
113
|
-
].filter((impact) => (byImpact.get(impact) ?? 0) > 0).map((impact) => ` <li><span class="badge ${escapeAttribute(impact)}">${escapeText(impact)}</span> ${byImpact.get(impact)}</li>`).join("\n");
|
|
114
|
-
const withViolations = audits.filter((audit) => audit.violations.length > 0).length;
|
|
107
|
+
const totals = buildSummary(audits, options.failOn);
|
|
108
|
+
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");
|
|
115
109
|
return `<h2>Summary</h2>
|
|
116
110
|
<ul class="counts">
|
|
117
|
-
<li><strong>${count(
|
|
111
|
+
<li><strong>${count(totals.violations, "violation")}</strong> on ${totals.pagesWithViolations} of ${count(totals.pages, "page")}, across ${count(totals.violatingElements, "element")}</li>
|
|
118
112
|
<li><strong>${failing}</strong> at or above ${escapeText(options.failOn)}</li>
|
|
119
|
-
<li><strong>${needsReview}</strong> ${needsReview === 1 ? "rule needs" : "rules need"} manual review</li>
|
|
120
|
-
<li><strong>${
|
|
121
|
-
${accepted > 0 ? ` <li><strong>${accepted}</strong> ${accepted === 1 ? "element is" : "elements are"} accepted by the baseline, and not counted above</li>` : ""}
|
|
113
|
+
<li><strong>${totals.needsReview}</strong> ${totals.needsReview === 1 ? "rule needs" : "rules need"} manual review</li>
|
|
114
|
+
<li><strong>${totals.notEvaluated}</strong> ${totals.notEvaluated === 1 ? "rule was" : "rules were"} not evaluated by this engine</li>
|
|
115
|
+
${totals.accepted > 0 ? ` <li><strong>${totals.accepted}</strong> ${totals.accepted === 1 ? "element is" : "elements are"} accepted by the baseline, and not counted above</li>` : ""}
|
|
122
116
|
</ul>
|
|
123
117
|
${impacts ? `<ul class="impacts">\n${impacts}\n</ul>` : ""}
|
|
124
118
|
<p class="note">
|
|
125
|
-
<strong>${passes}</strong> rule results were checked and met, and <strong>${inapplicable}</strong>
|
|
119
|
+
<strong>${totals.passes}</strong> rule results were checked and met, and <strong>${totals.inapplicable}</strong>
|
|
126
120
|
found nothing on the page to check. Those two are counted separately and never added
|
|
127
121
|
together: a rule with nothing to check is not a rule that passed, and a page with no
|
|
128
122
|
images proves nothing about image alternatives.
|
|
@@ -155,12 +149,12 @@ function pageSection(audit) {
|
|
|
155
149
|
return parts.join("\n");
|
|
156
150
|
}
|
|
157
151
|
function violation(finding) {
|
|
158
|
-
const impact =
|
|
152
|
+
const impact = impactLabel(finding.impact);
|
|
159
153
|
const shown = finding.nodes.slice(0, MAX_NODES);
|
|
160
154
|
const remaining = finding.nodes.length - shown.length;
|
|
161
155
|
const nodes = shown.map((node) => ` <li>
|
|
162
156
|
<code class="selector">${escapeText(node.target.join(" "))}</code>
|
|
163
|
-
<pre><code>${escapeText(
|
|
157
|
+
<pre><code>${escapeText(collapse(node.html, MAX_SNIPPET))}</code></pre>
|
|
164
158
|
</li>`).join("\n");
|
|
165
159
|
const more = remaining > 0 ? `\n <li class="more">and ${count(remaining, "more element")}</li>` : "";
|
|
166
160
|
return ` <li class="finding">
|
|
@@ -175,19 +169,8 @@ ${nodes}${more}
|
|
|
175
169
|
</ul>
|
|
176
170
|
</li>`;
|
|
177
171
|
}
|
|
178
|
-
/**
|
|
179
|
-
* What this page's result rests on.
|
|
180
|
-
*
|
|
181
|
-
* The four counts stay apart for the same reason they do everywhere else: only
|
|
182
|
-
* `passed` is evidence that anything was met here.
|
|
183
|
-
*/
|
|
184
172
|
function coverage(audit) {
|
|
185
|
-
|
|
186
|
-
const review = audit.incomplete.length - blind;
|
|
187
|
-
const parts = [`${audit.passes.length} passed`, `${audit.inapplicable.length} not applicable`];
|
|
188
|
-
if (review > 0) parts.push(`${review} to review`);
|
|
189
|
-
if (blind > 0) parts.push(`${blind} not evaluated`);
|
|
190
|
-
return `<p class="coverage">${escapeText(parts.join(" · "))}</p>`;
|
|
173
|
+
return `<p class="coverage">${escapeText(coverageParts(audit).join(" · "))}</p>`;
|
|
191
174
|
}
|
|
192
175
|
/**
|
|
193
176
|
* Rules this engine reached no verdict on, listed once at the end.
|
|
@@ -196,24 +179,17 @@ function coverage(audit) {
|
|
|
196
179
|
* the reader take the rest of the document for full coverage.
|
|
197
180
|
*/
|
|
198
181
|
function notEvaluated(audits) {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
if (finding.reason !== "engine-limitation") continue;
|
|
202
|
-
const existing = byRule.get(finding.ruleId);
|
|
203
|
-
if (existing) existing.pages += 1;
|
|
204
|
-
else byRule.set(finding.ruleId, {
|
|
205
|
-
pages: 1,
|
|
206
|
-
detail: finding.reasonDetail,
|
|
207
|
-
finding
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
if (byRule.size === 0) return "";
|
|
182
|
+
const blind = blindRules(audits);
|
|
183
|
+
if (blind.length === 0) return "";
|
|
211
184
|
return `<h2>Not evaluated</h2>
|
|
212
|
-
<p>This engine reached no verdict on these rules. They are never reported as passing
|
|
185
|
+
<p>This engine reached no verdict on these rules. They are never reported as passing.
|
|
186
|
+
Each one below says what to check by hand, because automated testing finds a minority of
|
|
187
|
+
accessibility barriers and this is where the rest of them are.</p>
|
|
213
188
|
<ul class="not-evaluated">
|
|
214
|
-
${
|
|
215
|
-
<code>${escapeText(ruleId)}</code> on ${count(
|
|
216
|
-
<span class="reason">${escapeText(
|
|
189
|
+
${blind.map(({ ruleId, pages, finding }) => ` <li>
|
|
190
|
+
<code>${escapeText(ruleId)}</code> on ${count(pages, "page")}${standards(finding)}
|
|
191
|
+
<span class="reason">${escapeText(finding.reasonDetail)}</span>
|
|
192
|
+
${manualBlock(ruleId, finding)}
|
|
217
193
|
</li>`).join("\n")}
|
|
218
194
|
</ul>`;
|
|
219
195
|
}
|
|
@@ -237,33 +213,13 @@ function standards(finding) {
|
|
|
237
213
|
return text ? ` — ${text}` : "";
|
|
238
214
|
}
|
|
239
215
|
function standardsText(finding) {
|
|
240
|
-
|
|
241
|
-
return escapeText(parts.join(", "));
|
|
242
|
-
}
|
|
243
|
-
/** Element markup, on one line and bounded, so one minified page cannot fill the report. */
|
|
244
|
-
function snippet(html) {
|
|
245
|
-
const collapsed = html.replace(/\s+/g, " ").trim();
|
|
246
|
-
return collapsed.length > MAX_SNIPPET ? `${collapsed.slice(0, 199)}…` : collapsed;
|
|
216
|
+
return escapeText(standardsReference(finding.successCriteria, finding.enClauses));
|
|
247
217
|
}
|
|
248
218
|
function totalViolations(audits) {
|
|
249
219
|
return audits.reduce((total, audit) => total + audit.violations.length, 0);
|
|
250
220
|
}
|
|
251
221
|
function byImpactThenRule(a, b) {
|
|
252
|
-
|
|
253
|
-
"critical",
|
|
254
|
-
"serious",
|
|
255
|
-
"moderate",
|
|
256
|
-
"minor"
|
|
257
|
-
];
|
|
258
|
-
const rank = (finding) => {
|
|
259
|
-
const index = finding.impact ? order.indexOf(finding.impact) : -1;
|
|
260
|
-
return index === -1 ? -1 : index;
|
|
261
|
-
};
|
|
262
|
-
const difference = rank(a) - rank(b);
|
|
263
|
-
return difference === 0 ? a.ruleId.localeCompare(b.ruleId) : difference;
|
|
264
|
-
}
|
|
265
|
-
function count(value, noun) {
|
|
266
|
-
return `${value} ${noun}${value === 1 ? "" : "s"}`;
|
|
222
|
+
return impactRank(a.impact) - impactRank(b.impact) || a.ruleId.localeCompare(b.ruleId);
|
|
267
223
|
}
|
|
268
224
|
const STYLES = `:root { color-scheme: light dark; }
|
|
269
225
|
body { margin: 0; background: #ffffff; color: #1a1a1a;
|
|
@@ -310,6 +266,34 @@ p.clean { color: #216e39; }
|
|
|
310
266
|
p.note { font-size: 0.95rem; }
|
|
311
267
|
hr { border: 0; border-top: 1px solid #d4d4d4; margin: 3rem 0 1.5rem; }
|
|
312
268
|
footer { color: #4a4a4a; font-size: 0.9rem; }
|
|
269
|
+
.scoreboard { list-style: none; display: flex; flex-wrap: wrap; gap: 0.75rem;
|
|
270
|
+
padding: 0; margin: 1.25rem 0 2rem; }
|
|
271
|
+
.tile { flex: 1 1 8rem; padding: 0.75rem 1rem; border: 1px solid; border-radius: 6px; }
|
|
272
|
+
.tile .figure { display: block; font-size: 1.75rem; font-weight: 700; line-height: 1.1; }
|
|
273
|
+
.tile .label { font-size: 0.85rem; }
|
|
274
|
+
.tile.critical { background: #fdeeee; border-color: #a01b1b; color: #7a1414; }
|
|
275
|
+
.tile.serious { background: #fdf1e8; border-color: #a4531b; color: #7d3f14; }
|
|
276
|
+
.tile.moderate { background: #fdf9e3; border-color: #7a6100; color: #5c4900; }
|
|
277
|
+
.tile.minor { background: #eef2f8; border-color: #40556f; color: #33455a; }
|
|
278
|
+
.tile.unclassified { background: #f1f1f1; border-color: #565656; color: #444444; }
|
|
279
|
+
.tile.none { background: #eef7ee; border-color: #216e39; color: #17512a; }
|
|
280
|
+
.tile.unchecked { background: #f4f4f5; border-color: #565656; color: #444444; }
|
|
281
|
+
p.intro { color: #4a4a4a; }
|
|
282
|
+
p.manual { margin: 0.35rem 0; }
|
|
283
|
+
p.manual.browser, p.criteria { margin: 0.35rem 0; font-size: 0.9rem; color: #4a4a4a; }
|
|
284
|
+
ol.issues { list-style: none; padding: 0; }
|
|
285
|
+
li.issue { border: 1px solid #d7d7d7; border-radius: 6px; padding: 0.75rem 1rem;
|
|
286
|
+
margin: 0 0 1rem; }
|
|
287
|
+
li.issue h3 { margin: 0 0 0.25rem; font-size: 1rem; display: flex; flex-wrap: wrap;
|
|
288
|
+
gap: 0.5rem; align-items: baseline; }
|
|
289
|
+
li.issue .wcag { color: #4a4a4a; font-size: 0.85rem; font-weight: 400; }
|
|
290
|
+
p.help { margin: 0 0 0.75rem; }
|
|
291
|
+
.element { border-top: 1px solid #e6e6e6; padding-top: 0.75rem; margin-top: 0.75rem; }
|
|
292
|
+
.element:first-of-type { border-top: 0; padding-top: 0; margin-top: 0; }
|
|
293
|
+
p.reach, p.sources, p.selector, p.more, p.written-in { margin: 0.35rem 0; font-size: 0.9rem; color: #4a4a4a; }
|
|
294
|
+
.element details { font-size: 0.9rem; color: #4a4a4a; }
|
|
295
|
+
.element summary { cursor: pointer; }
|
|
296
|
+
ul.page-list { margin: 0.35rem 0 0; padding-left: 1.25rem; }
|
|
313
297
|
@media (prefers-color-scheme: dark) {
|
|
314
298
|
body { background: #121212; color: #ededed; }
|
|
315
299
|
a { color: #9ec1ff; }
|
|
@@ -322,6 +306,17 @@ footer { color: #4a4a4a; font-size: 0.9rem; }
|
|
|
322
306
|
.verdict.pass { background: #10240f; border-color: #7ee2a8; }
|
|
323
307
|
.verdict.fail { background: #2b1111; border-color: #ff9d9d; }
|
|
324
308
|
.verdict.broken { background: #2b2310; border-color: #ffd28a; }
|
|
309
|
+
.tile.critical { background: #2b1111; border-color: #ff9d9d; color: #ffc9c9; }
|
|
310
|
+
.tile.serious { background: #2b1d11; border-color: #ffb98a; color: #ffd7bd; }
|
|
311
|
+
.tile.moderate { background: #2b2610; border-color: #f0d264; color: #f5e3a4; }
|
|
312
|
+
.tile.minor { background: #161c25; border-color: #9db6d6; color: #c6d6ea; }
|
|
313
|
+
.tile.unclassified { background: #1f1f1f; border-color: #9a9a9a; color: #d0d0d0; }
|
|
314
|
+
.tile.none { background: #10240f; border-color: #7ee2a8; color: #b6f0cd; }
|
|
315
|
+
.tile.unchecked { background: #1e1e1e; border-color: #9a9a9a; color: #d0d0d0; }
|
|
316
|
+
p.intro, li.issue .wcag, p.reach, p.sources, p.selector, .element details,
|
|
317
|
+
p.manual.browser, p.criteria { color: #b6b6b6; }
|
|
318
|
+
li.issue { border-color: #3a3a3a; }
|
|
319
|
+
.element { border-top-color: #2e2e2e; }
|
|
325
320
|
.badge.critical { background: #2b1111; border-color: #ff9d9d; color: #ffc9c9; }
|
|
326
321
|
.badge.serious { background: #2b1c11; border-color: #ffbb8a; color: #ffd9bd; }
|
|
327
322
|
.badge.moderate { background: #262110; border-color: #ffe08a; color: #ffeec2; }
|
|
@@ -333,5 +328,91 @@ footer { color: #4a4a4a; font-size: 0.9rem; }
|
|
|
333
328
|
main { max-width: none; }
|
|
334
329
|
a { color: #000000; }
|
|
335
330
|
}`;
|
|
331
|
+
/**
|
|
332
|
+
* The counts, worst first, with what was never checked beside them.
|
|
333
|
+
*
|
|
334
|
+
* A row of severity totals is the first thing anybody reads, and on its own it
|
|
335
|
+
* is the one place this document could mislead: a client seeing "0 critical, 0
|
|
336
|
+
* serious" concludes the site is fine, when the honest reading of that same run
|
|
337
|
+
* may be that six whole rule categories could not be evaluated at all. So the
|
|
338
|
+
* unevaluated count sits in the same row, in the same type size, rather than in
|
|
339
|
+
* a section further down that nobody scrolls to.
|
|
340
|
+
*/
|
|
341
|
+
function scoreboard(audits) {
|
|
342
|
+
const counted = /* @__PURE__ */ new Map();
|
|
343
|
+
for (const issue of groupIssues(audits)) {
|
|
344
|
+
const key = impactLabel(issue.impact);
|
|
345
|
+
counted.set(key, (counted.get(key) ?? 0) + issue.occurrences);
|
|
346
|
+
}
|
|
347
|
+
const blind = blindRules(audits);
|
|
348
|
+
const tiles = IMPACT_ORDER.filter((level) => (counted.get(level) ?? 0) > 0).map((level) => `<li class="tile ${level}"><span class="figure">${counted.get(level)}</span> <span class="label">${level}</span></li>`);
|
|
349
|
+
if (tiles.length === 0) tiles.push("<li class=\"tile none\"><span class=\"figure\">0</span> <span class=\"label\">violations</span></li>");
|
|
350
|
+
if (blind.length > 0) tiles.push(`<li class="tile unchecked"><span class="figure">${blind.length}</span> <span class="label">not evaluated</span></li>`);
|
|
351
|
+
return `<ul class="scoreboard">\n${tiles.join("\n")}\n</ul>`;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* What is broken, once per element rather than once per page.
|
|
355
|
+
*
|
|
356
|
+
* The page-by-page listing further down is the same information keyed the other
|
|
357
|
+
* way round. It is the right shape for working through one page and the wrong
|
|
358
|
+
* shape for deciding what to do: on a site built from components one broken
|
|
359
|
+
* header is one line in one file, and a per-page listing reports it as many
|
|
360
|
+
* findings without ever saying they are the same defect.
|
|
361
|
+
*/
|
|
362
|
+
function issues(audits, options) {
|
|
363
|
+
const found = groupIssues(audits);
|
|
364
|
+
if (found.length === 0) return "";
|
|
365
|
+
const elements = found.reduce((total, issue) => total + issue.elements.length, 0);
|
|
366
|
+
const occurrences = found.reduce((total, issue) => total + issue.occurrences, 0);
|
|
367
|
+
const intro = occurrences === elements ? `${count(elements, "distinct element")} to fix.` : `${count(occurrences, "violation")} across the site, from ${count(elements, "distinct element")}.`;
|
|
368
|
+
return `<h2 id="issues">What to fix</h2>
|
|
369
|
+
<p class="intro">${escapeText(intro)}</p>
|
|
370
|
+
<ol class="issues">
|
|
371
|
+
${found.map((issue) => issueSection(issue, options)).join("\n")}
|
|
372
|
+
</ol>`;
|
|
373
|
+
}
|
|
374
|
+
function issueSection(issue, options) {
|
|
375
|
+
const impact = impactLabel(issue.impact);
|
|
376
|
+
const criteria = issue.successCriteria.length > 0 ? `<span class="wcag">WCAG ${escapeText(issue.successCriteria.join(" "))}</span>` : "";
|
|
377
|
+
return `<li class="issue">
|
|
378
|
+
<h3><span class="badge ${impact}">${impact}</span> <code>${escapeText(issue.ruleId)}</code> ${criteria}</h3>
|
|
379
|
+
<p class="help">${escapeText(issue.help)}</p>
|
|
380
|
+
${issue.elements.slice(0, MAX_NODES).map((element) => issueElement(element, options)).join("\n")}
|
|
381
|
+
${issue.elements.length > MAX_NODES ? `<p class="more">…and ${count(issue.elements.length - MAX_NODES, "more element")}</p>` : ""}
|
|
382
|
+
</li>`;
|
|
383
|
+
}
|
|
384
|
+
function issueElement(element, options) {
|
|
385
|
+
const sources = [...new Set(element.pages.map((page) => options.sourceFor?.(page)).filter((source) => source !== void 0))];
|
|
386
|
+
const list = element.pages.map((page) => `<li>${escapeText(page)}</li>`).join("");
|
|
387
|
+
const component = options.componentFor?.(element.html);
|
|
388
|
+
return `<div class="element">
|
|
389
|
+
<pre><code>${escapeText(collapse(element.html, MAX_SNIPPET))}</code></pre>
|
|
390
|
+
${component === void 0 ? "" : `<p class="written-in">Written in <code>${escapeText(component)}</code></p>`}
|
|
391
|
+
${element.selector === "" ? "" : `<p class="selector"><code>${escapeText(element.selector)}</code></p>`}
|
|
392
|
+
<p class="reach">Found on ${count(element.pages.length, "page")}.${isShared(element) ? " <strong>Identical on each — likely one shared component.</strong>" : ""}</p>
|
|
393
|
+
${sources.length > 0 ? `<p class="sources">Source: ${sources.map((source) => `<code>${escapeText(source)}</code>`).join(", ")}</p>` : ""}
|
|
394
|
+
<details><summary>${count(element.pages.length, "page")}</summary><ul class="page-list">${list}</ul></details>
|
|
395
|
+
</div>`;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* The manual check for a rule, and where to read what the criterion requires.
|
|
399
|
+
*
|
|
400
|
+
* Unconditional here, unlike the console report, which hides it behind a flag.
|
|
401
|
+
* This document is read once by somebody deciding what to do, not printed on
|
|
402
|
+
* every run by somebody who has read it before.
|
|
403
|
+
*/
|
|
404
|
+
function manualBlock(ruleId, finding) {
|
|
405
|
+
const manual = manualCheckFor(ruleId);
|
|
406
|
+
const links = finding.successCriteria.map((criterion) => {
|
|
407
|
+
const url = understandingUrl(criterion);
|
|
408
|
+
return url === void 0 ? void 0 : `<a href="${escapeAttribute(url)}">WCAG ${escapeText(criterion)}</a>`;
|
|
409
|
+
}).filter((link) => link !== void 0);
|
|
410
|
+
if (manual === void 0 && links.length === 0) return "";
|
|
411
|
+
return [
|
|
412
|
+
manual === void 0 ? "" : ` <p class="manual">${escapeText(manual.check)}</p>`,
|
|
413
|
+
manual?.browserAnswers === true ? " <p class=\"manual browser\">Or run the audit again with <code>--browser</code>.</p>" : "",
|
|
414
|
+
links.length === 0 ? "" : ` <p class="criteria">${links.join(" · ")}</p>`
|
|
415
|
+
].filter((line) => line !== "").join("\n");
|
|
416
|
+
}
|
|
336
417
|
//#endregion
|
|
337
418
|
export { buildHtmlReport };
|
|
@@ -22,6 +22,20 @@ function meetsThreshold(impact, threshold) {
|
|
|
22
22
|
if (impact === null || !isImpactLevel(impact)) return true;
|
|
23
23
|
return IMPACT_LEVELS.indexOf(impact) >= IMPACT_LEVELS.indexOf(threshold);
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Where an impact sorts, most severe first.
|
|
27
|
+
*
|
|
28
|
+
* An unclassified impact ranks with the most severe, on the same reasoning as
|
|
29
|
+
* `--fail-on`: not knowing how bad a barrier is is not evidence that it is mild.
|
|
30
|
+
*/
|
|
31
|
+
function impactRank(impact) {
|
|
32
|
+
if (impact === null || !isImpactLevel(impact)) return 0;
|
|
33
|
+
return IMPACT_LEVELS.length - IMPACT_LEVELS.indexOf(impact);
|
|
34
|
+
}
|
|
35
|
+
/** The impact as a word, naming the gap rather than hiding it. */
|
|
36
|
+
function impactLabel(impact) {
|
|
37
|
+
return impact !== null && isImpactLevel(impact) ? impact : "unclassified";
|
|
38
|
+
}
|
|
25
39
|
/** Violations at or above `threshold`, counted per rule per page. */
|
|
26
40
|
function countAtOrAbove(audits, threshold) {
|
|
27
41
|
let total = 0;
|
|
@@ -29,4 +43,4 @@ function countAtOrAbove(audits, threshold) {
|
|
|
29
43
|
return total;
|
|
30
44
|
}
|
|
31
45
|
//#endregion
|
|
32
|
-
export {
|
|
46
|
+
export { impactRank as a, impactLabel as i, IMPACT_LEVELS as n, isImpactLevel as o, countAtOrAbove as r, DEFAULT_FAIL_ON as t };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as COMPLIANCE_STATUSES, c as ISSUE_REASONS, d as defineConfig, f as parseConfig, i as ASSESSMENT_METHODS, l as STATEMENT_LOCALES, n as findConfigFile, o as COUNTRIES, r as loadConfig, s as ConfigError, t as CONFIG_FILENAMES, u as configSchema } from "./load-
|
|
2
|
-
import { a as summariseAuditReport, i as readAuditReport, n as toHtmlBody, o as StatementError, r as toHtmlDocument, t as renderStatement } from "./render-
|
|
1
|
+
import { a as COMPLIANCE_STATUSES, c as ISSUE_REASONS, d as defineConfig, f as parseConfig, i as ASSESSMENT_METHODS, l as STATEMENT_LOCALES, n as findConfigFile, o as COUNTRIES, r as loadConfig, s as ConfigError, t as CONFIG_FILENAMES, u as configSchema } from "./load-UYXLqGV9.js";
|
|
2
|
+
import { a as summariseAuditReport, i as readAuditReport, n as toHtmlBody, o as StatementError, r as toHtmlDocument, t as renderStatement } from "./render-DI_aCnAZ.js";
|
|
3
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,5 +1,7 @@
|
|
|
1
|
-
import { o as COUNTRIES, t as CONFIG_FILENAMES } from "./load-
|
|
2
|
-
import {
|
|
1
|
+
import { o as COUNTRIES, t as CONFIG_FILENAMES } from "./load-UYXLqGV9.js";
|
|
2
|
+
import { t as exists } from "./fs-BmPtmFke.js";
|
|
3
|
+
import { i as note, o as warn, r as fail } from "./command-Dxpa00Ha.js";
|
|
4
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import pc from "picocolors";
|
|
5
7
|
import { createInterface } from "node:readline/promises";
|
|
@@ -26,10 +28,7 @@ async function detectDefaults(cwd) {
|
|
|
26
28
|
}
|
|
27
29
|
/** Whether a config is already there, so init never overwrites one silently. */
|
|
28
30
|
async function existingConfig(cwd) {
|
|
29
|
-
for (const name of CONFIG_FILENAMES)
|
|
30
|
-
await stat(path.join(cwd, name));
|
|
31
|
-
return name;
|
|
32
|
-
} catch {}
|
|
31
|
+
for (const name of CONFIG_FILENAMES) if (await exists(path.join(cwd, name))) return name;
|
|
33
32
|
}
|
|
34
33
|
/** Reads answers, showing what each will be if the reader just hits enter. */
|
|
35
34
|
function terminalPrompt() {
|
|
@@ -51,7 +50,7 @@ async function runInitCommand(options = {}) {
|
|
|
51
50
|
const target = path.resolve(cwd, options.output ?? "eaa.config.json");
|
|
52
51
|
const already = await existingConfig(cwd);
|
|
53
52
|
if (already !== void 0 && !options.force) {
|
|
54
|
-
|
|
53
|
+
warn(`${already} already exists. Pass --force to overwrite it.`);
|
|
55
54
|
return { exitCode: 1 };
|
|
56
55
|
}
|
|
57
56
|
const detected = await detectDefaults(cwd);
|
|
@@ -89,13 +88,17 @@ async function runInitCommand(options = {}) {
|
|
|
89
88
|
try {
|
|
90
89
|
await writeFile(target, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
91
90
|
} catch (cause) {
|
|
92
|
-
|
|
91
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
92
|
+
fail(`Could not write ${path.basename(target)}: ${reason}`);
|
|
93
93
|
return { exitCode: 2 };
|
|
94
94
|
}
|
|
95
95
|
process.stderr.write(`Wrote ${path.relative(cwd, target) || path.basename(target)}\n`);
|
|
96
96
|
const missing = [name === "" ? "site.name" : void 0, email === "" ? "provider.email" : void 0].filter((field) => field !== void 0);
|
|
97
|
-
if (missing.length > 0)
|
|
98
|
-
|
|
97
|
+
if (missing.length > 0) {
|
|
98
|
+
const [verb, pronoun] = missing.length === 1 ? ["is", "it"] : ["are", "them"];
|
|
99
|
+
warn(`${missing.join(" and ")} ${verb} empty and required. Fill ${pronoun} in before generating a statement.`);
|
|
100
|
+
}
|
|
101
|
+
note("Read it before publishing anything from it: status is partially-compliant,\nwhich is the honest default before an audit has run.\n\nNext: eaa-kit audit · eaa-kit statement");
|
|
99
102
|
return {
|
|
100
103
|
file: target,
|
|
101
104
|
exitCode: 0
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { i as
|
|
2
|
-
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { Script } from "node:vm";
|
|
1
|
+
import { i as pageUrl, n as failedPage, o as runOptions, s as shapeResults, t as DEFAULT_TAGS } from "./result-DLxd2Eip.js";
|
|
4
2
|
import axe from "axe-core";
|
|
3
|
+
import { Script } from "node:vm";
|
|
5
4
|
import { JSDOM, VirtualConsole } from "jsdom";
|
|
6
5
|
//#region src/audit/runners/jsdom.ts
|
|
7
6
|
/**
|
|
@@ -144,10 +143,6 @@ function blindRulesInScope(tags) {
|
|
|
144
143
|
blindScopeCache.set(key, inScope);
|
|
145
144
|
return inScope;
|
|
146
145
|
}
|
|
147
|
-
function pageUrl(page, baseUrl) {
|
|
148
|
-
if (!baseUrl) return pathToFileURL(page.absolutePath).href;
|
|
149
|
-
return new URL(page.relativePath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).href;
|
|
150
|
-
}
|
|
151
146
|
async function withTimeout(promise, ms) {
|
|
152
147
|
promise.catch(() => {});
|
|
153
148
|
let timer;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { i as isImpactLevel, r as countAtOrAbove } from "./impact-
|
|
1
|
+
import { i as impactLabel, o as isImpactLevel, r as countAtOrAbove } from "./impact-YdoOtFqm.js";
|
|
2
2
|
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
3
|
+
import { a as ruleOutcomes } from "./result-DLxd2Eip.js";
|
|
3
4
|
import axe from "axe-core";
|
|
4
5
|
/**
|
|
5
6
|
* Build the machine-readable report.
|
|
@@ -36,23 +37,14 @@ function buildJsonReport(audits, options) {
|
|
|
36
37
|
/** Every rule mentioned by any page, keyed by id and sorted for stable diffs. */
|
|
37
38
|
function buildRuleIndex(audits) {
|
|
38
39
|
const index = /* @__PURE__ */ new Map();
|
|
39
|
-
for (const audit of audits) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (const outcome of outcomes) {
|
|
48
|
-
if (index.has(outcome.ruleId)) continue;
|
|
49
|
-
index.set(outcome.ruleId, {
|
|
50
|
-
help: outcome.help,
|
|
51
|
-
helpUrl: outcome.helpUrl,
|
|
52
|
-
successCriteria: outcome.successCriteria,
|
|
53
|
-
en301549: outcome.enClauses
|
|
54
|
-
});
|
|
55
|
-
}
|
|
40
|
+
for (const audit of audits) for (const outcome of ruleOutcomes(audit)) {
|
|
41
|
+
if (index.has(outcome.ruleId)) continue;
|
|
42
|
+
index.set(outcome.ruleId, {
|
|
43
|
+
help: outcome.help,
|
|
44
|
+
helpUrl: outcome.helpUrl,
|
|
45
|
+
successCriteria: outcome.successCriteria,
|
|
46
|
+
en301549: outcome.enClauses
|
|
47
|
+
});
|
|
56
48
|
}
|
|
57
49
|
return Object.fromEntries([...index].sort(([a], [b]) => a.localeCompare(b)));
|
|
58
50
|
}
|
|
@@ -60,6 +52,7 @@ function buildRuleIndex(audits) {
|
|
|
60
52
|
function serialiseJsonReport(report) {
|
|
61
53
|
return `${JSON.stringify(report, null, 2)}\n`;
|
|
62
54
|
}
|
|
55
|
+
/** The run's tally, exported so the HTML report reads the same numbers. */
|
|
63
56
|
function buildSummary(audits, failOn) {
|
|
64
57
|
const byImpact = {
|
|
65
58
|
critical: 0,
|
|
@@ -80,8 +73,7 @@ function buildSummary(audits, failOn) {
|
|
|
80
73
|
for (const finding of audit.violations) {
|
|
81
74
|
violations += 1;
|
|
82
75
|
violatingElements += finding.nodes.length;
|
|
83
|
-
|
|
84
|
-
byImpact[impact && isImpactLevel(impact) ? impact : "unclassified"] += 1;
|
|
76
|
+
byImpact[impactLabel(finding.impact)] += 1;
|
|
85
77
|
}
|
|
86
78
|
for (const finding of audit.incomplete) if (finding.reason === "engine-limitation") notEvaluated += 1;
|
|
87
79
|
else needsReview += 1;
|
|
@@ -138,4 +130,4 @@ function byRuleId(a, b) {
|
|
|
138
130
|
return a.ruleId.localeCompare(b.ruleId);
|
|
139
131
|
}
|
|
140
132
|
//#endregion
|
|
141
|
-
export {
|
|
133
|
+
export { buildSummary as n, serialiseJsonReport as r, buildJsonReport as t };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { _ as withDefault, c as object, f as safeParse, g as url, h as union, i as isoDate, l as optional, m as transform, n as email, p as string, r as enumeration, t as array, u as pipe } from "./schema-CMZ8ItGk.js";
|
|
2
|
-
import {
|
|
2
|
+
import { r as isFile } from "./fs-BmPtmFke.js";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
5
6
|
//#region src/config/define.ts
|
|
@@ -181,12 +182,5 @@ async function importModule(file) {
|
|
|
181
182
|
if (module.default === void 0) throw new ConfigError(`${path.basename(file)} has no default export`, ["Expected: export default defineConfig({ … })"]);
|
|
182
183
|
return module.default;
|
|
183
184
|
}
|
|
184
|
-
async function isFile(candidate) {
|
|
185
|
-
try {
|
|
186
|
-
return (await stat(candidate)).isFile();
|
|
187
|
-
} catch {
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
185
|
//#endregion
|
|
192
186
|
export { COMPLIANCE_STATUSES as a, ISSUE_REASONS as c, defineConfig as d, parseConfig as f, ASSESSMENT_METHODS as i, STATEMENT_LOCALES as l, findConfigFile as n, COUNTRIES as o, loadConfig as r, ConfigError as s, CONFIG_FILENAMES as t, configSchema as u };
|