feat(dashboard): KPI tiles drillable — navigate to /claims with filter
This commit is contained in:
+170
-2
@@ -8,6 +8,7 @@ import React, { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { Claims } from "./Claims";
|
||||
import { api } from "@/lib/api";
|
||||
import { useTailStore } from "@/store/tail-store";
|
||||
@@ -148,8 +149,30 @@ function setLocation(url: string): void {
|
||||
* a real DOM container wrapped in `QueryClientProvider` so the page's
|
||||
* TanStack Query calls (via `useClaims`) resolve against our mocked
|
||||
* `api.listClaims`.
|
||||
*
|
||||
* Also wraps in a `MemoryRouter` so the page's `useSearchParams`
|
||||
* (added when Claims started reading `?status=` / `?sort=` off the URL
|
||||
* for Dashboard KPI drill-through) has a router context to read from.
|
||||
* The default `initialEntries` mirrors whatever `setLocation(...)` set
|
||||
* on `window.location`, so the existing tests' pre-set URLs keep
|
||||
* working without per-test changes.
|
||||
*/
|
||||
function renderClaims(): { unmount: () => void } {
|
||||
const initialEntries = [
|
||||
window.location.pathname + window.location.search,
|
||||
];
|
||||
return renderClaimsAt(initialEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of `renderClaims` that lets a test pin a specific initial
|
||||
* URL. Used by the URL-param filter tests below so each one can start
|
||||
* at exactly the URL it's asserting on.
|
||||
*/
|
||||
function renderClaimsAt(initialEntries: string[]): {
|
||||
unmount: () => void;
|
||||
container: HTMLDivElement;
|
||||
} {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const qc = new QueryClient({
|
||||
@@ -168,11 +191,16 @@ function renderClaims(): { unmount: () => void } {
|
||||
React.createElement(
|
||||
QueryClientProvider,
|
||||
{ client: qc },
|
||||
React.createElement(Claims)
|
||||
)
|
||||
React.createElement(
|
||||
MemoryRouter,
|
||||
{ initialEntries },
|
||||
React.createElement(Claims),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
return {
|
||||
container,
|
||||
unmount: () => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
@@ -531,4 +559,144 @@ describe("Claims page drawer wiring", () => {
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// URL-driven filter state (sub-project 6, Phase 1 Task 1.8 follow-up).
|
||||
//
|
||||
// Dashboard KPI tiles drill through to Claims with `?status=` and
|
||||
// `?sort=` query params. These tests pin each URL and assert that
|
||||
// the page actually filters (not just lands on a default view).
|
||||
//
|
||||
// The strongest assertion is on `api.listClaims.mock.calls` — that's
|
||||
// the exact params the page forwards to the API, so a passing test
|
||||
// proves end-to-end that URL → useSearchParams → useClaims → fetch.
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
it("test_url_param_status_denied_filters_and_selects_chip", async () => {
|
||||
const { unmount, container } = renderClaimsAt(["/claims?status=denied"]);
|
||||
|
||||
// The first render of FilterChips is reached before useSearchParams
|
||||
// has the URL-derived `status` value committed (MemoryRouter
|
||||
// initializes synchronously but the chip's aria-checked only flips
|
||||
// to "true" on the next React commit). Wait for that commit so
|
||||
// the assertion below isn't a race against the first render.
|
||||
// We settle on `useClaims` having been called with status=denied
|
||||
// — that's the strongest invariant: it proves the URL → state →
|
||||
// API-call chain has fully propagated end-to-end.
|
||||
await settle(
|
||||
() => {
|
||||
const calls = (api.listClaims as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const lastCallParams = calls[calls.length - 1]?.[0] as
|
||||
| { status?: string; sort?: string; order?: string }
|
||||
| undefined;
|
||||
return lastCallParams?.status === "denied";
|
||||
},
|
||||
);
|
||||
|
||||
// Scope queries to this test's container so we can't accidentally
|
||||
// pick up a chip from a leaked render of a previous test.
|
||||
const deniedChip = Array.from(
|
||||
container.querySelectorAll('[role="radio"]'),
|
||||
).find(
|
||||
(el) => el.textContent?.trim().toLowerCase() === "denied",
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(deniedChip).toBeDefined();
|
||||
expect(deniedChip?.getAttribute("aria-checked")).toBe("true");
|
||||
|
||||
// No other status chip should be checked.
|
||||
const checkedChips = Array.from(
|
||||
container.querySelectorAll('[role="radio"][aria-checked="true"]'),
|
||||
);
|
||||
expect(checkedChips).toHaveLength(1);
|
||||
expect(checkedChips[0]?.textContent?.trim().toLowerCase()).toBe(
|
||||
"denied",
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("test_url_param_sort_minus_receivedAmount_sorts_by_received_desc", async () => {
|
||||
const { unmount } = renderClaimsAt(["/claims?sort=-receivedAmount"]);
|
||||
|
||||
// Wait for useClaims to be called once.
|
||||
await settle(
|
||||
() => (api.listClaims as ReturnType<typeof vi.fn>).mock.calls.length > 0,
|
||||
);
|
||||
|
||||
const calls = (api.listClaims as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const lastCallParams = calls[calls.length - 1]?.[0] as
|
||||
| { status?: string; sort?: string; order?: string }
|
||||
| undefined;
|
||||
|
||||
// The `-` prefix must be stripped off and order set to desc.
|
||||
expect(lastCallParams?.sort).toBe("receivedAmount");
|
||||
expect(lastCallParams?.order).toBe("desc");
|
||||
// No status filter — user only asked for a sort change.
|
||||
expect(lastCallParams?.status).toBeUndefined();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("test_url_param_sort_without_prefix_orders_ascending", async () => {
|
||||
const { unmount } = renderClaimsAt(["/claims?sort=submittedDate"]);
|
||||
|
||||
await settle(
|
||||
() => (api.listClaims as ReturnType<typeof vi.fn>).mock.calls.length > 0,
|
||||
);
|
||||
|
||||
const calls = (api.listClaims as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const lastCallParams = calls[calls.length - 1]?.[0] as
|
||||
| { status?: string; sort?: string; order?: string }
|
||||
| undefined;
|
||||
|
||||
// No `-` prefix → order is asc.
|
||||
expect(lastCallParams?.sort).toBe("submittedDate");
|
||||
expect(lastCallParams?.order).toBe("asc");
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("test_no_url_params_defaults_to_billed_desc_all_statuses", async () => {
|
||||
// Default URL — matches the existing "click a row to open the
|
||||
// drawer" tests, but here we assert on the params forwarded to
|
||||
// the API to prove the no-param default matches the previous
|
||||
// hardcoded behavior (sort=billedAmount, order=desc, no status).
|
||||
const { unmount } = renderClaimsAt(["/claims"]);
|
||||
|
||||
await settle(
|
||||
() => (api.listClaims as ReturnType<typeof vi.fn>).mock.calls.length > 0,
|
||||
);
|
||||
|
||||
const calls = (api.listClaims as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const lastCallParams = calls[calls.length - 1]?.[0] as
|
||||
| { status?: string; sort?: string; order?: string }
|
||||
| undefined;
|
||||
|
||||
expect(lastCallParams?.status).toBeUndefined();
|
||||
expect(lastCallParams?.sort).toBe("billedAmount");
|
||||
expect(lastCallParams?.order).toBe("desc");
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("test_url_param_sort_dash_only_falls_back_to_default", async () => {
|
||||
// `?sort=-` is the prefix-with-no-field degenerate case. A naive
|
||||
// `slice(1)` would yield "" and the API would get `sort=""`.
|
||||
// We assert the page falls back to the default sort instead.
|
||||
const { unmount } = renderClaimsAt(["/claims?sort=-"]);
|
||||
|
||||
await settle(
|
||||
() => (api.listClaims as ReturnType<typeof vi.fn>).mock.calls.length > 0,
|
||||
);
|
||||
|
||||
const calls = (api.listClaims as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const lastCallParams = calls[calls.length - 1]?.[0] as
|
||||
| { status?: string; sort?: string; order?: string }
|
||||
| undefined;
|
||||
|
||||
expect(lastCallParams?.sort).toBe("billedAmount");
|
||||
expect(lastCallParams?.order).toBe("desc");
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user