feat(frontend): DiagnosesList with qualifier + code formatting
This commit is contained in:
@@ -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> = {}
|
||||||
|
): 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(
|
||||||
|
<DiagnosesList diagnoses={diagnoses} />
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DiagnosesList diagnoses={[]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DiagnosesList
|
||||||
|
diagnoses={[makeDiagnosis({ code: "E11.9", qualifier: null })]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DiagnosesList
|
||||||
|
diagnoses={[makeDiagnosis({ code: "E11.9", qualifier: "ABK" })]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DiagnosesList
|
||||||
|
diagnoses={[makeDiagnosis({ code: "E11.9", qualifier: null })]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DiagnosesList
|
||||||
|
diagnoses={[makeDiagnosis({ code: "E11.9", qualifier: null })]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DiagnosesList
|
||||||
|
diagnoses={[
|
||||||
|
makeDiagnosis({ code: "ZZZ.99", qualifier: null }),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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<string, string> = {
|
||||||
|
"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 (
|
||||||
|
<section className="flex flex-col gap-3 px-6 py-4">
|
||||||
|
<h3
|
||||||
|
data-testid="section-label"
|
||||||
|
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
|
||||||
|
>
|
||||||
|
Diagnoses ({diagnoses.length})
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{diagnoses.length === 0 ? (
|
||||||
|
<p
|
||||||
|
data-testid="diagnoses-empty"
|
||||||
|
className="text-sm text-[color:var(--m-ink-tertiary)]"
|
||||||
|
>
|
||||||
|
No diagnoses
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul className="flex flex-col gap-2" data-testid="diagnoses-list">
|
||||||
|
{diagnoses.map((d, i) => {
|
||||||
|
const desc = describe(d.code);
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`${d.code}-${i}`}
|
||||||
|
data-testid="diagnosis-item"
|
||||||
|
className="flex items-baseline gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="font-mono text-[color:var(--m-ink-primary)]"
|
||||||
|
style={{ fontFamily: "var(--m-font-mono)" }}
|
||||||
|
>
|
||||||
|
{d.qualifier ? `${d.qualifier} ${d.code}` : d.code}
|
||||||
|
</span>
|
||||||
|
{desc ? (
|
||||||
|
<span className="text-[color:var(--m-ink-secondary)]">
|
||||||
|
{desc}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user