import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, act } from "@testing-library/react"; import { ThemeProvider, useTheme } from "./ThemeProvider"; function ThemeConsumer(): React.JSX.Element { const { theme, themeId, themeDefinition, resolvedTheme, setTheme, toggleTheme } = useTheme(); return (
{theme} {themeId} {themeDefinition.name} {resolvedTheme}
); } describe("ThemeProvider", (): void => { let mockMatchMedia: ReturnType; beforeEach((): void => { localStorage.clear(); document.documentElement.removeAttribute("data-theme"); // Clear any inline style properties set by theme application document.documentElement.removeAttribute("style"); mockMatchMedia = vi.fn().mockReturnValue({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn(), }); Object.defineProperty(window, "matchMedia", { writable: true, value: mockMatchMedia, }); }); afterEach((): void => { vi.restoreAllMocks(); }); it("should use 'mosaic-theme' as storage key", (): void => { localStorage.setItem("mosaic-theme", "light"); render( ); expect(screen.getByTestId("theme")).toHaveTextContent("light"); expect(screen.getByTestId("themeId")).toHaveTextContent("light"); }); it("should NOT read from old 'jarvis-theme' storage key", (): void => { localStorage.setItem("jarvis-theme", "light"); render( ); expect(screen.getByTestId("theme")).toHaveTextContent("system"); }); it("should store theme under 'mosaic-theme' key", (): void => { render( ); act(() => { screen.getByText("Set Light").click(); }); expect(localStorage.getItem("mosaic-theme")).toBe("light"); expect(localStorage.getItem("jarvis-theme")).toBeNull(); }); it("should render children", (): void => { render(
Hello
); expect(screen.getByTestId("child")).toBeInTheDocument(); }); it("should throw when useTheme is used outside provider", (): void => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { // Intentionally empty }); expect(() => { render(); }).toThrow("useTheme must be used within a ThemeProvider"); consoleSpy.mockRestore(); }); it("should resolve 'system' to dark when OS prefers dark", (): void => { mockMatchMedia.mockReturnValue({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn(), }); render( ); expect(screen.getByTestId("themeId")).toHaveTextContent("dark"); expect(screen.getByTestId("resolved")).toHaveTextContent("dark"); }); it("should resolve 'system' to light when OS prefers light", (): void => { mockMatchMedia.mockReturnValue({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn(), }); render( ); expect(screen.getByTestId("themeId")).toHaveTextContent("light"); expect(screen.getByTestId("resolved")).toHaveTextContent("light"); }); it("should support Nord theme", (): void => { localStorage.setItem("mosaic-theme", "nord"); render( ); expect(screen.getByTestId("theme")).toHaveTextContent("nord"); expect(screen.getByTestId("themeId")).toHaveTextContent("nord"); expect(screen.getByTestId("themeName")).toHaveTextContent("Nord"); expect(screen.getByTestId("resolved")).toHaveTextContent("dark"); }); it("should support Dracula theme", (): void => { localStorage.setItem("mosaic-theme", "dracula"); render( ); expect(screen.getByTestId("themeId")).toHaveTextContent("dracula"); expect(screen.getByTestId("resolved")).toHaveTextContent("dark"); }); it("should support Solarized Dark theme", (): void => { localStorage.setItem("mosaic-theme", "solarized-dark"); render( ); expect(screen.getByTestId("themeId")).toHaveTextContent("solarized-dark"); expect(screen.getByTestId("resolved")).toHaveTextContent("dark"); }); it("should fall back to system for unknown theme IDs", (): void => { localStorage.setItem("mosaic-theme", "nonexistent-theme"); render( ); // Falls back to "system" because "nonexistent-theme" is not a valid theme ID expect(screen.getByTestId("theme")).toHaveTextContent("system"); }); it("should switch between themes via setTheme", (): void => { render( ); act(() => { screen.getByText("Set Nord").click(); }); expect(screen.getByTestId("themeId")).toHaveTextContent("nord"); expect(screen.getByTestId("themeName")).toHaveTextContent("Nord"); expect(localStorage.getItem("mosaic-theme")).toBe("nord"); act(() => { screen.getByText("Set Dracula").click(); }); expect(screen.getByTestId("themeId")).toHaveTextContent("dracula"); expect(localStorage.getItem("mosaic-theme")).toBe("dracula"); }); it("should toggle between dark and light", (): void => { localStorage.setItem("mosaic-theme", "dark"); render( ); expect(screen.getByTestId("resolved")).toHaveTextContent("dark"); act(() => { screen.getByText("Toggle").click(); }); expect(screen.getByTestId("themeId")).toHaveTextContent("light"); expect(screen.getByTestId("resolved")).toHaveTextContent("light"); }); it("should toggle from a dark theme (nord) to light", (): void => { localStorage.setItem("mosaic-theme", "nord"); render( ); expect(screen.getByTestId("resolved")).toHaveTextContent("dark"); act(() => { screen.getByText("Toggle").click(); }); expect(screen.getByTestId("themeId")).toHaveTextContent("light"); expect(screen.getByTestId("resolved")).toHaveTextContent("light"); }); it("should apply CSS variables on theme change", (): void => { render( ); act(() => { screen.getByText("Set Nord").click(); }); // Nord's bg-900 is #2e3440 const bgValue = document.documentElement.style.getPropertyValue("--ms-bg-900"); expect(bgValue).toBe("#2e3440"); }); it("should set data-theme attribute based on isDark", (): void => { render( ); act(() => { screen.getByText("Set Nord").click(); }); expect(document.documentElement.getAttribute("data-theme")).toBe("dark"); act(() => { screen.getByText("Set Light").click(); }); expect(document.documentElement.getAttribute("data-theme")).toBe("light"); }); it("should expose themeDefinition with full theme data", (): void => { localStorage.setItem("mosaic-theme", "dark"); render( ); expect(screen.getByTestId("themeName")).toHaveTextContent("Dark"); }); });