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"));
});
});
});