forked from ChelseaKR/queer-the-stacks
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patha11y-browser-check.js
More file actions
143 lines (131 loc) · 4.37 KB
/
Copy patha11y-browser-check.js
File metadata and controls
143 lines (131 loc) · 4.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#!/usr/bin/env node
"use strict";
/*
* Browser-only accessibility contract that pa11y cannot express by itself:
*
* - force both light and dark prefers-color-scheme values;
* - run the same axe runtime bundled with the pinned global pa11y install;
* - assert that each document reflows without page-level horizontal scrolling
* at a 320 CSS-pixel viewport.
*
* Internal table scrollers remain permitted by WCAG 1.4.10's two-dimensional
* content exception; only document-level overflow fails this gate.
*/
const { execFileSync } = require("node:child_process");
const { createRequire } = require("node:module");
const path = require("node:path");
const { pathToFileURL } = require("node:url");
const inputs = process.argv.slice(2);
if (inputs.length === 0) {
console.error("usage: node scripts/a11y-browser-check.js FILE.html [...]");
process.exit(2);
}
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
const globalRoot = execFileSync(npmCommand, ["root", "-g"], {
encoding: "utf8",
}).trim();
const requireGlobal = createRequire(path.join(globalRoot, "__a11y_gate__.js"));
const puppeteer = requireGlobal("pa11y/node_modules/puppeteer");
const axePath = requireGlobal.resolve("pa11y/node_modules/axe-core/axe.min.js");
const cases = [
{ label: "desktop light", width: 1280, height: 900, mobile: false, theme: "light" },
{ label: "desktop dark", width: 1280, height: 900, mobile: false, theme: "dark" },
{ label: "320px light", width: 320, height: 800, mobile: true, theme: "light" },
{ label: "320px dark", width: 320, height: 800, mobile: true, theme: "dark" },
];
async function auditPage(browser, input, scenario) {
const page = await browser.newPage();
await page.setBypassCSP(true);
await page.setViewport({
width: scenario.width,
height: scenario.height,
deviceScaleFactor: 1,
isMobile: scenario.mobile,
});
await page.emulateMediaFeatures([
{ name: "prefers-color-scheme", value: scenario.theme },
]);
await page.goto(pathToFileURL(path.resolve(input)).href, {
waitUntil: "networkidle0",
});
const darkPreference = await page.evaluate(() =>
window.matchMedia("(prefers-color-scheme: dark)").matches
);
const expectedDark = scenario.theme === "dark";
const failures = [];
if (darkPreference !== expectedDark) {
failures.push(
`media emulation mismatch: expected dark=${expectedDark}, got ${darkPreference}`
);
}
await page.addScriptTag({ path: axePath });
const violations = await page.evaluate(async () => {
const results = await window.axe.run(document);
return results.violations.map((violation) => ({
id: violation.id,
help: violation.help,
targets: violation.nodes.map((node) => node.target.join(" ")),
}));
});
for (const violation of violations) {
failures.push(
`${violation.id}: ${violation.help} (${violation.targets.join(", ")})`
);
}
if (scenario.mobile) {
const reflow = await page.evaluate(() => {
const root = document.documentElement;
const body = document.body;
return {
clientWidth: root.clientWidth,
scrollWidth: Math.max(root.scrollWidth, body ? body.scrollWidth : 0),
};
});
if (reflow.scrollWidth > reflow.clientWidth) {
failures.push(
`WCAG 1.4.10 reflow: document is ${reflow.scrollWidth}px wide ` +
`in a ${reflow.clientWidth}px viewport`
);
}
}
await page.close();
return failures;
}
async function main() {
const browser = await puppeteer.launch({
headless: true,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
],
});
let failed = false;
try {
for (const input of inputs) {
for (const scenario of cases) {
const failures = await auditPage(browser, input, scenario);
const label = `${path.basename(input)} · ${scenario.label}`;
if (failures.length === 0) {
console.log(`browser a11y: ${label}: 0 violations`);
continue;
}
failed = true;
console.error(`browser a11y: ${label}: ${failures.length} violation(s)`);
for (const failure of failures) {
console.error(` - ${failure}`);
}
}
}
} finally {
await browser.close();
}
if (failed) {
process.exitCode = 1;
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});