mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-03 03:23:20 +00:00
The Work Order Breakdown object from the backend serializes its keys without JsonPropertyName, so they arrive lowercase (pm/emergency/reactive/ overdue/other). The mapper copied them straight into `status`, which the card uses as both label and the `types` drill-down id, and `workOrderTypeDrilldownSearch` only accepts canonical WOType values — so every breakdown row rendered lowercase and was inert (SH-354). Route both mapping branches through a normalizer that maps type keys back to their canonical labels case-insensitively; `other` is not a WOType, so it stays a labelled, non-drillable "Other" row, and unknown values (legacy array-shape lifecycle labels) pass through unchanged. Dispatcher Performance gated its drill-down on `completionRate > 0`, so a dispatcher with assigned work but no completions yet was inert here while drillable from Workload. The drill-down filters by dispatcher + range and never uses the rate, so gate on identity instead. Also mock useDashboardTrend in the dashboard page test so it no longer issues a real request that failed quietly in jsdom.
163 lines
5.2 KiB
TypeScript
163 lines
5.2 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>();
|
|
|
|
vi.mock("react-router", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("react-router")>()),
|
|
useNavigate: () => navigate,
|
|
}));
|
|
|
|
vi.mock("@/domain/dashboard/use-cases/use-dashboard-workload", () => ({
|
|
useDashboardWorkload: () => ({ data: [], isLoading: false, isFetching: false, error: null }),
|
|
}));
|
|
|
|
vi.mock("@/domain/dashboard/use-cases/use-dashboard-performance", () => ({
|
|
useDashboardPerformance: () => ({ data: [], isLoading: false, isFetching: false, error: null }),
|
|
}));
|
|
|
|
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(() => {
|
|
navigate.mockReset();
|
|
statsRange.mockReset();
|
|
});
|
|
|
|
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",
|
|
"Dispatcher Workload",
|
|
"Dispatcher Performance",
|
|
"Work Orders by Region",
|
|
"Vendor Insights",
|
|
]) {
|
|
expect(screen.getByText(title)).toBeInTheDocument();
|
|
}
|
|
expect(statsRange).toHaveBeenCalledWith("this-week");
|
|
});
|
|
|
|
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();
|
|
|
|
// Breakdown and Trend share this empty copy, so both cards surface it.
|
|
expect(screen.getAllByText("No work orders match the current filters.")).toHaveLength(2);
|
|
expect(screen.getAllByText("No dispatchers are set up yet.")).toHaveLength(2);
|
|
expect(screen.getByText("No active vendor has any work orders yet.")).toBeInTheDocument();
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|