Files
Portfolio/scripts/axe-accessibility-audit.js
T
damjan_savicandClaude Opus 4.5 43484c5023 Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics
- Add .env.example for environment configuration
- Add accessibility and lighthouse audit scripts
- Remove obsolete SEO reports and temporary files
- Remove dev-dist build artifacts and backup files
- Remove unused portrait images (moved/consolidated elsewhere)
- Update contact form and component improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:42:11 +01:00

494 lines
15 KiB
JavaScript

/**
* Axe Accessibility Audit Script
*
* This script runs axe-core accessibility scans on all main pages of the portfolio site.
* It generates a comprehensive report with WCAG references.
*
* Usage:
* node scripts/axe-accessibility-audit.js
*
* Prerequisites:
* - Development server running on http://localhost:3000
* - Install deps: npm install puppeteer @axe-core/puppeteer --no-save
*/
const { AxePuppeteer } = require('@axe-core/puppeteer');
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');
// Configuration
const BASE_URL = 'http://localhost:3000';
const LOCALES = ['de', 'en', 'sr'];
const OUTPUT_DIR = path.join(__dirname, '..', '.auto-claude', 'specs', '001-suche-nach-optimierungsvorschl-gen');
// Pages to audit (main public pages)
const PAGES = [
{ path: '', name: 'Home' },
{ path: '/about', name: 'About' },
{ path: '/blog', name: 'Blog Index' },
{ path: '/contact', name: 'Contact' },
{ path: '/leistungen', name: 'Services' },
{ path: '/portfolio', name: 'Portfolio Index' },
{ path: '/imprint', name: 'Imprint' },
{ path: '/privacy', name: 'Privacy' },
{ path: '/terms', name: 'Terms' },
];
// WCAG 2.1 Impact mapping
const IMPACT_ORDER = {
'critical': 0,
'serious': 1,
'moderate': 2,
'minor': 3
};
const WCAG_TAGS = {
'wcag2a': 'WCAG 2.0 Level A',
'wcag2aa': 'WCAG 2.0 Level AA',
'wcag2aaa': 'WCAG 2.0 Level AAA',
'wcag21a': 'WCAG 2.1 Level A',
'wcag21aa': 'WCAG 2.1 Level AA',
'wcag22aa': 'WCAG 2.2 Level AA',
'best-practice': 'Best Practice'
};
/**
* Run axe audit on a single page
*/
async function auditPage(browser, url, pageName) {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
console.log(` Scanning: ${url}`);
try {
await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });
// Wait for dynamic content to load
await page.waitForTimeout(1000);
// Run axe analysis
const results = await new AxePuppeteer(page)
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice'])
.analyze();
await page.close();
return {
url,
pageName,
timestamp: new Date().toISOString(),
violations: results.violations,
passes: results.passes.length,
incomplete: results.incomplete,
inapplicable: results.inapplicable.length
};
} catch (error) {
await page.close();
return {
url,
pageName,
timestamp: new Date().toISOString(),
error: error.message,
violations: [],
passes: 0,
incomplete: [],
inapplicable: 0
};
}
}
/**
* Format violations for markdown output
*/
function formatViolation(violation) {
const wcagTags = violation.tags
.filter(tag => tag.startsWith('wcag') || tag === 'best-practice')
.map(tag => WCAG_TAGS[tag] || tag)
.join(', ');
const nodes = violation.nodes.map(node => ({
html: node.html.substring(0, 200),
target: node.target.join(', '),
failureSummary: node.failureSummary
}));
return {
id: violation.id,
impact: violation.impact,
description: violation.description,
help: violation.help,
helpUrl: violation.helpUrl,
wcagTags,
nodeCount: nodes.length,
nodes: nodes.slice(0, 5) // Limit to first 5 examples
};
}
/**
* Aggregate violations across all pages
*/
function aggregateViolations(allResults) {
const violationMap = new Map();
for (const result of allResults) {
if (result.error) continue;
for (const violation of result.violations) {
const key = violation.id;
if (!violationMap.has(key)) {
violationMap.set(key, {
...formatViolation(violation),
affectedPages: [],
totalInstances: 0
});
}
const aggregated = violationMap.get(key);
aggregated.affectedPages.push({
url: result.url,
pageName: result.pageName,
instances: violation.nodes.length
});
aggregated.totalInstances += violation.nodes.length;
}
}
// Sort by impact severity, then by total instances
return Array.from(violationMap.values()).sort((a, b) => {
const impactDiff = IMPACT_ORDER[a.impact] - IMPACT_ORDER[b.impact];
if (impactDiff !== 0) return impactDiff;
return b.totalInstances - a.totalInstances;
});
}
/**
* Generate markdown report
*/
function generateMarkdownReport(allResults, aggregatedViolations) {
const timestamp = new Date().toISOString();
// Calculate statistics
const totalPages = allResults.filter(r => !r.error).length;
const errorPages = allResults.filter(r => r.error).length;
const totalViolations = aggregatedViolations.length;
const totalInstances = aggregatedViolations.reduce((sum, v) => sum + v.totalInstances, 0);
const bySeverity = {
critical: aggregatedViolations.filter(v => v.impact === 'critical'),
serious: aggregatedViolations.filter(v => v.impact === 'serious'),
moderate: aggregatedViolations.filter(v => v.impact === 'moderate'),
minor: aggregatedViolations.filter(v => v.impact === 'minor')
};
let report = `# Axe Accessibility Audit Report
**Generated:** ${timestamp}
**Tool:** axe-core via @axe-core/puppeteer
**Standards:** WCAG 2.0 A/AA, WCAG 2.1 A/AA, Best Practices
---
## Executive Summary
| Metric | Value |
|--------|-------|
| Pages Scanned | ${totalPages} |
| Pages with Errors | ${errorPages} |
| Unique Violations | ${totalViolations} |
| Total Instances | ${totalInstances} |
| Critical Issues | ${bySeverity.critical.length} |
| Serious Issues | ${bySeverity.serious.length} |
| Moderate Issues | ${bySeverity.moderate.length} |
| Minor Issues | ${bySeverity.minor.length} |
`;
// Severity breakdown
report += `### Severity Distribution
`;
if (bySeverity.critical.length > 0) {
report += `🔴 **Critical (${bySeverity.critical.length}):** ${bySeverity.critical.map(v => v.id).join(', ')}\n\n`;
} else {
report += `✅ **Critical (0):** No critical issues found\n\n`;
}
if (bySeverity.serious.length > 0) {
report += `🟠 **Serious (${bySeverity.serious.length}):** ${bySeverity.serious.map(v => v.id).join(', ')}\n\n`;
} else {
report += `✅ **Serious (0):** No serious issues found\n\n`;
}
if (bySeverity.moderate.length > 0) {
report += `🟡 **Moderate (${bySeverity.moderate.length}):** ${bySeverity.moderate.map(v => v.id).join(', ')}\n\n`;
} else {
report += `✅ **Moderate (0):** No moderate issues found\n\n`;
}
if (bySeverity.minor.length > 0) {
report += `🟢 **Minor (${bySeverity.minor.length}):** ${bySeverity.minor.map(v => v.id).join(', ')}\n\n`;
} else {
report += `✅ **Minor (0):** No minor issues found\n\n`;
}
report += `---
## Page-by-Page Results
`;
for (const result of allResults) {
const locale = result.url.split('/')[3] || 'root';
const violationCount = result.violations?.length || 0;
const status = result.error ? '❌ Error' : (violationCount === 0 ? '✅ Pass' : `⚠️ ${violationCount} issues`);
report += `### ${result.pageName} (${locale.toUpperCase()})
**URL:** ${result.url}
**Status:** ${status}
`;
if (result.error) {
report += `**Error:** ${result.error}\n\n`;
} else {
report += `**Passes:** ${result.passes} | **Violations:** ${violationCount} | **Incomplete:** ${result.incomplete.length}\n\n`;
if (violationCount > 0) {
report += `**Issues found:**\n`;
for (const v of result.violations) {
report += `- \`${v.id}\` (${v.impact}): ${v.help} - ${v.nodes.length} instance(s)\n`;
}
report += '\n';
}
}
}
report += `---
## Detailed Violations
`;
for (const violation of aggregatedViolations) {
const impactEmoji = {
'critical': '🔴',
'serious': '🟠',
'moderate': '🟡',
'minor': '🟢'
}[violation.impact];
report += `### ${impactEmoji} ${violation.id}
**Impact:** ${violation.impact.toUpperCase()}
**WCAG:** ${violation.wcagTags || 'N/A'}
**Help:** ${violation.help}
**Total Instances:** ${violation.totalInstances}
**Documentation:** [Deque ${violation.id}](${violation.helpUrl})
**Description:** ${violation.description}
**Affected Pages:**
`;
for (const page of violation.affectedPages) {
report += `- ${page.pageName} (${page.url}): ${page.instances} instance(s)\n`;
}
report += '\n**Example Elements:**\n\n```html\n';
for (const node of violation.nodes.slice(0, 3)) {
report += `<!-- Target: ${node.target} -->\n`;
report += `${node.html}\n\n`;
}
report += '```\n\n';
if (violation.nodes[0]?.failureSummary) {
report += `**Fix:** ${violation.nodes[0].failureSummary}\n\n`;
}
report += `---\n\n`;
}
// Recommendations section
report += `## Recommendations
### Immediate Actions (Critical & Serious)
`;
for (const v of [...bySeverity.critical, ...bySeverity.serious]) {
report += `1. **Fix \`${v.id}\`**: ${v.help} - Affects ${v.affectedPages.length} page(s), ${v.totalInstances} total instance(s)\n`;
}
if (bySeverity.critical.length === 0 && bySeverity.serious.length === 0) {
report += `✅ No critical or serious issues to fix immediately.\n`;
}
report += `
### Short-term Improvements (Moderate)
`;
for (const v of bySeverity.moderate) {
report += `1. **Fix \`${v.id}\`**: ${v.help} - Affects ${v.affectedPages.length} page(s)\n`;
}
if (bySeverity.moderate.length === 0) {
report += `✅ No moderate issues to address.\n`;
}
report += `
### Nice-to-have (Minor & Best Practices)
`;
for (const v of bySeverity.minor) {
report += `1. **Consider \`${v.id}\`**: ${v.help}\n`;
}
if (bySeverity.minor.length === 0) {
report += `✅ No minor issues found.\n`;
}
report += `
---
## WCAG Compliance Summary
Based on axe automated testing, the following WCAG conformance levels have been evaluated:
| Level | Status | Notes |
|-------|--------|-------|
| WCAG 2.0 A | ${bySeverity.critical.length === 0 ? '✅ Pass' : '❌ Fail'} | ${bySeverity.critical.length > 0 ? `${bySeverity.critical.length} critical violations` : 'No critical violations'} |
| WCAG 2.0 AA | ${(bySeverity.critical.length + bySeverity.serious.length) === 0 ? '✅ Pass' : '⚠️ Issues'} | ${(bySeverity.critical.length + bySeverity.serious.length) > 0 ? `${bySeverity.critical.length + bySeverity.serious.length} issues` : 'No serious violations'} |
| WCAG 2.1 AA | ${(bySeverity.critical.length + bySeverity.serious.length) === 0 ? '✅ Pass' : '⚠️ Issues'} | Same as above |
**Note:** Automated testing can only catch ~30-40% of accessibility issues. Manual testing for keyboard navigation,
screen reader compatibility, and cognitive accessibility is also required for full WCAG compliance.
---
## Testing Methodology
- **Tool:** axe-core via @axe-core/puppeteer
- **Browser:** Chromium (Puppeteer)
- **Viewport:** 1280x800 (Desktop)
- **Standards Tested:** wcag2a, wcag2aa, wcag21a, wcag21aa, best-practice
- **Locales Tested:** German (de), English (en), Serbian (sr)
## Next Steps
1. Address all critical and serious violations first
2. Fix moderate issues for full WCAG 2.1 AA compliance
3. Perform manual keyboard navigation testing
4. Test with screen readers (NVDA, VoiceOver)
5. Review color contrast manually for custom components
6. Test with real users with disabilities if possible
`;
return report;
}
/**
* Main execution
*/
async function main() {
console.log('🔍 Axe Accessibility Audit Starting...\n');
// Check if dev server is running
try {
const http = require('http');
await new Promise((resolve, reject) => {
http.get(BASE_URL, (res) => {
if (res.statusCode === 200 || res.statusCode === 301 || res.statusCode === 302) {
resolve();
} else {
reject(new Error(`Server returned ${res.statusCode}`));
}
}).on('error', reject);
});
} catch (error) {
console.error('❌ Development server not running!');
console.error(' Please start the server with: pnpm dev');
process.exit(1);
}
console.log('✅ Development server detected\n');
// Launch browser
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const allResults = [];
try {
// Test each locale and page combination
for (const locale of LOCALES) {
console.log(`\n📍 Scanning locale: ${locale.toUpperCase()}`);
for (const page of PAGES) {
const url = `${BASE_URL}/${locale}${page.path}`;
const result = await auditPage(browser, url, page.name);
allResults.push(result);
}
}
console.log('\n\n📊 Processing results...');
// Aggregate violations
const aggregatedViolations = aggregateViolations(allResults);
// Generate report
const report = generateMarkdownReport(allResults, aggregatedViolations);
// Save report
const reportPath = path.join(OUTPUT_DIR, 'axe-accessibility-audit.md');
fs.writeFileSync(reportPath, report);
// Save JSON data for further analysis
const jsonPath = path.join(OUTPUT_DIR, 'axe-accessibility-audit.json');
fs.writeFileSync(jsonPath, JSON.stringify({
timestamp: new Date().toISOString(),
summary: {
pagesScanned: allResults.filter(r => !r.error).length,
uniqueViolations: aggregatedViolations.length,
totalInstances: aggregatedViolations.reduce((sum, v) => sum + v.totalInstances, 0),
bySeverity: {
critical: aggregatedViolations.filter(v => v.impact === 'critical').length,
serious: aggregatedViolations.filter(v => v.impact === 'serious').length,
moderate: aggregatedViolations.filter(v => v.impact === 'moderate').length,
minor: aggregatedViolations.filter(v => v.impact === 'minor').length
}
},
violations: aggregatedViolations,
pageResults: allResults
}, null, 2));
console.log(`\n✅ Audit complete!`);
console.log(` Report: ${reportPath}`);
console.log(` JSON: ${jsonPath}`);
// Print summary
console.log('\n📈 Summary:');
console.log(` Pages scanned: ${allResults.filter(r => !r.error).length}`);
console.log(` Unique violations: ${aggregatedViolations.length}`);
console.log(` Critical: ${aggregatedViolations.filter(v => v.impact === 'critical').length}`);
console.log(` Serious: ${aggregatedViolations.filter(v => v.impact === 'serious').length}`);
console.log(` Moderate: ${aggregatedViolations.filter(v => v.impact === 'moderate').length}`);
console.log(` Minor: ${aggregatedViolations.filter(v => v.impact === 'minor').length}`);
} finally {
await browser.close();
}
}
main().catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});