feat(web): upgrade ThemeProvider for multi-theme registry (#494)
All checks were successful
ci/woodpecker/push/web Pipeline was successful
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>
This commit was merged in pull request #494.
This commit is contained in:
@@ -3,10 +3,12 @@ import { render, screen, act } from "@testing-library/react";
|
||||
import { ThemeProvider, useTheme } from "./ThemeProvider";
|
||||
|
||||
function ThemeConsumer(): React.JSX.Element {
|
||||
const { theme, resolvedTheme, setTheme, toggleTheme } = useTheme();
|
||||
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={() => {
|
||||
@@ -22,6 +24,27 @@ function ThemeConsumer(): React.JSX.Element {
|
||||
>
|
||||
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();
|
||||
@@ -38,7 +61,9 @@ describe("ThemeProvider", (): void => {
|
||||
|
||||
beforeEach((): void => {
|
||||
localStorage.clear();
|
||||
document.documentElement.classList.remove("light", "dark");
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
// Clear any inline style properties set by theme application
|
||||
document.documentElement.removeAttribute("style");
|
||||
|
||||
mockMatchMedia = vi.fn().mockReturnValue({
|
||||
matches: false,
|
||||
@@ -65,6 +90,7 @@ describe("ThemeProvider", (): void => {
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("theme")).toHaveTextContent("light");
|
||||
expect(screen.getByTestId("themeId")).toHaveTextContent("light");
|
||||
});
|
||||
|
||||
it("should NOT read from old 'jarvis-theme' storage key", (): void => {
|
||||
@@ -76,7 +102,6 @@ describe("ThemeProvider", (): void => {
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
// Should default to system, not read from jarvis-theme
|
||||
expect(screen.getByTestId("theme")).toHaveTextContent("system");
|
||||
});
|
||||
|
||||
@@ -106,7 +131,6 @@ describe("ThemeProvider", (): void => {
|
||||
});
|
||||
|
||||
it("should throw when useTheme is used outside provider", (): void => {
|
||||
// Suppress console.error for expected error
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {
|
||||
// Intentionally empty
|
||||
});
|
||||
@@ -117,4 +141,201 @@ describe("ThemeProvider", (): void => {
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user