// @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). (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 { PartiesGrid } from "./PartiesGrid"; import { DrillStackProvider } from "@/components/drill/DrillStackProvider"; import type { ClaimDetailAddress, ClaimDetailParties, } from "@/types"; // Mock the api module so PayerPeekContent's usePayerSummary call // doesn't make a real network request. The spy is set per-test in // beforeEach. vi.mock("@/hooks/usePayerSummary", () => ({ usePayerSummary: vi.fn(), })); import { usePayerSummary } from "@/hooks/usePayerSummary"; function renderIntoContainer(element: React.ReactElement): { container: HTMLDivElement; unmount: () => void; } { const container = document.createElement("div"); document.body.appendChild(container); const root: Root = createRoot(container); // SP21 Phase 5 Task 5.8: PartiesGrid now uses useDrillStack() to // open payer peeks. Wrap every render in a DrillStackProvider so // the hook has a context to read from. act(() => { root.render({element}); }); return { container, unmount: () => { act(() => root.unmount()); container.remove(); }, }; } function makeAddress( overrides: Partial = {} ): ClaimDetailAddress { return { line1: "100 Main St", line2: null, city: "Springfield", state: "IL", zip: "62701", ...overrides, }; } function makeParties( overrides: Partial = {} ): ClaimDetailParties { return { billingProvider: { name: "Acme Medical Group", npi: "1234567890", taxId: "12-3456789", address: makeAddress(), }, subscriber: { firstName: "Jane", lastName: "Doe", memberId: "MEM12345", dob: "1980-05-15", gender: "F", }, payer: { name: "Aetna", id: "PAYER01", }, ...overrides, }; } describe("PartiesGrid", () => { it("test_section_label_is_parties", () => { 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 "Parties"; assert case-insensitively and // confirm the visual transform via the className sniff below. const text = (label?.textContent ?? "").toLowerCase(); expect(text).toContain("parties"); // 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_three_cards_rendered_for_each_party", () => { const { container, unmount } = renderIntoContainer( ); const billing = container.querySelector('[data-testid="party-billing-provider"]'); const subscriber = container.querySelector('[data-testid="party-subscriber"]'); const payer = container.querySelector('[data-testid="party-payer"]'); expect(billing).not.toBeNull(); expect(subscriber).not.toBeNull(); expect(payer).not.toBeNull(); unmount(); }); it("test_billing_provider_card_shows_name_npi_taxId_and_address", () => { const { container, unmount } = renderIntoContainer( ); const card = container.querySelector( '[data-testid="party-billing-provider"]' ); expect(card).not.toBeNull(); const text = card?.textContent ?? ""; expect(text).toContain("Acme Medical Group"); expect(text).toContain("1234567890"); expect(text).toContain("12-3456789"); // Address components. expect(text).toContain("100 Main St"); expect(text).toContain("Springfield"); expect(text).toContain("IL"); expect(text).toContain("62701"); unmount(); }); it("test_subscriber_card_shows_first_last_memberId_and_gender", () => { const { container, unmount } = renderIntoContainer( ); const card = container.querySelector('[data-testid="party-subscriber"]'); expect(card).not.toBeNull(); const text = card?.textContent ?? ""; expect(text).toContain("Jane"); expect(text).toContain("Doe"); expect(text).toContain("MEM12345"); expect(text).toContain("F"); // DOB is also present in this fixture. expect(text).toContain("1980"); unmount(); }); it("test_subscriber_card_omits_dob_when_null", () => { const { container, unmount } = renderIntoContainer( ); const card = container.querySelector('[data-testid="party-subscriber"]'); expect(card).not.toBeNull(); const text = (card?.textContent ?? "").toLowerCase(); // Still renders the identity pieces. expect(text).toContain("john"); expect(text).toContain("smith"); expect(text).toContain("mem99999"); expect(text).toContain("m"); // No "undefined" or "null" stringification for the absent DOB. expect(text).not.toContain("undefined"); expect(text).not.toContain("null"); unmount(); }); it("test_payer_card_shows_name_and_id", () => { const { container, unmount } = renderIntoContainer( ); const card = container.querySelector('[data-testid="party-payer"]'); expect(card).not.toBeNull(); const text = card?.textContent ?? ""; expect(text).toContain("Aetna"); expect(text).toContain("PAYER01"); unmount(); }); it("test_billing_provider_address_line2_rendered_when_present", () => { const { container, unmount } = renderIntoContainer( ); const card = container.querySelector( '[data-testid="party-billing-provider"]' ); const text = card?.textContent ?? ""; expect(text).toContain("200 Oak Ave"); expect(text).toContain("Suite 300"); expect(text).toContain("Portland"); expect(text).toContain("OR"); expect(text).toContain("97201"); unmount(); }); it("test_empty_address_object_does_not_render_garbage", () => { // The backend can return `{}` for a missing address (per the // ClaimDetailBillingProvider type's `Record` branch). // The component must not stringify an empty object. const { container, unmount } = renderIntoContainer( ); const card = container.querySelector( '[data-testid="party-billing-provider"]' ); const text = (card?.textContent ?? "").toLowerCase(); expect(text).toContain("addrless clinic"); expect(text).toContain("2222222222"); expect(text).not.toContain("[object object]"); expect(text).not.toContain("undefined"); expect(text).not.toContain("null"); unmount(); }); it("test_responsive_grid_uses_3_columns_on_desktop", () => { const { container, unmount } = renderIntoContainer( ); // The section follows the established `flex flex-col gap-3 px-6 py-4` // pattern; the responsive grid is the inner wrapper that holds the // three party cards. Sniff its className. const grid = container.querySelector('[data-testid="parties-grid-inner"]'); expect(grid).not.toBeNull(); const cls = grid?.className ?? ""; expect(cls).toContain("grid"); expect(cls).toContain("grid-cols-1"); expect(cls).toContain("md:grid-cols-3"); expect(cls).toContain("gap-4"); unmount(); }); it("SP21 Task 5.8: payer name is drillable (renders as a button)", () => { // The payer card's name now wraps in a button with the // `drillable` affordance + `data-testid="party-payer-name-drill"`. // The other two cards (billing-provider, subscriber) keep the // name as a plain div. const { container, unmount } = renderIntoContainer( ); const payerNameBtn = container.querySelector( '[data-testid="party-payer-name-drill"]', ); expect(payerNameBtn).not.toBeNull(); expect(payerNameBtn?.textContent).toContain("Aetna"); // The billing-provider and subscriber names stay plain divs. expect( container.querySelector( '[data-testid="party-billing-provider-name-drill"]', ), ).toBeNull(); expect( container.querySelector( '[data-testid="party-subscriber-name-drill"]', ), ).toBeNull(); unmount(); }); it("SP21 Task 5.8: clicking the payer name opens the PeekModal", async () => { // Mock the payer summary fetch so PayerPeekContent resolves // without a real network call. The peek modal renders // regardless of fetch state (loading skeleton → content). (usePayerSummary as unknown as ReturnType).mockReturnValue({ data: null, isLoading: true, }); const { container, unmount } = renderIntoContainer( ); const payerNameBtn = container.querySelector( '[data-testid="party-payer-name-drill"]', ) as HTMLButtonElement | null; expect(payerNameBtn).not.toBeNull(); await act(async () => { payerNameBtn!.click(); await Promise.resolve(); }); // The PeekModal is a Radix Dialog that portals into document.body. // Look for the [role="dialog"] (modal) with our eyebrow "Payer". const dialogs = document.body.querySelectorAll('[role="dialog"]'); // One dialog is the modal itself (the peek). PartiesGrid doesn't // open a drawer, so we expect exactly one. expect(dialogs.length).toBeGreaterThanOrEqual(1); const peekDialog = Array.from(dialogs).find((d) => d.textContent?.includes("Payer"), ); expect(peekDialog).not.toBeNull(); expect(peekDialog?.textContent).toContain("Aetna"); unmount(); }); it("SP21 Task 5.8: peek uses the X12 payer id, not the human name", () => { // The PayerPeekContent is given the X12 payer id from // `payer.id`. Verify the click flow passes the right id by // checking usePayerSummary was called with "PAYER01". const mockUsePayerSummary = vi.fn().mockReturnValue({ data: null, isLoading: true, }); (usePayerSummary as unknown as ReturnType).mockImplementation( mockUsePayerSummary, ); const { container, unmount } = renderIntoContainer( ); // No peek yet → no usePayerSummary call. expect(mockUsePayerSummary).not.toHaveBeenCalled(); const payerNameBtn = container.querySelector( '[data-testid="party-payer-name-drill"]', ) as HTMLButtonElement | null; act(() => { payerNameBtn!.click(); }); // After the click, PayerPeekContent mounts and calls // usePayerSummary("PAYER01") — NOT the human name "Aetna". expect(mockUsePayerSummary).toHaveBeenCalledWith("PAYER01"); unmount(); }); }); beforeEach(() => { // Reset the mock between tests so per-test implementations stick. (usePayerSummary as unknown as ReturnType).mockReturnValue({ data: null, isLoading: true, }); });