import { describe, expect, test } from "vitest"; import { act, renderHook, waitFor } from "@testing-library/react"; import type { ReactNode } from "react"; import { ThemeProvider } from "../../../contexts/theme/ThemeProvider.tsx"; import { AuthProvider } from "../../../contexts/auth/AuthProvider.tsx"; import { useTheme } from "../../../hooks/theme/useTheme.ts"; import { DEFAULT_SETTINGS } from "../../../../shared/settings.ts"; import { calls, route } from "../../fetch.ts"; import { makeSession, storeSession, wire } from "../../fixtures.ts"; describe("ThemeProvider", () => { const wrapper = ({ children }: { children: ReactNode }) => ( {children} ); const root = document.documentElement; test("smooth and auto by default, with no attributes", () => { const { result } = renderHook(useTheme, { wrapper }); expect(result.current.style).toBe("smooth"); expect(result.current.colorScheme).toBe("auto"); expect(root.hasAttribute("data-style")).toBe(false); expect(root.hasAttribute("data-color-scheme")).toBe(false); }); test("applies and persists a choice", () => { const { result } = renderHook(useTheme, { wrapper }); act(() => result.current.setStyle("brutalist")); act(() => result.current.setColorScheme("dark")); expect(root.getAttribute("data-style")).toBe("brutalist"); expect(root.getAttribute("data-color-scheme")).toBe("dark"); expect(localStorage.getItem("style")).toBe("brutalist"); expect(localStorage.getItem("color-scheme")).toBe("dark"); act(() => result.current.setStyle("smooth")); act(() => result.current.setColorScheme("auto")); expect(root.hasAttribute("data-style")).toBe(false); expect(root.hasAttribute("data-color-scheme")).toBe(false); }); test("restores the stored theme, migrating the legacy name", () => { localStorage.setItem("style", "nyt"); localStorage.setItem("color-scheme", "light"); const { result } = renderHook(useTheme, { wrapper }); expect(result.current.style).toBe("nyt"); expect(result.current.colorScheme).toBe("light"); localStorage.setItem("style", "default"); expect(renderHook(useTheme, { wrapper }).result.current.style).toBe( "smooth", ); }); describe("signed in", () => { const authWrapper = ({ children }: { children: ReactNode }) => ( {children} ); const signIn = (settings: typeof DEFAULT_SETTINGS) => { const session = makeSession(); storeSession(session); route("GET /api/users/me", () => wire(session.user)); route("GET /api/users/me/settings", () => settings); route( "PATCH /api/users/me/settings", (req) => ({ ...settings, ...(req.body as object) }), ); }; test("the account's choice wins over this browser's, and is kept here", async () => { localStorage.setItem("style", "nyt"); signIn({ ...DEFAULT_SETTINGS, style: "kanagawa", colorScheme: "dark" }); const { result } = renderHook(useTheme, { wrapper: authWrapper }); await waitFor(() => expect(result.current.style).toBe("kanagawa")); expect(result.current.colorScheme).toBe("dark"); expect(root.getAttribute("data-style")).toBe("kanagawa"); expect(localStorage.getItem("style")).toBe("kanagawa"); expect(calls("PATCH /api/users/me/settings")).toHaveLength(0); act(() => result.current.setStyle("melange")); await waitFor(() => expect(calls("PATCH /api/users/me/settings")[0]?.body).toEqual({ style: "melange", }) ); expect(result.current.style).toBe("melange"); act(() => result.current.setStyle("smooth")); act(() => result.current.setColorScheme("auto")); }); test("an account without a choice takes this browser's, once", async () => { localStorage.setItem("style", "brutalist"); signIn({ ...DEFAULT_SETTINGS }); const { result } = renderHook(useTheme, { wrapper: authWrapper }); await waitFor(() => expect(calls("PATCH /api/users/me/settings")).toHaveLength(1) ); expect(calls("PATCH /api/users/me/settings")[0].body).toEqual({ style: "brutalist", }); expect(result.current.style).toBe("brutalist"); act(() => result.current.setStyle("smooth")); }); }); });