feat(frontend): Claims page wires live tail + TailStatusPill

This commit is contained in:
Tyler
2026-06-20 17:11:56 -06:00
parent f1407dbb1d
commit 365e64e25a
2 changed files with 147 additions and 2 deletions
+111
View File
@@ -10,6 +10,7 @@ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Claims } from "./Claims";
import { api } from "@/lib/api";
import { useTailStore } from "@/store/tail-store";
import type { Claim, ClaimDetail } from "@/types";
// Module-level mock — vitest hoists vi.mock above imports. We mock the
@@ -38,6 +39,30 @@ vi.mock("@/lib/api", () => {
};
});
// ---------------------------------------------------------------------------
// Live-tail hook mock (sub-project 5, Phase 5 Task 22 page-level tests).
//
// We mock `useTailStream` directly (per the plan's "latter is simpler"
// guidance) so the page-level tests don't have to drive the real
// `streamTail` parser or the real AbortController-based reconnect loop.
// `useTailStream` is exercised exhaustively in its own test file
// (`useTailStream.test.ts`); the page only needs to observe that the
// returned status flows through to `<TailStatusPill>` and that items
// pushed into the tail store surface as rows via `useMergedTail`.
//
// The mock returns `status: "live"` by default — that matches what the
// real hook settles to after a `snapshot_end` event arrives, which is
// the state the page sees in production ~all of the time.
// ---------------------------------------------------------------------------
vi.mock("@/hooks/useTailStream", () => ({
useTailStream: vi.fn(() => ({
status: "live",
lastEventAt: null,
error: null,
forceReconnect: vi.fn(),
})),
}));
const SAMPLE_CLAIMS: Claim[] = [
{
id: "CLM-1",
@@ -190,6 +215,10 @@ describe("Claims page drawer wiring", () => {
(api.getClaimDetail as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
SAMPLE_DETAIL
);
// Live-tail slice is a singleton zustand store — clear it between
// tests so the live-arrival test doesn't see rows from a previous
// case.
useTailStore.getState().reset("claims");
});
afterEach(() => {
@@ -420,4 +449,86 @@ describe("Claims page drawer wiring", () => {
document.body.querySelector('[data-testid="keyboard-cheatsheet"]')
).toBeNull();
});
// -------------------------------------------------------------------
// Live-tail integration (sub-project 5, Phase 5 Task 22 page tests).
//
// We mock `useTailStream` at the module level (see the top of this
// file) so the page sees a settled `status: "live"` without driving
// the real streamTail parser. To exercise the "tail arrival triggers a
// row" path we push a new claim directly into `useTailStore` — this
// is exactly what the real `useTailStream` hook does on `item` events,
// so we cover the same render path (zustand notify → useMergedTail
// re-derive → `<TableRow>` mount).
// -------------------------------------------------------------------
it("test_live_tail_arrival_triggers_row_in_table", async () => {
const { unmount } = renderClaims();
// Base list (SAMPLE_CLAIMS, set in beforeEach) must be visible first.
await settle(
() => document.body.textContent?.includes("CLM-1") ?? false,
);
// The new claim id is brand new (not in base, not in tail store yet).
const TAIL_ID = "CLM-TAIL-1";
expect(
document.body.textContent?.includes(TAIL_ID) ?? false,
).toBe(false);
// Simulate a live-arriving claim — this is what `useTailStream`
// does when it dispatches an `item` event. Wrap in `act` because
// zustand subscribers re-render synchronously.
await act(async () => {
useTailStore.getState().addClaim({
id: TAIL_ID,
patientName: "Live Arrival",
providerNpi: "1234567890",
payerName: "Colorado Medicaid",
cptCode: "99215",
billedAmount: 999,
receivedAmount: 0,
status: "submitted",
submissionDate: "2026-06-20",
});
});
// The new row must appear — proves the end-to-end path
// zustand.addClaim → useTailStore notify → useMergedTail re-derive
// → <TableBody> mounts the row with the new id.
await settle(
() => document.body.textContent?.includes(TAIL_ID) ?? false,
);
expect(document.body.textContent).toContain(TAIL_ID);
// The row's secondary cells should also be present so we know the
// row fully rendered, not just that the id appeared somewhere
// (e.g. in the search-input placeholder or similar).
expect(document.body.textContent).toContain("Live Arrival");
expect(document.body.textContent).toContain("99215");
unmount();
});
it("test_status_pill_shows_live_after_stream_connects", async () => {
const { unmount } = renderClaims();
// Wait for the base table to mount — once it's there, the toolbar
// (which contains the pill) has also mounted.
await settle(
() => document.body.textContent?.includes("CLM-1") ?? false,
);
// The mocked `useTailStream` returns `status: "live"`, so the
// <TailStatusPill> should render with the human label "Live"
// (per `STATUS_LABEL` in `TailStatusPill.tsx`). We assert on the
// pill's textContent so this stays robust against future styling
// changes (the test doesn't depend on Tailwind classes).
const pill = document.body.querySelector(
'[data-testid="tail-status-pill"]',
);
expect(pill).not.toBeNull();
expect(pill?.textContent).toContain("Live");
unmount();
});
});