mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 17:23:12 +00:00
260 lines
7.1 KiB
TypeScript
260 lines
7.1 KiB
TypeScript
import { expect, test, type Page, type Route } from "@playwright/test";
|
|
|
|
const FROZEN_NOW = "2026-08-19T15:00:00.000Z";
|
|
|
|
function testUser(userRoles: string) {
|
|
return {
|
|
token: "dashboard-visual-token",
|
|
expiration: "2030-01-01T00:00:00.000Z",
|
|
email: "admin@seahavenind.com",
|
|
userRoles,
|
|
phoneNumber: "5551234567",
|
|
fullname: "Dashboard Visual User",
|
|
id: "1",
|
|
};
|
|
}
|
|
|
|
const DISPATCHERS = [
|
|
{ id: "1", name: "Pat Dispatcher" },
|
|
{ id: "2", name: "Riley Chen" },
|
|
{ id: "3", name: "Sam Okafor" },
|
|
];
|
|
|
|
const STATS = {
|
|
total: 128,
|
|
open: 52,
|
|
notDispatched: 8,
|
|
completed: 64,
|
|
dueCount: 40,
|
|
completedDueCount: 30,
|
|
completionRate: 82,
|
|
averageResolutionDays: 3.4,
|
|
scheduledTomorrow: 6,
|
|
pendingUplifts: 4,
|
|
avetaPending: 3,
|
|
breakdown: [
|
|
{ status: "PM", count: 24 },
|
|
{ status: "Emergency", count: 6 },
|
|
{ status: "Reactive", count: 12 },
|
|
{ status: "Overdue", count: 9 },
|
|
],
|
|
statusDistribution: [
|
|
{ status: "Scheduled", count: 30 },
|
|
{ status: "In Progress", count: 18 },
|
|
{ status: "Completed", count: 64 },
|
|
{ status: "Pending", count: 16 },
|
|
],
|
|
};
|
|
|
|
const WORKLOAD = {
|
|
items: [
|
|
{
|
|
dispatcherId: "1",
|
|
dispatcherName: "Pat Dispatcher",
|
|
openWorkOrders: 12,
|
|
totalWorkOrders: 20,
|
|
},
|
|
{ dispatcherId: "2", dispatcherName: "Riley Chen", openWorkOrders: 7, totalWorkOrders: 15 },
|
|
],
|
|
page: 1,
|
|
pageSize: 10,
|
|
totalDispatchers: 2,
|
|
};
|
|
|
|
const PERFORMANCE = {
|
|
items: [
|
|
{
|
|
dispatcherId: "1",
|
|
dispatcherName: "Pat Dispatcher",
|
|
completionRate: 94,
|
|
averageResolutionDays: 2.5,
|
|
assignedCount: 20,
|
|
completedCount: 18,
|
|
},
|
|
{
|
|
dispatcherId: "2",
|
|
dispatcherName: "Riley Chen",
|
|
completionRate: 78,
|
|
averageResolutionDays: 3.1,
|
|
assignedCount: 15,
|
|
completedCount: 11,
|
|
},
|
|
{
|
|
dispatcherId: "3",
|
|
dispatcherName: "Sam Okafor",
|
|
completionRate: 61,
|
|
averageResolutionDays: 4.4,
|
|
assignedCount: 9,
|
|
completedCount: 5,
|
|
},
|
|
],
|
|
page: 1,
|
|
pageSize: 10,
|
|
totalDispatchers: 3,
|
|
};
|
|
|
|
const REGIONS = {
|
|
items: [
|
|
{ region: "East", workOrderCount: 44 },
|
|
{ region: "Central", workOrderCount: 31 },
|
|
{ region: "West", workOrderCount: 22 },
|
|
{ region: "California", workOrderCount: 18 },
|
|
{ region: "Unmapped/Other", workOrderCount: 5 },
|
|
],
|
|
};
|
|
|
|
const VENDOR_INSIGHTS = {
|
|
items: [
|
|
{
|
|
vendorId: "101",
|
|
vendorName: "Gateway Plumbing",
|
|
completionRate: 88,
|
|
rescheduleRate: 12,
|
|
averageResolutionDays: 2.9,
|
|
totalJobs: 40,
|
|
},
|
|
{
|
|
vendorId: "102",
|
|
vendorName: "Summit HVAC",
|
|
completionRate: 76,
|
|
rescheduleRate: 20,
|
|
averageResolutionDays: 3.6,
|
|
totalJobs: 28,
|
|
},
|
|
],
|
|
};
|
|
|
|
const TREND = {
|
|
granularity: "week",
|
|
buckets: [
|
|
{
|
|
date: "2026-07-27",
|
|
label: "Jul 27",
|
|
total: 18,
|
|
open: 4,
|
|
completed: 12,
|
|
canceled: 1,
|
|
overdue: 1,
|
|
isCurrent: false,
|
|
},
|
|
{
|
|
date: "2026-08-03",
|
|
label: "Aug 3",
|
|
total: 22,
|
|
open: 6,
|
|
completed: 13,
|
|
canceled: 1,
|
|
overdue: 2,
|
|
isCurrent: false,
|
|
},
|
|
{
|
|
date: "2026-08-10",
|
|
label: "Aug 10",
|
|
total: 26,
|
|
open: 8,
|
|
completed: 15,
|
|
canceled: 0,
|
|
overdue: 3,
|
|
isCurrent: false,
|
|
},
|
|
{
|
|
date: "2026-08-17",
|
|
label: "Aug 17",
|
|
total: 20,
|
|
open: 9,
|
|
completed: 9,
|
|
canceled: 1,
|
|
overdue: 1,
|
|
isCurrent: true,
|
|
},
|
|
],
|
|
};
|
|
|
|
async function fulfillJson(route: Route, body: unknown, status = 200) {
|
|
await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) });
|
|
}
|
|
|
|
async function mockDashboardApi(page: Page, userRoles: string) {
|
|
await page.clock.setFixedTime(new Date(FROZEN_NOW));
|
|
await page.addInitScript(
|
|
(user) => localStorage.setItem("auth", JSON.stringify(user)),
|
|
testUser(userRoles),
|
|
);
|
|
|
|
await page.route("**/api/**", async (route) => {
|
|
const pathname = new URL(route.request().url()).pathname;
|
|
|
|
// Only stub real backend calls; let Vite dev module requests (e.g.
|
|
// /src/domain/auth/api/*.ts) through untouched.
|
|
if (!pathname.startsWith("/api/")) {
|
|
await route.continue();
|
|
return;
|
|
}
|
|
|
|
if (pathname.includes("/Dashboard/Stats")) {
|
|
// The server reports no unassigned work for a dispatcher-scoped view.
|
|
const unassigned = userRoles === "Dispatcher" ? 0 : 11;
|
|
return fulfillJson(route, { data: { ...STATS, unassigned } });
|
|
}
|
|
if (pathname.includes("/Dashboard/Workload")) return fulfillJson(route, { data: WORKLOAD });
|
|
if (pathname.includes("/Dashboard/Performance"))
|
|
return fulfillJson(route, { data: PERFORMANCE });
|
|
if (pathname.includes("/Dashboard/Regions")) return fulfillJson(route, { data: REGIONS });
|
|
if (pathname.includes("/Dashboard/VendorInsights")) {
|
|
return fulfillJson(route, { data: VENDOR_INSIGHTS });
|
|
}
|
|
if (pathname.includes("/Dashboard/Trend")) return fulfillJson(route, { data: TREND });
|
|
if (pathname.includes("/workorders/lookups/dispatchers")) {
|
|
return fulfillJson(route, { data: DISPATCHERS });
|
|
}
|
|
|
|
return fulfillJson(route, { data: [] });
|
|
});
|
|
}
|
|
|
|
async function openDashboard(page: Page, userRoles: string) {
|
|
await mockDashboardApi(page, userRoles);
|
|
await page.goto("/");
|
|
await page.evaluate(() => document.fonts.ready);
|
|
await page.addStyleTag({
|
|
content: `
|
|
*, *::before, *::after { transition: none !important; animation: none !important; }
|
|
.MuiDrawer-paper { transform: none !important; }
|
|
.Toastify__toast-container { display: none !important; }
|
|
.tsqd-parent-container { display: none !important; }
|
|
`,
|
|
});
|
|
}
|
|
|
|
async function expectStableScreenshot(page: Page, name: string) {
|
|
await page.waitForTimeout(250);
|
|
await page.evaluate(
|
|
() =>
|
|
new Promise<void>((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
|
|
),
|
|
);
|
|
await expect(page).toHaveScreenshot(name, { fullPage: true });
|
|
}
|
|
|
|
test.describe("Dashboard deterministic pixel regression", () => {
|
|
test("admin dashboard", async ({ page }) => {
|
|
await openDashboard(page, "Admin");
|
|
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
|
await expect(page.getByText("Status Distribution")).toBeVisible();
|
|
await expect(page.getByText("Dispatcher Performance")).toBeVisible();
|
|
// The scope picker is available to Admin/Scheduler (SH-336).
|
|
await expect(page.getByRole("button", { name: /All dispatchers/ })).toBeVisible();
|
|
await expectStableScreenshot(page, "dashboard-admin.png");
|
|
});
|
|
|
|
test("dispatcher scoped dashboard", async ({ page }) => {
|
|
await openDashboard(page, "Dispatcher");
|
|
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
|
// A Dispatcher gets the static "My WOs" label and no dispatcher tables (SH-336).
|
|
await expect(page.getByText("My WOs")).toBeVisible();
|
|
await expect(page.getByText("Dispatcher Performance")).toHaveCount(0);
|
|
await expect(page.getByText("Status Distribution")).toBeVisible();
|
|
await expectStableScreenshot(page, "dashboard-dispatcher.png");
|
|
});
|
|
});
|