- Introduced `.prettierrc.json` to enforce consistent code formatting across the project. - Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
734 lines
20 KiB
JavaScript
734 lines
20 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
|
|
|
const API_HOST =
|
|
/https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
|
|
|
|
function json(body, status = 200) {
|
|
return {
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
};
|
|
}
|
|
|
|
function suppressVueDevtoolsOverlay(page) {
|
|
return page.addInitScript(() => {
|
|
const style = document.createElement("style");
|
|
style.textContent =
|
|
"#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
|
|
document.documentElement.appendChild(style);
|
|
});
|
|
}
|
|
|
|
async function primeSession(page, { token, permissions, sessionData = {} }) {
|
|
await seedAuthenticatedState(page, token);
|
|
await page.goto("/login");
|
|
await page.evaluate(
|
|
async ({ sessionToken, sessionPermissions, data }) => {
|
|
const sessionModule = await import("/src/components/session/token/SessionUser.vue");
|
|
window.localStorage.setItem("token", sessionToken);
|
|
sessionModule.SessionUser.token.value = sessionToken;
|
|
sessionModule.SessionUser.authenticated.value = true;
|
|
sessionModule.SessionUser.permissions.value = sessionPermissions;
|
|
sessionModule.SessionUser.initiated.value = true;
|
|
sessionModule.SessionUser.user.customer_number.value = data.customer_number ?? 12345;
|
|
sessionModule.SessionUser.user.display_name.value = data.display_name ?? "POS E2E";
|
|
},
|
|
{
|
|
sessionToken: token,
|
|
sessionPermissions: permissions,
|
|
data: sessionData,
|
|
}
|
|
);
|
|
}
|
|
|
|
async function expectDesktopOrderItemsTable(scope) {
|
|
await expect(scope.getByTestId("pos-order-panel-cart")).toBeVisible();
|
|
await expect(scope.getByTestId("pos-order-detail-items-table")).toBeVisible();
|
|
await expect(scope.getByTestId("pos-order-metadata-grid")).toBeVisible();
|
|
await expect(scope.getByTestId("pos-order-total")).toBeVisible();
|
|
await expect(scope.locator('[data-auto-excel-export-button="1"]')).toHaveCount(0);
|
|
}
|
|
|
|
function createPosFixture() {
|
|
const customer = {
|
|
id: 1,
|
|
customerNumber: 12345,
|
|
name: "Pleno Logistics",
|
|
address: "Demo Street 1",
|
|
zip: "2630",
|
|
city: "Taastrup",
|
|
mobilePhone: "12345678",
|
|
email: "pos-e2e@example.com",
|
|
corporateIdentificationNumber: "12345678",
|
|
barred: false,
|
|
};
|
|
|
|
const products = [
|
|
{
|
|
id: 53,
|
|
name: "Tankvogn med hænger",
|
|
description: "Material product used for completion flow",
|
|
price: 599,
|
|
subscription_allowed: true,
|
|
category: 4,
|
|
piktogram: "truck",
|
|
apply_category_discount: true,
|
|
requires_note: false,
|
|
is_wash: true,
|
|
display_in_booking_form: true,
|
|
order_priority: 1,
|
|
addons: [],
|
|
},
|
|
{
|
|
id: 63,
|
|
name: "Dolly",
|
|
description: "Support product",
|
|
price: 275,
|
|
subscription_allowed: true,
|
|
category: 4,
|
|
piktogram: "truck",
|
|
apply_category_discount: true,
|
|
requires_note: false,
|
|
is_wash: true,
|
|
display_in_booking_form: true,
|
|
order_priority: 2,
|
|
addons: [],
|
|
},
|
|
];
|
|
|
|
const ordersById = {
|
|
9201: {
|
|
id: 9201,
|
|
customer_id: customer.customerNumber,
|
|
department_id: 1,
|
|
reference: "REF-9201",
|
|
notes: "",
|
|
reg_1: "AB12345",
|
|
reg_2: "",
|
|
reg_3: "",
|
|
invoice_collection_id: null,
|
|
booking_id: null,
|
|
completed_at: null,
|
|
created_at: "2026-01-01T10:00:00.000Z",
|
|
},
|
|
};
|
|
|
|
const orderItemsByOrderId = {
|
|
9201: [],
|
|
};
|
|
|
|
return {
|
|
customer,
|
|
products,
|
|
departmentCategories: [
|
|
{
|
|
id: 11,
|
|
department_id: 1,
|
|
category: {
|
|
id: 4,
|
|
name: "Udvendig",
|
|
meta: {
|
|
products: [53, 63],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
vehicles: [
|
|
{
|
|
id: 7001,
|
|
reg: "AB12345",
|
|
customer_id: customer.customerNumber,
|
|
customer_name: customer.name,
|
|
type: 53,
|
|
status: "verified",
|
|
barred: false,
|
|
wash_subscription: false,
|
|
addons: {
|
|
enabled: 0,
|
|
available: 0,
|
|
list: [],
|
|
},
|
|
reference: "REF-AB12345",
|
|
last_order_id: null,
|
|
},
|
|
],
|
|
unknownVehicles: [],
|
|
ordersById,
|
|
orderItemsByOrderId,
|
|
markCompletedOrderIds: [],
|
|
nextOrderId: 9300,
|
|
nextOrderItemId: 9800,
|
|
};
|
|
}
|
|
|
|
function buildOrderItem(product, body, id) {
|
|
const quantity = Number(body.quantity || 1);
|
|
return {
|
|
id,
|
|
order_id: Number(body.order_id),
|
|
product_id: product.id,
|
|
product,
|
|
quantity,
|
|
notes: body.notes || "",
|
|
reference: body.reference || "",
|
|
related_item_id: body.related_item_id ?? null,
|
|
price: Number(body.price ?? product.price ?? 0),
|
|
};
|
|
}
|
|
|
|
async function mockPosApi(page, fixture) {
|
|
await page.route(API_HOST, async (route) => {
|
|
const request = route.request();
|
|
const url = request.url();
|
|
const parsedUrl = new URL(url);
|
|
const pathname = parsedUrl.pathname;
|
|
const method = request.method();
|
|
|
|
if (pathname.endsWith("/departments") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: [
|
|
{
|
|
id: 1,
|
|
name: "Taastrup",
|
|
},
|
|
],
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/departments/categories") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: fixture.departmentCategories,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/bookings") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: [],
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/vehicles") && method === "GET") {
|
|
const search = (parsedUrl.searchParams.get("search") || "").toUpperCase();
|
|
const matches = fixture.vehicles.filter((vehicle) => vehicle.reg.includes(search));
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: matches,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/department/vehicles/unknown-customer") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: fixture.unknownVehicles,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/users/customer") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
customer_name: fixture.customer.name,
|
|
economic_customer: fixture.customer,
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/customers") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: [fixture.customer],
|
|
meta: {
|
|
pagination: {
|
|
page: 1,
|
|
limit: 10,
|
|
total: 1,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/customer/notes") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: [],
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/customer/attributes") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: [],
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/superuser/user/discounts") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: [],
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/products") && method === "GET") {
|
|
const id = parsedUrl.searchParams.get("id");
|
|
const category = parsedUrl.searchParams.get("category");
|
|
if (id) {
|
|
const product = fixture.products.find((entry) => Number(entry.id) === Number(id));
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: product || null,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
const list = category
|
|
? fixture.products.filter((entry) => Number(entry.category) === Number(category))
|
|
: fixture.products;
|
|
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: list,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: [],
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = fixture.nextOrderId++;
|
|
fixture.ordersById[orderId] = {
|
|
id: orderId,
|
|
customer_id: Number(body.customer_id),
|
|
department_id: Number(body.department_id),
|
|
reference: body.reference || "",
|
|
notes: body.notes || "",
|
|
reg_1: body.reg_1 || "",
|
|
reg_2: body.reg_2 || "",
|
|
reg_3: body.reg_3 || "",
|
|
invoice_collection_id: null,
|
|
booking_id: null,
|
|
completed_at: null,
|
|
created_at: new Date().toISOString(),
|
|
};
|
|
fixture.orderItemsByOrderId[orderId] = [];
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
id: orderId,
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order") && method === "GET") {
|
|
const id = Number(parsedUrl.searchParams.get("id"));
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: fixture.ordersById[id] || null,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order") && method === "PUT") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id);
|
|
if (fixture.ordersById[orderId]) {
|
|
if (body.field) {
|
|
fixture.ordersById[orderId][body.field] = body.value;
|
|
} else {
|
|
fixture.ordersById[orderId] = {
|
|
...fixture.ordersById[orderId],
|
|
...body,
|
|
};
|
|
}
|
|
}
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: fixture.ordersById[orderId] || null,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "GET") {
|
|
const orderId = Number(parsedUrl.searchParams.get("order_id"));
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: fixture.orderItemsByOrderId[orderId] || [],
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.order_id);
|
|
const productId = Number(body.product_id);
|
|
const product = fixture.products.find((entry) => Number(entry.id) === productId);
|
|
if (!product || !fixture.ordersById[orderId]) {
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Order or product not found",
|
|
},
|
|
},
|
|
422
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
const item = buildOrderItem(product, body, fixture.nextOrderItemId++);
|
|
if (!Array.isArray(fixture.orderItemsByOrderId[orderId])) {
|
|
fixture.orderItemsByOrderId[orderId] = [];
|
|
}
|
|
fixture.orderItemsByOrderId[orderId].push(item);
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: item,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "PUT") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderItemId = Number(body.id);
|
|
Object.keys(fixture.orderItemsByOrderId).forEach((orderIdKey) => {
|
|
fixture.orderItemsByOrderId[orderIdKey] = (fixture.orderItemsByOrderId[orderIdKey] || []).map((item) => {
|
|
if (item.id !== orderItemId) {
|
|
return item;
|
|
}
|
|
return {
|
|
...item,
|
|
price: Number(body.price ?? item.price),
|
|
notes: body.notes ?? item.notes,
|
|
reference: body.reference ?? item.reference,
|
|
quantity: Number(body.quantity ?? item.quantity),
|
|
};
|
|
});
|
|
});
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
id: orderItemId,
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "DELETE") {
|
|
const orderItemId = Number(parsedUrl.searchParams.get("id"));
|
|
Object.keys(fixture.orderItemsByOrderId).forEach((orderIdKey) => {
|
|
fixture.orderItemsByOrderId[orderIdKey] = (fixture.orderItemsByOrderId[orderIdKey] || []).filter(
|
|
(item) => item.id !== orderItemId
|
|
);
|
|
});
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: true,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/departments/order/recommended") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
reg_1: {
|
|
motorapi: [53],
|
|
order_history: {
|
|
2: [],
|
|
3: [],
|
|
4: [],
|
|
5: [],
|
|
},
|
|
},
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/mark_as_completed") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id);
|
|
fixture.markCompletedOrderIds.push(orderId);
|
|
if (fixture.ordersById[orderId]) {
|
|
fixture.ordersById[orderId].completed_at = new Date().toISOString();
|
|
}
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
id: orderId,
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
await route.fallback();
|
|
});
|
|
}
|
|
|
|
function seedMobilePosState(page) {
|
|
const state = {
|
|
vehicles: {
|
|
vehicle_1: {
|
|
reg: "AB12345",
|
|
customer_id: 12345,
|
|
type: 53,
|
|
status: "verified",
|
|
barred: false,
|
|
booking_id: null,
|
|
addons: [],
|
|
},
|
|
vehicle_2: null,
|
|
vehicle_3: null,
|
|
activeVehicleIndex: 1,
|
|
},
|
|
views: {
|
|
manualInput: false,
|
|
vehicleSelection: false,
|
|
additionalItemSelection: false,
|
|
transactionHistoryView: false,
|
|
},
|
|
transactionItems: {
|
|
primaryItem: {
|
|
id: 53,
|
|
name: "Tankvogn med hænger",
|
|
description: "Material product used for completion flow",
|
|
price: 599,
|
|
category: 4,
|
|
is_wash: true,
|
|
addons: [],
|
|
},
|
|
additionalItems: [],
|
|
},
|
|
categories: {
|
|
list: [],
|
|
selected: null,
|
|
},
|
|
productList: {
|
|
list: [],
|
|
},
|
|
metadata: {
|
|
customerId: 12345,
|
|
notes: "",
|
|
reference: "REF-9201",
|
|
washId: null,
|
|
bookingId: null,
|
|
laneId: null,
|
|
},
|
|
attachments: {
|
|
files: [],
|
|
base64: [],
|
|
wash_certificate: false,
|
|
},
|
|
transactionHistory: [],
|
|
lastVehicleOrders: {
|
|
vehicle_1: null,
|
|
vehicle_2: null,
|
|
vehicle_3: null,
|
|
},
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
return page.addInitScript((payload) => {
|
|
window.localStorage.setItem("pos", JSON.stringify(payload));
|
|
}, state);
|
|
}
|
|
|
|
test.describe("POS flow", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await suppressVueDevtoolsOverlay(page);
|
|
});
|
|
|
|
test("desktop bootstrap ignores customer_id-only query for order loading", async ({ page }, testInfo) => {
|
|
test.skip(testInfo.project.name !== "chromium-desktop", "Desktop POS bootstrap is validated on chromium-desktop.");
|
|
|
|
const fixture = createPosFixture();
|
|
const permissions = ["admin", "department_access_1"];
|
|
const invalidOrderRequests = [];
|
|
const reg1Errors = [];
|
|
|
|
page.on("request", (request) => {
|
|
const requestUrl = request.url();
|
|
if (
|
|
request.method() === "GET" &&
|
|
(requestUrl.includes("/order/items?order_id=null") ||
|
|
requestUrl.includes("/order/items?order_id=undefined") ||
|
|
requestUrl.includes("/order/items?order_id=NaN") ||
|
|
requestUrl.includes("/order?id=null") ||
|
|
requestUrl.includes("/order?id=undefined") ||
|
|
requestUrl.includes("/order?id=NaN"))
|
|
) {
|
|
invalidOrderRequests.push(requestUrl);
|
|
}
|
|
});
|
|
|
|
page.on("console", (message) => {
|
|
if (message.type() === "error" && message.text().includes("reg_1")) {
|
|
reg1Errors.push(message.text());
|
|
}
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions,
|
|
});
|
|
await mockPosApi(page, fixture);
|
|
await primeSession(page, {
|
|
token: "pos-bootstrap-token",
|
|
permissions,
|
|
sessionData: {
|
|
display_name: "POS Bootstrap",
|
|
},
|
|
});
|
|
|
|
await page.goto("/admin/1/modules/pos?customer_id=12345&step=1");
|
|
await expect(page.getByTestId("pos-step-1")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-step-1").getByText("Nummerplader").first()).toBeVisible({ timeout: 10_000 });
|
|
|
|
expect(invalidOrderRequests).toEqual([]);
|
|
expect(reg1Errors).toEqual([]);
|
|
});
|
|
|
|
test("desktop flow creates transaction, renders cart, and reaches completion step", async ({ page }, testInfo) => {
|
|
test.skip(testInfo.project.name !== "chromium-desktop", "Desktop POS flow is validated on chromium-desktop.");
|
|
|
|
const fixture = createPosFixture();
|
|
const permissions = ["admin", "department_access_1"];
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions,
|
|
});
|
|
await mockPosApi(page, fixture);
|
|
await primeSession(page, {
|
|
token: "pos-desktop-token",
|
|
permissions,
|
|
sessionData: {
|
|
display_name: "POS Desktop",
|
|
},
|
|
});
|
|
|
|
await page.goto("/admin/1/modules/pos");
|
|
await expect(page.getByTestId("pos-step-1")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.locator("#reg_1").fill("AB12345");
|
|
await expect(page.getByTestId("pos-step-1").getByText("Pleno Logistics").first()).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-step-1").locator(".pos-card-tabs > .tabs li.is-active")).toContainText("Kunde", {
|
|
timeout: 10_000,
|
|
});
|
|
|
|
await page.locator('[data-testid="pos-next-step"]:visible').click();
|
|
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page).toHaveURL(/step=2/);
|
|
await expectDesktopOrderItemsTable(page.getByTestId("pos-step-2"));
|
|
|
|
await page.getByTestId("pos-product-card-53").click();
|
|
await expect(page.getByTestId("pos-add-to-cart-53")).toBeVisible({ timeout: 10_000 });
|
|
|
|
const addItemRequest = page.waitForRequest((request) => {
|
|
return request.method() === "POST" && request.url().includes("/order/items");
|
|
});
|
|
await page.getByTestId("pos-add-to-cart-53").click();
|
|
await addItemRequest;
|
|
|
|
await expect(page.getByTestId("pos-step-2")).toContainText("Tankvogn med hænger");
|
|
|
|
await page.locator('[data-testid="pos-next-step"]:visible').click();
|
|
await expect(page.getByTestId("pos-step-3")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page).toHaveURL(/step=3/);
|
|
await expectDesktopOrderItemsTable(page.getByTestId("pos-step-3"));
|
|
|
|
const markCompletedRequest = page.waitForRequest((request) => {
|
|
return request.method() === "POST" && request.url().includes("/orders/mark_as_completed");
|
|
});
|
|
await page.locator('[data-testid="pos-next-step"]:visible').click();
|
|
await markCompletedRequest;
|
|
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
if (await page.getByTestId("pos-step-4").isVisible()) {
|
|
return "step4";
|
|
}
|
|
if (/\/admin\/1\/modules\/pos(?:\?|$)/.test(page.url()) && !page.url().includes("step=3")) {
|
|
return "reset";
|
|
}
|
|
return "pending";
|
|
},
|
|
{ timeout: 10_000 }
|
|
)
|
|
.not.toBe("pending");
|
|
|
|
if (await page.getByTestId("pos-step-4").isVisible()) {
|
|
await expectDesktopOrderItemsTable(page.getByTestId("pos-step-4"));
|
|
}
|
|
});
|
|
});
|