/** * 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>/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();