Files
pleno-vue/tests/e2e/superuser-products-layout.spec.ts
T

406 lines
12 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { API_HOST, seedAuthenticatedState } from "./support/network.js";
import { isDesktopProject } from "./support/projects";
const json = (body: unknown, status = 200) => ({
status,
contentType: "application/json",
body: JSON.stringify(body),
});
type ProductRouteOptions = {
token?: string;
products: unknown[];
categories?: unknown;
productOptions?: unknown;
economicProducts?: unknown;
onProductsRequest?: (url: URL) => void;
};
const collectBrowserErrors = (page: Page) => {
const pageErrors: string[] = [];
const consoleErrors: string[] = [];
page.on("pageerror", (error) => {
pageErrors.push(error.stack || error.message);
});
page.on("console", (message) => {
if (message.type() === "error" && !message.text().includes("Invalid version check response")) {
consoleErrors.push(message.text());
}
});
return { pageErrors, consoleErrors };
};
const stubSuperuserProductRoutes = async (
page: Page,
{
token = "superuser-products-layout-token",
products,
categories = [],
productOptions = [],
economicProducts = [],
onProductsRequest,
}: ProductRouteOptions
) => {
await seedAuthenticatedState(page, token);
await page.route(API_HOST, async (route) => {
const request = route.request();
const url = new URL(request.url());
const pathname = url.pathname;
const method = request.method();
if (pathname.endsWith("/auth/session") && method === "GET") {
await route.fulfill(
json({
data: {
id: 1,
customer_number: 12345,
group_id: 1,
email: "e2e@example.com",
phone: {
number: "12345678",
country_code: 45,
},
notifications: {
wash_certificate_email: null,
email_notifications_enabled: true,
sms_notifications_enabled: false,
},
display_name: "E2E User",
permissions: ["superuser", "user"],
economic_customer: [],
runtime_config: {
economic: {
transaction_draft_customer_number: null,
},
},
},
})
);
return;
}
if (
(pathname.endsWith("/auth/recaptcha/pre-check") || pathname.endsWith("/auth/reCAPTCHA/public")) &&
method === "GET"
) {
await route.fulfill(
json({
data: {
recaptcha: {
enabled: false,
site_key: "",
},
rate_limit: {
enabled: false,
limit: 0,
remaining: 0,
reset: 0,
warning: null,
},
},
})
);
return;
}
if (pathname.endsWith("/ping") && method === "GET") {
await route.fulfill(
json({
data: {
ok: true,
},
})
);
return;
}
if (pathname.endsWith("/worker/version") && method === "GET") {
await route.fulfill(
json({
data: {
version: "cc0b238d",
},
})
);
return;
}
if (pathname.endsWith("/departments") && method === "GET") {
await route.fulfill(
json({
data: [{ id: 1, name: "Hvidovre", visible: true }],
})
);
return;
}
if (pathname.endsWith("/categories") && method === "GET") {
await route.fulfill(json({ data: categories }));
return;
}
if (pathname.endsWith("/product/options") && method === "GET") {
await route.fulfill(json({ data: productOptions }));
return;
}
if (pathname.endsWith("/economic/products") && method === "GET") {
await route.fulfill(json({ data: economicProducts }));
return;
}
if (pathname.endsWith("/products") && method === "GET") {
onProductsRequest?.(url);
const perPage = Number(url.searchParams.get("limit") || "100");
await route.fulfill(
json({
data: products,
meta: {
pagination: {
page: 1,
per_page: perPage,
total: products.length,
},
},
})
);
return;
}
await route.fulfill(json({ data: [] }));
});
};
test.describe("Superuser products layout", () => {
test("fits a normal desktop viewport and exposes secondary product fields behind details rows", async ({
page,
}, testInfo) => {
test.skip(!isDesktopProject(testInfo), "Desktop only");
const { pageErrors, consoleErrors } = collectBrowserErrors(page);
const products = [
{
id: 1,
order_priority: 40,
name: "Trækker",
description:
"Trækkende køretøj med en lang beskrivelse der tidligere skubbede hele products-tabellen ud over normale desktopskærme.",
price: 579,
subscription_allowed: true,
category: 8,
piktogram: "01",
economic_product_id: 101,
apply_category_discount: true,
requires_note: false,
is_wash: true,
display_in_booking_form: true,
},
{
id: 2,
order_priority: 50,
name: "Trailer",
description: "Standard trailer-vask.",
price: 599,
subscription_allowed: true,
category: 8,
piktogram: "02",
economic_product_id: 102,
apply_category_discount: true,
requires_note: false,
is_wash: true,
display_in_booking_form: true,
},
{
id: 3,
order_priority: 60,
name: "Undervognsskyl pr. enhed",
description: "Tilvalg til undervognsskyl.",
price: 89,
subscription_allowed: false,
category: 8,
piktogram: "03",
economic_product_id: 103,
apply_category_discount: false,
requires_note: false,
is_wash: true,
display_in_booking_form: false,
},
{
id: 4,
order_priority: 70,
name: "Fælg Flex pr. enhed",
description: "Tilvalg til fælgvask.",
price: 129,
subscription_allowed: false,
category: 8,
piktogram: "04",
economic_product_id: 104,
apply_category_discount: false,
requires_note: true,
is_wash: true,
display_in_booking_form: false,
},
];
const categories = [{ id: 8, name: "Udvendig" }];
const productOptions = [
{
id: 9001,
product_id: "1",
option_id: "3",
name: "Undervognsskyl pr. enhed",
min: 0,
max: 1,
},
{
id: 9002,
product_id: "1",
option_id: "4",
name: "Fælg Flex pr. enhed",
min: 0,
max: 1,
},
];
const economicProducts = [
{ id: 101, name: "Trækker (01)" },
{ id: 102, name: "Trailer (02)" },
{ id: 103, name: "Undervognsskyl (03)" },
{ id: 104, name: "Fælg Flex (04)" },
];
await page.setViewportSize({ width: 1280, height: 720 });
const productRequestUrls: string[] = [];
await stubSuperuserProductRoutes(page, {
products,
categories,
productOptions,
economicProducts,
onProductsRequest: (url) => productRequestUrls.push(url.toString()),
});
await page.goto("/superuser/products");
const table = page.getByTestId("superuser-products-table");
const row = page.getByTestId("products-table-row-1");
const detailsToggle = page.getByTestId("products-table-row-toggle-1");
const detailsRow = page.getByTestId("products-table-row-details-1");
await expect(table).toBeVisible();
await expect(row).toContainText("Trækker");
await expect(row).toContainText("579");
await expect(row).toContainText("Udvendig");
await expect(row).toContainText("Ja");
const pageOverflow = await page.evaluate(() => {
const doc = document.documentElement;
return {
documentScrollWidth: doc.scrollWidth,
documentClientWidth: doc.clientWidth,
bodyScrollWidth: document.body.scrollWidth,
};
});
expect(pageOverflow.documentScrollWidth).toBeLessThanOrEqual(pageOverflow.documentClientWidth + 1);
expect(pageOverflow.bodyScrollWidth).toBeLessThanOrEqual(pageOverflow.documentClientWidth + 1);
await detailsToggle.click();
await expect(detailsRow).toBeVisible();
await expect(detailsRow).toContainText("Trækkende køretøj");
await expect(detailsRow).toContainText("Trækker (01)");
await expect(detailsRow).toContainText("Undervognsskyl pr. enhed");
await expect(detailsRow).toContainText("Fælg Flex pr. enhed");
const actionTrigger = row.locator("button.action-settings-wheel-trigger").first();
await expect(actionTrigger).toBeVisible();
await actionTrigger.click();
await expect(page.locator(".dropdown.is-active .dropdown-content")).toContainText(/produkt/i);
await page.goto("/superuser/products/1");
await expect(page.getByTestId("superuser-product-name")).toContainText("Trækker");
await expect(page.getByRole("heading", { name: "Trækker" })).toBeVisible();
const detailRequest = productRequestUrls
.map((url) => new URL(url))
.find((url) => url.searchParams.get("id") === "1");
expect(detailRequest).toBeDefined();
expect(detailRequest?.searchParams.get("department_id")).toBeNull();
expect(detailRequest?.searchParams.get("customer_id")).toBeNull();
expect(detailRequest?.searchParams.get("category_id")).toBeNull();
expect(detailRequest?.searchParams.get("final_price")).toBe("false");
expect(pageErrors).toEqual([]);
expect(consoleErrors).toEqual([]);
});
test("keeps product details open when related lookup responses are malformed", async ({ page }, testInfo) => {
test.skip(!isDesktopProject(testInfo), "Desktop only");
const { pageErrors, consoleErrors } = collectBrowserErrors(page);
const products = [
{
id: 5,
order_priority: 10,
name: "Crash test product",
description: "Malformed lookup product",
price: 399,
subscription_allowed: false,
category: 8,
piktogram: "05",
economic_product_id: 101,
apply_category_discount: false,
requires_note: false,
is_wash: true,
display_in_booking_form: false,
},
];
await page.setViewportSize({ width: 1280, height: 720 });
await stubSuperuserProductRoutes(page, {
token: "superuser-products-malformed-lookups-token",
products,
categories: [{ id: 8, name: "Udvendig" }],
productOptions: {
items: [{ id: 9003, product_id: 5, option_id: 6, name: "Ignored malformed add-on" }],
},
economicProducts: {
items: [{ id: 101, name: "Ignored malformed economic product" }],
},
});
await page.goto("/superuser/products");
const row = page.getByTestId("products-table-row-5");
const detailsToggle = page.getByTestId("products-table-row-toggle-5");
const detailsRow = page.getByTestId("products-table-row-details-5");
await expect(row).toContainText("Crash test product");
const productOptionsResponse = page.waitForResponse((response) => {
const url = new URL(response.url());
return response.request().method() === "GET" && url.pathname.endsWith("/product/options");
});
const economicProductsResponse = page.waitForResponse((response) => {
const url = new URL(response.url());
return response.request().method() === "GET" && url.pathname.endsWith("/economic/products");
});
await detailsToggle.click();
await Promise.all([productOptionsResponse, economicProductsResponse]);
await expect(detailsRow).toBeVisible();
await expect(detailsRow).toContainText("Malformed lookup product");
await expect(detailsRow).toContainText(/Ingen data|No data/i);
expect(pageErrors).toEqual([]);
expect(consoleErrors).toEqual([]);
});
});