import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; /** * Kontrast mindestens 4,5:1 (WCAG AA) für Text auf Grund und Panel – in beiden Themes. * Liest die Tokens direkt aus globals.css, damit eine Farbänderung hier auffällt. */ const css = readFileSync("src/app/globals.css", "utf8"); function block(selektor: string): Record { const anfang = css.indexOf(selektor); const inhalt = css.slice(css.indexOf("{", anfang) + 1, css.indexOf("}", anfang)); return Object.fromEntries( [...inhalt.matchAll(/--farbe-([a-z-]+):\s*(#[0-9a-f]{6})/gi)].map((t) => [t[1]!, t[2]!]), ); } function leuchtdichte(hex: string): number { const kanal = (i: number) => { const c = parseInt(hex.slice(i, i + 2), 16) / 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; }; return 0.2126 * kanal(1) + 0.7152 * kanal(3) + 0.0722 * kanal(5); } export function kontrast(a: string, b: string): number { const [hell, dunkel] = [leuchtdichte(a), leuchtdichte(b)].sort((x, y) => y - x) as [number, number]; return (hell + 0.05) / (dunkel + 0.05); } const THEMES = { dunkel: block('[data-theme="dunkel"]'), hell: block('[data-theme="hell"]') }; const TEXTFARBEN = ["text", "muted", "akzent", "laeuft", "fehler"]; describe("Kontrast der Theme-Farben", () => { for (const [name, farben] of Object.entries(THEMES)) { for (const flaeche of ["grund", "panel", "panel-hoch"]) { it.each(TEXTFARBEN)(`${name}: %s auf ${flaeche} ≥ 4,5:1`, (textfarbe) => { expect(kontrast(farben[textfarbe]!, farben[flaeche]!)).toBeGreaterThanOrEqual(4.5); }); } it(`${name}: Text auf Akzentfläche ≥ 4,5:1`, () => { expect(kontrast(farben["akzent-text"]!, farben["akzent"]!)).toBeGreaterThanOrEqual(4.5); }); } });