359 lines
14 KiB
TypeScript
359 lines
14 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, primeMockSession } from "./support/network.js";
|
|
|
|
function json(body: unknown, status = 200) {
|
|
return {
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
};
|
|
}
|
|
|
|
async function primeSuperuserSession(page) {
|
|
const token = "superuser-customer-complaints-token";
|
|
await primeMockSession(page, { token });
|
|
}
|
|
|
|
async function selectComplaintAction(page, complaintId: number, actionTestId: string) {
|
|
const actionWheel = page.getByTestId(`superuser-complaint-actions-${complaintId}`);
|
|
await actionWheel.locator("button[aria-haspopup='true']").click();
|
|
await page.getByTestId(actionTestId).click();
|
|
}
|
|
|
|
function complaintWashDateInput(page) {
|
|
return page.getByTestId("superuser-complaint-edit-wash-date").locator("input");
|
|
}
|
|
|
|
async function setComplaintDescription(page, value: string) {
|
|
const description = page.getByTestId("superuser-complaint-edit-description");
|
|
await expect(description).toBeVisible();
|
|
await description.evaluate((element, nextValue) => {
|
|
element.value = nextValue;
|
|
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
}, value);
|
|
await expect(description).toHaveValue(value);
|
|
}
|
|
|
|
function parseFilters(filters: string | null) {
|
|
return (filters || "")
|
|
.split(",")
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean)
|
|
.reduce<Record<string, string>>((accumulator, entry) => {
|
|
const [key, ...valueParts] = entry.split(":");
|
|
accumulator[key] = valueParts.join(":");
|
|
return accumulator;
|
|
}, {});
|
|
}
|
|
|
|
async function installComplaintMocks(page) {
|
|
const requestLog = {
|
|
getUrls: [] as string[],
|
|
customerLookupQueries: [] as string[],
|
|
updateBodies: [] as Array<Record<string, unknown>>,
|
|
deleteIds: [] as number[],
|
|
};
|
|
|
|
const complaints = [
|
|
{
|
|
id: 101,
|
|
department_id: 1,
|
|
department_name: "Copenhagen",
|
|
customer_number: 12345,
|
|
customer_name: "Acme Transport",
|
|
wash_date: "2026-04-06",
|
|
category: "damage_mirrors",
|
|
description: "Broken mirror after wash",
|
|
created_by: 7,
|
|
created_by_name: "Jeppe",
|
|
created_at: "2026-04-07 09:00:00",
|
|
},
|
|
{
|
|
id: 102,
|
|
department_id: 2,
|
|
department_name: "Odense",
|
|
customer_number: null,
|
|
customer_name: null,
|
|
wash_date: null,
|
|
category: null,
|
|
description: "Late service response",
|
|
created_by: 8,
|
|
created_by_name: "Anna",
|
|
created_at: "2026-04-08 10:30:00",
|
|
},
|
|
];
|
|
|
|
await page.route(/\/departments\/daily-reports\/complaints\/customers(?:\?.*)?$/i, async (route) => {
|
|
const url = new URL(route.request().url());
|
|
const search = (url.searchParams.get("search") || "").trim().toLowerCase();
|
|
requestLog.customerLookupQueries.push(search);
|
|
|
|
let matches = [
|
|
{ customer_number: 12345, customer_name: "Acme Transport" },
|
|
{ customer_number: 45678, customer_name: "Updated Customer" },
|
|
];
|
|
|
|
if (search !== "") {
|
|
matches = matches.filter((customer) =>
|
|
`${customer.customer_name} ${customer.customer_number}`.toLowerCase().includes(search)
|
|
);
|
|
}
|
|
|
|
await route.fulfill(json({ success: true, data: matches }));
|
|
});
|
|
|
|
await page.route(/\/departments\/daily-reports\/complaints(?:\?.*)?$/i, async (route) => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
const method = request.method().toUpperCase();
|
|
|
|
if (method === "GET") {
|
|
requestLog.getUrls.push(request.url());
|
|
|
|
const id = Number.parseInt(url.searchParams.get("id") || "", 10);
|
|
if (Number.isFinite(id) && id > 0) {
|
|
const complaint = complaints.find((item) => item.id === id) || null;
|
|
await route.fulfill(json({ success: true, data: complaint }));
|
|
return;
|
|
}
|
|
|
|
const filters = parseFilters(url.searchParams.get("filters"));
|
|
const search = (url.searchParams.get("search") || "").trim().toLowerCase();
|
|
const pageNumber = Math.max(1, Number.parseInt(url.searchParams.get("page") || "1", 10) || 1);
|
|
const limit = Math.max(1, Number.parseInt(url.searchParams.get("limit") || "100", 10) || 100);
|
|
const order = url.searchParams.get("order") || "created_at:desc";
|
|
const [orderBy = "created_at", orderDirection = "desc"] = order.split(":");
|
|
|
|
let rows = [...complaints];
|
|
|
|
if (filters.department_id) {
|
|
rows = rows.filter((complaint) => String(complaint.department_id) === filters.department_id);
|
|
}
|
|
|
|
if (search !== "") {
|
|
rows = rows.filter((complaint) =>
|
|
[
|
|
complaint.description,
|
|
complaint.customer_number,
|
|
complaint.customer_name,
|
|
complaint.wash_date,
|
|
complaint.category,
|
|
complaint.department_name,
|
|
complaint.created_by_name,
|
|
complaint.id,
|
|
]
|
|
.filter((value) => value !== null && value !== undefined)
|
|
.some((value) => String(value).toLowerCase().includes(search))
|
|
);
|
|
}
|
|
|
|
rows.sort((left, right) => {
|
|
const leftValue = String((left as Record<string, unknown>)[orderBy] || "");
|
|
const rightValue = String((right as Record<string, unknown>)[orderBy] || "");
|
|
const comparison = leftValue.localeCompare(rightValue);
|
|
return orderDirection.toLowerCase() === "asc" ? comparison : comparison * -1;
|
|
});
|
|
|
|
const startIndex = (pageNumber - 1) * limit;
|
|
const paginatedRows = rows.slice(startIndex, startIndex + limit);
|
|
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: paginatedRows,
|
|
meta: {
|
|
pagination: {
|
|
page: pageNumber,
|
|
per_page: limit,
|
|
total: rows.length,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (method === "PUT") {
|
|
const body = request.postDataJSON() as Record<string, unknown>;
|
|
requestLog.updateBodies.push(body);
|
|
|
|
const complaint = complaints.find((item) => item.id === Number(body.id));
|
|
if (!complaint) {
|
|
await route.fulfill(json({ message: "Complaint not found" }, 404));
|
|
return;
|
|
}
|
|
|
|
complaint.department_id = Number(body.department_id);
|
|
complaint.department_name = complaint.department_id === 2 ? "Odense" : "Copenhagen";
|
|
complaint.customer_number = body.customer_number === null ? null : Number(body.customer_number);
|
|
complaint.customer_name = complaint.customer_number ? "Updated Customer" : null;
|
|
complaint.wash_date = body.wash_date === null ? null : String(body.wash_date || "");
|
|
complaint.category = body.category === null ? null : String(body.category || "");
|
|
complaint.description = String(body.description || "");
|
|
|
|
await route.fulfill(json({ success: true, data: complaint }));
|
|
return;
|
|
}
|
|
|
|
if (method === "DELETE") {
|
|
const id = Number.parseInt(url.searchParams.get("id") || "", 10);
|
|
requestLog.deleteIds.push(id);
|
|
const index = complaints.findIndex((item) => item.id === id);
|
|
if (index >= 0) {
|
|
complaints.splice(index, 1);
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
message: "Complaint deleted successfully",
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(json({ success: false, data: [] }, 405));
|
|
});
|
|
|
|
return {
|
|
complaints,
|
|
requestLog,
|
|
};
|
|
}
|
|
|
|
test.describe("Superuser customer complaints", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
});
|
|
|
|
test("renders the complaints page, filters by department, and searches complaint rows", async ({ page }) => {
|
|
const { requestLog } = await installComplaintMocks(page);
|
|
|
|
await page.goto("/superuser/complaints");
|
|
|
|
await expect(page).toHaveURL(/\/superuser\/complaints$/);
|
|
await expect(page.getByTestId("superuser-complaints-page")).toBeVisible();
|
|
await expect(page.getByTestId("pagination-search-input")).toBeVisible();
|
|
await expect(page.getByTestId("pagination-reload-actions")).toBeVisible();
|
|
await expect(page.getByTestId("superuser-complaint-row-101")).toContainText("Broken mirror after wash");
|
|
await expect(page.getByTestId("superuser-complaint-row-102")).toContainText("Late service response");
|
|
await expect(page.getByTestId("superuser-complaint-category-101")).toContainText("Skade spejle");
|
|
await expect(page.getByTestId("superuser-complaint-wash-date-102")).toContainText("-");
|
|
await expect(page.getByTestId("superuser-complaint-category-102")).toContainText("-");
|
|
|
|
await page.getByTestId("superuser-complaints-department-filter").selectOption("2");
|
|
await expect(page.getByTestId("superuser-complaint-row-102")).toBeVisible();
|
|
await expect(page.locator('[data-testid="superuser-complaint-row-101"]')).toHaveCount(0);
|
|
|
|
await page.getByTestId("superuser-complaints-department-filter").selectOption("*");
|
|
await expect(page.getByTestId("superuser-complaint-row-101")).toBeVisible();
|
|
|
|
await page.getByTestId("pagination-search-input").fill("mirror");
|
|
await expect(page.getByTestId("superuser-complaint-row-101")).toBeVisible();
|
|
await expect(page.locator('[data-testid="superuser-complaint-row-102"]')).toHaveCount(0);
|
|
|
|
await expect
|
|
.poll(() =>
|
|
requestLog.getUrls.some(
|
|
(url) => url.includes("filters=department_id%3A2") || url.includes("filters=department_id:2")
|
|
)
|
|
)
|
|
.toBe(true);
|
|
await expect.poll(() => requestLog.getUrls.some((url) => url.toLowerCase().includes("search=mirror"))).toBe(true);
|
|
});
|
|
|
|
test("edits complaints with department, searchable customer selection, and description updates", async ({ page }) => {
|
|
const { requestLog } = await installComplaintMocks(page);
|
|
|
|
await page.goto("/superuser/complaints");
|
|
await expect(page.getByTestId("superuser-complaints-page")).toBeVisible();
|
|
|
|
await selectComplaintAction(page, 101, "superuser-complaint-edit-101");
|
|
await expect(page.getByTestId("superuser-complaint-edit-department")).toHaveValue("1");
|
|
await expect(page.getByTestId("superuser-complaint-edit-customer-search")).toHaveValue("Acme Transport - 12345");
|
|
await expect(complaintWashDateInput(page)).toHaveValue("06.04.2026");
|
|
await expect(page.getByTestId("superuser-complaint-edit-category")).toHaveValue("damage_mirrors");
|
|
await expect(page.getByTestId("superuser-complaint-edit-description")).toHaveValue("Broken mirror after wash");
|
|
|
|
await page.getByTestId("superuser-complaint-edit-department").selectOption("2");
|
|
await page.getByTestId("superuser-complaint-edit-customer-search").fill("updated");
|
|
await expect(page.getByTestId("superuser-complaint-edit-customer-option-45678")).toBeVisible();
|
|
await expect(page.getByTestId("superuser-complaint-edit-description")).toBeVisible();
|
|
await page.getByTestId("superuser-complaint-edit-customer-option-45678").click();
|
|
await complaintWashDateInput(page).fill("2026-04-08");
|
|
await page.getByTestId("superuser-complaint-edit-category").selectOption("wash_quality");
|
|
await setComplaintDescription(page, "Updated complaint description");
|
|
await page.locator(".swal2-confirm").click();
|
|
|
|
await expect.poll(() => requestLog.updateBodies.length).toBe(1);
|
|
expect(requestLog.customerLookupQueries).toContain("updated");
|
|
expect(requestLog.updateBodies[0]).toEqual({
|
|
id: 101,
|
|
department_id: 2,
|
|
customer_number: 45678,
|
|
wash_date: "2026-04-08",
|
|
category: "wash_quality",
|
|
description: "Updated complaint description",
|
|
});
|
|
|
|
await expect(page.getByTestId("superuser-complaint-row-101")).toContainText("Updated complaint description");
|
|
await expect(page.getByTestId("superuser-complaint-row-101")).toContainText("Odense");
|
|
await expect(page.getByTestId("superuser-complaint-row-101")).toContainText("45678 - Updated Customer");
|
|
await expect(page.getByTestId("superuser-complaint-category-101")).toContainText("Vaskekvalitet");
|
|
await expect(page.getByTestId("superuser-complaint-wash-date-101")).toContainText(/8\.4\.2026|08\.04\.2026/);
|
|
});
|
|
|
|
test("requires wash date/category for legacy rows, supports clearing the optional customer number, and deletes complaints", async ({
|
|
page,
|
|
}) => {
|
|
const { requestLog } = await installComplaintMocks(page);
|
|
|
|
await page.goto("/superuser/complaints");
|
|
await expect(page.getByTestId("superuser-complaints-page")).toBeVisible();
|
|
|
|
await selectComplaintAction(page, 102, "superuser-complaint-edit-102");
|
|
await expect(complaintWashDateInput(page)).toHaveValue("");
|
|
await expect(page.getByTestId("superuser-complaint-edit-category")).toHaveValue("");
|
|
await page.locator(".swal2-confirm").click();
|
|
await expect(page.locator(".swal2-validation-message")).toContainText("Dato for vask");
|
|
await complaintWashDateInput(page).fill("2026-04-09");
|
|
await page.locator(".swal2-confirm").click();
|
|
await expect(page.locator(".swal2-validation-message")).toContainText("Kategori");
|
|
await page.locator(".swal2-cancel").click();
|
|
|
|
await selectComplaintAction(page, 101, "superuser-complaint-edit-101");
|
|
await page.getByTestId("superuser-complaint-edit-customer-clear").click();
|
|
await expect(page.getByTestId("superuser-complaint-edit-customer-search")).toHaveValue("");
|
|
await complaintWashDateInput(page).fill("2026-04-09");
|
|
await page.getByTestId("superuser-complaint-edit-category").selectOption("service");
|
|
await setComplaintDescription(page, "Customer number removed");
|
|
await page.locator(".swal2-confirm").click();
|
|
|
|
await expect.poll(() => requestLog.updateBodies.length).toBe(1);
|
|
expect(requestLog.updateBodies[0]).toEqual({
|
|
id: 101,
|
|
department_id: 1,
|
|
customer_number: null,
|
|
wash_date: "2026-04-09",
|
|
category: "service",
|
|
description: "Customer number removed",
|
|
});
|
|
await expect(page.getByTestId("superuser-complaint-row-101")).not.toContainText("12345");
|
|
await expect(page.getByTestId("superuser-complaint-category-101")).toContainText("Service");
|
|
await expect(page.getByTestId("superuser-complaint-wash-date-101")).toContainText(/9\.4\.2026|09\.04\.2026/);
|
|
|
|
await selectComplaintAction(page, 102, "superuser-complaint-delete-102");
|
|
await page.locator(".swal2-confirm").click();
|
|
|
|
await expect.poll(() => requestLog.deleteIds).toEqual([102]);
|
|
await expect(page.locator('[data-testid="superuser-complaint-row-102"]')).toHaveCount(0);
|
|
});
|
|
});
|