eaa-kit 0.3.0 → 0.5.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.
Files changed (66) hide show
  1. package/README.md +82 -14
  2. package/dist/astro/index.d.ts +1 -1
  3. package/dist/astro/index.js +1 -1
  4. package/dist/audit/runners/worker.js +5 -1
  5. package/dist/{audit-DcL73mOC.js → audit-B282A3EA.js} +207 -41
  6. package/dist/audit-DyHPmGeD.js +2 -0
  7. package/dist/{baseline-CuKFq4IF.js → baseline-0u4df12v.js} +1 -1
  8. package/dist/{baseline-s9F3fXTN.js → baseline-CuAS2lMk.js} +6 -5
  9. package/dist/cli/index.js +306 -18
  10. package/dist/{collect-BkAQ0viT.js → collect-Cu1q9NTB.js} +53 -11
  11. package/dist/command-C3D7JWn6.js +160 -0
  12. package/dist/{component-7kEBjv_y.js → component-C3GL1Mnu.js} +41 -6
  13. package/dist/component-DKd3EHOg.js +2 -0
  14. package/dist/coverage-B_Y6l-Ra.js +522 -0
  15. package/dist/{crawl-Oxt2Gaqo.js → crawl-BUWPgaGW.js} +83 -9
  16. package/dist/eleventy/index.d.ts +28 -0
  17. package/dist/eleventy/index.js +19 -0
  18. package/dist/fingerprint-BjYV_0F7.js +67 -0
  19. package/dist/{frameworks-BYa3tULg.js → frameworks-B4ClIJgE.js} +71 -0
  20. package/dist/{frameworks-DaDqrJOw.js → frameworks-etFg_O8K.js} +1 -1
  21. package/dist/{html-DKiI_3gs.js → html-C4vktg0A.js} +130 -5
  22. package/dist/{impact-YdoOtFqm.js → impact-DZt2oBCP.js} +1 -1
  23. package/dist/index.d.ts +119 -4
  24. package/dist/index.js +2 -2
  25. package/dist/{init-DiLiYvN1.js → init-CW7LfGT5.js} +10 -4
  26. package/dist/jsdom-22Bkt65v.js +3 -0
  27. package/dist/jsdom-B--cEH-G.js +89 -0
  28. package/dist/{json-Cnv9nd6U.js → json-DROX33kh.js} +9 -4
  29. package/dist/{json-B0Y7rNjt.js → json-D_Mnnft5.js} +1 -1
  30. package/dist/load-CFq2VQtT.js +2 -0
  31. package/dist/{load-UYXLqGV9.js → load-yAR4wzez.js} +133 -8
  32. package/dist/nuxt/index.d.ts +35 -0
  33. package/dist/nuxt/index.js +23 -0
  34. package/dist/{playwright-DYFsGUNd.js → playwright-BojtYVUa.js} +30 -6
  35. package/dist/{pool-BWkWZiJW.js → pool-BO25OIez.js} +52 -2
  36. package/dist/{project-DW08TseF.js → project-CzOnkLH6.js} +8 -2
  37. package/dist/project-MFrXcw1M.js +2 -0
  38. package/dist/remediation-CMBIrnpN.js +321 -0
  39. package/dist/{render-DI_aCnAZ.js → render-DbGOVmhx.js} +48 -10
  40. package/dist/{result-DLxd2Eip.js → result-BWcYXeRs.js} +114 -3
  41. package/dist/routes-CmdRUuOs.js +265 -0
  42. package/dist/{run-BW6CVuND.js → run-C2nKFcb-.js} +19 -2
  43. package/dist/{run-BMASMmwO.d.ts → run-CtcEUhbe.d.ts} +7 -0
  44. package/dist/{sarif-DB3WG7T9.js → sarif-B-UBcVu8.js} +16 -8
  45. package/dist/{schema-CMZ8ItGk.js → schema-DJSF4K05.js} +18 -1
  46. package/dist/statement/templates/es.en.md +125 -0
  47. package/dist/statement/templates/es.es.md +127 -0
  48. package/dist/statement/templates/fr.en.md +128 -0
  49. package/dist/statement/templates/fr.fr.md +131 -0
  50. package/dist/statement/templates/it.en.md +127 -0
  51. package/dist/statement/templates/it.it.md +130 -0
  52. package/dist/statement/templates/nl.en.md +125 -0
  53. package/dist/statement/templates/nl.nl.md +127 -0
  54. package/dist/{text-BFmNtMsV.js → text-CKKpzkYM.js} +1 -1
  55. package/dist/vite/index.d.ts +1 -1
  56. package/dist/vite/index.js +1 -1
  57. package/dist/webpack/index.d.ts +33 -0
  58. package/dist/webpack/index.js +20 -0
  59. package/package.json +35 -9
  60. package/dist/audit-CuG2hYyo.js +0 -2
  61. package/dist/command-Dxpa00Ha.js +0 -77
  62. package/dist/fingerprint-DRoneAjj.js +0 -20
  63. package/dist/jsdom-BjpF-2V-.js +0 -158
  64. package/dist/jsdom-X4KYfTp8.js +0 -3
  65. package/dist/manual-Vz-oX1I_.js +0 -239
  66. package/dist/routes-C2Cgf6Ko.js +0 -119
@@ -1,5 +1,5 @@
1
1
  import { i as toPosix } from "./fs-BmPtmFke.js";
2
- import { t as collapse } from "./text-BFmNtMsV.js";
2
+ import { t as collapse } from "./text-CKKpzkYM.js";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { glob } from "tinyglobby";
@@ -96,16 +96,51 @@ function searchTermsFor(html) {
96
96
  * Returns undefined when nothing matches and when more than one does. The
97
97
  * second case is the important one: naming a file that merely happens to
98
98
  * contain the same path sends somebody to edit the wrong component.
99
+ *
100
+ * The position comes free. The search already has to find the literal to know
101
+ * the file contains it, so recording where it found it costs one more scan of
102
+ * one string — and a report that says `Header.astro:12` opens an editor where a
103
+ * report that says `Header.astro` starts a search.
99
104
  */
100
105
  function componentFor(index, html) {
101
106
  for (const term of searchTermsFor(html)) {
102
107
  const matches = [];
103
- for (const [file, source] of index.files) if (source.includes(term)) {
104
- matches.push(file);
105
- if (matches.length > 1) break;
108
+ for (const [file, source] of index.files) {
109
+ const offset = source.indexOf(term);
110
+ if (offset !== -1) {
111
+ matches.push({
112
+ file,
113
+ offset
114
+ });
115
+ if (matches.length > 1) break;
116
+ }
117
+ }
118
+ const only = matches.length === 1 ? matches[0] : void 0;
119
+ if (only !== void 0) {
120
+ const source = index.files.get(only.file);
121
+ return {
122
+ file: only.file,
123
+ ...positionOf(source, only.offset)
124
+ };
106
125
  }
107
- if (matches.length === 1) return matches[0];
108
126
  }
109
127
  }
128
+ /** 1-based line and column of a byte offset in a source file. */
129
+ function positionOf(source, offset) {
130
+ let line = 1;
131
+ let lineStart = 0;
132
+ for (let i = 0; i < offset; i += 1) if (source[i] === "\n") {
133
+ line += 1;
134
+ lineStart = i + 1;
135
+ }
136
+ return {
137
+ line,
138
+ column: offset - lineStart + 1
139
+ };
140
+ }
141
+ /** `components/Header.astro:12`, as both reports name a component. */
142
+ function componentPath(location) {
143
+ return `${location.file}:${location.line}`;
144
+ }
110
145
  //#endregion
111
- export { buildComponentIndex, componentFor };
146
+ export { searchTermsFor as i, componentFor as n, componentPath as r, buildComponentIndex as t };
@@ -0,0 +1,2 @@
1
+ import { n as componentFor, t as buildComponentIndex } from "./component-C3GL1Mnu.js";
2
+ export { buildComponentIndex, componentFor };
@@ -0,0 +1,522 @@
1
+ import { d as successCriteria, n as DEFAULT_TAGS, r as ENGINE_BLIND_RULES } from "./result-BWcYXeRs.js";
2
+ import axe from "axe-core";
3
+ //#region src/audit/manual.ts
4
+ /**
5
+ * What a person has to check, and where to read about it.
6
+ *
7
+ * Automated testing finds a minority of accessibility barriers. This tool says
8
+ * so everywhere, and saying so is not much use on its own: a reader told that
9
+ * six rules "could not be evaluated" is left knowing there is a gap and not
10
+ * what to do about it.
11
+ *
12
+ * So each rule the browserless engine cannot decide carries the check somebody
13
+ * would do by hand, and each success criterion carries a link to what it
14
+ * actually requires. The point is to turn the honest disclaimer into the part
15
+ * of the report with the most work in it.
16
+ */
17
+ /**
18
+ * WCAG 2.2 Understanding pages, by success criterion.
19
+ *
20
+ * The Understanding document rather than the criterion text: the text says what
21
+ * conformance is, and the Understanding page says what it is for and how to
22
+ * meet it, which is what somebody reading a report needs.
23
+ *
24
+ * Level A and AA only, since that is what this tool audits against.
25
+ */
26
+ const UNDERSTANDING = {
27
+ "1.1.1": "non-text-content",
28
+ "1.2.1": "audio-only-and-video-only-prerecorded",
29
+ "1.2.2": "captions-prerecorded",
30
+ "1.2.3": "audio-description-or-media-alternative-prerecorded",
31
+ "1.2.4": "captions-live",
32
+ "1.2.5": "audio-description-prerecorded",
33
+ "1.3.1": "info-and-relationships",
34
+ "1.3.2": "meaningful-sequence",
35
+ "1.3.3": "sensory-characteristics",
36
+ "1.3.4": "orientation",
37
+ "1.3.5": "identify-input-purpose",
38
+ "1.4.1": "use-of-color",
39
+ "1.4.2": "audio-control",
40
+ "1.4.3": "contrast-minimum",
41
+ "1.4.4": "resize-text",
42
+ "1.4.5": "images-of-text",
43
+ "1.4.10": "reflow",
44
+ "1.4.11": "non-text-contrast",
45
+ "1.4.12": "text-spacing",
46
+ "1.4.13": "content-on-hover-or-focus",
47
+ "2.1.1": "keyboard",
48
+ "2.1.2": "no-keyboard-trap",
49
+ "2.1.4": "character-key-shortcuts",
50
+ "2.2.1": "timing-adjustable",
51
+ "2.2.2": "pause-stop-hide",
52
+ "2.3.1": "three-flashes-or-below-threshold",
53
+ "2.4.1": "bypass-blocks",
54
+ "2.4.2": "page-titled",
55
+ "2.4.3": "focus-order",
56
+ "2.4.4": "link-purpose-in-context",
57
+ "2.4.5": "multiple-ways",
58
+ "2.4.6": "headings-and-labels",
59
+ "2.4.7": "focus-visible",
60
+ "2.4.11": "focus-not-obscured-minimum",
61
+ "2.5.1": "pointer-gestures",
62
+ "2.5.2": "pointer-cancellation",
63
+ "2.5.3": "label-in-name",
64
+ "2.5.4": "motion-actuation",
65
+ "2.5.7": "dragging-movements",
66
+ "2.5.8": "target-size-minimum",
67
+ "3.1.1": "language-of-page",
68
+ "3.1.2": "language-of-parts",
69
+ "3.2.1": "on-focus",
70
+ "3.2.2": "on-input",
71
+ "3.2.3": "consistent-navigation",
72
+ "3.2.4": "consistent-identification",
73
+ "3.2.6": "consistent-help",
74
+ "3.3.1": "error-identification",
75
+ "3.3.2": "labels-or-instructions",
76
+ "3.3.3": "error-suggestion",
77
+ "3.3.4": "error-prevention-legal-financial-data",
78
+ "3.3.7": "redundant-entry",
79
+ "3.3.8": "accessible-authentication-minimum",
80
+ "4.1.2": "name-role-value",
81
+ "4.1.3": "status-messages"
82
+ };
83
+ /** Where to read what a success criterion requires, or undefined if unknown. */
84
+ function understandingUrl(criterion) {
85
+ const slug = UNDERSTANDING[criterion];
86
+ return slug === void 0 ? void 0 : `https://www.w3.org/WAI/WCAG22/Understanding/${slug}.html`;
87
+ }
88
+ /**
89
+ * The check a person does for a rule this engine could not decide.
90
+ *
91
+ * Written as an action rather than a restatement of the criterion. "Ensure
92
+ * sufficient contrast" tells somebody nothing they did not already know; the
93
+ * useful sentence names what to open and what to look at.
94
+ */
95
+ const MANUAL_CHECKS = {
96
+ "color-contrast": {
97
+ check: "Open the page and check text against its background with a contrast checker. Body text needs 4.5:1, and large or bold text 3:1. Check the states too — hover, focus, visited, disabled and placeholder text are the ones usually missed.",
98
+ browserAnswers: true
99
+ },
100
+ "color-contrast-enhanced": {
101
+ check: "Only needed if you are claiming AAA. Body text needs 7:1 and large text 4.5:1.",
102
+ browserAnswers: true
103
+ },
104
+ "target-size": {
105
+ check: "Measure the clickable area of buttons, icon links and form controls: 24×24 CSS pixels at minimum, unless they are inline in a sentence or have that much clear space around them. Icon-only controls in a header or a media player are where this usually fails.",
106
+ browserAnswers: true
107
+ },
108
+ "scrollable-region-focusable": {
109
+ check: "Find anything that scrolls inside the page — a code block, a wide table, a long list — and try reaching it with the Tab key alone. If it cannot take focus, a keyboard user cannot scroll it.",
110
+ browserAnswers: true
111
+ },
112
+ "link-in-text-block": {
113
+ check: "Look at links inside paragraphs. If the only thing distinguishing them from the surrounding text is colour, they need an underline or a 3:1 contrast difference against that text as well.",
114
+ browserAnswers: true
115
+ },
116
+ "no-autoplay-audio": {
117
+ check: "Load the page and listen. Anything that plays for more than three seconds on its own needs a pause or stop control, or a volume control independent of the system volume.",
118
+ browserAnswers: false
119
+ },
120
+ "avoid-inline-spacing": {
121
+ check: "Override line height to 1.5×, paragraph spacing to 2×, letter spacing to 0.12× and word spacing to 0.16× the font size, then check nothing is clipped or overlapping. Inline styles that set spacing with !important are what break this.",
122
+ browserAnswers: true
123
+ },
124
+ "p-as-heading": {
125
+ check: "Look for paragraphs styled to look like headings. A screen reader reads them as body text, so they are invisible to anyone navigating by heading.",
126
+ browserAnswers: false
127
+ },
128
+ "css-orientation-lock": {
129
+ check: "Rotate a phone or tablet. The content should work in both orientations unless one is essential, which is rare outside games and instruments.",
130
+ browserAnswers: false
131
+ }
132
+ };
133
+ /** The check for a rule, if there is one written for it. */
134
+ function manualCheckFor(ruleId) {
135
+ return MANUAL_CHECKS[ruleId];
136
+ }
137
+ //#endregion
138
+ //#region src/audit/coverage.ts
139
+ /**
140
+ * WCAG 2.2 at Levels A and AA, which is what this tool audits against.
141
+ *
142
+ * 4.1.1 Parsing is deliberately absent: it was removed in WCAG 2.2 and counting
143
+ * it would inflate the denominator with a criterion nobody has to meet.
144
+ */
145
+ const WCAG22_AA_CRITERIA = [
146
+ {
147
+ number: "1.1.1",
148
+ title: "Non-text Content",
149
+ level: "A"
150
+ },
151
+ {
152
+ number: "1.2.1",
153
+ title: "Audio-only and Video-only (Prerecorded)",
154
+ level: "A"
155
+ },
156
+ {
157
+ number: "1.2.2",
158
+ title: "Captions (Prerecorded)",
159
+ level: "A"
160
+ },
161
+ {
162
+ number: "1.2.3",
163
+ title: "Audio Description or Media Alternative (Prerecorded)",
164
+ level: "A"
165
+ },
166
+ {
167
+ number: "1.2.4",
168
+ title: "Captions (Live)",
169
+ level: "AA"
170
+ },
171
+ {
172
+ number: "1.2.5",
173
+ title: "Audio Description (Prerecorded)",
174
+ level: "AA"
175
+ },
176
+ {
177
+ number: "1.3.1",
178
+ title: "Info and Relationships",
179
+ level: "A"
180
+ },
181
+ {
182
+ number: "1.3.2",
183
+ title: "Meaningful Sequence",
184
+ level: "A"
185
+ },
186
+ {
187
+ number: "1.3.3",
188
+ title: "Sensory Characteristics",
189
+ level: "A"
190
+ },
191
+ {
192
+ number: "1.3.4",
193
+ title: "Orientation",
194
+ level: "AA"
195
+ },
196
+ {
197
+ number: "1.3.5",
198
+ title: "Identify Input Purpose",
199
+ level: "AA"
200
+ },
201
+ {
202
+ number: "1.4.1",
203
+ title: "Use of Color",
204
+ level: "A"
205
+ },
206
+ {
207
+ number: "1.4.2",
208
+ title: "Audio Control",
209
+ level: "A"
210
+ },
211
+ {
212
+ number: "1.4.3",
213
+ title: "Contrast (Minimum)",
214
+ level: "AA"
215
+ },
216
+ {
217
+ number: "1.4.4",
218
+ title: "Resize Text",
219
+ level: "AA"
220
+ },
221
+ {
222
+ number: "1.4.5",
223
+ title: "Images of Text",
224
+ level: "AA"
225
+ },
226
+ {
227
+ number: "1.4.10",
228
+ title: "Reflow",
229
+ level: "AA"
230
+ },
231
+ {
232
+ number: "1.4.11",
233
+ title: "Non-text Contrast",
234
+ level: "AA"
235
+ },
236
+ {
237
+ number: "1.4.12",
238
+ title: "Text Spacing",
239
+ level: "AA"
240
+ },
241
+ {
242
+ number: "1.4.13",
243
+ title: "Content on Hover or Focus",
244
+ level: "AA"
245
+ },
246
+ {
247
+ number: "2.1.1",
248
+ title: "Keyboard",
249
+ level: "A"
250
+ },
251
+ {
252
+ number: "2.1.2",
253
+ title: "No Keyboard Trap",
254
+ level: "A"
255
+ },
256
+ {
257
+ number: "2.1.4",
258
+ title: "Character Key Shortcuts",
259
+ level: "A"
260
+ },
261
+ {
262
+ number: "2.2.1",
263
+ title: "Timing Adjustable",
264
+ level: "A"
265
+ },
266
+ {
267
+ number: "2.2.2",
268
+ title: "Pause, Stop, Hide",
269
+ level: "A"
270
+ },
271
+ {
272
+ number: "2.3.1",
273
+ title: "Three Flashes or Below Threshold",
274
+ level: "A"
275
+ },
276
+ {
277
+ number: "2.4.1",
278
+ title: "Bypass Blocks",
279
+ level: "A"
280
+ },
281
+ {
282
+ number: "2.4.2",
283
+ title: "Page Titled",
284
+ level: "A"
285
+ },
286
+ {
287
+ number: "2.4.3",
288
+ title: "Focus Order",
289
+ level: "A"
290
+ },
291
+ {
292
+ number: "2.4.4",
293
+ title: "Link Purpose (In Context)",
294
+ level: "A"
295
+ },
296
+ {
297
+ number: "2.4.5",
298
+ title: "Multiple Ways",
299
+ level: "AA"
300
+ },
301
+ {
302
+ number: "2.4.6",
303
+ title: "Headings and Labels",
304
+ level: "AA"
305
+ },
306
+ {
307
+ number: "2.4.7",
308
+ title: "Focus Visible",
309
+ level: "AA"
310
+ },
311
+ {
312
+ number: "2.4.11",
313
+ title: "Focus Not Obscured (Minimum)",
314
+ level: "AA"
315
+ },
316
+ {
317
+ number: "2.5.1",
318
+ title: "Pointer Gestures",
319
+ level: "A"
320
+ },
321
+ {
322
+ number: "2.5.2",
323
+ title: "Pointer Cancellation",
324
+ level: "A"
325
+ },
326
+ {
327
+ number: "2.5.3",
328
+ title: "Label in Name",
329
+ level: "A"
330
+ },
331
+ {
332
+ number: "2.5.4",
333
+ title: "Motion Actuation",
334
+ level: "A"
335
+ },
336
+ {
337
+ number: "2.5.7",
338
+ title: "Dragging Movements",
339
+ level: "AA"
340
+ },
341
+ {
342
+ number: "2.5.8",
343
+ title: "Target Size (Minimum)",
344
+ level: "AA"
345
+ },
346
+ {
347
+ number: "3.1.1",
348
+ title: "Language of Page",
349
+ level: "A"
350
+ },
351
+ {
352
+ number: "3.1.2",
353
+ title: "Language of Parts",
354
+ level: "AA"
355
+ },
356
+ {
357
+ number: "3.2.1",
358
+ title: "On Focus",
359
+ level: "A"
360
+ },
361
+ {
362
+ number: "3.2.2",
363
+ title: "On Input",
364
+ level: "A"
365
+ },
366
+ {
367
+ number: "3.2.3",
368
+ title: "Consistent Navigation",
369
+ level: "AA"
370
+ },
371
+ {
372
+ number: "3.2.4",
373
+ title: "Consistent Identification",
374
+ level: "AA"
375
+ },
376
+ {
377
+ number: "3.2.6",
378
+ title: "Consistent Help",
379
+ level: "A"
380
+ },
381
+ {
382
+ number: "3.3.1",
383
+ title: "Error Identification",
384
+ level: "A"
385
+ },
386
+ {
387
+ number: "3.3.2",
388
+ title: "Labels or Instructions",
389
+ level: "A"
390
+ },
391
+ {
392
+ number: "3.3.3",
393
+ title: "Error Suggestion",
394
+ level: "AA"
395
+ },
396
+ {
397
+ number: "3.3.4",
398
+ title: "Error Prevention (Legal, Financial, Data)",
399
+ level: "AA"
400
+ },
401
+ {
402
+ number: "3.3.7",
403
+ title: "Redundant Entry",
404
+ level: "A"
405
+ },
406
+ {
407
+ number: "3.3.8",
408
+ title: "Accessible Authentication (Minimum)",
409
+ level: "AA"
410
+ },
411
+ {
412
+ number: "4.1.2",
413
+ title: "Name, Role, Value",
414
+ level: "A"
415
+ },
416
+ {
417
+ number: "4.1.3",
418
+ title: "Status Messages",
419
+ level: "AA"
420
+ }
421
+ ];
422
+ /**
423
+ * Rules axe-core would run under these tags, mapped to the criteria they cover.
424
+ *
425
+ * Experimental rules are excluded, matching `blindRulesInScope`: axe-core leaves
426
+ * them off by default, so counting them as coverage would promise checks that
427
+ * never run.
428
+ */
429
+ function rulesByCriterion(tags) {
430
+ const byCriterion = /* @__PURE__ */ new Map();
431
+ for (const rule of axe.getRules([...tags])) {
432
+ if (rule.tags.includes("experimental")) continue;
433
+ for (const criterion of successCriteria(rule.tags)) {
434
+ const rules = byCriterion.get(criterion);
435
+ if (rules) rules.push(rule.ruleId);
436
+ else byCriterion.set(criterion, [rule.ruleId]);
437
+ }
438
+ }
439
+ for (const rules of byCriterion.values()) rules.sort();
440
+ return byCriterion;
441
+ }
442
+ /** Rule ids this run reached a pass or a violation on, across every page. */
443
+ function decidedRules(audits) {
444
+ const decided = /* @__PURE__ */ new Set();
445
+ for (const audit of audits) {
446
+ for (const finding of audit.violations) decided.add(finding.ruleId);
447
+ for (const finding of audit.accepted ?? []) decided.add(finding.ruleId);
448
+ for (const outcome of audit.passes) decided.add(outcome.ruleId);
449
+ }
450
+ return decided;
451
+ }
452
+ /** Rule ids this run reached no verdict on because the engine cannot see them. */
453
+ function blindedRules(audits) {
454
+ const blind = /* @__PURE__ */ new Set();
455
+ for (const audit of audits) for (const finding of audit.incomplete) if (finding.reason === "engine-limitation") blind.add(finding.ruleId);
456
+ return blind;
457
+ }
458
+ /**
459
+ * What this run reached, criterion by criterion.
460
+ *
461
+ * `audits` decides `evaluated` rather than the engine's capabilities alone: a
462
+ * rule that could have run and never matched anything has not established that
463
+ * the criterion is met on this site, and saying otherwise would be a coverage
464
+ * claim resting on an empty page.
465
+ */
466
+ function buildCoverage(audits, tags = DEFAULT_TAGS) {
467
+ const byCriterion = rulesByCriterion(tags);
468
+ const decided = decidedRules(audits);
469
+ const blinded = blindedRules(audits);
470
+ const jsdomBlindApplies = audits.length === 0 || audits.some((audit) => audit.engine !== "browser");
471
+ const criteria = WCAG22_AA_CRITERIA.map((criterion) => {
472
+ const rules = byCriterion.get(criterion.number) ?? [];
473
+ if (rules.length === 0) return {
474
+ ...criterion,
475
+ status: "no-automated-rule",
476
+ rules,
477
+ browserWouldAnswer: false
478
+ };
479
+ if (rules.some((rule) => decided.has(rule))) return {
480
+ ...criterion,
481
+ status: "evaluated",
482
+ rules,
483
+ browserWouldAnswer: false
484
+ };
485
+ const engineBlind = rules.filter((rule) => blinded.has(rule) || jsdomBlindApplies && ENGINE_BLIND_RULES[rule] !== void 0);
486
+ if (engineBlind.length > 0) return {
487
+ ...criterion,
488
+ status: "not-evaluated",
489
+ rules,
490
+ browserWouldAnswer: engineBlind.every((rule) => manualCheckFor(rule)?.browserAnswers === true)
491
+ };
492
+ return {
493
+ ...criterion,
494
+ status: "nothing-to-check",
495
+ rules,
496
+ browserWouldAnswer: false
497
+ };
498
+ });
499
+ const count = (status) => criteria.filter((criterion) => criterion.status === status).length;
500
+ return {
501
+ criteria,
502
+ evaluated: count("evaluated"),
503
+ notEvaluated: count("not-evaluated"),
504
+ nothingToCheck: count("nothing-to-check"),
505
+ noAutomatedRule: count("no-automated-rule"),
506
+ browserWouldAnswer: criteria.filter((criterion) => criterion.browserWouldAnswer).length,
507
+ total: WCAG22_AA_CRITERIA.length
508
+ };
509
+ }
510
+ /**
511
+ * The sentence a reader needs before drawing anything from a clean report.
512
+ *
513
+ * Deliberately leads with what was not checked. A report that opened with "23
514
+ * criteria evaluated" would read as a score, which is precisely what this
515
+ * refuses to be.
516
+ */
517
+ function coverageSummary(coverage) {
518
+ const unautomatable = coverage.noAutomatedRule;
519
+ return `Of the ${coverage.total} WCAG 2.2 A and AA success criteria, ${unautomatable} cannot be checked by any automated engine and need a person. This run reached a verdict on ${coverage.evaluated}.`;
520
+ }
521
+ //#endregion
522
+ export { understandingUrl as i, coverageSummary as n, manualCheckFor as r, buildCoverage as t };