test(design-system): assert WCAG 2.2 AA contrast for key color pairs
White on --green-dark (not --green) is the correct button background; --green (#3D8C4A) gives only 4.16:1 which fails AA for normal-size text. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
51
frontend/src/lib/design-system/contrast.test.ts
Normal file
51
frontend/src/lib/design-system/contrast.test.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user