UNPKG

theme-o-rama

Version:

A TypeScript library for dynamic theme management in react + shadcn + tailwind applications

320 lines (319 loc) 12.9 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { describe, it, expect, beforeEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import { renderHook, act } from "@testing-library/react"; import { ThemeProvider, useTheme } from "./theme-context"; import * as themeModule from "./theme"; // Mock the applyTheme function vi.mock("./theme", async () => { const actual = await vi.importActual("./theme"); return { ...actual, applyTheme: vi.fn(), }; }); describe("ThemeProvider", () => { beforeEach(() => { vi.clearAllMocks(); }); it("should render children", () => { render(_jsx(ThemeProvider, { children: _jsx("div", { "data-testid": "child", children: "Test Child" }) })); expect(screen.getByTestId("child")).toBeInTheDocument(); expect(screen.getByText("Test Child")).toBeInTheDocument(); }); it("should start with loading state and then finish loading", async () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider, }); // Initially loading expect(result.current.isLoading).toBe(true); // Wait for loading to complete await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.error).toBe(null); }); it("should load with default theme (light)", async () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.currentTheme).toBeDefined(); expect(result.current.currentTheme?.name).toBe("light"); }); it("should respect defaultTheme prop", async () => { const { result } = renderHook(() => useTheme(), { wrapper: ({ children }) => _jsx(ThemeProvider, { defaultTheme: "dark", children: children }), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.currentTheme?.name).toBe("dark"); }); it("should provide list of available themes", async () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.availableThemes).toBeDefined(); expect(Array.isArray(result.current.availableThemes)).toBe(true); expect(result.current.availableThemes.length).toBeGreaterThan(0); // Should include built-in themes const themeNames = result.current.availableThemes.map((t) => t.name); expect(themeNames).toContain("light"); expect(themeNames).toContain("dark"); expect(themeNames).toContain("color"); }); it("should switch themes using setTheme", async () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.currentTheme?.name).toBe("light"); await act(async () => { await result.current.setTheme("dark"); }); expect(result.current.currentTheme?.name).toBe("dark"); }); it("should call applyTheme when switching themes", async () => { const applyThemeSpy = vi.spyOn(themeModule, "applyTheme"); const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); applyThemeSpy.mockClear(); // Clear initial theme application await act(async () => { await result.current.setTheme("dark"); }); expect(applyThemeSpy).toHaveBeenCalled(); const callArgs = applyThemeSpy.mock.calls[applyThemeSpy.mock.calls.length - 1]; expect(callArgs[0].name).toBe("dark"); expect(callArgs[1]).toBe(document.documentElement); }); it("should call onThemeChange callback when theme changes", async () => { const onThemeChange = vi.fn(); const { result } = renderHook(() => useTheme(), { wrapper: ({ children }) => (_jsx(ThemeProvider, { onThemeChange: onThemeChange, children: children })), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); await act(async () => { await result.current.setTheme("dark"); }); expect(onThemeChange).toHaveBeenCalledWith("dark"); }); it("should load discovered themes from discoverThemes function", async () => { const customTheme = { name: "custom", displayName: "Custom Theme", schemaVersion: 1, colors: { background: "hsl(0 0% 50%)", }, }; const discoverThemes = vi.fn(async () => [customTheme]); const { result } = renderHook(() => useTheme(), { wrapper: ({ children }) => (_jsx(ThemeProvider, { discoverThemes: discoverThemes, children: children })), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(discoverThemes).toHaveBeenCalled(); const themeNames = result.current.availableThemes.map((t) => t.name); expect(themeNames).toContain("custom"); // Should be able to switch to the discovered theme await act(async () => { await result.current.setTheme("custom"); }); expect(result.current.currentTheme?.name).toBe("custom"); }); it("should handle imageResolver prop", async () => { const imageResolver = vi.fn(async (themeName, imagePath) => { return `/resolved/${themeName}/${imagePath}`; }); const { result } = renderHook(() => useTheme(), { wrapper: ({ children }) => (_jsx(ThemeProvider, { imageResolver: imageResolver, children: children })), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.initializeTheme).toBeDefined(); }); it("should setCustomTheme from JSON string", async () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); const customThemeJson = JSON.stringify({ name: "custom-json", displayName: "Custom JSON Theme", schemaVersion: 1, colors: { background: "hsl(0 0% 75%)", }, }); let success = false; await act(async () => { success = await result.current.setCustomTheme(customThemeJson); }); expect(success).toBe(true); expect(result.current.currentTheme?.name).toBe("custom-json"); }); it("should handle invalid JSON in setCustomTheme", async () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); let success = false; await act(async () => { success = await result.current.setCustomTheme("invalid json"); }); expect(success).toBe(false); expect(result.current.error).toBe("Failed to load custom theme"); }); it("should reload themes with reloadThemes", async () => { const customTheme = { name: "custom-reload", displayName: "Custom Reload Theme", schemaVersion: 1, }; const discoverThemes = vi.fn(async () => [customTheme]); const { result } = renderHook(() => useTheme(), { wrapper: ({ children }) => (_jsx(ThemeProvider, { discoverThemes: discoverThemes, defaultTheme: "dark", children: children })), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(discoverThemes).toHaveBeenCalledTimes(1); // Change to custom theme await act(async () => { await result.current.setTheme("custom-reload"); }); expect(result.current.currentTheme?.name).toBe("custom-reload"); // Reload themes await act(async () => { await result.current.reloadThemes(); }); expect(discoverThemes).toHaveBeenCalledTimes(2); // Should reset to default theme after reload expect(result.current.currentTheme?.name).toBe("dark"); }); it("should prevent concurrent setTheme calls", async () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); // Call setTheme twice rapidly await act(async () => { const promise1 = result.current.setTheme("dark"); const promise2 = result.current.setTheme("color"); await Promise.all([promise1, promise2]); }); // Should have set one of them (the concurrent call should be prevented) expect(result.current.currentTheme?.name).toMatch(/dark|color|light/); }); it("should handle errors when setting theme", async () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); // Try to set a non-existent theme const applyThemeSpy = vi.spyOn(themeModule, "applyTheme"); applyThemeSpy.mockImplementationOnce(() => { throw new Error("Test error"); }); await act(async () => { await result.current.setTheme("dark"); }); expect(result.current.error).toBe("Failed to set theme"); }); it("should clear error on successful theme change", async () => { const applyThemeSpy = vi.spyOn(themeModule, "applyTheme"); const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); // First, cause an error applyThemeSpy.mockImplementationOnce(() => { throw new Error("Test error"); }); await act(async () => { await result.current.setTheme("dark"); }); expect(result.current.error).toBe("Failed to set theme"); // Now set successfully applyThemeSpy.mockImplementation(vi.fn()); await act(async () => { await result.current.setTheme("color"); }); expect(result.current.error).toBe(null); }); it("should provide initializeTheme function", async () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.initializeTheme).toBeDefined(); expect(typeof result.current.initializeTheme).toBe("function"); const testTheme = { name: "test-init", displayName: "Test Init", schemaVersion: 1, backgroundImage: "test.jpg", }; await act(async () => { const initialized = await result.current.initializeTheme(testTheme); expect(initialized).toBeDefined(); }); }); it("should handle theme inheritance through initializeTheme", async () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); const inheritedTheme = { name: "inherited", displayName: "Inherited Theme", schemaVersion: 1, inherits: "dark", colors: { primary: "hsl(200 100% 50%)", }, }; await act(async () => { const initialized = await result.current.initializeTheme(inheritedTheme); expect(initialized).toBeDefined(); expect(initialized.name).toBe("inherited"); }); }); }); describe("useTheme", () => { it("should throw error when used outside ThemeProvider", () => { expect(() => { renderHook(() => useTheme()); }).toThrow("useTheme must be used within a ThemeProvider"); }); });