Files
pleno-vue/tests/e2e/admin-pos-drafts.spec.ts
T
Jeppe Bundgaard 0fa2284e89 Add connectivity issue handling logic and tests
- Add `ConnectivityIssue.vue` component for displaying server connection issues.
- Implement polling logic with retry intervals to check server health.
- Update i18n with new connectivity issue messages.
- Add unit tests to cover connectivity failure and recovery scenarios.
- Update test suite for breadth of coverage, including router contract and timer cleanup.
2026-05-28 18:11:21 +02:00

796 lines
24 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { mockApi, seedAuthenticatedState } from "./support/network.js";
import { isDesktopProject } from "./support/projects";
const json = (body: unknown, status = 200) => ({
status,
contentType: "application/json",
body: JSON.stringify(body),
});
test.describe("Admin POS drafts", () => {
test("assigns a draft order to a customer, invoice collection, and recalculated pricing", async ({
page,
}, testInfo) => {
test.skip(!isDesktopProject(testInfo), "Desktop only");
const pageErrors: string[] = [];
const navigationRenderErrors: string[] = [];
page.on("pageerror", (error) => {
pageErrors.push(error.stack || error.message);
});
page.on("console", (message) => {
const text = message.text();
if (
message.type() === "error" ||
text.includes("Unhandled error during execution of render function") ||
text.includes("DesktopNavigationBuefy")
) {
navigationRenderErrors.push(`${message.type()}: ${text}`);
}
});
await seedAuthenticatedState(page);
await mockApi(page, {
authenticated: true,
permissions: [
"admin",
"department_access_1",
"add_order",
"edit_order",
"list_orders",
"list_order_items",
"edit_order_items",
"list_products",
],
sessionData: {
runtime_config: {
economic: {
transaction_draft_customer_number: 6001,
},
},
},
});
const currentOrder = {
id: 56625,
customer_id: 6001,
customer_name: "Uspecificeret kunde",
user_id: 77,
cashier_id: 5,
cashier_name: "Jeppe",
department_id: 1,
reg_1: "EC21233",
reg_2: null,
reg_3: null,
reference: "reff",
notes: "",
po: "po00",
total_net_amount: 111700,
created_at: "2026-04-20 13:20:57",
invoice_collection_id: 7001,
invoice_collection: {
id: 7001,
customer_number: 6001,
closed_at: null,
booked_invoice_id: null,
},
economic_invoice_module: null,
stripe_invoice_module: null,
error_message: null,
pending_handheld: false,
completed_at: null,
booking_id: null,
wash_id: null,
lane: null,
safety_seal: null,
};
const foreignDepartmentDraft = {
...currentOrder,
id: 56626,
department_id: 2,
reg_1: "ZZ99999",
reference: "foreign-draft",
};
const orderPutPayloads: Array<Record<string, unknown>> = [];
const orderItemsPutPayloads: Array<Record<string, unknown>> = [];
const productPriceRequests: Array<Record<string, string>> = [];
await page.route("https://api.truckwash.io:4433/departments**", async (route) => {
await route.fulfill(
json({
data: [{ id: 1, name: "Demo", visible: true }],
})
);
});
await page.route("https://api.truckwash.io:4433/orders**", async (route) => {
const request = route.request();
if (request.method() === "PUT") {
const payload = request.postDataJSON();
orderPutPayloads.push(payload);
if (payload.customer_id) {
currentOrder.customer_id = Number(payload.customer_id);
currentOrder.customer_name = "Acme Logistics";
}
if (payload.invoice_collection_id) {
currentOrder.invoice_collection_id = Number(payload.invoice_collection_id);
currentOrder.invoice_collection = {
id: Number(payload.invoice_collection_id),
customer_number: currentOrder.customer_id,
closed_at: null,
booked_invoice_id: null,
};
}
await route.fulfill(json({ success: true, data: { message: "Order updated successfully" } }));
return;
}
const url = new URL(request.url());
const filters = url.searchParams.get("filters") || "";
const includesDraftCustomer = filters.includes("customer_id:6001");
const includesDepartmentOne = filters.includes("department_id:1");
if (includesDraftCustomer && !includesDepartmentOne) {
await new Promise((resolve) => setTimeout(resolve, 150));
await route.fulfill(
json({
data: [foreignDepartmentDraft],
meta: {
pagination: {
page: 1,
per_page: 100,
total: 1,
},
},
})
);
return;
}
const includesDraftOrder = currentOrder.customer_id === 6001 && includesDraftCustomer && includesDepartmentOne;
await route.fulfill(
json({
data: includesDraftOrder ? [currentOrder] : [],
meta: {
pagination: {
page: 1,
per_page: 100,
total: includesDraftOrder ? 1 : 0,
},
},
})
);
});
await page.route("https://api.truckwash.io:4433/customers?search=**", async (route) => {
await route.fulfill(
json({
data: [
{
customerNumber: 424242,
name: "Acme Logistics",
city: "Roskilde",
},
],
meta: {
pagination: {
page: 1,
per_page: 100,
total: 1,
},
},
})
);
});
await page.route("https://api.truckwash.io:4433/collected-invoices**", async (route) => {
const request = route.request();
if (request.method() !== "GET") {
await route.fallback();
return;
}
const url = new URL(request.url());
const filters = url.searchParams.get("filters") || "";
if (!filters.includes("customer_number:424242")) {
await route.fulfill(json({ data: [], meta: { pagination: { page: 1, per_page: 100, total: 0 } } }));
return;
}
await route.fulfill(
json({
data: [
{
id: 9001,
customer_number: 424242,
customer_name: "Acme Logistics",
created_at: "2026-04-18 09:00:00",
closed_at: null,
booked_invoice_id: null,
total_net_amount: 100000,
},
{
id: 9002,
customer_number: 424242,
customer_name: "Acme Logistics",
created_at: "2026-04-16 09:00:00",
closed_at: "2026-04-17 00:00:00",
booked_invoice_id: null,
total_net_amount: 250000,
},
],
meta: {
pagination: {
page: 1,
per_page: 100,
total: 2,
},
},
})
);
});
await page.route("https://api.truckwash.io:4433/order/items**", async (route) => {
const request = route.request();
if (request.method() === "PUT") {
orderItemsPutPayloads.push(request.postDataJSON());
await route.fulfill(json({ success: true, data: { message: "Order item updated" } }));
return;
}
await route.fulfill(
json({
data: [
{
id: 8001,
order_id: currentOrder.id,
product_id: 101,
reference: "",
notes: "",
cashier_id: currentOrder.cashier_id,
price: 111700,
quantity: 1,
related_item_id: 0,
include_in_invoice: true,
product: {
id: 101,
name: "Truck wash",
price: 111700,
},
},
],
})
);
});
await page.route("https://api.truckwash.io:4433/products**", async (route) => {
const url = new URL(route.request().url());
productPriceRequests.push(Object.fromEntries(url.searchParams.entries()));
await route.fulfill(
json({
data: {
id: 101,
name: "Truck wash",
price: 99900,
},
})
);
});
await page.goto("/admin/1/modules/pos/drafts");
await expect(page).toHaveURL(/\/admin\/1\/modules\/pos\/drafts$/);
await expect(page.getByTestId("desktop-buefy-nav-drafts-label")).toBeVisible();
await expect(page.getByTestId("desktop-buefy-nav-drafts-label")).toContainText("Kladder");
await expect(page.getByTestId("desktop-buefy-nav-drafts-badge")).toBeVisible();
await expect(page.getByTestId("desktop-buefy-nav-drafts-badge")).toHaveText("1");
expect(pageErrors).toEqual([]);
expect(navigationRenderErrors).toEqual([]);
await expect(page.getByTestId("draft-order-assign-customer-button-56625")).toBeVisible();
await page.waitForTimeout(250);
await expect(page.getByText("ZZ99999")).toHaveCount(0);
await page.getByTestId("draft-order-assign-customer-button-56625").click();
const modal = page.getByTestId("draft-order-assign-customer-modal");
await expect(modal).toBeVisible();
await expect(page.getByTestId("draft-order-recalculate-prices")).toBeChecked();
await page.getByTestId("draft-order-assign-customer-search").fill("Acme");
await page.getByTestId("draft-order-customer-option-424242").click();
await expect(page.getByTestId("draft-order-invoice-collection-option-9001")).toBeVisible();
await page.getByTestId("draft-order-invoice-collection-option-9002").click();
await page.getByTestId("draft-order-assign-submit").click();
await expect(modal).toHaveCount(0);
await expect(page.getByTestId("draft-order-assign-customer-button-56625")).toHaveCount(0);
await expect(page.getByTestId("desktop-buefy-nav-drafts-badge")).toHaveCount(0);
expect(orderPutPayloads).toEqual([
{ id: 56625, customer_id: 424242 },
{ id: 56625, invoice_collection_id: 9002 },
]);
expect(orderItemsPutPayloads).toHaveLength(1);
expect(orderItemsPutPayloads[0]).toMatchObject({
id: 8001,
price: 99900,
quantity: 1,
});
expect(productPriceRequests).toHaveLength(1);
expect(productPriceRequests[0]).toMatchObject({
id: "101",
customer_id: "424242",
department_id: "1",
final_price: "true",
});
});
test("accepts a self-serve wash draft from the actions menu", async ({ page }, testInfo) => {
test.skip(!isDesktopProject(testInfo), "Desktop only");
await page.setViewportSize({ width: 1900, height: 900 });
await seedAuthenticatedState(page);
await mockApi(page, {
authenticated: true,
permissions: [
"admin",
"department_access_1",
"add_order",
"edit_order",
"list_orders",
"list_order_items",
"edit_order_items",
"list_products",
],
sessionData: {
runtime_config: {
economic: {
transaction_draft_customer_number: 6001,
},
},
},
});
const currentOrder = {
id: 56630,
customer_id: 6001,
customer_name: "Uspecificeret kunde",
user_id: 77,
cashier_id: 5,
cashier_name: "Jeppe",
department_id: 1,
reg_1: "EC21233",
reg_2: null,
reg_3: null,
reference: "self-serve",
notes: "",
po: "",
total_net_amount: 111700,
created_at: "2026-04-20 13:20:57",
invoice_collection_id: 7001,
invoice_collection: {
id: 7001,
customer_number: 6001,
closed_at: null,
booked_invoice_id: null,
},
economic_invoice_module: null,
stripe_invoice_module: null,
error_message: null,
pending_handheld: false,
completed_at: null,
booking_id: null,
wash_id: null,
lane: 12,
safety_seal: null,
};
const selfServeAttachment = {
id: 401,
object_type: "orders",
object_id: currentOrder.id,
content: {
image: null,
document: null,
relation: null,
other: {
type: "SELF_SERVE_WASH",
customer_number: 424242,
draft_customer_number: 6001,
subuser_id: 77,
subuser: {
name: "Driver One",
username: "driver-one",
},
license_plate: "EC21233",
elapsed_wash_time_seconds: 601,
},
src: null,
},
created_at: "2026-04-20 13:20:57",
updated_at: null,
deleted_at: null,
};
const orderPutPayloads: Array<Record<string, unknown>> = [];
const orderItemsPutPayloads: Array<Record<string, unknown>> = [];
await page.route("https://api.truckwash.io:4433/departments**", async (route) => {
await route.fulfill(
json({
data: [{ id: 1, name: "Demo", visible: true }],
})
);
});
await page.route("https://api.truckwash.io:4433/orders**", async (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.pathname.endsWith("/orders/attachments") && request.method() === "GET") {
await route.fulfill(json({ success: true, data: [selfServeAttachment] }));
return;
}
if (url.pathname !== "/orders") {
await route.fallback();
return;
}
if (request.method() === "PUT") {
const payload = request.postDataJSON();
orderPutPayloads.push(payload);
if (payload.customer_id) {
currentOrder.customer_id = Number(payload.customer_id);
currentOrder.customer_name = "Acme Logistics";
}
if (payload.invoice_collection_id) {
currentOrder.invoice_collection_id = Number(payload.invoice_collection_id);
currentOrder.invoice_collection = {
id: Number(payload.invoice_collection_id),
customer_number: currentOrder.customer_id,
closed_at: null,
booked_invoice_id: null,
};
}
await route.fulfill(json({ success: true, data: { message: "Order updated successfully" } }));
return;
}
const filters = url.searchParams.get("filters") || "";
const includesDraftOrder =
currentOrder.customer_id === 6001 &&
filters.includes("customer_id:6001") &&
filters.includes("department_id:1");
await route.fulfill(
json({
data: includesDraftOrder ? [currentOrder] : [],
meta: {
pagination: {
page: 1,
per_page: Number(url.searchParams.get("limit") || "100"),
total: includesDraftOrder ? 1 : 0,
},
},
})
);
});
await page.route("https://api.truckwash.io:4433/collected-invoices**", async (route) => {
if (route.request().method() !== "GET") {
await route.fallback();
return;
}
await route.fulfill(
json({
data: [
{
id: 9001,
customer_number: 424242,
customer_name: "Acme Logistics",
created_at: "2026-04-18 09:00:00",
closed_at: null,
booked_invoice_id: null,
total_net_amount: 100000,
},
],
meta: {
pagination: {
page: 1,
per_page: 100,
total: 1,
},
},
})
);
});
await page.route("https://api.truckwash.io:4433/order/items**", async (route) => {
if (route.request().method() === "PUT") {
orderItemsPutPayloads.push(route.request().postDataJSON());
await route.fulfill(json({ success: true, data: { message: "Order item updated" } }));
return;
}
await route.fulfill(
json({
data: [
{
id: 8001,
order_id: currentOrder.id,
product_id: 101,
reference: "",
notes: "",
cashier_id: currentOrder.cashier_id,
price: 111700,
quantity: 1,
related_item_id: 0,
include_in_invoice: true,
product: {
id: 101,
name: "Truck wash",
price: 111700,
},
},
],
})
);
});
await page.route("https://api.truckwash.io:4433/products**", async (route) => {
await route.fulfill(
json({
data: {
id: 101,
name: "Truck wash",
price: 99900,
},
})
);
});
await page.goto("/admin/1/modules/pos/drafts", { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("department-pos-drafts-page")).toBeVisible();
const settingsRoot = page.getByTestId("pos-order-list-settings-56630");
await expect(settingsRoot).toBeVisible();
await settingsRoot.locator(".dropdown-trigger > button").click();
await expect(settingsRoot.getByRole("button", { name: /Godkend selvbetjent vask/ })).toBeVisible();
await settingsRoot.getByTestId("action-settings-wheel-section-attachments").hover();
await settingsRoot.getByTestId("action-settings-wheel-attachment-row-401").hover();
await expect(settingsRoot.getByTestId("action-settings-wheel-attachment-panel")).toContainText("Selvbetjent vask");
await expect(settingsRoot.getByTestId("action-settings-wheel-attachment-panel")).toContainText("#424242");
await expect(settingsRoot.getByTestId("action-settings-wheel-attachment-panel")).toContainText("Driver One");
await settingsRoot.getByTestId("action-settings-wheel-section-order").hover();
await settingsRoot.getByRole("button", { name: /Godkend selvbetjent vask/ }).click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await page.locator(".swal2-confirm").click();
await expect
.poll(() => orderPutPayloads)
.toEqual([
{ id: 56630, customer_id: 424242 },
{ id: 56630, invoice_collection_id: 9001 },
]);
expect(orderItemsPutPayloads).toHaveLength(1);
expect(orderItemsPutPayloads[0]).toMatchObject({
id: 8001,
price: 99900,
quantity: 1,
});
await expect(settingsRoot).toHaveCount(0);
});
test("hides the draft badge when the selected department only has system drafts", async ({ page }, testInfo) => {
test.skip(!isDesktopProject(testInfo), "Desktop only");
await seedAuthenticatedState(page);
await mockApi(page, {
authenticated: true,
permissions: ["admin", "department_access_1", "add_order", "list_orders"],
sessionData: {
runtime_config: {
economic: {
transaction_draft_customer_number: 6001,
},
},
},
});
const hiddenSystemDraft = {
id: 56627,
customer_id: 6001,
customer_name: "Uspecificeret kunde",
user_id: 77,
cashier_id: 1857,
cashier_name: "System",
department_id: 1,
reg_1: "SYSTEM01",
reg_2: null,
reg_3: null,
reference: "system-draft",
notes: "",
po: "",
total_net_amount: 111700,
created_at: "2026-04-20 13:20:57",
invoice_collection_id: null,
invoice_collection: null,
economic_invoice_module: null,
stripe_invoice_module: null,
error_message: null,
pending_handheld: false,
completed_at: null,
booking_id: null,
wash_id: null,
lane: null,
safety_seal: null,
};
await page.route("https://api.truckwash.io:4433/departments**", async (route) => {
await route.fulfill(
json({
data: [{ id: 1, name: "Taastrup", visible: true }],
})
);
});
await page.route("https://api.truckwash.io:4433/orders**", async (route) => {
const url = new URL(route.request().url());
const filters = url.searchParams.get("filters") || "";
const isDraftRequest = filters.includes("customer_id:6001");
await route.fulfill(
json({
data: isDraftRequest ? [hiddenSystemDraft] : [],
meta: {
pagination: {
page: 1,
per_page: Number(url.searchParams.get("limit") || "100"),
total: isDraftRequest ? 1 : 0,
},
},
})
);
});
await page.goto("/admin/1/modules/pos/drafts");
await expect(page).toHaveURL(/\/admin\/1\/modules\/pos\/drafts$/);
await expect(page.getByTestId("department-pos-drafts-page")).toBeVisible();
await expect(page.getByText("Viser 0 vaske")).toBeVisible();
await expect(page.getByText("SYSTEM01")).toHaveCount(0);
await expect(page.getByTestId("desktop-buefy-nav-drafts-badge")).toHaveCount(0);
});
test("keeps the draft actions menu inside the viewport near the bottom edge", async ({ page }, testInfo) => {
test.skip(!isDesktopProject(testInfo), "Desktop only");
await page.setViewportSize({ width: 1280, height: 420 });
await seedAuthenticatedState(page);
await mockApi(page, {
authenticated: true,
permissions: ["superuser", "admin", "department_access_1", "list_orders"],
sessionData: {
runtime_config: {
economic: {
transaction_draft_customer_number: 6001,
},
},
},
});
const currentOrder = {
id: 56625,
customer_id: 6001,
customer_name: "Uspecificeret kunde",
user_id: 77,
cashier_id: 5,
cashier_name: "Jeppe",
department_id: 1,
reg_1: "EC21233",
reg_2: "TR12345",
reg_3: null,
reference: "reff",
notes: "",
po: "po00",
total_net_amount: 111700,
created_at: "2026-04-20 13:20:57",
invoice_collection_id: 7001,
invoice_collection: {
id: 7001,
customer_number: 6001,
closed_at: null,
booked_invoice_id: null,
},
economic_invoice_module: null,
stripe_invoice_module: null,
error_message: null,
pending_handheld: false,
completed_at: null,
booking_id: null,
wash_id: null,
lane: null,
safety_seal: null,
};
await page.route("https://api.truckwash.io:4433/departments**", async (route) => {
const url = new URL(route.request().url());
if (url.pathname !== "/departments") {
await route.fallback();
return;
}
await route.fulfill(
json({
data: [{ id: 1, name: "Demo", visible: true }],
})
);
});
await page.route("https://api.truckwash.io:4433/orders**", async (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.pathname !== "/orders" || request.method() !== "GET") {
await route.fallback();
return;
}
const filters = url.searchParams.get("filters") || "";
const isDraftRequest = filters.includes("customer_id:6001") && filters.includes("department_id:1");
await route.fulfill(
json({
data: isDraftRequest ? [currentOrder] : [],
meta: {
pagination: {
page: 1,
per_page: Number(url.searchParams.get("limit") || "100"),
total: isDraftRequest ? 1 : 0,
},
},
})
);
});
await page.goto("/admin/1/modules/pos/drafts");
const settingsRoot = page.getByTestId("pos-order-list-settings-56625");
const settingsTrigger = settingsRoot.locator(".dropdown-trigger > button");
const menu = settingsRoot.locator(".dropdown-content");
await expect(settingsTrigger).toBeVisible();
await settingsTrigger.click();
await expect(menu).toBeVisible();
const viewport = page.viewportSize();
const menuBox = await menu.boundingBox();
expect(viewport).not.toBeNull();
expect(menuBox).not.toBeNull();
expect(menuBox?.y ?? -1).toBeGreaterThanOrEqual(0);
expect((menuBox?.y ?? 0) + (menuBox?.height ?? 0)).toBeLessThanOrEqual((viewport?.height ?? 0) + 1);
});
});