mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 11:33:12 +00:00
265 lines
9 KiB
TypeScript
265 lines
9 KiB
TypeScript
import { expect, test, type Page, type Request, type Route } from "@playwright/test";
|
|
|
|
// Technicians registered inline from work-order entry points
|
|
// become real vendor records only on save, through the additive vendor-company roster
|
|
// PATCH (never a whole-vendor POST that restates or blanks company contact fields).
|
|
|
|
const FROZEN_NOW = "2026-08-19T15:00:00.000Z";
|
|
|
|
const TEST_USER = {
|
|
token: "wo-inline-technician-e2e-token",
|
|
expiration: "2030-01-01T00:00:00.000Z",
|
|
email: "admin@seahavenind.com",
|
|
userRoles: "Admin",
|
|
phoneNumber: "5551234567",
|
|
fullname: "Work Order E2E Admin",
|
|
id: "1",
|
|
};
|
|
|
|
const VENDORS = [
|
|
{
|
|
id: 41,
|
|
companyName: "Vinewood LLC",
|
|
contactName: "Adam Whyte",
|
|
tradeSpecialties: "HVAC",
|
|
address: "1 Market St",
|
|
},
|
|
];
|
|
|
|
const ROSTER = {
|
|
companyId: 7,
|
|
rowVersion: "AAAAAAAAB9E=",
|
|
name: "Vinewood LLC",
|
|
companyPhone: "(314) 555-0100",
|
|
email: "",
|
|
address: "1 Market St",
|
|
city: "St. Louis",
|
|
state: "MO",
|
|
zip: "63101",
|
|
notes: "",
|
|
technicians: [{ id: 41, contactName: "Adam Whyte", phone: "", email: "", isActive: true }],
|
|
};
|
|
|
|
const BOARD_ROW = {
|
|
id: 1,
|
|
woNumber: "WO-501",
|
|
workOrderType: 2,
|
|
siteCode: "STL-01",
|
|
locationName: "St. Louis HQ",
|
|
locationId: 10,
|
|
lifecycleStatusLabel: "Scheduled",
|
|
scheduledDate: "2026-08-19",
|
|
dispatcherId: "1",
|
|
dispatcherName: "Pat Dispatcher",
|
|
pm: "HVAC",
|
|
vendorId: 41,
|
|
vendorName: "Vinewood LLC",
|
|
techName: "Adam Whyte",
|
|
title: "Quarterly HVAC PM",
|
|
rowVersion: "rv-1",
|
|
};
|
|
|
|
type Captured = { method: string; pathname: string; body: unknown };
|
|
|
|
async function fulfillJson(route: Route, body: unknown, status = 200) {
|
|
await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) });
|
|
}
|
|
|
|
function bodyOf(request: Request): unknown {
|
|
try {
|
|
return request.postDataJSON();
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function rosterAfterAdd(body: { addTechnicians?: Array<Record<string, unknown>> }) {
|
|
const added = (body.addTechnicians ?? []).map((technician, index) => ({
|
|
...technician,
|
|
id: 42 + index,
|
|
}));
|
|
return { ...ROSTER, rowVersion: "AAAAAAAAB9F=", technicians: [...ROSTER.technicians, ...added] };
|
|
}
|
|
|
|
async function mockApi(page: Page, writes: Captured[]) {
|
|
await page.clock.install({ time: new Date(FROZEN_NOW) });
|
|
await page.clock.resume();
|
|
await page.addInitScript((user) => localStorage.setItem("auth", JSON.stringify(user)), TEST_USER);
|
|
|
|
await page.route("**/api/**", async (route) => {
|
|
const request = route.request();
|
|
const pathname = new URL(request.url()).pathname;
|
|
if (!pathname.startsWith("/api/")) return route.fallback();
|
|
const method = request.method();
|
|
if (method !== "GET") writes.push({ method, pathname, body: bodyOf(request) });
|
|
|
|
if (pathname.includes("/workorders/board/search")) {
|
|
return fulfillJson(route, { items: [], totalCount: 0, page: 0, pageSize: 50 });
|
|
}
|
|
if (pathname === "/api/workorders/board" && method === "GET") {
|
|
return fulfillJson(route, {
|
|
weekStart: "2026-08-17",
|
|
weekEnd: "2026-08-21",
|
|
counts: { returned: 1, total: 1 },
|
|
unscheduled: [],
|
|
scheduled: [BOARD_ROW],
|
|
});
|
|
}
|
|
if (pathname === "/api/workorders/board" && method === "POST") {
|
|
return fulfillJson(route, { data: { id: 900, woNumber: "SH-900" } });
|
|
}
|
|
if (pathname === "/api/workorders/1/board" && method === "PATCH") {
|
|
return fulfillJson(route, { ...BOARD_ROW, vendorId: 42, techName: "Jordan Lee" });
|
|
}
|
|
if (pathname === "/api/vendor-company-roster" && method === "GET") {
|
|
return fulfillJson(route, ROSTER);
|
|
}
|
|
if (pathname === "/api/vendor-company-roster/7" && method === "PATCH") {
|
|
return fulfillJson(route, rosterAfterAdd(bodyOf(request) as never));
|
|
}
|
|
if (pathname.toLowerCase().includes("/vendors/dropdown")) {
|
|
return fulfillJson(route, { data: VENDORS });
|
|
}
|
|
if (pathname.includes("/locations/sites")) {
|
|
return fulfillJson(route, {
|
|
data: [{ id: 10, name: "STL-01 · St. Louis, MO", siteCode: "STL-01", code: "STL-01" }],
|
|
});
|
|
}
|
|
if (pathname === "/api/locations/10") {
|
|
return fulfillJson(route, {
|
|
data: {
|
|
id: 10,
|
|
name: "St. Louis HQ",
|
|
siteCode: "STL-01",
|
|
contact: "Pat Site",
|
|
phone: "3145550111",
|
|
},
|
|
});
|
|
}
|
|
return fulfillJson(route, { data: [] });
|
|
});
|
|
}
|
|
|
|
function vendorWrites(writes: Captured[]) {
|
|
return writes.filter(
|
|
(write) =>
|
|
write.pathname.startsWith("/api/vendors") ||
|
|
write.pathname.startsWith("/api/vendor-company-roster"),
|
|
);
|
|
}
|
|
|
|
async function openWizardVendorStep(page: Page) {
|
|
await page.goto("/workorders");
|
|
await page.getByRole("button", { name: "New WO" }).click();
|
|
await expect(page.getByRole("heading", { name: "Type & schedule" })).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 expect(page.getByRole("button", { name: "Continue" })).toBeEnabled();
|
|
await page.getByRole("button", { name: "Continue" }).click();
|
|
|
|
await page.getByRole("button", { name: /select company/i }).click();
|
|
await page.getByRole("button", { name: "Vinewood LLC" }).last().click();
|
|
await page.getByRole("button", { name: "+ Add technician" }).click();
|
|
await page.getByLabel("New Technician 1 name").fill("Jordan Lee");
|
|
await page.getByLabel("New Technician 1 phone").fill("3145550199");
|
|
}
|
|
|
|
test.describe("Inline technician registration", () => {
|
|
test("wizard persists the technician under the company on create and assigns it", async ({
|
|
page,
|
|
}) => {
|
|
const writes: Captured[] = [];
|
|
await mockApi(page, writes);
|
|
await openWizardVendorStep(page);
|
|
|
|
// Staging a technician is local only.
|
|
expect(vendorWrites(writes)).toEqual([]);
|
|
|
|
await page.getByRole("button", { name: "Create work order" }).click();
|
|
|
|
await expect
|
|
.poll(() => writes.find((write) => write.pathname === "/api/workorders/board"))
|
|
.toBeTruthy();
|
|
expect(vendorWrites(writes)).toEqual([
|
|
{
|
|
method: "PATCH",
|
|
pathname: "/api/vendor-company-roster/7",
|
|
body: {
|
|
rowVersion: "AAAAAAAAB9E=",
|
|
addTechnicians: [
|
|
{
|
|
contactName: "Jordan Lee",
|
|
phone: "(314) 555-0199",
|
|
email: "",
|
|
tradeSpecialties: "HVAC",
|
|
isActive: true,
|
|
preferredContact: "Phone",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
]);
|
|
const create = writes.find((write) => write.pathname === "/api/workorders/board");
|
|
expect(create?.body).toEqual(expect.objectContaining({ vendorId: 42 }));
|
|
});
|
|
|
|
test("cancelling the wizard after staging a technician writes no vendor (SH-366)", async ({
|
|
page,
|
|
}) => {
|
|
const writes: Captured[] = [];
|
|
await mockApi(page, writes);
|
|
await openWizardVendorStep(page);
|
|
|
|
await page.getByRole("button", { name: "Back" }).click();
|
|
await page.getByRole("button", { name: "Back" }).click();
|
|
await page.getByRole("button", { name: "Cancel" }).click();
|
|
await expect(page.getByRole("heading", { name: "Type & schedule" })).toBeHidden();
|
|
|
|
// Reopening starts clean: the staged technician was discarded, not persisted.
|
|
await page.getByRole("button", { name: "New WO" }).click();
|
|
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 expect(page.getByRole("group", { name: "New Technician 1" })).toHaveCount(0);
|
|
|
|
expect(writes).toEqual([]);
|
|
});
|
|
|
|
test("vendor assignment modal saves an inline technician for an existing company (SH-365)", async ({
|
|
page,
|
|
}) => {
|
|
const writes: Captured[] = [];
|
|
await mockApi(page, writes);
|
|
await page.goto("/workorders");
|
|
|
|
await page
|
|
.locator("#wo-row-1")
|
|
.getByRole("button", { name: /Adam Whyte/ })
|
|
.click();
|
|
const dialog = page.getByRole("dialog");
|
|
await dialog.getByRole("button", { name: /\+ add technician/i }).click();
|
|
await dialog.getByPlaceholder("Full name").fill("Jordan Lee");
|
|
await dialog.getByPlaceholder("(000) 000-0000").fill("3145550199");
|
|
await dialog.getByRole("button", { name: /^add$/i }).click();
|
|
await dialog.getByRole("button", { name: /^save$/i }).click();
|
|
|
|
await expect(dialog).toBeHidden();
|
|
await expect
|
|
.poll(() => writes.find((write) => write.pathname === "/api/workorders/1/board"))
|
|
.toBeTruthy();
|
|
expect(writes.some((write) => write.pathname.startsWith("/api/vendors"))).toBe(false);
|
|
const rosterPatch = writes.find((write) => write.pathname === "/api/vendor-company-roster/7");
|
|
expect(rosterPatch?.body).not.toHaveProperty("companyFields");
|
|
const boardPatch = writes.find((write) => write.pathname === "/api/workorders/1/board");
|
|
expect(JSON.stringify(boardPatch?.body)).toContain("42");
|
|
});
|
|
});
|