mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 03:23:12 +00:00
605 lines
24 KiB
TypeScript
605 lines
24 KiB
TypeScript
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,
|
||
CompanyId: 101,
|
||
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,
|
||
PreferredContact: "Email",
|
||
},
|
||
{
|
||
Id: 2,
|
||
CompanyId: 102,
|
||
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 });
|
||
}
|
||
|
||
async function mockVendorApi(
|
||
page: Page,
|
||
options: {
|
||
listError?: boolean;
|
||
deactivationBlocked?: boolean;
|
||
deleteConflict?: boolean;
|
||
legacyGateway?: boolean;
|
||
} = {},
|
||
) {
|
||
const state: MockState = { listUrls: [] };
|
||
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";
|
||
}
|
||
|
||
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) => ({
|
||
companyId: vendor.CompanyId,
|
||
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 = vendorRecords.filter((vendor) => vendor.IsActive === isActive);
|
||
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} ${vendor.City} ${vendor.State}`
|
||
.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\/vendor-company-roster(?:\/\d+)?(?:\?.*)?$/, async (route) => {
|
||
const request = route.request();
|
||
const url = new URL(request.url());
|
||
const pathCompanyId = url.pathname.match(/vendor-company-roster\/(\d+)$/)?.[1];
|
||
|
||
if (request.method() === "POST") {
|
||
state.createdBody = request.postDataJSON();
|
||
await fulfillJson(route, {
|
||
...state.createdBody,
|
||
companyId: 999,
|
||
rowVersion: "rv-created",
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (request.method() === "PUT" && pathCompanyId) {
|
||
state.updatedBody = request.postDataJSON();
|
||
const technicians = Array.isArray(state.updatedBody.technicians)
|
||
? (state.updatedBody.technicians as Array<Record<string, unknown>>)
|
||
: [];
|
||
for (const technician of technicians) {
|
||
const vendor = vendorRecords.find((item) => String(item.Id) === String(technician.id));
|
||
if (vendor && typeof technician.isActive === "boolean") {
|
||
vendor.IsActive = technician.isActive;
|
||
}
|
||
}
|
||
await fulfillJson(route, {
|
||
...state.updatedBody,
|
||
companyId: Number(pathCompanyId),
|
||
rowVersion: "rv-updated",
|
||
});
|
||
return;
|
||
}
|
||
|
||
const vendorId = url.searchParams.get("vendorId");
|
||
const requestedCompanyId = url.searchParams.get("companyId") ?? pathCompanyId;
|
||
const anchor = vendorId
|
||
? vendorRecords.find((vendor) => String(vendor.Id) === vendorId)
|
||
: vendorRecords.find((vendor) => String(vendor.CompanyId) === requestedCompanyId);
|
||
if (!anchor) {
|
||
await fulfillJson(route, { message: "Vendor roster not found" }, 404);
|
||
return;
|
||
}
|
||
await fulfillJson(route, {
|
||
companyId: anchor.CompanyId,
|
||
rowVersion: "rv-1",
|
||
name: anchor.CompanyName,
|
||
companyPhone: anchor.CompanyPhone,
|
||
email: anchor.Email,
|
||
address: anchor.Address,
|
||
city: anchor.City,
|
||
state: anchor.State,
|
||
zip: anchor.Zip,
|
||
googleMapsUrl: anchor.GoogleMapsUrl,
|
||
notes: anchor.Notes,
|
||
technicians: vendorRecords
|
||
.filter((vendor) => vendor.CompanyId === anchor.CompanyId)
|
||
.map((vendor) => ({
|
||
id: vendor.Id,
|
||
contactName: vendor.ContactName,
|
||
phone: vendor.Phone,
|
||
email: vendor.Email,
|
||
preferredContact: vendor.PreferredContact ?? "Phone",
|
||
tradeSpecialties: vendor.TradeSpecialties,
|
||
isActive: vendor.IsActive,
|
||
totalJobs: vendor.TotalJobs,
|
||
})),
|
||
});
|
||
});
|
||
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,
|
||
},
|
||
]
|
||
: [],
|
||
},
|
||
});
|
||
});
|
||
await page.route(/\/api\/vendors\/\d+\/portal-token$/, (route) =>
|
||
fulfillJson(route, {
|
||
data: {
|
||
portalUrl: "https://vendor.example.test/token",
|
||
issuedAt: "2026-07-31T12:00:00Z",
|
||
expiresAt: "2026-08-07T12:00:00Z",
|
||
},
|
||
}),
|
||
);
|
||
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();
|
||
const vendor = vendorRecords.find((item) => String(item.Id) === id);
|
||
if (options.legacyGateway && vendor && typeof state.updatedBody?.isActive === "boolean") {
|
||
vendor.IsActive = state.updatedBody.isActive;
|
||
}
|
||
await fulfillJson(route, {
|
||
data: { ...vendor, ...state.updatedBody, Id: Number(id) },
|
||
});
|
||
return;
|
||
}
|
||
if (route.request().method() === "DELETE") {
|
||
if (options.deleteConflict) {
|
||
await fulfillJson(
|
||
route,
|
||
{ message: "Vendor has open work orders and cannot be deactivated." },
|
||
409,
|
||
);
|
||
return;
|
||
}
|
||
state.deletedId = id;
|
||
const vendor = vendorRecords.find((item) => String(item.Id) === id);
|
||
if (options.legacyGateway && vendor) vendor.IsActive = false;
|
||
await fulfillJson(route, { status: "200" });
|
||
return;
|
||
}
|
||
const vendor = vendorRecords.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,
|
||
}) => {
|
||
await page.setViewportSize({ width: 1440, height: 1000 });
|
||
const state = await mockVendorApi(page);
|
||
await page.goto("/vendors");
|
||
|
||
const heading = page.getByRole("heading", { name: "Vendors / Technicians" });
|
||
await expect(heading).toBeVisible();
|
||
await expect(heading).toHaveCSS("font-family", /Montserrat/);
|
||
await expect(heading).toHaveCSS("font-size", "30px");
|
||
await expect(heading).toHaveCSS("font-weight", "800");
|
||
await expect(heading).toHaveCSS("line-height", "33px");
|
||
await expect(heading).toHaveCSS("letter-spacing", "-0.6px");
|
||
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",
|
||
]) {
|
||
await expect(page.getByRole("columnheader", { name: heading })).toBeVisible();
|
||
}
|
||
await expect(page.getByRole("columnheader", { name: "Status" })).toBeVisible();
|
||
const activeTabBox = await page.getByRole("tab", { name: "Active (2)" }).boundingBox();
|
||
expect(activeTabBox).toMatchObject({ x: 256, y: 159.5, height: 36 });
|
||
const tableContainerBox = await page.locator("table").locator("..").boundingBox();
|
||
expect(tableContainerBox).toMatchObject({ x: 260, y: 212.5, width: 1164 });
|
||
expect(tableContainerBox?.height).toBeCloseTo(738, 0);
|
||
expect((await page.locator("table").boundingBox())?.width).toBe(1550);
|
||
expect((await page.getByRole("columnheader", { name: "Company" }).boundingBox())?.height).toBe(
|
||
47,
|
||
);
|
||
expect((await page.locator("tbody tr").first().boundingBox())?.height).toBe(52);
|
||
await expect(page.getByText("Gateway Plumbing")).toBeVisible();
|
||
await expect(page.getByText("+1")).toBeVisible();
|
||
await expect(page.getByText("2 active technicians")).toBeVisible();
|
||
await expect(page.getByText("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…").fill("clayton");
|
||
await expect(page.getByText("Metro HVAC")).toBeVisible();
|
||
|
||
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 filters" }).click();
|
||
await expect(page.getByText("Gateway Plumbing").first()).toBeVisible();
|
||
await expect(page.getByText("1 technician · filtered by")).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("tab", { name: "Inactive (1)" }).click();
|
||
await expect(page.getByText("No inactive technicians found")).toBeVisible();
|
||
await page.getByRole("button", { name: "Clear all" }).click();
|
||
await expect(page.getByText("Drain Cleaning Bros")).toBeVisible();
|
||
await expect(page.getByRole("columnheader", { name: "Status" })).toBeVisible();
|
||
await expect(page.getByRole("button", { name: "Deactivate vendor" })).toHaveCount(0);
|
||
});
|
||
|
||
test("adds a technician to an existing company, then views and edits the aggregate", async ({
|
||
page,
|
||
}) => {
|
||
const state = await mockVendorApi(page);
|
||
await page.goto("/vendors");
|
||
|
||
await page.getByRole("button", { name: "Add Vendor", exact: true }).click();
|
||
await expect(page.getByText("Register a vendor company and its technicians.")).toBeVisible();
|
||
await captureVisual(page, "/tmp/seahaven-vendor-implemented-add.png");
|
||
await expect(
|
||
page.getByRole("button", { name: "Add Vendor", exact: true }).last(),
|
||
).toBeEnabled();
|
||
|
||
await page.getByRole("combobox", { name: "Company" }).click();
|
||
await page.getByRole("option", { name: "Gateway Plumbing" }).click();
|
||
await expect(page.getByLabel("Company Phone (optional)")).toHaveValue("314-555-0100");
|
||
await expect(page.getByRole("textbox", { name: "Email (optional)", exact: true })).toHaveValue(
|
||
"dispatch@gateway.test",
|
||
);
|
||
await expect(page.getByLabel("Address (optional)")).toHaveValue("1 Market St");
|
||
await expect(page.getByLabel("Google Maps URL (optional)")).toHaveValue(
|
||
"https://maps.google.com/gateway",
|
||
);
|
||
await expect(page.getByLabel("Preferred Contact")).toHaveCount(0);
|
||
await page.getByRole("button", { name: "Add technician" }).click();
|
||
await page.getByLabel("Technician name (optional)").last().fill("New Technician");
|
||
const tradeInput = page.getByRole("combobox", { name: "Add Trade" }).last();
|
||
await tradeInput.fill("HVAC");
|
||
await tradeInput.press("Enter");
|
||
await tradeInput.fill("Plumbing");
|
||
await tradeInput.press("Enter");
|
||
await page.getByRole("button", { name: "Move Plumbing up" }).click();
|
||
await page.getByLabel("Notes (optional)").fill("Created in browser E2E");
|
||
await page.getByRole("button", { name: "Add Vendor", exact: true }).last().click();
|
||
await expect(page.getByRole("dialog", { name: /Add Vendor/ })).toHaveCount(0);
|
||
expect(state.updatedBody).toMatchObject({
|
||
name: "Gateway Plumbing",
|
||
companyPhone: "(314) 555-0100",
|
||
notes: "Created in browser E2E",
|
||
rowVersion: "rv-1",
|
||
});
|
||
expect(state.updatedBody?.technicians).toEqual(
|
||
expect.arrayContaining([
|
||
expect.objectContaining({
|
||
contactName: "New Technician",
|
||
tradeSpecialties: "Plumbing, HVAC",
|
||
}),
|
||
]),
|
||
);
|
||
const submittedTechnicians = Array.isArray(state.updatedBody?.technicians)
|
||
? (state.updatedBody.technicians as Array<Record<string, unknown>>)
|
||
: [];
|
||
const newTechnician = submittedTechnicians.find(
|
||
(technician) => technician.contactName === "New Technician",
|
||
);
|
||
expect(newTechnician?.preferredContact).toBeUndefined();
|
||
|
||
await page.getByRole("button", { name: "View vendor Gateway Plumbing" }).click();
|
||
const detailDrawer = page.locator(".MuiDrawer-paper").last();
|
||
await expect(detailDrawer.getByRole("heading", { name: "Adam Whyte" })).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(page).toHaveURL(/\/vendors$/);
|
||
await detailDrawer.getByLabel("Notes (optional)").fill("Updated in browser E2E");
|
||
await detailDrawer.getByRole("button", { name: "Save changes" }).click();
|
||
await expect.poll(() => state.updatedBody?.notes).toBe("Updated in browser E2E");
|
||
});
|
||
|
||
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: "Edit vendor Gateway Plumbing" }).click();
|
||
await page.getByRole("switch", { name: "Active status" }).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: "Edit vendor Gateway Plumbing" }).click();
|
||
const detailDrawer = page.locator(".MuiDrawer-paper").last();
|
||
await detailDrawer.getByRole("switch", { name: "Active status" }).click();
|
||
await detailDrawer.getByRole("button", { name: "Save changes" }).click();
|
||
await expect.poll(() => state.updatedBody).toEqual({ isActive: true });
|
||
await detailDrawer.getByRole("button", { name: "Close drawer" }).click();
|
||
await page.getByRole("tab", { name: "Active (2)", exact: true }).click();
|
||
await expect(page.getByText("Gateway Plumbing").first()).toBeVisible();
|
||
});
|
||
|
||
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", exact: true });
|
||
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 expect(
|
||
page.getByRole("button", { name: "Add Vendor", exact: true }).last(),
|
||
).toBeEnabled();
|
||
await page.getByRole("combobox", { name: "Company" }).fill("Independent Vendor LLC");
|
||
await page.getByLabel("Company Phone (optional)").fill("3145550199");
|
||
await page.getByRole("button", { name: "Add Vendor", exact: true }).last().click();
|
||
|
||
await expect.poll(() => state.createdBody?.name).toBe("Independent Vendor LLC");
|
||
expect(state.createdBody?.technicians).toEqual([]);
|
||
});
|
||
|
||
test("closes vendor details with Escape and keeps only Edit in the footer", async ({ page }) => {
|
||
await mockVendorApi(page);
|
||
await page.goto("/vendors");
|
||
|
||
await page.getByRole("button", { name: "View vendor Gateway Plumbing" }).click();
|
||
const detailDrawer = page.locator(".MuiDrawer-paper").last();
|
||
await expect(detailDrawer).toBeVisible();
|
||
await expect(detailDrawer.getByRole("button", { name: "Edit" })).toBeVisible();
|
||
await expect(detailDrawer.getByRole("button", { name: "Maps" })).toHaveCount(0);
|
||
|
||
await page.keyboard.press("Escape");
|
||
await expect(page.getByRole("button", { name: "Close drawer" })).toHaveCount(0);
|
||
});
|
||
|
||
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: "Edit vendor Gateway Plumbing" }).click();
|
||
await page.getByRole("switch", { name: "Active status" }).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: "Edit vendor Gateway Plumbing" }).click();
|
||
await page.getByRole("switch", { name: "Active status" }).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();
|
||
});
|
||
|
||
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");
|
||
|
||
const mainBox = await page.getByRole("main").boundingBox();
|
||
expect(mainBox).toMatchObject({ x: 0, width: 390 });
|
||
const navigation = page.getByRole("navigation", { name: "Primary navigation" });
|
||
await expect(navigation).not.toBeVisible();
|
||
|
||
const openNavigation = page.getByRole("button", { name: "Toggle navigation" });
|
||
await openNavigation.focus();
|
||
await page.keyboard.press("Enter");
|
||
await expect(navigation).toBeVisible();
|
||
await expect(page.getByRole("link", { name: "Sites" })).toBeVisible();
|
||
await page.keyboard.press("Escape");
|
||
await expect(navigation).not.toBeVisible();
|
||
await expect(openNavigation).toBeFocused();
|
||
|
||
await expect(page.getByRole("main").getByRole("alert")).toContainText(
|
||
/Vendor directory unavailable|failed|server error/i,
|
||
{ timeout: 15_000 },
|
||
);
|
||
await captureVisual(page, "/tmp/seahaven-vendor-implemented-mobile-list.png");
|
||
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();
|
||
});
|
||
|
||
test("keeps protected shell destinations available and marks their selected state", async ({
|
||
page,
|
||
}) => {
|
||
await mockVendorApi(page);
|
||
|
||
for (const destination of [
|
||
{ path: "/workorders", label: "Work Orders" },
|
||
{ path: "/accounts", label: "Accounts" },
|
||
{ path: "/locations", label: "Sites" },
|
||
{ path: "/notifications", label: "Notifications" },
|
||
]) {
|
||
await page.goto(destination.path);
|
||
await expect(page.getByRole("link", { name: destination.label })).toHaveAttribute(
|
||
"aria-current",
|
||
"page",
|
||
);
|
||
}
|
||
});
|
||
});
|