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", "edit_order_items", "get_user", "list_customer_notes", "list_customer_attributes", "get_custom_prices_other", ]; const SUPERUSER_POS_PERMISSIONS = [...POS_PERMISSIONS, "superuser"]; const POS_BOOT_URL = "/admin/12/modules/pos?step=1"; const DRAFT_TRANSACTION_CUSTOMER_ID = 44556677; const DRAFT_TRANSACTION_CUSTOMER_NAME = "(TEST) Draft Transaction Customer"; function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } function jsonResponse(body: unknown, status = 200) { return { status, contentType: "application/json", body: JSON.stringify(body), }; } function getPosBootStep(page: Page) { return page.locator('[data-testid="pos-step-1"]:visible, [data-testid="pos-mobile-step-1"]:visible').first(); } async function navigateToPosBootPage(page: Page, { waitForSession = false }: { waitForSession?: boolean } = {}) { let lastError: unknown; for (let attempt = 0; attempt < 3; attempt += 1) { const sessionRequest = waitForSession ? page .waitForResponse( (response) => response.request().method() === "GET" && response.url().includes("/auth/session"), { timeout: 10_000 } ) .catch(() => null) : Promise.resolve(null); try { await page.goto(POS_BOOT_URL, { waitUntil: "domcontentloaded", timeout: 30_000 }); await sessionRequest; await expect(getPosBootStep(page)).toBeVisible({ timeout: 10_000 }); return; } catch (error) { lastError = error; if (attempt === 2) { throw error; } await page.goto("about:blank").catch(() => {}); } } throw lastError; } async function primeOperatorSession(page: Page, token = "pos-orders-token", _permissions = POS_PERMISSIONS) { await seedAuthenticatedState(page, token); await navigateToPosBootPage(page, { waitForSession: true }); } 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 selectStepOneCustomer( page: Page, customerNumber: number | string, expectedName: string | RegExp = /\(TEST\) Pleno Vognmandsforretning/ ) { await page.locator("#pos_select_customer_input").fill(String(customerNumber)); 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(expectedName); } 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 waitForSwalToClose(page: Page) { await page.waitForFunction(() => { const popup = document.querySelector(".swal2-popup"); if (!popup) { return true; } return ( popup.classList.contains("swal2-hide") || popup.getAttribute("aria-hidden") === "true" || window.getComputedStyle(popup).display === "none" ); }); } 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 revisitCurrentPage(page: Page) { await page.goto(page.url(), { waitUntil: "domcontentloaded" }); } async function reloadOrderSettings(page: Page, orderId: number) { await revisitCurrentPage(page); 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 getOrderSettingsField(page: Page, field: string) { return page.locator(`[data-testid="pos-order-settings-field-${field}"]:visible`).first(); } function getOrderSettingsEditButton(page: Page, field: string) { return page.locator(`[data-testid="pos-order-settings-edit-${field}"]:visible`).first(); } function getOrderDetailField(page: Page, field: string) { return page.locator(`[data-testid="pos-order-detail-${field}"]:visible`).first(); } function getOrderDetailInput(page: Page, field: string) { return getOrderDetailField(page, field).locator("input, textarea").first(); } async function openOrderDetailsTab(page: Page) { await clickVisibleTestId(page, "pos-order-tab-details"); await expect(page.getByTestId("pos-order-panel-details")).toBeVisible(); } async function submitOrderSettingModal(page: Page, field: string, value: string, type: "input" | "select" = "input") { await getOrderSettingsEditButton(page, field).click(); const popup = page.locator(".swal2-popup"); await expect(popup).toBeVisible(); const control = popup.locator(`#${field}`).first(); await expect(control).toBeVisible(); if (type === "select") { await control.selectOption(value); } else { await control.fill(value); } await popup.locator(".swal2-confirm").click(); await expect(popup).toBeHidden({ timeout: 10000 }); } async function createInvoiceCollectionFromPicker(page: Page, closedAt: string) { const popup = page.locator(".swal2-popup"); await expect(popup).toBeVisible(); await expect(popup.locator(".tabs")).toBeVisible({ timeout: 10000 }); const createTab = popup.locator(".tabs li a").last(); await expect(createTab).toBeVisible({ timeout: 10000 }); await createTab.click(); const dateInput = popup.locator('input[type="date"]').first(); await expect(dateInput).toBeVisible({ timeout: 10000 }); await dateInput.fill(closedAt); const createRequest = page.waitForRequest((request) => { return request.method() === "POST" && request.url().includes("/collected-invoices"); }); const createResponse = page.waitForResponse((response) => { return response.request().method() === "POST" && response.url().includes("/collected-invoices"); }); const createButton = popup.locator("button.button.is-primary").first(); await expect(createButton).toBeEnabled({ timeout: 10000 }); await createButton.click(); await createRequest; await createResponse; } async function waitForOrderInvoiceCollectionSuccess(page: Page) { const successPopup = page.locator(".swal2-popup"); await expect(successPopup).toContainText(/Faktura samling ændret/i, { timeout: 10000 }); await expect(successPopup).toBeHidden({ timeout: 4000 }); } async function changeOrderCustomer(page: Page, customerNumber: string, closedAt: string) { await getOrderSettingsEditButton(page, "customer_id").click(); const customerPopup = page.locator(".swal2-popup"); await expect(customerPopup).toBeVisible(); await customerPopup.locator("#pos_select_customer_input").fill(customerNumber); const customerResult = customerPopup.locator(".customer-drop-down-select").first(); await expect(customerResult).toBeVisible(); await customerResult.click(); await expect(customerPopup.locator("#pos_select_customer_input")).toHaveCount(0, { timeout: 10000 }); await createInvoiceCollectionFromPicker(page, closedAt); await waitForOrderInvoiceCollectionSuccess(page); } async function changeOrderInvoiceCollection(page: Page, closedAt: string) { await getOrderSettingsEditButton(page, "invoice_collection_id").click(); await createInvoiceCollectionFromPicker(page, closedAt); await waitForOrderInvoiceCollectionSuccess(page); } async function openOrderDetail(page: Page, orderId = 54518) { const orderUrl = `/admin/12/modules/pos/orders/${orderId}`; for (let attempt = 0; attempt < 2; attempt += 1) { try { await page.goto(orderUrl, { waitUntil: "domcontentloaded", timeout: 30_000 }); await expect(page.getByTestId("pos-order-detail")).toBeVisible({ timeout: 10_000 }); return; } catch (error) { if (attempt === 1) { throw error; } await navigateToPosBootPage(page); } } } 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(); } async function openLowestVisibleOrderActionDropdown(page: Page) { const triggers = page.locator("tbody .action-settings-wheel-trigger:visible"); const viewportHeight = page.viewportSize()?.height ?? 720; const triggerCount = await triggers.count(); let bestIndex = -1; let bestY = -1; for (let index = 0; index < triggerCount; index += 1) { const trigger = triggers.nth(index); const box = await trigger.boundingBox(); if (!box) { continue; } if (box.y + box.height > viewportHeight - 8) { continue; } if (box.y > bestY) { bestY = box.y; bestIndex = index; } } if (bestIndex === -1) { throw new Error("No visible order action trigger was positioned within the current viewport."); } const trigger = triggers.nth(bestIndex); await trigger.click(); const activeDropdown = page.locator("tbody .dropdown.is-active").last(); await expect(activeDropdown).toBeVisible(); return activeDropdown; } 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()); }); } async function waitForOrderMutation( page: Page, method: "PUT" | "POST" | "DELETE", endpoint: "/order" | "/orders", predicate: (body: Record) => boolean ) { return page.waitForRequest((request) => { if (request.method() !== method || !request.url().includes(endpoint)) { return false; } const body = (request.postDataJSON?.() || {}) as Record; return predicate(body); }); } function createRequiredWarningsPosFixture() { const customerNumber = 12345679; const baseFixture = createPosFixture(); return createPosFixture({ customerAttributesByNumber: { [customerNumber]: [ { id: 11, customer_number: customerNumber, attribute: "requiresReferenceNumber", }, { id: 12, customer_number: customerNumber, attribute: "usePONumbers", }, ], }, ordersById: { 54518: { ...baseFixture.ordersById[54518], reference: "", po: "", }, }, }); } function createCustomerConflictPosFixture() { const baseFixture = createPosFixture(); const defaultCustomer = baseFixture.customersByNumber[12345679]; const vehicleCustomerNumber = 22334455; const vehicleCustomerName = "(TEST) Conflict Fleet"; return createPosFixture({ customersByNumber: { [vehicleCustomerNumber]: { ...defaultCustomer, id: 3, customerNumber: vehicleCustomerNumber, economic_customer: vehicleCustomerNumber, name: vehicleCustomerName, email: "conflict@example.com", mobilePhone: "22334455", }, }, vehicles: [ ...baseFixture.vehicles.filter((vehicle) => vehicle.reg !== "CONFLICT1"), { id: 7701, reg: "CONFLICT1", customer_id: vehicleCustomerNumber, customer_name: vehicleCustomerName, type: 53, status: "verified", barred: false, wash_subscription: false, addons: { enabled: 0, available: 0, list: [], }, reference: "CONFLICT-VEHICLE-REF", last_order_id: null, }, ], }); } function createDraftTransactionPosFixture() { const baseFixture = createPosFixture(); const defaultCustomer = baseFixture.customersByNumber[12345679]; return createPosFixture({ customersByNumber: { [DRAFT_TRANSACTION_CUSTOMER_ID]: { ...defaultCustomer, id: 77, customerNumber: DRAFT_TRANSACTION_CUSTOMER_ID, economic_customer: DRAFT_TRANSACTION_CUSTOMER_ID, name: DRAFT_TRANSACTION_CUSTOMER_NAME, email: "draft-transaction@example.com", mobilePhone: "44556677", }, }, collectedInvoices: [ ...baseFixture.collectedInvoices, { id: 400, customer_number: DRAFT_TRANSACTION_CUSTOMER_ID, customer_name: DRAFT_TRANSACTION_CUSTOMER_NAME, total_net_amount: 0, created_at: "2026-05-01", closed_at: null, }, ], ordersById: { 54518: { ...baseFixture.ordersById[54518], customer_id: DRAFT_TRANSACTION_CUSTOMER_ID, invoice_collection_id: 400, }, }, }); } function createOpenStripeInvoiceOrderFixture() { const baseFixture = createPosFixture(); return createPosFixture({ ordersById: { 54518: { ...baseFixture.ordersById[54518], customer_id: 999, invoice_collection_id: 200, }, }, stripeModuleOrdersByOrderId: { 54518: { id: 54518, invoice_id: "in_54518_open", customer_id: 999, url: "https://stripe.example.test/invoices/in_54518_open", created_at: "2026-04-21 13:09:03", paid: false, status: "open", amount_due: 1695, amount_paid: 0, }, }, }); } function createTallActionMenuPosFixture() { const baseFixture = createPosFixture(); const tallOrderIds = Array.from({ length: 12 }, (_, index) => 54518 + index); return createPosFixture({ ordersById: tallOrderIds.reduce>>((orders, orderId, index) => { orders[orderId] = { ...baseFixture.ordersById[54518], id: orderId, user_id: 77, booking_id: 9100 + index, customer_name: "(TEST) Pleno Vognmandsforretning", cashier_name: "Jeppe", invoice_collection_id: 101, total_net_amount: 1372, reference: `Tall action menu ${orderId}`, reg_1: `EC${21233 + index}`, reg_2: index % 2 === 0 ? `TR${11220 + index}` : "", created_at: `2026-04-21 ${String(8 + index).padStart(2, "0")}:44:07`, }; return orders; }, {}), attachmentsByOrderId: tallOrderIds.reduce>>>( (attachments, orderId) => { attachments[orderId] = Array.from({ length: 3 }, (_, index) => ({ id: 7000 + orderId * 10 + index, object_type: "orders", object_id: orderId, content: { image: null, document: `action-menu-${orderId}-${index + 1}.pdf`, relation: null, other: `action-menu-${orderId}-${index + 1}.pdf`, src: null, }, created_at: "2026-04-21 08:44:07", updated_at: "2026-04-21 08:44:07", deleted_at: null, })); return attachments; }, {} ), }); } function createAsyncActionMenuPosFixture() { const baseFixture = createPosFixture(); return createPosFixture({ ordersById: { 54518: { ...baseFixture.ordersById[54518], booking_id: 9101, invoice_collection_id: 101, reference: "Async action menu 54518", reg_1: "EC21233", }, }, attachmentsByOrderId: { 54518: Array.from({ length: 3 }, (_, index) => ({ id: 7300 + index, object_type: "orders", object_id: 54518, content: { image: null, document: `async-action-menu-54518-${index + 1}.pdf`, relation: null, other: `async-action-menu-54518-${index + 1}.pdf`, src: null, }, created_at: "2026-04-21 08:44:07", updated_at: "2026-04-21 08:44:07", deleted_at: null, })), }, }); } 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-customer-wishes")).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 customerWishesSection = page.getByTestId("pos-order-metadata-customer-wishes"); const noteSection = page.getByTestId("pos-order-metadata-note"); const metadataSections = [licensePlatesSection, customerWishesSection, 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"); const customerWishesReference = page.getByTestId("pos-order-customer-wishes-reference"); const customerWishesPo = page.getByTestId("pos-order-customer-wishes-po"); await expect(reg1).toBeVisible(); await expect(reg2Add).toBeVisible(); await expect(reg3Add).toBeVisible(); await expect(customerWishesReference).toBeVisible(); await expect(customerWishesPo).toBeVisible(); await expect(customerWishesReference).toContainText("EC21233 - Test Ref. / Intern nummer"); await expect(customerWishesPo).toContainText("+ Tilføj"); 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 customerWishesSectionBox = await customerWishesSection.boundingBox(); const noteSectionBox = await noteSection.boundingBox(); const customerWishesReferenceBox = await customerWishesReference.boundingBox(); const customerWishesPoBox = await customerWishesPo.boundingBox(); expect(reg1Box).not.toBeNull(); expect(reg2AddBox).not.toBeNull(); expect(licensePlatesBox).not.toBeNull(); expect(customerWishesSectionBox).not.toBeNull(); expect(noteSectionBox).not.toBeNull(); expect(customerWishesReferenceBox).not.toBeNull(); expect(customerWishesPoBox).not.toBeNull(); expect(Math.abs((reg1Box?.height ?? 0) - (reg2AddBox?.height ?? 0))).toBeLessThanOrEqual(2); expect(Math.abs((licensePlatesBox?.height ?? 0) - (customerWishesSectionBox?.height ?? 0))).toBeLessThanOrEqual(2); expect(customerWishesSectionBox?.x ?? 0).toBeGreaterThan(licensePlatesBox?.x ?? 0); expect(noteSectionBox?.y ?? 0).toBeGreaterThan(customerWishesSectionBox?.y ?? 0); expect(Math.abs((licensePlatesBox?.x ?? 0) - (noteSectionBox?.x ?? 0))).toBeLessThanOrEqual(2); expect(noteSectionBox?.width ?? 0).toBeGreaterThan(customerWishesSectionBox?.width ?? 0); const noteControlBox = await noteSection.locator(".pos-order-field__control").boundingBox(); expect(noteControlBox).not.toBeNull(); expect( Math.abs((customerWishesReferenceBox?.height ?? 0) - (customerWishesPoBox?.height ?? 0)) ).toBeLessThanOrEqual(2); 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 addItemsBackButton = page.locator('[data-testid="pos-order-add-items-back"]:visible').first(); const afterMainBox = await main.boundingBox(); const workspaceBox = await workspace.boundingBox(); const productPanelBox = await productPanel.boundingBox(); const cartPanelBox = await cartPanel.boundingBox(); const addItemsBackButtonBox = await addItemsBackButton.boundingBox(); expect(afterMainBox).not.toBeNull(); expect(workspaceBox).not.toBeNull(); expect(productPanelBox).not.toBeNull(); expect(cartPanelBox).not.toBeNull(); expect(addItemsBackButtonBox).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); expect(addItemsBackButtonBox?.y ?? 0).toBeGreaterThan((cartPanelBox?.y ?? 0) + (cartPanelBox?.height ?? 0) - 2); expect(Math.abs((addItemsBackButtonBox?.x ?? 0) - (cartPanelBox?.x ?? 0))).toBeLessThanOrEqual(8); 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-customer-wishes")).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.goto("/admin/12/modules/pos/orders/54518", { waitUntil: "domcontentloaded" }); 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 directly below the basket card", 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 stepTwoUrl = page.url(); const stepTwo = page.getByTestId("pos-step-2"); const orderCard = stepTwo.getByTestId("pos-order-card"); 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 customerWishesCard = stepTwo.getByTestId("pos-order-metadata-customer-wishes"); const noteCard = stepTwo.getByTestId("pos-order-metadata-note"); await expect(stepTwo).toBeVisible(); await expect(customerName()).toHaveText(/\(TEST\) Pleno Vognmandsforretning/, { timeout: 10000 }); await expect(actions.getByTestId("pos-next-step")).toBeVisible(); await expect(licensePlatesCard).toBeVisible(); await expect(customerWishesCard).toBeVisible(); await expect(noteCard).toBeVisible(); const orderCardBox = await orderCard.boundingBox(); const actionPanelBox = await actions.boundingBox(); const licensePlatesCardBox = await licensePlatesCard.boundingBox(); const customerWishesCardBox = await customerWishesCard.boundingBox(); expect(orderCardBox).not.toBeNull(); expect(actionPanelBox).not.toBeNull(); expect(licensePlatesCardBox).not.toBeNull(); expect(customerWishesCardBox).not.toBeNull(); const orderCardBottom = (orderCardBox?.y ?? 0) + (orderCardBox?.height ?? 0); expect(actionPanelBox?.y ?? 0).toBeGreaterThan(orderCardBottom - 2); expect((actionPanelBox?.y ?? 0) - orderCardBottom).toBeLessThanOrEqual(40); expect(Math.abs((actionPanelBox?.x ?? 0) - (orderCardBox?.x ?? 0))).toBeLessThanOrEqual(2); expect(Math.abs((actionPanelBox?.width ?? 0) - (orderCardBox?.width ?? 0))).toBeLessThanOrEqual(2); expect(customerWishesCardBox?.y ?? 0).toBeGreaterThan( (licensePlatesCardBox?.y ?? 0) + (licensePlatesCardBox?.height ?? 0) - 2 ); expect(Math.abs((customerWishesCardBox?.x ?? 0) - (licensePlatesCardBox?.x ?? 0))).toBeLessThanOrEqual(2); await page.goto(stepTwoUrl, { waitUntil: "domcontentloaded" }); await expect(stepTwo).toBeVisible(); await expect(page).toHaveURL(/\/admin\/12\/modules\/pos\?id=\d+&customer_id=12345679&step=2/); await expect(getVisibleTestId(page, "pos-order-registration-1")).toContainText("EC21235"); await expect(getVisibleTestId(page, "pos-order-metadata-customer-wishes")).toContainText( "EC21233 - Test Ref. / Intern nummer" ); await expect(stepTwo.getByTestId("pos-next-step")).toBeVisible(); }); test("autosaves customer wishes and note metadata after a typing pause without blur", async ({ page }) => { await openOrderDetail(page); const referenceValue = "AUTOSAVE-REF-2026"; const poValue = "AUTOSAVE-PO-2026"; const noteValue = "Autosaved desktop note"; const referenceRequest = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.reference === referenceValue ); await page.getByTestId("pos-order-customer-wishes-reference").click(); const referenceInput = page.getByTestId("pos-order-customer-wishes-reference-input"); await referenceInput.fill(referenceValue); const capturedReferenceRequest = await referenceRequest; expect(capturedReferenceRequest.postDataJSON()).toMatchObject({ id: 54518, reference: referenceValue, }); await expect(referenceInput).toBeFocused(); const poRequest = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.po === poValue ); await page.getByTestId("pos-order-customer-wishes-po").click(); const poInput = page.getByTestId("pos-order-customer-wishes-po-input"); await poInput.fill(poValue); const capturedPoRequest = await poRequest; expect(capturedPoRequest.postDataJSON()).toMatchObject({ id: 54518, po: poValue, }); await expect(poInput).toBeFocused(); const noteRequest = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.notes === noteValue ); await page.getByTestId("pos-order-metadata-note").locator("button").click(); const noteInput = page.getByTestId("pos-order-note-textarea"); await noteInput.fill(noteValue); const capturedNoteRequest = await noteRequest; expect(capturedNoteRequest.postDataJSON()).toMatchObject({ id: 54518, notes: noteValue, }); await expect(noteInput).toBeFocused(); await revisitCurrentPage(page); await expect(page.getByTestId("pos-order-detail")).toBeVisible(); await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toContainText(referenceValue); await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toContainText(poValue); await expect(page.getByTestId("pos-order-metadata-note")).toContainText(noteValue); }); test("shows empty-state pills for cleared customer wishes fields", async ({ page }) => { await openOrderDetail(page); const clearReferenceRequest = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.reference === "" ); await page.getByTestId("pos-order-customer-wishes-reference").click(); const referenceInput = page.getByTestId("pos-order-customer-wishes-reference-input"); await referenceInput.fill(""); const capturedReferenceRequest = await clearReferenceRequest; expect(capturedReferenceRequest.postDataJSON()).toMatchObject({ id: 54518, reference: "", }); await page.getByTestId("pos-order-customer-wishes-po").click(); const poInput = page.getByTestId("pos-order-customer-wishes-po-input"); const setPoRequest = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.po === "temp-po" ); await poInput.fill("temp-po"); const capturedSetPoRequest = await setPoRequest; expect(capturedSetPoRequest.postDataJSON()).toMatchObject({ id: 54518, po: "temp-po", }); const clearPoRequest = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.po === "" ); await poInput.fill(""); const capturedPoRequest = await clearPoRequest; expect(capturedPoRequest.postDataJSON()).toMatchObject({ id: 54518, po: "", }); await revisitCurrentPage(page); await expect(page.getByTestId("pos-order-detail")).toBeVisible(); await expect(page.getByTestId("pos-order-customer-wishes-reference")).toContainText("+ Tilføj"); await expect(page.getByTestId("pos-order-customer-wishes-po")).toContainText("+ Tilføj"); }); test("autosaves inline registration metadata and reloads normalized values", async ({ page }) => { await openOrderDetail(page); const rawReg1 = "xy-12 34"; const rawReg2 = "tr/56 78"; const rawReg3 = "no_90 12"; const reg1Request = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.reg_1 === "XY-12 34" ); await page.getByTestId("pos-order-registration-1").click(); const reg1Input = page.getByTestId("pos-order-registration-input-1"); await reg1Input.fill(rawReg1); const capturedReg1Request = await reg1Request; expect(capturedReg1Request.postDataJSON()).toMatchObject({ id: 54518, reg_1: "XY-12 34", }); await expect(reg1Input).toHaveValue("XY1234", { timeout: 10000 }); const reg2Request = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.reg_2 === "TR/56 78" ); await page.getByTestId("pos-order-registration-add-2").click(); const reg2Input = page.getByTestId("pos-order-registration-input-2"); await reg2Input.fill(rawReg2); const capturedReg2Request = await reg2Request; expect(capturedReg2Request.postDataJSON()).toMatchObject({ id: 54518, reg_2: "TR/56 78", }); await expect(reg2Input).toHaveValue("TR5678", { timeout: 10000 }); const reg3Request = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.reg_3 === "NO_90 12" ); await page.getByTestId("pos-order-registration-add-3").click(); const reg3Input = page.getByTestId("pos-order-registration-input-3"); await reg3Input.fill(rawReg3); const capturedReg3Request = await reg3Request; expect(capturedReg3Request.postDataJSON()).toMatchObject({ id: 54518, reg_3: "NO_90 12", }); await expect(reg3Input).toHaveValue("NO9012", { timeout: 10000 }); await revisitCurrentPage(page); await expect(page.getByTestId("pos-order-detail")).toBeVisible(); await expect(page.getByTestId("pos-order-registration-1")).toContainText("XY1234"); await expect(page.getByTestId("pos-order-registration-2")).toContainText("TR5678"); await expect(page.getByTestId("pos-order-registration-3")).toContainText("NO9012"); }); 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 revisitCurrentPage(page); 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 revisitCurrentPage(page); 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 revisitCurrentPage(page); 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 revisitCurrentPage(page); 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 waitForSwalToClose(page); 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 waitForSwalToClose(page); }); test("renders the settings surface and keeps destructive actions available", async ({ page }) => { const { orderId } = await createDisposableOrder(page); await openOrderSettings(page, orderId); await expect(getOrderSettingsField(page, "customer_id")).toBeVisible(); await expect(getOrderSettingsField(page, "department_id")).toBeVisible(); await expect(getOrderSettingsField(page, "created_at")).toBeVisible(); await expect(getOrderSettingsField(page, "include_in_invoice")).toBeVisible(); await expect(page.getByTestId("pos-order-settings-include-helper")).not.toHaveText(/^\s*$/); await expect(getOrderSettingsEditButton(page, "reference")).toBeVisible(); await expect(page.getByTestId("pos-order-settings-delete")).toBeVisible(); }); test("persists scalar metadata edits across settings and details tabs", async ({ page }) => { const { orderId } = await createDisposableOrder(page); const submittedValues = { reference: "UPDATED-REF-2026", reg_1: " zz-99 881 ", reg_2: " tr/99-882 ", reg_3: " tl_99 883 ", notes: "Updated order note", po: "PO-9001", lane: "7", wash_id: "WASH-77", booking_id: "8801", }; const expectedValues = { ...submittedValues, reg_1: "ZZ99881", reg_2: "TR99882", reg_3: "TL99883", }; await openOrderSettings(page, orderId); await submitOrderSettingModal(page, "reference", submittedValues.reference); await submitOrderSettingModal(page, "reg_1", submittedValues.reg_1); await submitOrderSettingModal(page, "reg_2", submittedValues.reg_2); await submitOrderSettingModal(page, "reg_3", submittedValues.reg_3); await submitOrderSettingModal(page, "notes", submittedValues.notes); await submitOrderSettingModal(page, "po", submittedValues.po); await submitOrderSettingModal(page, "lane", submittedValues.lane); await submitOrderSettingModal(page, "wash_id", submittedValues.wash_id); await submitOrderSettingModal(page, "booking_id", submittedValues.booking_id); await reloadOrderSettings(page, orderId); await expect(getOrderSettingsField(page, "reference")).toContainText(expectedValues.reference); await expect(getOrderSettingsField(page, "reg_1")).toContainText(expectedValues.reg_1); await expect(getOrderSettingsField(page, "reg_2")).toContainText(expectedValues.reg_2); await expect(getOrderSettingsField(page, "reg_3")).toContainText(expectedValues.reg_3); await expect(getOrderSettingsField(page, "notes")).toContainText(expectedValues.notes); await expect(getOrderSettingsField(page, "po")).toContainText(expectedValues.po); await expect(getOrderSettingsField(page, "lane")).toContainText(expectedValues.lane); await expect(getOrderSettingsField(page, "wash_id")).toContainText(expectedValues.wash_id); await expect(getOrderSettingsField(page, "booking_id")).toContainText(expectedValues.booking_id); await openOrderDetailsTab(page); await expect(getOrderDetailInput(page, "reference")).toHaveValue(expectedValues.reference); await expect(getOrderDetailInput(page, "reg_1")).toHaveValue(expectedValues.reg_1); await expect(getOrderDetailInput(page, "reg_2")).toHaveValue(expectedValues.reg_2); await expect(getOrderDetailInput(page, "reg_3")).toHaveValue(expectedValues.reg_3); await expect(getOrderDetailInput(page, "notes")).toHaveValue(expectedValues.notes); await expect(getOrderDetailInput(page, "po")).toHaveValue(expectedValues.po); await expect(getOrderDetailInput(page, "lane")).toHaveValue(expectedValues.lane); await expect(getOrderDetailInput(page, "wash_id")).toHaveValue(expectedValues.wash_id); await expect(getOrderDetailInput(page, "booking_id")).toHaveValue(expectedValues.booking_id); }); test("opens linked order bookings from the details tab using the order-booking route", async ({ page }) => { const posFixture = createPosFixture(); posFixture.ordersById[54518] = { ...posFixture.ordersById[54518], booking_id: 7001, }; posFixture.orderBookings = [ { id: 7001, customer_number: 12345679, customer_name: "(TEST) Pleno Vognmandsforretning", department: 12, datetime: "2026-04-08 10:15:00", date: "2026-04-08", reg_1: "EC21235", reg_2: "", reg_3: "", reference: "EC21233 - Test Ref. / Intern nummer", notes: "", po: "", pickup: false, items: [], order_id: 54518, }, ]; await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: posFixture, }); await openOrderDetail(page); await openOrderDetailsTab(page); const linkedBooking = page.getByTestId("pos-order-detail-booking_id").locator(".is-clickable").first(); await expect(linkedBooking).toBeVisible(); await linkedBooking.click(); await expect(page).toHaveURL(/\/admin\/12\/modules\/bookings\/order\/7001$/); await expect(page.locator("h1").first()).toContainText("#7001"); }); test("persists department, created_at, and invoice inclusion changes through per-field modals", async ({ page }) => { const { orderId } = await createDisposableOrder(page); const updatedCreatedAt = "2026-04-09T13:37"; const helper = page.getByTestId("pos-order-settings-include-helper"); await openOrderSettings(page, orderId); const initialHelper = ((await helper.textContent()) || "").trim(); expect(initialHelper).not.toBe(""); await submitOrderSettingModal(page, "department_id", "1", "select"); await submitOrderSettingModal(page, "created_at", updatedCreatedAt); await submitOrderSettingModal(page, "include_in_invoice", "exclude", "select"); await reloadOrderSettings(page, orderId); await expect(getOrderSettingsField(page, "department_id")).toContainText("Taastrup"); await expect(getOrderSettingsField(page, "created_at")).toContainText("2026-04-09 13:37:00"); await expect(getOrderSettingsField(page, "include_in_invoice")).toContainText(/Ekskluder|Exclude/); const excludedHelper = ((await helper.textContent()) || "").trim(); expect(excludedHelper).not.toBe(""); expect(excludedHelper).not.toBe(initialHelper); await submitOrderSettingModal(page, "include_in_invoice", "include", "select"); await reloadOrderSettings(page, orderId); await expect(getOrderSettingsField(page, "include_in_invoice")).toContainText(/Inkluder|Include/); const includedHelper = ((await helper.textContent()) || "").trim(); expect(includedHelper).not.toBe(""); expect(includedHelper).not.toBe(excludedHelper); await submitOrderSettingModal(page, "include_in_invoice", "use_department", "select"); await reloadOrderSettings(page, orderId); await expect(getOrderSettingsField(page, "include_in_invoice")).toContainText(/Brug afdeling|Use department/); const inheritedExcludedHelper = ((await helper.textContent()) || "").trim(); expect(inheritedExcludedHelper).not.toBe(""); expect(inheritedExcludedHelper).not.toBe(includedHelper); await openOrderDetailsTab(page); await expect(getOrderDetailInput(page, "department_id")).toHaveValue("Taastrup"); await expect(getOrderDetailInput(page, "created_at")).toHaveValue("2026-04-09 13:37:00"); await expect(getOrderDetailInput(page, "include_in_invoice")).toHaveValue(/Ekskluder|Exclude/); }); test("updates customer and invoice collection through the dedicated modal flows without a manual reload", async ({ page, }) => { const { orderId } = await createDisposableOrder(page); await openOrderSettings(page, orderId); const initialUrl = page.url(); await changeOrderCustomer(page, "999", "2026-04-30"); await expect(page).toHaveURL(initialUrl); await expect(getOrderSettingsField(page, "customer_id")).toContainText("999"); await expect(getOrderSettingsField(page, "invoice_collection_id")).toContainText("300"); await openOrderDetailsTab(page); await expect(getOrderDetailInput(page, "customer_id")).toHaveValue("999"); await expect(getOrderDetailInput(page, "invoice_collection_id")).toHaveValue("300"); await clickVisibleTestId(page, "pos-order-tab-settings"); await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible(); await changeOrderInvoiceCollection(page, "2026-05-01"); await expect(page).toHaveURL(initialUrl); await expect(getOrderSettingsField(page, "invoice_collection_id")).toContainText("301"); await openOrderDetailsTab(page); await expect(getOrderDetailInput(page, "invoice_collection_id")).toHaveValue("301"); }); test("shows a paperclip attachment control, all attachment actions, and a hover preview when the order has attachments", async ({ page, }) => { await page.goto("/admin/12/modules/pos/orders"); const attachmentDropdown = getVisibleTestId(page, "pos-order-list-attachments-54518"); const attachmentAction = attachmentDropdown.locator(".dropdown-trigger button"); const settingsAction = getVisibleTestId(page, "pos-order-list-settings-54518").locator(".dropdown-trigger button"); await expect(attachmentAction).toBeVisible(); await expect(settingsAction).toBeVisible(); const attachmentBox = await attachmentAction.boundingBox(); const settingsBox = await settingsAction.boundingBox(); expect(attachmentBox).not.toBeNull(); expect(settingsBox).not.toBeNull(); expect(attachmentBox?.x ?? 0).toBeLessThan(settingsBox?.x ?? 0); await expect(attachmentAction).toHaveClass(/is-dark/); await expect(attachmentAction.locator(".fa-paperclip")).toBeVisible(); await attachmentAction.click(); const attachmentItems = attachmentDropdown.locator(".dropdown-content button.dropdown-item-action"); await expect(attachmentItems.nth(0)).toContainText(/vaskecertifikat|wash certificate/i); await expect(attachmentItems.nth(1)).toContainText(/upload/i); await expect(attachmentItems.nth(2)).toContainText(/safety-seal\.pdf/i); const hoveredAttachmentItem = page.getByTestId("pos-order-list-attachment-item-54518-301"); await hoveredAttachmentItem.hover(); const dropdownContent = attachmentDropdown.locator(".dropdown-content"); const previewPanel = page.getByTestId("pos-order-list-attachment-preview-54518"); await expect(previewPanel).toBeVisible(); await expect(previewPanel.locator("iframe")).toBeVisible(); const dropdownContentBox = await dropdownContent.boundingBox(); const previewPanelBox = await previewPanel.boundingBox(); expect(dropdownContentBox).not.toBeNull(); expect(previewPanelBox).not.toBeNull(); expect(previewPanelBox?.x ?? 0).toBeLessThan(dropdownContentBox?.x ?? 0); }); test("shows an icon-only plus attachment control and outlined dropdown when the order has no attachments", async ({ page, }) => { const { orderId } = await createDisposableOrder(page); await page.goto("/admin/12/modules/pos/orders"); const paperclipAction = getVisibleTestId(page, "pos-order-list-attachments-54518").locator( ".dropdown-trigger button" ); const attachmentDropdown = getVisibleTestId(page, `pos-order-list-attachments-${orderId}`); const attachmentAction = attachmentDropdown.locator(".dropdown-trigger button"); const settingsAction = getVisibleTestId(page, `pos-order-list-settings-${orderId}`).locator( ".dropdown-trigger button" ); await expect(attachmentAction).toBeVisible(); await expect(settingsAction).toBeVisible(); await expect(attachmentAction).toHaveClass(/action-settings-wheel-trigger--text/); await expect(attachmentAction).not.toHaveClass(/is-text/); await expect(attachmentAction).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); await expect(attachmentAction).toHaveCSS("text-decoration-line", "none"); await expect(attachmentAction).toHaveCSS("justify-content", "center"); await expect(attachmentAction.locator(".fa-plus")).toBeVisible(); await expect(attachmentAction.locator(".ml-2")).toHaveCount(0); const paperclipBox = await paperclipAction.boundingBox(); const attachmentBox = await attachmentAction.boundingBox(); const settingsBox = await settingsAction.boundingBox(); const plusIconBox = await attachmentAction.locator(".fa-plus").boundingBox(); expect(paperclipBox).not.toBeNull(); expect(attachmentBox).not.toBeNull(); expect(settingsBox).not.toBeNull(); expect(plusIconBox).not.toBeNull(); expect(Math.abs((attachmentBox?.width ?? 0) - (paperclipBox?.width ?? 0))).toBeLessThanOrEqual(1); expect(Math.abs((attachmentBox?.height ?? 0) - (paperclipBox?.height ?? 0))).toBeLessThanOrEqual(1); expect( Math.abs( (plusIconBox?.x ?? 0) + (plusIconBox?.width ?? 0) / 2 - ((attachmentBox?.x ?? 0) + (attachmentBox?.width ?? 0) / 2) ) ).toBeLessThanOrEqual(1); expect( Math.abs( (plusIconBox?.y ?? 0) + (plusIconBox?.height ?? 0) / 2 - ((attachmentBox?.y ?? 0) + (attachmentBox?.height ?? 0) / 2) ) ).toBeLessThanOrEqual(1); expect(attachmentBox?.x ?? 0).toBeLessThan(settingsBox?.x ?? 0); await attachmentAction.click(); const dropdownContent = attachmentDropdown.locator(".dropdown-content"); const attachmentItems = dropdownContent.locator("button.dropdown-item-action"); await expect(dropdownContent).toBeVisible(); await expect(dropdownContent).toHaveCSS("border-top-width", "1px"); await expect(dropdownContent).toHaveCSS("border-right-width", "1px"); await expect(dropdownContent).toHaveCSS("border-bottom-width", "1px"); await expect(dropdownContent).toHaveCSS("border-left-width", "1px"); await expect(dropdownContent).toHaveCSS("border-top-style", "solid"); await expect(attachmentItems.nth(0)).toContainText(/vaskecertifikat|wash certificate/i); await expect(attachmentItems.nth(1)).toContainText(/upload/i); }); }); test("hides the draft export action in the order rail for non-superusers", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only order rail coverage"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await primeOperatorSession(page, "pos-orders-order-rail-token"); await openOrderDetail(page); const draftExportFlow = page.getByTestId("economic-draft-export-flow"); const invoiceExportFlow = page.getByTestId("economic-invoice-export-flow"); const receiptButton = page.getByTestId("pos-order-print-receipt"); await expect(draftExportFlow).toHaveCount(0); await expect(invoiceExportFlow).toHaveCount(0); await expect(receiptButton).toBeVisible(); await expect(receiptButton).toContainText("Kvittering"); const popupPromise = page.waitForEvent("popup"); await receiptButton.click(); const receiptPopup = await popupPromise; await receiptPopup.waitForLoadState("domcontentloaded"); await expect(receiptPopup.locator('[data-testid="order-print-receipt"]')).toBeVisible(); await expect(receiptPopup.locator('[data-testid="order-print-receipt-table"] tbody tr')).toHaveCount(4); await expect(receiptPopup.locator("body")).toContainText("Transaktions ID: 54518"); await expect(receiptPopup.locator("body")).toContainText("Betalingsstatus"); await expect(receiptPopup.locator("body")).not.toContainText("Download Excel"); }); test("shows the draft export and receipt actions in the order rail for superusers", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only order rail coverage"); await mockApi(page, { authenticated: true, permissions: SUPERUSER_POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await primeOperatorSession(page, "pos-orders-order-rail-superuser-token", SUPERUSER_POS_PERMISSIONS); await openOrderDetail(page); const draftExportFlow = page.getByTestId("economic-draft-export-flow"); const draftExportButton = page.getByTestId("economic-draft-export-submit"); const invoiceExportFlow = page.getByTestId("economic-invoice-export-flow"); const receiptButton = page.getByTestId("pos-order-print-receipt"); await expect(draftExportFlow).toBeVisible(); await expect(draftExportButton).toContainText("Send til e-conomic"); await expect(invoiceExportFlow).toHaveCount(0); await expect(receiptButton).toBeVisible(); await expect(receiptButton).toContainText("Kvittering"); const [draftExportWidth, receiptButtonWidth] = await Promise.all([ draftExportButton.evaluate((element) => element.getBoundingClientRect().width), receiptButton.evaluate((element) => element.getBoundingClientRect().width), ]); expect(Math.abs(draftExportWidth - receiptButtonWidth)).toBeLessThanOrEqual(2); const popupPromise = page.waitForEvent("popup"); await receiptButton.click(); const receiptPopup = await popupPromise; await receiptPopup.waitForLoadState("domcontentloaded"); await expect(receiptPopup.locator('[data-testid="order-print-receipt"]')).toBeVisible(); await expect(receiptPopup.locator('[data-testid="order-print-receipt-table"] tbody tr')).toHaveCount(4); await expect(receiptPopup.locator("body")).toContainText("Transaktions ID: 54518"); await expect(receiptPopup.locator("body")).toContainText("Betalingsstatus"); await expect(receiptPopup.locator("body")).not.toContainText("Download Excel"); }); test("shows live Stripe payment status and cancellation for open card payment orders", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only Stripe order rail coverage"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createOpenStripeInvoiceOrderFixture(), }); await primeOperatorSession(page, "pos-orders-open-stripe-order-token"); await openOrderDetail(page); const stripePanel = page.getByTestId("pos-order-stripe-email-panel"); const deleteStripeInvoiceRequest = page.waitForRequest((request) => { if (request.method() !== "DELETE" || !request.url().includes("/modules/stripe/invoice")) { return false; } const body = (request.postDataJSON?.() || {}) as Record; return Number(body.order_id) === 54518; }); await expect(stripePanel).toBeVisible(); await expect(page.getByTestId("pos-order-stripe-email-status")).toHaveText(/Afventer betaling|Awaiting payment/); await expect(page.getByTestId("pos-order-stripe-email-link")).toBeVisible(); await expect(page.getByTestId("pos-order-stripe-email-cancel")).toBeVisible(); await expect(page.getByTestId("pos-order-print-receipt")).toHaveCount(0); await expect(page.getByRole("button", { name: /Fuldfør ordre|Complete order/ })).toHaveCount(0); await expect(page.getByRole("button", { name: /Hent faktura|Get invoice/ })).toHaveCount(0); await page.getByTestId("pos-order-stripe-email-cancel").click(); await deleteStripeInvoiceRequest; await expect(page.getByTestId("pos-order-stripe-email-panel")).toHaveCount(0); await expect(page.getByTestId("pos-order-stripe-email-cancel")).toHaveCount(0); }); test.describe("Admin POS Orders - draft transaction customer", () => { test.beforeEach(async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only draft customer coverage"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createDraftTransactionPosFixture(), sessionData: { runtime_config: { economic: { transaction_draft_customer_number: DRAFT_TRANSACTION_CUSTOMER_ID, }, }, }, }); await primeOperatorSession(page, "pos-orders-draft-customer-token"); }); test("shows the desktop quick action for the configured draft customer and allows switching back to a regular customer", async ({ page, }) => { await page.goto(POS_BOOT_URL); await expect(page.getByTestId("pos-draft-customer-quick-action")).toBeVisible(); await page.getByTestId("pos-draft-customer-quick-action").click(); await expect(page.locator(".field.has-addons input[disabled]").last()).toHaveValue(DRAFT_TRANSACTION_CUSTOMER_NAME); await page.getByRole("button", { name: "Clear" }).click(); await selectStepOneCustomer(page, 12345679); await expect(page.locator(".field.has-addons input[disabled]").last()).toHaveValue( /\(TEST\) Pleno Vognmandsforretning/ ); }); test("opens the draft customer assignment modal from the blocked warnings and restores export actions after selecting a real customer", async ({ page, }) => { await openOrderDetail(page); const warningAction = page.getByTestId("pos-order-economic-export-blocked-action"); const railWarningAction = page.getByTestId("pos-order-economic-export-blocked-rail-action"); const cardPaymentAction = page.getByTestId("pos-order-switch-to-card-payment"); await expect(page.getByTestId("pos-order-draft-transaction-badge")).toBeVisible(); await expect(page.getByText(/Gemt til fakturering/i)).toHaveCount(0); await expect(page.getByTestId("pos-order-economic-export-blocked")).toBeVisible(); await expect(page.getByTestId("pos-order-economic-export-blocked-rail")).toBeVisible(); await expect(warningAction).toBeVisible(); await expect(railWarningAction).toBeVisible(); await expect(warningAction).toHaveClass(/is-light/); await expect(warningAction).toHaveClass(/is-warning/); await expect(railWarningAction).toHaveClass(/is-light/); await expect(railWarningAction).toHaveClass(/is-warning/); await expect(page.getByTestId("economic-draft-export-flow")).toHaveCount(0); await expect(page.getByTestId("economic-invoice-export-flow")).toHaveCount(0); await expect(cardPaymentAction).toBeVisible(); await expect(page.getByTestId("pos-order-print-receipt")).toHaveCount(0); const modal = page.getByTestId("draft-order-assign-customer-modal"); await railWarningAction.click(); await expect(modal).toBeVisible(); await modal.locator(".delete").click(); await expect(modal).toHaveCount(0); await warningAction.click(); await expect(modal).toBeVisible(); await expect(page.getByTestId("draft-order-recalculate-prices")).toBeChecked(); await page.getByTestId("draft-order-assign-customer-search").fill("12345679"); await page.getByTestId("draft-order-customer-option-12345679").click(); await expect(page.getByTestId("draft-order-invoice-collection-option-101")).toBeVisible(); await page.getByTestId("draft-order-invoice-collection-option-101").click(); await page.getByTestId("draft-order-assign-submit").click(); await expect(modal).toHaveCount(0); await expect(page.getByTestId("pos-order-draft-transaction-badge")).toHaveCount(0); await expect(page.getByTestId("pos-order-economic-export-blocked")).toHaveCount(0); await expect(page.getByTestId("pos-order-economic-export-blocked-rail")).toHaveCount(0); await expect(page.getByTestId("pos-order-economic-export-blocked-action")).toHaveCount(0); await expect(page.getByTestId("pos-order-economic-export-blocked-rail-action")).toHaveCount(0); await expect(page.getByTestId("economic-draft-export-flow")).toHaveCount(0); await expect(page.getByTestId("economic-invoice-export-flow")).toHaveCount(0); await expect(cardPaymentAction).toHaveCount(0); await expect(page.getByTestId("pos-order-print-receipt")).toBeVisible(); }); test("switches a draft transaction into the card payment flow from the rail action", async ({ page }) => { await openOrderDetail(page); const updateCustomerRequest = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && Number(body.customer_id) === 999 ); const updateInvoiceCollectionRequest = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && Number(body.invoice_collection_id) > 0 ); await page.getByTestId("pos-order-switch-to-card-payment").click(); const [customerMutation, invoiceCollectionMutation] = await Promise.all([ updateCustomerRequest, updateInvoiceCollectionRequest, page.waitForURL(/\/admin\/12\/modules\/pos\?id=54518&customer_id=999&step=3/), ]); expect(customerMutation.postDataJSON()).toMatchObject({ id: 54518, customer_id: 999, }); expect(Number(invoiceCollectionMutation.postDataJSON().invoice_collection_id)).toBeGreaterThan(0); const stepThree = page.getByTestId("pos-step-3"); await expect(stepThree).toBeVisible(); await expect(stepThree.getByTestId("pos-order-customer-name")).toContainText("Card Terminal Customer"); }); }); test.describe("Admin POS wash certificate completion", () => { test("shows and autosaves the safety seal field when the order contains a wash certificate item", async ({ page, }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only wash certificate metadata coverage"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await primeOperatorSession(page, "pos-safety-seal-token"); await openOrderDetail(page); const safetySealField = page.getByTestId("pos-order-customer-wishes-safety-seal"); const referenceControl = page.getByTestId("pos-order-customer-wishes-reference-control"); const poControl = page.getByTestId("pos-order-customer-wishes-po-control"); const safetySealControl = page.getByTestId("pos-order-customer-wishes-safety-seal-control"); await expect(safetySealField).toBeVisible(); await expect(referenceControl).toBeVisible(); await expect(poControl).toBeVisible(); await expect(safetySealControl).toBeVisible(); const [referenceBox, poBox, safetySealBox] = await Promise.all([ referenceControl.boundingBox(), poControl.boundingBox(), safetySealControl.boundingBox(), ]); expect(referenceBox).not.toBeNull(); expect(poBox).not.toBeNull(); expect(safetySealBox).not.toBeNull(); expect(safetySealBox!.width).toBeGreaterThan(referenceBox!.width * 1.8); expect(safetySealBox!.width).toBeGreaterThan(poBox!.width * 1.8); const updateRequest = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.safety_seal === "SEAL-5566" ); await safetySealField.click(); const safetySealInput = page.getByTestId("pos-order-customer-wishes-safety-seal-input"); await expect(safetySealInput).toBeVisible(); await safetySealInput.fill("SEAL-5566"); const capturedRequest = await updateRequest; expect(capturedRequest.postDataJSON()).toMatchObject({ id: 54518, safety_seal: "SEAL-5566", }); await expect(safetySealInput).toBeFocused(); await expect(safetySealInput).toHaveValue("SEAL-5566"); }); test("refreshes the attachments tab after regenerating a wash certificate from a safety seal edit", async ({ page, }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only wash certificate refresh coverage"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await primeOperatorSession(page, "pos-safety-seal-refresh-token"); await openOrderAttachments(page); await openAddAttachmentCard(page); const attachWashCertificateButton = page.getByTestId("pos-order-attachments-attach-wash-certificate"); await attachWashCertificateButton.click(); await expect(page.locator(".swal2-popup")).toBeVisible(); await page.locator(".swal2-input").fill("SEAL-INITIAL"); await page.locator(".swal2-confirm").click(); await expect(page.getByTestId("pos-order-attachment-card-400")).toBeVisible(); await waitForSwalToClose(page); await clickVisibleTestId(page, "pos-order-tab-cart"); await expect(page.getByTestId("pos-order-panel-cart")).toBeVisible(); const updateRequest = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.safety_seal === "SEAL-UPDATED" ); await page.getByTestId("pos-order-customer-wishes-safety-seal").click(); const safetySealInput = page.getByTestId("pos-order-customer-wishes-safety-seal-input"); await expect(safetySealInput).toBeVisible(); await safetySealInput.fill("SEAL-UPDATED"); await updateRequest; await clickVisibleTestId(page, "pos-order-tab-attachments"); await expect(page.getByTestId("pos-order-panel-attachments")).toBeVisible(); await expect(page.getByTestId("pos-order-attachment-card-401")).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId("pos-order-attachment-card-400")).toHaveCount(0); }); test("defers material desktop completion to step 4 and auto-attaches the wash certificate on final completion", async ({ page, }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only completion sequencing coverage"); const baseFixture = createPosFixture(); const materialOrderId = 56001; const fixture = createPosFixture({ ordersById: { ...baseFixture.ordersById, [materialOrderId]: { ...baseFixture.ordersById[54518], id: materialOrderId, reference: "AUTO-SEAL-REF", completed_at: null, }, }, orderItemsByOrderId: { ...baseFixture.orderItemsByOrderId, [materialOrderId]: [ { ...baseFixture.orderItemsByOrderId[54518][0], id: 9801, order_id: materialOrderId, related_item_id: null, }, { ...baseFixture.orderItemsByOrderId[54518][2], id: 9802, order_id: materialOrderId, related_item_id: 9801, }, ], }, attachmentsByOrderId: { ...baseFixture.attachmentsByOrderId, [materialOrderId]: [], }, }); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: fixture, }); await primeOperatorSession(page, "pos-material-sequencing-token"); await page.goto(`/admin/12/modules/pos?id=${materialOrderId}&customer_id=12345679&step=3`); await expect(page.getByTestId("pos-step-3")).toBeVisible(); await clickVisibleTestId(page, "pos-next-step"); await expect(page.getByTestId("pos-step-4")).toBeVisible(); expect(fixture.ordersById[materialOrderId]?.completed_at).toBeNull(); await expect(page.getByTestId("pos-step-4").getByTestId("pos-next-step")).toBeVisible({ timeout: 10_000 }); const completionRequest = page.waitForRequest((request) => { return request.method() === "POST" && request.url().includes("/orders/mark_as_completed"); }); await clickVisibleTestId(page, "pos-next-step"); await completionRequest; await expect.poll(() => Boolean(fixture.ordersById[materialOrderId]?.completed_at), { timeout: 10_000 }).toBe(true); await expect .poll( () => Boolean( fixture.attachmentsByOrderId[materialOrderId]?.some( (attachment) => String(attachment?.content?.other || "").toUpperCase() === "WASH_CERTIFICATE" ) ), { timeout: 10_000 } ) .toBe(true); }); }); test.describe("Admin POS Orders - desktop required warning states", () => { test.beforeEach(async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only required warning coverage"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createRequiredWarningsPosFixture(), }); await primeOperatorSession(page, "pos-orders-required-warnings-token"); }); test("shows required reference and po warnings in order detail and clears them without changing field size", async ({ page, }) => { await openOrderDetail(page); const referenceControl = page.getByTestId("pos-order-customer-wishes-reference-control"); const poControl = page.getByTestId("pos-order-customer-wishes-po-control"); await expect(referenceControl).toHaveAttribute("data-warning-state", "danger"); await expect(poControl).toHaveAttribute("data-warning-state", "warning"); await expect(page.getByTestId("pos-order-customer-wishes-reference-warning-icon")).toBeVisible(); await expect(page.getByTestId("pos-order-customer-wishes-po-warning-icon")).toBeVisible(); const warningReferenceBox = await referenceControl.boundingBox(); const warningPoBox = await poControl.boundingBox(); expect(warningReferenceBox).not.toBeNull(); expect(warningPoBox).not.toBeNull(); expect(Math.abs((warningReferenceBox?.height ?? 0) - (warningPoBox?.height ?? 0))).toBeLessThanOrEqual(2); const referenceValue = "REF1"; const poValue = "PO1"; const referenceRequest = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.reference === referenceValue ); await page.getByTestId("pos-order-customer-wishes-reference").click(); const referenceInput = page.getByTestId("pos-order-customer-wishes-reference-input"); await referenceInput.fill(referenceValue); await expect(referenceControl).not.toHaveAttribute("data-warning-state", "danger"); await expect(page.getByTestId("pos-order-customer-wishes-reference-warning-icon")).toHaveCount(0); await referenceInput.press("Enter"); await expect(referenceInput).toHaveCount(0); await referenceRequest; await expect(referenceControl).not.toHaveClass(/is-loading/); const poRequest = waitForOrderMutation( page, "PUT", "/orders", (body) => Number(body.id) === 54518 && body.po === poValue ); await page.getByTestId("pos-order-customer-wishes-po").click(); const poInput = page.getByTestId("pos-order-customer-wishes-po-input"); await poInput.fill(poValue); await expect(poControl).not.toHaveAttribute("data-warning-state", "warning"); await expect(page.getByTestId("pos-order-customer-wishes-po-warning-icon")).toHaveCount(0); await poInput.press("Enter"); await expect(poInput).toHaveCount(0); await poRequest; await expect(poControl).not.toHaveClass(/is-loading/); await expect(page.getByTestId("pos-order-customer-wishes-reference")).toContainText(referenceValue); await expect(page.getByTestId("pos-order-customer-wishes-po")).toContainText(poValue); await expect(referenceControl).not.toHaveClass(/is-loading/); await expect(poControl).not.toHaveClass(/is-loading/); const clearedReferenceBox = await referenceControl.boundingBox(); const clearedPoBox = await poControl.boundingBox(); expect(clearedReferenceBox).not.toBeNull(); expect(clearedPoBox).not.toBeNull(); expect(Math.abs((clearedReferenceBox?.height ?? 0) - (clearedPoBox?.height ?? 0))).toBeLessThanOrEqual(2); }); test("shows the same required warnings in the shared desktop step 2 workspace", async ({ page }) => { await page.goto("/admin/12/modules/pos?step=1"); await expect(page.getByTestId("pos-step-1")).toBeVisible(); await page.locator("#reg_1").fill("WARN123"); 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 page.getByTestId("pos-step-1").getByTestId("pos-next-step").click(); const stepTwo = page.getByTestId("pos-step-2"); await expect(stepTwo).toBeVisible(); await expect(stepTwo.getByTestId("pos-order-customer-wishes-reference-control")).toHaveAttribute( "data-warning-state", "danger" ); await expect(stepTwo.getByTestId("pos-order-customer-wishes-po-control")).toHaveAttribute( "data-warning-state", "warning" ); await expect(stepTwo.getByTestId("pos-order-customer-wishes-reference-warning-icon")).toBeVisible(); await expect(stepTwo.getByTestId("pos-order-customer-wishes-po-warning-icon")).toBeVisible(); }); }); test.describe("Admin POS Orders - desktop step 1 customer and vehicle ownership", () => { test.beforeEach(async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only step 1 customer ownership coverage"); }); test("preserves a manually selected required-reference customer and manual reference while editing reg_1", async ({ page, }) => { await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createRequiredWarningsPosFixture(), }); await primeOperatorSession(page, "pos-orders-manual-customer-reg-edit-token"); await page.goto("/admin/12/modules/pos?step=1"); await expect(page.getByTestId("pos-step-1")).toBeVisible(); await page.locator("#reg_1").fill("MAN"); await selectStepOneCustomer(page, 12345679); const referenceInput = page.locator("#reference"); await referenceInput.fill("MANUAL-REF-001"); await page.locator("#reg_1").fill("MANUAL1"); await expect( page .locator(".pos-selected-customer__title") .filter({ hasText: /\(TEST\) Pleno/ }) .first() ).toBeVisible(); await expect(referenceInput).toHaveValue("MANUAL-REF-001"); await page.locator("#reg_1").fill(""); await expect( page .locator(".pos-selected-customer__title") .filter({ hasText: /\(TEST\) Pleno/ }) .first() ).toBeVisible(); await expect(referenceInput).toHaveValue("MANUAL-REF-001"); await page.locator("#reg_1").fill("MANUAL2"); await expect( page .locator(".pos-selected-customer__title") .filter({ hasText: /\(TEST\) Pleno/ }) .first() ).toBeVisible(); await expect(referenceInput).toHaveValue("MANUAL-REF-001"); }); test("asks whether to keep the selected customer when an exact plate belongs to another customer", async ({ page, }) => { const fixture = createCustomerConflictPosFixture(); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: fixture, }); await primeOperatorSession(page, "pos-orders-customer-conflict-keep-token"); await page.goto("/admin/12/modules/pos?step=1"); await expect(page.getByTestId("pos-step-1")).toBeVisible(); await page.locator("#reg_1").fill("CON"); await selectStepOneCustomer(page, 12345679); await page.locator("#reference").fill("KEEP-REF"); await page.locator("#reg_1").fill("CONFLICT1"); const activeConflictModal = page.locator('[data-testid="pos-desktop-customer-conflict-modal"].is-active'); await expect(activeConflictModal).toBeVisible(); await activeConflictModal.getByTestId("pos-desktop-customer-conflict-keep").click(); await expect(activeConflictModal).toHaveCount(0); await expect( page .locator(".pos-selected-customer__title") .filter({ hasText: /\(TEST\) Pleno/ }) .first() ).toBeVisible(); await expect(page.locator("#reference")).toHaveValue("KEEP-REF"); await expect(page.locator("#reg_1")).toHaveValue("CONFLICT1"); 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/); }); test("can switch to the matched vehicle customer from the conflict modal", async ({ page }) => { const fixture = createCustomerConflictPosFixture(); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: fixture, }); await primeOperatorSession(page, "pos-orders-customer-conflict-use-vehicle-token"); await page.goto("/admin/12/modules/pos?step=1"); await expect(page.getByTestId("pos-step-1")).toBeVisible(); await page.locator("#reg_1").fill("CON"); await selectStepOneCustomer(page, 12345679); await page.locator("#reference").fill("MANUAL-REF"); await page.locator("#reg_1").fill("CONFLICT1"); const activeConflictModal = page.locator('[data-testid="pos-desktop-customer-conflict-modal"].is-active'); await expect(activeConflictModal).toBeVisible(); await activeConflictModal.getByTestId("pos-desktop-customer-conflict-use-vehicle").click(); await expect(activeConflictModal).toHaveCount(0); await expect( page .locator(".pos-selected-customer__title") .filter({ hasText: /\(TEST\) Conflict Fleet/ }) .first() ).toBeVisible(); await expect(page.locator("#reference")).toHaveValue("CONFLICT-VEHICLE-REF"); await expect(page.locator("#reg_1")).toHaveValue("CONFLICT1"); await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click(); await expect(page).toHaveURL(/\/admin\/12\/modules\/pos\?id=\d+&customer_id=22334455&step=2/); }); }); test.describe("Admin POS Orders - desktop attachment discovery", () => { test.beforeEach(async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only attachment discovery coverage"); const posFixture = createPosFixture(); posFixture.ordersById[54518] = { ...posFixture.ordersById[54518], attachments: [], has_attachments: false, attachments_count: 0, attachment_count: 0, attachmentsCount: 0, }; await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: posFixture, }); await primeOperatorSession(page, "pos-orders-attachment-discovery-token"); }); test("discovers fetched attachments and switches the list trigger from plus to paperclip", async ({ page }) => { await page.goto("/admin/12/modules/pos/orders"); const attachmentDropdown = getVisibleTestId(page, "pos-order-list-attachments-54518"); const attachmentAction = attachmentDropdown.locator(".dropdown-trigger button"); await expect.poll(async () => attachmentAction.locator(".fa-paperclip").count(), { timeout: 10000 }).toBe(1); await expect(attachmentAction).toHaveClass(/is-dark/); await expect(attachmentAction.locator(".fa-plus")).toHaveCount(0); await attachmentAction.click(); const attachmentItems = attachmentDropdown.locator(".dropdown-content button.dropdown-item-action"); await expect(attachmentItems.nth(0)).toContainText(/vaskecertifikat|wash certificate/i); await expect(attachmentItems.nth(1)).toContainText(/upload/i); await expect(attachmentItems.nth(2)).toContainText(/safety-seal\.pdf/i); }); }); test.describe("Admin POS Orders - desktop action menu layout", () => { test.beforeEach(async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only action menu layout coverage"); await mockApi(page, { authenticated: true, permissions: SUPERUSER_POS_PERMISSIONS, edgeGateways: false, pos: createTallActionMenuPosFixture(), }); await primeOperatorSession(page, "pos-orders-action-menu-layout-token", SUPERUSER_POS_PERMISSIONS); }); test("keeps tall menus within the usable viewport and starts from the first action", async ({ page }) => { await page.goto("/admin/12/modules/pos/orders"); await expect(page.locator("table")).toBeVisible(); const dropdownRoot = await openLowestVisibleOrderActionDropdown(page); const dropdownContent = dropdownRoot.locator(".dropdown-content").first(); const firstAction = dropdownContent.locator(".dropdown-item-action").first(); const lastAction = dropdownContent.locator(".dropdown-item-action").last(); const fixedHeader = page.locator(".navbar.is-fixed-top").first(); await expect(dropdownContent).toBeVisible(); await expect(firstAction).toBeVisible(); const [headerBox, firstActionBox, scrollTop] = await Promise.all([ fixedHeader.boundingBox(), firstAction.boundingBox(), dropdownContent.evaluate((element) => element.scrollTop), ]); expect(headerBox).not.toBeNull(); expect(firstActionBox).not.toBeNull(); expect(scrollTop).toBe(0); expect(firstActionBox!.y).toBeGreaterThanOrEqual(headerBox!.y + headerBox!.height - 1); await dropdownContent.evaluate((element) => { element.scrollTop = element.scrollHeight; }); await expect(lastAction).toBeVisible(); const lastActionBox = await lastAction.boundingBox(); const viewportHeight = page.viewportSize()?.height ?? 720; expect(lastActionBox).not.toBeNull(); expect((lastActionBox?.y ?? 0) + (lastActionBox?.height ?? 0)).toBeLessThanOrEqual(viewportHeight - 8); }); test("shows desktop categories with left-side flyout submenus and attachment actions on wide viewports", async ({ page, }) => { await page.setViewportSize({ width: 1900, height: 900 }); await page.goto("/admin/12/modules/pos/orders"); await expect(page.locator("table")).toBeVisible(); const dropdownRoot = await openLowestVisibleOrderActionDropdown(page); const dropdownContent = dropdownRoot.locator(".dropdown-content").first(); const flyout = dropdownContent.getByTestId("action-settings-wheel-flyout"); const sectionsRail = dropdownContent.getByTestId("action-settings-wheel-sections"); const customerSection = dropdownContent.getByTestId("action-settings-wheel-section-customer"); const vehicleSection = dropdownContent.getByTestId("action-settings-wheel-section-vehicle"); const attachmentsSection = dropdownContent.getByTestId("action-settings-wheel-section-attachments"); await expect(flyout).toBeVisible(); await expect(customerSection.locator(".fa-chevron-left")).toBeVisible(); await customerSection.hover(); const customerSubmenu = dropdownContent.getByTestId("action-settings-wheel-submenu-customer"); await expect(customerSubmenu).toBeVisible(); await expect(customerSubmenu).toContainText(/log ind som bruger/i); const [flyoutBoxBefore, sectionsRailBox, customerSubmenuBox] = await Promise.all([ flyout.boundingBox(), sectionsRail.boundingBox(), customerSubmenu.boundingBox(), ]); expect(sectionsRailBox).not.toBeNull(); expect(customerSubmenuBox).not.toBeNull(); expect((customerSubmenuBox?.x ?? 0) + (customerSubmenuBox?.width ?? 0)).toBeLessThanOrEqual( (sectionsRailBox?.x ?? 0) - 4 ); await vehicleSection.hover(); const vehicleSubmenu = dropdownContent.getByTestId("action-settings-wheel-submenu-vehicle"); await expect(vehicleSubmenu).toBeVisible(); await expect(vehicleSubmenu).toContainText(/åbn køretøj i ny fane/i); const flyoutBoxAfter = await flyout.boundingBox(); expect(flyoutBoxBefore).not.toBeNull(); expect(flyoutBoxAfter).not.toBeNull(); expect(Math.abs((flyoutBoxAfter?.height ?? 0) - (flyoutBoxBefore?.height ?? 0))).toBeLessThanOrEqual(1); await attachmentsSection.hover(); const attachmentsSubmenu = dropdownContent.getByTestId("action-settings-wheel-submenu-attachments"); const attachmentRows = attachmentsSubmenu.locator('[data-testid^="action-settings-wheel-attachment-row-"]'); await expect(attachmentsSubmenu).toBeVisible(); await expect(attachmentRows).toHaveCount(3); const firstAttachmentRow = attachmentRows.first(); await firstAttachmentRow.hover(); const attachmentPanel = dropdownContent.getByTestId("action-settings-wheel-attachment-panel"); const previewContainer = dropdownContent.getByTestId("action-settings-wheel-attachment-preview"); await expect(attachmentPanel).toBeVisible(); await expect(previewContainer.locator("img, iframe")).toBeVisible(); await expect(dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-preview-"]')).toBeVisible(); await expect(dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-download-"]')).toBeVisible(); await expect(dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-print-"]')).toBeVisible(); await expect(dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-delete-"]')).toBeVisible(); const [firstAttachmentRowBox, attachmentPanelBox] = await Promise.all([ firstAttachmentRow.boundingBox(), attachmentPanel.boundingBox(), ]); expect(firstAttachmentRowBox).not.toBeNull(); expect(attachmentPanelBox).not.toBeNull(); expect(attachmentPanelBox?.x ?? 0).toBeLessThan(firstAttachmentRowBox?.x ?? 0); await dropdownContent.locator('[data-testid^="action-settings-wheel-attachment-action-delete-"]').click(); await expect(attachmentRows).toHaveCount(2); await expect(flyout).toBeVisible(); await expect(attachmentsSection).toBeVisible(); }); test("keeps the first action reachable while async menu sections load", async ({ page }) => { const posFixture = createAsyncActionMenuPosFixture(); await mockApi(page, { authenticated: true, permissions: SUPERUSER_POS_PERMISSIONS, edgeGateways: false, pos: posFixture, }); await page.route(/\/admin\/customer\/getUserId(?:\?.*)?$/i, async (route) => { await delay(350); await route.fulfill( jsonResponse({ success: true, data: { user_id: 77, }, }) ); }); await page.route(/\/orders\/attachments(?:\?.*)?$/i, async (route) => { const parsedUrl = new URL(route.request().url()); const orderId = Number(parsedUrl.searchParams.get("id") || 0); await delay(500); await route.fulfill( jsonResponse({ success: true, data: posFixture.attachmentsByOrderId[orderId] || [], }) ); }); await primeOperatorSession(page, "pos-orders-action-menu-async-layout-token", SUPERUSER_POS_PERMISSIONS); await page.goto("/admin/12/modules/pos/orders"); await expect(page.locator("table")).toBeVisible(); const dropdownRoot = await openLowestVisibleOrderActionDropdown(page); const dropdownContent = dropdownRoot.locator(".dropdown-content").first(); const firstAction = dropdownContent.locator(".dropdown-item-action").first(); const lastAction = dropdownContent.locator(".dropdown-item-action").last(); const fixedHeader = page.locator(".navbar.is-fixed-top").first(); await expect(dropdownContent).toBeVisible(); await expect(firstAction).toBeVisible(); await expect(dropdownContent).toContainText("async-action-menu-54518-1.pdf", { timeout: 10000 }); const [headerBox, firstActionBox, scrollTop] = await Promise.all([ fixedHeader.boundingBox(), firstAction.boundingBox(), dropdownContent.evaluate((element) => element.scrollTop), ]); expect(headerBox).not.toBeNull(); expect(firstActionBox).not.toBeNull(); expect(scrollTop).toBe(0); expect(firstActionBox!.y).toBeGreaterThanOrEqual(headerBox!.y + headerBox!.height - 1); await dropdownContent.evaluate((element) => { element.scrollTop = element.scrollHeight; }); await expect(lastAction).toBeVisible(); }); }); 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(getOrderSettingsField(page, "department_id")).toBeVisible(); await expect(getOrderSettingsField(page, "created_at")).toBeVisible(); await expect(getOrderSettingsField(page, "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 revisitCurrentPage(page); await expect(page.getByTestId("pos-order-detail")).toBeVisible(); await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible(); await expectOrderTotal(page, 1397); }); });