feat(batch-diff): claim ids drillable to /claims?claim=ID
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
Plus,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { DrillableCell } from "@/components/drill/DrillableCell";
|
||||
import { fmt } from "@/lib/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
@@ -263,13 +265,28 @@ function RowIndicator({
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ClaimIdCell({ id }: { id: string }) {
|
||||
// SP21 Phase 5 Task 5.6: each claim id is drillable to
|
||||
// /claims?claim=ID so the operator can jump straight from a
|
||||
// "Removed from A" or "Changed" row into the ClaimDrawer.
|
||||
// DrillableCell handles e.stopPropagation internally — important
|
||||
// if these rows ever get a row-level onClick. For removed claims
|
||||
// that no longer exist in the DB, the ClaimDrawer's 404 state
|
||||
// takes over (verified in Phase 2 testing).
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<DrillableCell
|
||||
onClick={() =>
|
||||
navigate(`/claims?claim=${encodeURIComponent(id)}`)
|
||||
}
|
||||
ariaLabel={`View claim ${id} in detail`}
|
||||
>
|
||||
<span
|
||||
className="font-mono text-[12px] tracking-tight"
|
||||
data-testid="diff-claim-id"
|
||||
>
|
||||
{id}
|
||||
</span>
|
||||
</DrillableCell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -481,8 +481,184 @@ describe("BatchDiff page", () => {
|
||||
).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;
|
||||
|
||||
Reference in New Issue
Block a user