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> }) { 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"); }); });