import { describe, expect, it } from "vitest"; import { readFileSync, readdirSync, statSync } from "node:fs"; import path from "node:path"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { applyTheme, currentTheme, readStoredTheme, THEME_KEY, THEME_SCRIPT } from "@/lib/theme"; import { useTheme } from "@/lib/use-theme"; const memoryStorage = (initial: Record = {}) => { const data = new Map(Object.entries(initial)); return { data, getItem: (k: string) => data.get(k) ?? null, setItem: (k: string, v: string) => void data.set(k, v), }; }; const throwingStorage = { getItem: () => { throw new Error("blocked"); }, setItem: () => { throw new Error("blocked"); }, }; const fakeRoot = () => ({ dataset: {} as DOMStringMap, style: { colorScheme: "" } }); describe("readStoredTheme", () => { it("is dark on a first visit", () => { expect(readStoredTheme(memoryStorage())).toBe("dark"); expect(readStoredTheme(null)).toBe("dark"); }); it("returns a stored light or dark choice", () => { expect(readStoredTheme(memoryStorage({ [THEME_KEY]: "light" }))).toBe("light"); expect(readStoredTheme(memoryStorage({ [THEME_KEY]: "dark" }))).toBe("dark"); }); it("ignores junk values and storage that throws", () => { expect(readStoredTheme(memoryStorage({ [THEME_KEY]: "neon" }))).toBe("dark"); expect(readStoredTheme(throwingStorage)).toBe("dark"); }); }); describe("applyTheme", () => { it("sets the attribute and color-scheme and stores the choice", () => { const root = fakeRoot(); const storage = memoryStorage(); applyTheme("light", root, storage); expect(root.dataset.theme).toBe("light"); expect(root.style.colorScheme).toBe("light"); expect(storage.data.get(THEME_KEY)).toBe("light"); expect(currentTheme(root)).toBe("light"); }); it("still changes the theme when storage throws", () => { const root = fakeRoot(); expect(() => applyTheme("light", root, throwingStorage)).not.toThrow(); expect(root.dataset.theme).toBe("light"); }); it("reads a missing attribute as dark", () => { expect(currentTheme(fakeRoot())).toBe("dark"); }); }); describe("THEME_SCRIPT (pre-paint)", () => { const run = (storage: unknown) => { const documentElement = fakeRoot(); new Function("localStorage", "document", THEME_SCRIPT)(storage, { documentElement }); return documentElement; }; it("applies a stored light theme", () => { expect(run(memoryStorage({ [THEME_KEY]: "light" })).dataset.theme).toBe("light"); }); it("falls back to dark for nothing stored, junk, or throwing storage", () => { expect(run(memoryStorage()).dataset.theme).toBe("dark"); expect(run(memoryStorage({ [THEME_KEY]: "x" })).dataset.theme).toBe("dark"); expect(run(throwingStorage).dataset.theme).toBe("dark"); }); }); describe("useTheme", () => { it("renders dark on the server", () => { let seen = ""; const Probe = () => { seen = useTheme().theme; return null; }; renderToStaticMarkup(createElement(Probe)); expect(seen).toBe("dark"); }); }); describe("no raw colors outside the token definitions", () => { const files = (dir: string): string[] => readdirSync(dir).flatMap((f) => { const p = path.join(dir, f); return statSync(p).isDirectory() ? files(p) : [p]; }); it("has no hex color in any source file except globals.css", () => { const src = path.resolve(__dirname, "../src"); const offenders = files(src) .filter((f) => /\.(tsx?|css)$/.test(f) && !f.endsWith(path.join("app", "globals.css"))) .filter((f) => /#[0-9a-fA-F]{3,8}\b|rgba?\(/.test(readFileSync(f, "utf8"))); expect(offenders).toEqual([]); }); });