import { renderHook, waitFor } from "@testing-library/react"; import { type ReactNode } from "react"; import { Provider as UrqlProvider, type Client, type CombinedError, type TypedDocumentNode, } from "urql"; import { describe, it, expect } from "vitest"; import type { CursorConnection } from "@/lib/fetch-all-pages"; import { useAllConnectionNodes } from "./use-all-connection-nodes"; interface Node { id: string; } interface Result { conn: CursorConnection; } // Extends Record to satisfy the hook's variables constraint. interface Variables extends Record { id: string; first?: number | null; after?: string | null; } const QUERY = {} as TypedDocumentNode; const select = (data: Result) => data.conn; /** * Fake urql client: page 0 (no cursor) returns two nodes with a next page; page * 1 (cursor "c1") returns one node and ends. `behavior: "error"` fails with a * non-network (non-retryable) error so fetchAllPages rejects immediately. */ function makeClient(behavior: "ok" | "error"): Client { return { query: (_query: unknown, vars: Variables) => ({ toPromise: () => { if (behavior === "error") { return Promise.resolve({ error: { message: "boom", graphQLErrors: [], networkError: undefined, } as unknown as CombinedError, }); } if (vars.after == null) { return Promise.resolve({ data: { conn: { pageInfo: { hasNextPage: true, endCursor: "c1" }, edges: [{ node: { id: "1" } }, { node: { id: "2" } }], }, }, }); } return Promise.resolve({ data: { conn: { pageInfo: { hasNextPage: false, endCursor: null }, edges: [{ node: { id: "3" } }], }, }, }); }, }), } as unknown as Client; } // Module-level wrapper reading the per-test client from a mutable binding. let currentClient: Client; function Wrapper({ children }: { children: ReactNode }) { return {children}; } describe("useAllConnectionNodes", () => { it("stays null while loading, then returns every node paged to completion", async () => { currentClient = makeClient("ok"); const { result } = renderHook( () => useAllConnectionNodes(QUERY, { id: "x" }, select, 2), { wrapper: Wrapper }, ); // Loading: null until the full set has loaded (never a phantom []). expect(result.current.nodes).toBeNull(); await waitFor(() => expect(result.current.nodes).not.toBeNull()); expect(result.current.nodes?.map((n) => n.id)).toEqual(["1", "2", "3"]); expect(result.current.error).toBeUndefined(); }); it("keeps nodes null and surfaces the error on failure", async () => { currentClient = makeClient("error"); const { result } = renderHook( () => useAllConnectionNodes(QUERY, { id: "x" }, select, 2), { wrapper: Wrapper }, ); await waitFor(() => expect(result.current.error).toBeDefined()); // A failed load must not look like a successful empty result. expect(result.current.nodes).toBeNull(); }); });