import { describe, expect, it } from "vitest"; import { contrastRatio, deriveAdminPalette, derivePublicForegrounds, readableColor, } from "@/lib/theme-contrast"; import { PRESETS, THEME_COLOR_KEYS } from "@/lib/theme-presets"; describe("contrastRatio", () => { it("calculates the WCAG ratio for black on white", () => { expect(contrastRatio("#000000", "#ffffff")).toBe(21); }); it("returns null for unsupported color formats", () => { expect(contrastRatio("var(--color-text)", "#ffffff")).toBeNull(); }); }); describe("built-in theme presets", () => { it("defines every theme color in light and dark mode", () => { for (const preset of Object.values(PRESETS)) { expect(Object.keys(preset).sort()).toEqual(["dark", "light"]); for (const palette of [preset.light, preset.dark]) { expect(Object.keys(palette).sort()).toEqual( [...THEME_COLOR_KEYS].sort(), ); } } }); it("derives WCAG-readable public foregrounds for every palette", () => { for (const preset of Object.values(PRESETS)) { for (const palette of [preset.light, preset.dark]) { const backgrounds = [palette.color_background, palette.color_surface]; const foregrounds = [ readableColor(palette.color_text, backgrounds), readableColor(palette.color_text_muted, backgrounds), readableColor(palette.color_primary, backgrounds), readableColor(palette.color_accent, backgrounds), ]; for (const foreground of foregrounds) { for (const background of backgrounds) { expect( contrastRatio(foreground, background), ).toBeGreaterThanOrEqual(4.5); } } const buttonText = readableColor(palette.button_text_color, [ palette.button_primary_color, ]); expect( contrastRatio(buttonText, palette.button_primary_color), ).toBeGreaterThanOrEqual(4.5); const primaryForeground = readableColor(palette.button_text_color, [ palette.color_primary, ]); expect( contrastRatio(primaryForeground, palette.color_primary), ).toBeGreaterThanOrEqual(4.5); } } }); it.each([ "#22c55e", "#ef4444", ])("derives readable text for default button %s", (background) => { const foreground = readableColor("#ffffff", [background]); expect(contrastRatio(foreground, background)).toBeGreaterThanOrEqual(4.5); }); }); describe("readableColor", () => { it("keeps the preferred color when it passes on every background", () => { expect(readableColor("#0f172a", ["#ffffff", "#f8fafc"], 4.5)).toBe( "#0f172a", ); }); it("replaces low-contrast golden text with a readable neutral", () => { const color = readableColor("#f59e0b", ["#ffffff", "#f8fafc"], 4.5); expect(color).toBe("#000000"); expect(contrastRatio(color, "#ffffff")).toBeGreaterThanOrEqual(4.5); expect(contrastRatio(color, "#f8fafc")).toBeGreaterThanOrEqual(4.5); }); it("chooses white text for a dark button when configured text fails", () => { expect(readableColor("#777777", ["#111827"], 4.5)).toBe("#ffffff"); }); it("preserves unsupported custom colors instead of corrupting CSS", () => { expect(readableColor("rgb(1, 2, 3)", ["#ffffff"], 4.5)).toBe( "rgb(1, 2, 3)", ); }); }); describe("deriveAdminPalette", () => { it.each( Object.entries(PRESETS), )("keeps admin text readable for %s", (_name, preset) => { for (const palette of [preset.light, preset.dark]) { const admin = deriveAdminPalette(palette); expect(contrastRatio(admin.text, admin.canvas)).toBeGreaterThanOrEqual( 4.5, ); expect(contrastRatio(admin.text, admin.surface)).toBeGreaterThanOrEqual( 4.5, ); expect(contrastRatio(admin.muted, admin.surface)).toBeGreaterThanOrEqual( 4.5, ); expect( contrastRatio(admin.accentForeground, admin.accent), ).toBeGreaterThanOrEqual(4.5); } }); it("does not copy hostile public structural colors into admin surfaces", () => { const admin = deriveAdminPalette({ color_background: "#ff00ff", color_surface: "#00ff00", color_primary: "#ffff00", }); expect(admin.canvas).not.toBe("#ff00ff"); expect(admin.surface).not.toBe("#00ff00"); }); it("repairs default light text when admin canvas is forced light", () => { const admin = deriveAdminPalette( { color_primary: "#f59e0b" }, { canvas: "#ffffff", surface: "#f8fafc", surfaceElevated: "#f1f5f9", sidebar: "#ffffff", }, ); expect(contrastRatio(admin.text, admin.canvas)).toBeGreaterThanOrEqual(4.5); expect(contrastRatio(admin.text, admin.surface)).toBeGreaterThanOrEqual(4.5); expect(contrastRatio(admin.muted, admin.surface)).toBeGreaterThanOrEqual( 4.5, ); expect( contrastRatio(admin.sidebarText, admin.sidebar), ).toBeGreaterThanOrEqual(4.5); expect( contrastRatio(admin.accentForeground, admin.accent), ).toBeGreaterThanOrEqual(4.5); }); it("repairs hostile admin text overrides on dark canvas", () => { const admin = deriveAdminPalette( { color_primary: "#111827" }, { canvas: "#0b0f19", surface: "#151b27", text: "#0b0f19", muted: "#111827", }, ); expect(contrastRatio(admin.text, admin.canvas)).toBeGreaterThanOrEqual(4.5); expect(contrastRatio(admin.muted, admin.surface)).toBeGreaterThanOrEqual( 4.5, ); expect( contrastRatio(admin.accentForeground, admin.accent), ).toBeGreaterThanOrEqual(4.5); }); }); describe("derivePublicForegrounds", () => { it.each( Object.entries(PRESETS), )("makes every semantic pair readable for %s", (_name, preset) => { for (const palette of [preset.light, preset.dark]) { const result = derivePublicForegrounds(palette); for (const pair of result.pairs) { expect( contrastRatio(pair.foreground, pair.background), pair.name, ).toBeGreaterThanOrEqual(4.5); } } }); it("covers admin hierarchy and every semantic status", () => { const required = [ "subtle", "disabled", "sidebar", "input", "success-text", "success-solid", "warning-text", "warning-solid", "error-text", "error-solid", "info-text", "info-solid", ]; for (const preset of Object.values(PRESETS)) { for (const palette of [preset.light, preset.dark]) { const result = derivePublicForegrounds(palette); for (const name of required) { const pair = result.pairs.find( (candidate) => candidate.name === name, ); expect(pair, `missing ${name}`).toBeDefined(); expect( contrastRatio(pair!.foreground, pair!.background), name, ).toBeGreaterThanOrEqual(4.5); } } } }); it("repairs hostile white-on-white custom colors", () => { const result = derivePublicForegrounds({ color_background: "#ffffff", color_surface: "#ffffff", color_dropdown: "#ffffff", color_navbar: "#ffffff", color_navbar_text: "#ffffff", color_text: "#ffffff", color_text_muted: "#ffffff", color_primary: "#ffffff", color_accent: "#ffffff", button_primary_color: "#ffffff", button_text_color: "#ffffff", button_secondary_color: "#ffffff", button_secondary_text_color: "#ffffff", button_danger_color: "#ffffff", button_danger_text_color: "#ffffff", button_outline_color: "#ffffff", button_outline_text_color: "#ffffff", link_color: "#ffffff", link_hover_color: "#ffffff", }); expect(result.pairs.length).toBeGreaterThan(10); for (const pair of result.pairs) expect( contrastRatio(pair.foreground, pair.background), pair.name, ).toBeGreaterThanOrEqual(4.5); }); });