// @vitest-environment happy-dom // Pure prop-driven component; match the act() convention used by the // other ClaimDrawer test files (ClaimDrawerHeader / ClaimDrawerSkeleton / // ClaimDrawerError / ValidationPanel / ServiceLinesTable / DiagnosesList // / PartiesGrid / RawSegmentsPanel). (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 } from "vitest"; import { MatchedRemitCard } from "./MatchedRemitCard"; import type { ClaimDetailMatchedRemittance } from "@/types"; function renderIntoContainer(element: React.ReactElement): { container: HTMLDivElement; unmount: () => void; } { const container = document.createElement("div"); document.body.appendChild(container); const root: Root = createRoot(container); act(() => { root.render(element); }); return { container, unmount: () => { act(() => root.unmount()); container.remove(); }, }; } function makeMatchedRemittance( overrides: Partial = {} ): ClaimDetailMatchedRemittance { return { id: "REM-2026-0001", totalPaid: 142.5, status: "received", receivedAt: "2026-06-12T10:00:00Z", ...overrides, }; } describe("MatchedRemitCard", () => { it("test_null_prop_returns_no_dom", () => { const { container, unmount } = renderIntoContainer( ); // No DOM at all — the component returns null before rendering. expect(container.innerHTML).toBe(""); unmount(); }); it("test_populated_prop_renders_section_label", () => { const { container, unmount } = renderIntoContainer( ); const label = container.querySelector('[data-testid="section-label"]'); expect(label).not.toBeNull(); // `uppercase` is applied via Tailwind CSS, so textContent gives the // source-case string "Matched Remittance"; assert case-insensitively // and confirm the visual transform via the className sniff below. const text = (label?.textContent ?? "").toLowerCase(); expect(text).toContain("matched remittance"); // Eyebrow style (uppercase + tracking) — sniff the className. // The shared `.eyebrow` class in index.css consolidates the // eyebrow treatment; tests assert it lands on the section label. const cls = label?.className ?? ""; expect(cls).toContain("eyebrow"); unmount(); }); it("test_renders_id_totalPaid_status_and_receivedAt", () => { const { container, unmount } = renderIntoContainer( ); // Remit ID rendered in mono. const idEl = container.querySelector('[data-testid="matched-remit-id"]'); expect(idEl).not.toBeNull(); expect(idEl?.textContent ?? "").toContain("REM-2026-9999"); // totalPaid formatted via fmt.usdPrecise → "$250.00". const totalPaidEl = container.querySelector( '[data-testid="matched-remit-total-paid"]' ); expect(totalPaidEl).not.toBeNull(); expect(totalPaidEl?.textContent ?? "").toContain("$250.00"); // Status badge. const statusEl = container.querySelector( '[data-testid="matched-remit-status"]' ); expect(statusEl).not.toBeNull(); expect(statusEl?.textContent ?? "").toContain("reconciled"); // receivedAt formatted via fmt.date — the date contains the year at // minimum, and `new Date("ISO Z")` is UTC-stable. const receivedEl = container.querySelector( '[data-testid="matched-remit-received"]' ); expect(receivedEl).not.toBeNull(); expect(receivedEl?.textContent ?? "").toContain("2026"); unmount(); }); it("test_status_badge_uses_danger_variant_for_rejected_status", () => { // The remittance status field is an unconstrained string (per // `ClaimDetailMatchedRemittance`), so unknown statuses should fall // back to the muted variant rather than throwing. const { container, unmount } = renderIntoContainer( ); const statusEl = container.querySelector( '[data-testid="matched-remit-status"]' ); expect(statusEl).not.toBeNull(); // Muted variant (no color) — the badge still renders without error. expect(statusEl?.textContent ?? "").toContain("weird"); unmount(); }); it("test_full_match_badge_renders_when_matchedLines_equals_totalLines", () => { const { container, unmount } = renderIntoContainer( ); const badge = container.querySelector( '[data-testid="matched-remit-line-counts"]' ); expect(badge).not.toBeNull(); expect(badge?.textContent ?? "").toMatch(/4\/4 matched/); expect(badge?.getAttribute("data-all-matched")).toBe("true"); unmount(); }); it("test_partial_match_badge_renders_unmatched_count", () => { const { container, unmount } = renderIntoContainer( ); const badge = container.querySelector( '[data-testid="matched-remit-line-counts"]' ); expect(badge).not.toBeNull(); expect(badge?.textContent ?? "").toMatch(/3\/4 matched/); expect(badge?.textContent ?? "").toMatch(/1 unmatched/); expect(badge?.getAttribute("data-all-matched")).toBe("false"); unmount(); }); it("test_badge_omitted_when_line_counts_undefined", () => { // Backward compatibility: pre-SP7 claim-detail responses don't // include matchedLines / totalLines. The card should still render // without the badge. const { container, unmount } = renderIntoContainer( ); expect( container.querySelector('[data-testid="matched-remit-line-counts"]') ).toBeNull(); unmount(); }); it("test_view_remittance_link_sets_window_location_href", () => { // Spy on the `href` setter so the click doesn't actually navigate // away in the test runner, and so we can assert what was assigned. const setHrefSpy = vi.spyOn(window.location, "href", "set"); setHrefSpy.mockImplementation(() => { // No-op — happy-dom would otherwise try to update the URL. }); try { const { container, unmount } = renderIntoContainer( ); const link = container.querySelector( '[data-testid="view-remittance-link"]' ); expect(link).not.toBeNull(); // The button should advertise the destination via the visible label. expect(link?.textContent ?? "").toContain("View remittance"); act(() => { link?.dispatchEvent( new MouseEvent("click", { bubbles: true, cancelable: true }) ); }); // The click handler should have assigned to window.location.href // exactly once, with the remittance-detail deep link. expect(setHrefSpy).toHaveBeenCalledTimes(1); expect(setHrefSpy).toHaveBeenCalledWith("/remittances?id=REM-XYZ"); unmount(); } finally { setHrefSpy.mockRestore(); } }); });