- Add 26 documentation files including test reports, deployment guides, and troubleshooting documentation - Include 3 CSV data files for trainer imports and user registration tracking - Add 43 JavaScript test files covering mobile optimization, Safari compatibility, and E2E testing - Include 18 PHP utility files for debugging, geocoding, and data analysis - Add 12 shell scripts for deployment verification, user management, and database operations - Update .gitignore with whitelist patterns for development files, documentation, and CSV data 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
408 lines
No EOL
16 KiB
JavaScript
408 lines
No EOL
16 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
class AccessibilityAnalyzer {
|
||
constructor() {
|
||
this.accessibilityIssues = [];
|
||
this.accessibilityFeatures = [];
|
||
this.colorContrast = [];
|
||
}
|
||
|
||
analyzeAccessibility() {
|
||
console.log('♿ ACCESSIBILITY ANALYSIS');
|
||
console.log('='.repeat(50));
|
||
|
||
const cssFiles = [
|
||
'./assets/css/hvac-common.css',
|
||
'./assets/css/hvac-dashboard.css',
|
||
'./assets/css/hvac-registration.css',
|
||
'./assets/css/hvac-certificates.css',
|
||
'./assets/css/hvac-email-attendees.css',
|
||
'./assets/css/hvac-event-summary.css',
|
||
'./assets/css/hvac-attendee-profile.css',
|
||
'./assets/css/hvac-mobile-nav.css'
|
||
];
|
||
|
||
cssFiles.forEach(file => {
|
||
if (fs.existsSync(file)) {
|
||
this.analyzeFile(file);
|
||
}
|
||
});
|
||
|
||
this.generateAccessibilityReport();
|
||
}
|
||
|
||
analyzeFile(filePath) {
|
||
const content = fs.readFileSync(filePath, 'utf8');
|
||
const fileName = path.basename(filePath);
|
||
|
||
console.log(`\n🔍 ${fileName}:`);
|
||
|
||
// Check for focus management
|
||
this.checkFocusManagement(content, fileName);
|
||
|
||
// Check for keyboard navigation
|
||
this.checkKeyboardNavigation(content, fileName);
|
||
|
||
// Check for screen reader support
|
||
this.checkScreenReaderSupport(content, fileName);
|
||
|
||
// Check color contrast and color dependencies
|
||
this.checkColorAccessibility(content, fileName);
|
||
|
||
// Check for motion and animation accessibility
|
||
this.checkMotionAccessibility(content, fileName);
|
||
|
||
// Check for text scaling support
|
||
this.checkTextScaling(content, fileName);
|
||
}
|
||
|
||
checkFocusManagement(content, fileName) {
|
||
const focusPatterns = {
|
||
':focus': /:focus(?![a-zA-Z-])/g,
|
||
':focus-within': /:focus-within/g,
|
||
':focus-visible': /:focus-visible/g,
|
||
'outline': /outline:\s*[^;]+/g,
|
||
'skip-links': /skip-link/g
|
||
};
|
||
|
||
let focusScore = 0;
|
||
|
||
Object.entries(focusPatterns).forEach(([pattern, regex]) => {
|
||
const matches = content.match(regex);
|
||
if (matches) {
|
||
console.log(` ✅ ${pattern}: ${matches.length} instances`);
|
||
focusScore += matches.length;
|
||
this.accessibilityFeatures.push({
|
||
file: fileName,
|
||
feature: `Focus Management - ${pattern}`,
|
||
count: matches.length
|
||
});
|
||
}
|
||
});
|
||
|
||
if (focusScore === 0) {
|
||
console.log(` ❌ No focus management detected`);
|
||
this.accessibilityIssues.push({
|
||
file: fileName,
|
||
issue: 'No focus styles defined',
|
||
severity: 'high',
|
||
impact: 'Keyboard users cannot see focused elements',
|
||
wcagCriteria: '2.4.7 (Focus Visible)'
|
||
});
|
||
} else {
|
||
console.log(` 📊 Focus management score: ${focusScore}`);
|
||
}
|
||
|
||
// Check for proper focus removal (should be avoided)
|
||
const focusRemoval = content.match(/outline:\s*(none|0)/g);
|
||
if (focusRemoval) {
|
||
console.log(` ⚠️ Focus removal detected: ${focusRemoval.length} instances`);
|
||
this.accessibilityIssues.push({
|
||
file: fileName,
|
||
issue: 'Focus indicators removed',
|
||
severity: 'high',
|
||
impact: 'Breaks keyboard navigation',
|
||
wcagCriteria: '2.4.7 (Focus Visible)'
|
||
});
|
||
}
|
||
}
|
||
|
||
checkKeyboardNavigation(content, fileName) {
|
||
const keyboardPatterns = {
|
||
'tabindex': /tabindex/g,
|
||
'keyboard navigation': /user-is-tabbing|keyboard-nav/g,
|
||
'interactive elements': /:hover.*:focus|:focus.*:hover/g
|
||
};
|
||
|
||
Object.entries(keyboardPatterns).forEach(([pattern, regex]) => {
|
||
const matches = content.match(regex);
|
||
if (matches) {
|
||
console.log(` ✅ ${pattern}: ${matches.length} instances`);
|
||
this.accessibilityFeatures.push({
|
||
file: fileName,
|
||
feature: `Keyboard Navigation - ${pattern}`,
|
||
count: matches.length
|
||
});
|
||
}
|
||
});
|
||
|
||
// Check for hover-only interactions (accessibility issue)
|
||
const hoverOnly = content.match(/:hover(?!.*:focus)/g);
|
||
if (hoverOnly) {
|
||
console.log(` ⚠️ Hover-only interactions: ${hoverOnly.length}`);
|
||
this.accessibilityIssues.push({
|
||
file: fileName,
|
||
issue: 'Hover-only interactions',
|
||
severity: 'medium',
|
||
impact: 'Inaccessible to keyboard users',
|
||
wcagCriteria: '2.1.1 (Keyboard)'
|
||
});
|
||
}
|
||
}
|
||
|
||
checkScreenReaderSupport(content, fileName) {
|
||
const srPatterns = {
|
||
'visually-hidden': /visually-hidden|sr-only|screen-reader/g,
|
||
'aria attributes': /aria-[a-z]+/g,
|
||
'role attributes': /role\s*=/g
|
||
};
|
||
|
||
Object.entries(srPatterns).forEach(([pattern, regex]) => {
|
||
const matches = content.match(regex);
|
||
if (matches) {
|
||
console.log(` ✅ ${pattern}: ${matches.length} instances`);
|
||
this.accessibilityFeatures.push({
|
||
file: fileName,
|
||
feature: `Screen Reader - ${pattern}`,
|
||
count: matches.length
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
checkColorAccessibility(content, fileName) {
|
||
// Extract color values
|
||
const colors = {
|
||
hex: content.match(/#[0-9a-fA-F]{3,6}/g) || [],
|
||
rgb: content.match(/rgb\([^)]+\)/g) || [],
|
||
rgba: content.match(/rgba\([^)]+\)/g) || [],
|
||
hsl: content.match(/hsl\([^)]+\)/g) || [],
|
||
named: content.match(/:\s*(red|blue|green|black|white|gray|grey|yellow|orange|purple|pink|brown)\s*[;}]/g) || []
|
||
};
|
||
|
||
const totalColors = Object.values(colors).flat().length;
|
||
if (totalColors > 0) {
|
||
console.log(` 🎨 Color usage: ${totalColors} color declarations`);
|
||
|
||
// Check for high contrast media query
|
||
const highContrast = content.includes('prefers-contrast') ||
|
||
content.includes('high-contrast');
|
||
if (highContrast) {
|
||
console.log(` ✅ High contrast support detected`);
|
||
this.accessibilityFeatures.push({
|
||
file: fileName,
|
||
feature: 'High Contrast Support',
|
||
count: 1
|
||
});
|
||
}
|
||
|
||
// Check for color-only information
|
||
const colorOnlyWarnings = this.checkColorOnlyInformation(content);
|
||
if (colorOnlyWarnings.length > 0) {
|
||
this.accessibilityIssues.push({
|
||
file: fileName,
|
||
issue: 'Potential color-only information',
|
||
severity: 'medium',
|
||
impact: 'Information may not be accessible to colorblind users',
|
||
wcagCriteria: '1.4.1 (Use of Color)'
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
checkColorOnlyInformation(content) {
|
||
// Simple heuristic check for color-only information
|
||
const suspiciousPatterns = [
|
||
/color:\s*red.*error|error.*color:\s*red/gi,
|
||
/color:\s*green.*success|success.*color:\s*green/gi,
|
||
/background.*red.*warning|warning.*background.*red/gi
|
||
];
|
||
|
||
return suspiciousPatterns.filter(pattern => content.match(pattern));
|
||
}
|
||
|
||
checkMotionAccessibility(content, fileName) {
|
||
const motionPatterns = {
|
||
'animations': /@keyframes|animation:|animation-/g,
|
||
'transitions': /transition:/g,
|
||
'transforms': /transform:/g,
|
||
'reduced motion': /prefers-reduced-motion/g
|
||
};
|
||
|
||
let hasMotion = false;
|
||
let hasReducedMotionSupport = false;
|
||
|
||
Object.entries(motionPatterns).forEach(([pattern, regex]) => {
|
||
const matches = content.match(regex);
|
||
if (matches) {
|
||
if (pattern === 'reduced motion') {
|
||
hasReducedMotionSupport = true;
|
||
console.log(` ✅ ${pattern}: ${matches.length} instances`);
|
||
this.accessibilityFeatures.push({
|
||
file: fileName,
|
||
feature: 'Reduced Motion Support',
|
||
count: matches.length
|
||
});
|
||
} else {
|
||
hasMotion = true;
|
||
console.log(` 🎬 ${pattern}: ${matches.length} instances`);
|
||
}
|
||
}
|
||
});
|
||
|
||
if (hasMotion && !hasReducedMotionSupport) {
|
||
console.log(` ⚠️ Motion without reduced motion support`);
|
||
this.accessibilityIssues.push({
|
||
file: fileName,
|
||
issue: 'Animations without reduced motion support',
|
||
severity: 'medium',
|
||
impact: 'May cause issues for users with vestibular disorders',
|
||
wcagCriteria: '2.3.3 (Animation from Interactions)'
|
||
});
|
||
}
|
||
}
|
||
|
||
checkTextScaling(content, fileName) {
|
||
// Check for relative units that support text scaling
|
||
const relativeUnits = {
|
||
'rem': (content.match(/\d+(\.\d+)?rem/g) || []).length,
|
||
'em': (content.match(/\d+(\.\d+)?em/g) || []).length,
|
||
'%': (content.match(/\d+(\.\d+)?%/g) || []).length
|
||
};
|
||
|
||
const absoluteUnits = {
|
||
'px': (content.match(/\d+px/g) || []).length
|
||
};
|
||
|
||
const totalRelative = Object.values(relativeUnits).reduce((a, b) => a + b, 0);
|
||
const totalAbsolute = Object.values(absoluteUnits).reduce((a, b) => a + b, 0);
|
||
|
||
if (totalRelative > 0) {
|
||
console.log(` ✅ Relative units: ${totalRelative} uses`);
|
||
console.log(` rem: ${relativeUnits.rem}, em: ${relativeUnits.em}, %: ${relativeUnits['%']}`);
|
||
}
|
||
|
||
if (totalAbsolute > 0) {
|
||
console.log(` 📏 Absolute units: ${totalAbsolute} px values`);
|
||
|
||
const ratio = totalRelative / (totalRelative + totalAbsolute);
|
||
if (ratio < 0.5) {
|
||
this.accessibilityIssues.push({
|
||
file: fileName,
|
||
issue: 'Heavy use of absolute units',
|
||
severity: 'low',
|
||
impact: 'May not scale well with user font size preferences',
|
||
wcagCriteria: '1.4.4 (Resize text)'
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
generateAccessibilityReport() {
|
||
console.log('\n' + '='.repeat(80));
|
||
console.log('♿ ACCESSIBILITY ANALYSIS REPORT');
|
||
console.log('='.repeat(80));
|
||
|
||
// WCAG Compliance Overview
|
||
console.log('\n📋 WCAG 2.1 COMPLIANCE OVERVIEW:');
|
||
|
||
const wcagAreas = {
|
||
'Perceivable': ['Focus Visible', 'Use of Color', 'Resize text'],
|
||
'Operable': ['Keyboard', 'Focus Visible', 'Animation from Interactions'],
|
||
'Understandable': ['Focus Visible'],
|
||
'Robust': ['Valid HTML/CSS']
|
||
};
|
||
|
||
Object.entries(wcagAreas).forEach(([principle, criteria]) => {
|
||
console.log(`\n ${principle}:`);
|
||
criteria.forEach(criterion => {
|
||
const relatedIssues = this.accessibilityIssues.filter(issue =>
|
||
issue.wcagCriteria && issue.wcagCriteria.includes(criterion)
|
||
);
|
||
|
||
if (relatedIssues.length === 0) {
|
||
console.log(` ✅ ${criterion} - No major issues detected`);
|
||
} else {
|
||
console.log(` ⚠️ ${criterion} - ${relatedIssues.length} issues found`);
|
||
}
|
||
});
|
||
});
|
||
|
||
// Critical Issues
|
||
if (this.accessibilityIssues.length > 0) {
|
||
console.log('\n❌ ACCESSIBILITY ISSUES:');
|
||
|
||
const highSeverity = this.accessibilityIssues.filter(i => i.severity === 'high');
|
||
const mediumSeverity = this.accessibilityIssues.filter(i => i.severity === 'medium');
|
||
const lowSeverity = this.accessibilityIssues.filter(i => i.severity === 'low');
|
||
|
||
if (highSeverity.length > 0) {
|
||
console.log('\n 🚨 HIGH SEVERITY:');
|
||
highSeverity.forEach((issue, i) => {
|
||
console.log(` ${i + 1}. ${issue.file}: ${issue.issue}`);
|
||
console.log(` Impact: ${issue.impact}`);
|
||
console.log(` WCAG: ${issue.wcagCriteria}`);
|
||
});
|
||
}
|
||
|
||
if (mediumSeverity.length > 0) {
|
||
console.log('\n ⚠️ MEDIUM SEVERITY:');
|
||
mediumSeverity.forEach((issue, i) => {
|
||
console.log(` ${i + 1}. ${issue.file}: ${issue.issue}`);
|
||
console.log(` Impact: ${issue.impact}`);
|
||
console.log(` WCAG: ${issue.wcagCriteria}`);
|
||
});
|
||
}
|
||
|
||
if (lowSeverity.length > 0) {
|
||
console.log('\n ℹ️ LOW SEVERITY:');
|
||
lowSeverity.forEach((issue, i) => {
|
||
console.log(` ${i + 1}. ${issue.file}: ${issue.issue}`);
|
||
console.log(` Impact: ${issue.impact}`);
|
||
console.log(` WCAG: ${issue.wcagCriteria}`);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Positive Features
|
||
if (this.accessibilityFeatures.length > 0) {
|
||
console.log('\n✅ ACCESSIBILITY FEATURES DETECTED:');
|
||
const featureSummary = new Map();
|
||
|
||
this.accessibilityFeatures.forEach(feature => {
|
||
const key = feature.feature;
|
||
if (featureSummary.has(key)) {
|
||
featureSummary.set(key, featureSummary.get(key) + feature.count);
|
||
} else {
|
||
featureSummary.set(key, feature.count);
|
||
}
|
||
});
|
||
|
||
Array.from(featureSummary.entries()).forEach(([feature, count]) => {
|
||
console.log(` • ${feature}: ${count} implementations`);
|
||
});
|
||
}
|
||
|
||
// Recommendations
|
||
console.log('\n💡 ACCESSIBILITY RECOMMENDATIONS:');
|
||
console.log(' 1. Add focus styles to all interactive elements');
|
||
console.log(' 2. Implement skip links for keyboard navigation');
|
||
console.log(' 3. Add prefers-reduced-motion support for animations');
|
||
console.log(' 4. Use semantic HTML with proper ARIA attributes');
|
||
console.log(' 5. Test with screen readers (NVDA, JAWS, VoiceOver)');
|
||
console.log(' 6. Verify color contrast ratios meet WCAG AA standards');
|
||
console.log(' 7. Ensure all functionality is keyboard accessible');
|
||
console.log(' 8. Test with browser zoom up to 200%');
|
||
console.log(' 9. Consider implementing high contrast mode support');
|
||
console.log(' 10. Add proper error handling and messaging');
|
||
|
||
// Summary
|
||
console.log('\n📊 ACCESSIBILITY SUMMARY:');
|
||
console.log(` High Severity Issues: ${this.accessibilityIssues.filter(i => i.severity === 'high').length}`);
|
||
console.log(` Medium Severity Issues: ${this.accessibilityIssues.filter(i => i.severity === 'medium').length}`);
|
||
console.log(` Low Severity Issues: ${this.accessibilityIssues.filter(i => i.severity === 'low').length}`);
|
||
console.log(` Positive Features: ${this.accessibilityFeatures.length}`);
|
||
|
||
const overallScore = Math.max(0, 100 - (
|
||
(this.accessibilityIssues.filter(i => i.severity === 'high').length * 20) +
|
||
(this.accessibilityIssues.filter(i => i.severity === 'medium').length * 10) +
|
||
(this.accessibilityIssues.filter(i => i.severity === 'low').length * 5)
|
||
));
|
||
|
||
console.log(` Overall Accessibility Score: ${overallScore}/100`);
|
||
}
|
||
}
|
||
|
||
const analyzer = new AccessibilityAnalyzer();
|
||
analyzer.analyzeAccessibility(); |