Files
cyclone/src/pages/Upload.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

271 lines
8.0 KiB
TypeScript

// @vitest-environment happy-dom
// Tell React this is an `act`-aware test environment so react-query's
// internal state updates flush through without noisy console warnings.
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
import React, { act, useEffect } from "react";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { MemoryRouter, useLocation } from "react-router-dom";
import { createRoot, type Root } from "react-dom/client";
import { ClaimCard837 } from "@/components/ClaimCard837";
import { ClaimCard835 } from "./Upload";
import { useAppStore } from "@/store";
import type { ClaimOutput, ClaimPayment, ParsedBatch } from "@/types";
// Fixtures — kept tiny, just enough to exercise the drill logic.
const CLAIM_837: ClaimOutput = {
claim_id: "CLM-PERSISTED",
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: [] },
};
const CLAIM_835: ClaimPayment = {
payer_claim_control_number: "PCN-PERSISTED",
status_code: "1",
status_label: "Processed as Primary",
claim_filing_indicator: "CI",
facility_type: "11",
frequency_code: "1",
total_charge: "100",
total_paid: "80",
patient_responsibility: "20",
service_payments: [
{
line_number: 1,
procedure_qualifier: "HC",
procedure_code: "99213",
modifiers: [],
service_date: "2026-06-01",
units: "1",
unit_type: "UN",
charge: "100",
payment: "80",
adjustments: [],
},
],
original_claim_id: null,
};
// Mount the card inside a MemoryRouter so the useNavigate call has
// a router context (without this, clicking the drill link would
// throw "useNavigate may be used only in a Router").
function renderCard(
element: React.ReactElement,
initialEntries: string[] = ["/upload"],
): {
container: HTMLDivElement;
unmount: () => void;
tracker: { pathname: string; search: string };
} {
const container = document.createElement("div");
document.body.appendChild(container);
const tracker = { pathname: "/upload", search: "" };
const Tracker = () => {
const loc = useLocation();
useEffect(() => {
tracker.pathname = loc.pathname;
tracker.search = loc.search;
}, [loc.pathname, loc.search]);
return null;
};
const root: Root = createRoot(container);
act(() => {
root.render(
React.createElement(
MemoryRouter,
{ initialEntries },
React.createElement(Tracker, null),
element,
),
);
});
return {
container,
unmount: () => {
act(() => root.unmount());
container.remove();
},
tracker,
};
}
describe("ClaimCard837 / ClaimCard835 drill link (SP21 Phase 5 Task 5.7)", () => {
beforeEach(() => {
// Reset parsedBatches to empty by default — individual tests
// populate it as needed.
useAppStore.setState({ parsedBatches: [] });
});
afterEach(() => {
useAppStore.setState({ parsedBatches: [] });
});
it("ClaimCard837: renders the drill link when the claim_id is in a persisted batch", async () => {
// Pre-populate the store with a parsed batch that contains this
// claim id, then expand the card and verify the link is present.
const persisted: ParsedBatch = {
id: "batch-1",
kind: "837p",
inputFilename: "test.837",
parsedAt: "2026-06-21T12:00:00Z",
claimCount: 1,
passed: 1,
failed: 0,
claimIds: ["CLM-PERSISTED"],
summary: { total_claims: 1, passed: 1, failed: 0 },
};
useAppStore.setState({ parsedBatches: [persisted] });
const { container, unmount } = renderCard(
React.createElement(ClaimCard837, { claim: CLAIM_837 }),
);
// Expand the card.
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
(header as HTMLButtonElement).click();
await Promise.resolve();
});
// The drill link should now be visible.
const link = container.querySelector('[data-testid="upload-claim-drill"]');
expect(link).not.toBeNull();
expect(link?.textContent).toContain("See claim in detail");
unmount();
});
it("ClaimCard837: does NOT render the drill link when the claim_id is not persisted", async () => {
// Streaming-only claim — the parsedBatches slice is empty, so
// the link must be absent (clicking would 404 ClaimDrawer).
const { container, unmount } = renderCard(
React.createElement(ClaimCard837, { claim: CLAIM_837 }),
);
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
(header as HTMLButtonElement).click();
await Promise.resolve();
});
expect(
container.querySelector('[data-testid="upload-claim-drill"]'),
).toBeNull();
unmount();
});
it("ClaimCard837: clicking the drill link navigates to /claims?claim=ID", async () => {
const persisted: ParsedBatch = {
id: "batch-1",
kind: "837p",
inputFilename: "test.837",
parsedAt: "2026-06-21T12:00:00Z",
claimCount: 1,
passed: 1,
failed: 0,
claimIds: ["CLM-PERSISTED"],
summary: { total_claims: 1, passed: 1, failed: 0 },
};
useAppStore.setState({ parsedBatches: [persisted] });
const { container, unmount, tracker } = renderCard(
React.createElement(ClaimCard837, { claim: CLAIM_837 }),
);
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
(header as HTMLButtonElement).click();
await Promise.resolve();
});
const link = container.querySelector(
'[data-testid="upload-claim-drill"]',
) as HTMLButtonElement;
await act(async () => {
link.click();
await Promise.resolve();
});
expect(tracker.pathname).toBe("/claims");
expect(tracker.search).toBe("?claim=CLM-PERSISTED");
unmount();
});
it("ClaimCard835: renders the drill link when the PCN is persisted", async () => {
const persisted: ParsedBatch = {
id: "batch-1",
kind: "835",
inputFilename: "test.835",
parsedAt: "2026-06-21T12:00:00Z",
claimCount: 1,
passed: 1,
failed: 0,
claimIds: ["PCN-PERSISTED"],
summary: { total_claims: 1, passed: 1, failed: 0 },
};
useAppStore.setState({ parsedBatches: [persisted] });
const { container, unmount, tracker } = renderCard(
React.createElement(ClaimCard835, { claim: CLAIM_835 }),
);
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
(header as HTMLButtonElement).click();
await Promise.resolve();
});
const link = container.querySelector('[data-testid="upload-remit-drill"]');
expect(link).not.toBeNull();
await act(async () => {
(link as HTMLButtonElement).click();
await Promise.resolve();
});
// 835 cards drill to /remittances?remit=PCN (not /claims), since
// the RemitDrawer is the right surface for the payment side.
expect(tracker.pathname).toBe("/remittances");
expect(tracker.search).toBe("?remit=PCN-PERSISTED");
unmount();
});
it("ClaimCard835: does NOT render the drill link when the PCN is not persisted", async () => {
const { container, unmount } = renderCard(
React.createElement(ClaimCard835, { claim: CLAIM_835 }),
);
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
(header as HTMLButtonElement).click();
await Promise.resolve();
});
expect(
container.querySelector('[data-testid="upload-remit-drill"]'),
).toBeNull();
unmount();
});
});