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); }); });