Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 8s
Nightly QA / call-qa-workflow (push) Failing after 1m34s
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
242 lines
7.3 KiB
TypeScript
242 lines
7.3 KiB
TypeScript
import axios from "axios";
|
|
import * as cheerio from "cheerio";
|
|
import { execSync } from "child_process";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
import { AxiosError } from "axios";
|
|
|
|
interface Pa11yConfig {
|
|
defaults: {
|
|
threshold?: number;
|
|
runners?: string[];
|
|
ignore?: string[];
|
|
chromeLaunchConfig?: {
|
|
executablePath?: string;
|
|
args?: string[];
|
|
};
|
|
headers?: Record<string, string>;
|
|
timeout?: number;
|
|
};
|
|
urls?: string[];
|
|
}
|
|
|
|
interface Pa11yResult {
|
|
type?: string;
|
|
message?: string;
|
|
code?: string;
|
|
context?: string;
|
|
selector?: string;
|
|
}
|
|
|
|
/**
|
|
* WCAG Audit Script
|
|
*
|
|
* 1. Fetches sitemap.xml from the target URL
|
|
* 2. Extracts all URLs
|
|
* 3. Runs pa11y-ci on those URLs
|
|
*/
|
|
|
|
const targetUrl =
|
|
process.argv[2] ||
|
|
process.env.NEXT_PUBLIC_BASE_URL ||
|
|
"http://localhost:3000";
|
|
const limit = process.env.PAGESPEED_LIMIT
|
|
? parseInt(process.env.PAGESPEED_LIMIT)
|
|
: 20;
|
|
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
|
|
|
|
async function main() {
|
|
console.log(`\n🚀 Starting WCAG Audit for: ${targetUrl}`);
|
|
console.log(`📊 Limit: ${limit} pages\n`);
|
|
|
|
try {
|
|
// 1. Fetch Sitemap
|
|
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
|
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
|
|
|
const response = await axios.get(sitemapUrl, {
|
|
headers: {
|
|
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
|
},
|
|
validateStatus: (status) => status < 400,
|
|
});
|
|
|
|
const $ = cheerio.load(response.data, { xmlMode: true });
|
|
let urls = $("url loc")
|
|
.map((i, el) => $(el).text())
|
|
.get();
|
|
|
|
// Cleanup, filter and normalize domains to targetUrl
|
|
const urlPattern = /https?:\/\/[^\/]+/;
|
|
urls = [...new Set(urls)]
|
|
.filter((u) => u.startsWith("http"))
|
|
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
|
.sort();
|
|
|
|
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
|
|
|
if (urls.length === 0) {
|
|
console.error("❌ No URLs found in sitemap. Is the site up?");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (urls.length > limit) {
|
|
console.log(
|
|
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
|
);
|
|
const home = urls.filter(
|
|
(u) => u.endsWith("/de") || u.endsWith("/en") || u === targetUrl,
|
|
);
|
|
const others = urls.filter((u) => !home.includes(u));
|
|
urls = [...home, ...others.slice(0, limit - home.length)];
|
|
}
|
|
|
|
console.log(`🧪 Pages to be tested:`);
|
|
urls.forEach((u) => console.log(` - ${u}`));
|
|
|
|
// 2. Prepare pa11y-ci config
|
|
const baseConfigPath = path.join(process.cwd(), ".pa11yci.json");
|
|
let baseConfig: Pa11yConfig = { defaults: {} };
|
|
if (fs.existsSync(baseConfigPath)) {
|
|
baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, "utf8"));
|
|
}
|
|
|
|
// Extract domain for cookie (not currently used)
|
|
// const urlObj = new URL(targetUrl);
|
|
// domain is not used, so remove or keep if needed later? Linter says unused.
|
|
// const domain = urlObj.hostname;
|
|
|
|
// Update config with discovered URLs and gatekeeper cookie
|
|
const tempConfig = {
|
|
...baseConfig,
|
|
defaults: {
|
|
...baseConfig.defaults,
|
|
threshold: 0, // Force threshold to 0 so all errors are shown in JSON
|
|
runners: ["axe"],
|
|
ignore: [...(baseConfig.defaults?.ignore || []), "color-contrast"],
|
|
chromeLaunchConfig: {
|
|
...baseConfig.defaults?.chromeLaunchConfig,
|
|
...(process.env.CHROME_PATH
|
|
? { executablePath: process.env.CHROME_PATH }
|
|
: {}),
|
|
args: [
|
|
...(baseConfig.defaults?.chromeLaunchConfig?.args || []),
|
|
"--no-sandbox",
|
|
"--disable-setuid-sandbox",
|
|
],
|
|
},
|
|
headers: {
|
|
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
|
},
|
|
timeout: 60000, // Increase timeout for slower pages
|
|
},
|
|
urls: urls,
|
|
};
|
|
|
|
// Create output directory
|
|
const outputDir = path.join(process.cwd(), ".pa11yci");
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
const tempConfigPath = path.join(outputDir, "config.temp.json");
|
|
const reportPath = path.join(outputDir, "report.json");
|
|
fs.writeFileSync(tempConfigPath, JSON.stringify(tempConfig, null, 2));
|
|
|
|
// 3. Execute pa11y-ci
|
|
console.log(`\n💻 Executing pa11y-ci...`);
|
|
const pa11yCommand = `npx pa11y-ci --config .pa11yci/config.temp.json --reporter json > .pa11yci/report.json`;
|
|
|
|
try {
|
|
execSync(pa11yCommand, {
|
|
encoding: "utf8",
|
|
stdio: "inherit",
|
|
});
|
|
} catch {
|
|
// pa11y-ci exits with non-zero if issues are found, which is expected
|
|
}
|
|
|
|
// 4. Summarize Results
|
|
if (fs.existsSync(reportPath)) {
|
|
const reportData = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
console.log(`\n📊 WCAG Audit Summary:\n`);
|
|
|
|
const summaryTable = Object.keys(reportData.results).map((url) => {
|
|
const results = reportData.results[url];
|
|
// Results might have errors or just a top level message if it crashed
|
|
let errors = 0;
|
|
let warnings = 0;
|
|
let notices = 0;
|
|
|
|
if (Array.isArray(results)) {
|
|
// pa11y action execution errors come as objects with a message but no type
|
|
const actionErrors = results.filter(
|
|
(r: Pa11yResult) => !r.type && r.message,
|
|
).length;
|
|
errors =
|
|
results.filter((r: Pa11yResult) => r.type === "error").length +
|
|
actionErrors;
|
|
warnings = results.filter(
|
|
(r: Pa11yResult) => r.type === "warning",
|
|
).length;
|
|
notices = results.filter(
|
|
(r: Pa11yResult) => r.type === "notice",
|
|
).length;
|
|
}
|
|
|
|
// Clean URL for display
|
|
const displayUrl = url.replace(targetUrl, "") || "/";
|
|
|
|
return {
|
|
URL:
|
|
displayUrl.length > 50
|
|
? displayUrl.substring(0, 47) + "..."
|
|
: displayUrl,
|
|
Errors: errors,
|
|
Warnings: warnings,
|
|
Notices: notices,
|
|
Status: errors === 0 ? "✅" : "❌",
|
|
};
|
|
});
|
|
|
|
console.table(summaryTable);
|
|
|
|
const totalErrors = summaryTable.reduce(
|
|
(acc, curr) => acc + curr.Errors,
|
|
0,
|
|
);
|
|
const totalPages = summaryTable.length;
|
|
const cleanPages = summaryTable.filter((p) => p.Errors === 0).length;
|
|
|
|
console.log(
|
|
`\n📈 Result: ${cleanPages}/${totalPages} pages are error-free.`,
|
|
);
|
|
if (totalErrors > 0) {
|
|
console.log(` Total Errors discovered: ${totalErrors}`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
console.log(`\n✨ WCAG Audit completed!`);
|
|
} catch (error: unknown) {
|
|
console.error(`\n❌ Error during WCAG Audit:`);
|
|
if (error instanceof AxiosError) {
|
|
console.error(`Status: ${error.response?.status}`);
|
|
console.error(`URL: ${error.config?.url}`);
|
|
} else {
|
|
const errorBody = error instanceof Error ? error.message : String(error);
|
|
console.error(errorBody);
|
|
}
|
|
process.exit(1);
|
|
} finally {
|
|
// Clean up temp config file, keep report
|
|
const tempConfigPath = path.join(
|
|
process.cwd(),
|
|
".pa11yci/config.temp.json",
|
|
);
|
|
if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
|
|
}
|
|
}
|
|
|
|
main();
|