// @vitest-environment happy-dom import { act, cleanup, render, screen } from "@testing-library/react"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { PALETTES, ThemeProvider, useTheme } from "./ThemeContext"; // happy-dom's vitest environment ships neither localStorage nor matchMedia; // back both with plain in-memory fakes so persistence and system-theme // reactivity are observable (mirrors ../theme.test.ts's localStorage fake). let store = new Map(); let systemPrefersLight = false; let mediaListeners: Array<(e: MediaQueryListEvent) => void> = []; beforeAll(() => { Object.defineProperty(window, "localStorage", { value: { getItem: (k: string) => store.get(k) ?? null, setItem: (k: string, v: string) => store.set(k, v), removeItem: (k: string) => store.delete(k), }, }); Object.defineProperty(window, "matchMedia", { value: (query: string) => ({ matches: query.includes("light") ? systemPrefersLight : false, media: query, addEventListener: (_type: string, cb: (e: MediaQueryListEvent) => void) => { mediaListeners.push(cb); }, removeEventListener: (_type: string, cb: (e: MediaQueryListEvent) => void) => { mediaListeners = mediaListeners.filter((l) => l !== cb); }, }), }); }); function fireSystemThemeChange(matches: boolean): void { systemPrefersLight = matches; for (const cb of mediaListeners) { cb({ matches } as MediaQueryListEvent); } } function Probe() { const { theme, palette, setTheme, toggleTheme, setPalette } = useTheme(); return (
{theme} {palette}
); } describe("ThemeProvider / useTheme", () => { beforeEach(() => { store = new Map(); mediaListeners = []; systemPrefersLight = false; delete document.documentElement.dataset.theme; delete document.documentElement.dataset.palette; }); afterEach(() => { cleanup(); }); it("defaults to graphite and the system theme, no data-palette attribute", () => { render( , ); expect(screen.getByTestId("theme").textContent).toBe("dark"); expect(screen.getByTestId("palette").textContent).toBe("graphite"); expect(document.documentElement.dataset.palette).toBeUndefined(); }); it("follows a system theme change while unpinned", () => { render( , ); act(() => fireSystemThemeChange(true)); expect(screen.getByTestId("theme").textContent).toBe("light"); }); it("setTheme pins, persists, and stops following the system", () => { render( , ); act(() => screen.getByText("pin-light").click()); expect(screen.getByTestId("theme").textContent).toBe("light"); expect(document.documentElement.dataset.theme).toBe("light"); expect(localStorage.getItem("maestro-theme")).toBe("light"); act(() => fireSystemThemeChange(true)); expect(screen.getByTestId("theme").textContent).toBe("light"); }); it("toggleTheme flips the resolved theme", () => { render( , ); act(() => screen.getByText("toggle").click()); expect(screen.getByTestId("theme").textContent).toBe("light"); act(() => screen.getByText("toggle").click()); expect(screen.getByTestId("theme").textContent).toBe("dark"); }); it("setPalette stamps data-palette and persists; graphite clears it", () => { render( , ); act(() => screen.getByText("set-ember").click()); expect(document.documentElement.dataset.palette).toBe("ember"); expect(localStorage.getItem("maestro-palette")).toBe("ember"); act(() => screen.getByText("set-graphite").click()); expect(document.documentElement.dataset.palette).toBeUndefined(); }); it("a fresh mount re-applies a stored palette", () => { localStorage.setItem("maestro-palette", "periwinkle"); render( , ); expect(screen.getByTestId("palette").textContent).toBe("periwinkle"); expect(document.documentElement.dataset.palette).toBe("periwinkle"); }); it("exposes every palette in picker order", () => { expect(PALETTES).toEqual([ "graphite", "ice-cyan", "periwinkle", "amber-phosphor", "ember", "orchid", ]); }); it("throws when used outside a provider", () => { const Bare = () => { useTheme(); return null; }; expect(() => render()).toThrow( "useTheme must be used within a ThemeProvider", ); }); });