Files
pleno-vue/tests/e2e/admin-pos-orders.spec.ts
T
Jeppe Bundgaard b39b458f4f Add .prettierrc.json and refactor test files for improved formatting consistency:
- 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.
2026-04-13 09:13:27 +02:00

890 lines
41 KiB
TypeScript

import { test, expect, Page } from "@playwright/test";
import { createPosFixture, mockApi, seedAuthenticatedState } from "./support/network.js";
const POS_PERMISSIONS = [
"admin",
"department_access_12",
"delete_order",
"edit_order_items",
"get_user",
"list_customer_notes",
"list_customer_attributes",
"get_custom_prices_other",
];
async function primeOperatorSession(page: Page, token = "pos-orders-token", _permissions = POS_PERMISSIONS) {
await seedAuthenticatedState(page, token);
const sessionRequest = page.waitForResponse((response) => {
return response.request().method() === "GET" && response.url().includes("/auth/session");
});
await page.goto("/login");
await sessionRequest;
}
async function createDisposableOrder(page: Page) {
const uniqueSuffix = Date.now().toString().slice(-6);
const reg1 = `PW${uniqueSuffix}`;
const reference = `E2E-${uniqueSuffix}`;
await page.goto("/admin/12/modules/pos?step=1");
await expect(page.getByTestId("pos-step-1")).toBeVisible();
await page.locator("#reg_1").fill(reg1);
await page.locator("#reference").fill(reference);
await page.locator("#pos_select_customer_input").fill("12345679");
await expect(page.locator(".customer-drop-down-select").first()).toBeVisible();
await page.locator(".customer-drop-down-select").first().click();
await expect(page.locator(".field.has-addons input[disabled]").last()).toHaveValue(
/\(TEST\) Pleno Vognmandsforretning/
);
await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click();
await expect(page).toHaveURL(/\/admin\/12\/modules\/pos\?id=\d+&customer_id=12345679&step=2/);
const url = new URL(page.url());
const orderId = Number(url.searchParams.get("id"));
expect(orderId).toBeGreaterThan(0);
return {
orderId,
reg1,
reference,
};
}
async function clickVisibleTestId(page: Page, testId: string) {
const locator = page.getByTestId(testId);
const count = await locator.count();
for (let index = 0; index < count; index += 1) {
const candidate = locator.nth(index);
if (await candidate.isVisible()) {
await candidate.click();
return;
}
}
throw new Error(`No visible element found for test id "${testId}"`);
}
async function openOrderSettings(page: Page, orderId: number) {
await page.goto(`/admin/12/modules/pos/orders/${orderId}`);
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await clickVisibleTestId(page, "pos-order-tab-settings");
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
}
async function reloadOrderSettings(page: Page, orderId: number) {
await page.reload();
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await clickVisibleTestId(page, "pos-order-tab-settings");
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
}
function getCreatedAtInput(page: Page) {
return page.getByTestId("pos-order-settings-created-at").locator("input").first();
}
async function openOrderDetail(page: Page, orderId = 54518) {
await page.goto(`/admin/12/modules/pos/orders/${orderId}`);
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
}
async function openOrderAttachments(page: Page, orderId = 54518) {
await openOrderDetail(page, orderId);
await clickVisibleTestId(page, "pos-order-tab-attachments");
await expect(page.getByTestId("pos-order-panel-attachments")).toBeVisible();
}
async function openAddAttachmentCard(page: Page) {
const addAttachmentCard = getVisibleTestId(page, "pos-order-attachments-add-card");
const attachWashCertificateButton = page.getByTestId("pos-order-attachments-attach-wash-certificate");
await expect(addAttachmentCard).toBeVisible();
if ((await attachWashCertificateButton.count()) === 0) {
await addAttachmentCard.locator(".card-header").click();
}
await expect(attachWashCertificateButton).toBeVisible();
}
async function openAddItemsPanel(page: Page) {
const addItemsPanel = page.locator('[data-testid="pos-order-add-items-panel"]:visible').first();
const addItemsBackButton = page.locator('[data-testid="pos-order-add-items-back"]:visible').first();
await page.getByTestId("pos-order-add-item").click();
await expect(addItemsPanel).toBeVisible();
await expect(addItemsBackButton).toBeVisible();
}
function getVisibleTestId(page: Page, testId: string) {
return page.locator(`[data-testid="${testId}"]:visible`).first();
}
async function expectOrderTotal(page: Page, total: number) {
await expect(page.getByTestId("pos-order-total").first()).toHaveText(`${total} DKK`);
}
async function openOrderItemEditModal(page: Page, orderItemId: number) {
await page.getByTestId(`pos-order-item-edit-${orderItemId}`).click();
await expect(page.getByTestId("pos-order-item-edit-modal")).toBeVisible();
}
async function waitForOrderItemMutation(page: Page, method: "POST" | "PUT" | "DELETE", match: RegExp | string) {
return page.waitForRequest((request) => {
if (request.method() !== method || !request.url().includes("/order/items")) {
return false;
}
return typeof match === "string" ? request.url().includes(match) : match.test(request.url());
});
}
test.describe("Admin POS Orders - desktop settings", () => {
test.beforeEach(async ({ page }, testInfo) => {
test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only order settings coverage");
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: createPosFixture(),
});
await primeOperatorSession(page);
});
test("loads order detail metadata without order_id prop/setup errors on first render", async ({ page }) => {
const consoleMessages: string[] = [];
const pageErrors: string[] = [];
page.on("console", (message) => {
if (message.type() === "warning" || message.type() === "error") {
consoleMessages.push(message.text());
}
});
page.on("pageerror", (error) => {
pageErrors.push(String(error));
});
await openOrderDetail(page);
await expect(page.getByTestId("pos-order-registration-1")).toBeVisible();
await expect(page.getByTestId("pos-order-metadata-reference")).toBeVisible();
await expect(page.getByTestId("pos-order-metadata-note")).toBeVisible();
const combinedMessages = [...consoleMessages, ...pageErrors].join("\n");
expect(combinedMessages).not.toContain("Missing required prop: order_id");
expect(combinedMessages).not.toContain('Invalid prop: type check failed for prop "order_id"');
expect(combinedMessages).not.toContain("Unhandled error during execution of setup function");
});
test("renders order detail metadata and item actions without the Excel export affordance", async ({ page }) => {
await openOrderDetail(page);
await expect(page.getByTestId("pos-order-header-export-actions")).toHaveCount(0);
await expect(page.locator('[data-auto-excel-export-button="1"]')).toHaveCount(0);
const inlineDepartment = page.getByTestId("pos-order-inline-department");
const inlineCreated = page.getByTestId("pos-order-inline-created");
await expect(inlineDepartment).toBeVisible();
await expect(inlineCreated).toBeVisible();
await expect(inlineDepartment).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
await expect(inlineCreated).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
await expect(inlineDepartment).toHaveCSS("padding-left", "0px");
await expect(inlineCreated).toHaveCSS("padding-left", "0px");
const transactionLabel = page.getByTestId("pos-order-transaction-label");
await expect(transactionLabel).toBeVisible();
const priceHeader = page.getByTestId("pos-order-price-header");
const actionsHeader = page.getByTestId("pos-order-actions-header");
await expect(priceHeader).toContainText("Pris (DKK)");
await expect(actionsHeader).toContainText("Handlinger");
await expect(actionsHeader).not.toContainText("table.actions");
const [transactionFontSize, inlineDepartmentFontSize, inlineCreatedFontSize] = await Promise.all([
transactionLabel.evaluate((element) => window.getComputedStyle(element).fontSize),
inlineDepartment.evaluate((element) => window.getComputedStyle(element).fontSize),
inlineCreated.evaluate((element) => window.getComputedStyle(element).fontSize),
]);
expect(inlineDepartmentFontSize).toBe(transactionFontSize);
expect(inlineCreatedFontSize).toBe(transactionFontSize);
const [priceHeaderStyles, actionsHeaderStyles] = await Promise.all([
priceHeader.evaluate((element) => {
const target = element.querySelector(".th-wrap") || element;
const styles = window.getComputedStyle(target);
return {
justifyContent: styles.justifyContent,
textAlign: styles.textAlign,
};
}),
actionsHeader.evaluate((element) => {
const target = element.querySelector(".th-wrap") || element;
const styles = window.getComputedStyle(target);
return {
justifyContent: styles.justifyContent,
textAlign: styles.textAlign,
};
}),
]);
expect(priceHeaderStyles.justifyContent).toBe("flex-end");
expect(priceHeaderStyles.textAlign).toBe("right");
expect(actionsHeaderStyles.justifyContent).toBe("flex-end");
expect(actionsHeaderStyles.textAlign).toBe("right");
await expect(page.getByTestId("pos-order-item-price-9101")).toHaveCSS("text-align", "right");
const licensePlatesSection = page.getByTestId("pos-order-metadata-license-plates");
const referenceSection = page.getByTestId("pos-order-metadata-reference");
const noteSection = page.getByTestId("pos-order-metadata-note");
const metadataSections = [licensePlatesSection, referenceSection, noteSection];
for (const section of metadataSections) {
await expect(section).toBeVisible();
const box = await section.boundingBox();
expect(box).not.toBeNull();
expect(box?.width ?? 0).toBeGreaterThan(120);
expect(box?.height ?? 0).toBeGreaterThan(70);
}
await expect(licensePlatesSection.locator(".skeleton-lines")).toHaveCount(0);
const reg1 = page.getByTestId("pos-order-registration-1");
const reg2Add = page.getByTestId("pos-order-registration-add-2");
const reg3Add = page.getByTestId("pos-order-registration-add-3");
await expect(reg1).toBeVisible();
await expect(reg2Add).toBeVisible();
await expect(reg3Add).toBeVisible();
const noteEmptyState = page.getByTestId("pos-order-note-empty-state");
await expect(noteEmptyState).toBeVisible();
await expect(noteEmptyState.locator(".pos-order-field__empty-pill")).toHaveText(/^\+\s+\S+/);
await expect(noteEmptyState).not.toContainText(/Ingen data/i);
await expect(noteEmptyState.locator(".pos-order-note-editor__empty-copy")).toHaveCount(0);
const reg1Box = await reg1.boundingBox();
const reg2AddBox = await reg2Add.boundingBox();
const licensePlatesBox = await licensePlatesSection.boundingBox();
const referenceSectionBox = await referenceSection.boundingBox();
const noteSectionBox = await noteSection.boundingBox();
expect(reg1Box).not.toBeNull();
expect(reg2AddBox).not.toBeNull();
expect(licensePlatesBox).not.toBeNull();
expect(referenceSectionBox).not.toBeNull();
expect(noteSectionBox).not.toBeNull();
expect(Math.abs((reg1Box?.height ?? 0) - (reg2AddBox?.height ?? 0))).toBeLessThanOrEqual(2);
expect(Math.abs((licensePlatesBox?.height ?? 0) - (referenceSectionBox?.height ?? 0))).toBeLessThanOrEqual(2);
expect(referenceSectionBox?.x ?? 0).toBeGreaterThan(licensePlatesBox?.x ?? 0);
expect(noteSectionBox?.y ?? 0).toBeGreaterThan(referenceSectionBox?.y ?? 0);
expect(Math.abs((licensePlatesBox?.x ?? 0) - (noteSectionBox?.x ?? 0))).toBeLessThanOrEqual(2);
expect(noteSectionBox?.width ?? 0).toBeGreaterThan(referenceSectionBox?.width ?? 0);
const referenceControlBox = await referenceSection.locator(".pos-order-field__control").boundingBox();
const noteControlBox = await noteSection.locator(".pos-order-field__control").boundingBox();
expect(referenceControlBox).not.toBeNull();
expect(noteControlBox).not.toBeNull();
expect(referenceControlBox?.height ?? 0).toBeLessThan(65);
expect(noteControlBox?.height ?? 0).toBeLessThan(100);
await expect(noteEmptyState).not.toContainText("pos.add_note_placeholder");
await expect(page.getByTestId("pos-order-item-edit-9101")).toBeVisible();
await expect(page.getByTestId("pos-order-item-delete-9101")).toBeVisible();
await expect(page.getByTestId("pos-order-add-item")).toBeVisible();
await expectOrderTotal(page, 1372);
await page.getByTestId("pos-order-add-item").click();
await expect(page.getByTestId("pos-order-add-items-panel").first()).toBeVisible();
await expect(page.getByTestId("pos-product-card-53").first()).toBeVisible();
});
test("keeps a readable split layout in add-items mode and restores the normal shell after backing out", async ({
page,
}) => {
await openOrderDetail(page);
const main = getVisibleTestId(page, "pos-order-main");
const beforeMainBox = await main.boundingBox();
const beforeRailBox = await getVisibleTestId(page, "pos-order-rail").boundingBox();
expect(beforeMainBox).not.toBeNull();
expect(beforeRailBox).not.toBeNull();
await openAddItemsPanel(page);
await expect(page.locator('[data-testid="pos-order-rail"]:visible')).toHaveCount(0);
const workspace = getVisibleTestId(page, "pos-order-workspace");
const productPanel = page.locator('[data-testid="pos-order-add-items-panel"]:visible').first();
const cartPanel = page.locator('[data-testid="pos-order-panel-cart"]:visible').first();
const afterMainBox = await main.boundingBox();
const workspaceBox = await workspace.boundingBox();
const productPanelBox = await productPanel.boundingBox();
const cartPanelBox = await cartPanel.boundingBox();
expect(afterMainBox).not.toBeNull();
expect(workspaceBox).not.toBeNull();
expect(productPanelBox).not.toBeNull();
expect(cartPanelBox).not.toBeNull();
expect((afterMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0)).toBeGreaterThan(200);
expect(workspaceBox?.width ?? 0).toBeGreaterThan(800);
const productPanelWidth = productPanelBox?.width ?? 0;
const cartPanelWidth = cartPanelBox?.width ?? 0;
const workspaceWidth = workspaceBox?.width ?? 0;
expect(productPanelWidth).toBeGreaterThan(470);
expect(cartPanelWidth).toBeGreaterThan(300);
expect(cartPanelWidth).toBeLessThan(420);
expect(productPanelWidth).toBeGreaterThan(cartPanelWidth);
expect(productPanelWidth / workspaceWidth).toBeGreaterThan(0.55);
expect(cartPanelWidth / workspaceWidth).toBeLessThan(0.42);
expect(productPanelBox?.x ?? 0).toBeLessThan(cartPanelBox?.x ?? 0);
await expect(getVisibleTestId(page, "pos-order-total")).toBeVisible();
await expect(getVisibleTestId(page, "pos-order-item-edit-9101")).toBeVisible();
await expect(getVisibleTestId(page, "pos-order-registration-1")).toBeVisible();
await expect(getVisibleTestId(page, "pos-order-metadata-reference")).toBeVisible();
const categoryTabs = productPanel.locator(".tabs li");
await expect(categoryTabs).toHaveCount(3);
await categoryTabs.nth(2).click();
await expect(productPanel.getByTestId("pos-product-card-64")).toBeVisible();
await expect(productPanel.getByTestId("pos-product-card-53")).toHaveCount(0);
await productPanel.getByTestId("pos-product-card-64").click();
const addRequest = waitForOrderItemMutation(page, "POST", "/order/items");
await productPanel.getByTestId("pos-add-to-cart-64").click();
await addRequest;
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
await expect(getVisibleTestId(page, "pos-order-item-name-9200")).toContainText("Vaskecertifikat");
await expectOrderTotal(page, 1397);
await page.locator('[data-testid="pos-order-add-items-back"]:visible').first().click();
await expect(page.locator('[data-testid="pos-order-add-items-panel"]:visible')).toHaveCount(0);
await expect(getVisibleTestId(page, "pos-order-rail")).toBeVisible();
const restoredMainBox = await main.boundingBox();
const restoredRailBox = await getVisibleTestId(page, "pos-order-rail").boundingBox();
expect(restoredMainBox).not.toBeNull();
expect(restoredRailBox).not.toBeNull();
expect(Math.abs((restoredMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0))).toBeLessThanOrEqual(20);
expect(Math.abs((beforeRailBox?.width ?? 0) - (restoredRailBox?.width ?? 0))).toBeLessThanOrEqual(20);
await page.reload();
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
await expectOrderTotal(page, 1397);
});
test("loads step 2 customer context after reload and keeps actions below the basket column", async ({ page }) => {
await page.goto("/admin/12/modules/pos");
await expect(page.getByTestId("pos-step-1")).toBeVisible();
await page.locator("#reg_1").fill("EC21235");
await expect(page.getByTestId("pos-step-1").locator(".pos-card-tabs > .tabs li.is-active")).toContainText("Kunde");
await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click();
await expect(page).toHaveURL(/\/admin\/12\/modules\/pos\?id=\d+&customer_id=12345679&step=2/);
const stepTwo = page.getByTestId("pos-step-2");
const cartPanel = stepTwo.getByTestId("pos-order-panel-cart");
const actions = stepTwo.getByTestId("pos-step-2-actions");
const customerName = page.locator('[data-testid="pos-order-customer-name"]:visible').first();
const licensePlatesCard = stepTwo.getByTestId("pos-order-metadata-license-plates");
const referenceCard = stepTwo.getByTestId("pos-order-metadata-reference");
await expect(stepTwo).toBeVisible();
await expect(customerName).toHaveText(/\(TEST\) Pleno Vognmandsforretning/);
await expect(actions.getByTestId("pos-next-step")).toBeVisible();
await expect(licensePlatesCard).toBeVisible();
await expect(referenceCard).toBeVisible();
const cartPanelBox = await cartPanel.boundingBox();
const actionPanelBox = await actions.boundingBox();
const licensePlatesCardBox = await licensePlatesCard.boundingBox();
const referenceCardBox = await referenceCard.boundingBox();
expect(cartPanelBox).not.toBeNull();
expect(actionPanelBox).not.toBeNull();
expect(licensePlatesCardBox).not.toBeNull();
expect(referenceCardBox).not.toBeNull();
expect(actionPanelBox?.y ?? 0).toBeGreaterThan((cartPanelBox?.y ?? 0) + (cartPanelBox?.height ?? 0) - 2);
expect(Math.abs((actionPanelBox?.x ?? 0) - (cartPanelBox?.x ?? 0))).toBeLessThanOrEqual(2);
expect(Math.abs((actionPanelBox?.width ?? 0) - (cartPanelBox?.width ?? 0))).toBeLessThanOrEqual(2);
expect(referenceCardBox?.y ?? 0).toBeGreaterThan(
(licensePlatesCardBox?.y ?? 0) + (licensePlatesCardBox?.height ?? 0) - 2
);
expect(Math.abs((referenceCardBox?.x ?? 0) - (licensePlatesCardBox?.x ?? 0))).toBeLessThanOrEqual(2);
await page.reload();
await expect(stepTwo).toBeVisible();
await expect(customerName).toHaveText(/\(TEST\) Pleno Vognmandsforretning/);
await expect(stepTwo.getByTestId("pos-next-step")).toBeVisible();
});
test("edits a primary order item in the Buefy modal and persists after reload", async ({ page }) => {
await openOrderDetail(page);
await expectOrderTotal(page, 1372);
await openOrderItemEditModal(page, 9101);
await page.getByTestId("pos-order-item-edit-price").fill("700");
await page.getByTestId("pos-order-item-edit-quantity").fill("2");
await page.getByTestId("pos-order-item-edit-notes").fill("Primary item note");
await page.getByTestId("pos-order-item-edit-reference").fill("PRIMARY-REF");
const editRequest = waitForOrderItemMutation(page, "PUT", "/order/items");
await page.getByTestId("pos-order-item-edit-save").click();
await editRequest;
await expect(page.getByTestId("pos-order-item-edit-modal")).toBeHidden();
await expect(page.getByTestId("pos-order-item-note-trigger-9101")).toBeVisible();
await expect(page.getByTestId("pos-order-item-reference-trigger-9101")).toBeVisible();
await expect(page.getByTestId("pos-order-item-quantity-9101")).toContainText("2");
await expect(page.getByTestId("pos-order-item-price-9101")).toContainText("1400");
await expectOrderTotal(page, 2123);
await page.reload();
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expect(page.getByTestId("pos-order-item-note-trigger-9101")).toBeVisible();
await expect(page.getByTestId("pos-order-item-reference-trigger-9101")).toBeVisible();
await expect(page.getByTestId("pos-order-item-price-9101")).toContainText("1400");
await expectOrderTotal(page, 2123);
});
test("edits a related addon item and preserves grouped rendering after reload", async ({ page }) => {
await openOrderDetail(page);
await openOrderItemEditModal(page, 9102);
await page.getByTestId("pos-order-item-edit-price").fill("450");
await page.getByTestId("pos-order-item-edit-notes").fill("Addon note");
const editRequest = waitForOrderItemMutation(page, "PUT", "/order/items");
await page.getByTestId("pos-order-item-edit-save").click();
await editRequest;
await expect(page.getByTestId("pos-order-item-edit-modal")).toBeHidden();
await expect(page.getByTestId("pos-order-item-note-trigger-9102")).toBeVisible();
await expect(page.getByTestId("pos-order-item-price-9102")).toContainText("450");
await expect(page.getByTestId("pos-order-item-name-9102")).toContainText("Indvendig vask Forvogn");
await expectOrderTotal(page, 1423);
await page.reload();
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expect(page.getByTestId("pos-order-item-note-trigger-9102")).toBeVisible();
await expect(page.getByTestId("pos-order-item-price-9102")).toContainText("450");
await expectOrderTotal(page, 1423);
});
test("cancels modal edits without mutating the order item", async ({ page }) => {
const putRequests: string[] = [];
page.on("request", (request) => {
if (request.method() === "PUT" && request.url().includes("/order/items")) {
putRequests.push(request.url());
}
});
await openOrderDetail(page);
await openOrderItemEditModal(page, 9104);
await page.getByTestId("pos-order-item-edit-price").fill("999");
await page.getByTestId("pos-order-item-edit-notes").fill("Do not save");
await page.getByTestId("pos-order-item-edit-cancel").click();
await expect(page.getByTestId("pos-order-item-edit-modal")).toBeHidden();
await page.waitForTimeout(200);
expect(putRequests).toHaveLength(0);
await expect(page.getByTestId("pos-order-item-price-9104")).toContainText("299");
await expect(page.getByTestId("pos-order-item-note-trigger-9104")).toHaveCount(0);
await expectOrderTotal(page, 1372);
});
test("deletes related and primary items and hides orphaned related rows after reload", async ({ page }) => {
await openOrderDetail(page);
await expectOrderTotal(page, 1372);
const deleteRelatedRequest = waitForOrderItemMutation(page, "DELETE", "id=9104");
await page.getByTestId("pos-order-item-delete-9104").click();
await deleteRelatedRequest;
await expect(page.getByTestId("pos-order-item-delete-9104")).toHaveCount(0);
await expectOrderTotal(page, 1073);
const deletePrimaryRequest = waitForOrderItemMutation(page, "DELETE", "id=9101");
await page.getByTestId("pos-order-item-delete-9101").click();
await deletePrimaryRequest;
await expect(page.getByTestId("pos-order-empty-state")).toBeVisible();
await expect(page.getByTestId("pos-order-item-delete-9102")).toHaveCount(0);
await expect(page.getByTestId("pos-order-item-delete-9103")).toHaveCount(0);
await expectOrderTotal(page, 0);
await page.reload();
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expect(page.getByTestId("pos-order-empty-state")).toBeVisible();
await expectOrderTotal(page, 0);
});
test("adds a new product from the side panel and persists the created order item", async ({ page }) => {
await openOrderDetail(page);
await expectOrderTotal(page, 1372);
await page.getByTestId("pos-order-add-item").click();
await expect(page.getByTestId("pos-order-add-items-panel").first()).toBeVisible();
await page.getByTestId("pos-product-card-53").first().click();
await expect(page.getByTestId("pos-add-to-cart-53")).toBeVisible();
const addRequest = waitForOrderItemMutation(page, "POST", "/order/items");
await page.getByTestId("pos-add-to-cart-53").click();
await addRequest;
await expect(page.getByTestId("pos-order-item-edit-9200").first()).toBeVisible();
await expect(page.getByTestId("pos-order-item-name-9200").first()).toContainText("Forvogn");
await expectOrderTotal(page, 2021);
await page.reload();
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expect(page.getByTestId("pos-order-item-edit-9200").first()).toBeVisible();
await expectOrderTotal(page, 2021);
});
test("attaches a wash certificate from the attachments tab and keeps duplicate attempts idempotent", async ({
page,
}) => {
await openOrderAttachments(page);
await openAddAttachmentCard(page);
const attachmentCards = page.locator(".card-header-title").filter({ hasText: /Attachment #|Vedhæftning #/i });
const attachWashCertificateButton = page.getByTestId("pos-order-attachments-attach-wash-certificate");
const uploadAction = page.getByTestId("pos-order-attachments-upload-action");
await expect(attachmentCards).toHaveCount(1);
await expect(attachWashCertificateButton).toBeVisible();
await expect(uploadAction).toHaveText("Upload");
const firstRequestPromise = page.waitForRequest((request) => {
if (request.method() !== "POST" || !request.url().includes("/order/wash-certificate")) {
return false;
}
const body = request.postDataJSON?.();
return Number(body?.id) === 54518;
});
await attachWashCertificateButton.click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await page.locator(".swal2-input").fill("12345");
await page.locator(".swal2-confirm").click();
const firstRequest = await firstRequestPromise;
expect(firstRequest.postDataJSON()).toMatchObject({
id: 54518,
safety_seal: "12345",
});
await expect(page.getByText(/(Attachment|Vedhæftning) #400/i)).toBeVisible();
await expect(attachmentCards).toHaveCount(2);
await expect(page.locator(".swal2-popup")).toContainText(/generated and attached|genereret og vedhæftet/i);
await expect(page.locator(".swal2-popup")).toBeHidden({ timeout: 3000 });
await getVisibleTestId(page, "pos-order-attachment-card-400").locator(".card-header").click();
await expect(page.getByTestId("pos-order-attachment-download-400")).toHaveText("Download");
const secondRequestPromise = page.waitForRequest((request) => {
if (request.method() !== "POST" || !request.url().includes("/order/wash-certificate")) {
return false;
}
const body = request.postDataJSON?.();
return Number(body?.id) === 54518;
});
await attachWashCertificateButton.click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await page.locator(".swal2-input").fill("99999");
await page.locator(".swal2-confirm").click();
const secondRequest = await secondRequestPromise;
expect(secondRequest.postDataJSON()).toMatchObject({
id: 54518,
safety_seal: "99999",
});
await expect(page.locator(".swal2-popup")).toContainText(/already attached|allerede vedhæftet/i);
await expect(attachmentCards).toHaveCount(2);
await expect(page.locator(".swal2-popup")).toBeHidden({ timeout: 3000 });
});
test("renders the settings surface and keeps destructive actions available", async ({ page }) => {
const { orderId } = await createDisposableOrder(page);
await openOrderSettings(page, orderId);
await expect(page.getByTestId("pos-order-settings-department")).toBeVisible();
await expect(page.getByTestId("pos-order-settings-created-at")).toBeVisible();
await expect(page.getByTestId("pos-order-settings-include-in-invoice")).toBeVisible();
await expect(page.getByTestId("pos-order-settings-include-helper")).not.toHaveText(/^\s*$/);
await expect(page.getByTestId("pos-order-settings-reset")).toBeVisible();
await expect(page.getByTestId("pos-order-settings-save")).toBeVisible();
await expect(page.getByTestId("pos-order-settings-delete")).toBeVisible();
});
test("persists department changes and shows department names in read-only details", async ({ page }) => {
const { orderId } = await createDisposableOrder(page);
await openOrderSettings(page, orderId);
await page.getByTestId("pos-order-settings-department").selectOption("1");
await expect(page.getByTestId("pos-order-settings-save")).toBeEnabled();
await page.getByTestId("pos-order-settings-save").click();
await expect(page.getByTestId("pos-order-settings-save")).toBeDisabled();
await reloadOrderSettings(page, orderId);
await expect(page.getByTestId("pos-order-settings-department")).toHaveValue("1");
await clickVisibleTestId(page, "pos-order-tab-details");
const detailInputs = page.getByTestId("pos-order-panel-details").locator("input");
await expect(detailInputs.first()).toHaveValue("Taastrup");
});
test("persists created_at updates and shows the Buefy datetime display after reload", async ({ page }) => {
const { orderId } = await createDisposableOrder(page);
const updatedCreatedAt = "09/04/2026 13.37";
await openOrderSettings(page, orderId);
await getCreatedAtInput(page).fill(updatedCreatedAt);
await getCreatedAtInput(page).press("Tab");
await page.getByTestId("pos-order-settings-save").click();
await expect(page.getByTestId("pos-order-settings-save")).toBeDisabled();
await reloadOrderSettings(page, orderId);
await expect(getCreatedAtInput(page)).toHaveValue(updatedCreatedAt);
await clickVisibleTestId(page, "pos-order-tab-details");
const detailInputs = page.getByTestId("pos-order-panel-details").locator("input");
await expect(detailInputs.nth(1)).toHaveValue("2026-04-09 13:37:00");
});
test("supports invoice inclusion override across inherited, excluded, and included states", async ({ page }) => {
const { orderId } = await createDisposableOrder(page);
await openOrderSettings(page, orderId);
const helper = page.getByTestId("pos-order-settings-include-helper");
const initialHelper = ((await helper.textContent()) || "").trim();
expect(initialHelper).not.toBe("");
await expect(page.getByTestId("pos-order-settings-include-in-invoice")).toHaveValue("use_department");
await page.getByTestId("pos-order-settings-include-in-invoice").selectOption("exclude");
await page.getByTestId("pos-order-settings-save").click();
await expect(page.getByTestId("pos-order-settings-save")).toBeDisabled();
await reloadOrderSettings(page, orderId);
await expect(page.getByTestId("pos-order-settings-include-in-invoice")).toHaveValue("exclude");
const excludedHelper = ((await helper.textContent()) || "").trim();
expect(excludedHelper).not.toBe("");
expect(excludedHelper).not.toBe(initialHelper);
await page.getByTestId("pos-order-settings-include-in-invoice").selectOption("include");
await page.getByTestId("pos-order-settings-save").click();
await expect(page.getByTestId("pos-order-settings-save")).toBeDisabled();
await reloadOrderSettings(page, orderId);
await expect(page.getByTestId("pos-order-settings-include-in-invoice")).toHaveValue("include");
const includedHelper = ((await helper.textContent()) || "").trim();
expect(includedHelper).not.toBe("");
expect(includedHelper).not.toBe(excludedHelper);
await page.getByTestId("pos-order-settings-department").selectOption("1");
await page.getByTestId("pos-order-settings-include-in-invoice").selectOption("use_department");
await page.getByTestId("pos-order-settings-save").click();
await expect(page.getByTestId("pos-order-settings-save")).toBeDisabled();
await reloadOrderSettings(page, orderId);
await expect(page.getByTestId("pos-order-settings-department")).toHaveValue("1");
await expect(page.getByTestId("pos-order-settings-include-in-invoice")).toHaveValue("use_department");
const inheritedExcludedHelper = ((await helper.textContent()) || "").trim();
expect(inheritedExcludedHelper).not.toBe("");
expect(inheritedExcludedHelper).not.toBe(includedHelper);
});
});
test.describe("Admin POS Orders - desktop locked states", () => {
test.beforeEach(async ({ page }, testInfo) => {
test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only order lock coverage");
});
test("hides item mutation controls when edit permission is missing", async ({ page }) => {
const limitedPermissions = POS_PERMISSIONS.filter((permission) => permission !== "edit_order_items");
await mockApi(page, {
authenticated: true,
permissions: limitedPermissions,
edgeGateways: false,
pos: createPosFixture(),
});
await primeOperatorSession(page, "pos-orders-no-edit-token", limitedPermissions);
await openOrderDetail(page);
await expect(page.getByTestId("pos-order-item-edit-9101")).toHaveCount(0);
await expect(page.getByTestId("pos-order-item-delete-9101")).toHaveCount(0);
await expect(page.getByTestId("pos-order-add-item")).toHaveCount(0);
await expectOrderTotal(page, 1372);
});
test("hides item mutation controls when the order is already invoiced", async ({ page }) => {
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: createPosFixture({
economicModuleOrdersByOrderId: {
54518: {
invoice_id: 99101,
invoice_draft_id: null,
},
},
}),
});
await primeOperatorSession(page, "pos-orders-invoiced-token");
await openOrderDetail(page);
await expect(page.getByTestId("pos-order-item-edit-9101")).toHaveCount(0);
await expect(page.getByTestId("pos-order-item-delete-9101")).toHaveCount(0);
await expect(page.getByTestId("pos-order-add-item")).toHaveCount(0);
await expectOrderTotal(page, 1372);
});
});
test.describe("Admin POS Orders - mobile smoke", () => {
test.beforeEach(async ({ page }, testInfo) => {
test.skip(!testInfo.project.name.toLowerCase().includes("mobile"), "Mobile-only order settings smoke");
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: createPosFixture(),
});
await primeOperatorSession(page, "pos-orders-mobile-token");
});
test("opens the settings tab and renders the new controls", async ({ page }) => {
await page.goto("/admin/12/modules/pos/orders/54518");
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await clickVisibleTestId(page, "pos-order-tab-settings");
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
await expect(page.getByTestId("pos-order-settings-department")).toBeVisible();
await expect(page.getByTestId("pos-order-settings-created-at")).toBeVisible();
await expect(page.getByTestId("pos-order-settings-include-in-invoice")).toBeVisible();
});
test("shows the wash certificate action in the attachments tab", async ({ page }) => {
await openOrderAttachments(page);
await openAddAttachmentCard(page);
});
test("keeps horizontal gutters around the mobile order tabs", async ({ page }) => {
await openOrderDetail(page);
const noteEmptyState = page.getByTestId("pos-order-note-empty-state");
await expect(noteEmptyState).toBeVisible();
await expect(noteEmptyState.locator(".pos-order-field__empty-pill")).toHaveText(/^\+\s+\S+/);
const tabsContainer = getVisibleTestId(page, "pos-order-mobile-tabs");
const cartTab = getVisibleTestId(page, "pos-order-tab-cart");
const containerBox = await tabsContainer.boundingBox();
const cartTabBox = await cartTab.boundingBox();
expect(containerBox).not.toBeNull();
expect(cartTabBox).not.toBeNull();
expect(cartTabBox!.x - containerBox!.x).toBeGreaterThanOrEqual(10);
expect(containerBox!.x + containerBox!.width - (cartTabBox!.x + cartTabBox!.width)).toBeGreaterThanOrEqual(10);
});
test("supports mobile item edit, add-panel access, and delete control visibility", async ({ page }) => {
await openOrderDetail(page);
await expectOrderTotal(page, 1372);
await openOrderItemEditModal(page, 9101);
await page.getByTestId("pos-order-item-edit-quantity").fill("2");
const editRequest = waitForOrderItemMutation(page, "PUT", "/order/items");
await page.getByTestId("pos-order-item-edit-save").click();
await editRequest;
await expect(page.getByTestId("pos-order-item-edit-modal")).toBeHidden();
await expectOrderTotal(page, 2021);
await page.getByTestId("pos-order-add-item").click();
await expect(page.getByTestId("pos-order-add-items-panel").first()).toBeVisible();
await page.getByTestId("pos-order-item-delete-9101").first().scrollIntoViewIfNeeded();
await expect(page.getByTestId("pos-order-item-delete-9101").first()).toBeVisible();
});
test("keeps the mobile search and reload controls inline and shows a clear card boundary", async ({ page }) => {
await page.goto("/admin/12/modules/pos/orders");
const searchInput = page.getByTestId("pagination-search-input");
const reloadActions = page.getByTestId("pagination-reload-actions");
const orderCard = page.getByTestId("pos-order-list-card-54518");
await expect(searchInput).toBeVisible();
await expect(reloadActions).toBeVisible();
await expect(orderCard).toBeVisible();
const searchBox = await searchInput.boundingBox();
const reloadBox = await reloadActions.boundingBox();
expect(searchBox).not.toBeNull();
expect(reloadBox).not.toBeNull();
const searchCenterY = (searchBox?.y ?? 0) + (searchBox?.height ?? 0) / 2;
const reloadCenterY = (reloadBox?.y ?? 0) + (reloadBox?.height ?? 0) / 2;
expect(Math.abs(searchCenterY - reloadCenterY)).toBeLessThanOrEqual(4);
expect(searchBox?.x ?? 0).toBeLessThan(reloadBox?.x ?? 0);
expect((searchBox?.x ?? 0) + (searchBox?.width ?? 0) - (reloadBox?.x ?? 0)).toBeLessThanOrEqual(4);
expect(searchBox?.width ?? 0).toBeGreaterThan(reloadBox?.width ?? 0);
await expect(orderCard).toHaveCSS("border-top-width", "1px");
await expect(orderCard).toHaveCSS("border-right-width", "1px");
await expect(orderCard).toHaveCSS("border-bottom-width", "1px");
await expect(orderCard).toHaveCSS("border-left-width", "1px");
});
test("stacks the add-items workspace on mobile and still lets the user add products", async ({ page }) => {
await openOrderDetail(page);
const main = getVisibleTestId(page, "pos-order-main");
const beforeMainBox = await main.boundingBox();
const beforeRailBox = await getVisibleTestId(page, "pos-order-rail").boundingBox();
expect(beforeMainBox).not.toBeNull();
expect(beforeRailBox).not.toBeNull();
await openAddItemsPanel(page);
await expect(page.locator('[data-testid="pos-order-rail"]:visible')).toHaveCount(0);
const productPanel = page.locator('[data-testid="pos-order-add-items-panel"]:visible').first();
const cartPanel = page.locator('[data-testid="pos-order-panel-cart"]:visible').first();
const workspace = getVisibleTestId(page, "pos-order-workspace");
const productPanelBox = await productPanel.boundingBox();
const cartPanelBox = await cartPanel.boundingBox();
const workspaceBox = await workspace.boundingBox();
expect(productPanelBox).not.toBeNull();
expect(cartPanelBox).not.toBeNull();
expect(workspaceBox).not.toBeNull();
expect(workspaceBox?.width ?? 0).toBeGreaterThan(300);
expect(Math.abs((productPanelBox?.x ?? 0) - (cartPanelBox?.x ?? 0))).toBeLessThanOrEqual(12);
expect(Math.abs((productPanelBox?.width ?? 0) - (cartPanelBox?.width ?? 0))).toBeLessThanOrEqual(24);
expect(cartPanelBox?.y ?? 0).toBeGreaterThan((productPanelBox?.y ?? 0) + 100);
await productPanel.locator("select").first().selectOption("8");
await expect(productPanel.getByTestId("pos-product-card-64")).toBeVisible();
await expect(productPanel.getByTestId("pos-product-card-53")).toHaveCount(0);
await productPanel.getByTestId("pos-product-card-64").click();
const addRequest = waitForOrderItemMutation(page, "POST", "/order/items");
await productPanel.getByTestId("pos-add-to-cart-64").click();
await addRequest;
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
await expect(getVisibleTestId(page, "pos-order-item-name-9200")).toContainText("Vaskecertifikat");
await expectOrderTotal(page, 1397);
await page.locator('[data-testid="pos-order-add-items-back"]:visible').first().click();
await expect(page.locator('[data-testid="pos-order-add-items-panel"]:visible')).toHaveCount(0);
await expect(getVisibleTestId(page, "pos-order-rail")).toBeVisible();
const restoredMainBox = await main.boundingBox();
expect(restoredMainBox).not.toBeNull();
expect(Math.abs((restoredMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0))).toBeLessThanOrEqual(20);
await page.reload();
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
await expectOrderTotal(page, 1397);
});
});