import { beforeEach, describe, expect, it, vi } from "vitest"; import { act, renderHook, waitFor } from "@testing-library/react"; import { fetchUserSettings, updateUserSettings } from "@/lib/api/domains/settings-api"; import { fetchGitHubWorkspaceSettings, updateGitHubWorkspaceSettings, } from "@/lib/api/domains/github-api"; import { __resetSnapshotForTests, useDefaultQueryPresets, type StoredQueryPreset, } from "./use-default-query-presets"; const STORAGE_KEY = "kandev:github-default-queries:v1"; const SYNC_FAILED_KEY = "kandev:github-default-queries:sync-failed:v1"; const WORKSPACE_ID = "ws-1"; const SETTINGS_TIMESTAMP = "2026-01-01T00:00:00Z"; vi.mock("@/lib/api/domains/settings-api", () => ({ fetchUserSettings: vi.fn(), updateUserSettings: vi.fn(), })); vi.mock("@/lib/api/domains/github-api", () => ({ fetchGitHubWorkspaceSettings: vi.fn(), updateGitHubWorkspaceSettings: vi.fn(), })); function makeLocalStorageMock() { const store = new Map(); return { getItem: (key: string) => store.get(key) ?? null, setItem: (key: string, value: string) => store.set(key, value), removeItem: (key: string) => store.delete(key), clear: () => store.clear(), get length() { return store.size; }, key: (index: number) => Array.from(store.keys())[index] ?? null, }; } const localStorageMock = makeLocalStorageMock(); vi.stubGlobal("localStorage", localStorageMock); Object.defineProperty(window, "localStorage", { value: localStorageMock, configurable: true }); const preset: StoredQueryPreset = { value: "mine", label: "Mine", filter: "author:@me is:open", group: "created", }; function workspaceSettings( defaultQueryPresets: unknown = null, ): Awaited> { return { workspace_id: WORKSPACE_ID, repo_scope_mode: "all", repo_scope_orgs: [], repo_scope_repos: [], saved_presets: [], default_query_presets: defaultQueryPresets, created_at: SETTINGS_TIMESTAMP, updated_at: SETTINGS_TIMESTAMP, } as Awaited>; } function resetMocks() { vi.clearAllMocks(); localStorageMock.clear(); __resetSnapshotForTests(); vi.mocked(fetchUserSettings).mockResolvedValue({ settings: { github_default_query_presets: null }, } as Awaited>); vi.mocked(updateUserSettings).mockResolvedValue({ settings: {}, } as Awaited>); vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings()); vi.mocked(updateGitHubWorkspaceSettings).mockResolvedValue( {} as Awaited>, ); } describe("useDefaultQueryPresets legacy sync", () => { beforeEach(resetMocks); it("retries local defaults after a failed backend sync and clears the marker", async () => { const local = { pr: [preset], issue: [] }; localStorageMock.setItem(STORAGE_KEY, JSON.stringify(local)); localStorageMock.setItem(SYNC_FAILED_KEY, "1"); renderHook(() => useDefaultQueryPresets()); await waitFor(() => { expect(updateUserSettings).toHaveBeenCalledWith({ github_default_query_presets: local, }); expect(localStorageMock.getItem(SYNC_FAILED_KEY)).toBeNull(); }); }); it("retries null local defaults after a failed reset sync", async () => { localStorageMock.setItem(SYNC_FAILED_KEY, "1"); vi.mocked(fetchUserSettings).mockResolvedValue({ settings: { github_default_query_presets: { pr: [preset], issue: [] } }, } as Awaited>); renderHook(() => useDefaultQueryPresets()); await waitFor(() => { expect(updateUserSettings).toHaveBeenCalledWith({ github_default_query_presets: null, }); expect(localStorageMock.getItem(SYNC_FAILED_KEY)).toBeNull(); }); }); it("does not overwrite cross-tab local changes with a stale hydration response", async () => { const initial = { pr: [preset], issue: [] }; const crossTab = { pr: [], issue: [preset] }; const server = { pr: [{ ...preset, value: "server", label: "Server" }], issue: [], }; let resolveFetch: (value: Awaited>) => void = () => {}; vi.mocked(fetchUserSettings).mockReturnValue( new Promise((resolve) => { resolveFetch = resolve; }), ); localStorageMock.setItem(STORAGE_KEY, JSON.stringify(initial)); renderHook(() => useDefaultQueryPresets()); await waitFor(() => expect(fetchUserSettings).toHaveBeenCalled()); localStorageMock.setItem(STORAGE_KEY, JSON.stringify(crossTab)); const event = new Event("storage"); Object.defineProperty(event, "key", { value: STORAGE_KEY }); window.dispatchEvent(event); resolveFetch({ settings: { github_default_query_presets: server }, } as Awaited>); await Promise.resolve(); await Promise.resolve(); expect(localStorageMock.getItem(STORAGE_KEY)).toBe(JSON.stringify(crossTab)); expect(updateUserSettings).not.toHaveBeenCalled(); }); }); describe("useDefaultQueryPresets workspace sync", () => { beforeEach(resetMocks); it("migrates local defaults into a fresh workspace", async () => { const local = { pr: [preset], issue: [] }; localStorageMock.setItem(STORAGE_KEY, JSON.stringify(local)); __resetSnapshotForTests(); const { result } = renderHook(() => useDefaultQueryPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.prPresets[0]?.value).toBe("mine")); expect(updateGitHubWorkspaceSettings).toHaveBeenCalledWith({ workspace_id: WORKSPACE_ID, default_query_presets: local, }); }); it("does not migrate local defaults over existing workspace defaults", async () => { const server = { pr: [{ ...preset, value: "server", label: "Server" }], issue: [] }; localStorageMock.setItem(STORAGE_KEY, JSON.stringify({ pr: [preset], issue: [] })); __resetSnapshotForTests(); vi.mocked(fetchGitHubWorkspaceSettings).mockResolvedValue(workspaceSettings(server)); const { result } = renderHook(() => useDefaultQueryPresets(WORKSPACE_ID)); await waitFor(() => expect(result.current.prPresets[0]?.value).toBe("server")); expect(updateGitHubWorkspaceSettings).not.toHaveBeenCalled(); }); it("does not save or reset while workspace defaults are still loading", () => { vi.mocked(fetchGitHubWorkspaceSettings).mockReturnValue(new Promise(() => {})); const { result } = renderHook(() => useDefaultQueryPresets(WORKSPACE_ID)); act(() => { result.current.save({ pr: [preset], issue: [] }); result.current.reset(); }); expect(updateGitHubWorkspaceSettings).not.toHaveBeenCalled(); }); it("does not save or reset after workspace defaults fail to load", async () => { vi.mocked(fetchGitHubWorkspaceSettings).mockRejectedValue(new Error("settings down")); const { result } = renderHook(() => useDefaultQueryPresets(WORKSPACE_ID)); await waitFor(() => expect(fetchGitHubWorkspaceSettings).toHaveBeenCalled()); act(() => { result.current.save({ pr: [preset], issue: [] }); result.current.reset(); }); expect(updateGitHubWorkspaceSettings).not.toHaveBeenCalled(); }); });