665 lines
20 KiB
TypeScript
665 lines
20 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.
|
|
// Mirrors the convention used by Batches.test.tsx and Acks.test.tsx.
|
|
(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 { MemoryRouter } from "react-router-dom";
|
|
import {
|
|
describe,
|
|
expect,
|
|
it,
|
|
vi,
|
|
beforeEach,
|
|
afterEach,
|
|
} from "vitest";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { BatchDiff } from "./BatchDiff";
|
|
import { api, ApiError } from "@/lib/api";
|
|
import type { BatchDiff as BatchDiffResponse } from "@/types";
|
|
|
|
// Mock the api module. Only the surface the page actually touches is
|
|
// stubbed; everything else is left undefined so an accidental call
|
|
// blows up loudly.
|
|
vi.mock("@/lib/api", () => ({
|
|
api: {
|
|
isConfigured: true,
|
|
listBatches: vi.fn(),
|
|
getBatchDiff: vi.fn(),
|
|
getRemittance: vi.fn(),
|
|
},
|
|
ApiError: class ApiError extends Error {
|
|
constructor(public status: number, message: string) {
|
|
super(message);
|
|
}
|
|
},
|
|
}));
|
|
|
|
/**
|
|
* Mount the page into a container wrapped in MemoryRouter +
|
|
* QueryClientProvider. Uses a 10ms retryDelay so non-404 errors don't
|
|
* blow past the test timeout — same pattern used by Batches.test.tsx.
|
|
*/
|
|
function renderIntoContainer(
|
|
element: React.ReactElement,
|
|
initialEntries: string[] = ["/batch-diff"],
|
|
): { container: HTMLDivElement; unmount: () => void } {
|
|
const container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
const qc = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false,
|
|
retryDelay: 10,
|
|
},
|
|
},
|
|
});
|
|
const root: Root = createRoot(container);
|
|
act(() => {
|
|
root.render(
|
|
<MemoryRouter initialEntries={initialEntries}>
|
|
<QueryClientProvider client={qc}>{element}</QueryClientProvider>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
return {
|
|
container,
|
|
unmount: () => {
|
|
act(() => root.unmount());
|
|
container.remove();
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Flush microtasks + react state updates until the predicate holds or
|
|
* the deadline expires. react-query's fetch resolves asynchronously
|
|
* so we have to await a tick before the rendered DOM reflects the
|
|
* mocked data.
|
|
*/
|
|
async function waitFor(
|
|
predicate: () => boolean,
|
|
description: string,
|
|
timeoutMs = 2000,
|
|
): Promise<void> {
|
|
const start = Date.now();
|
|
while (!predicate()) {
|
|
if (Date.now() - start > timeoutMs) {
|
|
throw new Error(`waitFor: ${description} did not become true within ${timeoutMs}ms`);
|
|
}
|
|
await act(async () => {
|
|
await Promise.resolve();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Fixtures used by the data-driven tests.
|
|
const BATCH_A = {
|
|
id: "B-A",
|
|
kind: "837p" as const,
|
|
inputFilename: "original.837",
|
|
parsedAt: "2026-06-15T12:00:00Z",
|
|
claimCount: 2,
|
|
};
|
|
const BATCH_B = {
|
|
id: "B-B",
|
|
kind: "837p" as const,
|
|
inputFilename: "corrected.837",
|
|
parsedAt: "2026-06-16T12:00:00Z",
|
|
claimCount: 3,
|
|
};
|
|
|
|
function makeDiffPayload(): BatchDiffResponse {
|
|
return {
|
|
a: {
|
|
id: BATCH_A.id,
|
|
kind: "837p",
|
|
parsedAt: BATCH_A.parsedAt,
|
|
inputFilename: BATCH_A.inputFilename,
|
|
claimCount: 2,
|
|
},
|
|
b: {
|
|
id: BATCH_B.id,
|
|
kind: "837p",
|
|
parsedAt: BATCH_B.parsedAt,
|
|
inputFilename: BATCH_B.inputFilename,
|
|
claimCount: 3,
|
|
},
|
|
added: [
|
|
{
|
|
claim_id: "CLM-3",
|
|
patient_name: "Sam Roe",
|
|
total_charge: 200,
|
|
service_date: "2026-06-02",
|
|
status: "submitted",
|
|
cpt_codes: ["99213"],
|
|
},
|
|
],
|
|
removed: [
|
|
{
|
|
claim_id: "CLM-2",
|
|
patient_name: "Jane Doe",
|
|
total_charge: 75,
|
|
service_date: "2026-06-01",
|
|
status: "submitted",
|
|
cpt_codes: ["99213"],
|
|
},
|
|
],
|
|
changed: [
|
|
{
|
|
a: {
|
|
claim_id: "CLM-1",
|
|
patient_name: "Pat Lee",
|
|
total_charge: 100,
|
|
service_date: "2026-06-01",
|
|
status: "submitted",
|
|
cpt_codes: ["99213"],
|
|
},
|
|
b: {
|
|
claim_id: "CLM-1",
|
|
patient_name: "Pat Lee",
|
|
total_charge: 150,
|
|
service_date: "2026-06-01",
|
|
status: "submitted",
|
|
cpt_codes: ["99213"],
|
|
},
|
|
diff: [{ field: "total_charge", a: 100, b: 150 }],
|
|
},
|
|
],
|
|
summary: {
|
|
addedCount: 1,
|
|
removedCount: 1,
|
|
changedCount: 1,
|
|
unchangedCount: 1,
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("BatchDiff page", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Default for the per-remit detail fetch — the drawer fetches
|
|
// this whenever `?remit=` is in the URL. Return a never-resolving
|
|
// promise so the drawer stays in the loading state; the smoke
|
|
// test only asserts the drawer mounts, not the loaded data.
|
|
(
|
|
api.getRemittance as unknown as ReturnType<typeof vi.fn>
|
|
).mockReturnValue(new Promise(() => {}));
|
|
});
|
|
|
|
afterEach(() => {
|
|
// happy-dom persists window.location between tests; reset so the
|
|
// URL-state hook starts from a clean slate.
|
|
(window as unknown as { happyDOM: { setURL: (u: string) => void } })
|
|
.happyDOM.setURL("http://localhost/batch-diff");
|
|
});
|
|
|
|
it("SP21 Task 4.5: deep-link ?remit=ID opens the RemitDrawer on mount", async () => {
|
|
// Pre-set the URL with `?remit=`. The page doesn't surface any
|
|
// per-remit rows (the diff payload is claim-level), but the
|
|
// drawer must still mount so the URL contract is honored.
|
|
//
|
|
// `useRemitDrawerUrlState` reads `window.location.search`
|
|
// (NOT React Router's search params) so we have to set the
|
|
// global window URL via happyDOM AND give MemoryRouter an
|
|
// initial entry — both stay in lockstep.
|
|
(window as unknown as { happyDOM: { setURL: (u: string) => void } })
|
|
.happyDOM.setURL("http://localhost/batch-diff?remit=REM-7");
|
|
(
|
|
api.listBatches as unknown as ReturnType<typeof vi.fn>
|
|
).mockResolvedValue([BATCH_A, BATCH_B]);
|
|
|
|
const { unmount } = renderIntoContainer(
|
|
<BatchDiff />,
|
|
["/batch-diff?remit=REM-7"],
|
|
);
|
|
|
|
// Page header must be present + drawer must be open.
|
|
await waitFor(
|
|
() => !!document.querySelector('[data-testid="batch-diff-page"]'),
|
|
"page header mounted",
|
|
);
|
|
await waitFor(
|
|
() => !!document.querySelector('[data-testid="remit-drawer"]'),
|
|
"remit drawer mounted via deep link",
|
|
);
|
|
expect(
|
|
document.querySelector('[data-testid="remit-drawer"]'),
|
|
).not.toBeNull();
|
|
|
|
unmount();
|
|
});
|
|
|
|
it("renders the awaiting-picks empty state when no batches are selected", async () => {
|
|
(api.listBatches as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
BATCH_A, BATCH_B,
|
|
]);
|
|
|
|
const { unmount } = renderIntoContainer(<BatchDiff />);
|
|
await waitFor(
|
|
() => !!document.querySelector('[data-testid="batch-diff-page"]'),
|
|
"page header mounted",
|
|
);
|
|
// The copy unique to the empty state — verifier the page rendered
|
|
// the awaiting-picks branch and not the loading skeleton.
|
|
expect(document.body.textContent).toContain("awaiting picks");
|
|
expect(document.querySelector('[data-testid="diff-picker-a"]')).not.toBeNull();
|
|
expect(document.querySelector('[data-testid="diff-picker-b"]')).not.toBeNull();
|
|
// Diff not requested yet — getBatchDiff should NOT have been
|
|
// called because the hook gates on both picks being non-null.
|
|
expect(api.getBatchDiff).not.toHaveBeenCalled();
|
|
unmount();
|
|
});
|
|
|
|
it("renders the loading skeleton while the diff is in flight", async () => {
|
|
(api.listBatches as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
BATCH_A, BATCH_B,
|
|
]);
|
|
// Never resolves — keeps the query in the loading state.
|
|
(api.getBatchDiff as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
|
|
new Promise(() => {}),
|
|
);
|
|
|
|
// Start the page with both picks already in the URL.
|
|
const { unmount } = renderIntoContainer(
|
|
<BatchDiff />,
|
|
[`/batch-diff?a=${BATCH_A.id}&b=${BATCH_B.id}`],
|
|
);
|
|
await waitFor(
|
|
() => !!document.querySelector('[data-testid="batch-diff-skeleton"]'),
|
|
"loading skeleton visible",
|
|
);
|
|
unmount();
|
|
});
|
|
|
|
it("renders the three-section diff view when the backend returns deltas", async () => {
|
|
(api.listBatches as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
BATCH_A, BATCH_B,
|
|
]);
|
|
(api.getBatchDiff as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
makeDiffPayload(),
|
|
);
|
|
|
|
const { unmount } = renderIntoContainer(
|
|
<BatchDiff />,
|
|
[`/batch-diff?a=${BATCH_A.id}&b=${BATCH_B.id}`],
|
|
);
|
|
|
|
// Wait for the diff view (full, non-empty payload).
|
|
await waitFor(
|
|
() => !!document.querySelector('[data-testid="batch-diff-view"]'),
|
|
"diff view rendered",
|
|
);
|
|
|
|
// Sections are present.
|
|
expect(
|
|
document.querySelector('[data-testid="batch-diff-added-section"]'),
|
|
).not.toBeNull();
|
|
expect(
|
|
document.querySelector('[data-testid="batch-diff-removed-section"]'),
|
|
).not.toBeNull();
|
|
expect(
|
|
document.querySelector('[data-testid="batch-diff-changed-section"]'),
|
|
).not.toBeNull();
|
|
|
|
// Per-claim rows render with the right indicator + id.
|
|
expect(
|
|
document.querySelector('[data-testid="diff-added-row-CLM-3"]'),
|
|
).not.toBeNull();
|
|
expect(
|
|
document.querySelector('[data-testid="diff-removed-row-CLM-2"]'),
|
|
).not.toBeNull();
|
|
expect(
|
|
document.querySelector('[data-testid="diff-changed-row-CLM-1"]'),
|
|
).not.toBeNull();
|
|
|
|
// Indicator chips are present for each bucket.
|
|
expect(
|
|
document.querySelector('[data-testid="diff-indicator-added"]'),
|
|
).not.toBeNull();
|
|
expect(
|
|
document.querySelector('[data-testid="diff-indicator-removed"]'),
|
|
).not.toBeNull();
|
|
expect(
|
|
document.querySelector('[data-testid="diff-indicator-changed"]'),
|
|
).not.toBeNull();
|
|
|
|
unmount();
|
|
});
|
|
|
|
it("renders the no-deltas state when the backend reports an empty diff", async () => {
|
|
(api.listBatches as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
BATCH_A, BATCH_B,
|
|
]);
|
|
const empty = makeDiffPayload();
|
|
empty.added = [];
|
|
empty.removed = [];
|
|
empty.changed = [];
|
|
empty.summary = {
|
|
addedCount: 0,
|
|
removedCount: 0,
|
|
changedCount: 0,
|
|
unchangedCount: 2,
|
|
};
|
|
(api.getBatchDiff as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
empty,
|
|
);
|
|
|
|
const { unmount } = renderIntoContainer(
|
|
<BatchDiff />,
|
|
[`/batch-diff?a=${BATCH_A.id}&b=${BATCH_B.id}`],
|
|
);
|
|
|
|
await waitFor(
|
|
() => !!document.querySelector('[data-testid="batch-diff-empty"]'),
|
|
"empty-diff state visible",
|
|
);
|
|
// Summary cards still surface the counts even when zero.
|
|
expect(
|
|
document.querySelector('[data-testid="batch-diff-summary"]'),
|
|
).not.toBeNull();
|
|
unmount();
|
|
});
|
|
|
|
it("renders the not-found state on a 404 from getBatchDiff", async () => {
|
|
(api.listBatches as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
BATCH_A, BATCH_B,
|
|
]);
|
|
// Mirror the backend's 404 contract: ApiError(404).
|
|
(api.getBatchDiff as unknown as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
new ApiError(404, "Not found — batch B-A not found"),
|
|
);
|
|
|
|
const { unmount } = renderIntoContainer(
|
|
<BatchDiff />,
|
|
[`/batch-diff?a=${BATCH_A.id}&b=${BATCH_B.id}`],
|
|
);
|
|
|
|
// The not-found state uses the ErrorState primitive with a retry
|
|
// affordance; assert by message text since we don't ship a
|
|
// dedicated testid for the variant.
|
|
await waitFor(
|
|
() => (document.body.textContent ?? "").includes("not found"),
|
|
"not-found error text visible",
|
|
);
|
|
expect(
|
|
(document.body.textContent ?? "").toLowerCase(),
|
|
).toContain("one of these batches was not found");
|
|
unmount();
|
|
});
|
|
|
|
it("renders the network error state on a non-404 failure", async () => {
|
|
(api.listBatches as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
BATCH_A, BATCH_B,
|
|
]);
|
|
(api.getBatchDiff as unknown as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
new Error("500 Internal Server Error — upstream"),
|
|
);
|
|
|
|
const { unmount } = renderIntoContainer(
|
|
<BatchDiff />,
|
|
[`/batch-diff?a=${BATCH_A.id}&b=${BATCH_B.id}`],
|
|
);
|
|
|
|
await waitFor(
|
|
() =>
|
|
(document.body.textContent ?? "").includes(
|
|
"Couldn't compute the diff",
|
|
),
|
|
"network error text visible",
|
|
);
|
|
// Retry button is rendered (the ErrorState primitive's affordance).
|
|
expect(
|
|
(document.body.textContent ?? "").toLowerCase(),
|
|
).toContain("retry");
|
|
unmount();
|
|
});
|
|
|
|
it("passes the URL a/b picks through to getBatchDiff", async () => {
|
|
(api.listBatches as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
BATCH_A, BATCH_B,
|
|
]);
|
|
(api.getBatchDiff as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
makeDiffPayload(),
|
|
);
|
|
|
|
const { unmount } = renderIntoContainer(
|
|
<BatchDiff />,
|
|
[`/batch-diff?a=${BATCH_A.id}&b=${BATCH_B.id}`],
|
|
);
|
|
|
|
await waitFor(
|
|
() => (api.getBatchDiff as unknown as ReturnType<typeof vi.fn>).mock.calls.length > 0,
|
|
"getBatchDiff invoked",
|
|
);
|
|
|
|
// The two picks are passed through in order: a, b.
|
|
expect(api.getBatchDiff).toHaveBeenCalledWith(BATCH_A.id, BATCH_B.id);
|
|
unmount();
|
|
});
|
|
|
|
it("Clear picks button resets both pickers and returns to awaiting-picks state", async () => {
|
|
(api.listBatches as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
BATCH_A, BATCH_B,
|
|
]);
|
|
(api.getBatchDiff as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
makeDiffPayload(),
|
|
);
|
|
|
|
const { unmount } = renderIntoContainer(
|
|
<BatchDiff />,
|
|
[`/batch-diff?a=${BATCH_A.id}&b=${BATCH_B.id}`],
|
|
);
|
|
|
|
await waitFor(
|
|
() => !!document.querySelector('[data-testid="diff-clear-button"]'),
|
|
"clear button visible",
|
|
);
|
|
|
|
const clear = document.querySelector(
|
|
'[data-testid="diff-clear-button"]',
|
|
) as HTMLButtonElement | null;
|
|
await act(async () => {
|
|
clear?.click();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
// Back to the awaiting-picks empty state. The hook's
|
|
// `enabled: a && b` predicate stops getBatchDiff from being
|
|
// re-invoked once both ids are null.
|
|
await waitFor(
|
|
() => (document.body.textContent ?? "").includes("awaiting picks"),
|
|
"empty state re-appears",
|
|
);
|
|
// The picker for B should now be empty (the trigger renders the
|
|
// placeholder text).
|
|
expect(
|
|
(document.body.textContent ?? "").toLowerCase(),
|
|
).toContain("pick a batch");
|
|
unmount();
|
|
});
|
|
|
|
it("SP21 Task 5.6: clicking an added-claim id navigates to /claims?claim=ID", async () => {
|
|
// Each ClaimIdCell wraps its id text with DrillableCell, whose
|
|
// onClick navigates to /claims?claim=ID. The MemoryRouter gives
|
|
// us a router context so the navigation completes; we observe
|
|
// it via the rendered pathname on a LocationTracker spy.
|
|
(api.listBatches as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
BATCH_A, BATCH_B,
|
|
]);
|
|
(api.getBatchDiff as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
makeDiffPayload(),
|
|
);
|
|
|
|
const captured: { pathname: string; search: string } = {
|
|
pathname: "/batch-diff",
|
|
search: "",
|
|
};
|
|
const Tracker = () => {
|
|
const loc = useLocationSafe();
|
|
React.useEffect(() => {
|
|
captured.pathname = loc.pathname;
|
|
captured.search = loc.search;
|
|
}, [loc.pathname, loc.search]);
|
|
return null;
|
|
};
|
|
|
|
const { unmount } = renderIntoContainer(
|
|
<>
|
|
<Tracker />
|
|
<BatchDiff />
|
|
</>,
|
|
[`/batch-diff?a=${BATCH_A.id}&b=${BATCH_B.id}`],
|
|
);
|
|
|
|
await waitFor(
|
|
() => !!document.querySelector('[data-testid="diff-added-row-CLM-3"]'),
|
|
"added row rendered",
|
|
);
|
|
|
|
const cell = document.querySelector(
|
|
'[data-testid="diff-added-row-CLM-3"] [data-testid="diff-claim-id"]',
|
|
);
|
|
expect(cell).not.toBeNull();
|
|
// DrillableCell wraps the id text in its own <button>; click the
|
|
// nearest button ancestor so the drillable onClick fires.
|
|
const drillBtn =
|
|
(cell?.closest("button") as HTMLButtonElement | null) ?? null;
|
|
expect(drillBtn).not.toBeNull();
|
|
await act(async () => {
|
|
drillBtn!.click();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await waitFor(
|
|
() => captured.pathname === "/claims" && captured.search === "?claim=CLM-3",
|
|
"navigation to /claims?claim=CLM-3",
|
|
);
|
|
unmount();
|
|
});
|
|
|
|
it("SP21 Task 5.6: clicking a removed-claim id still navigates (ClaimDrawer handles 404)", async () => {
|
|
// The diff can list claim ids that no longer exist in the DB
|
|
// (Removed from A means "was in A, not in B" — the claim may
|
|
// still exist or may have been purged). Either way, clicking
|
|
// the id drills to /claims?claim=ID; the ClaimDrawer's 404
|
|
// state (Phase 2) handles the missing-claim case. This test
|
|
// only verifies the click path, not the drawer's 404 surface.
|
|
(api.listBatches as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
BATCH_A, BATCH_B,
|
|
]);
|
|
(api.getBatchDiff as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
makeDiffPayload(),
|
|
);
|
|
|
|
const captured: { pathname: string; search: string } = {
|
|
pathname: "/batch-diff",
|
|
search: "",
|
|
};
|
|
const Tracker = () => {
|
|
const loc = useLocationSafe();
|
|
React.useEffect(() => {
|
|
captured.pathname = loc.pathname;
|
|
captured.search = loc.search;
|
|
}, [loc.pathname, loc.search]);
|
|
return null;
|
|
};
|
|
|
|
const { unmount } = renderIntoContainer(
|
|
<>
|
|
<Tracker />
|
|
<BatchDiff />
|
|
</>,
|
|
[`/batch-diff?a=${BATCH_A.id}&b=${BATCH_B.id}`],
|
|
);
|
|
|
|
await waitFor(
|
|
() => !!document.querySelector('[data-testid="diff-removed-row-CLM-2"]'),
|
|
"removed row rendered",
|
|
);
|
|
|
|
const cell = document.querySelector(
|
|
'[data-testid="diff-removed-row-CLM-2"] [data-testid="diff-claim-id"]',
|
|
);
|
|
const drillBtn =
|
|
(cell?.closest("button") as HTMLButtonElement | null) ?? null;
|
|
expect(drillBtn).not.toBeNull();
|
|
await act(async () => {
|
|
drillBtn!.click();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await waitFor(
|
|
() => captured.pathname === "/claims" && captured.search === "?claim=CLM-2",
|
|
"navigation to /claims?claim=CLM-2",
|
|
);
|
|
unmount();
|
|
});
|
|
|
|
it("SP21 Task 5.6: clicking a changed-claim id navigates to /claims?claim=ID", async () => {
|
|
// The Changed row uses the A-side id (per existing test
|
|
// assertions). Drill should fire on that id.
|
|
(api.listBatches as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
BATCH_A, BATCH_B,
|
|
]);
|
|
(api.getBatchDiff as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
makeDiffPayload(),
|
|
);
|
|
|
|
const captured: { pathname: string; search: string } = {
|
|
pathname: "/batch-diff",
|
|
search: "",
|
|
};
|
|
const Tracker = () => {
|
|
const loc = useLocationSafe();
|
|
React.useEffect(() => {
|
|
captured.pathname = loc.pathname;
|
|
captured.search = loc.search;
|
|
}, [loc.pathname, loc.search]);
|
|
return null;
|
|
};
|
|
|
|
const { unmount } = renderIntoContainer(
|
|
<>
|
|
<Tracker />
|
|
<BatchDiff />
|
|
</>,
|
|
[`/batch-diff?a=${BATCH_A.id}&b=${BATCH_B.id}`],
|
|
);
|
|
|
|
await waitFor(
|
|
() => !!document.querySelector('[data-testid="diff-changed-row-CLM-1"]'),
|
|
"changed row rendered",
|
|
);
|
|
|
|
const cell = document.querySelector(
|
|
'[data-testid="diff-changed-row-CLM-1"] [data-testid="diff-claim-id"]',
|
|
);
|
|
const drillBtn =
|
|
(cell?.closest("button") as HTMLButtonElement | null) ?? null;
|
|
expect(drillBtn).not.toBeNull();
|
|
await act(async () => {
|
|
drillBtn!.click();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await waitFor(
|
|
() => captured.pathname === "/claims" && captured.search === "?claim=CLM-1",
|
|
"navigation to /claims?claim=CLM-1",
|
|
);
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
// SP21 Phase 5 Task 5.6: react-router-dom's `useLocation` hook, used
|
|
// by the LocationTracker test helper below. Aliased to keep the import
|
|
// block tidy alongside React + the page import.
|
|
import { useLocation as useLocationSafe } from "react-router-dom";
|
|
|
|
// Keep `ApiError` referenced so the import isn't tree-shaken by
|
|
// vitest's transformer when the mock factory above is hoisted.
|
|
void ApiError;
|