import { beforeEach, describe, expect, it } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { ThemeProvider } from "next-themes"; import { DeviceMemorySwitch } from "./device-memory-switch"; import { DEVICE_MEMORY_OPT_OUT_KEY, installDeviceMemorySuppression, } from "@/lib/device-memory"; import { memoryStorage, useStorage } from "@/test/memory-storage"; // What this file can and cannot settle, said once. The cross-document half of // the theme story - the pre-hydration script's first paint, and a second tab's // next-themes rewrite being dropped - needs a real browser: jsdom runs neither, // so an assertion about either would pass against any implementation at all. // What *is* real here is next-themes itself, because `vitest.setup.ts` stubs // `matchMedia`, so the provider resolves `system` and applies its class exactly // as it does in a browser. That is the assertion below. const theSwitch = () => screen.getByRole("switch", { name: /^don’t remember display preferences$/i, }); beforeEach(() => { useStorage(memoryStorage()); installDeviceMemorySuppression(); document.documentElement.className = ""; }); describe("DeviceMemorySwitch", () => { it("starts from what storage says", async () => { window.localStorage.setItem(DEVICE_MEMORY_OPT_OUT_KEY, "1"); render(); await waitFor(() => expect(theSwitch()).toBeChecked()); }); it("records the objection, and takes it back", async () => { const user = userEvent.setup(); render(); await user.click(theSwitch()); expect( window.localStorage.getItem(DEVICE_MEMORY_OPT_OUT_KEY), ).not.toBeNull(); expect(theSwitch()).toBeChecked(); await user.click(theSwitch()); expect(window.localStorage.getItem(DEVICE_MEMORY_OPT_OUT_KEY)).toBeNull(); expect(theSwitch()).not.toBeChecked(); }); // The mirror, in the only form a unit test can see it: `/privacy` §10.3 and // the `/settings` device card render this same component, so the property // that matters is that two instances share one state rather than each // carrying their own. it("renders one state across every surface showing it", async () => { const user = userEvent.setup(); render( <>
, ); const [onPrivacy, onSettings] = screen.getAllByRole("switch"); expect(onPrivacy).not.toBeChecked(); expect(onSettings).not.toBeChecked(); await user.click(onSettings); await waitFor(() => expect(onPrivacy).toBeChecked()); expect(onSettings).toBeChecked(); }); // The fifth owner decision, and the half of it jsdom can hold: a same-document // `removeItem` fires no `storage` event and next-themes has no other change // detection, so without the control's own `setTheme` this tab would keep its // dark theme until reload while every other open tab reverted at once. The // suppressed write inside that call is what keeps the key from coming back. it("settles the initiating tab's theme, and stores nothing doing it", async () => { const user = userEvent.setup(); window.localStorage.setItem("theme", "dark"); render( , ); await waitFor(() => expect(document.documentElement.classList.contains("dark")).toBe(true), ); await user.click(theSwitch()); // `matchMedia` is stubbed to report no dark preference, so `system` // resolves light - which is the point: the tab settles on the default // rather than keeping a preference nothing is storing any more. await waitFor(() => expect(document.documentElement.classList.contains("light")).toBe(true), ); expect(document.documentElement.classList.contains("dark")).toBe(false); expect(window.localStorage.getItem("theme")).toBeNull(); }); it("does not throw where the browser refuses storage", async () => { const user = userEvent.setup(); useStorage(undefined); render(); await user.click(theSwitch()); expect(theSwitch()).not.toBeChecked(); }); });