Files
fusion/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx
gsxdsm 6bfdb6bcde feat(KB-024): add comprehensive theme system to dashboard
- Add ThemePreference type and useTheme hook for theme state management
- Create CSS theme architecture with 8 built-in color themes
- Build ThemeSelector component with live preview and keyboard navigation
- Add theme toggle to Header with quick-switch capability
- Integrate Appearance section into SettingsModal
- Wire theme system into App component with system preference detection
- Add comprehensive tests for useTheme, ThemeSelector, and Header
- Include theming documentation in dashboard README
- Add changeset for patch release
2026-03-29 20:22:00 -07:00

256 lines
7.2 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { ThemeSelector } from "../ThemeSelector";
describe("ThemeSelector", () => {
it("renders theme mode toggle buttons", () => {
render(
<ThemeSelector
themeMode="dark"
colorTheme="default"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
expect(screen.getByLabelText("Light mode")).toBeDefined();
expect(screen.getByLabelText("Dark mode")).toBeDefined();
expect(screen.getByLabelText("System mode")).toBeDefined();
});
it("marks current theme mode as active", () => {
render(
<ThemeSelector
themeMode="light"
colorTheme="default"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
const lightBtn = screen.getByLabelText("Light mode");
expect(lightBtn.className).toContain("active");
expect(lightBtn.getAttribute("aria-pressed")).toBe("true");
});
it("marks non-active theme modes as not pressed", () => {
render(
<ThemeSelector
themeMode="dark"
colorTheme="default"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
const lightBtn = screen.getByLabelText("Light mode");
expect(lightBtn.className).not.toContain("active");
expect(lightBtn.getAttribute("aria-pressed")).toBe("false");
});
it("calls onThemeModeChange when a mode is clicked", () => {
const onThemeModeChange = vi.fn();
render(
<ThemeSelector
themeMode="dark"
colorTheme="default"
onThemeModeChange={onThemeModeChange}
onColorThemeChange={vi.fn()}
/>
);
fireEvent.click(screen.getByLabelText("Light mode"));
expect(onThemeModeChange).toHaveBeenCalledWith("light");
fireEvent.click(screen.getByLabelText("System mode"));
expect(onThemeModeChange).toHaveBeenCalledWith("system");
});
it("renders all color theme options", () => {
render(
<ThemeSelector
themeMode="dark"
colorTheme="default"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
expect(screen.getByLabelText("Default theme")).toBeDefined();
expect(screen.getByLabelText("Ocean theme")).toBeDefined();
expect(screen.getByLabelText("Forest theme")).toBeDefined();
expect(screen.getByLabelText("Sunset theme")).toBeDefined();
expect(screen.getByLabelText("Berry theme")).toBeDefined();
expect(screen.getByLabelText("Mono theme")).toBeDefined();
expect(screen.getByLabelText("High Contrast theme")).toBeDefined();
expect(screen.getByLabelText("Solarized theme")).toBeDefined();
});
it("marks current color theme as active", () => {
render(
<ThemeSelector
themeMode="dark"
colorTheme="ocean"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
const oceanBtn = screen.getByLabelText("Ocean theme");
expect(oceanBtn.className).toContain("active");
expect(oceanBtn.getAttribute("aria-pressed")).toBe("true");
});
it("calls onColorThemeChange when a color theme is clicked", () => {
const onColorThemeChange = vi.fn();
render(
<ThemeSelector
themeMode="dark"
colorTheme="default"
onThemeModeChange={vi.fn()}
onColorThemeChange={onColorThemeChange}
/>
);
fireEvent.click(screen.getByLabelText("Forest theme"));
expect(onColorThemeChange).toHaveBeenCalledWith("forest");
fireEvent.click(screen.getByLabelText("Berry theme"));
expect(onColorThemeChange).toHaveBeenCalledWith("berry");
});
it("displays current theme preview", () => {
render(
<ThemeSelector
themeMode="dark"
colorTheme="ocean"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
expect(screen.getByText(/Current theme/)).toBeDefined();
expect(screen.getByText(/Dark \/ Ocean/)).toBeDefined();
});
it("displays system theme in preview when system mode", () => {
render(
<ThemeSelector
themeMode="system"
colorTheme="solarized"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
expect(screen.getByText(/System \/ Solarized/)).toBeDefined();
});
it("displays light theme in preview when light mode", () => {
render(
<ThemeSelector
themeMode="light"
colorTheme="forest"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
expect(screen.getByText(/Light \/ Forest/)).toBeDefined();
});
it("shows correct icon for dark mode in preview", () => {
render(
<ThemeSelector
themeMode="dark"
colorTheme="default"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
const previewIcon = screen.getByText(/Current theme/).closest(".theme-current-preview")?.querySelector("svg");
expect(previewIcon).toBeDefined();
});
it("shows correct icon for light mode in preview", () => {
render(
<ThemeSelector
themeMode="light"
colorTheme="default"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
const previewIcon = screen.getByText(/Current theme/).closest(".theme-current-preview")?.querySelector("svg");
expect(previewIcon).toBeDefined();
});
it("shows correct icon for system mode in preview", () => {
render(
<ThemeSelector
themeMode="system"
colorTheme="default"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
const previewIcon = screen.getByText(/Current theme/).closest(".theme-current-preview")?.querySelector("svg");
expect(previewIcon).toBeDefined();
});
it("renders reset to defaults button", () => {
render(
<ThemeSelector
themeMode="light"
colorTheme="ocean"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
expect(screen.getByLabelText("Reset to default theme")).toBeDefined();
});
it("calls both change handlers when reset is clicked", () => {
const onThemeModeChange = vi.fn();
const onColorThemeChange = vi.fn();
render(
<ThemeSelector
themeMode="light"
colorTheme="ocean"
onThemeModeChange={onThemeModeChange}
onColorThemeChange={onColorThemeChange}
/>
);
fireEvent.click(screen.getByLabelText("Reset to default theme"));
expect(onThemeModeChange).toHaveBeenCalledWith("dark");
expect(onColorThemeChange).toHaveBeenCalledWith("default");
});
it("each color theme has a swatch", () => {
render(
<ThemeSelector
themeMode="dark"
colorTheme="default"
onThemeModeChange={vi.fn()}
onColorThemeChange={vi.fn()}
/>
);
// Query all buttons in theme-grid that have aria-pressed (these are the color theme buttons)
const themeOptions = screen.getAllByRole("button").filter(
(btn) => btn.className.includes("theme-option")
);
expect(themeOptions.length).toBe(8);
themeOptions.forEach((btn) => {
const swatch = btn.querySelector(".theme-option-swatch");
expect(swatch).toBeDefined();
});
});
});