shoc-frontend-new/e2e/vendors/vendors.spec.ts

677 lines
27 KiB
TypeScript
Raw Normal View History

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>;
patchedBody?: Record<string, unknown>;
patchedCompanyId?: string;
deletedId?: string;
deleteConfirmedOpenWorkOrders?: boolean;
}
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() === "PATCH" && pathCompanyId) {
state.patchedBody = request.postDataJSON();
state.patchedCompanyId = pathCompanyId;
const anchor = vendorRecords.find((vendor) => String(vendor.CompanyId) === pathCompanyId);
if (!anchor) {
await fulfillJson(route, { message: "Vendor roster not found" }, 404);
return;
}
const added = Array.isArray(state.patchedBody.addTechnicians)
? (state.patchedBody.addTechnicians as Array<Record<string, unknown>>)
: [];
await fulfillJson(route, {
companyId: anchor.CompanyId,
rowVersion: "rv-patched",
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,
})),
...added.map((technician, index) => ({
id: 900 + index,
totalJobs: 0,
...technician,
})),
],
});
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 requestUrl = new URL(route.request().url());
const id = requestUrl.pathname.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") {
state.deleteConfirmedOpenWorkOrders =
requestUrl.searchParams.get("confirmOpenWorkOrders") === "true";
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 expect(page.getByLabel("Technician name (optional)")).toHaveCount(1);
await expect(page.getByLabel("Technician name (optional)")).toHaveValue("");
await expect(
page.getByRole("dialog", { name: /Add Vendor/ }).getByText("Adam Whyte"),
).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.patchedCompanyId).toBe("101");
expect(state.patchedBody).toMatchObject({ rowVersion: "rv-1" });
// Only the field the user actually typed into is transmitted. Address/phone/email were
// populated by selecting the company and never edited, so they must not be sent — otherwise a
// concurrent edit by another user to any of them would be silently reverted on resubmit.
expect(state.patchedBody?.companyFields).toEqual({ notes: "Created in browser E2E" });
expect(state.patchedBody?.companyFields).not.toHaveProperty("companyPhone");
expect(state.patchedBody?.companyFields).not.toHaveProperty("address");
expect(state.patchedBody?.companyFields).not.toHaveProperty("email");
expect(state.patchedBody?.companyFields).not.toHaveProperty("name");
expect(state.patchedBody?.addTechnicians).toEqual([
expect.objectContaining({
contactName: "New Technician",
tradeSpecialties: "Plumbing, HVAC",
}),
]);
const patchedTechnicians = Array.isArray(state.patchedBody?.addTechnicians)
? (state.patchedBody.addTechnicians as Array<Record<string, unknown>>)
: [];
const newTechnician = patchedTechnicians.find(
(technician) => technician.contactName === "New Technician",
);
expect(newTechnician?.preferredContact).toBeUndefined();
expect(patchedTechnicians.some((technician) => technician.contactName === "Adam Whyte")).toBe(
false,
);
expect(state.updatedBody).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("confirms deactivation past linked work orders and preserves the vendor on a raced 409", async ({
page,
}) => {
const confirmState = 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 dialog = page.getByRole("dialog", { name: "Deactivate this vendor?" });
await expect(dialog).toContainText("It still has 1 open work order");
await expect(
dialog.getByRole("link", { name: /WO-501 — Emergency boiler repair/ }),
).toHaveAttribute("href", "/workorders/501");
// SH-254: the open work orders inform the decision, they no longer block it.
const confirm = dialog.getByRole("button", { name: "Deactivate anyway" });
await expect(confirm).toBeEnabled();
await confirm.click();
await expect.poll(() => confirmState.deletedId).toBe("1");
expect(confirmState.deleteConfirmedOpenWorkOrders).toBe(true);
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 this vendor?" })
.getByRole("button", {
name: /^Deactivate$/,
})
.click();
await expect(page.getByRole("dialog", { name: "Deactivate this 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",
);
}
});
});