shoc-frontend-new/src/test/app/(protected)/dashboard/dashboard-page.test.tsx
Codex Review Integration 1271d3c11e feat(dashboard): dispatcher scope picker, status distribution, region drilldown, perf counts
- SH-336: add the dispatcher-scope picker (All / My WOs / individual) for
  Scheduler/Admin via a new viewAllDispatchersOnDashboard role gate, a static
  'My WOs' label for Dispatcher, and hide the dispatcher tables (and skip their
  queries) when the viewer cannot see all dispatchers. Scope drives dispatcherId
  on stats, workload, performance, and regions (SH-347 contract).
- SH-352: add the Status Distribution module with its empty state and status
  drill-down.
- SH-347: show Assigned/Completed counts and colour the completion rate
  (green >=90, amber 70-89, red <70) on Dispatcher Performance.
- SH-348: drill a region bar into the board region filter; hold the
  unconfirmed 'Unmapped/Other' bucket off the chart.
- SH-349: label Vendor Insights as all-time, company-wide.
- Make the drilldown-filters effect fire only on URL change; add a deterministic
  clock to the status-drilldown test; add the dashboard visual regression spec
  and baselines.
2026-09-17 02:56:31 -03:00

265 lines
8.8 KiB
TypeScript

import { fireEvent, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { DashboardRangeKey } from "@/domain/dashboard/types/dashboard-range";
import { renderWithProviders } from "@/test/test-utils";
const navigate = vi.fn();
const statsRange = vi.fn<(range: DashboardRangeKey) => void>();
const { workloadMock, performanceMock, authUser } = vi.hoisted(() => ({
workloadMock: vi.fn(),
performanceMock: vi.fn(),
authUser: { current: { id: "99", userRoles: "Admin" } as { id: string; userRoles: string } },
}));
const EMPTY_DISPATCHER_PAGE = { items: [], page: 1, pageSize: 10, totalDispatchers: 0 };
vi.mock("react-router", async (importOriginal) => ({
...(await importOriginal<typeof import("react-router")>()),
useNavigate: () => navigate,
}));
// An Admin has `viewAllDispatchersOnDashboard`, so the scope picker and the
// dispatcher tables render (SH-336).
vi.mock("@/providers/auth-context", async (importOriginal) => ({
...(await importOriginal<typeof import("@/providers/auth-context")>()),
useAuthContext: () => ({ user: authUser.current }),
}));
vi.mock("@/domain/work-orders/use-cases/use-work-orders-list", () => ({
useWorkOrderUsers: () => ({ data: [], isLoading: false, isFetching: false, error: null }),
}));
vi.mock("@/domain/dashboard/use-cases/use-dashboard-workload", () => ({
useDashboardWorkload: workloadMock,
}));
vi.mock("@/domain/dashboard/use-cases/use-dashboard-performance", () => ({
useDashboardPerformance: performanceMock,
}));
vi.mock("@/domain/dashboard/use-cases/use-dashboard-regions", () => ({
useDashboardRegions: () => ({ data: [], isLoading: false, isFetching: false, error: null }),
}));
vi.mock("@/domain/dashboard/use-cases/use-dashboard-vendor-insights", () => ({
useDashboardVendorInsights: () => ({
data: [],
isLoading: false,
isFetching: false,
error: null,
}),
}));
vi.mock("@/domain/dashboard/use-cases/use-dashboard-trend", () => ({
useDashboardTrend: () => ({
data: { granularity: null, points: [] },
isLoading: false,
isFetching: false,
error: null,
}),
}));
const statsData = {
scheduledTomorrow: 3,
pendingUplifts: 5,
avetaPending: 0,
breakdown: [
{ status: "PM", count: 7 },
{ status: "Completed", count: 0 },
],
};
vi.mock("@/domain/dashboard/use-cases/use-dashboard-stats", () => ({
useDashboardStats: (range: DashboardRangeKey) => {
statsRange(range);
return { data: statsData, isLoading: false, isFetching: false, error: null };
},
}));
import DashboardPage from "@/app/(protected)/dashboard";
import {
avetaPendingDrilldownSearch,
scheduledTomorrowDrilldownSearch,
} from "@/domain/dashboard/utils/work-order-drilldown-links";
import { addDaysIso, businessTodayIso } from "@/domain/dashboard/utils/dashboard-range-utils";
function renderDashboard() {
return renderWithProviders(<DashboardPage />);
}
describe("DashboardPage", () => {
beforeEach(() => {
authUser.current = { id: "99", userRoles: "Admin" };
navigate.mockReset();
statsRange.mockReset();
workloadMock.mockReset();
performanceMock.mockReset();
workloadMock.mockReturnValue({
data: EMPTY_DISPATCHER_PAGE,
isLoading: false,
isFetching: false,
error: null,
});
performanceMock.mockReturnValue({
data: EMPTY_DISPATCHER_PAGE,
isLoading: false,
isFetching: false,
error: null,
});
});
it("renders the prototype title, range options, and card titles", () => {
renderDashboard();
expect(screen.getByRole("heading", { name: "Dashboard" })).toBeInTheDocument();
for (const label of ["This week", "Last 3 months", "Today", "All time"]) {
expect(screen.getByRole("button", { name: label })).toBeInTheDocument();
}
for (const title of [
"Scheduled Tomorrow",
"Pending Uplifts",
"Aveta Pending",
"Work Order Breakdown",
"Status Distribution",
"Dispatcher Workload",
"Dispatcher Performance",
"Work Orders by Region",
"Vendor Insights",
]) {
expect(screen.getByText(title)).toBeInTheDocument();
}
expect(statsRange).toHaveBeenCalledWith("this-week");
});
it("shows a Dispatcher the static My WOs scope and hides the dispatcher tables", () => {
authUser.current = { id: "7", userRoles: "Dispatcher" };
renderDashboard();
expect(screen.getByText("My WOs")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "All dispatchers" })).toBeNull();
expect(screen.queryByText("Dispatcher Workload")).toBeNull();
expect(screen.queryByText("Dispatcher Performance")).toBeNull();
// Their scope is server-scoped, so the tables are never even queried.
expect(workloadMock).toHaveBeenCalledWith("this-week", 1, null, false);
});
it("lets an Admin open the dispatcher-scope picker", () => {
renderDashboard();
expect(screen.getByRole("button", { name: /All dispatchers/ })).toBeInTheDocument();
expect(screen.getByText("Dispatcher Workload")).toBeInTheDocument();
expect(workloadMock).toHaveBeenCalledWith("this-week", 1, null, true);
});
it("exposes positive KPI cards as keyboard-accessible drill-downs", () => {
renderDashboard();
const tomorrowCard = screen.getByRole("button", { name: /Scheduled Tomorrow/ });
expect(tomorrowCard).toBeInTheDocument();
fireEvent.click(tomorrowCard);
const today = businessTodayIso();
expect(navigate).toHaveBeenCalledWith(
`/workorders?${scheduledTomorrowDrilldownSearch(today).toString()}`,
);
fireEvent.click(screen.getByRole("button", { name: /Pending Uplifts/ }));
expect(navigate).toHaveBeenCalledWith("/uplifts");
});
it("keeps zero counts inert", () => {
renderDashboard();
expect(screen.queryByRole("button", { name: /Aveta Pending/ })).toBeNull();
fireEvent.click(screen.getByText("Aveta Pending"));
expect(navigate).not.toHaveBeenCalled();
const inertRow = screen.getByText("Completed");
fireEvent.click(inertRow);
expect(navigate).not.toHaveBeenCalled();
});
it("drills the breakdown status into filtered work orders", () => {
renderDashboard();
fireEvent.click(screen.getByText("PM"));
const calls = navigate.mock.calls;
const [url] = calls[calls.length - 1] ?? [];
expect(String(url)).toContain("/workorders?");
expect(String(url)).toContain("types=PM");
});
it("switches the stats range from the toggle", () => {
renderDashboard();
fireEvent.click(screen.getByRole("button", { name: "Today" }));
expect(statsRange).toHaveBeenCalledWith("today");
});
it("shows the documented empty states when datasets are empty", () => {
renderDashboard();
// Region, Status Distribution, and Trend share this empty copy.
expect(screen.getAllByText("No work orders match the current filters.")).toHaveLength(3);
expect(screen.getAllByText("No dispatchers are set up yet.")).toHaveLength(2);
expect(screen.getByText("No active vendor has any work orders yet.")).toBeInTheDocument();
});
it("pages the dispatcher tables at 10 rows and re-queries on page change", () => {
const rows = Array.from({ length: 10 }, (_, index) => ({
dispatcherId: String(index + 1),
dispatcherName: `Dispatcher ${index + 1}`,
openWorkOrders: 1,
totalWorkOrders: 2,
}));
workloadMock.mockReturnValue({
data: { items: rows, page: 1, pageSize: 10, totalDispatchers: 23 },
isLoading: false,
isFetching: false,
error: null,
});
renderDashboard();
// 23 dispatchers at 10/page = 3 pages; selecting page 2 re-queries with page 2.
fireEvent.click(screen.getByRole("button", { name: "Go to page 2" }));
const requestedPages = workloadMock.mock.calls.map((call) => call[1]);
expect(requestedPages).toContain(2);
});
it("resets the dispatcher page when the range changes", () => {
const rows = Array.from({ length: 10 }, (_, index) => ({
dispatcherId: String(index + 1),
dispatcherName: `Dispatcher ${index + 1}`,
openWorkOrders: 1,
totalWorkOrders: 2,
}));
workloadMock.mockReturnValue({
data: { items: rows, page: 2, pageSize: 10, totalDispatchers: 23 },
isLoading: false,
isFetching: false,
error: null,
});
renderDashboard();
fireEvent.click(screen.getByRole("button", { name: "Go to page 2" }));
fireEvent.click(screen.getByRole("button", { name: "Today" }));
const calls = workloadMock.mock.calls;
const lastRequestedPage = calls[calls.length - 1]?.[1];
expect(lastRequestedPage).toBe(1);
});
it("keeps drilldown dates deterministic on the business clock", () => {
const today = businessTodayIso();
expect(scheduledTomorrowDrilldownSearch(today).get("dateFrom")).toBe(addDaysIso(today, 1));
expect(avetaPendingDrilldownSearch(today).get("aveta")).toBe("1");
});
});