mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +00:00
* fix(vendors): add accessible trade overflow tooltip * fix(vendors): add accessible trade overflow tooltip * refactor(vendors): satisfy row maintainability gate * feat(vendors): group directory rows by company * chore(vendors): trigger SH-281 review checks * test(vendors): align company directory visuals * test(vendors): complete company visual baselines * fix(vendors): keep empty states in viewport * test(vendors): align centered empty baseline * fix(vendors): use grouped directory endpoint * test(vendors): follow grouped directory endpoint * test(vendors): refresh combined visual snapshots --------- Co-authored-by: Codex Review Integration <codex-review@local.invalid>
274 lines
8.9 KiB
TypeScript
274 lines
8.9 KiB
TypeScript
import { expect, test, type Page, type Route } from "@playwright/test";
|
||
|
||
const TEST_USER = {
|
||
token: "vendor-visual-token",
|
||
expiration: "2030-01-01T00:00:00.000Z",
|
||
email: "admin@seahavenind.com",
|
||
userRoles: "Admin",
|
||
phoneNumber: "5551234567",
|
||
fullname: "Vendor Visual 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,
|
||
CompanyId: 103,
|
||
CompanyName: "Drain Cleaning Bros",
|
||
ContactName: "Drew Banks",
|
||
TotalJobs: 151,
|
||
IsActive: false,
|
||
},
|
||
];
|
||
|
||
async function fulfillJson(route: Route, body: unknown, status = 200) {
|
||
await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) });
|
||
}
|
||
|
||
async function mockVendorApi(page: Page, mode: "default" | "empty" | "error" = "default") {
|
||
await page.clock.setFixedTime(new Date("2026-08-10T12:00:00.000Z"));
|
||
await page.addInitScript((user) => localStorage.setItem("auth", JSON.stringify(user)), TEST_USER);
|
||
|
||
// The vendor address map preview embeds maps.google.com in an iframe. Its
|
||
// content depends on the network, on timing, and on whatever Google serves
|
||
// that minute, none of which belongs in a test with "deterministic pixel
|
||
// regression" in its name. Aborting the request pins the frame to a blank
|
||
// box, so the shot measures our layout and nothing else.
|
||
await page.route(/https:\/\/(www\.)?google\.com\/maps/, (route) => route.abort());
|
||
|
||
await page.route("**/api/**", async (route) => {
|
||
const request = route.request();
|
||
const url = new URL(request.url());
|
||
|
||
if (!url.pathname.startsWith("/api/")) {
|
||
await route.continue();
|
||
return;
|
||
}
|
||
|
||
if (url.pathname.includes("/DropdownOptions/ByCategory/Trade")) {
|
||
await fulfillJson(route, {
|
||
data: [
|
||
{ id: 1, value: "Plumbing" },
|
||
{ id: 2, value: "HVAC" },
|
||
],
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (url.pathname.endsWith("/api/Vendor/facets")) {
|
||
await fulfillJson(route, {
|
||
data: {
|
||
companies: activeVendors.map((vendor) => ({
|
||
companyId: vendor.CompanyId,
|
||
name: vendor.CompanyName,
|
||
})),
|
||
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+" },
|
||
],
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (url.pathname.endsWith("/api/Vendor/GetVendorDirectoryList")) {
|
||
const isCount = url.searchParams.get("pageSize") === "1";
|
||
if (mode === "error" && !isCount) {
|
||
await fulfillJson(route, { message: "Vendor directory unavailable" }, 500);
|
||
return;
|
||
}
|
||
const source =
|
||
mode === "empty"
|
||
? []
|
||
: url.searchParams.get("isActive") === "false"
|
||
? inactiveVendors
|
||
: activeVendors;
|
||
await fulfillJson(route, {
|
||
Data: isCount ? source.slice(0, 1) : source,
|
||
TotalCount: source.length,
|
||
PageNumber: 1,
|
||
PageSize: Number(url.searchParams.get("pageSize") ?? 12),
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (url.pathname.startsWith("/api/vendor-company-roster")) {
|
||
await fulfillJson(route, {
|
||
companyId: 101,
|
||
rowVersion: "rv-visual",
|
||
name: "Gateway Plumbing",
|
||
companyPhone: "314-555-0100",
|
||
email: "dispatch@gateway.test",
|
||
address: "1 Market St",
|
||
city: "St. Louis",
|
||
state: "MO",
|
||
zip: "63101",
|
||
googleMapsUrl: "https://maps.google.com/gateway",
|
||
notes: "Preferred after-hours vendor",
|
||
technicians: [
|
||
{
|
||
id: 1,
|
||
contactName: "Adam Whyte",
|
||
phone: "314-555-0198",
|
||
email: "dispatch@gateway.test",
|
||
preferredContact: "Email",
|
||
tradeSpecialties: "Plumbing, Backflow Preventers",
|
||
isActive: true,
|
||
totalJobs: 72,
|
||
},
|
||
],
|
||
});
|
||
return;
|
||
}
|
||
|
||
await fulfillJson(route, { data: {} });
|
||
});
|
||
}
|
||
|
||
async function openVendorPage(page: Page, mode: "default" | "empty" | "error" = "default") {
|
||
await mockVendorApi(page, mode);
|
||
await page.goto("/vendors");
|
||
await page.evaluate(() => document.fonts.ready);
|
||
await page.addStyleTag({
|
||
content: `
|
||
*, *::before, *::after { transition: none !important; animation: none !important; }
|
||
.MuiDrawer-paper { transform: none !important; }
|
||
.Toastify__toast-container { display: none !important; }
|
||
.tsqd-parent-container { display: none !important; }
|
||
`,
|
||
});
|
||
}
|
||
|
||
async function expectStableScreenshot(page: Page, name: string) {
|
||
await page.waitForTimeout(250);
|
||
await page.evaluate(
|
||
() =>
|
||
new Promise<void>((resolve) =>
|
||
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
|
||
),
|
||
);
|
||
await page.screenshot({ animations: "disabled", caret: "hide" });
|
||
await expect(page).toHaveScreenshot(name);
|
||
}
|
||
|
||
test.describe("Vendor deterministic pixel regression", () => {
|
||
test("list", async ({ page }) => {
|
||
await openVendorPage(page);
|
||
await expect(page.getByText("Gateway Plumbing")).toBeVisible();
|
||
await expectStableScreenshot(page, "vendor-list.png");
|
||
});
|
||
|
||
test("filter", async ({ page }) => {
|
||
await openVendorPage(page);
|
||
await page.getByRole("button", { name: "Open vendor filters" }).click();
|
||
await expect(page.getByRole("heading", { name: "Filters" })).toBeVisible();
|
||
await expectStableScreenshot(page, "vendor-filter.png");
|
||
});
|
||
|
||
test("add", async ({ page }) => {
|
||
await openVendorPage(page);
|
||
await page.getByRole("button", { name: "Add Vendor", exact: true }).click();
|
||
await expect(page.getByRole("dialog", { name: /Add Vendor/ })).toBeVisible();
|
||
await expectStableScreenshot(page, "vendor-add.png");
|
||
});
|
||
|
||
test("detail", async ({ page }) => {
|
||
await openVendorPage(page);
|
||
await page.getByRole("button", { name: "View vendor Gateway Plumbing" }).click();
|
||
await expect(page.getByRole("button", { name: "Close drawer" })).toBeVisible();
|
||
await expectStableScreenshot(page, "vendor-detail.png");
|
||
});
|
||
|
||
test("edit", async ({ page }) => {
|
||
await openVendorPage(page);
|
||
await page.getByRole("button", { name: "Edit vendor Gateway Plumbing" }).click();
|
||
await expect(page.getByRole("button", { name: "Save changes" })).toBeVisible();
|
||
await expectStableScreenshot(page, "vendor-edit.png");
|
||
});
|
||
|
||
test("inactive", async ({ page }) => {
|
||
await openVendorPage(page);
|
||
await page.getByRole("tab", { name: "Inactive (1)" }).click();
|
||
await expect(page.getByText("Drain Cleaning Bros")).toBeVisible();
|
||
await expectStableScreenshot(page, "vendor-inactive.png");
|
||
});
|
||
|
||
test("empty", async ({ page }) => {
|
||
await openVendorPage(page, "empty");
|
||
const emptyState = page.getByText("No active companies found");
|
||
await expect(emptyState).toBeVisible();
|
||
await expectStableScreenshot(page, "vendor-empty.png");
|
||
|
||
await page.setViewportSize({ width: 390, height: 844 });
|
||
await expect(emptyState).toBeInViewport();
|
||
});
|
||
|
||
test("error", async ({ page }) => {
|
||
await openVendorPage(page, "error");
|
||
await expect(page.getByRole("main").getByRole("alert")).toContainText(
|
||
/server error|vendor directory unavailable/i,
|
||
{ timeout: 15_000 },
|
||
);
|
||
await expect(page.getByRole("progressbar")).toHaveCount(0);
|
||
await expectStableScreenshot(page, "vendor-error.png");
|
||
});
|
||
|
||
test("mobile", async ({ page }) => {
|
||
await page.setViewportSize({ width: 390, height: 844 });
|
||
await openVendorPage(page);
|
||
await expect(page.getByText("Gateway Plumbing")).toBeVisible();
|
||
await expectStableScreenshot(page, "vendor-mobile.png");
|
||
|
||
await page.getByRole("button", { name: "Toggle navigation" }).click();
|
||
await expect(page.getByRole("navigation", { name: "Primary navigation" })).toBeVisible();
|
||
await expectStableScreenshot(page, "vendor-mobile-navigation.png");
|
||
});
|
||
});
|