Files
cyclone/src/components/ClaimCard837.test.tsx
T
Tyler 9bca4b608a feat(release): v0.2.0 — batch 837 export, ClaimCard, theme tokens
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.
2026-06-22 11:01:58 -06:00

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();
});
});