import { describe, expect, it } from "vitest"; import { applyThemeTo, resolveTheme, systemDarkQuery, themeLocalStorageKey, type ThemeDocument, } from "./theme.js"; describe("resolveTheme", () => { it("light pref wins over system", () => { expect(resolveTheme("light", true)).toBe("light"); }); it("dark pref wins over system", () => { expect(resolveTheme("dark", false)).toBe("dark"); }); it("system follows OS (dark)", () => { expect(resolveTheme("system", true)).toBe("dark"); }); it("system follows OS (light)", () => { expect(resolveTheme("system", false)).toBe("light"); }); }); describe("constants", () => { it("storage key + media query", () => { expect(themeLocalStorageKey()).toBe("factory.theme"); expect(systemDarkQuery()).toBe("(prefers-color-scheme: dark)"); }); }); interface FakeWindow { matchMedia: (q: string) => { matches: boolean; listeners: Array<() => void>; addEventListener(type: string, h: () => void): void; removeEventListener(type: string, h: () => void): void; /** Legacy (Safari <14) API; `applyThemeTo` also supports it. */ addListener(h: () => void): void; removeListener(h: () => void): void; }; calls: string[]; } function makeWindow(matches = true): FakeWindow { // One shared media object per window (like real matchMedia for the same query). const media: ReturnType = { matches, listeners: [], addEventListener(_t, h) { media.listeners.push(h); }, removeEventListener(_t, h) { const i = media.listeners.indexOf(h); if (i >= 0) media.listeners.splice(i, 1); }, addListener(h) { media.listeners.push(h); }, removeListener(h) { const i = media.listeners.indexOf(h); if (i >= 0) media.listeners.splice(i, 1); }, }; return { calls: [], matchMedia: (q) => { expect(q).toBe(systemDarkQuery()); return media; }, }; } function makeDocument(): ThemeDocument { return { dataset: {} }; } describe("applyThemeTo", () => { it("sets data-theme for explicit light with a dark system", () => { const doc = makeDocument(); const win = makeWindow(true); const resolved = applyThemeTo(doc, "light", win); expect(resolved).toBe("light"); expect(doc.dataset.theme).toBe("light"); }); it("sets data-theme for system pref (dark system)", () => { const doc = makeDocument(); const resolved = applyThemeTo(doc, "system", makeWindow(true)); expect(resolved).toBe("dark"); expect(doc.dataset.theme).toBe("dark"); }); it("registers a matchMedia change listener for system pref", () => { const doc = makeDocument(); const win = makeWindow(true); applyThemeTo(doc, "system", win); const media = win.matchMedia(systemDarkQuery()); expect(media.listeners).toHaveLength(1); // simulate OS switch to light media.matches = false; media.listeners[0]?.(); expect(doc.dataset.theme).toBe("light"); }); it("does not register a listener for explicit prefs", () => { const doc = makeDocument(); const win = makeWindow(true); applyThemeTo(doc, "dark", win); expect(win.matchMedia(systemDarkQuery()).listeners).toHaveLength(0); }); it("unregisters the previous listener when re-applied (idempotent)", () => { const doc = makeDocument(); const win = makeWindow(true); applyThemeTo(doc, "system", win); applyThemeTo(doc, "system", win); expect(win.matchMedia(systemDarkQuery()).listeners).toHaveLength(1); applyThemeTo(doc, "light", win); expect(win.matchMedia(systemDarkQuery()).listeners).toHaveLength(0); }); it("works without matchMedia (SSR guard): resolves from default, no crash", () => { const doc = makeDocument(); const resolved = applyThemeTo(doc, "system", undefined); expect(resolved).toBe("light"); // no window → treated as light system expect(doc.dataset.theme).toBe("light"); }); });