9bca4b608a
Backend:
- New POST /api/batches/{id}/export-837: regenerate X12 837 files
for a list of claim_ids into a ZIP using HCPF file naming standards,
with a unique interchange/group control number per export. Wire
the clearhouse Loop 1000A (NM1*41 + PER) and per-payer receiver
(NM1*40) blocks so the serializer no longer falls back to
CYCLONE / RECEIVER placeholders.
- /api/parse-837 and /api/parse-835 now surface the server-side
batch_id in both JSON and NDJSON response shapes so the frontend
can hit batch-scoped endpoints without an extra listBatches
round-trip.
- Filename helpers and the 837 serializer updated to match the new
HCPF envelope; tests cover batch export, parse batch_id, and the
serializer's control-number uniqueness guarantee.
Frontend:
- New shared components: ClaimCard, ClaimCard837, DominantKpiCard,
EditorialNote, ExportBar, TickerTape, and a charts/ set
(BarChart, HBarChart, SegmentedBar, AgingBars).
- New useBatchExport hook driving ExportBar's download flow against
the new endpoint.
- ClaimDrawer, Lane, and Layout migrated from raw CSS-variable
colors to Tailwind theme tokens (bg-card, text-foreground,
border/60, etc.) for consistency with the rest of the instrument
chrome; the active tab indicator gains a subtle accent glow.
- Upload, Inbox, Batches, BatchDiff, Reconciliation, and Acks pages
reworked to compose the new shared components and consume the new
batch-scoped API surface (notably ExportBar wired into Batches).
Tooling / Docs:
- Add audit-uiux.mjs and a docs/goodclaim.x12 sample fixture.
- Update ClaimDrawer testids and add coverage for the new
components and the useBatchExport hook.
Rolls up into the v0.2.0 release tag.
163 lines
5.0 KiB
TypeScript
163 lines
5.0 KiB
TypeScript
// @vitest-environment happy-dom
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
|
|
true;
|
|
|
|
import React, { act } from "react";
|
|
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
import { MemoryRouter } from "react-router-dom";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { useAppStore } from "@/store";
|
|
import { ClaimCard837 } from "./ClaimCard837";
|
|
import type { ClaimOutput } from "@/types";
|
|
|
|
// Tiny fixture — just enough to exercise the card's UI surface.
|
|
const CLAIM: ClaimOutput = {
|
|
claim_id: "CLM-TEST",
|
|
subscriber: { first_name: "Jane", last_name: "Doe", member_id: "MEM-1" },
|
|
payer: { name: "Test Payer", id: "P1" },
|
|
billing_provider: { npi: "1234567890" },
|
|
claim: {
|
|
total_charge: 100,
|
|
place_of_service: "11",
|
|
frequency_code: "1",
|
|
prior_auth: null,
|
|
},
|
|
service_lines: [
|
|
{
|
|
line_number: 1,
|
|
procedure: { qualifier: "HC", code: "99213", modifiers: [] },
|
|
charge: "100",
|
|
units: "1",
|
|
unit_type: "UN",
|
|
service_date: "2026-06-01",
|
|
},
|
|
],
|
|
diagnoses: [{ qualifier: "ABK", code: "J20.9" }],
|
|
validation: { passed: true, errors: [], warnings: [] },
|
|
};
|
|
|
|
function renderCard(
|
|
props: Partial<React.ComponentProps<typeof ClaimCard837>> = {},
|
|
): {
|
|
container: HTMLDivElement;
|
|
unmount: () => void;
|
|
} {
|
|
const container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
const root: Root = createRoot(container);
|
|
const onToggleSelect = props.onToggleSelect ?? (() => {});
|
|
act(() => {
|
|
root.render(
|
|
React.createElement(
|
|
MemoryRouter,
|
|
null,
|
|
React.createElement(ClaimCard837, {
|
|
claim: CLAIM,
|
|
selected: false,
|
|
onToggleSelect,
|
|
...props,
|
|
}),
|
|
),
|
|
);
|
|
});
|
|
return {
|
|
container,
|
|
unmount: () => {
|
|
act(() => root.unmount());
|
|
container.remove();
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("ClaimCard837 — checkbox + select interaction", () => {
|
|
beforeEach(() => {
|
|
useAppStore.setState({ parsedBatches: [] });
|
|
});
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("renders a checkbox with checked={selected}", () => {
|
|
const { container, unmount } = renderCard({ selected: true });
|
|
const cb = container.querySelector<HTMLInputElement>(
|
|
'input[type="checkbox"]',
|
|
);
|
|
expect(cb).not.toBeNull();
|
|
expect(cb!.checked).toBe(true);
|
|
unmount();
|
|
});
|
|
|
|
it("renders an unchecked checkbox when selected={false}", () => {
|
|
const { container, unmount } = renderCard({ selected: false });
|
|
const cb = container.querySelector<HTMLInputElement>(
|
|
'input[type="checkbox"]',
|
|
);
|
|
expect(cb!.checked).toBe(false);
|
|
unmount();
|
|
});
|
|
|
|
it("clicking the checkbox fires onToggleSelect with the claim_id", () => {
|
|
const onToggleSelect = vi.fn();
|
|
const { container, unmount } = renderCard({ onToggleSelect });
|
|
const cb = container.querySelector<HTMLInputElement>(
|
|
'input[type="checkbox"]',
|
|
)!;
|
|
act(() => {
|
|
cb.click();
|
|
});
|
|
expect(onToggleSelect).toHaveBeenCalledWith("CLM-TEST");
|
|
unmount();
|
|
});
|
|
|
|
it("clicking the checkbox does NOT toggle the card's expand state", () => {
|
|
// Regression guard: the card body is a <button> that toggles expand
|
|
// on click. The new checkbox must not bubble its click into that
|
|
// button (which would also fire onToggleSelect, double-counting).
|
|
// We assert this by checking the expanded content (the service lines
|
|
// table) is NOT in the DOM after a checkbox click.
|
|
const { container, unmount } = renderCard();
|
|
const cb = container.querySelector<HTMLInputElement>(
|
|
'input[type="checkbox"]',
|
|
)!;
|
|
act(() => {
|
|
cb.click();
|
|
});
|
|
// The service lines table only renders when the card is expanded.
|
|
const table = container.querySelector("table");
|
|
expect(table).toBeNull();
|
|
unmount();
|
|
});
|
|
|
|
it("clicking the card body still toggles expand (existing behavior preserved)", () => {
|
|
const { container, unmount } = renderCard();
|
|
// The card body is a <button aria-expanded="false">. Clicking it
|
|
// should reveal the expanded details (service lines table).
|
|
const expandButton = container.querySelector<HTMLButtonElement>(
|
|
'button[aria-expanded]',
|
|
);
|
|
expect(expandButton).not.toBeNull();
|
|
act(() => {
|
|
expandButton!.click();
|
|
});
|
|
// After expand, the service lines table is in the DOM.
|
|
const table = container.querySelector("table");
|
|
expect(table).not.toBeNull();
|
|
unmount();
|
|
});
|
|
|
|
it("checkbox has an accessible label that names the claim", () => {
|
|
const { container, unmount } = renderCard();
|
|
const cb = container.querySelector<HTMLInputElement>(
|
|
'input[type="checkbox"]',
|
|
);
|
|
// aria-label or wrapping <label> — at minimum the input must be
|
|
// findable by an accessible name. The simplest assertion is that an
|
|
// aria-label exists on the input itself.
|
|
expect(
|
|
cb!.getAttribute("aria-label") ||
|
|
cb!.closest("label")?.textContent,
|
|
).toContain("CLM-TEST");
|
|
unmount();
|
|
});
|
|
});
|