237 lines
9.6 KiB
TypeScript
237 lines
9.6 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" });
|
|
await expect(deniedPage.getByText("403 Forbidden", { exact: true }).first()).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");
|
|
});
|
|
});
|