resume/tests/accessibility/playwright-axe.js

200 lines
6.3 KiB
JavaScript

/**
* WCAG 2.1 AAA compliance test using Playwright and axe-core
*
* This script uses Playwright to load pages and axe-core to test them for accessibility issues.
* It's an alternative to the axe-test.js script that properly injects axe-core into the page.
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const axeCore = require('axe-core');
// Base URL to test
const BASE_URL = process.argv[2] || 'http://localhost:8080';
// Create reports directory if it doesn't exist
const reportsDir = path.join(__dirname, '../reports');
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true });
}
async function getPagesFromSitemap(sitemapUrl) {
try {
const response = await fetch(sitemapUrl);
if (!response.ok) {
throw new Error(`Failed to fetch sitemap: ${response.statusText}`);
}
const sitemapText = await response.text();
const urls = sitemapText.match(/<loc>(.*?)<\/loc>/g) || [];
return urls.map(url => {
const urlContent = url.replace(/<\/?loc>/g, '');
const urlObject = new URL(urlContent);
return urlObject.pathname;
});
} catch (error) {
console.error(`Error reading sitemap: ${error}`);
// Fallback to a default list if sitemap fails
return [
'/',
'/stories/',
'/stories/open-source-success.html',
'/stories/viperwire.html',
'/one-pager-tools/csv-tool.html'
];
}
}
async function runAxe(page, pageUrl) {
try {
// Use a file URL to load axe-core from a local file to avoid CSP issues
const axeScriptPath = path.join(__dirname, 'axe-core.js');
fs.writeFileSync(axeScriptPath, axeCore.source);
// Add the script as a file
await page.addScriptTag({ path: axeScriptPath, type: 'text/javascript' });
// Run axe with WCAG 2.1 AAA rules
const results = await page.evaluate(() => {
if (typeof axe === 'undefined') {
return { error: 'axe-core not loaded' };
}
return new Promise(resolve => {
axe.run(document, {
runOnly: {
type: 'tag',
values: ['wcag2aaa']
},
resultTypes: ['violations', 'incomplete', 'inapplicable'],
rules: {
'color-contrast': { enabled: true, options: { noScroll: true } }
}
})
.then(results => resolve(results))
.catch(err => resolve({ error: err.toString() }));
});
});
// Clean up temporary file
fs.unlinkSync(axeScriptPath);
return results;
} catch (error) {
console.error('Error running axe:', error);
return { error: error.toString() };
}
}
async function testPage(browser, pageUrl) {
const context = await browser.newContext({
bypassCSP: true
});
const page = await context.newPage();
console.log(`Testing ${pageUrl}...`);
try {
// Navigate to the page
await page.goto(pageUrl, { waitUntil: 'networkidle' });
// Run axe-core tests
const results = await runAxe(page, pageUrl);
// Save results to file
const fileName = pageUrl === BASE_URL ? 'index' : pageUrl.replace(BASE_URL, '').replace(/\//g, '-').replace(/^-/, '');
const reportPath = path.join(reportsDir, `axe-${fileName || 'index'}.json`);
fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
// Log results summary
if (results.error) {
console.error(`Error running axe-core on ${pageUrl}:`, results.error);
return { url: pageUrl, success: false, error: results.error };
}
const { violations, incomplete, passes, inapplicable } = results;
console.log(`Results for ${pageUrl}:`);
console.log(`- Violations: ${violations.length}`);
console.log(`- Incomplete: ${incomplete.length}`);
console.log(`- Passes: ${passes.length}`);
console.log(`- Inapplicable: ${inapplicable.length}`);
// Print violations
if (violations.length > 0) {
console.log('\nViolations:');
violations.forEach((violation, i) => {
console.log(`${i + 1}. ${violation.id} - ${violation.help} (Impact: ${violation.impact})`);
console.log(` Description: ${violation.description}`);
console.log(` Help: ${violation.helpUrl}`);
violation.nodes.forEach(node => {
console.log(` - Element: ${node.html}`);
console.log(` Selector: ${node.target.join(', ')}`);
});
});
}
return {
url: pageUrl,
success: violations.length === 0,
violations: violations.length,
incomplete: incomplete.length,
passes: passes.length
};
} catch (error) {
console.error(`Error testing ${pageUrl}:`, error);
return { url: pageUrl, success: false, error: error.toString() };
} finally {
await page.close();
await context.close();
}
}
async function runTests() {
const browser = await chromium.launch();
const results = [];
const sitemapUrl = `${BASE_URL}/sitemap.xml`;
console.log(`Fetching pages from sitemap: ${sitemapUrl}`);
const pagesToTest = await getPagesFromSitemap(sitemapUrl);
console.log(`Found ${pagesToTest.length} pages to test.`);
try {
// Test each page
for (const pagePath of pagesToTest) {
const pageUrl = `${BASE_URL}${pagePath.startsWith('/') ? '' : '/'}${pagePath}`;
const result = await testPage(browser, pageUrl);
results.push(result);
}
// Save overall results
const overallReport = {
timestamp: new Date().toISOString(),
baseUrl: BASE_URL,
pages: results,
summary: {
total: results.length,
passed: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length
}
};
fs.writeFileSync(path.join(reportsDir, 'axe-summary.json'), JSON.stringify(overallReport, null, 2));
// Print overall summary
console.log('\n=== Overall Summary ===');
console.log(`Total pages tested: ${overallReport.summary.total}`);
console.log(`Pages passed: ${overallReport.summary.passed}`);
console.log(`Pages failed: ${overallReport.summary.failed}`);
// Exit with appropriate code
process.exit(overallReport.summary.failed > 0 ? 1 : 0);
} catch (error) {
console.error('Error running tests:', error);
process.exit(1);
} finally {
await browser.close();
}
}
runTests();