JavaScript · 4335 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2 //
3 // axe-core runner. Drives Puppeteer through the authenticated and
4 // anonymous routes that matter for WCAG AA compliance, runs axe on
5 // each, and prints a structured report. Exits non-zero on any
6 // "critical" or "serious" violation.
7 //
8 // Usage:
9 // SHITHUB_URL=http://127.0.0.1:8080 \
10 // SHITHUB_USER=alice SHITHUB_PASS=... \
11 // node tests/a11y/axe-runner.js
12 //
13 // Dependencies (install once in CI or your dev machine):
14 // npm i puppeteer @axe-core/puppeteer
15 //
16 // The runner is intentionally narrow — pa11y-ci handles broad
17 // scanning; this script focuses on flows that need a logged-in
18 // session (settings, /admin, repo settings) plus the diff view,
19 // which has subtle semantics for screen readers (old/new sides).
20
21 const { AxePuppeteer } = require("@axe-core/puppeteer");
22 const puppeteer = require("puppeteer");
23
24 const BASE = process.env.SHITHUB_URL || "http://127.0.0.1:8080";
25 const USER = process.env.SHITHUB_USER || "";
26 const PASS = process.env.SHITHUB_PASS || "";
27
28 // Each entry: { name, path, requiresAuth, axeOpts? }.
29 const ROUTES = [
30 { name: "home (anon)", path: "/", requiresAuth: false },
31 { name: "signup", path: "/signup", requiresAuth: false },
32 { name: "login", path: "/login", requiresAuth: false },
33 { name: "explore", path: "/explore", requiresAuth: false },
34
35 // Authenticated surfaces. SHITHUB_USER + SHITHUB_PASS must be set;
36 // these are the routes most likely to have form-error association
37 // and focus-trap regressions.
38 { name: "dashboard", path: "/", requiresAuth: true },
39 { name: "settings/profile", path: "/settings/profile", requiresAuth: true },
40 { name: "settings/2fa", path: "/settings/security/2fa", requiresAuth: true },
41 { name: "new repo form", path: "/new", requiresAuth: true },
42 { name: "notifications", path: "/notifications", requiresAuth: true },
43 ];
44
45 async function login(page) {
46 if (!USER || !PASS) {
47 throw new Error("SHITHUB_USER and SHITHUB_PASS must be set for authenticated routes");
48 }
49 await page.goto(`${BASE}/login`, { waitUntil: "networkidle0" });
50 await page.type('input[name="username"]', USER);
51 await page.type('input[name="password"]', PASS);
52 await Promise.all([
53 page.waitForNavigation({ waitUntil: "networkidle0" }),
54 page.click('button[type="submit"]'),
55 ]);
56 }
57
58 function summarizeViolations(name, results) {
59 const high = results.violations.filter(v => v.impact === "critical" || v.impact === "serious");
60 const low = results.violations.filter(v => v.impact !== "critical" && v.impact !== "serious");
61 console.log(`\n=== ${name} ===`);
62 console.log(` passes: ${results.passes.length}, incomplete: ${results.incomplete.length}, violations: ${results.violations.length}`);
63 if (high.length > 0) {
64 console.log(` HIGH SEVERITY (${high.length}):`);
65 for (const v of high) {
66 console.log(` - [${v.impact}] ${v.id}: ${v.help}`);
67 console.log(` ${v.helpUrl}`);
68 console.log(` affected nodes: ${v.nodes.length}`);
69 }
70 }
71 if (low.length > 0) {
72 console.log(` Lower severity (${low.length}):`);
73 for (const v of low) {
74 console.log(` - [${v.impact}] ${v.id}: ${v.help}`);
75 }
76 }
77 return high.length;
78 }
79
80 async function main() {
81 const browser = await puppeteer.launch({
82 args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"],
83 });
84 const page = await browser.newPage();
85 await page.setViewport({ width: 1280, height: 1024 });
86
87 let totalHigh = 0;
88 let didLogin = false;
89 for (const route of ROUTES) {
90 if (route.requiresAuth && !didLogin) {
91 try {
92 await login(page);
93 didLogin = true;
94 } catch (e) {
95 console.log(`SKIP authenticated routes: ${e.message}`);
96 break;
97 }
98 }
99 await page.goto(`${BASE}${route.path}`, { waitUntil: "networkidle0" });
100 const results = await new AxePuppeteer(page)
101 .withTags(["wcag2a", "wcag2aa", "best-practice"])
102 .analyze();
103 totalHigh += summarizeViolations(route.name, results);
104 }
105
106 await browser.close();
107
108 console.log(`\nTotal high-severity violations: ${totalHigh}`);
109 process.exit(totalHigh === 0 ? 0 : 1);
110 }
111
112 main().catch(err => {
113 console.error(err);
114 process.exit(2);
115 });