// @vitest-environment happy-dom import { describe, expect, it, vi } from "vitest"; import { fireEvent, render } from "@testing-library/react"; import { RecentBatchesWidget } from "./RecentBatchesWidget"; import type { BatchSummary } from "@/lib/api"; function makeBatch(over: Partial = {}): BatchSummary { return { id: "b-1", kind: "837p", inputFilename: "2026-07-02-morning.edi", parsedAt: "2026-07-02T15:00:00Z", claimCount: 5, claimIds: ["C-1", "C-2"], acceptedCount: 5, rejectedCount: 0, pendingCount: 0, billedTotal: 425.5, topRejectionReason: null, hasProblem: false, ...over, }; } describe("RecentBatchesWidget", () => { it("renders the empty-state copy when batches is empty", () => { const { container } = render( , ); expect(container.textContent).toContain("No batches yet."); // No interactive rows when there's nothing to show. expect(container.querySelectorAll('[role="button"]').length).toBe(0); }); it("renders one row per batch with status icon, billed total, accepted/total count, top rejection reason", () => { const batches: BatchSummary[] = [ makeBatch({ id: "b-clean", inputFilename: "clean.edi", acceptedCount: 4, rejectedCount: 0, pendingCount: 1, billedTotal: 425.5, hasProblem: false, topRejectionReason: null, }), makeBatch({ id: "b-bad", inputFilename: "rejected-batch.edi", acceptedCount: 1, rejectedCount: 3, pendingCount: 1, billedTotal: 890.25, hasProblem: true, topRejectionReason: "999 AK5 R: envelope reject", }), ]; const onRowClick = vi.fn(); const { container } = render( , ); // Both filenames present. expect(container.textContent).toContain("clean.edi"); expect(container.textContent).toContain("rejected-batch.edi"); // Billed total rendered for the 837p rows via fmt.usd (whole // dollars per the formatter's maximumFractionDigits: 0 setting // — the Dashboard's KPI tiles use the same convention). expect(container.textContent).toContain("$426"); expect(container.textContent).toContain("$890"); // accepted/total counts (4/5 accepted on the clean row, 1/5 on the bad). expect(container.textContent).toContain("4/5 accepted"); expect(container.textContent).toContain("1/5 accepted"); // Top rejection reason appears on the bad batch's row. const rejSpans = container.querySelectorAll( '[data-testid="recent-batch-rejection"]', ); expect(rejSpans.length).toBe(1); expect(rejSpans[0].textContent).toBe("999 AK5 R: envelope reject"); // The rejection span carries the destructive-color tint. const rejColor = (rejSpans[0] as HTMLElement).style.color; expect(rejColor).toMatch(/destructive|229|72|77/i); // Click handler fires with the batch id. const cleanRow = container.querySelector( '[data-testid="recent-batch-row-b-clean"]', ) as HTMLElement; expect(cleanRow).toBeTruthy(); fireEvent.click(cleanRow); expect(onRowClick).toHaveBeenCalledWith("b-clean"); // Enter key on the same row also fires onRowClick (a11y contract). onRowClick.mockClear(); fireEvent.keyDown(cleanRow, { key: "Enter" }); expect(onRowClick).toHaveBeenCalledWith("b-clean"); }); it("renders 835 rows with payments count instead of a dollar figure", () => { // ERAs (835) don't carry a billed total — the Remittance table // has total_paid but no batch-level total_charge aggregate. The // widget shows the payment count and the "payments" label, not $. const batches: BatchSummary[] = [ makeBatch({ id: "b-era", kind: "835", inputFilename: "remit.835", claimCount: 12, acceptedCount: 0, rejectedCount: 0, pendingCount: 0, billedTotal: 0, hasProblem: false, topRejectionReason: null, }), ]; const { container } = render( , ); // Payments count visible. expect(container.textContent).toContain("12"); // Label "payments" present. expect(container.textContent).toContain("payments"); // No dollar figure for the ERA row — no "$" anywhere on the row. const row = container.querySelector( '[data-testid="recent-batch-row-b-era"]', ) as HTMLElement; expect(row.textContent).not.toContain("$"); // And the billed span is absent for ERA rows. expect(container.querySelectorAll('[data-testid="recent-batch-billed"]').length).toBe(0); }); });