Files
pleno-vue/tests/e2e/pos-flow.spec.js
T
Jeppe Bundgaard ce049d9196 Enhance POS booking flow logic and tests:
- Added support for trailers (`reg_2`) in booking selection and vehicle matching logic.
- Implemented logic to prioritize bookings from registered vehicles and trailers.
- Enhanced booking dropdown with date labels and validation for booked vehicles.
- Improved e2e tests to cover booking date, trailer registration, and vehicle-search scenarios.
- Refactored Vue components for consistency in booking handling and dropdown interaction.
2026-04-13 17:56:19 +02:00

1214 lines
37 KiB
JavaScript

import { expect, test } from "@playwright/test";
import { mockApi, primeMockSession } 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 primeMockSession(page, { token });
}
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: [],
orderBookings: [],
ordersById,
orderItemsByOrderId,
markCompletedOrderIds: [],
completedBookingIds: [],
bookingOrderAssignments: [],
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),
};
}
function buildOrderBooking(id, overrides = {}) {
return {
id,
customer_number: 12345,
customer_name: "Pleno Logistics",
department: 1,
datetime: "2026-01-01T08:00:00.000Z",
reg_1: "AB12345",
reg_2: "",
reg_3: "",
reference: `BOOKING-${id}`,
reference_number: `BOOKING-${id}`,
notes: `Booking note ${id}`,
note: `Booking note ${id}`,
po: `PO-${id}`,
status: "pending",
order_id: null,
wash_type: "Tankvogn med hænger",
parsed_services: {
string: "Tankvogn med hænger",
array: ["Tankvogn med hænger"],
},
items: [
{
id: 53,
name: "Tankvogn med hænger",
price: 599,
quantity: 1,
},
],
...overrides,
};
}
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("/order-bookings") && method === "GET") {
const bookingId = Number(parsedUrl.searchParams.get("id") || 0);
const filters = String(parsedUrl.searchParams.get("filters") || "");
let bookings = Array.isArray(fixture.orderBookings) ? [...fixture.orderBookings] : [];
if (filters.includes("order_id:null")) {
bookings = bookings.filter((booking) => booking.order_id === null || booking.order_id === undefined);
}
if (filters.includes("department:")) {
const departmentFilter = Number(filters.split("department:")[1]?.split(",")[0] || 0);
if (departmentFilter > 0) {
bookings = bookings.filter((booking) => Number(booking.department) === departmentFilter);
}
}
if (bookingId > 0) {
await route.fulfill(
json({
success: true,
data: bookings.find((booking) => Number(booking.id) === bookingId) || null,
})
);
return;
}
await route.fulfill(
json({
success: true,
data: bookings,
})
);
return;
}
if (/\/order-bookings\/\d+$/.test(pathname) && method === "GET") {
const bookingId = Number(pathname.split("/").pop());
await route.fulfill(
json({
success: true,
data: (fixture.orderBookings || []).find((booking) => Number(booking.id) === bookingId) || null,
})
);
return;
}
if (pathname.endsWith("/order-bookings") && method === "PUT") {
const body = request.postDataJSON?.() || {};
const bookingId = Number(body.id);
const bookingIndex = (fixture.orderBookings || []).findIndex((booking) => Number(booking.id) === bookingId);
if (bookingIndex >= 0) {
fixture.orderBookings[bookingIndex] = {
...fixture.orderBookings[bookingIndex],
order_id: body.order_id ?? body.value ?? null,
};
fixture.bookingOrderAssignments.push({
id: bookingId,
order_id: fixture.orderBookings[bookingIndex].order_id,
});
}
await route.fulfill(
json({
success: true,
data: bookingIndex >= 0 ? fixture.orderBookings[bookingIndex] : null,
})
);
return;
}
if (pathname.endsWith("/order-bookings/complete") && method === "POST") {
const body = request.postDataJSON?.() || {};
const bookingId = Number(body.id);
const bookingIndex = (fixture.orderBookings || []).findIndex((booking) => Number(booking.id) === bookingId);
if (bookingIndex >= 0) {
fixture.orderBookings[bookingIndex] = {
...fixture.orderBookings[bookingIndex],
status: "completed",
};
fixture.completedBookingIds.push(bookingId);
}
await route.fulfill(
json({
success: true,
data: bookingIndex >= 0 ? fixture.orderBookings[bookingIndex] : { id: bookingId, status: "completed" },
})
);
return;
}
if (pathname.endsWith("/vehicles/search") && 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("/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("/orders") && method === "PUT") {
const body = request.postDataJSON?.() || {};
const orderId = Number(body.id);
if (fixture.ordersById[orderId]) {
fixture.ordersById[orderId] = {
...fixture.ordersById[orderId],
...body,
};
}
await route.fulfill(
json({
success: true,
data: fixture.ordersById[orderId] || null,
})
);
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);
}
async function setupDesktopPosPage(page, fixture, { token = "pos-desktop-token" } = {}) {
const permissions = ["admin", "department_access_1"];
await mockApi(page, {
authenticated: true,
permissions,
sessionData: {
display_name: "POS Desktop",
},
});
await mockPosApi(page, fixture);
await primeSession(page, {
token,
permissions,
});
await page.goto("/admin/1/modules/pos");
await expect(page.getByTestId("pos-step-1")).toBeVisible({ timeout: 10_000 });
}
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 consoleProblems = [];
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() === "warning" || message.type() === "error") {
consoleProblems.push(message.text());
}
});
await mockApi(page, {
authenticated: true,
permissions,
sessionData: {
display_name: "POS Bootstrap",
},
});
await mockPosApi(page, fixture);
await primeSession(page, {
token: "pos-bootstrap-token",
permissions,
});
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(consoleProblems.join("\n")).not.toContain("Extraneous non-props attributes");
expect(consoleProblems.join("\n")).not.toContain("reg_1");
});
test("desktop hides the Sidste vask section when the selected vehicle has no last_order_id", async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== "chromium-desktop", "Desktop POS step 1 is validated on chromium-desktop.");
const fixture = createPosFixture();
await setupDesktopPosPage(page, fixture, { token: "pos-no-last-wash-token" });
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.getByText(/Sidste vask/i)).toHaveCount(0);
await expect(page.getByRole("button", { name: /Kopier sidste vask/i })).toHaveCount(0);
});
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"];
const consoleProblems = [];
page.on("console", (message) => {
if (message.type() === "warning" || message.type() === "error") {
consoleProblems.push(message.text());
}
});
await mockApi(page, {
authenticated: true,
permissions,
sessionData: {
display_name: "POS Desktop",
},
});
await mockPosApi(page, fixture);
await primeSession(page, {
token: "pos-desktop-token",
permissions,
});
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"));
}
expect(consoleProblems.join("\n")).not.toContain("Extraneous non-props attributes");
});
test("desktop marks booked vehicles in the dropdown when the vehicle payload already carries a booking", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."
);
const fixture = createPosFixture();
fixture.vehicles = [
{
...fixture.vehicles[0],
reg: "EC21235",
booking_id: 8991,
status: "booked",
},
{
...fixture.vehicles[0],
id: 7002,
reg: "EC21234",
booking_id: 8992,
status: "booked",
},
];
fixture.orderBookings = [
buildOrderBooking(8991, {
reg_1: "EC21235",
datetime: "2026-01-03T07:00:00.000Z",
}),
buildOrderBooking(8992, {
reg_1: "EC21234",
datetime: "2026-01-04T09:00:00.000Z",
}),
];
await setupDesktopPosPage(page, fixture, { token: "pos-desktop-booked-dropdown" });
await page.locator("#reg_1").fill("EC212");
await expect(page.getByTestId("desktop-booked-icon-7001")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("desktop-booked-icon-7002")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("desktop-booked-date-7001")).toContainText(/\d/, { timeout: 10_000 });
await expect(page.getByTestId("desktop-booked-date-7002")).toContainText(/\d/, { timeout: 10_000 });
});
test("desktop auto-applies a single matching order booking and completes that booking", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."
);
const fixture = createPosFixture();
fixture.orderBookings = [
buildOrderBooking(8101, {
reference: "SINGLE-BOOKING-REF",
reference_number: "SINGLE-BOOKING-REF",
reg_2: "TRAILER-1",
notes: "Single desktop booking",
note: "Single desktop booking",
items: [
{ id: 53, name: "Tankvogn med hænger", price: 599, quantity: 1 },
{ id: 63, name: "Dolly", price: 275, quantity: 1 },
],
parsed_services: {
string: "Tankvogn med hænger, Dolly",
array: ["Tankvogn med hænger", "Dolly"],
},
}),
];
await setupDesktopPosPage(page, fixture, { token: "pos-desktop-single-booking" });
const activeBookingSelector = page.locator('[data-testid="default-object-selector"].is-active');
await page.locator("#reg_1").fill("AB12345");
await expect(activeBookingSelector).toHaveCount(0);
await expect(page.locator("#reg_2")).toHaveValue("TRAILER-1");
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: 10_000 });
await expect
.poll(() => fixture.ordersById[9300]?.reference || null, { timeout: 10_000 })
.toBe("SINGLE-BOOKING-REF");
await expect.poll(() => fixture.ordersById[9300]?.reg_2 || null, { timeout: 10_000 }).toBe("TRAILER-1");
await expect
.poll(() => (fixture.orderItemsByOrderId[9300] || []).map((item) => Number(item.product_id)), { timeout: 10_000 })
.toEqual([53, 63]);
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect(page.getByTestId("pos-step-3")).toBeVisible({ timeout: 10_000 });
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect.poll(() => fixture.ordersById[9300]?.booking_id || null, { timeout: 10_000 }).toBe(8101);
await expect
.poll(() => fixture.bookingOrderAssignments, { timeout: 10_000 })
.toContainEqual({
id: 8101,
order_id: 9300,
});
await expect.poll(() => fixture.completedBookingIds, { timeout: 10_000 }).toContain(8101);
});
test("desktop opens a chooser for multiple matching order bookings and hydrates the selected booking", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."
);
const fixture = createPosFixture();
fixture.orderBookings = [
buildOrderBooking(8102, {
datetime: "2026-01-01T07:00:00.000Z",
reference: "BOOKING-A-REF",
reference_number: "BOOKING-A-REF",
reg_2: "TRAILER-A",
items: [
{ id: 53, name: "Tankvogn med hænger", price: 599, quantity: 1 },
{ id: 63, name: "Dolly", price: 275, quantity: 1 },
],
parsed_services: {
string: "Tankvogn med hænger, Dolly",
array: ["Tankvogn med hænger", "Dolly"],
},
}),
buildOrderBooking(8103, {
datetime: "2026-01-01T09:00:00.000Z",
reference: "BOOKING-B-REF",
reference_number: "BOOKING-B-REF",
reg_2: "TRAILER-B",
items: [{ id: 63, name: "Dolly", price: 275, quantity: 1 }],
parsed_services: {
string: "Dolly",
array: ["Dolly"],
},
}),
];
await setupDesktopPosPage(page, fixture, { token: "pos-desktop-multi-booking" });
const activeBookingSelector = page.locator('[data-testid="default-object-selector"].is-active');
await page.locator("#reg_1").fill("AB12345");
await expect(activeBookingSelector).toBeVisible({ timeout: 10_000 });
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8102")).toBeVisible({
timeout: 10_000,
});
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8103")).toBeVisible({
timeout: 10_000,
});
await activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8102").click();
await expect(page.locator("#reference")).toHaveValue("BOOKING-A-REF");
await expect(page.locator("#reg_2")).toHaveValue("TRAILER-A");
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: 10_000 });
await expect.poll(() => fixture.ordersById[9300]?.reg_2 || null, { timeout: 10_000 }).toBe("TRAILER-A");
await expect
.poll(() => (fixture.orderItemsByOrderId[9300] || []).map((item) => Number(item.product_id)), { timeout: 10_000 })
.toEqual([53, 63]);
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect(page.getByTestId("pos-step-3")).toBeVisible({ timeout: 10_000 });
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect.poll(() => fixture.ordersById[9300]?.booking_id || null, { timeout: 10_000 }).toBe(8102);
await expect
.poll(() => fixture.bookingOrderAssignments, { timeout: 10_000 })
.toContainEqual({
id: 8102,
order_id: 9300,
});
await expect.poll(() => fixture.completedBookingIds, { timeout: 10_000 }).toContain(8102);
expect(fixture.completedBookingIds).not.toContain(8103);
});
test("desktop opens the booking chooser for a booked plate even without a vehicle match", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."
);
const fixture = createPosFixture();
fixture.orderBookings = [
buildOrderBooking(8111, {
reg_1: "BOOKONLY1",
datetime: "2026-01-01T07:00:00.000Z",
reference: "BOOK-ONLY-A",
reference_number: "BOOK-ONLY-A",
items: [
{ id: 53, name: "Tankvogn med hænger", price: 599, quantity: 1 },
{ id: 63, name: "Dolly", price: 275, quantity: 1 },
],
parsed_services: {
string: "Tankvogn med hænger, Dolly",
array: ["Tankvogn med hænger", "Dolly"],
},
}),
buildOrderBooking(8112, {
reg_1: "BOOKONLY1",
datetime: "2026-01-01T09:00:00.000Z",
reference: "BOOK-ONLY-B",
reference_number: "BOOK-ONLY-B",
items: [{ id: 63, name: "Dolly", price: 275, quantity: 1 }],
parsed_services: {
string: "Dolly",
array: ["Dolly"],
},
}),
];
await setupDesktopPosPage(page, fixture, { token: "pos-desktop-booking-only" });
const activeBookingSelector = page.locator('[data-testid="default-object-selector"].is-active');
await page.locator("#reg_1").fill("BOOKONLY1");
await expect(activeBookingSelector).toBeVisible({ timeout: 10_000 });
await activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8111").click();
await expect(page.locator("#reference")).toHaveValue("BOOK-ONLY-A");
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: 10_000 });
await expect
.poll(() => (fixture.orderItemsByOrderId[9300] || []).map((item) => Number(item.product_id)), { timeout: 10_000 })
.toEqual([53, 63]);
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect(page.getByTestId("pos-step-3")).toBeVisible({ timeout: 10_000 });
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect.poll(() => fixture.ordersById[9300]?.booking_id || null, { timeout: 10_000 }).toBe(8111);
await expect
.poll(
() =>
fixture.bookingOrderAssignments.some(
(entry) => Number(entry?.id) === 8111 && Number(entry?.order_id) === 9300
),
{ timeout: 10_000 }
)
.toBe(true);
await expect.poll(() => fixture.completedBookingIds, { timeout: 10_000 }).toContain(8111);
expect(fixture.completedBookingIds).not.toContain(8112);
});
test("desktop continue without booking skips hydration and completion requests", async ({ page }, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."
);
const fixture = createPosFixture();
fixture.orderBookings = [
buildOrderBooking(8104, {
reference: "SKIP-A-REF",
reference_number: "SKIP-A-REF",
}),
buildOrderBooking(8105, {
datetime: "2026-01-01T09:00:00.000Z",
reference: "SKIP-B-REF",
reference_number: "SKIP-B-REF",
}),
];
await setupDesktopPosPage(page, fixture, { token: "pos-desktop-skip-booking" });
const activeBookingSelector = page.locator('[data-testid="default-object-selector"].is-active');
await page.locator("#reg_1").fill("AB12345");
await expect(activeBookingSelector).toBeVisible({ timeout: 10_000 });
await activeBookingSelector.getByTestId("pos-desktop-order-booking-skip").click();
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: 10_000 });
await expect.poll(() => (fixture.orderItemsByOrderId[9300] || []).length, { timeout: 10_000 }).toBe(0);
await page.getByTestId("pos-product-card-53").click();
await expect(page.getByTestId("pos-add-to-cart-53")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-add-to-cart-53").click();
await expect.poll(() => (fixture.orderItemsByOrderId[9300] || []).length, { timeout: 10_000 }).toBe(1);
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect(page.getByTestId("pos-step-3")).toBeVisible({ timeout: 10_000 });
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect.poll(() => fixture.bookingOrderAssignments.length, { timeout: 10_000 }).toBe(0);
await expect.poll(() => fixture.completedBookingIds.length, { timeout: 10_000 }).toBe(0);
await expect.poll(() => fixture.ordersById[9300]?.booking_id ?? null, { timeout: 10_000 }).toBe(null);
});
});