// @vitest-environment jsdom // // The persistence/hydration wiring of useSortState (the pure hydrateSort/healSort logic it leans on is // covered in columnSort.test.ts). storage is mocked with an in-memory map so the hook can be exercised // without a logged-in account (getAccountRaw/setAccountRaw are account-keyed in the real module). import { describe, it, expect, beforeEach, vi } from "vitest"; import { renderHook, act } from "@testing-library/react"; // `ctl.account` models the tab's pinned account: getAccountRaw returns null while it's null (mirrors the // real accountKey → no key → null), so a fresh-tab mount reads defaults until the account is pinned. const { store, ctl } = vi.hoisted(() => ({ store: new Map(), ctl: { account: 1 as number | null }, })); vi.mock("./storage", () => ({ getAccountRaw: (k: string) => (ctl.account == null ? null : (store.get(k) ?? null)), setAccountRaw: (k: string, v: string) => { if (ctl.account != null) store.set(k, v); }, })); vi.mock("./activeAccount", () => ({ useActiveAccountId: () => ctl.account })); import { useSortState } from "./useSortState"; describe("useSortState", () => { beforeEach(() => { store.clear(); ctl.account = 1; }); it("hydrates to the fallback when storage is empty", () => { const { result } = renderHook(() => useSortState("k")); expect(result.current[0]).toBeNull(); const withFallback = renderHook(() => useSortState("k2", [], { key: "date", dir: "desc" })); expect(withFallback.result.current[0]).toEqual({ key: "date", dir: "desc" }); }); it("hydrates a stored SortState on mount", () => { store.set("k", JSON.stringify({ key: "views", dir: "desc" })); const { result } = renderHook(() => useSortState("k")); expect(result.current[0]).toEqual({ key: "views", dir: "desc" }); }); it("persists on set, and a fresh mount reads it back (write → reload → hydrate)", () => { const first = renderHook(() => useSortState("k")); act(() => first.result.current[1]({ key: "name", dir: "asc" })); expect(first.result.current[0]).toEqual({ key: "name", dir: "asc" }); expect(store.get("k")).toBe(JSON.stringify({ key: "name", dir: "asc" })); const second = renderHook(() => useSortState("k")); expect(second.result.current[0]).toEqual({ key: "name", dir: "asc" }); }); it("heals a stored key that the supplied options no longer offer", () => { store.set("k", JSON.stringify({ key: "gone", dir: "asc" })); const opts = [{ key: "name", label: "Name" }]; const { result } = renderHook(() => useSortState("k", opts, { key: "name", dir: "asc" })); expect(result.current[0]).toEqual({ key: "name", dir: "asc" }); }); it("keeps a stored key un-healed when no options are supplied (caller heals for display)", () => { store.set("k", JSON.stringify({ key: "anything", dir: "desc" })); const { result } = renderHook(() => useSortState("k")); expect(result.current[0]).toEqual({ key: "anything", dir: "desc" }); }); // The account-pin re-hydrate (via useAccountRehydrate) integrated into useSortState. it("re-hydrates the stored sort once the account is pinned after mount", () => { ctl.account = null; // fresh tab: no account at mount store.set("k", JSON.stringify({ key: "views", dir: "desc" })); const { result, rerender } = renderHook(() => useSortState("k")); expect(result.current[0]).toBeNull(); // mount read with no account → default act(() => { ctl.account = 1; // pins the account rerender(); }); expect(result.current[0]).toEqual({ key: "views", dir: "desc" }); }); it("does NOT clobber a pre-pin user edit when the account is then pinned (markDirty guard)", () => { ctl.account = null; store.set("k", JSON.stringify({ key: "views", dir: "desc" })); // the stored value const { result, rerender } = renderHook(() => useSortState("k")); act(() => result.current[1]({ key: "name", dir: "asc" })); // user edits before the pin act(() => { ctl.account = 1; rerender(); }); expect(result.current[0]).toEqual({ key: "name", dir: "asc" }); // the edit survives, not "views" }); });