diff --git a/src/components/ClaimDrawer/DiagnosesList.test.tsx b/src/components/ClaimDrawer/DiagnosesList.test.tsx new file mode 100644 index 0000000..f9dce1c --- /dev/null +++ b/src/components/ClaimDrawer/DiagnosesList.test.tsx @@ -0,0 +1,188 @@ +// @vitest-environment happy-dom +// Pure prop-driven component; match the act() convention used by the +// other ClaimDrawer test files (ClaimDrawerHeader / ClaimDrawerSkeleton / +// ClaimDrawerError / ValidationPanel / ServiceLinesTable). +(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 } from "vitest"; +import { DiagnosesList } from "./DiagnosesList"; +import type { ClaimDetailDiagnosis } 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 makeDiagnosis( + overrides: Partial = {} +): ClaimDetailDiagnosis { + return { + code: "E11.9", + qualifier: "ABK", + ...overrides, + }; +} + +describe("DiagnosesList", () => { + it("test_section_label_includes_count", () => { + const diagnoses = [ + makeDiagnosis({ code: "E11.9" }), + makeDiagnosis({ code: "I10", qualifier: null }), + makeDiagnosis({ code: "M54.5", qualifier: null }), + ]; + + 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 "Diagnoses"; assert case-insensitively and + // confirm the visual transform via the className sniff below. + const text = (label?.textContent ?? "").toLowerCase(); + expect(text).toContain("diagnoses"); + expect(text).toContain("(3)"); + + // Eyebrow style (uppercase + tracking) โ€” sniff the className. + const cls = label?.className ?? ""; + expect(cls).toContain("uppercase"); + expect(cls).toContain("tracking-[0.18em]"); + + unmount(); + }); + + it("test_empty_state_renders_when_no_diagnoses", () => { + const { container, unmount } = renderIntoContainer( + + ); + + const empty = container.querySelector('[data-testid="diagnoses-empty"]'); + expect(empty).not.toBeNull(); + expect(empty?.textContent ?? "").toContain("No diagnoses"); + + // No list rendered in the empty state. + expect(container.querySelector('[data-testid="diagnoses-list"]')).toBeNull(); + expect( + container.querySelectorAll('[data-testid="diagnosis-item"]').length + ).toBe(0); + + // Section label still present, but with count "(0)". + const label = container.querySelector('[data-testid="section-label"]'); + expect(label?.textContent ?? "").toContain("(0)"); + + unmount(); + }); + + it("test_single_diagnosis_renders_code", () => { + const { container, unmount } = renderIntoContainer( + + ); + + const items = container.querySelectorAll('[data-testid="diagnosis-item"]'); + expect(items.length).toBe(1); + expect(items[0].textContent ?? "").toContain("E11.9"); + + unmount(); + }); + + it("test_qualifier_shown_when_present", () => { + const { container, unmount } = renderIntoContainer( + + ); + + const item = container.querySelector('[data-testid="diagnosis-item"]'); + expect(item).not.toBeNull(); + + const text = item?.textContent ?? ""; + // Both the qualifier and the code should appear together. + expect(text).toContain("ABK"); + expect(text).toContain("E11.9"); + + unmount(); + }); + + it("test_qualifier_absent_does_not_render_garbage", () => { + const { container, unmount } = renderIntoContainer( + + ); + + const item = container.querySelector('[data-testid="diagnosis-item"]'); + expect(item).not.toBeNull(); + + const text = (item?.textContent ?? "").toLowerCase(); + // No stringified null/undefined. + expect(text).not.toContain("undefined"); + expect(text).not.toContain("null"); + // No stray separator that would imply a missing qualifier slot. + expect(text).not.toContain("ยท"); + + unmount(); + }); + + it("test_description_from_inline_dict_when_available", () => { + const { container, unmount } = renderIntoContainer( + + ); + + const item = container.querySelector('[data-testid="diagnosis-item"]'); + expect(item).not.toBeNull(); + + const text = item?.textContent ?? ""; + // E11.9 is in the inline ICD-10 dict: "Type 2 diabetes mellitus + // without complications". Assert a stable substring. + expect(text.toLowerCase()).toContain("type 2 diabetes"); + + unmount(); + }); + + it("test_description_absent_for_unknown_code", () => { + // A clearly-fake code that's not in the inline dict. + const { container, unmount } = renderIntoContainer( + + ); + + const item = container.querySelector('[data-testid="diagnosis-item"]'); + expect(item).not.toBeNull(); + + const text = (item?.textContent ?? "").trim(); + // Only the code should appear โ€” no description placeholder, no + // separator dangling after the code. + expect(text).toContain("ZZZ.99"); + // Should not contain any of the known dict descriptions. + expect(text.toLowerCase()).not.toContain("diabetes"); + expect(text.toLowerCase()).not.toContain("hypertension"); + expect(text.toLowerCase()).not.toContain("asthma"); + + unmount(); + }); +}); \ No newline at end of file diff --git a/src/components/ClaimDrawer/DiagnosesList.tsx b/src/components/ClaimDrawer/DiagnosesList.tsx new file mode 100644 index 0000000..4b4c839 --- /dev/null +++ b/src/components/ClaimDrawer/DiagnosesList.tsx @@ -0,0 +1,78 @@ +import type { ClaimDetail } from "@/types"; + +type DiagnosesListProps = { + diagnoses: ClaimDetail["diagnoses"]; +}; + +// Minimal ICD-10 description lookup. Covers the most common codes +// likely to appear in the test fixtures; the full ICD-10 catalog is +// ~95k entries and out of scope. Codes not in this map render without +// a description (the code + qualifier still display). +const ICD10_DESCRIPTION: Record = { + "E11.9": "Type 2 diabetes mellitus without complications", + "E11.65": "Type 2 diabetes mellitus with hyperglycemia", + "I10": "Essential (primary) hypertension", + "J45.909": "Unspecified asthma, uncomplicated", + "M54.5": "Low back pain", + "R51": "Headache", + "Z00.00": "General adult medical examination", +}; + +function describe(code: string): string | null { + return ICD10_DESCRIPTION[code] ?? null; +} + +/** + * Diagnoses list for the claim detail drawer (SP4). + * + * Inline list of mono codes + (when present) qualifier + (when the + * code is in our small ICD-10 dictionary) a short human description. + * The dictionary is intentionally tiny โ€” the drawer's purpose is to + * show what the claim says, not to be an ICD-10 reference. + */ +export function DiagnosesList({ diagnoses }: DiagnosesListProps) { + return ( +
+

+ Diagnoses ({diagnoses.length}) +

+ + {diagnoses.length === 0 ? ( +

+ No diagnoses +

+ ) : ( +
    + {diagnoses.map((d, i) => { + const desc = describe(d.code); + return ( +
  • + + {d.qualifier ? `${d.qualifier} ${d.code}` : d.code} + + {desc ? ( + + {desc} + + ) : null} +
  • + ); + })} +
+ )} +
+ ); +} \ No newline at end of file