import { describe, expect, test, vi } from "vitest"; import { act, renderHook, waitFor } from "@testing-library/react"; import { useFetchState } from "../../../hooks/shared/useFetchState.ts"; import { deferred } from "../../fetch.ts"; describe("useFetchState", () => { test("loads once enabled, and only once per key", async () => { const load = vi.fn(() => Promise.resolve(1)); const { result, rerender } = renderHook( ({ enabled }) => useFetchState("a", load, { enabled }), { initialProps: { enabled: false } }, ); expect(load).not.toHaveBeenCalled(); expect(result.current.state).toEqual({ status: "loading" }); rerender({ enabled: true }); await waitFor(() => expect(result.current.state).toEqual({ status: "loaded", data: 1 }) ); rerender({ enabled: false }); rerender({ enabled: true }); expect(load).toHaveBeenCalledTimes(1); expect(result.current.state).toEqual({ status: "loaded", data: 1 }); }); test("turning it off doesn't stop a load under way", async () => { const d = deferred(); const { result, rerender } = renderHook( ({ enabled }) => useFetchState("a", () => d.promise, { enabled }), { initialProps: { enabled: true } }, ); rerender({ enabled: false }); act(() => d.resolve(2)); await waitFor(() => expect(result.current.state).toEqual({ status: "loaded", data: 2 }) ); }); test("a new key starts over and aborts the last key's load", async () => { const signals: AbortSignal[] = []; const load = vi.fn((signal: AbortSignal) => { signals.push(signal); return signals.length === 1 ? new Promise(() => {}) : Promise.resolve("b"); }); const { result, rerender } = renderHook( ({ key }) => useFetchState(key, load), { initialProps: { key: "a" } }, ); rerender({ key: "b" }); expect(signals[0].aborted).toBe(true); await waitFor(() => expect(result.current.state).toEqual({ status: "loaded", data: "b" }) ); }); test("an undefined key loads nothing", () => { const load = vi.fn(() => Promise.resolve(1)); renderHook(() => useFetchState(undefined, load)); expect(load).not.toHaveBeenCalled(); }); test("a failed load shows its message", async () => { const { result } = renderHook(() => useFetchState("a", () => Promise.reject(new Error("Nope"))) ); await waitFor(() => expect(result.current.state).toEqual({ status: "error", error: "Nope" }) ); }); test("setData edits the loaded data, and nothing before", async () => { const d = deferred(); const { result } = renderHook(() => useFetchState("a", () => d.promise)); act(() => result.current.setData((xs) => [...xs, 9])); expect(result.current.state).toEqual({ status: "loading" }); act(() => d.resolve([1])); await waitFor(() => expect(result.current.state.status).toBe("loaded")); act(() => result.current.setData((xs) => [...xs, 2])); expect(result.current.state).toEqual({ status: "loaded", data: [1, 2] }); }); test("an initial value starts loaded and loads nothing, for its key only", async () => { const load = vi.fn(() => Promise.resolve(2)); const { result, rerender } = renderHook( ({ key, initial }) => useFetchState(key, load, { initial }), { initialProps: { key: "a", initial: 1 as number | undefined } }, ); expect(result.current.state).toEqual({ status: "loaded", data: 1 }); expect(load).not.toHaveBeenCalled(); rerender({ key: "b", initial: undefined }); await waitFor(() => expect(result.current.state).toEqual({ status: "loaded", data: 2 }) ); }); });