UNPKG

a11yanalyze

Version:

A command-line tool for developers and QA engineers to test web pages and websites for WCAG 2.2 AA accessibility compliance

650 lines 26.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JsonReporter = void 0; /** * JSON report generator for accessibility scan results * Converts scan results into comprehensive JSON reports with detailed analysis */ class JsonReporter { /** * Generate a single page accessibility report */ async generatePageReport(scanResult, options = {}) { const startTime = Date.now(); const reportOptions = this.mergeDefaultOptions(options); try { const metadata = this.generateMetadata('page', scanResult.url, reportOptions); const summary = this.generateExecutiveSummary([scanResult], 'page'); const pageReport = this.generatePageReportDetails(scanResult, reportOptions); const technical = this.generateTechnicalDetails(scanResult, [], reportOptions); const report = { metadata: { ...metadata, generationTime: Date.now() - startTime, }, summary, page: pageReport, technical, errors: [], }; return this.validateAndEnhanceReport(report); } catch (error) { return this.generateErrorReport(error, 'page', scanResult.url, startTime); } } /** * Generate a multi-page site accessibility report */ async generateSiteReport(crawlSession, scanResults, options = {}) { const startTime = Date.now(); const reportOptions = this.mergeDefaultOptions(options); try { const metadata = this.generateMetadata('site', crawlSession.startUrls, reportOptions); const summary = this.generateExecutiveSummary(scanResults, 'site'); const siteReport = this.generateSiteReportDetails(crawlSession, scanResults, reportOptions); const technical = this.generateTechnicalDetails(scanResults[0] || {}, scanResults, reportOptions); const report = { metadata: { ...metadata, generationTime: Date.now() - startTime, }, summary, site: siteReport, technical, errors: this.extractReportErrors(crawlSession, scanResults), }; return this.validateAndEnhanceReport(report); } catch (error) { return this.generateErrorReport(error, 'site', crawlSession.startUrls, startTime); } } /** * Generate report metadata * @private */ generateMetadata(type, target, options) { return { version: JsonReporter.REPORT_VERSION, type, generatedAt: new Date().toISOString(), generationTime: 0, // Will be updated by caller target, title: options.title || this.generateDefaultTitle(type, target), description: options.description, tool: { name: JsonReporter.TOOL_NAME, version: JsonReporter.TOOL_VERSION, userAgent: `${JsonReporter.TOOL_NAME}/${JsonReporter.TOOL_VERSION}`, }, }; } /** * Generate executive summary * @private */ generateExecutiveSummary(scanResults, type) { const totalIssues = scanResults.reduce((sum, result) => sum + result.issues.length, 0); const issuesBySeverity = this.calculateSeverityBreakdown(scanResults); const wcagLevelBreakdown = this.calculateWCAGLevelBreakdown(scanResults); const averageScore = this.calculateAverageScore(scanResults); const overallCompliant = this.determineOverallCompliance(scanResults); const achievedLevel = this.determineAchievedWCAGLevel(scanResults); return { overallScore: averageScore, compliant: overallCompliant, achievedLevel, pagesScanned: scanResults.length, totalIssues, issuesBySeverity, wcagLevelBreakdown, keyRecommendations: this.generateKeyRecommendations(scanResults, issuesBySeverity), status: scanResults.length > 0 ? 'completed' : 'failed', }; } /** * Generate page report details * @private */ generatePageReportDetails(scanResult, options) { const filteredIssues = this.filterIssuesBySeverity(scanResult.issues, options.minSeverity); const reportIssues = this.convertToReportIssues(filteredIssues, options); return { url: scanResult.url, title: 'Unknown Title', // Title would come from actual page scan score: scanResult.score, compliant: scanResult.compliance?.compliant || false, issues: reportIssues, metadata: this.convertPageMetadata(scanResult), performance: this.convertPerformanceMetrics(scanResult), screenshots: options.includeScreenshots ? this.extractScreenshots(scanResult) : undefined, }; } /** * Generate site report details * @private */ generateSiteReportDetails(crawlSession, scanResults, options) { const crawlInfo = this.convertCrawlInfo(crawlSession); const statistics = this.calculateSiteStatistics(crawlSession, scanResults); const pages = this.generatePageSummaries(scanResults, crawlSession); const issuesAnalysis = this.analyzeIssues(scanResults, options); const complianceAnalysis = this.analyzeCompliance(scanResults); const domainBreakdown = this.calculateDomainBreakdown(scanResults); return { baseUrl: crawlSession.startUrls[0] || '', crawlInfo, statistics, pages, issuesAnalysis, complianceAnalysis, domainBreakdown: domainBreakdown.length > 1 ? domainBreakdown : undefined, }; } /** * Convert crawl session to crawl info * @private */ convertCrawlInfo(crawlSession) { return { sessionId: crawlSession.id, startUrls: crawlSession.startUrls, startTime: crawlSession.startTime.toISOString(), endTime: crawlSession.endTime?.toISOString() || new Date().toISOString(), duration: crawlSession.endTime ? crawlSession.endTime.getTime() - crawlSession.startTime.getTime() : Date.now() - crawlSession.startTime.getTime(), config: { maxDepth: crawlSession.config.maxDepth, maxPages: crawlSession.config.maxPages, allowedDomains: crawlSession.config.allowedDomains, excludedPaths: crawlSession.config.excludedPaths, }, status: this.mapCrawlStatus(crawlSession.status), }; } /** * Calculate site statistics * @private */ calculateSiteStatistics(crawlSession, scanResults) { const urls = Array.from(crawlSession.urls.values()); const scoreDistribution = this.calculateScoreDistribution(scanResults); const responseTimeStats = this.calculateResponseTimeStats(scanResults); return { pagesDiscovered: urls.length, pagesScanned: scanResults.length, pagesFailed: urls.filter(u => u.status === 'failed').length, pagesSkipped: urls.filter(u => u.status === 'skipped').length, averageScore: this.calculateAverageScore(scanResults), scoreDistribution, responseTimeStats, }; } /** * Generate page summaries * @private */ generatePageSummaries(scanResults, crawlSession) { return scanResults.map(result => { const urlEntry = crawlSession.urls.get(result.url); const issuesBySeverity = this.calculateSeverityBreakdown([result]); return { url: result.url, title: 'Unknown Title', // Would be extracted from page content during scanning score: result.score, compliant: result.compliance?.compliant || false, issuesBySeverity, loadTime: result.metadata?.pageLoadTime || 0, scanTime: result.metadata?.scanDuration || 0, depth: urlEntry?.depth || 0, errors: result.errors?.map(e => e.message) || [], }; }); } /** * Analyze issues across the site * @private */ analyzeIssues(scanResults, options) { const allIssues = scanResults.flatMap(result => result.issues); const filteredIssues = this.filterIssuesBySeverity(allIssues, options.minSeverity); const mostCommonIssues = this.findMostCommonIssues(filteredIssues, scanResults.length); const issuesByCategory = this.categorizeIssues(filteredIssues); const criticalIssues = this.convertToReportIssues(filteredIssues.filter(issue => issue.severity === 'critical'), options); const patterns = this.identifyIssuePatterns(scanResults); return { mostCommonIssues, issuesByCategory, criticalIssues, patterns, }; } /** * Analyze WCAG compliance * @private */ analyzeCompliance(scanResults) { const levelCompliance = { A: this.calculateComplianceLevel(scanResults, 'A'), AA: this.calculateComplianceLevel(scanResults, 'AA'), AAA: this.calculateComplianceLevel(scanResults, 'AAA'), }; const successCriteria = this.analyzeSuccessCriteria(scanResults); return { levelCompliance, successCriteria, }; } /** * Calculate compliance level statistics * @private */ calculateComplianceLevel(scanResults, level) { const relevantResults = scanResults.filter(result => result.compliance?.levelBreakdown[level] !== undefined); const passingPages = relevantResults.filter(result => { const levelIssues = result.issues.filter(issue => issue.level === level && issue.severity !== 'warning'); return levelIssues.length === 0; }).length; const failingPages = relevantResults.length - passingPages; const compliant = failingPages === 0; const percentage = relevantResults.length > 0 ? (passingPages / relevantResults.length) * 100 : 0; const blockingIssues = this.findBlockingIssues(scanResults, level); return { compliant, percentage: Math.round(percentage), passingPages, failingPages, blockingIssues, }; } /** * Convert scan result issues to report issues * @private */ convertToReportIssues(issues, options) { const issueGroups = options.groupIssues ? this.groupIssuesByType(issues) : issues.map(issue => [issue]); return issueGroups.map(group => { const primaryIssue = group[0]; if (!primaryIssue) { throw new Error('Issue group is empty'); } const elements = group.map(issue => this.convertToAffectedElement(issue)); return { id: primaryIssue.id, wcagReference: primaryIssue.wcagReference, level: primaryIssue.level, severity: primaryIssue.severity, category: this.categorizeIssue(primaryIssue), title: this.generateIssueTitle(primaryIssue), description: primaryIssue.message, impact: primaryIssue.impact, remediation: options.includeRemediation ? primaryIssue.remediation : '', helpUrl: primaryIssue.helpUrl, occurrences: group.length, elements, }; }); } /** * Convert accessibility issue to affected element * @private */ convertToAffectedElement(issue) { return { selector: issue.selector, html: this.truncateHtml(issue.element), text: this.extractElementText(issue.element), context: `Issue: ${issue.message}`, }; } /** * Calculate severity breakdown * @private */ calculateSeverityBreakdown(scanResults) { const allIssues = scanResults.flatMap(result => result.issues); return { critical: allIssues.filter(issue => issue.severity === 'critical').length, serious: allIssues.filter(issue => issue.severity === 'serious').length, moderate: allIssues.filter(issue => issue.severity === 'moderate').length, minor: allIssues.filter(issue => issue.severity === 'minor').length, warning: allIssues.filter(issue => issue.severity === 'warning').length, }; } /** * Calculate WCAG level breakdown * @private */ calculateWCAGLevelBreakdown(scanResults) { const allIssues = scanResults.flatMap(result => result.issues); return { A: allIssues.filter(issue => issue.level === 'A').length, AA: allIssues.filter(issue => issue.level === 'AA').length, AAA: allIssues.filter(issue => issue.level === 'AAA').length, ARIA: allIssues.filter(issue => issue.level === 'ARIA').length, }; } /** * Calculate average accessibility score * @private */ calculateAverageScore(scanResults) { if (scanResults.length === 0) return 0; const totalScore = scanResults.reduce((sum, result) => sum + result.score, 0); return Math.round(totalScore / scanResults.length); } /** * Determine overall compliance status * @private */ determineOverallCompliance(scanResults) { return scanResults.every(result => result.compliance?.compliant || false); } /** * Determine achieved WCAG level * @private */ determineAchievedWCAGLevel(scanResults) { const hasA = scanResults.every(result => result.issues.filter(issue => issue.level === 'A' && issue.severity !== 'warning').length === 0); const hasAA = hasA && scanResults.every(result => result.issues.filter(issue => issue.level === 'AA' && issue.severity !== 'warning').length === 0); const hasAAA = hasAA && scanResults.every(result => result.issues.filter(issue => issue.level === 'AAA' && issue.severity !== 'warning').length === 0); if (hasAAA) return 'AAA'; if (hasAA) return 'AA'; if (hasA) return 'A'; return 'None'; } /** * Generate key recommendations * @private */ generateKeyRecommendations(scanResults, issuesBySeverity) { const recommendations = []; if (issuesBySeverity.critical > 0) { recommendations.push(`Address ${issuesBySeverity.critical} critical accessibility issues immediately`); } if (issuesBySeverity.serious > 0) { recommendations.push(`Fix ${issuesBySeverity.serious} serious issues to improve compliance`); } const averageScore = this.calculateAverageScore(scanResults); if (averageScore < 80) { recommendations.push('Implement systematic accessibility testing in development workflow'); } if (recommendations.length === 0) { recommendations.push('Continue monitoring and maintaining current accessibility standards'); } return recommendations.slice(0, 5); // Limit to top 5 recommendations } /** * Helper methods for data processing * @private */ mergeDefaultOptions(options) { return { includeScreenshots: false, includeRawData: false, includeTrends: false, minSeverity: 'warning', groupIssues: true, includeRemediation: true, version: JsonReporter.REPORT_VERSION, ...options, }; } generateDefaultTitle(type, target) { const targetStr = Array.isArray(target) ? target[0] : target; return type === 'page' ? `Accessibility Report - ${targetStr}` : `Site Accessibility Report - ${targetStr}`; } filterIssuesBySeverity(issues, minSeverity) { const severityOrder = ['warning', 'minor', 'moderate', 'serious', 'critical']; const minIndex = severityOrder.indexOf(minSeverity); return issues.filter(issue => severityOrder.indexOf(issue.severity) >= minIndex); } categorizeIssue(issue) { // Simple categorization based on WCAG reference or tags if (issue.wcagReference.startsWith('1.1')) return 'images'; if (issue.wcagReference.startsWith('1.3')) return 'structure'; if (issue.wcagReference.startsWith('1.4')) return 'color'; if (issue.wcagReference.startsWith('2.1')) return 'keyboard'; if (issue.wcagReference.startsWith('2.4')) return 'navigation'; if (issue.wcagReference.startsWith('3.1')) return 'content'; if (issue.wcagReference.startsWith('3.2')) return 'navigation'; if (issue.wcagReference.startsWith('3.3')) return 'forms'; if (issue.wcagReference.startsWith('4.1')) return 'aria'; return 'other'; } generateIssueTitle(issue) { // Generate a concise title from the issue message const messagePart = issue.message?.split('.')[0]; return messagePart?.trim() || 'Accessibility Issue'; } truncateHtml(html, maxLength = 200) { return html.length > maxLength ? html.substring(0, maxLength) + '...' : html; } extractElementText(html) { // Simple text extraction from HTML const textMatch = html.match(/>([^<]+)</); return textMatch && textMatch[1] ? textMatch[1].trim() : undefined; } // Additional helper methods would be implemented here... convertPageMetadata(scanResult) { return { title: 'Unknown', // Would be extracted from page content during scanning language: 'en', // Default, should be extracted from scan viewport: '1280x720', // Default, should come from scan metadata charset: 'UTF-8', // Default, should be extracted size: { totalElements: scanResult.metadata?.totalElements || 0, interactiveElements: scanResult.metadata?.testedElements || 0, images: 0, // Should be extracted from scan links: 0, // Should be extracted from scan formControls: 0, // Should be extracted from scan headings: 0, // Should be extracted from scan }, accessibilityFeatures: [], // Should be detected during scan }; } convertPerformanceMetrics(scanResult) { return { loadTime: scanResult.metadata?.pageLoadTime || 0, scanTime: scanResult.metadata?.scanDuration || 0, totalTime: (scanResult.metadata?.pageLoadTime || 0) + (scanResult.metadata?.scanDuration || 0), resources: { total: 0, // Should come from browser manager failed: 0, totalSize: 0, }, }; } extractScreenshots(scanResult) { // Screenshots would be extracted from scan result if available return []; } mapCrawlStatus(status) { // Map internal crawl status to report status return status === 'completed' ? 'completed' : 'partial'; } // Placeholder implementations for complex analysis methods calculateScoreDistribution(scanResults) { const scores = scanResults.map(r => r.score); return { excellent: scores.filter(s => s >= 90).length, good: scores.filter(s => s >= 80 && s < 90).length, fair: scores.filter(s => s >= 70 && s < 80).length, poor: scores.filter(s => s >= 60 && s < 70).length, critical: scores.filter(s => s < 60).length, }; } calculateResponseTimeStats(scanResults) { const loadTimes = scanResults.map(r => r.metadata?.pageLoadTime || 0); const scanTimes = scanResults.map(r => r.metadata?.scanDuration || 0); return { averageLoadTime: loadTimes.reduce((a, b) => a + b, 0) / loadTimes.length, fastestLoadTime: Math.min(...loadTimes), slowestLoadTime: Math.max(...loadTimes), averageScanTime: scanTimes.reduce((a, b) => a + b, 0) / scanTimes.length, }; } findMostCommonIssues(issues, totalPages) { // Group issues by WCAG reference and count occurrences const issueGroups = new Map(); issues.forEach(issue => { const key = issue.wcagReference; if (!issueGroups.has(key)) { issueGroups.set(key, []); } issueGroups.get(key).push(issue); }); return Array.from(issueGroups.entries()) .filter(([, groupIssues]) => groupIssues.length > 0) .map(([wcagRef, groupIssues]) => ({ id: groupIssues[0].id, title: this.generateIssueTitle(groupIssues[0]), wcagReference: wcagRef, occurrences: groupIssues.length, affectedPages: 1, // Simplified - would need page tracking affectedPercentage: Math.round((1 / totalPages) * 100), })) .sort((a, b) => b.occurrences - a.occurrences) .slice(0, 10); } categorizeIssues(issues) { const categories = { images: 0, forms: 0, navigation: 0, content: 0, color: 0, keyboard: 0, focus: 0, aria: 0, structure: 0, multimedia: 0, timing: 0, input: 0, error: 0, other: 0 }; issues.forEach(issue => { const category = this.categorizeIssue(issue); categories[category]++; }); return categories; } identifyIssuePatterns(scanResults) { // Simplified pattern detection return []; } analyzeSuccessCriteria(scanResults) { // Simplified success criteria analysis return []; } calculateDomainBreakdown(scanResults) { // Group by domain and calculate statistics return []; } findBlockingIssues(scanResults, level) { // Find issues that block compliance at this level return []; } groupIssuesByType(issues) { // Group similar issues together const groups = new Map(); issues.forEach(issue => { const key = `${issue.wcagReference}-${issue.severity}`; if (!groups.has(key)) { groups.set(key, []); } groups.get(key).push(issue); }); return Array.from(groups.values()); } generateTechnicalDetails(primaryResult, allResults, options) { return { scanConfig: { wcagLevel: 'AA', // Should come from actual config includeAAA: true, includeARIA: true, browser: { name: 'chromium', version: '1.0.0', headless: true, viewport: { width: 1280, height: 720 }, }, }, rulesExecuted: [], // Should be populated from actual rule execution environment: { toolVersion: JsonReporter.TOOL_VERSION, nodeVersion: process.version, platform: process.platform, arch: process.arch, }, processing: { totalRules: 0, rulesPassed: 0, rulesFailed: 0, customRules: 0, }, }; } extractReportErrors(crawlSession, scanResults) { const errors = []; // Extract errors from scan results scanResults.forEach(result => { if (result.errors) { result.errors.forEach(error => { errors.push({ type: 'scan', severity: error.type === 'timeout' ? 'error' : 'warning', message: error.message, details: error.details, url: result.url, timestamp: new Date().toISOString(), }); }); } }); return errors; } validateAndEnhanceReport(report) { // Validate report structure and enhance with additional data return report; } generateErrorReport(error, type, target, startTime) { const errorMessage = error instanceof Error ? error.message : String(error); return { metadata: this.generateMetadata(type, target, this.mergeDefaultOptions({})), summary: { overallScore: 0, compliant: false, achievedLevel: 'None', pagesScanned: 0, totalIssues: 0, issuesBySeverity: { critical: 0, serious: 0, moderate: 0, minor: 0, warning: 0 }, wcagLevelBreakdown: { A: 0, AA: 0, AAA: 0, ARIA: 0 }, keyRecommendations: ['Report generation failed - please try again'], status: 'failed', }, technical: this.generateTechnicalDetails({}, [], this.mergeDefaultOptions({})), errors: [{ type: 'generation', severity: 'critical', message: `Report generation failed: ${errorMessage}`, timestamp: new Date().toISOString(), stack: error instanceof Error ? error.stack : undefined, }], }; } } exports.JsonReporter = JsonReporter; JsonReporter.REPORT_VERSION = '1.0.0'; JsonReporter.TOOL_NAME = 'a11yanalyze'; JsonReporter.TOOL_VERSION = '1.0.0'; //# sourceMappingURL=json-reporter.js.map