// @vitest-environment happy-dom // Tell React this is an `act`-aware test environment so react-query's // internal state updates flush through without noisy console warnings. (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 { Remittances } from "./Remittances"; import { api } from "@/lib/api"; import { useTailStore } from "@/store/tail-store"; // Module-level mock: vitest hoists `vi.mock` calls above imports. We only // stub the method this page touches via the `useRemittances` hook. // `isConfigured: true` is critical โ€” without it, `useRemittances` falls // back to the empty zustand store instead of calling our mocked API. vi.mock("@/lib/api", () => ({ api: { isConfigured: true, listRemittances: vi.fn(), getRemittance: vi.fn(), }, })); // Mock the live-tail hook so the page renders the pill in the settled // `live` state without driving the real streamTail parser. The hook's // own test file (`useTailStream.test.ts`) covers the lifecycle in depth. vi.mock("@/hooks/useTailStream", () => ({ useTailStream: vi.fn(() => ({ status: "live", lastEventAt: null, error: null, forceReconnect: vi.fn(), })), })); /** * Minimal `render` helper using react-dom/client + act(). Mirrors the * `renderIntoContainer` helper in `Reconciliation.test.tsx` โ€” see that * file's comment for the rationale (project doesn't ship a DOM test * library yet, and adding one just for these tests would inflate the * dev-deps tree). Returns the rendered container so tests can assert * against the live DOM via `document.body.textContent`. */ function renderIntoContainer(element: React.ReactElement): { container: HTMLDivElement; unmount: () => void; } { const container = document.createElement("div"); document.body.appendChild(container); const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const root: Root = createRoot(container); act(() => { root.render( React.createElement(QueryClientProvider, { client: qc }, element) ); }); return { container, unmount: () => { act(() => root.unmount()); container.remove(); }, }; } /** * Flush microtasks + react state updates until the predicate holds or we * time out. react-query's fetch resolves asynchronously, so we have to * await a tick before the rendered DOM reflects the mocked data. */ async function waitForText( text: string, timeoutMs = 2000 ): Promise { const start = Date.now(); while (!document.body.textContent?.includes(text)) { if (Date.now() - start > timeoutMs) { throw new Error(`waitForText: "${text}" did not appear within ${timeoutMs}ms`); } await act(async () => { await Promise.resolve(); }); } } describe("Remittances", () => { beforeEach(() => { vi.clearAllMocks(); // Singleton tail-store: clear the remittances slice between tests // so a tail-arrival case (if added later) doesn't see rows from a // previous test. useTailStore.getState().reset("remittances"); }); it("renders a CAS adjustment label inside the expanded detail row", async () => { ( api.listRemittances as unknown as ReturnType ).mockResolvedValue({ items: [ { id: "PCN-1", claimId: "CLM-1", payerName: "Colorado Medicaid", paidAmount: 100, adjustmentAmount: 60, receivedDate: "2026-06-19T12:00:00Z", checkNumber: "", status: "received", adjustments: [ { group: "CO", reason: "45", label: "Charge exceeds fee schedule/maximum allowable", amount: 50, quantity: null, }, { group: "PR", reason: "1", label: "Deductible amount", amount: 10, quantity: 1, }, ], }, ], total: 1, returned: 1, has_more: false, }); const { unmount } = renderIntoContainer(React.createElement(Remittances)); await waitForText("PCN-1"); // The chevron + "Adjustments" header should not yet be visible because // the row hasn't been expanded yet. expect(document.body.textContent).not.toContain("Adjustments (2)"); // Expand the row by clicking on the remit ID cell. We click the parent // row by selecting the cell containing "PCN-1" and bubbling up. const cell = Array.from(document.querySelectorAll("td")).find( (td) => td.textContent === "PCN-1" ); const row = cell?.closest("tr"); expect(row).toBeTruthy(); await act(async () => { (row as HTMLTableRowElement).click(); }); await waitForText("Adjustments (2)"); // Both CAS labels must surface (not the raw codes alone). expect(document.body.textContent).toContain( "Charge exceeds fee schedule/maximum allowable" ); expect(document.body.textContent).toContain("Deductible amount"); // Group/reason pills show the CARC code alongside. expect(document.body.textContent).toContain("CO-45"); expect(document.body.textContent).toContain("PR-1"); unmount(); }); // ------------------------------------------------------------------- // Live-tail status pill (sub-project 5, Phase 5 Task 23, optional // page-level test). Cheap smoke check: the toolbar renders the pill // with the `live` label whenever the mocked stream is settled. The // hook's lifecycle is covered exhaustively in its own test file. // ------------------------------------------------------------------- it("test_status_pill_shows_live_in_toolbar", async () => { ( api.listRemittances as unknown as ReturnType ).mockResolvedValue({ items: [], total: 0, returned: 0, has_more: false, }); const { unmount } = renderIntoContainer(React.createElement(Remittances)); // Wait for the empty-state to mount โ€” once the page body is rendered, // the toolbar (and thus the pill) is in the DOM. await waitForText("Remittances ยท awaiting first 835"); const pill = document.body.querySelector( '[data-testid="tail-status-pill"]', ); expect(pill).not.toBeNull(); expect(pill?.textContent).toContain("Live"); unmount(); }); });