From cd28df78e0ed92b7eb2e8fb4fb406f88a127a72f Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Mon, 22 Jun 2026 08:22:26 +0200 Subject: [PATCH] test: cover theme helpers --- src/lib/__tests__/theme.test.ts | 99 +++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/lib/__tests__/theme.test.ts diff --git a/src/lib/__tests__/theme.test.ts b/src/lib/__tests__/theme.test.ts new file mode 100644 index 0000000..60ec189 --- /dev/null +++ b/src/lib/__tests__/theme.test.ts @@ -0,0 +1,99 @@ +import { effectiveTheme, readTheme, writeTheme } from "../theme"; + +const STORAGE_KEY = "stableroute.theme"; +const originalWindow = globalThis.window; + +function mockMatchMedia(matches: boolean) { + // Theme helpers only read `.matches`; the rest mirrors the browser shape. + Object.defineProperty(window, "matchMedia", { + configurable: true, + value: jest.fn().mockReturnValue({ + matches, + media: "(prefers-color-scheme: dark)", + onchange: null, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + addListener: jest.fn(), + removeListener: jest.fn(), + dispatchEvent: jest.fn(), + }), + }); +} + +function withoutWindow(fn: () => T): T { + Object.defineProperty(globalThis, "window", { + configurable: true, + value: undefined, + }); + try { + return fn(); + } finally { + Object.defineProperty(globalThis, "window", { + configurable: true, + value: originalWindow, + }); + } +} + +describe("theme helpers", () => { + beforeEach(() => { + window.localStorage.clear(); + mockMatchMedia(false); + }); + + afterEach(() => { + window.localStorage.clear(); + jest.restoreAllMocks(); + }); + + it.each(["light", "dark", "system"] as const)( + "reads valid stored theme %s", + (theme) => { + window.localStorage.setItem(STORAGE_KEY, theme); + + expect(readTheme()).toBe(theme); + } + ); + + it("falls back to system when no stored theme exists", () => { + expect(readTheme()).toBe("system"); + }); + + it("falls back to system for invalid stored values", () => { + window.localStorage.setItem(STORAGE_KEY, "blue"); + + expect(readTheme()).toBe("system"); + }); + + it("writes the selected theme to localStorage", () => { + writeTheme("dark"); + + expect(window.localStorage.getItem(STORAGE_KEY)).toBe("dark"); + }); + + it("passes explicit themes through as effective themes", () => { + expect(effectiveTheme("light")).toBe("light"); + expect(effectiveTheme("dark")).toBe("dark"); + }); + + it("resolves system to dark when the media query matches", () => { + mockMatchMedia(true); + + expect(effectiveTheme("system")).toBe("dark"); + expect(window.matchMedia).toHaveBeenCalledWith("(prefers-color-scheme: dark)"); + }); + + it("resolves system to light when the media query does not match", () => { + mockMatchMedia(false); + + expect(effectiveTheme("system")).toBe("light"); + }); + + it("uses SSR-safe defaults when window is unavailable", () => { + withoutWindow(() => { + expect(readTheme()).toBe("system"); + expect(effectiveTheme("system")).toBe("light"); + expect(() => writeTheme("dark")).not.toThrow(); + }); + }); +});