mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-05 11:52:03 +00:00
221 lines
7.3 KiB
TypeScript
221 lines
7.3 KiB
TypeScript
import { expect, test, type Page, type Route } from "@playwright/test";
|
|
|
|
const PORTAL_TOKEN = "portal-e2e-token";
|
|
|
|
const futureDate = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString();
|
|
const pastDate = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString();
|
|
|
|
interface DispatchDetail {
|
|
id: number;
|
|
dispatchNumber: string;
|
|
workOrderTitle?: string;
|
|
internalWONumber?: string;
|
|
status: string;
|
|
nteAmount?: number;
|
|
scheduledDate?: string;
|
|
poNumber?: string;
|
|
dispatchedAt?: string;
|
|
completedDate?: string;
|
|
refusedAt?: string;
|
|
statusUpdatedAt?: string;
|
|
description?: string;
|
|
workOrder?: Record<string, unknown>;
|
|
location?: Record<string, unknown>;
|
|
dispatcherContact?: { name?: string; email?: string; phone?: string };
|
|
checklist: never[];
|
|
comments: never[];
|
|
upliftRequests: never[];
|
|
signoffs: never[];
|
|
documents: never[];
|
|
}
|
|
|
|
const sentDetail: DispatchDetail = {
|
|
id: 101,
|
|
dispatchNumber: "DSP-101",
|
|
workOrderTitle: "Emergency boiler repair",
|
|
internalWONumber: "WO-8842",
|
|
status: "Sent",
|
|
nteAmount: 1250,
|
|
scheduledDate: futureDate,
|
|
poNumber: "PO-555",
|
|
statusUpdatedAt: "2026-08-09T16:30:00Z",
|
|
description: "Boiler is leaking; vendor visit required.",
|
|
workOrder: {
|
|
workerOrderTitle: "Emergency boiler repair",
|
|
internalWONumber: "WO-8842",
|
|
trade: "Plumbing",
|
|
subTrade: "Backflow Preventers",
|
|
siteCode: "STL-01",
|
|
service: "Boiler Repair",
|
|
},
|
|
location: {
|
|
name: "Central Distribution Center",
|
|
address: "100 Industrial Pkwy",
|
|
city: "St. Louis",
|
|
state: "MO",
|
|
zipCode: "63101",
|
|
},
|
|
dispatcherContact: {
|
|
name: "Dana Field",
|
|
email: "dana@seahavenind.com",
|
|
phone: "314-555-0142",
|
|
},
|
|
checklist: [],
|
|
comments: [],
|
|
upliftRequests: [],
|
|
signoffs: [],
|
|
documents: [],
|
|
};
|
|
|
|
const listDispatches = [
|
|
{
|
|
id: 101,
|
|
dispatchNumber: "DSP-101",
|
|
workOrderTitle: "Emergency boiler repair",
|
|
internalWONumber: "WO-8842",
|
|
locationName: "Central Distribution Center",
|
|
locationCity: "St. Louis",
|
|
locationState: "MO",
|
|
status: "Sent",
|
|
nteAmount: 1250,
|
|
scheduledDate: futureDate,
|
|
},
|
|
{
|
|
id: 102,
|
|
dispatchNumber: "DSP-102",
|
|
workOrderTitle: "HVAC quarterly service",
|
|
status: "Refused",
|
|
refusedAt: "2026-08-08T10:00:00Z",
|
|
},
|
|
{
|
|
id: 103,
|
|
dispatchNumber: "DSP-103",
|
|
workOrderTitle: "PastAcknowledged job",
|
|
status: "Acknowledged",
|
|
scheduledDate: pastDate,
|
|
},
|
|
];
|
|
|
|
async function fulfill(route: Route, body: unknown, status = 200) {
|
|
await route.fulfill({
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
async function mockPortalApi(page: Page, options: { sessionError?: boolean } = {}) {
|
|
const detailState: { current: DispatchDetail } = { current: { ...sentDetail } };
|
|
|
|
await page.route("**/api/vendor-portal/session", (route) => {
|
|
if (options.sessionError) {
|
|
fulfill(route, { message: "Invalid or expired token" }, 401);
|
|
return;
|
|
}
|
|
fulfill(route, { data: { companyName: "Gateway Plumbing", vendorId: 101 } });
|
|
});
|
|
|
|
await page.route(/\/api\/vendor-portal\/dispatches\/\d+\/refuse$/, async (route) => {
|
|
const body = route.request().postDataJSON() as { reason?: string };
|
|
detailState.current = {
|
|
...detailState.current,
|
|
status: "Refused",
|
|
refusedAt: new Date().toISOString(),
|
|
};
|
|
fulfill(route, {
|
|
data: {
|
|
id: detailState.current.id,
|
|
status: "Refused",
|
|
refusedAt: detailState.current.refusedAt,
|
|
reason: body?.reason,
|
|
},
|
|
});
|
|
});
|
|
|
|
await page.route(/\/api\/vendor-portal\/dispatches\/\d+$/, (route) =>
|
|
fulfill(route, { data: detailState.current }),
|
|
);
|
|
|
|
await page.route(/\/api\/vendor-portal\/dispatches(?:\?.*)?$/, (route) =>
|
|
fulfill(route, { data: listDispatches }),
|
|
);
|
|
|
|
return detailState;
|
|
}
|
|
|
|
test.describe("Vendor portal lifecycle", () => {
|
|
test("upcoming filter, detail snapshot, refuse dialog, and locked Refused state", async ({
|
|
page,
|
|
}) => {
|
|
await mockPortalApi(page);
|
|
await page.goto(`/v/${PORTAL_TOKEN}/dashboard`);
|
|
|
|
await expect(page.getByRole("heading", { name: "Your Work" })).toBeVisible();
|
|
|
|
await expect(page.getByRole("button", { name: /^Upcoming/ })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: /^Refused/ })).toBeVisible();
|
|
|
|
await page.getByRole("button", { name: /^Upcoming/ }).click();
|
|
await expect(page.getByText("DSP-101 — Emergency boiler repair")).toBeVisible();
|
|
await expect(page.getByText("DSP-103")).toHaveCount(0);
|
|
|
|
await page.getByText("DSP-101 — Emergency boiler repair").click();
|
|
await expect(page).toHaveURL(/\/dispatch\/101$/);
|
|
|
|
await expect(page.getByRole("heading", { name: "DSP-101" })).toBeVisible();
|
|
await expect(page.getByText("Boiler Repair", { exact: true })).toBeVisible();
|
|
await expect(page.getByText("STL-01")).toBeVisible();
|
|
await expect(page.getByText("$1250.00").first()).toBeVisible();
|
|
await expect(page.getByRole("link", { name: "dana@seahavenind.com" })).toBeVisible();
|
|
await expect(page.getByText("Dana Field")).toBeVisible();
|
|
await expect(page.getByText("314-555-0142")).toBeVisible();
|
|
await expect(page.getByText(/Status updated/i)).toBeVisible();
|
|
|
|
await page.getByRole("button", { name: "Refuse Dispatch" }).click();
|
|
const dialog = page.getByRole("dialog", { name: "Refuse dispatch" });
|
|
await expect(dialog).toBeVisible();
|
|
await dialog.getByLabel(/Reason/i).fill("Equipment unavailable this week.");
|
|
await expect(dialog.getByText(/\d+\/500/)).toBeVisible();
|
|
await dialog.getByRole("button", { name: "Refuse dispatch" }).click();
|
|
|
|
await expect(dialog).toHaveCount(0);
|
|
await expect(page.getByText("Refused", { exact: true })).toBeVisible();
|
|
await expect(page.getByText(/refused and locked/i)).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Accept Dispatch" })).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: "Refuse Dispatch" })).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: "Submit Signoff" })).toHaveCount(0);
|
|
});
|
|
|
|
test("covers the refuse dialog at a mobile viewport", async ({ page }) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await mockPortalApi(page);
|
|
await page.goto(`/v/${PORTAL_TOKEN}/dashboard`);
|
|
|
|
await page.getByText("DSP-101 — Emergency boiler repair").click();
|
|
await expect(page).toHaveURL(/\/dispatch\/101$/);
|
|
|
|
await page.getByRole("button", { name: "Refuse Dispatch" }).click();
|
|
const dialog = page.getByRole("dialog", { name: "Refuse dispatch" });
|
|
await expect(dialog).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
await expect(dialog).toHaveCount(0);
|
|
|
|
await page.getByRole("button", { name: "Refuse Dispatch" }).click();
|
|
await page
|
|
.getByRole("dialog", { name: "Refuse dispatch" })
|
|
.getByRole("button", {
|
|
name: "Refuse dispatch",
|
|
})
|
|
.click();
|
|
await expect(page.getByText("Refused", { exact: true })).toBeVisible();
|
|
await expect(page.getByText(/refused and locked/i)).toBeVisible();
|
|
});
|
|
|
|
test("shows an expired-token state when the session is rejected", async ({ page }) => {
|
|
await mockPortalApi(page, { sessionError: true });
|
|
await page.goto(`/v/${PORTAL_TOKEN}/dashboard`);
|
|
|
|
await expect(page.getByRole("heading", { name: "Access Denied" })).toBeVisible();
|
|
await expect(page.getByRole("alert")).toContainText(/invalid or expired/i);
|
|
});
|
|
});
|