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")) return fulfillJson(route, { data: STATS }); 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((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"); }); });