import { describe, it, expect } from 'vitest'; import { AVATAR_PALETTE } from './avatarPalette'; // ─── WCAG 2.1 relative-luminance / contrast (1.4.3) ────────────────────────── // White initials sit on every palette color (DESIGN_RULES §5). The avatar // background is theme-invariant (same hex in light and dark), so a single // white-on-color check covers both themes. function srgbToLinear(channel: number): number { const c = channel / 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 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 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b); } function contrastRatio(a: string, b: string): number { const la = relativeLuminance(a); const lb = relativeLuminance(b); const [hi, lo] = la > lb ? [la, lb] : [lb, la]; return (hi + 0.05) / (lo + 0.05); } const WHITE = '#ffffff'; describe('AVATAR_PALETTE', () => { it('has exactly 10 colors (DESIGN_RULES §5)', () => { expect(AVATAR_PALETTE).toHaveLength(10); }); it('is all lowercase 6-digit hex', () => { for (const c of AVATAR_PALETTE) { expect(c).toMatch(/^#[0-9a-f]{6}$/); } }); it('has no duplicate colors (each person color is distinct)', () => { expect(new Set(AVATAR_PALETTE).size).toBe(AVATAR_PALETTE.length); }); it('every color meets WCAG AA (>=4.5:1) against white initials in both themes', () => { for (const c of AVATAR_PALETTE) { expect(contrastRatio(c, WHITE)).toBeGreaterThanOrEqual(4.5); } }); });