const puppeteer = require('puppeteer'); const { AxePuppeteer } = require('@axe-core/puppeteer'); class AccessibilityService { constructor() { this.axeConfig = { tags: ['wcag2a', 'wcag2aa', 'best-practice'] }; } async scanAccessibility(url, options = {}) { const { viewport = { width: 1920, height: 1080 }, includeImages = true, timeout = 30000 } = options; let browser = null; try { browser = await puppeteer.launch({ headless: 'new', args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage' ] }); const page = await browser.newPage(); await page.setViewport(viewport); // Navigate to the page await page.goto(url, { waitUntil: 'networkidle0', timeout }); // Wait a bit more for dynamic content await page.waitForTimeout(2000); // Run axe accessibility scan with better error handling let results; try { results = await new AxePuppeteer(page) .withTags(['wcag2a', 'wcag2aa', 'best-practice']) .analyze(); } catch (axeError) { console.warn('Axe-core scan failed, returning minimal results:', axeError.message); // Return minimal valid results if axe fails results = { violations: [], passes: [], incomplete: [], inapplicable: [] }; } // Process and categorize results const processedResults = this.processAxeResults(results); return { url, timestamp: new Date().toISOString(), summary: this.generateSummary(processedResults), violations: processedResults.violations, passes: processedResults.passes, incomplete: processedResults.incomplete, inapplicable: processedResults.inapplicable }; } catch (error) { console.error('Accessibility scan error:', error); throw new Error(`Failed to scan accessibility: ${error.message}`); } finally { if (browser) { await browser.close(); } } } processAxeResults(axeResults) { const processRule = (rule) => ({ id: rule.id, impact: rule.impact, tags: rule.tags, description: rule.description, help: rule.help, helpUrl: rule.helpUrl, nodes: rule.nodes.map(node => ({ html: node.html, impact: node.impact, target: node.target, failureSummary: node.failureSummary, any: node.any, all: node.all, none: node.none })) }); return { violations: axeResults.violations.map(processRule), passes: axeResults.passes.map(processRule), incomplete: axeResults.incomplete.map(processRule), inapplicable: axeResults.inapplicable.map(processRule) }; } generateSummary(results) { const violationsByImpact = { critical: 0, serious: 0, moderate: 0, minor: 0 }; results.violations.forEach(violation => { if (violation.impact && violationsByImpact.hasOwnProperty(violation.impact)) { violationsByImpact[violation.impact] += violation.nodes.length; } }); const totalViolations = Object.values(violationsByImpact).reduce((a, b) => a + b, 0); const totalPasses = results.passes.reduce((total, pass) => total + pass.nodes.length, 0); const totalIncomplete = results.incomplete.reduce((total, incomplete) => total + incomplete.nodes.length, 0); return { totalViolations, violationsByImpact, totalPasses, totalIncomplete, score: this.calculateAccessibilityScore(violationsByImpact, totalPasses), categories: this.categorizeViolations(results.violations) }; } calculateAccessibilityScore(violationsByImpact, totalPasses) { // Simple scoring algorithm - can be made more sophisticated const weights = { critical: 10, serious: 5, moderate: 2, minor: 1 }; const totalDeductions = Object.entries(violationsByImpact) .reduce((total, [impact, count]) => total + (count * weights[impact]), 0); const baseScore = 100; const passBonus = Math.min(totalPasses * 0.1, 20); // Max 20 bonus points const score = Math.max(0, baseScore - totalDeductions + passBonus); return Math.round(score); } categorizeViolations(violations) { const categories = {}; violations.forEach(violation => { violation.tags.forEach(tag => { if (!categories[tag]) { categories[tag] = []; } categories[tag].push({ id: violation.id, impact: violation.impact, nodeCount: violation.nodes.length, description: violation.description }); }); }); return categories; } async scanMultipleViewports(url, viewports = ['desktop', 'tablet', 'mobile']) { const viewportConfigs = { desktop: { width: 1920, height: 1080 }, tablet: { width: 768, height: 1024 }, mobile: { width: 375, height: 667 } }; const results = {}; const errors = {}; for (const viewportName of viewports) { try { const viewport = viewportConfigs[viewportName]; if (!viewport) { throw new Error(`Unknown viewport: ${viewportName}`); } const result = await this.scanAccessibility(url, { viewport }); results[viewportName] = result; } catch (error) { console.error(`Error scanning ${viewportName} accessibility:`, error); errors[viewportName] = error.message; } } return { results, errors: Object.keys(errors).length > 0 ? errors : null }; } generateAccessibilityReport(scanResults) { const report = { executive_summary: this.generateExecutiveSummary(scanResults), detailed_findings: this.generateDetailedFindings(scanResults), recommendations: this.generateRecommendations(scanResults), compliance_status: this.generateComplianceStatus(scanResults) }; return report; } generateExecutiveSummary(scanResults) { const summary = scanResults.summary; return { overall_score: summary.score, total_issues: summary.totalViolations, critical_issues: summary.violationsByImpact.critical, accessibility_level: this.getAccessibilityLevel(summary.score), primary_concerns: this.getPrimaryConcerns(scanResults.violations) }; } generateDetailedFindings(scanResults) { return scanResults.violations.map(violation => ({ rule: violation.id, impact: violation.impact, description: violation.description, help: violation.help, instances: violation.nodes.length, affected_elements: violation.nodes.map(node => ({ target: node.target.join(', '), html: node.html.substring(0, 200) + (node.html.length > 200 ? '...' : '') })) })); } generateRecommendations(scanResults) { const recommendations = []; // Group violations by priority const criticalViolations = scanResults.violations.filter(v => v.impact === 'critical'); const seriousViolations = scanResults.violations.filter(v => v.impact === 'serious'); if (criticalViolations.length > 0) { recommendations.push({ priority: 'High', category: 'Critical Accessibility Issues', items: criticalViolations.map(v => v.help), description: 'These issues prevent users from accessing content and must be fixed immediately.' }); } if (seriousViolations.length > 0) { recommendations.push({ priority: 'Medium', category: 'Serious Accessibility Issues', items: seriousViolations.map(v => v.help), description: 'These issues significantly impact user experience for people with disabilities.' }); } return recommendations; } generateComplianceStatus(scanResults) { const violations = scanResults.violations; const wcag2aViolations = violations.filter(v => v.tags.includes('wcag2a')); const wcag2aaViolations = violations.filter(v => v.tags.includes('wcag2aa')); return { wcag_2_1_a: wcag2aViolations.length === 0 ? 'Compliant' : 'Non-compliant', wcag_2_1_aa: wcag2aaViolations.length === 0 ? 'Compliant' : 'Non-compliant', total_violations: violations.length, compliance_score: scanResults.summary.score }; } getAccessibilityLevel(score) { if (score >= 90) return 'Excellent'; if (score >= 75) return 'Good'; if (score >= 60) return 'Fair'; if (score >= 40) return 'Poor'; return 'Critical'; } getPrimaryConcerns(violations) { return violations .filter(v => v.impact === 'critical' || v.impact === 'serious') .slice(0, 5) .map(v => v.description); } } module.exports = AccessibilityService;