import { describe, it, expect } from 'vitest'; // WCAG 2.2 relative luminance for a single 8-bit channel value function channelLuminance(val: number): number { const sRGB = val / 255; return sRGB <= 0.04045 ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ** 2.4; } function relativeLuminance(hex: string): number { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return 0.2126 * channelLuminance(r) + 0.7152 * channelLuminance(g) + 0.0722 * channelLuminance(b); } function contrastRatio(hex1: string, hex2: string): number { const l1 = relativeLuminance(hex1); const l2 = relativeLuminance(hex2); const lighter = Math.max(l1, l2); const darker = Math.min(l1, l2); return (lighter + 0.05) / (darker + 0.05); } // Design token values from app.css @theme const tokens = { colorText: '#1C1C18', colorTextMuted: '#6B6A63', colorPage: '#FAFAF7', colorSurface: '#F5F4EE', greenDark: '#2E6E39', // button background — --green (#3D8C4A) only gives 4.16:1, fails AA white: '#FFFFFF' }; describe('WCAG 2.2 AA contrast ratios', () => { it('--color-text on --color-page meets 4.5:1 (normal text)', () => { expect(contrastRatio(tokens.colorText, tokens.colorPage)).toBeGreaterThanOrEqual(4.5); }); it('--color-text on --color-surface meets 4.5:1 (card text)', () => { expect(contrastRatio(tokens.colorText, tokens.colorSurface)).toBeGreaterThanOrEqual(4.5); }); it('--color-text-muted on --color-page meets 4.5:1 (muted labels)', () => { expect(contrastRatio(tokens.colorTextMuted, tokens.colorPage)).toBeGreaterThanOrEqual(4.5); }); it('white on --green-dark meets 4.5:1 (primary button background)', () => { // --green (#3D8C4A) only gives 4.16:1 — buttons use --green-dark (#2E6E39) instead expect(contrastRatio(tokens.white, tokens.greenDark)).toBeGreaterThanOrEqual(4.5); }); });