import { expect, test, type Page, type Route } from "@playwright/test"; const TEST_USER = { token: "vendor-e2e-token", expiration: new Date(Date.now() + 60 * 60 * 1000).toISOString(), email: "admin@seahavenind.com", userRoles: "Admin", phoneNumber: "5551234567", fullname: "Vendor E2E Admin", id: "1", }; const activeVendors = [ { Id: 1, CompanyName: "Gateway Plumbing", ContactName: "Adam Whyte", Email: "dispatch@gateway.test", Phone: "314-555-0198", CompanyPhone: "314-555-0100", Address: "1 Market St", City: "St. Louis", State: "MO", Zip: "63101", TradeSpecialties: "Plumbing, Backflow Preventers", GoogleMapsUrl: "https://maps.google.com/gateway", Notes: "Preferred after-hours vendor", TotalJobs: 72, IsActive: true, }, { Id: 2, CompanyName: "Metro HVAC", ContactName: "Maya Chen", Email: "maya@metro.test", Phone: "314-555-0112", CompanyPhone: "314-555-0110", Address: "2 Pine St", City: "Clayton", State: "MO", Zip: "63105", TradeSpecialties: "HVAC", GoogleMapsUrl: "", Notes: "", TotalJobs: 18, IsActive: true, }, ]; const inactiveVendors = [ { ...activeVendors[0], Id: 3, CompanyName: "Drain Cleaning Bros", ContactName: "Drew Banks", TotalJobs: 151, IsActive: false, }, ]; interface MockState { listUrls: string[]; createdBody?: Record; updatedBody?: Record; deletedId?: string; } async function fulfillJson(route: Route, body: unknown, status = 200) { await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) }); } async function captureVisual(page: Page, path: string) { if (!process.env.VENDOR_VISUAL_CAPTURE) return; await page.waitForTimeout(400); await page.screenshot({ path, fullPage: true }); } async function mockVendorApi(page: Page, options: { listError?: boolean } = {}) { const state: MockState = { listUrls: [] }; await page.addInitScript((user) => localStorage.setItem("auth", JSON.stringify(user)), TEST_USER); await page.route("**/api/DropdownOptions/ByCategory/Trade*", (route) => fulfillJson(route, { data: [ { id: 1, value: "Plumbing" }, { id: 2, value: "HVAC" }, ], }), ); await page.route("**/api/Vendor/facets*", (route) => fulfillJson(route, { data: { companies: activeVendors.map((vendor) => ({ name: vendor.CompanyName, companyPhone: vendor.CompanyPhone, email: vendor.Email, address: vendor.Address, city: vendor.City, state: vendor.State, zip: vendor.Zip, googleMapsUrl: vendor.GoogleMapsUrl, })), trades: ["Plumbing", "Backflow Preventers", "HVAC"], locations: [ { city: "St. Louis", state: "MO", label: "St. Louis, MO" }, { city: "Clayton", state: "MO", label: "Clayton, MO" }, ], jobBuckets: [ { id: "under-50", label: "Under 50" }, { id: "50-99", label: "50–99" }, { id: "100-149", label: "100–149" }, { id: "150-plus", label: "150+" }, ], }, }), ); await page.route("**/api/Vendor/GetVendorList*", async (route) => { const url = new URL(route.request().url()); state.listUrls.push(url.toString()); if (options.listError && url.searchParams.get("pageSize") !== "1") { await fulfillJson(route, { message: "Vendor directory unavailable" }, 500); return; } const isActive = url.searchParams.get("isActive") !== "false"; const source = isActive ? activeVendors : inactiveVendors; const search = (url.searchParams.get("search") ?? "").toLowerCase(); const company = url.searchParams.get("companies[0]"); const filtered = source.filter( (vendor) => (!search || `${vendor.CompanyName} ${vendor.ContactName} ${vendor.TradeSpecialties}` .toLowerCase() .includes(search)) && (!company || vendor.CompanyName === company), ); const isCount = url.searchParams.get("pageSize") === "1"; await fulfillJson(route, { Data: isCount ? filtered.slice(0, 1) : filtered, TotalCount: filtered.length, PageNumber: 1, PageSize: Number(url.searchParams.get("pageSize") ?? 12), }); }); await page.route(/\/api\/vendors\/\d+$/, async (route) => { const id = route.request().url().split("/").pop() ?? ""; if (route.request().method() === "PUT") { state.updatedBody = route.request().postDataJSON(); await fulfillJson(route, { data: { ...activeVendors[0], ...state.updatedBody, Id: Number(id) }, }); return; } if (route.request().method() === "DELETE") { state.deletedId = id; await fulfillJson(route, { status: "200" }); return; } const vendor = [...activeVendors, ...inactiveVendors].find((item) => String(item.Id) === id); await fulfillJson(route, { data: vendor }); }); await page.route(/\/api\/vendors\/?$/, async (route) => { if (route.request().method() !== "POST") { await route.fallback(); return; } state.createdBody = route.request().postDataJSON(); await fulfillJson(route, { data: { ...state.createdBody, id: 99, totalJobs: 0 } }); }); return state; } test.describe("Vendor directory prototype parity", () => { test("covers status tabs, global counts, search, facets, table fields, and filtered footer", async ({ page, }) => { const state = await mockVendorApi(page); await page.goto("/vendors"); await expect(page.getByRole("heading", { name: "Vendors / Technicians" })).toBeVisible(); await expect(page.getByText("3 technicians · 2 active")).toBeVisible(); await expect(page.getByRole("tab", { name: "Active (2)" })).toHaveAttribute( "aria-selected", "true", ); for (const heading of [ "Company", "Technician", "Location", "Trade", "Phone", "Email", "Total Jobs", "Status", ]) { await expect(page.getByRole("columnheader", { name: heading })).toBeVisible(); } await expect(page.getByText("Gateway Plumbing")).toBeVisible(); await expect(page.getByText("+1")).toBeVisible(); await expect(page.getByText("2 active technicians · 2 active · 1 inactive")).toBeVisible(); await captureVisual(page, "/tmp/seahaven-vendor-implemented-list.png"); await page.getByPlaceholder("Search name, company, trade...").fill("metro"); await expect(page.getByText("Metro HVAC")).toBeVisible(); await expect(page.getByText(/1 active technician · filtered/)).toBeVisible(); expect(state.listUrls.some((url) => new URL(url).searchParams.get("search") === "metro")).toBe( true, ); await page.getByPlaceholder("Search name, company, trade...").clear(); await page.getByRole("button", { name: "Open vendor filters" }).click(); await expect(page.getByRole("heading", { name: "Filters" })).toBeVisible(); await captureVisual(page, "/tmp/seahaven-vendor-implemented-filters.png"); await page.getByRole("button", { name: /Company/ }).click(); await page.getByPlaceholder("All companies").click(); await page.getByRole("option", { name: /Gateway Plumbing/ }).click(); await page.getByRole("button", { name: "Apply" }).click(); await expect(page.getByText("Gateway Plumbing").first()).toBeVisible(); await expect(page.getByText(/1 active technician · filtered/)).toBeVisible(); expect( state.listUrls.some( (url) => new URL(url).searchParams.get("companies[0]") === "Gateway Plumbing", ), ).toBe(true); await page.getByRole("button", { name: "Deactivate vendor" }).first().click(); await expect(page.getByRole("dialog", { name: "Deactivate Vendor" })).toContainText( "history will be preserved", ); await page.getByRole("button", { name: "Deactivate", exact: true }).click(); await expect.poll(() => state.deletedId).toBe("1"); await page.getByRole("tab", { name: "Inactive (1)" }).click(); await expect(page.getByText("No vendors match the current filters.")).toBeVisible(); await page.getByRole("button", { name: "Clear all" }).click(); await expect(page.getByText("Drain Cleaning Bros")).toBeVisible(); await expect(page.getByRole("button", { name: "Deactivate vendor" })).toHaveCount(0); }); test("creates with company autofill, then views and edits every detail field", async ({ page, }) => { const state = await mockVendorApi(page); await page.goto("/vendors"); await page.getByRole("button", { name: "+ Add Vendor" }).click(); await expect(page.getByText("Register a technician and their vendor company.")).toBeVisible(); await captureVisual(page, "/tmp/seahaven-vendor-implemented-add.png"); await page.getByRole("button", { name: "Add Vendor" }).last().click(); await expect(page.getByText("Technician name is required")).toBeVisible(); await expect(page.getByText("Company is required")).toBeVisible(); await page.getByLabel("Technician Name").fill("New Technician"); await page.getByRole("combobox", { name: "Company" }).click(); await page.getByRole("option", { name: "Gateway Plumbing" }).click(); await expect(page.getByLabel("Company Phone")).toHaveValue("314-555-0100"); await expect(page.getByLabel("Email")).toHaveValue("dispatch@gateway.test"); await expect(page.getByLabel("Address")).toHaveValue("1 Market St"); await expect(page.getByLabel("Google Maps URL")).toHaveValue("https://maps.google.com/gateway"); await page.getByRole("combobox", { name: "Add Trade" }).fill("Plumbing"); await page.getByRole("button", { name: "Add trade" }).click(); await page.getByRole("combobox", { name: "Add Trade" }).fill("HVAC"); await page.getByRole("button", { name: "Add trade" }).click(); await page.getByRole("button", { name: "Move HVAC up" }).click(); await page.getByLabel("Notes").fill("Created in browser E2E"); await page.getByRole("button", { name: "Add Vendor" }).last().click(); await expect(page.getByRole("dialog", { name: "Add Vendor" })).toHaveCount(0); expect(state.createdBody).toMatchObject({ companyName: "Gateway Plumbing", contactName: "New Technician", companyPhone: "314-555-0100", tradeSpecialties: "HVAC, Plumbing", notes: "Created in browser E2E", }); await page.getByRole("button", { name: "View vendor Gateway Plumbing" }).click(); const detailDrawer = page.locator(".MuiDrawer-paper").last(); await expect(detailDrawer.getByRole("heading", { name: "Gateway Plumbing" })).toBeVisible(); await expect(detailDrawer.getByText("Adam Whyte")).toBeVisible(); await expect(detailDrawer.getByText("72")).toBeVisible(); await expect( detailDrawer.getByRole("link", { name: "Open in Google Maps" }).first(), ).toHaveAttribute("target", "_blank"); await captureVisual(page, "/tmp/seahaven-vendor-implemented-detail.png"); await detailDrawer.getByRole("button", { name: "Edit" }).click(); await expect(detailDrawer.getByLabel("Total Jobs")).toHaveValue("72"); await expect(detailDrawer.getByLabel("Total Jobs")).toHaveAttribute("readonly"); await detailDrawer.getByLabel("Notes").fill("Updated in browser E2E"); await detailDrawer.getByRole("switch", { name: "Active" }).click(); await detailDrawer.getByRole("button", { name: "Save" }).click(); await expect.poll(() => state.updatedBody?.notes).toBe("Updated in browser E2E"); expect(state.updatedBody?.isActive).toBe(false); }); test("supports keyboard dismissal and free-text company creation", async ({ page }) => { const state = await mockVendorApi(page); await page.goto("/vendors"); const addButton = page.getByRole("button", { name: "+ Add Vendor" }); await addButton.focus(); await page.keyboard.press("Enter"); await expect(page.getByRole("dialog", { name: /Add Vendor/ })).toBeVisible(); await page.keyboard.press("Escape"); await expect(page.getByRole("dialog", { name: /Add Vendor/ })).toHaveCount(0); await expect(addButton).toBeFocused(); await addButton.click(); await page.getByLabel("Technician Name").fill("Independent Tech"); await page.getByRole("combobox", { name: "Company" }).fill("Independent Vendor LLC"); await page.getByRole("button", { name: "Add Vendor" }).last().click(); await expect.poll(() => state.createdBody?.companyName).toBe("Independent Vendor LLC"); }); test("shows API failure and keeps drawers usable at a mobile viewport", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await mockVendorApi(page, { listError: true }); await page.goto("/vendors"); await expect(page.getByRole("main").getByRole("alert")).toContainText( /Vendor directory unavailable|failed|server error/i, { timeout: 15_000 }, ); await page.getByRole("button", { name: "Open vendor filters" }).click(); const filterDrawer = page.locator(".MuiDrawer-paper").filter({ hasText: "Total Jobs" }); await expect(filterDrawer).toBeVisible(); const box = await filterDrawer.boundingBox(); expect(box?.width).toBeLessThanOrEqual(391); await captureVisual(page, "/tmp/seahaven-vendor-implemented-mobile.png"); await page.getByRole("button", { name: "Close filters" }).click(); }); });