feat(batch-diff): claim ids drillable to /claims?claim=ID
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { DrillableCell } from "@/components/drill/DrillableCell";
|
||||||
import { fmt } from "@/lib/format";
|
import { fmt } from "@/lib/format";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type {
|
import type {
|
||||||
@@ -263,13 +265,28 @@ function RowIndicator({
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function ClaimIdCell({ id }: { id: string }) {
|
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 (
|
return (
|
||||||
<span
|
<DrillableCell
|
||||||
className="font-mono text-[12px] tracking-tight"
|
onClick={() =>
|
||||||
data-testid="diff-claim-id"
|
navigate(`/claims?claim=${encodeURIComponent(id)}`)
|
||||||
|
}
|
||||||
|
ariaLabel={`View claim ${id} in detail`}
|
||||||
>
|
>
|
||||||
{id}
|
<span
|
||||||
</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");
|
).toContain("pick a batch");
|
||||||
unmount();
|
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
|
// Keep `ApiError` referenced so the import isn't tree-shaken by
|
||||||
// vitest's transformer when the mock factory above is hoisted.
|
// vitest's transformer when the mock factory above is hoisted.
|
||||||
void ApiError;
|
void ApiError;
|
||||||
|
|||||||
Reference in New Issue
Block a user