// @vitest-environment happy-dom // React Query isn't used by this component (it receives a resolved // ClaimDetail as a prop), but the surrounding test harness convention // sets IS_REACT_ACT_ENVIRONMENT to suppress noisy act() warnings. // Mirror the convention from ClaimDrawerSkeleton.test.tsx. (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 { beforeEach, describe, expect, it, vi } from "vitest"; import { ClaimDrawerHeader } from "./ClaimDrawerHeader"; import * as apiModule from "@/lib/api"; import * as downloadModule from "@/lib/download"; import type { ClaimDetail } from "@/types"; // Mock the api + download helpers so we can assert the wiring without // hitting the network or the real Blob/anchor machinery. The spies live // at module scope but `beforeEach` resets their call history so the // happy-path test doesn't leak into the error-path test. const serializeSpy = vi.spyOn(apiModule.api, "serializeClaim837"); const downloadSpy = vi.spyOn(downloadModule, "downloadTextFile"); 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(); }, }; } /** * Minimal valid ClaimDetail fixture — every required key present so the * component typechecks. Mirrors the SAMPLE_DETAIL in * useClaimDetail.test.ts; only the fields the header actually reads * (id, state, stateLabel, billedAmount) vary between tests. */ function makeDetail(overrides: Partial = {}): ClaimDetail { return { id: "CLM-1", batchId: "B-1", state: "submitted", stateLabel: "Submitted", billedAmount: 123.45, patientName: "Jane Doe", providerNpi: "1234567890", providerName: "Acme Clinic", payerName: "Medicaid", payerId: "MEDCO", submissionDate: "2026-01-15T00:00:00Z", serviceDateFrom: "2026-01-10", serviceDateTo: "2026-01-10", parsedAt: "2026-01-15T00:00:01Z", diagnoses: [{ code: "E11.9", qualifier: "ABK" }], serviceLines: [ { lineNumber: 1, procedureQualifier: "HC", procedureCode: "99213", modifiers: [], charge: 100.0, units: 1, unitType: "UN", serviceDate: "2026-01-10", }, ], parties: { billingProvider: { name: "Acme Clinic", npi: "1234567890", taxId: "12-3456789", address: { line1: "123 Main St", line2: null, city: "Denver", state: "CO", zip: "80202", }, }, subscriber: { firstName: "Jane", lastName: "Doe", memberId: "M-1", dob: null, gender: "F", }, payer: { name: "Medicaid", id: "MEDCO" }, }, validation: { passed: true, errors: [], warnings: [] }, rawSegments: [["ISA*00*"]], matchedRemittance: null, stateHistory: [ { kind: "claim_submitted", ts: "2026-01-15T00:00:00Z", batchId: "B-1", remittanceId: null, }, ], ...overrides, }; } function renderHeader( claim: Partial, onClose: () => void = vi.fn(), onError: (message: string) => void = vi.fn() ): { container: HTMLDivElement; unmount: () => void; onClose: () => void; onError: (m: string) => void } { const element = ( ); const { container, unmount } = renderIntoContainer(element); return { container, unmount, onClose, onError }; } describe("ClaimDrawerHeader", () => { beforeEach(() => { serializeSpy.mockReset(); downloadSpy.mockReset(); }); it("test_renders_claim_id_label_and_value", () => { const { container, unmount } = renderHeader({ id: "CLM-42" }); // The eyebrow label "Claim" is rendered as uppercase text. const text = (container.textContent ?? "").toLowerCase(); expect(text).toContain("claim"); // The claim id sits in a node tagged with data-testid="header-id". const idEl = container.querySelector('[data-testid="header-id"]'); expect(idEl).not.toBeNull(); expect(idEl?.textContent).toBe("CLM-42"); unmount(); }); it("test_renders_state_badge_with_label", () => { const { container, unmount } = renderHeader({ state: "submitted", stateLabel: "Submitted", }); const badge = container.querySelector('[data-testid="header-state"]'); expect(badge).not.toBeNull(); expect(badge?.textContent).toBe("Submitted"); unmount(); }); it("test_state_badge_variant_matches_state", () => { // Each state must surface the right Badge variant. We sniff the // rendered className for the per-variant token (defined in // badge.tsx's cva config) so the assertion is independent of the // surrounding wrapper classes. const cases: Array<{ state: string; stateLabel: string; expectedToken: string; }> = [ // secondary → bg-muted text-muted-foreground { state: "submitted", stateLabel: "Submitted", expectedToken: "bg-muted text-muted-foreground" }, // default → bg-primary/15 text-primary { state: "accepted", stateLabel: "Accepted", expectedToken: "bg-primary/15" }, // destructive → bg-destructive/15 { state: "denied", stateLabel: "Denied", expectedToken: "bg-destructive/15" }, // success → bg-[hsl(var(--success)/0.15)] (covered by both matched and paid) { state: "matched", stateLabel: "Matched", expectedToken: "bg-[hsl(var(--success)/0.15)]" }, { state: "paid", stateLabel: "Paid", expectedToken: "bg-[hsl(var(--success)/0.15)]" }, // warning → bg-[hsl(var(--warning)/0.15)] { state: "pending", stateLabel: "Pending", expectedToken: "bg-[hsl(var(--warning)/0.15)]" }, ]; for (const c of cases) { const { container, unmount } = renderHeader({ state: c.state, stateLabel: c.stateLabel, }); const badge = container.querySelector('[data-testid="header-state"]'); expect(badge, `badge missing for state=${c.state}`).not.toBeNull(); const cls = badge?.className ?? ""; expect( cls.includes(c.expectedToken), `expected badge className to include "${c.expectedToken}" for state=${c.state}, got "${cls}"` ).toBe(true); unmount(); } }); it("test_renders_total_amount_formatted", () => { // 123.45 → "$123.45" (the project's fmt.usdPrecise helper, which // uses minimumFractionDigits:2 / maximumFractionDigits:2). const { container, unmount } = renderHeader({ billedAmount: 123.45 }); const amountEl = container.querySelector('[data-testid="header-amount"]'); expect(amountEl).not.toBeNull(); expect(amountEl?.textContent).toBe("$123.45"); unmount(); }); it("test_close_button_calls_onClose", () => { const onClose = vi.fn(); const { container, unmount } = renderHeader({}, onClose); const closeBtn = container.querySelector( '[data-testid="header-close"]' ) as HTMLButtonElement | null; expect(closeBtn).not.toBeNull(); expect(onClose).not.toHaveBeenCalled(); act(() => { closeBtn?.click(); }); expect(onClose).toHaveBeenCalledTimes(1); unmount(); }); it("test_uses_modern_palette_surface", () => { // The drawer header anchors itself on the light surface palette // token (matches ClaimDrawerSkeleton / ClaimDrawerError). The root //
element is tagged with data-testid="claim-drawer-header" // so we can sniff its className without coupling to the badge // or close-button wrappers. const { container, unmount } = renderHeader({}); const root = container.querySelector('[data-testid="claim-drawer-header"]'); expect(root).not.toBeNull(); const cls = root?.className ?? ""; expect(cls).toContain("bg-[color:var(--m-surface)]"); unmount(); }); it("test_download_button_fetches_and_saves_file_for_claim_id", async () => { // Happy path: click → api.serializeClaim837(claim.id) → downloadTextFile // with the returned text + filename. We don't actually need to wait for // the download side-effect — we just verify the wiring (right ids, // right payload, right mime). serializeSpy.mockResolvedValueOnce({ text: "ISA*00*~IEA*0*1~", filename: "claim-CLM-1.x12", }); const { container, unmount } = renderHeader({ id: "CLM-1" }); const btn = container.querySelector( '[data-testid="header-download-837"]' ) as HTMLButtonElement | null; expect(btn).not.toBeNull(); await act(async () => { btn?.click(); // Flush the microtask queue so the awaited handler resolves. await Promise.resolve(); }); expect(serializeSpy).toHaveBeenCalledTimes(1); expect(serializeSpy).toHaveBeenCalledWith("CLM-1"); expect(downloadSpy).toHaveBeenCalledTimes(1); expect(downloadSpy).toHaveBeenCalledWith( "claim-CLM-1.x12", "text/x12", "ISA*00*~IEA*0*1~" ); unmount(); }); it("test_download_button_routes_api_error_through_onError", async () => { // When serializeClaim837 throws, the header swallows the error and // hands the message to the optional onError callback. The download // helper must NOT be called on the failure path — that's the // difference between "the file was corrupted in the DB" and "we just // gave the user a broken empty file". serializeSpy.mockRejectedValueOnce(new Error("404 Not found")); const onError = vi.fn(); const { container, unmount } = renderHeader({ id: "CLM-1" }, vi.fn(), onError); const btn = container.querySelector( '[data-testid="header-download-837"]' ) as HTMLButtonElement | null; await act(async () => { btn?.click(); await Promise.resolve(); }); expect(onError).toHaveBeenCalledTimes(1); expect(onError).toHaveBeenCalledWith("404 Not found"); expect(downloadSpy).not.toHaveBeenCalled(); unmount(); }); });