// @vitest-environment jsdom import { act, createElement, StrictMode } from "react"; import { createRoot, type Root } from "react-dom/client"; import { IsRestoringProvider, QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AppTabsProvider, useOptionalAppTabs } from "@/lib/app-tabs-context"; import { createHomeTab, type AppTab } from "@/lib/app-tabs"; import { appTabsStorageKey } from "@/lib/app-tabs-storage"; import { invalidateLocalSnapshotWrites } from "@/lib/local-snapshots"; import { navigationPreparationPending, navigationPreparationPriority, subscribeNavigationPreparation } from "@/lib/navigation-preparation-priority"; import { scheduleSearchIndexArm } from "@/lib/use-search-index"; const auth = vi.hoisted(() => ({ user: { id: "owner" } as { id: string } | null, sdkOwner: "owner" })); const viewport = vi.hoisted(() => ({ mobile: false as boolean | undefined })); const router = vi.hoisted(() => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() })); const visit = vi.hoisted(() => vi.fn()); const select = vi.hoisted(() => vi.fn()); vi.mock("@/lib/auth-context", () => ({ useAuth: () => auth })); vi.mock("@/lib/use-mobile-layout", () => ({ useMobileLayout: () => viewport.mobile })); vi.mock("@/lib/supabase", () => ({ getSupabase: () => ({ auth: { getSession: async () => ({ data: { session: { user: { id: auth.sdkOwner } } }, error: null }), } }) })); vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key })); vi.mock("next/navigation", () => ({ useRouter: () => router })); vi.mock("@/components/app-tab-route-sync", () => ({ AppTabRouteSync: () => null })); vi.mock("@/lib/use-pr-background", () => ({ PrBackgroundSync: () => null })); vi.mock("@/lib/pr-tab-preparation", () => ({ createPrTabPreparation: () => ({ visit, select, dispose: vi.fn() }) })); vi.mock("@/lib/prefetch-tab-destination", () => ({ prefetchAppTabDestination: vi.fn(), isPreparedAppTabDestination: () => false, })); interface PendingRead { signal: AbortSignal; finish: (value: unknown, status?: number) => void; } interface PendingOpen { owner: string; finish: (value: unknown, status?: number) => void; } let root: Root; let container: HTMLDivElement; let client: QueryClient; let value: ReturnType; let reads: PendingRead[]; let opens: PendingOpen[]; let writes: string[]; let seals: unknown[]; let holdSeals: boolean; let sealReads: Array<{ signal: AbortSignal; selection: { id: string; href: string }; finish: () => void }>; const firstId = "10000000-0000-0000-0000-000000000001"; const secondId = "10000000-0000-0000-0000-000000000002"; const rows = (owner = "owner"): AppTab[] => [createHomeTab(owner, firstId), { ...createHomeTab(owner, secondId, 1), href: "/all" }]; const snapshot = (owner: string) => ({ format: "minddy-local-v1", owner, expiresAt: Date.now() + 60_000, ciphertext: `sealed-${owner}` }); function Probe() { value = useOptionalAppTabs(); return createElement("span", null, value?.activeId ?? "Pending"); } async function render(restoring = false, strict = false) { const tree = createElement(QueryClientProvider, { client }, createElement(IsRestoringProvider, { value: restoring }, createElement(AppTabsProvider, { children: createElement(Probe) }))); await act(() => root.render(strict ? createElement(StrictMode, null, tree) : tree)); } const settle = async () => { await act(async () => { await new Promise((resolve) => setTimeout(resolve, 5)); }); }; // Settles until an expected outcome lands. A single fixed settle raced the // response-to-render chain on loaded CI runners. const settleUntil = (check: () => void) => vi.waitFor(async () => { await settle(); check(); }); beforeEach(() => { vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); auth.user = { id: "owner" }; auth.sdkOwner = "owner"; viewport.mobile = false; vi.clearAllMocks(); visit.mockReset(); select.mockReset(); invalidateLocalSnapshotWrites(); sessionStorage.clear(); window.history.replaceState(null, "", "/home"); reads = []; opens = []; writes = []; seals = []; sealReads = []; holdSeals = false; value = null; client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity } } }); vi.stubGlobal("fetch", vi.fn((url: string, init?: RequestInit) => { if (url === "/api/me/app-tabs" && !init?.method) return new Promise((resolve, reject) => { const signal = init!.signal as AbortSignal; signal.addEventListener("abort", () => reject(signal.reason), { once: true }); reads.push({ signal, finish: (body, status = 200) => resolve(Response.json(body, { status })) }); }); if (url === "/api/me/local-snapshots") { const body = JSON.parse(String(init?.body)); if (body.operation === "open") return new Promise((resolve) => { opens.push({ owner: body.snapshot.owner, finish: (opened, status = 200) => resolve(Response.json({ value: opened }, { status })) }); }); seals.push(body.value); if (holdSeals) return new Promise((resolve) => { const owner = auth.sdkOwner; const ordinal = sealReads.length; // Intentionally ignore abort at the transport layer. The real helper // must fence late ACKs without relying on a cooperative server. sealReads.push({ signal: init!.signal as AbortSignal, selection: body.value, finish: () => resolve(Response.json({ snapshot: { ...snapshot(owner), ciphertext: `sealed-selection-${ordinal}` } })) }); }); return Promise.resolve(Response.json({ snapshot: snapshot(auth.sdkOwner) })); } writes.push(`${init?.method ?? "GET"} ${url}`); return Promise.resolve(Response.json({ code: "unexpected_write" }, { status: 500 })); })); container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); }); afterEach(async () => { await act(() => root.unmount()); invalidateLocalSnapshotWrites(); for (const pending of reads) pending.finish([]); for (const pending of opens) pending.finish(undefined); for (const pending of sealReads) pending.finish(); client.clear(); container.remove(); vi.restoreAllMocks(); vi.unstubAllGlobals(); }); describe("mounted restored-tab startup admission", () => { it("reserves optional admission while the ordinary canonical query is restoring", async () => { await render(true); expect(reads).toHaveLength(0); expect(navigationPreparationPending(client)).toBe(true); let runQuietPeriod: (() => void) | undefined; const arm = vi.fn(); const stop = scheduleSearchIndexArm({ setTimeout(callback) { runQuietPeriod = callback; return 1; }, clearTimeout() {}, }, arm, { pending: () => navigationPreparationPending(client), subscribe: (changed) => subscribeNavigationPreparation(client, changed) }); runQuietPeriod!(); expect(arm).not.toHaveBeenCalled(); await render(false); expect(reads).toHaveLength(1); expect(navigationPreparationPending(client)).toBe(true); reads[0].finish(rows()); await settleUntil(() => expect(value?.activeId).toBe(firstId)); expect(visit).toHaveBeenCalledOnce(); expect(navigationPreparationPending(client)).toBe(false); expect(arm).toHaveBeenCalledOnce(); expect(writes).toEqual([]); stop(); }); it("holds the canonical list until the existing sealed window selection resolves", async () => { sessionStorage.setItem(appTabsStorageKey("owner"), JSON.stringify(snapshot("owner"))); await render(false); reads[0].finish(rows()); await settleUntil(() => expect(opens).toHaveLength(1)); expect(value?.activeId).toBeNull(); expect(navigationPreparationPending(client)).toBe(true); expect(visit).not.toHaveBeenCalled(); opens[0].finish({ id: firstId, href: "/home" }); await settleUntil(() => expect(value?.activeId).toBe(firstId)); expect(visit).toHaveBeenCalledOnce(); expect(navigationPreparationPending(client)).toBe(false); expect(writes).toEqual([]); }); it("hands off to the finite worker without admitting optional work between owners", async () => { const worker = navigationPreparationPriority(client), transitions: boolean[] = []; visit.mockImplementation(() => worker.acquire()); await render(false); const stop = subscribeNavigationPreparation(client, () => transitions.push(navigationPreparationPending(client))); reads[0].finish(rows()); await settleUntil(() => expect(value?.activeId).toBe(firstId)); expect(navigationPreparationPending(client)).toBe(true); expect(transitions).not.toContain(false); worker.release(); expect(navigationPreparationPending(client)).toBe(false); stop(); }); it("publishes an activation's destination at once and prepares the rest after it paints", async () => { await render(false); reads[0].finish(rows()); await settleUntil(() => expect(visit).toHaveBeenCalledOnce()); expect(select).not.toHaveBeenCalled(); await act(async () => { await value!.session.activate(secondId); }); expect(value?.activeId).toBe(secondId); expect(visit).toHaveBeenCalledOnce(); expect(select).toHaveBeenCalledExactlyOnceWith("/all"); await vi.waitFor(() => expect(visit).toHaveBeenCalledTimes(2)); expect(visit).toHaveBeenLastCalledWith("/all", ["/home", "/all"]); // Switches within one frame share one full pass for the newest destination. await act(async () => { await value!.session.activate(firstId); await value!.session.activate(secondId); }); expect(select.mock.calls.slice(1)).toEqual([["/home"], ["/all"]]); await vi.waitFor(() => expect(visit).toHaveBeenCalledTimes(3)); await act(async () => { await new Promise((resolve) => setTimeout(resolve, 50)); }); expect(visit).toHaveBeenCalledTimes(3); expect(visit).toHaveBeenLastCalledWith("/all", ["/home", "/all"]); expect(navigationPreparationPending(client)).toBe(false); }); it("releases optional admission after the ordinary canonical query exhausts its retries", async () => { await render(false); expect(navigationPreparationPending(client)).toBe(true); reads[0].finish({ code: "database" }, 500); await settleUntil(() => expect(value?.loadError).toBe(true)); expect(navigationPreparationPending(client)).toBe(false); expect(visit).not.toHaveBeenCalled(); expect(writes).toEqual([]); }); it("releases admission when initial creation fails instead of remaining pending forever", async () => { await render(false); reads[0].finish([]); await settleUntil(() => expect(writes).toEqual(["POST /api/me/app-tabs"])); expect(value?.activeId).toBeNull(); expect(navigationPreparationPending(client)).toBe(false); expect(value?.error).toBe("unexpected_write"); }); it("keeps the startup reservation through Strict Mode replay and releases on mobile retirement", async () => { const transitions: boolean[] = []; const stop = subscribeNavigationPreparation(client, () => transitions.push(navigationPreparationPending(client))); await render(true, true); await settle(); expect(navigationPreparationPending(client)).toBe(true); expect(transitions).toEqual([true]); viewport.mobile = true; await render(true, true); await settle(); expect(navigationPreparationPending(client)).toBe(false); expect(reads).toHaveLength(0); stop(); }); it("releases the exact old account/client while a replacement still resolves", async () => { await render(false); const oldClient = client; expect(navigationPreparationPending(oldClient)).toBe(true); auth.user = { id: "replacement" }; auth.sdkOwner = "replacement"; client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity } } }); await render(true); await settle(); expect(navigationPreparationPending(oldClient)).toBe(false); expect(navigationPreparationPending(client)).toBe(true); reads[0].finish(rows("owner")); await settle(); expect(navigationPreparationPending(client)).toBe(true); expect(visit).not.toHaveBeenCalled(); oldClient.clear(); }); });