2026-07-23 15:23:03 -03:00
|
|
|
|
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,
|
2026-07-23 17:36:21 -03:00
|
|
|
|
CompanyId: 101,
|
2026-07-23 15:23:03 -03:00
|
|
|
|
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,
|
2026-07-23 17:36:21 -03:00
|
|
|
|
PreferredContact: "Email",
|
2026-07-23 15:23:03 -03:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
Id: 2,
|
2026-07-23 17:36:21 -03:00
|
|
|
|
CompanyId: 102,
|
2026-07-23 15:23:03 -03:00
|
|
|
|
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<string, unknown>;
|
|
|
|
|
|
updatedBody?: Record<string, unknown>;
|
|
|
|
|
|
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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 17:36:21 -03:00
|
|
|
|
async function mockVendorApi(
|
|
|
|
|
|
page: Page,
|
2026-07-28 17:15:42 -03:00
|
|
|
|
options: {
|
|
|
|
|
|
listError?: boolean;
|
|
|
|
|
|
deactivationBlocked?: boolean;
|
|
|
|
|
|
deleteConflict?: boolean;
|
|
|
|
|
|
legacyGateway?: boolean;
|
|
|
|
|
|
} = {},
|
2026-07-23 17:36:21 -03:00
|
|
|
|
) {
|
2026-07-23 15:23:03 -03:00
|
|
|
|
const state: MockState = { listUrls: [] };
|
2026-07-28 17:15:42 -03:00
|
|
|
|
const vendorRecords = [...activeVendors, ...inactiveVendors].map((vendor) => ({ ...vendor }));
|
|
|
|
|
|
if (options.legacyGateway) {
|
|
|
|
|
|
vendorRecords[0].Phone = "555-1234 ext 9";
|
|
|
|
|
|
vendorRecords[0].GoogleMapsUrl = "http://maps.example.test/legacy";
|
|
|
|
|
|
}
|
2026-07-23 15:23:03 -03:00
|
|
|
|
|
|
|
|
|
|
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) => ({
|
2026-07-23 17:36:21 -03:00
|
|
|
|
companyId: vendor.CompanyId,
|
2026-07-23 15:23:03 -03:00
|
|
|
|
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";
|
2026-07-28 17:15:42 -03:00
|
|
|
|
const source = vendorRecords.filter((vendor) => vendor.IsActive === isActive);
|
2026-07-23 15:23:03 -03:00
|
|
|
|
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),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-07-23 17:36:21 -03:00
|
|
|
|
await page.route(/\/api\/vendors\/\d+\/deactivation-impact$/, async (route) => {
|
|
|
|
|
|
const vendorId = Number(route.request().url().split("/").at(-2));
|
|
|
|
|
|
await fulfillJson(route, {
|
|
|
|
|
|
data: {
|
|
|
|
|
|
vendorId,
|
|
|
|
|
|
canDeactivate: !options.deactivationBlocked,
|
|
|
|
|
|
openWorkOrders: options.deactivationBlocked
|
|
|
|
|
|
? [
|
|
|
|
|
|
{
|
|
|
|
|
|
workOrderId: 501,
|
|
|
|
|
|
workOrderNumber: "WO-501",
|
|
|
|
|
|
workOrderTitle: "Emergency boiler repair",
|
|
|
|
|
|
status: "Dispatched",
|
|
|
|
|
|
scheduledDate: "2026-07-24T14:00:00Z",
|
|
|
|
|
|
dispatchId: 801,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
: [],
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-07-23 15:23:03 -03:00
|
|
|
|
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();
|
2026-07-28 17:15:42 -03:00
|
|
|
|
const vendor = vendorRecords.find((item) => String(item.Id) === id);
|
2026-07-28 17:22:42 -03:00
|
|
|
|
if (options.legacyGateway && vendor && typeof state.updatedBody?.isActive === "boolean") {
|
2026-07-28 17:15:42 -03:00
|
|
|
|
vendor.IsActive = state.updatedBody.isActive;
|
|
|
|
|
|
}
|
2026-07-23 15:23:03 -03:00
|
|
|
|
await fulfillJson(route, {
|
2026-07-28 17:15:42 -03:00
|
|
|
|
data: { ...vendor, ...state.updatedBody, Id: Number(id) },
|
2026-07-23 15:23:03 -03:00
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (route.request().method() === "DELETE") {
|
2026-07-23 17:36:21 -03:00
|
|
|
|
if (options.deleteConflict) {
|
|
|
|
|
|
await fulfillJson(
|
|
|
|
|
|
route,
|
|
|
|
|
|
{ message: "Vendor has open work orders and cannot be deactivated." },
|
|
|
|
|
|
409,
|
|
|
|
|
|
);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-23 15:23:03 -03:00
|
|
|
|
state.deletedId = id;
|
2026-07-28 17:15:42 -03:00
|
|
|
|
const vendor = vendorRecords.find((item) => String(item.Id) === id);
|
2026-07-28 17:22:42 -03:00
|
|
|
|
if (options.legacyGateway && vendor) vendor.IsActive = false;
|
2026-07-23 15:23:03 -03:00
|
|
|
|
await fulfillJson(route, { status: "200" });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-28 17:15:42 -03:00
|
|
|
|
const vendor = vendorRecords.find((item) => String(item.Id) === id);
|
2026-07-23 15:23:03 -03:00
|
|
|
|
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");
|
2026-07-23 17:36:21 -03:00
|
|
|
|
await expect(page.getByRole("button", { name: "Add Vendor" }).last()).toBeDisabled();
|
2026-07-23 15:23:03 -03:00
|
|
|
|
|
|
|
|
|
|
await page.getByLabel("Technician Name").fill("New Technician");
|
|
|
|
|
|
await page.getByRole("combobox", { name: "Company" }).click();
|
|
|
|
|
|
await page.getByRole("option", { name: "Gateway Plumbing" }).click();
|
2026-07-23 19:17:14 -03:00
|
|
|
|
await expect(page.getByLabel("Company Phone")).toHaveValue("314-555-0100");
|
2026-07-23 17:36:21 -03:00
|
|
|
|
await expect(page.getByRole("textbox", { name: "Email" })).toHaveValue("dispatch@gateway.test");
|
2026-07-23 15:23:03 -03:00
|
|
|
|
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");
|
2026-07-23 17:36:21 -03:00
|
|
|
|
await page.getByRole("button", { name: "Text" }).click();
|
|
|
|
|
|
await expect(page.getByRole("button", { name: "Text" })).toHaveAttribute(
|
|
|
|
|
|
"aria-pressed",
|
|
|
|
|
|
"true",
|
|
|
|
|
|
);
|
2026-07-23 15:23:03 -03:00
|
|
|
|
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",
|
2026-07-23 17:36:21 -03:00
|
|
|
|
companyId: 101,
|
2026-07-23 15:23:03 -03:00
|
|
|
|
contactName: "New Technician",
|
2026-07-23 17:36:21 -03:00
|
|
|
|
companyPhone: "(314) 555-0100",
|
|
|
|
|
|
preferredContact: "Text",
|
2026-07-23 15:23:03 -03:00
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-28 17:15:42 -03:00
|
|
|
|
test("reactivates a legacy inactive vendor with an exact sparse update", async ({ page }) => {
|
|
|
|
|
|
const state = await mockVendorApi(page, { legacyGateway: true });
|
|
|
|
|
|
await page.goto("/vendors");
|
|
|
|
|
|
|
|
|
|
|
|
await page.getByRole("button", { name: "Deactivate vendor" }).first().click();
|
|
|
|
|
|
await page.getByRole("button", { name: "Deactivate", exact: true }).click();
|
|
|
|
|
|
await expect.poll(() => state.deletedId).toBe("1");
|
|
|
|
|
|
|
|
|
|
|
|
await page.getByRole("tab", { name: "Inactive (2)" }).click();
|
|
|
|
|
|
await page.getByRole("button", { name: "View vendor Gateway Plumbing" }).click();
|
|
|
|
|
|
const detailDrawer = page.locator(".MuiDrawer-paper").last();
|
|
|
|
|
|
await detailDrawer.getByRole("button", { name: "Edit" }).click();
|
|
|
|
|
|
await expect(detailDrawer.getByRole("button", { name: "Save" })).toBeDisabled();
|
|
|
|
|
|
|
|
|
|
|
|
await detailDrawer.getByRole("switch", { name: "Inactive" }).click();
|
|
|
|
|
|
await detailDrawer.getByRole("button", { name: "Save" }).click();
|
|
|
|
|
|
await expect.poll(() => state.updatedBody).toEqual({ isActive: true });
|
|
|
|
|
|
await expect(detailDrawer.getByText("Active").first()).toBeVisible();
|
|
|
|
|
|
|
|
|
|
|
|
await detailDrawer.getByRole("button", { name: "Close drawer" }).click();
|
|
|
|
|
|
await page.getByRole("tab", { name: "Active (2)" }).click();
|
|
|
|
|
|
await expect(page.getByText("Gateway Plumbing").first()).toBeVisible();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-23 15:23:03 -03:00
|
|
|
|
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();
|
2026-07-23 17:36:21 -03:00
|
|
|
|
await expect(page.getByRole("button", { name: "Add Vendor" }).last()).toBeDisabled();
|
2026-07-23 15:23:03 -03:00
|
|
|
|
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");
|
2026-07-23 17:36:21 -03:00
|
|
|
|
expect(state.createdBody?.companyId).toBeNull();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("blocks deactivation for linked work orders and preserves the vendor on a raced 409", async ({
|
|
|
|
|
|
page,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const blockedState = await mockVendorApi(page, { deactivationBlocked: true });
|
|
|
|
|
|
await page.goto("/vendors");
|
|
|
|
|
|
|
|
|
|
|
|
await page.getByRole("button", { name: "Deactivate vendor" }).first().click();
|
|
|
|
|
|
const blockedDialog = page.getByRole("dialog", { name: "Deactivate Vendor" });
|
|
|
|
|
|
await expect(blockedDialog).toContainText("WO-501 — Emergency boiler repair");
|
|
|
|
|
|
await expect(blockedDialog.getByRole("button", { name: /^Deactivate$/ })).toBeDisabled();
|
|
|
|
|
|
expect(blockedState.deletedId).toBeUndefined();
|
|
|
|
|
|
|
|
|
|
|
|
await blockedDialog.getByRole("button", { name: "Cancel" }).click();
|
|
|
|
|
|
await page.unrouteAll({ behavior: "wait" });
|
|
|
|
|
|
|
|
|
|
|
|
const racedState = await mockVendorApi(page, { deleteConflict: true });
|
|
|
|
|
|
await page.reload();
|
|
|
|
|
|
await page.getByRole("button", { name: "Deactivate vendor" }).first().click();
|
|
|
|
|
|
await page
|
|
|
|
|
|
.getByRole("dialog", { name: "Deactivate Vendor" })
|
|
|
|
|
|
.getByRole("button", {
|
|
|
|
|
|
name: /^Deactivate$/,
|
|
|
|
|
|
})
|
|
|
|
|
|
.click();
|
|
|
|
|
|
|
|
|
|
|
|
await expect(page.getByRole("dialog", { name: "Deactivate Vendor" })).toContainText(
|
|
|
|
|
|
/open work orders|conflict/i,
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(racedState.deletedId).toBeUndefined();
|
|
|
|
|
|
await expect(page.getByText("Gateway Plumbing").first()).toBeVisible();
|
2026-07-23 15:23:03 -03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|