Files
pleno-vue/tests/e2e/superuser-customer-rules.spec.ts
T
Jeppe B eee9ba1c13 Fix localized forbidden assertions in full E2E (#243)
Use the stable localized forbidden-page contract in customer, subuser, and superuser denial tests while retaining the protected-action and zero-request assertions.
2026-08-02 12:24:36 +02:00

239 lines
9.7 KiB
TypeScript

import { expect, test, type Page, type Route } from "@playwright/test";
import { mockApi, primeMockSession } from "./support/network.js";
import { isDesktopProject } from "./support/projects";
test.use({ serviceWorkers: "block" });
const json = (body: unknown, status = 200) => ({
status,
contentType: "application/json",
body: JSON.stringify(body),
});
const products = [
{ id: 23, name: "RO rinse", category_name: "Exterior" },
{ id: 24, name: "Premium rinse", category_name: "Exterior" },
{ id: 63, name: "Interior add-on", category_name: "Add-ons" },
{ id: 91, name: "Extra detergent", category_name: "Additional services", is_active: false },
];
const productRuleAttributes = [
"restrictAdditionalServices",
"restrictTankCleaning",
"restrictSpotFree",
"restrictInteriorCleaning",
"onlyTankCleaning",
];
type Collection = {
id: number;
name: string;
sort_order: number;
product_ids: number[];
};
type Rule = {
attribute: string;
version: number;
collections: Collection[];
disabled_product_ids: number[];
};
const createInitialRules = (): Rule[] =>
productRuleAttributes.map((attribute, index) => ({
attribute,
version: 1,
collections: [
{
id: index + 5,
name: attribute === "restrictSpotFree" ? "Legacy rinses" : "Legacy migration",
sort_order: 0,
product_ids: attribute === "restrictSpotFree" ? [23] : [],
},
],
disabled_product_ids: attribute === "restrictSpotFree" ? [23] : [],
}));
const restrictionApiPattern = /\/superuser\/customer-rules\/product-restrictions(?:\/([^/?#]+))?(?:[?#].*)?$/i;
const setup = async (
page: Page,
permissions: string[],
initialPutMode: "success" | "conflict" | "invalid" = "success",
groupId = 2
) => {
await page.addInitScript(() => window.localStorage.setItem("locale", "en"));
await mockApi(page, {
authenticated: true,
permissions,
sessionData: { group_id: groupId },
});
await primeMockSession(page, { token: "customer-rule-configuration-token", bootPath: null });
let rules = createInitialRules();
let nextCollectionId = 100;
let putMode = initialPutMode;
let getRequests = 0;
const putPayloads: Array<Record<string, unknown>> = [];
await page.route(restrictionApiPattern, async (route: Route) => {
const method = route.request().method();
const match = new URL(route.request().url()).pathname.match(restrictionApiPattern);
const attribute = match?.[1] ? decodeURIComponent(match[1]) : null;
if (method === "GET" && !attribute) {
getRequests += 1;
await route.fulfill(json({ success: true, data: { rules, products } }));
return;
}
if (method !== "PUT" || !attribute) {
await route.fallback();
return;
}
const payload = route.request().postDataJSON() as {
version: number;
collections: Array<Partial<Collection>>;
};
putPayloads.push(payload as unknown as Record<string, unknown>);
if (putMode === "conflict") {
putMode = "success";
await route.fulfill(json({ success: false, message: "Stale version" }, 409));
return;
}
if (putMode === "invalid") {
putMode = "success";
await route.fulfill(json({ success: false, message: "Invalid product membership" }, 422));
return;
}
const current = rules.find((rule) => rule.attribute === attribute);
const collections = payload.collections.map((collection, index) => ({
id: Number(collection.id) > 0 ? Number(collection.id) : nextCollectionId++,
name: String(collection.name),
sort_order: index,
product_ids: (collection.product_ids || []).map(Number),
}));
const saved: Rule = {
attribute,
version: (current?.version ?? payload.version) + 1,
collections,
disabled_product_ids: [...new Set(collections.flatMap((collection) => collection.product_ids))],
};
rules = rules.map((rule) => (rule.attribute === attribute ? saved : rule));
await route.fulfill(json({ success: true, data: saved }));
});
return {
getRequests: () => getRequests,
putPayloads,
setPutMode: (mode: "success" | "conflict" | "invalid") => {
putMode = mode;
},
};
};
test.describe("Superuser customer rule product restrictions", () => {
test("creates, renames, deletes, and persists exact product collections", async ({ page }, testInfo) => {
test.skip(!isDesktopProject(testInfo), "Desktop-focused global configuration coverage.");
const api = await setup(page, ["superuser", "superuser_customer_rules_view", "superuser_customer_rules_manage"]);
await page.goto("/superuser/customer-rules", { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("superuser-customer-rules-page")).toBeVisible();
await expect(page.getByTestId("customer-rules-global-warning")).toBeVisible();
await expect.poll(() => api.getRequests()).toBeGreaterThan(0);
const rule = page.getByTestId("customer-rule-config-restrictSpotFree");
await expect(rule).toBeVisible();
await expect(rule.getByTestId("customer-rule-collection-restrictSpotFree-7")).toBeVisible();
await rule.getByTestId("customer-rule-collection-name-restrictSpotFree-7").fill("Premium rinses");
await rule.getByTestId("customer-rule-product-restrictSpotFree-7-24").check();
await rule.getByTestId("customer-rule-add-collection-restrictSpotFree").click();
await rule.getByTestId("customer-rule-collection-name-restrictSpotFree--1").fill("Archived services");
await rule.getByTestId("customer-rule-product-restrictSpotFree--1-91").check();
await rule.getByTestId("customer-rule-collection-move-up-restrictSpotFree--1").click();
await expect(rule).toContainText("3 disabled products");
await rule.getByTestId("customer-rule-save-restrictSpotFree").click();
await expect.poll(() => api.putPayloads.length).toBe(1);
expect(api.putPayloads[0]).toEqual({
version: 1,
collections: [
{ name: "Archived services", sort_order: 0, product_ids: [91] },
{ id: 7, name: "Premium rinses", sort_order: 1, product_ids: [23, 24] },
],
});
await expect(rule).toContainText("Version 2");
await rule.getByTestId("customer-rule-collection-delete-restrictSpotFree-100").click();
await rule.getByTestId("customer-rule-save-restrictSpotFree").click();
await expect.poll(() => api.putPayloads.length).toBe(2);
await page.reload({ waitUntil: "domcontentloaded" });
const reloadedRule = page.getByTestId("customer-rule-config-restrictSpotFree");
await expect(reloadedRule.getByTestId("customer-rule-collection-name-restrictSpotFree-7")).toHaveValue(
"Premium rinses"
);
await expect(reloadedRule.getByTestId("customer-rule-product-restrictSpotFree-7-24")).toBeChecked();
await expect(reloadedRule.getByTestId("customer-rule-collection-restrictSpotFree-100")).toHaveCount(0);
await expect(reloadedRule).toContainText("2 disabled products");
});
test("separates view-only, root-superuser, and denied access", async ({ page }, testInfo) => {
test.skip(!isDesktopProject(testInfo), "Desktop-focused global configuration coverage.");
await setup(page, ["superuser", "superuser_customer_rules_view"]);
await page.goto("/superuser/customer-rules", { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("customer-rules-read-only")).toBeVisible();
await expect(page.getByTestId("customer-rule-collection-name-restrictSpotFree-7")).toBeDisabled();
await expect(page.getByTestId("customer-rule-add-collection-restrictSpotFree")).toHaveCount(0);
const rootPage = await page.context().newPage();
await setup(rootPage, ["superuser"], "success", 1);
await rootPage.goto("/superuser/customer-rules", { waitUntil: "domcontentloaded" });
await expect(rootPage.getByTestId("customer-rule-collection-name-restrictSpotFree-7")).toBeEnabled();
await expect(rootPage.getByTestId("customer-rule-add-collection-restrictSpotFree")).toBeVisible();
await rootPage.close();
const deniedPage = await page.context().newPage();
const deniedApi = await setup(deniedPage, ["superuser"]);
await deniedPage.goto("/superuser/customer-rules", { waitUntil: "domcontentloaded" });
const forbidden = deniedPage.getByTestId("restricted-forbidden");
await expect(forbidden).toBeVisible();
await expect(forbidden.getByRole("heading", { name: "403" })).toBeVisible();
expect(deniedApi.getRequests()).toBe(0);
await deniedPage.close();
});
test("preserves drafts on conflicts and validation failures until an explicit reload", async ({ page }, testInfo) => {
test.skip(!isDesktopProject(testInfo), "Desktop-focused global configuration coverage.");
const api = await setup(
page,
["superuser", "superuser_customer_rules_view", "superuser_customer_rules_manage"],
"conflict"
);
await page.goto("/superuser/customer-rules", { waitUntil: "domcontentloaded" });
const name = page.getByTestId("customer-rule-collection-name-restrictSpotFree-7");
await name.fill("Conflicting draft");
await page.getByTestId("customer-rule-save-restrictSpotFree").click();
await expect(page.getByTestId("customer-rule-conflict-restrictSpotFree")).toBeVisible();
await expect(name).toHaveValue("Conflicting draft");
await expect(page.getByTestId("customer-rule-save-restrictSpotFree")).toBeDisabled();
await page.getByTestId("customer-rule-conflict-restrictSpotFree").getByRole("button").click();
await expect(name).toHaveValue("Legacy rinses");
api.setPutMode("invalid");
await name.fill("Invalid draft");
await page.getByTestId("customer-rule-save-restrictSpotFree").click();
await expect(page.getByTestId("customer-rule-save-error-restrictSpotFree")).toContainText(
"Invalid product membership"
);
await expect(name).toHaveValue("Invalid draft");
});
});