Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Successful in 4m40s
Build & Deploy / 🏗️ Build (push) Failing after 31s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
187 lines
6.3 KiB
TypeScript
187 lines
6.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';
|
|
|
|
/**
|
|
* 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 || 'klz2026';
|
|
|
|
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: `klz_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: any = { defaults: {} };
|
|
if (fs.existsSync(baseConfigPath)) {
|
|
baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, 'utf8'));
|
|
}
|
|
|
|
// Extract domain for cookie
|
|
const urlObj = new URL(targetUrl);
|
|
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,
|
|
args: [
|
|
...(baseConfig.defaults?.chromeLaunchConfig?.args || []),
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
],
|
|
},
|
|
headers: {
|
|
Cookie: `klz_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 (err: any) {
|
|
// 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: any) => !r.type && r.message).length;
|
|
errors = results.filter((r: any) => r.type === 'error').length + actionErrors;
|
|
warnings = results.filter((r: any) => r.type === 'warning').length;
|
|
notices = results.filter((r: any) => 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: any) {
|
|
console.error(`\n❌ Error during WCAG Audit:`);
|
|
if (axios.isAxiosError(error)) {
|
|
console.error(`Status: ${error.response?.status}`);
|
|
console.error(`URL: ${error.config?.url}`);
|
|
} else {
|
|
console.error(error.message);
|
|
}
|
|
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();
|