// @vitest-environment happy-dom // Mirror the IS_REACT_ACT_ENVIRONMENT setup from useClaimDetail.test.ts so // React doesn't log act() warnings about the createRoot render/unmount. (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; import React, { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useRemitDetail } from "./useRemitDetail"; import { api, ApiError } from "@/lib/api"; import type { Remittance } from "@/types"; // Module-level mock: vitest hoists `vi.mock` above imports. We mock both // `api.getRemittance` (the only method the hook calls) AND `ApiError` // (the class used in the retry predicate) so that `instanceof ApiError` // in the hook and `new ApiError(404, ...)` in the test resolve to the // same class at runtime. vi.mock("@/lib/api", () => { class ApiError extends Error { constructor(public status: number, message: string) { super(message); } } return { api: { getRemittance: vi.fn(), }, ApiError, }; }); /** * Minimal renderHook shim — same pattern as `useClaimDetail.test.ts`. * The project doesn't ship `@testing-library/react`, so we wire a Probe * component into a real `createRoot` and read its return value through * a shared `result` object. */ function renderHook( useCallback: () => TResult ): { result: { current: TResult | undefined }; unmount: () => void; } { const result: { current: TResult | undefined } = { current: undefined }; const container = document.createElement("div"); document.body.appendChild(container); const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); function Probe() { result.current = useCallback(); return null; } const root: Root = createRoot(container); act(() => { root.render( React.createElement(QueryClientProvider, { client: qc }, React.createElement(Probe)) ); }); return { result, unmount: () => { act(() => root.unmount()); container.remove(); }, }; } /** Poll the result until `predicate` is true or we time out. */ async function waitForState( predicate: () => boolean, timeoutMs = 1000 ): Promise { const start = Date.now(); while (!predicate()) { if (Date.now() - start > timeoutMs) { throw new Error(`waitForState: predicate did not hold within ${timeoutMs}ms`); } await act(async () => { await Promise.resolve(); }); } } // Minimal valid Remittance — every required key present on the base // `Remittance` interface. Mirrors the shape used by the page-level // tests in `Remittances.test.tsx` so the type-checks line up with the // rest of the codebase. const SAMPLE_DETAIL: Remittance = { id: "REM-1", claimId: "CLM-1", payerName: "Medicaid", paidAmount: 100.0, adjustmentAmount: 20.0, receivedDate: "2026-01-15T00:00:00Z", checkNumber: "EFT-12345", status: "received", }; describe("useRemitDetail", () => { beforeEach(() => { vi.clearAllMocks(); }); it("returns the empty drawer state and does not fetch when id is null", () => { // The drawer is closed → no network call, no loading, no error. // The hook must short-circuit BEFORE the queryFn is ever invoked. const { result, unmount } = renderHook(() => useRemitDetail(null)); expect(result.current).toBeDefined(); expect(result.current?.data).toBeNull(); expect(result.current?.isLoading).toBe(false); expect(result.current?.isError).toBe(false); expect(result.current?.error).toBeNull(); expect(typeof result.current?.refetch).toBe("function"); expect(api.getRemittance).not.toHaveBeenCalled(); unmount(); }); it("fetches the remit and returns the parsed body for a valid id", async () => { (api.getRemittance as unknown as ReturnType).mockResolvedValue( SAMPLE_DETAIL ); const { result, unmount } = renderHook(() => useRemitDetail("REM-1")); await waitForState( () => result.current?.data !== null && result.current?.data !== undefined && result.current?.isLoading === false ); expect(result.current?.data).toEqual(SAMPLE_DETAIL); expect(result.current?.isError).toBe(false); expect(result.current?.error).toBeNull(); expect(api.getRemittance).toHaveBeenCalledTimes(1); expect(api.getRemittance).toHaveBeenCalledWith("REM-1"); unmount(); }); it("surfaces a 404 ApiError as isError=true with the original error object", async () => { // The hook's retry predicate must give up immediately on 404 — the // drawer needs to render the "remit doesn't exist" state rather than // masking it with three back-to-back retries. (api.getRemittance as unknown as ReturnType).mockRejectedValue( new ApiError(404, "Remittance ghost not found") ); const { result, unmount } = renderHook(() => useRemitDetail("ghost")); await waitForState(() => result.current?.isError === true); expect(result.current?.data).toBeNull(); expect(result.current?.isLoading).toBe(false); expect(result.current?.isError).toBe(true); expect(result.current?.error).toBeInstanceOf(ApiError); expect((result.current?.error as ApiError).status).toBe(404); // 404s must not retry — exactly one fetch attempt. expect(api.getRemittance).toHaveBeenCalledTimes(1); unmount(); }); });