eaa-kit 0.3.0 → 0.4.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 +51 -5
- package/dist/astro/index.js +1 -1
- package/dist/audit/runners/worker.js +1 -1
- package/dist/{audit-DcL73mOC.js → audit-CPoZMXGM.js} +180 -36
- package/dist/audit-CpXH2Mk8.js +2 -0
- package/dist/{baseline-CuKFq4IF.js → baseline-22Y1NWxM.js} +1 -1
- package/dist/{baseline-s9F3fXTN.js → baseline-DB9CZGnV.js} +2 -2
- package/dist/cli/index.js +267 -10
- package/dist/{collect-BkAQ0viT.js → collect-CFM8gEVv.js} +21 -10
- package/dist/{command-Dxpa00Ha.js → command-D8l_oYbV.js} +2 -2
- package/dist/{component-7kEBjv_y.js → component-C3GL1Mnu.js} +41 -6
- package/dist/component-DKd3EHOg.js +2 -0
- package/dist/coverage-B4IBKMO8.js +521 -0
- package/dist/{crawl-Oxt2Gaqo.js → crawl-BiI1Lau_.js} +2 -2
- package/dist/eleventy/index.d.ts +28 -0
- package/dist/eleventy/index.js +19 -0
- package/dist/{frameworks-BYa3tULg.js → frameworks-B4ClIJgE.js} +71 -0
- package/dist/{frameworks-DaDqrJOw.js → frameworks-etFg_O8K.js} +1 -1
- package/dist/{html-DKiI_3gs.js → html-BGTO3ypW.js} +130 -5
- package/dist/{impact-YdoOtFqm.js → impact-DZt2oBCP.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/{init-DiLiYvN1.js → init-DIWDE35F.js} +2 -2
- package/dist/jsdom-4IMzv0eE.js +3 -0
- package/dist/jsdom-DCpGSLfW.js +81 -0
- package/dist/{json-Cnv9nd6U.js → json-DjEvy1nX.js} +8 -3
- package/dist/{json-B0Y7rNjt.js → json-QQuFIw1W.js} +1 -1
- package/dist/{load-UYXLqGV9.js → load-5wRGLvub.js} +1 -1
- package/dist/nuxt/index.d.ts +35 -0
- package/dist/nuxt/index.js +23 -0
- package/dist/{playwright-DYFsGUNd.js → playwright-BWniOain.js} +1 -1
- package/dist/{pool-BWkWZiJW.js → pool-BMevaLWD.js} +2 -2
- package/dist/{project-DW08TseF.js → project-CzOnkLH6.js} +8 -2
- package/dist/project-MFrXcw1M.js +2 -0
- package/dist/remediation-Dtowi2EC.js +321 -0
- package/dist/{render-DI_aCnAZ.js → render-DrvXRCEn.js} +3 -3
- package/dist/{result-DLxd2Eip.js → result-DoamKFsp.js} +78 -1
- package/dist/routes-CmdRUuOs.js +265 -0
- package/dist/{run-BW6CVuND.js → run-DB34BSOZ.js} +19 -2
- package/dist/{sarif-DB3WG7T9.js → sarif-SR3_lLYd.js} +14 -6
- package/dist/{schema-CMZ8ItGk.js → schema-is6CGX2D.js} +4 -1
- package/dist/{text-BFmNtMsV.js → text-CKKpzkYM.js} +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/webpack/index.d.ts +33 -0
- package/dist/webpack/index.js +20 -0
- package/package.json +30 -8
- package/dist/audit-CuG2hYyo.js +0 -2
- package/dist/jsdom-BjpF-2V-.js +0 -158
- package/dist/jsdom-X4KYfTp8.js +0 -3
- package/dist/manual-Vz-oX1I_.js +0 -239
- package/dist/routes-C2Cgf6Ko.js +0 -119
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
//#region src/integration/run.ts
|
|
2
2
|
/**
|
|
3
|
+
* Where an integration writes when its host offers no logger of its own.
|
|
4
|
+
*
|
|
5
|
+
* stderr rather than stdout, so a line about accessibility never lands in the
|
|
6
|
+
* middle of a report being piped to a file, and prefixed either way: a message
|
|
7
|
+
* in somebody's build log has to say what produced it.
|
|
8
|
+
*/
|
|
9
|
+
function stderrLogger() {
|
|
10
|
+
const write = (message) => {
|
|
11
|
+
process.stderr.write(`eaa-kit: ${message}\n`);
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
info: write,
|
|
15
|
+
warn: write,
|
|
16
|
+
error: write
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
3
20
|
* Thrown to fail the build. Its own class so a consumer can tell an audit
|
|
4
21
|
* failure from the build tool falling over.
|
|
5
22
|
*/
|
|
@@ -18,7 +35,7 @@ async function auditBuild(directory, options, logger) {
|
|
|
18
35
|
logger.info("skipped (enabled: false)");
|
|
19
36
|
return;
|
|
20
37
|
}
|
|
21
|
-
const { runAuditCommand } = await import("./audit-
|
|
38
|
+
const { runAuditCommand } = await import("./audit-CpXH2Mk8.js");
|
|
22
39
|
const { exitCode } = await runAuditCommand(directory, auditOptions);
|
|
23
40
|
if (exitCode === 0) {
|
|
24
41
|
logger.info("no violations at or above the threshold");
|
|
@@ -33,4 +50,4 @@ async function auditBuild(directory, options, logger) {
|
|
|
33
50
|
throw new BuildAuditError(`eaa-kit: ${message}`);
|
|
34
51
|
}
|
|
35
52
|
//#endregion
|
|
36
|
-
export { auditBuild as n, BuildAuditError as t };
|
|
53
|
+
export { auditBuild as n, stderrLogger as r, BuildAuditError as t };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { o as isImpactLevel } from "./impact-
|
|
2
|
-
import {
|
|
1
|
+
import { o as isImpactLevel } from "./impact-DZt2oBCP.js";
|
|
2
|
+
import { o as standardsReference } from "./text-CKKpzkYM.js";
|
|
3
3
|
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
4
4
|
import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
|
|
5
|
-
import { a as
|
|
5
|
+
import { a as findingElements, s as ruleOutcomes } from "./result-DoamKFsp.js";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
//#region src/audit/report/sarif.ts
|
|
8
8
|
const SARIF_VERSION = "2.1.0";
|
|
@@ -75,7 +75,7 @@ function buildSarifReport(audits, options) {
|
|
|
75
75
|
toolExecutionNotifications: notifications
|
|
76
76
|
}],
|
|
77
77
|
results,
|
|
78
|
-
properties: summaryProperties(audits)
|
|
78
|
+
properties: summaryProperties(audits, options.completeness)
|
|
79
79
|
}]
|
|
80
80
|
};
|
|
81
81
|
}
|
|
@@ -140,7 +140,7 @@ function toSarifRule(outcome) {
|
|
|
140
140
|
* Coverage that has no place in `results` but should not vanish: a SARIF log
|
|
141
141
|
* with no results must not be mistaken for "everything was checked".
|
|
142
142
|
*/
|
|
143
|
-
function summaryProperties(audits) {
|
|
143
|
+
function summaryProperties(audits, completeness) {
|
|
144
144
|
let needsReview = 0;
|
|
145
145
|
let notEvaluated = 0;
|
|
146
146
|
const notEvaluatedRules = /* @__PURE__ */ new Set();
|
|
@@ -153,7 +153,15 @@ function summaryProperties(audits) {
|
|
|
153
153
|
pages: audits.length,
|
|
154
154
|
needsReview,
|
|
155
155
|
notEvaluated,
|
|
156
|
-
notEvaluatedRules: [...notEvaluatedRules].sort()
|
|
156
|
+
notEvaluatedRules: [...notEvaluatedRules].sort(),
|
|
157
|
+
...completeness ? {
|
|
158
|
+
complete: completeness.complete,
|
|
159
|
+
discovery: completeness.discovery,
|
|
160
|
+
pagesAudited: completeness.audited,
|
|
161
|
+
pagesErrored: completeness.errored,
|
|
162
|
+
pagesUnreachable: completeness.unreachable.length,
|
|
163
|
+
truncated: completeness.truncated
|
|
164
|
+
} : {}
|
|
157
165
|
};
|
|
158
166
|
}
|
|
159
167
|
/**
|
|
@@ -64,6 +64,9 @@ function string(options = {}) {
|
|
|
64
64
|
return value;
|
|
65
65
|
} };
|
|
66
66
|
}
|
|
67
|
+
function boolean() {
|
|
68
|
+
return { read: (value, path, issues) => typeof value === "boolean" ? value : fail(issues, path, "expected true or false") };
|
|
69
|
+
}
|
|
67
70
|
function number() {
|
|
68
71
|
return { read: (value, path, issues) => typeof value === "number" && Number.isFinite(value) ? value : fail(issues, path, "expected a number") };
|
|
69
72
|
}
|
|
@@ -189,4 +192,4 @@ function email() {
|
|
|
189
192
|
} };
|
|
190
193
|
}
|
|
191
194
|
//#endregion
|
|
192
|
-
export {
|
|
195
|
+
export { url as _, isoDate as a, number as c, pipe as d, record as f, union as g, transform as h, enumeration as i, object as l, string as m, boolean as n, isoDateTime as o, safeParse as p, email as r, nullable as s, array as t, optional as u, withDefault as v };
|
|
@@ -40,4 +40,4 @@ function standardsReference(successCriteria, enClauses) {
|
|
|
40
40
|
return [...successCriteria.map((criterion) => `WCAG ${criterion}`), ...enClauses.map((clause) => `EN 301 549 ${clause}`)].join(", ");
|
|
41
41
|
}
|
|
42
42
|
//#endregion
|
|
43
|
-
export {
|
|
43
|
+
export { plural as a, escapeText as i, count as n, standardsReference as o, escapeAttribute as r, collapse as t };
|
package/dist/vite/index.js
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { n as IntegrationOptions, t as BuildAuditError } from "../run-BMASMmwO.js";
|
|
2
|
+
//#region src/webpack/index.d.ts
|
|
3
|
+
/** The part of a webpack compilation this reads. */
|
|
4
|
+
interface CompilationLike {
|
|
5
|
+
/** Non-empty when the build itself failed; there is nothing to audit then. */
|
|
6
|
+
errors: readonly unknown[];
|
|
7
|
+
}
|
|
8
|
+
/** The part of the webpack compiler this plugin touches. */
|
|
9
|
+
interface CompilerLike {
|
|
10
|
+
options: {
|
|
11
|
+
output?: {
|
|
12
|
+
path?: string;
|
|
13
|
+
};
|
|
14
|
+
mode?: string;
|
|
15
|
+
};
|
|
16
|
+
watchMode?: boolean;
|
|
17
|
+
hooks: {
|
|
18
|
+
afterEmit: {
|
|
19
|
+
tapPromise(name: string, handler: (compilation: CompilationLike) => Promise<void>): void;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
interface EaaKitWebpackOptions extends IntegrationOptions {
|
|
24
|
+
/** Directory to audit. Defaults to webpack's own `output.path`. */
|
|
25
|
+
directory?: string;
|
|
26
|
+
}
|
|
27
|
+
declare class EaaKitWebpackPlugin {
|
|
28
|
+
private readonly options;
|
|
29
|
+
constructor(options?: EaaKitWebpackOptions);
|
|
30
|
+
apply(compiler: CompilerLike): void;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { BuildAuditError, CompilationLike, CompilerLike, EaaKitWebpackOptions, EaaKitWebpackPlugin as default };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-DB34BSOZ.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/webpack/index.ts
|
|
4
|
+
var EaaKitWebpackPlugin = class {
|
|
5
|
+
options;
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
apply(compiler) {
|
|
10
|
+
compiler.hooks.afterEmit.tapPromise("eaa-kit", async (compilation) => {
|
|
11
|
+
if (compiler.watchMode === true) return;
|
|
12
|
+
if (compilation.errors.length > 0) return;
|
|
13
|
+
const output = this.options.directory ?? compiler.options.output?.path;
|
|
14
|
+
if (output === void 0) throw new BuildAuditError("eaa-kit: webpack has no output.path, so there is no directory to audit. Set one, or pass `directory` to the plugin.");
|
|
15
|
+
await auditBuild(path.resolve(output), this.options, stderrLogger());
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
export { BuildAuditError, EaaKitWebpackPlugin as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eaa-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "WCAG 2.2 AA auditor and EU accessibility statement generator (EAA / BFSG / BaFG). Audits a static build or a running site from the command line.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,15 +34,33 @@
|
|
|
34
34
|
"exports": {
|
|
35
35
|
".": {
|
|
36
36
|
"types": "./dist/index.d.ts",
|
|
37
|
-
"import": "./dist/index.js"
|
|
37
|
+
"import": "./dist/index.js",
|
|
38
|
+
"require": "./dist/index.js"
|
|
38
39
|
},
|
|
39
40
|
"./astro": {
|
|
40
41
|
"types": "./dist/astro/index.d.ts",
|
|
41
|
-
"import": "./dist/astro/index.js"
|
|
42
|
+
"import": "./dist/astro/index.js",
|
|
43
|
+
"require": "./dist/astro/index.js"
|
|
42
44
|
},
|
|
43
45
|
"./vite": {
|
|
44
46
|
"types": "./dist/vite/index.d.ts",
|
|
45
|
-
"import": "./dist/vite/index.js"
|
|
47
|
+
"import": "./dist/vite/index.js",
|
|
48
|
+
"require": "./dist/vite/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./eleventy": {
|
|
51
|
+
"types": "./dist/eleventy/index.d.ts",
|
|
52
|
+
"import": "./dist/eleventy/index.js",
|
|
53
|
+
"require": "./dist/eleventy/index.js"
|
|
54
|
+
},
|
|
55
|
+
"./webpack": {
|
|
56
|
+
"types": "./dist/webpack/index.d.ts",
|
|
57
|
+
"import": "./dist/webpack/index.js",
|
|
58
|
+
"require": "./dist/webpack/index.js"
|
|
59
|
+
},
|
|
60
|
+
"./nuxt": {
|
|
61
|
+
"types": "./dist/nuxt/index.d.ts",
|
|
62
|
+
"import": "./dist/nuxt/index.js",
|
|
63
|
+
"require": "./dist/nuxt/index.js"
|
|
46
64
|
},
|
|
47
65
|
"./package.json": "./package.json"
|
|
48
66
|
},
|
|
@@ -57,13 +75,13 @@
|
|
|
57
75
|
"typecheck": "tsc --noEmit",
|
|
58
76
|
"lint": "biome check .",
|
|
59
77
|
"format": "biome check --write .",
|
|
60
|
-
"smoke": "pnpm build && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json && node dist/cli/index.js audit tests/fixtures/site --include \"about/**\" \"blog/**\" \"drafts/**\" \"legacy.htm\" --concurrency 2 && node dist/cli/index.js audit tests/fixtures/site --include about/** --format html && node dist/cli/index.js audit tests/fixtures/site --baseline examples/baseline.json && node -e \"import('./dist/astro/index.js').then(m => { const i = m.default(); if (i.name !== 'eaa-kit' || typeof i.hooks['astro:build:done'] !== 'function') { throw new Error('astro entry point is not an integration') } console.log('astro entry ok') })\" && node dist/cli/index.js statement --config examples/eaa.config.json && node dist/cli/index.js statement --config examples/eaa.config.json --audit examples/report.json --format html",
|
|
78
|
+
"smoke": "pnpm build && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json && node dist/cli/index.js audit tests/fixtures/site --include \"about/**\" \"blog/**\" \"drafts/**\" \"legacy.htm\" --concurrency 2 && node dist/cli/index.js audit tests/fixtures/site --include about/** --format html && node dist/cli/index.js audit tests/fixtures/site --baseline examples/baseline.json && node -e \"import('./dist/astro/index.js').then(m => { const i = m.default(); if (i.name !== 'eaa-kit' || typeof i.hooks['astro:build:done'] !== 'function') { throw new Error('astro entry point is not an integration') } console.log('astro entry ok') })\" && node dist/cli/index.js statement --config examples/eaa.config.json && node dist/cli/index.js statement --config examples/eaa.config.json --audit examples/report.json --format html && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json --output .eaa-kit/smoke-base.json && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json --output .eaa-kit/smoke-head.json && node dist/cli/index.js diff .eaa-kit/smoke-base.json .eaa-kit/smoke-head.json --format json",
|
|
61
79
|
"examples": "pnpm build && node scripts/generate-examples.mjs",
|
|
62
80
|
"test:packaged": "pnpm build && node scripts/test-packaged.mjs"
|
|
63
81
|
},
|
|
64
82
|
"peerDependencies": {
|
|
65
|
-
"
|
|
66
|
-
"
|
|
83
|
+
"astro": ">=4.0.0",
|
|
84
|
+
"playwright": ">=1.40.0"
|
|
67
85
|
},
|
|
68
86
|
"peerDependenciesMeta": {
|
|
69
87
|
"playwright": {
|
|
@@ -81,16 +99,20 @@
|
|
|
81
99
|
"tinyglobby": "^0.2.17"
|
|
82
100
|
},
|
|
83
101
|
"devDependencies": {
|
|
102
|
+
"@11ty/eleventy": "^3.1.6",
|
|
84
103
|
"@biomejs/biome": "^2.5.9",
|
|
85
104
|
"@types/jsdom": "^30.0.0",
|
|
86
105
|
"@types/node": "^26.2.0",
|
|
87
106
|
"ajv": "^8.20.0",
|
|
88
107
|
"ajv-formats": "^3.0.1",
|
|
89
108
|
"astro": "^7.2.9",
|
|
109
|
+
"nuxt": "^4.5.2",
|
|
90
110
|
"playwright": "^1.62.1",
|
|
91
111
|
"tsdown": "^0.22.14",
|
|
92
112
|
"typescript": "^7.0.2",
|
|
93
|
-
"
|
|
113
|
+
"vite": "^8.2.2",
|
|
114
|
+
"vitest": "^4.1.11",
|
|
115
|
+
"webpack": "^5.110.2"
|
|
94
116
|
},
|
|
95
117
|
"publishConfig": {
|
|
96
118
|
"access": "public",
|
package/dist/audit-CuG2hYyo.js
DELETED
package/dist/jsdom-BjpF-2V-.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { i as pageUrl, n as failedPage, o as runOptions, s as shapeResults, t as DEFAULT_TAGS } from "./result-DLxd2Eip.js";
|
|
2
|
-
import axe from "axe-core";
|
|
3
|
-
import { Script } from "node:vm";
|
|
4
|
-
import { JSDOM, VirtualConsole } from "jsdom";
|
|
5
|
-
//#region src/audit/runners/jsdom.ts
|
|
6
|
-
/**
|
|
7
|
-
* Rules this engine structurally cannot decide.
|
|
8
|
-
*
|
|
9
|
-
* jsdom has no layout: every element reports a 0x0 box, computed style is
|
|
10
|
-
* limited to the inline cascade, and nothing is ever fetched. axe-core does not
|
|
11
|
-
* know that, so it happily returns `pass` for some of these — `target-size`
|
|
12
|
-
* passes on any page because a 0x0 target gets measured against nothing, and
|
|
13
|
-
* `color-contrast` passes on pages whose colours were never computed. Both were
|
|
14
|
-
* observed on real sites. Reporting either as a pass would be a false clean
|
|
15
|
-
* bill of health, so every rule listed here is force-reported as incomplete no
|
|
16
|
-
* matter which bucket axe-core put it in.
|
|
17
|
-
*
|
|
18
|
-
* The browser runner passes an empty map instead: with real layout these rules
|
|
19
|
-
* are exactly the ones it exists to answer.
|
|
20
|
-
*
|
|
21
|
-
* Rules tagged `experimental` are listed for completeness but filtered out at
|
|
22
|
-
* scope time: axe-core does not run them by default, so a browser run would not
|
|
23
|
-
* evaluate them either and telling the user to re-run with --browser would be
|
|
24
|
-
* misleading.
|
|
25
|
-
*/
|
|
26
|
-
const ENGINE_BLIND_RULES = {
|
|
27
|
-
"color-contrast": {
|
|
28
|
-
detail: "needs rendered foreground and background colours",
|
|
29
|
-
applicabilityUnreliable: false
|
|
30
|
-
},
|
|
31
|
-
"color-contrast-enhanced": {
|
|
32
|
-
detail: "needs rendered foreground and background colours",
|
|
33
|
-
applicabilityUnreliable: false
|
|
34
|
-
},
|
|
35
|
-
"target-size": {
|
|
36
|
-
detail: "needs element geometry; every box is 0x0 without layout",
|
|
37
|
-
applicabilityUnreliable: false
|
|
38
|
-
},
|
|
39
|
-
"scrollable-region-focusable": {
|
|
40
|
-
detail: "needs computed overflow",
|
|
41
|
-
applicabilityUnreliable: true
|
|
42
|
-
},
|
|
43
|
-
"link-in-text-block": {
|
|
44
|
-
detail: "needs rendered colours and text decoration",
|
|
45
|
-
applicabilityUnreliable: true
|
|
46
|
-
},
|
|
47
|
-
"no-autoplay-audio": {
|
|
48
|
-
detail: "needs media duration, and media is never loaded",
|
|
49
|
-
applicabilityUnreliable: true
|
|
50
|
-
},
|
|
51
|
-
"avoid-inline-spacing": {
|
|
52
|
-
detail: "needs computed spacing after the full cascade",
|
|
53
|
-
applicabilityUnreliable: false
|
|
54
|
-
},
|
|
55
|
-
"p-as-heading": {
|
|
56
|
-
detail: "needs computed font size and weight",
|
|
57
|
-
applicabilityUnreliable: false
|
|
58
|
-
},
|
|
59
|
-
"css-orientation-lock": {
|
|
60
|
-
detail: "needs CSS media query evaluation",
|
|
61
|
-
applicabilityUnreliable: true
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
/** Per-page ceiling; one pathological document must not stall a CI run. */
|
|
65
|
-
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
66
|
-
/**
|
|
67
|
-
* Audit collected pages with axe-core inside jsdom.
|
|
68
|
-
*
|
|
69
|
-
* Pages are processed sequentially: jsdom parsing and axe-core are both
|
|
70
|
-
* CPU-bound on the main thread, so concurrency buys nothing here. A page that
|
|
71
|
-
* throws or times out is recorded with an `error` and the run continues.
|
|
72
|
-
*/
|
|
73
|
-
async function runJsdomAudit(pages, options = {}) {
|
|
74
|
-
const audits = [];
|
|
75
|
-
for (const page of pages) audits.push(await auditPage(page, options));
|
|
76
|
-
return audits;
|
|
77
|
-
}
|
|
78
|
-
async function auditPage(page, options = {}) {
|
|
79
|
-
const tags = options.tags ?? DEFAULT_TAGS;
|
|
80
|
-
const url = pageUrl(page, options.baseUrl);
|
|
81
|
-
const startedAt = Date.now();
|
|
82
|
-
const identity = {
|
|
83
|
-
relativePath: page.relativePath,
|
|
84
|
-
absolutePath: page.absolutePath,
|
|
85
|
-
url,
|
|
86
|
-
engine: "jsdom"
|
|
87
|
-
};
|
|
88
|
-
let dom;
|
|
89
|
-
try {
|
|
90
|
-
dom = createDom(page.html, url);
|
|
91
|
-
injectAxe(dom);
|
|
92
|
-
const { axe: pageAxe } = dom.window;
|
|
93
|
-
const results = await withTimeout(pageAxe.run(dom.window.document, runOptions(tags)), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
94
|
-
return shapeResults(results, {
|
|
95
|
-
...identity,
|
|
96
|
-
durationMs: Date.now() - startedAt,
|
|
97
|
-
blind: blindRulesInScope(tags)
|
|
98
|
-
});
|
|
99
|
-
} catch (cause) {
|
|
100
|
-
return failedPage({
|
|
101
|
-
...identity,
|
|
102
|
-
durationMs: Date.now() - startedAt
|
|
103
|
-
}, cause instanceof Error ? cause.message : String(cause));
|
|
104
|
-
} finally {
|
|
105
|
-
dom?.window.close();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
function createDom(html, url) {
|
|
109
|
-
const virtualConsole = new VirtualConsole();
|
|
110
|
-
virtualConsole.on("jsdomError", () => {});
|
|
111
|
-
return new JSDOM(html, {
|
|
112
|
-
url,
|
|
113
|
-
virtualConsole,
|
|
114
|
-
runScripts: "outside-only",
|
|
115
|
-
pretendToBeVisual: true
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* axe-core is 1.3 MB of source. Compiling it once and re-running the compiled
|
|
120
|
-
* script in each window's context avoids re-parsing it for every page.
|
|
121
|
-
*/
|
|
122
|
-
let axeScript;
|
|
123
|
-
function injectAxe(dom) {
|
|
124
|
-
axeScript ??= new Script(axe.source, { filename: "axe-core.js" });
|
|
125
|
-
axeScript.runInContext(dom.getInternalVMContext());
|
|
126
|
-
}
|
|
127
|
-
const blindScopeCache = /* @__PURE__ */ new Map();
|
|
128
|
-
/**
|
|
129
|
-
* Blind rules the requested tag filter would actually have run. Experimental
|
|
130
|
-
* rules are excluded: axe-core leaves them off by default, so a browser run
|
|
131
|
-
* would not have evaluated them either.
|
|
132
|
-
*/
|
|
133
|
-
function blindRulesInScope(tags) {
|
|
134
|
-
const key = [...tags].sort().join(",");
|
|
135
|
-
const cached = blindScopeCache.get(key);
|
|
136
|
-
if (cached) return cached;
|
|
137
|
-
const inScope = /* @__PURE__ */ new Map();
|
|
138
|
-
for (const rule of axe.getRules([...tags])) {
|
|
139
|
-
if (rule.tags.includes("experimental")) continue;
|
|
140
|
-
const blind = ENGINE_BLIND_RULES[rule.ruleId];
|
|
141
|
-
if (blind) inScope.set(rule.ruleId, blind);
|
|
142
|
-
}
|
|
143
|
-
blindScopeCache.set(key, inScope);
|
|
144
|
-
return inScope;
|
|
145
|
-
}
|
|
146
|
-
async function withTimeout(promise, ms) {
|
|
147
|
-
promise.catch(() => {});
|
|
148
|
-
let timer;
|
|
149
|
-
try {
|
|
150
|
-
return await Promise.race([promise, new Promise((_resolve, reject) => {
|
|
151
|
-
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`axe-core timed out after ${ms}ms`)), ms);
|
|
152
|
-
})]);
|
|
153
|
-
} finally {
|
|
154
|
-
clearTimeout(timer);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
//#endregion
|
|
158
|
-
export { runJsdomAudit as i, auditPage as n, blindRulesInScope as r, ENGINE_BLIND_RULES as t };
|
package/dist/jsdom-X4KYfTp8.js
DELETED
package/dist/manual-Vz-oX1I_.js
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { a as impactRank } from "./impact-YdoOtFqm.js";
|
|
2
|
-
import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
|
|
3
|
-
import { r as findingElements } from "./result-DLxd2Eip.js";
|
|
4
|
-
/** Whether this element looks like one component rendered on many pages. */
|
|
5
|
-
function isShared(element) {
|
|
6
|
-
return element.pages.length >= 3;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Fold a run's violations into one entry per rule, and one per element within it.
|
|
10
|
-
*
|
|
11
|
-
* Accepted violations are not included: a baseline moves them out of what fails
|
|
12
|
-
* the build, and this is a view of what fails.
|
|
13
|
-
*/
|
|
14
|
-
function groupIssues(audits) {
|
|
15
|
-
const byRule = /* @__PURE__ */ new Map();
|
|
16
|
-
for (const audit of audits) for (const finding of audit.violations) {
|
|
17
|
-
let issue = byRule.get(finding.ruleId);
|
|
18
|
-
if (issue === void 0) {
|
|
19
|
-
issue = {
|
|
20
|
-
ruleId: finding.ruleId,
|
|
21
|
-
help: finding.help,
|
|
22
|
-
impact: finding.impact ?? null,
|
|
23
|
-
successCriteria: finding.successCriteria,
|
|
24
|
-
enClauses: finding.enClauses,
|
|
25
|
-
helpUrl: finding.helpUrl,
|
|
26
|
-
elements: [],
|
|
27
|
-
pages: [],
|
|
28
|
-
occurrences: 0
|
|
29
|
-
};
|
|
30
|
-
byRule.set(finding.ruleId, issue);
|
|
31
|
-
}
|
|
32
|
-
if (!issue.pages.includes(audit.relativePath)) issue.pages.push(audit.relativePath);
|
|
33
|
-
for (const node of findingElements(finding)) {
|
|
34
|
-
issue.occurrences += 1;
|
|
35
|
-
const fingerprint = elementFingerprint(finding.ruleId, node.selector, node.html);
|
|
36
|
-
const existing = issue.elements.find((element) => element.fingerprint === fingerprint);
|
|
37
|
-
if (existing) {
|
|
38
|
-
if (!existing.pages.includes(audit.relativePath)) existing.pages.push(audit.relativePath);
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
issue.elements.push({
|
|
42
|
-
fingerprint,
|
|
43
|
-
selector: node.selector,
|
|
44
|
-
html: node.html,
|
|
45
|
-
pages: [audit.relativePath]
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const issues = [...byRule.values()];
|
|
50
|
-
for (const issue of issues) {
|
|
51
|
-
issue.pages.sort();
|
|
52
|
-
for (const element of issue.elements) element.pages.sort();
|
|
53
|
-
issue.elements.sort(byReachThenSelector);
|
|
54
|
-
}
|
|
55
|
-
issues.sort(bySeverityThenReach);
|
|
56
|
-
return issues;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Unevaluated rules folded across the site, sorted by rule id.
|
|
60
|
-
*
|
|
61
|
-
* Both reports list these once at the end rather than under every page: on a
|
|
62
|
-
* large site the same handful recurs on each one, and a wall of "not evaluated"
|
|
63
|
-
* would bury the findings that are real.
|
|
64
|
-
*/
|
|
65
|
-
function blindRules(audits) {
|
|
66
|
-
const byRule = /* @__PURE__ */ new Map();
|
|
67
|
-
for (const audit of audits) for (const finding of audit.incomplete) {
|
|
68
|
-
if (finding.reason !== "engine-limitation") continue;
|
|
69
|
-
const entry = byRule.get(finding.ruleId);
|
|
70
|
-
if (entry) entry.pages += 1;
|
|
71
|
-
else byRule.set(finding.ruleId, {
|
|
72
|
-
ruleId: finding.ruleId,
|
|
73
|
-
pages: 1,
|
|
74
|
-
finding
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
return [...byRule.values()].sort((a, b) => a.ruleId.localeCompare(b.ruleId));
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* What one page's result actually rests on, as phrases both reports print.
|
|
81
|
-
*
|
|
82
|
-
* The counts stay separate on purpose. Only `passed` is evidence that a
|
|
83
|
-
* criterion was met here; `not applicable` means the rule found nothing to
|
|
84
|
-
* check, and adding the two together would turn an empty page into a
|
|
85
|
-
* near-perfect score.
|
|
86
|
-
*/
|
|
87
|
-
function coverageParts(audit) {
|
|
88
|
-
const blind = audit.incomplete.filter((finding) => finding.reason === "engine-limitation").length;
|
|
89
|
-
const review = audit.incomplete.length - blind;
|
|
90
|
-
const parts = [`${audit.passes.length} passed`, `${audit.inapplicable.length} not applicable`];
|
|
91
|
-
if (review > 0) parts.push(`${review} to review`);
|
|
92
|
-
if (blind > 0) parts.push(`${blind} not evaluated`);
|
|
93
|
-
return parts;
|
|
94
|
-
}
|
|
95
|
-
/** Widest reach first, then by selector so two runs agree. */
|
|
96
|
-
function byReachThenSelector(a, b) {
|
|
97
|
-
return b.pages.length - a.pages.length || a.selector.localeCompare(b.selector);
|
|
98
|
-
}
|
|
99
|
-
/** Worst first, then widest reach, then by rule id. */
|
|
100
|
-
function bySeverityThenReach(a, b) {
|
|
101
|
-
return impactRank(a.impact) - impactRank(b.impact) || b.pages.length - a.pages.length || a.ruleId.localeCompare(b.ruleId);
|
|
102
|
-
}
|
|
103
|
-
//#endregion
|
|
104
|
-
//#region src/audit/manual.ts
|
|
105
|
-
/**
|
|
106
|
-
* What a person has to check, and where to read about it.
|
|
107
|
-
*
|
|
108
|
-
* Automated testing finds a minority of accessibility barriers. This tool says
|
|
109
|
-
* so everywhere, and saying so is not much use on its own: a reader told that
|
|
110
|
-
* six rules "could not be evaluated" is left knowing there is a gap and not
|
|
111
|
-
* what to do about it.
|
|
112
|
-
*
|
|
113
|
-
* So each rule the browserless engine cannot decide carries the check somebody
|
|
114
|
-
* would do by hand, and each success criterion carries a link to what it
|
|
115
|
-
* actually requires. The point is to turn the honest disclaimer into the part
|
|
116
|
-
* of the report with the most work in it.
|
|
117
|
-
*/
|
|
118
|
-
/**
|
|
119
|
-
* WCAG 2.2 Understanding pages, by success criterion.
|
|
120
|
-
*
|
|
121
|
-
* The Understanding document rather than the criterion text: the text says what
|
|
122
|
-
* conformance is, and the Understanding page says what it is for and how to
|
|
123
|
-
* meet it, which is what somebody reading a report needs.
|
|
124
|
-
*
|
|
125
|
-
* Level A and AA only, since that is what this tool audits against.
|
|
126
|
-
*/
|
|
127
|
-
const UNDERSTANDING = {
|
|
128
|
-
"1.1.1": "non-text-content",
|
|
129
|
-
"1.2.1": "audio-only-and-video-only-prerecorded",
|
|
130
|
-
"1.2.2": "captions-prerecorded",
|
|
131
|
-
"1.2.3": "audio-description-or-media-alternative-prerecorded",
|
|
132
|
-
"1.2.4": "captions-live",
|
|
133
|
-
"1.2.5": "audio-description-prerecorded",
|
|
134
|
-
"1.3.1": "info-and-relationships",
|
|
135
|
-
"1.3.2": "meaningful-sequence",
|
|
136
|
-
"1.3.3": "sensory-characteristics",
|
|
137
|
-
"1.3.4": "orientation",
|
|
138
|
-
"1.3.5": "identify-input-purpose",
|
|
139
|
-
"1.4.1": "use-of-color",
|
|
140
|
-
"1.4.2": "audio-control",
|
|
141
|
-
"1.4.3": "contrast-minimum",
|
|
142
|
-
"1.4.4": "resize-text",
|
|
143
|
-
"1.4.5": "images-of-text",
|
|
144
|
-
"1.4.10": "reflow",
|
|
145
|
-
"1.4.11": "non-text-contrast",
|
|
146
|
-
"1.4.12": "text-spacing",
|
|
147
|
-
"1.4.13": "content-on-hover-or-focus",
|
|
148
|
-
"2.1.1": "keyboard",
|
|
149
|
-
"2.1.2": "no-keyboard-trap",
|
|
150
|
-
"2.1.4": "character-key-shortcuts",
|
|
151
|
-
"2.2.1": "timing-adjustable",
|
|
152
|
-
"2.2.2": "pause-stop-hide",
|
|
153
|
-
"2.3.1": "three-flashes-or-below-threshold",
|
|
154
|
-
"2.4.1": "bypass-blocks",
|
|
155
|
-
"2.4.2": "page-titled",
|
|
156
|
-
"2.4.3": "focus-order",
|
|
157
|
-
"2.4.4": "link-purpose-in-context",
|
|
158
|
-
"2.4.5": "multiple-ways",
|
|
159
|
-
"2.4.6": "headings-and-labels",
|
|
160
|
-
"2.4.7": "focus-visible",
|
|
161
|
-
"2.4.11": "focus-not-obscured-minimum",
|
|
162
|
-
"2.5.1": "pointer-gestures",
|
|
163
|
-
"2.5.2": "pointer-cancellation",
|
|
164
|
-
"2.5.3": "label-in-name",
|
|
165
|
-
"2.5.4": "motion-actuation",
|
|
166
|
-
"2.5.7": "dragging-movements",
|
|
167
|
-
"2.5.8": "target-size-minimum",
|
|
168
|
-
"3.1.1": "language-of-page",
|
|
169
|
-
"3.1.2": "language-of-parts",
|
|
170
|
-
"3.2.1": "on-focus",
|
|
171
|
-
"3.2.2": "on-input",
|
|
172
|
-
"3.2.3": "consistent-navigation",
|
|
173
|
-
"3.2.4": "consistent-identification",
|
|
174
|
-
"3.2.6": "consistent-help",
|
|
175
|
-
"3.3.1": "error-identification",
|
|
176
|
-
"3.3.2": "labels-or-instructions",
|
|
177
|
-
"3.3.3": "error-suggestion",
|
|
178
|
-
"3.3.4": "error-prevention-legal-financial-data",
|
|
179
|
-
"3.3.7": "redundant-entry",
|
|
180
|
-
"3.3.8": "accessible-authentication-minimum",
|
|
181
|
-
"4.1.2": "name-role-value",
|
|
182
|
-
"4.1.3": "status-messages"
|
|
183
|
-
};
|
|
184
|
-
/** Where to read what a success criterion requires, or undefined if unknown. */
|
|
185
|
-
function understandingUrl(criterion) {
|
|
186
|
-
const slug = UNDERSTANDING[criterion];
|
|
187
|
-
return slug === void 0 ? void 0 : `https://www.w3.org/WAI/WCAG22/Understanding/${slug}.html`;
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* The check a person does for a rule this engine could not decide.
|
|
191
|
-
*
|
|
192
|
-
* Written as an action rather than a restatement of the criterion. "Ensure
|
|
193
|
-
* sufficient contrast" tells somebody nothing they did not already know; the
|
|
194
|
-
* useful sentence names what to open and what to look at.
|
|
195
|
-
*/
|
|
196
|
-
const MANUAL_CHECKS = {
|
|
197
|
-
"color-contrast": {
|
|
198
|
-
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.",
|
|
199
|
-
browserAnswers: true
|
|
200
|
-
},
|
|
201
|
-
"color-contrast-enhanced": {
|
|
202
|
-
check: "Only needed if you are claiming AAA. Body text needs 7:1 and large text 4.5:1.",
|
|
203
|
-
browserAnswers: true
|
|
204
|
-
},
|
|
205
|
-
"target-size": {
|
|
206
|
-
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.",
|
|
207
|
-
browserAnswers: true
|
|
208
|
-
},
|
|
209
|
-
"scrollable-region-focusable": {
|
|
210
|
-
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.",
|
|
211
|
-
browserAnswers: true
|
|
212
|
-
},
|
|
213
|
-
"link-in-text-block": {
|
|
214
|
-
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.",
|
|
215
|
-
browserAnswers: true
|
|
216
|
-
},
|
|
217
|
-
"no-autoplay-audio": {
|
|
218
|
-
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.",
|
|
219
|
-
browserAnswers: false
|
|
220
|
-
},
|
|
221
|
-
"avoid-inline-spacing": {
|
|
222
|
-
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.",
|
|
223
|
-
browserAnswers: true
|
|
224
|
-
},
|
|
225
|
-
"p-as-heading": {
|
|
226
|
-
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.",
|
|
227
|
-
browserAnswers: false
|
|
228
|
-
},
|
|
229
|
-
"css-orientation-lock": {
|
|
230
|
-
check: "Rotate a phone or tablet. The content should work in both orientations unless one is essential, which is rare outside games and instruments.",
|
|
231
|
-
browserAnswers: false
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
/** The check for a rule, if there is one written for it. */
|
|
235
|
-
function manualCheckFor(ruleId) {
|
|
236
|
-
return MANUAL_CHECKS[ruleId];
|
|
237
|
-
}
|
|
238
|
-
//#endregion
|
|
239
|
-
export { groupIssues as a, coverageParts as i, understandingUrl as n, isShared as o, blindRules as r, manualCheckFor as t };
|