a11yanalyze
Version:
A command-line tool for developers and QA engineers to test web pages and websites for WCAG 2.2 AA accessibility compliance
491 lines • 19.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.IssueProcessor = void 0;
const wcag_database_1 = require("../data/wcag-database");
/**
* Enhanced issue processor for detailed accessibility reporting
*/
class IssueProcessor {
constructor() {
this.defaultOptions = {
includeCodeExamples: true,
includeTestingGuidance: true,
includeTimeEstimates: true,
priorityMode: 'impact',
contextAware: true,
maxCodeExamples: 3,
};
}
/**
* Process and enhance accessibility issues with detailed WCAG information
*/
async processIssues(issues, options = {}) {
const processingOptions = { ...this.defaultOptions, ...options };
const enhancedIssues = await Promise.all(issues.map(issue => this.enhanceIssue(issue, processingOptions)));
// Sort by priority score if available
return enhancedIssues.sort((a, b) => (b.priorityScore || 0) - (a.priorityScore || 0));
}
/**
* Enhance a single issue with detailed information
* @private
*/
async enhanceIssue(issue, options) {
const wcagCriterion = (0, wcag_database_1.getWCAGCriterion)(issue.wcagReference);
const baseReportIssue = this.convertToReportIssue(issue);
const enhanced = {
...baseReportIssue,
wcagCriterion,
priorityScore: this.calculatePriorityScore(issue, wcagCriterion, options.priorityMode),
impactAssessment: this.assessIssueImpact(issue, wcagCriterion),
complexity: this.assessFixComplexity(issue, wcagCriterion),
};
if (options.contextAware) {
enhanced.contextualRemediation = this.generateContextualRemediation(issue, wcagCriterion, options);
}
if (options.includeTestingGuidance && wcagCriterion) {
enhanced.testingGuidance = this.generateTestingGuidance(wcagCriterion, issue);
}
if (wcagCriterion) {
enhanced.relatedIssues = this.findRelatedIssues(issue, wcagCriterion);
}
return enhanced;
}
/**
* Convert AccessibilityIssue to ReportIssue format
* @private
*/
convertToReportIssue(issue) {
return {
id: issue.id,
wcagReference: issue.wcagReference,
level: issue.level,
severity: issue.severity,
category: this.categorizeIssue(issue),
title: this.generateIssueTitle(issue),
description: issue.message,
impact: issue.impact,
remediation: issue.remediation,
helpUrl: issue.helpUrl,
occurrences: 1,
elements: [this.convertToAffectedElement(issue)],
};
}
/**
* Generate contextual remediation guidance
* @private
*/
generateContextualRemediation(issue, wcagCriterion, options) {
const context = this.analyzeElementContext(issue);
const patternMatch = this.findPatternMatch(issue);
let summary = issue.remediation;
let steps = [];
let codeExamples = [];
let urgency = 'medium';
if (wcagCriterion) {
summary = wcagCriterion.remediation.summary;
steps = wcagCriterion.remediation.steps;
if (options?.includeCodeExamples) {
codeExamples = wcagCriterion.examples.slice(0, options.maxCodeExamples);
}
// Adjust urgency based on severity and level
urgency = this.determineUrgency(issue, wcagCriterion);
}
// Add context-specific guidance
if (context.isFormElement) {
steps = this.addFormSpecificSteps(steps, issue);
}
if (context.isInteractiveElement) {
steps = this.addInteractiveElementSteps(steps, issue);
}
if (context.isImageElement) {
steps = this.addImageSpecificSteps(steps, issue);
}
return {
summary: this.contextualizeGuidance(summary, context),
steps: this.contextualizeSteps(steps, context),
codeExamples,
recommendedTools: this.getRecommendedTools(issue, wcagCriterion),
considerations: this.getContextualConsiderations(issue, context, wcagCriterion),
urgency,
};
}
/**
* Calculate priority score for issue
* @private
*/
calculatePriorityScore(issue, wcagCriterion, priorityMode = 'impact') {
let score = 0;
// Base severity score
const severityScores = {
critical: 100,
serious: 80,
moderate: 60,
minor: 40,
warning: 20
};
score += severityScores[issue.severity];
// WCAG level impact
const levelScores = { A: 30, AA: 20, AAA: 10, ARIA: 15 };
score += levelScores[issue.level];
switch (priorityMode) {
case 'severity':
// Severity-first prioritization
score *= 1.5;
break;
case 'impact':
// User impact prioritization
if (this.isHighImpactIssue(issue))
score += 25;
break;
case 'effort':
// Effort-based prioritization (easier fixes first)
const complexity = wcagCriterion?.remediation.difficulty;
if (complexity === 'easy')
score += 20;
else if (complexity === 'moderate')
score += 10;
break;
case 'quickWins':
// Quick wins (high impact, low effort)
if (this.isQuickWin(issue, wcagCriterion))
score += 30;
break;
}
return Math.round(score);
}
/**
* Assess the impact of an issue
* @private
*/
assessIssueImpact(issue, wcagCriterion) {
const assistiveTechImpact = this.determineAssistiveTechImpact(issue);
const usersAffected = this.estimateUsersAffected(issue, wcagCriterion);
const complianceRisk = this.assessComplianceRisk(issue);
return {
usersAffected,
assistiveTechImpact,
businessImpact: this.generateBusinessImpact(issue, usersAffected),
complianceRisk,
};
}
/**
* Assess fix complexity
* @private
*/
assessFixComplexity(issue, wcagCriterion) {
const context = this.analyzeElementContext(issue);
let level = 'moderate';
let estimatedTime = '15-30 minutes';
let skillsRequired = ['HTML/CSS'];
let dependencies = [];
let riskLevel = 'low';
if (wcagCriterion) {
switch (wcagCriterion.remediation.difficulty) {
case 'easy':
level = 'simple';
estimatedTime = wcagCriterion.remediation.estimatedTime;
break;
case 'complex':
level = 'complex';
estimatedTime = wcagCriterion.remediation.estimatedTime;
skillsRequired = ['HTML/CSS', 'JavaScript', 'ARIA'];
riskLevel = 'medium';
break;
default:
estimatedTime = wcagCriterion.remediation.estimatedTime;
}
}
// Adjust based on context
if (context.isCustomComponent) {
level = level === 'simple' ? 'moderate' : 'complex';
skillsRequired.push('Component architecture');
riskLevel = 'medium';
}
if (context.requiresJavaScript) {
skillsRequired.push('JavaScript');
if (level === 'simple')
level = 'moderate';
}
return {
level,
estimatedTime,
skillsRequired,
dependencies,
riskLevel,
};
}
/**
* Generate testing guidance
* @private
*/
generateTestingGuidance(wcagCriterion, issue) {
const quickTests = this.generateQuickTests(issue, wcagCriterion);
return {
quickTests,
automatedTools: wcagCriterion.testing.automated,
screenReaderTests: wcagCriterion.testing.screenReader,
keyboardTests: wcagCriterion.testing.keyboard,
verificationCriteria: this.generateVerificationCriteria(issue, wcagCriterion),
};
}
/**
* Analyze element context
* @private
*/
analyzeElementContext(issue) {
const element = issue.element.toLowerCase();
const selector = issue.selector.toLowerCase();
return {
isFormElement: /input|select|textarea|button|fieldset|legend|label/.test(element),
isInteractiveElement: /button|input|select|textarea|a\[href\]|role="button"|role="link"/.test(element + selector),
isImageElement: /img|svg|role="img"/.test(element + selector),
isCustomComponent: /role=|aria-/.test(element + selector),
requiresJavaScript: /onclick|role="button"|role="tab"|role="dialog"/.test(element + selector),
isTableElement: /table|th|td|caption/.test(element),
isHeadingElement: /h[1-6]|role="heading"/.test(element + selector),
isLandmarkElement: /nav|main|aside|section|role="banner"|role="navigation"/.test(element + selector),
};
}
/**
* Helper methods for issue analysis
* @private
*/
categorizeIssue(issue) {
const ref = issue.wcagReference;
const element = issue.element.toLowerCase();
if (ref.startsWith('1.1'))
return 'images';
if (ref.startsWith('1.3'))
return 'structure';
if (ref.startsWith('1.4') && ref.includes('3'))
return 'color';
if (ref.startsWith('2.1'))
return 'keyboard';
if (ref.startsWith('2.4'))
return 'navigation';
if (ref.startsWith('3.3'))
return 'forms';
if (ref.startsWith('4.1'))
return 'aria';
if (element.includes('input') || element.includes('form'))
return 'forms';
if (element.includes('img'))
return 'images';
return 'other';
}
generateIssueTitle(issue) {
const message = issue.message;
return message?.split('.')[0]?.trim() || 'Accessibility Issue';
}
convertToAffectedElement(issue) {
return {
selector: issue.selector,
html: this.truncateHtml(issue.element),
text: this.extractElementText(issue.element),
context: `Issue: ${issue.message}`,
};
}
truncateHtml(html, maxLength = 200) {
return html.length > maxLength ? html.substring(0, maxLength) + '...' : html;
}
extractElementText(html) {
const textMatch = html.match(/>([^<]+)</);
return textMatch?.[1]?.trim();
}
findPatternMatch(issue) {
return Object.entries(wcag_database_1.COMMON_ISSUE_PATTERNS).find(([_, pattern]) => pattern.criteria.includes(issue.wcagReference));
}
determineUrgency(issue, wcagCriterion) {
if (issue.severity === 'critical')
return 'immediate';
if (issue.severity === 'serious' && issue.level === 'A')
return 'high';
if (issue.severity === 'serious')
return 'medium';
return 'low';
}
addFormSpecificSteps(steps, issue) {
const formSteps = [
'Test form completion using only a screen reader',
'Verify error messages are properly associated with form controls',
'Check that required field indicators are accessible',
];
return [...steps, ...formSteps];
}
addInteractiveElementSteps(steps, issue) {
const interactiveSteps = [
'Test keyboard navigation to and from this element',
'Verify focus indicators are visible and sufficient',
'Test activation with both Enter and Space keys',
];
return [...steps, ...interactiveSteps];
}
addImageSpecificSteps(steps, issue) {
const imageSteps = [
'Review image in context to determine appropriate alt text',
'Consider if image is decorative, informative, or functional',
'Test with images disabled to verify text alternative is sufficient',
];
return [...steps, ...imageSteps];
}
contextualizeGuidance(summary, context) {
if (context.isFormElement) {
return `${summary} This is particularly important for form elements as screen reader users rely on proper labeling to complete forms successfully.`;
}
if (context.isInteractiveElement) {
return `${summary} Interactive elements must be keyboard accessible and properly announced to assistive technologies.`;
}
return summary;
}
contextualizeSteps(steps, context) {
// Add context-specific steps at the beginning
const contextSteps = [];
if (context.isCustomComponent) {
contextSteps.push('Review ARIA authoring practices for this component type');
}
if (context.requiresJavaScript) {
contextSteps.push('Ensure progressive enhancement - basic functionality without JavaScript');
}
return [...contextSteps, ...steps];
}
getRecommendedTools(issue, wcagCriterion) {
const tools = ['axe browser extension', 'Screen reader'];
if (wcagCriterion) {
tools.push(...wcagCriterion.remediation.tools);
}
// Add context-specific tools
if (issue.wcagReference === '1.4.3') {
tools.push('WebAIM Contrast Checker', 'Colour Contrast Analyser');
}
if (issue.wcagReference.startsWith('2.1')) {
tools.push('Keyboard navigation testing');
}
return [...new Set(tools)]; // Remove duplicates
}
getContextualConsiderations(issue, context, wcagCriterion) {
const considerations = wcagCriterion?.remediation.considerations || [];
if (context.isFormElement) {
considerations.push('Consider the user journey and form validation feedback');
}
if (context.isCustomComponent) {
considerations.push('Follow established design patterns for consistency');
}
return considerations;
}
isHighImpactIssue(issue) {
const highImpactRefs = ['1.1.1', '2.1.1', '3.3.2', '4.1.2'];
return highImpactRefs.includes(issue.wcagReference) || issue.severity === 'critical';
}
isQuickWin(issue, wcagCriterion) {
return (issue.severity === 'serious' &&
wcagCriterion?.remediation.difficulty === 'easy');
}
determineAssistiveTechImpact(issue) {
const impact = [];
if (issue.wcagReference.startsWith('1.1')) {
impact.push('Screen readers', 'Braille displays');
}
if (issue.wcagReference.startsWith('2.1')) {
impact.push('Keyboard users', 'Switch devices', 'Voice control');
}
if (issue.wcagReference.startsWith('1.4.3')) {
impact.push('Users with low vision', 'Users with color vision deficiency');
}
return impact;
}
estimateUsersAffected(issue, wcagCriterion) {
// Rough estimates based on disability statistics and issue type
const baseRates = {
'1.1.1': 15, // Screen reader users + others who benefit
'1.4.3': 25, // Low vision + color vision deficiency
'2.1.1': 20, // Motor disabilities + preference users
'3.3.2': 30, // Cognitive disabilities + all users
'4.1.2': 15, // Assistive technology users
};
return baseRates[issue.wcagReference] || 10;
}
assessComplianceRisk(issue) {
if (issue.level === 'A' && issue.severity === 'critical')
return 'high';
if (issue.level === 'AA' && ['critical', 'serious'].includes(issue.severity))
return 'high';
if (issue.severity === 'serious')
return 'medium';
return 'low';
}
generateBusinessImpact(issue, usersAffected) {
if (usersAffected > 20) {
return 'High impact: Affects a significant portion of users and may impact brand reputation';
}
if (usersAffected > 10) {
return 'Medium impact: Affects notable user segment and may impact user satisfaction';
}
return 'Low impact: Affects smaller user segment but important for compliance and inclusion';
}
generateQuickTests(issue, wcagCriterion) {
const tests = wcagCriterion.testing.manual.slice(0, 3);
// Add issue-specific quick tests
if (issue.wcagReference === '1.1.1') {
tests.unshift('Right-click image and check if alt text appears in context menu');
}
if (issue.wcagReference.startsWith('2.1')) {
tests.unshift('Tab to element and try activating with Enter and Space keys');
}
return tests;
}
generateVerificationCriteria(issue, wcagCriterion) {
return [
'Issue is no longer detected by automated testing tools',
'Manual testing confirms expected behavior',
'Screen reader announces content appropriately',
'Solution works across different browsers and assistive technologies',
];
}
findRelatedIssues(issue, wcagCriterion) {
return wcagCriterion.relatedCriteria.slice(0, 3);
}
/**
* Generate issue summary statistics
*/
generateIssueSummary(issues) {
const summary = {
total: issues.length,
bySeverity: this.groupBySeverity(issues),
byCategory: this.groupByCategory(issues),
byComplexity: this.groupByComplexity(issues),
byUrgency: this.groupByUrgency(issues),
quickWins: issues.filter(issue => issue.complexity?.level === 'simple' &&
['critical', 'serious'].includes(issue.severity)).length,
highImpact: issues.filter(issue => (issue.impactAssessment?.usersAffected || 0) > 20).length,
};
return summary;
}
groupBySeverity(issues) {
return issues.reduce((acc, issue) => {
acc[issue.severity] = (acc[issue.severity] || 0) + 1;
return acc;
}, {});
}
groupByCategory(issues) {
return issues.reduce((acc, issue) => {
acc[issue.category] = (acc[issue.category] || 0) + 1;
return acc;
}, {});
}
groupByComplexity(issues) {
return issues.reduce((acc, issue) => {
const level = issue.complexity?.level || 'unknown';
acc[level] = (acc[level] || 0) + 1;
return acc;
}, {});
}
groupByUrgency(issues) {
return issues.reduce((acc, issue) => {
const urgency = issue.contextualRemediation?.urgency || 'medium';
acc[urgency] = (acc[urgency] || 0) + 1;
return acc;
}, {});
}
}
exports.IssueProcessor = IssueProcessor;
//# sourceMappingURL=issue-processor.js.map