All checks were successful
ci/woodpecker/push/web Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
342 lines
8.6 KiB
TypeScript
342 lines
8.6 KiB
TypeScript
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 (
|
|
<div>
|
|
<span data-testid="theme">{theme}</span>
|
|
<span data-testid="themeId">{themeId}</span>
|
|
<span data-testid="themeName">{themeDefinition.name}</span>
|
|
<span data-testid="resolved">{resolvedTheme}</span>
|
|
<button
|
|
onClick={() => {
|
|
setTheme("light");
|
|
}}
|
|
>
|
|
Set Light
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setTheme("dark");
|
|
}}
|
|
>
|
|
Set Dark
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setTheme("nord");
|
|
}}
|
|
>
|
|
Set Nord
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setTheme("dracula");
|
|
}}
|
|
>
|
|
Set Dracula
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setTheme("system");
|
|
}}
|
|
>
|
|
Set System
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
toggleTheme();
|
|
}}
|
|
>
|
|
Toggle
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
describe("ThemeProvider", (): void => {
|
|
let mockMatchMedia: ReturnType<typeof vi.fn>;
|
|
|
|
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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
expect(screen.getByTestId("theme")).toHaveTextContent("system");
|
|
});
|
|
|
|
it("should store theme under 'mosaic-theme' key", (): void => {
|
|
render(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
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(
|
|
<ThemeProvider>
|
|
<div data-testid="child">Hello</div>
|
|
</ThemeProvider>
|
|
);
|
|
|
|
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(<ThemeConsumer />);
|
|
}).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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
expect(screen.getByTestId("themeId")).toHaveTextContent("light");
|
|
expect(screen.getByTestId("resolved")).toHaveTextContent("light");
|
|
});
|
|
|
|
it("should support Nord theme", (): void => {
|
|
localStorage.setItem("mosaic-theme", "nord");
|
|
|
|
render(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
// 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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
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(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
);
|
|
|
|
expect(screen.getByTestId("themeName")).toHaveTextContent("Dark");
|
|
});
|
|
});
|