/** * Test text contrast ratios for WCAG AA compliance * WCAG AA requires: * - 4.5:1 for normal text * - 3:1 for large text (18pt+ or 14pt+ bold) and UI components */ // Color definitions from tailwind.config.js const colors = { light: { bg: '#f1f5f9', surface: '#f8fafc', post: '#ffffff', border: '#cbd5e1', text: '#475569', 'text-light': '#64748b', accent: '#94a3b8', highlight: '#e2e8f0' }, dark: { bg: '#0f172a', surface: '#1e293b', post: '#334155', border: '#475569', text: '#cbd5e1', 'text-light': '#94a3b8', accent: '#64748b', highlight: '#475569' } }; /** * Convert hex to RGB */ function hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; } /** * Calculate relative luminance */ function getLuminance(rgb) { const [r, g, b] = [rgb.r / 255, rgb.g / 255, rgb.b / 255].map(val => { return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4); }); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } /** * Calculate contrast ratio */ function getContrastRatio(color1, color2) { const rgb1 = hexToRgb(color1); const rgb2 = hexToRgb(color2); if (!rgb1 || !rgb2) return 0; const lum1 = getLuminance(rgb1); const lum2 = getLuminance(rgb2); const lighter = Math.max(lum1, lum2); const darker = Math.min(lum1, lum2); return (lighter + 0.05) / (darker + 0.05); } /** * Check if contrast meets WCAG AA standards */ function checkContrast(ratio, isLargeText = false) { if (isLargeText) { return ratio >= 3.0; // Large text or UI components } return ratio >= 4.5; // Normal text } /** * Format ratio with pass/fail indicator */ function formatResult(ratio, isLargeText = false) { const passes = checkContrast(ratio, isLargeText); const standard = isLargeText ? '3.0:1' : '4.5:1'; const status = passes ? '✓ PASS' : '✗ FAIL'; return `${ratio.toFixed(2)}:1 (${status} - requires ${standard})`; } console.log('='.repeat(80)); console.log('WCAG AA CONTRAST RATIO TEST'); console.log('='.repeat(80)); console.log(''); // Test Light Mode console.log('LIGHT MODE:'); console.log('-'.repeat(80)); const lightTests = [ { name: 'Text on background', fg: colors.light.text, bg: colors.light.bg }, { name: 'Text-light on background', fg: colors.light['text-light'], bg: colors.light.bg }, { name: 'Text on post', fg: colors.light.text, bg: colors.light.post }, { name: 'Text-light on post', fg: colors.light['text-light'], bg: colors.light.post }, { name: 'Text on surface', fg: colors.light.text, bg: colors.light.surface }, { name: 'Text-light on surface', fg: colors.light['text-light'], bg: colors.light.surface }, { name: 'Text on highlight', fg: colors.light.text, bg: colors.light.highlight }, { name: 'Text-light on highlight', fg: colors.light['text-light'], bg: colors.light.highlight }, { name: 'Accent on background', fg: colors.light.accent, bg: colors.light.bg }, { name: 'Accent on post', fg: colors.light.accent, bg: colors.light.post }, { name: 'Text on accent (buttons)', fg: '#ffffff', bg: colors.light.accent }, ]; lightTests.forEach(test => { const ratio = getContrastRatio(test.fg, test.bg); const normalResult = formatResult(ratio, false); const largeResult = formatResult(ratio, true); console.log(`${test.name.padEnd(35)} Normal: ${normalResult}`); if (!checkContrast(ratio, false) && checkContrast(ratio, true)) { console.log(`${' '.repeat(35)} Large: ${largeResult}`); } }); console.log(''); // Test Dark Mode console.log('DARK MODE:'); console.log('-'.repeat(80)); const darkTests = [ { name: 'Text on background', fg: colors.dark.text, bg: colors.dark.bg }, { name: 'Text-light on background', fg: colors.dark['text-light'], bg: colors.dark.bg }, { name: 'Text on post', fg: colors.dark.text, bg: colors.dark.post }, { name: 'Text-light on post', fg: colors.dark['text-light'], bg: colors.dark.post }, { name: 'Text on surface', fg: colors.dark.text, bg: colors.dark.surface }, { name: 'Text-light on surface', fg: colors.dark['text-light'], bg: colors.dark.surface }, { name: 'Text on highlight', fg: colors.dark.text, bg: colors.dark.highlight }, { name: 'Text-light on highlight', fg: colors.dark['text-light'], bg: colors.dark.highlight }, { name: 'Accent on background', fg: colors.dark.accent, bg: colors.dark.bg }, { name: 'Accent on post', fg: colors.dark.accent, bg: colors.dark.post }, { name: 'Text on accent (buttons)', fg: '#ffffff', bg: colors.dark.accent }, ]; darkTests.forEach(test => { const ratio = getContrastRatio(test.fg, test.bg); const normalResult = formatResult(ratio, false); const largeResult = formatResult(ratio, true); console.log(`${test.name.padEnd(35)} Normal: ${normalResult}`); if (!checkContrast(ratio, false) && checkContrast(ratio, true)) { console.log(`${' '.repeat(35)} Large: ${largeResult}`); } }); console.log(''); console.log('='.repeat(80)); // Summary let lightFailures = 0; let darkFailures = 0; lightTests.forEach(test => { const ratio = getContrastRatio(test.fg, test.bg); if (!checkContrast(ratio, false)) lightFailures++; }); darkTests.forEach(test => { const ratio = getContrastRatio(test.fg, test.bg); if (!checkContrast(ratio, false)) darkFailures++; }); console.log(`SUMMARY:`); console.log(`Light mode failures: ${lightFailures}`); console.log(`Dark mode failures: ${darkFailures}`); console.log(''); if (lightFailures === 0 && darkFailures === 0) { console.log('✓ All contrast ratios meet WCAG AA standards!'); } else { console.log('✗ Some contrast ratios need adjustment.'); console.log(''); console.log('RECOMMENDATIONS:'); if (lightFailures > 0) { console.log('- Light mode: Consider darkening text colors or lightening backgrounds'); } if (darkFailures > 0) { console.log('- Dark mode: Consider lightening text colors or darkening backgrounds'); } }