feat: theme dropdown with system preference and comprehensive bottle-type fixes (#138)
- Replace dark/light toggle with Light/Dark/System dropdown menu - System theme follows OS prefers-color-scheme setting - Apply theme dropdown to shared schedule page - Fix 7 packageType (bottle) bugs across stock calc, share, refills, export/import - Fix planner bottle-type stock calculation and display - Fix dailyRate double-counting with per-intake takenBy - Fix About modal update check stale caching - Fix intake reminder past-intake seeding and push title - Fix phantom DB path in drizzle.config.ts - Fix mobile dose field visibility - Make medication name clickable in dashboard reminder bar - Improve planner checkbox UX with inline tooltip - Add 20+ new tests covering all fixes
This commit is contained in:
@@ -8,6 +8,20 @@ describe("useTheme", () => {
|
||||
(window.localStorage.getItem as ReturnType<typeof vi.fn>).mockReturnValue(null);
|
||||
// Reset mock to default behavior
|
||||
(window.localStorage.setItem as ReturnType<typeof vi.fn>).mockImplementation(() => {});
|
||||
// Mock matchMedia to return dark system theme by default
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -17,58 +31,76 @@ describe("useTheme", () => {
|
||||
it("returns dark as default theme", () => {
|
||||
const { result } = renderHook(() => useTheme());
|
||||
expect(result.current.theme).toBe("dark");
|
||||
expect(result.current.themePreference).toBe("dark");
|
||||
});
|
||||
|
||||
it("reads theme from localStorage", () => {
|
||||
it("reads theme preference from localStorage", () => {
|
||||
(window.localStorage.getItem as ReturnType<typeof vi.fn>).mockReturnValue("light");
|
||||
const { result } = renderHook(() => useTheme());
|
||||
expect(result.current.theme).toBe("light");
|
||||
expect(result.current.themePreference).toBe("light");
|
||||
});
|
||||
|
||||
it("toggles theme from dark to light", () => {
|
||||
const { result } = renderHook(() => useTheme());
|
||||
|
||||
expect(result.current.theme).toBe("dark");
|
||||
|
||||
act(() => {
|
||||
result.current.toggleTheme();
|
||||
});
|
||||
|
||||
expect(result.current.theme).toBe("light");
|
||||
});
|
||||
|
||||
it("toggles theme from light to dark", () => {
|
||||
it("toggles theme through light → dark → system → light", () => {
|
||||
(window.localStorage.getItem as ReturnType<typeof vi.fn>).mockReturnValue("light");
|
||||
const { result } = renderHook(() => useTheme());
|
||||
expect(result.current.themePreference).toBe("light");
|
||||
|
||||
act(() => {
|
||||
result.current.toggleTheme();
|
||||
});
|
||||
expect(result.current.themePreference).toBe("dark");
|
||||
|
||||
act(() => {
|
||||
result.current.toggleTheme();
|
||||
});
|
||||
expect(result.current.themePreference).toBe("system");
|
||||
|
||||
act(() => {
|
||||
result.current.toggleTheme();
|
||||
});
|
||||
expect(result.current.themePreference).toBe("light");
|
||||
});
|
||||
|
||||
it("sets theme preference directly", () => {
|
||||
const { result } = renderHook(() => useTheme());
|
||||
expect(result.current.themePreference).toBe("dark");
|
||||
|
||||
act(() => {
|
||||
result.current.setThemePreference("light");
|
||||
});
|
||||
expect(result.current.themePreference).toBe("light");
|
||||
expect(result.current.theme).toBe("light");
|
||||
|
||||
act(() => {
|
||||
result.current.toggleTheme();
|
||||
result.current.setThemePreference("system");
|
||||
});
|
||||
|
||||
expect(result.current.themePreference).toBe("system");
|
||||
// System resolves to dark (matchMedia returns false for light)
|
||||
expect(result.current.theme).toBe("dark");
|
||||
});
|
||||
|
||||
it("saves theme to localStorage on change", () => {
|
||||
it("saves theme preference to localStorage on change", () => {
|
||||
const { result } = renderHook(() => useTheme());
|
||||
|
||||
act(() => {
|
||||
result.current.toggleTheme();
|
||||
result.current.setThemePreference("light");
|
||||
});
|
||||
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith("theme", "light");
|
||||
|
||||
act(() => {
|
||||
result.current.setThemePreference("system");
|
||||
});
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith("theme", "system");
|
||||
});
|
||||
|
||||
it("sets data-theme attribute on document", () => {
|
||||
const { result } = renderHook(() => useTheme());
|
||||
|
||||
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
|
||||
|
||||
act(() => {
|
||||
result.current.toggleTheme();
|
||||
result.current.setThemePreference("light");
|
||||
});
|
||||
|
||||
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user