diff --git a/e2e/work-orders/pending-status-menu.spec.ts b/e2e/work-orders/pending-status-menu.spec.ts new file mode 100644 index 00000000..90c1beca --- /dev/null +++ b/e2e/work-orders/pending-status-menu.spec.ts @@ -0,0 +1,183 @@ +import { expect, test, type Page, type Route } from "@playwright/test"; + +const FROZEN_NOW = "2026-08-19T15:00:00.000Z"; + +const BOARD_ROW = { + id: 1, + woNumber: "WO-501", + workOrderType: 2, + siteCode: "STL-01", + locationName: "St. Louis HQ", + locationId: 10, + lifecycleStatusLabel: "In Progress", + scheduledDate: "2026-08-19", + dispatcherId: "1", + dispatcherName: "Pat Dispatcher", + pm: "HVAC", + rowVersion: "rv-1", +}; + +type Role = "Admin" | "Dispatcher"; + +async function fulfillJson(route: Route, body: unknown) { + await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(body) }); +} + +async function mockApi(page: Page, role: Role, createPayloads: unknown[] = []) { + await page.clock.install({ time: new Date(FROZEN_NOW) }); + await page.clock.resume(); + await page.addInitScript( + (user) => { + localStorage.setItem("auth", JSON.stringify(user)); + const style = document.createElement("style"); + style.textContent = ".tsqd-parent-container { display: none !important; }"; + document.documentElement.appendChild(style); + }, + { + token: `wo-pending-${role.toLowerCase()}-e2e-token`, + expiration: "2030-01-01T00:00:00.000Z", + email: `${role.toLowerCase()}@seahavenind.com`, + userRoles: role, + phoneNumber: "5551234567", + fullname: `Work Order E2E ${role}`, + id: "1", + }, + ); + + let row = { ...BOARD_ROW }; + let createdRow: typeof BOARD_ROW | undefined; + await page.route( + (url) => url.pathname.startsWith("/api/"), + async (route) => { + const request = route.request(); + const { pathname } = new URL(request.url()); + + if (pathname.includes("/workorders/board/search")) { + await fulfillJson(route, { items: [], totalCount: 0, page: 0, pageSize: 50 }); + return; + } + if (pathname === "/api/workorders/board" && request.method() === "GET") { + await fulfillJson(route, { + weekStart: "2026-08-17", + weekEnd: "2026-08-21", + counts: { returned: createdRow ? 2 : 1, total: createdRow ? 2 : 1 }, + unscheduled: createdRow ? [createdRow] : [], + scheduled: [row], + }); + return; + } + if (pathname === "/api/workorders/board" && request.method() === "POST") { + createPayloads.push(request.postDataJSON()); + createdRow = { + ...BOARD_ROW, + id: 900, + woNumber: "WO-900", + lifecycleStatusLabel: "Pending", + scheduledDate: "", + rowVersion: "rv-created", + }; + await fulfillJson(route, { data: { id: 900, internalWONumber: "WO-900" } }); + return; + } + if (pathname === "/api/workorders/1/board" && request.method() === "PATCH") { + const body = request.postDataJSON() as { field: string; value: string }; + row = { ...row, lifecycleStatusLabel: body.value, rowVersion: "rv-2" }; + await fulfillJson(route, row); + return; + } + if (pathname.includes("/locations/sites") || pathname.includes("/Contact/GetLocations")) { + await fulfillJson(route, { + data: [{ id: 10, name: "STL-01 ยท St. Louis, MO", siteCode: "STL-01", code: "STL-01" }], + }); + return; + } + if (pathname === "/api/locations/10") { + await fulfillJson(route, { + data: { + id: 10, + name: "St. Louis HQ", + siteCode: "STL-01", + contact: "Pat Site", + phone: "3145550111", + }, + }); + return; + } + await fulfillJson(route, { data: [], items: [] }); + }, + ); +} + +async function expectManualTransitionWithoutPending(page: Page) { + const row = page.locator("#wo-row-1"); + await expect(row.getByText("WO-501")).toBeVisible(); + await row.getByRole("button", { name: "In Progress" }).click(); + + const menu = page.locator(".MuiPopover-paper").last(); + await expect(menu.getByRole("button", { name: "Pending", exact: true })).toHaveCount(0); + await expect(menu.getByRole("button", { name: "On Site", exact: true })).toBeEnabled(); + await menu.getByRole("button", { name: "On Site", exact: true }).click(); + + await expect(row.getByRole("button", { name: "On Site" })).toBeVisible(); +} + +test.describe("existing work order status menu", () => { + test.setTimeout(60_000); + + test("Admin can choose a manual status but not Pending", async ({ page }) => { + await mockApi(page, "Admin"); + await page.goto("/workorders"); + await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({ + timeout: 20_000, + }); + + await expectManualTransitionWithoutPending(page); + }); + + test("Dispatcher can choose a manual status but not Pending", async ({ page }) => { + await mockApi(page, "Dispatcher"); + await page.goto("/workorders"); + await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({ + timeout: 20_000, + }); + + await expectManualTransitionWithoutPending(page); + }); + + test("Admin can select Pending for a new work order when Schedule On has no date", async ({ + page, + }) => { + const createPayloads: unknown[] = []; + await mockApi(page, "Admin", createPayloads); + await page.goto("/workorders"); + await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({ + timeout: 20_000, + }); + await page.getByRole("button", { name: "New WO" }).click(); + await expect(page.getByRole("heading", { name: "Type & schedule" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Pick a date" })).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); + + await page.getByRole("button", { name: /select site/i }).click(); + await page + .getByRole("button", { name: /STL-01/ }) + .last() + .click(); + await page.getByRole("button", { name: "Continue" }).click(); + + await page.getByRole("button", { name: "Incomplete", exact: true }).last().click(); + const menu = page.locator(".MuiPopover-paper").last(); + await expect(menu.getByRole("button", { name: "Pending", exact: true })).toBeEnabled(); + await menu.getByRole("button", { name: "Pending", exact: true }).click(); + await expect(page.getByRole("button", { name: "Pending", exact: true })).toBeVisible(); + + await page.getByRole("button", { name: "Create work order" }).click(); + await expect(page.getByRole("heading", { name: "Type & schedule" })).toBeHidden(); + await expect + .poll(() => createPayloads) + .toEqual([expect.objectContaining({ lifecycleStatus: 2 })]); + const createdRow = page.locator("#wo-row-900"); + await expect(createdRow.getByText("WO-900")).toBeVisible(); + await expect(createdRow.getByRole("button", { name: "Pending" })).toBeVisible(); + }); +});