import { expect, test } from "@playwright/test"; import { createPosFixture, mockApi, seedAuthenticatedState } from "./support/network.js"; import { createMobilePosFixture, REGULAR_CUSTOMER_ID, setupMobilePosPage } from "./support/mobilePos.js"; const POS_PERMISSIONS = [ "admin", "department_access_12", "edit_order_items", "get_user", "list_customer_notes", "list_customer_attributes", "get_custom_prices_other", ]; async function primeSession(page, token = "pos-visual-token") { await seedAuthenticatedState(page, token); const sessionRequest = page.waitForResponse((response) => { return response.request().method() === "GET" && response.url().includes("/auth/session"); }); await page.goto("/login"); await sessionRequest; } async function seedMobileStepTwoState(page, { customerId = 12345679, orderId = 54518 } = {}) { await page.addInitScript( ({ activeCustomerId, activeOrderId }) => { window.localStorage.removeItem("pos_order_id"); window.localStorage.setItem( "pos", JSON.stringify({ vehicles: { vehicle_1: { reg: "EC21235", status: "verified", reference: "EC21233 - Test Ref. / Intern nummer", type: 53, customer_id: activeCustomerId, }, vehicle_2: null, vehicle_3: null, activeVehicleIndex: 1, }, views: { manualInput: false, vehicleSelection: false, additionalItemSelection: false, transactionHistoryView: false, }, transactionItems: { primaryItem: { id: 53, name: "Forvogn", price: 649, addons: [ { id: 63, quantity: 1, product: { id: 63, name: "Indvendig vask Forvogn", price: 399, }, }, ], }, additionalItems: [ { id: 65, name: "Ekstraordinær pr. 10 min inkl. kemi", price: 299, quantity: 1, }, ], }, categories: { list: [], selected: null, }, productList: { list: [], }, metadata: { customerId: activeCustomerId, notes: "Visual regression note", reference: "EC21233 - Test Ref. / Intern nummer", washId: null, bookingId: null, laneId: null, }, attachments: { files: [], base64: [], wash_certificate: false, }, transactionHistory: [], lastVehicleOrders: { vehicle_1: { id: activeOrderId, created_at: "2026-04-08 08:44:07", items: [], }, vehicle_2: null, vehicle_3: null, }, timestamp: Date.now(), }) ); }, { activeCustomerId: customerId, activeOrderId: orderId, } ); } async function expectCustomerTabsDividerSpacing(scope) { const tabs = scope.locator(".pos-selected-customer__tabs").first(); await expect(tabs).toBeVisible(); const spacing = await tabs.evaluate((element) => Number.parseFloat(window.getComputedStyle(element).marginBottom)); expect(spacing).toBeGreaterThan(5); } async function expectPosCardTabsDividerSpacing(scope) { const tabs = scope.locator(".pos-card-tabs > .tabs").first(); await expect(tabs).toBeVisible(); const spacing = await tabs.evaluate((element) => Number.parseFloat(window.getComputedStyle(element).paddingBottom)); expect(spacing).toBeGreaterThan(5); } async function commitDesktopReg1ByBlur(page, value) { const reg1Input = page.locator("#reg_1"); await reg1Input.fill(value); await reg1Input.evaluate((element) => { element.blur(); }); } async function getElementMetrics(locator) { await expect(locator).toBeVisible(); return locator.evaluate((element) => { const styles = window.getComputedStyle(element); const rect = element.getBoundingClientRect(); return { fontSize: Number.parseFloat(styles.fontSize), lineHeight: Number.parseFloat(styles.lineHeight), height: rect.height, }; }); } async function getSidebarTabMetrics(locator) { await expect(locator).toBeVisible(); return locator.evaluate((element) => { const styles = window.getComputedStyle(element); const rect = element.getBoundingClientRect(); return { color: styles.color, backgroundColor: styles.backgroundColor, borderRadius: styles.borderRadius, minHeight: styles.minHeight, paddingTop: styles.paddingTop, paddingRight: styles.paddingRight, paddingBottom: styles.paddingBottom, paddingLeft: styles.paddingLeft, height: rect.height, }; }); } async function getSidebarTabListMetrics(locator) { await expect(locator).toBeVisible(); return locator.evaluate((element) => { const styles = window.getComputedStyle(element); return { gap: styles.gap, rowGap: styles.rowGap, columnGap: styles.columnGap, }; }); } async function getHorizontalBounds(locator) { await expect(locator).toBeVisible(); return locator.evaluate((element) => { const rect = element.getBoundingClientRect(); return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, height: rect.height, width: rect.width, }; }); } async function expectPrimaryActionAboveClearAll(primaryAction, clearAllAction) { const primaryBounds = await getHorizontalBounds(primaryAction); const clearAllBounds = await getHorizontalBounds(clearAllAction); expect(primaryBounds.top).toBeLessThan(clearAllBounds.top); expect(primaryBounds.bottom).toBeLessThan(clearAllBounds.bottom); } async function openOrderDetailAddItems(page, { mobile = false } = {}) { 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.goto("/admin/12/modules/pos/orders/54518"); await expect(page.getByTestId("pos-order-detail")).toBeVisible(); await page.getByTestId("pos-order-add-item").click(); await expect(addItemsPanel).toBeVisible(); await expect(addItemsBackButton).toBeVisible(); await expect(getVisibleTestId(page, "pos-order-panel-cart")).toBeVisible(); await expect(getVisibleTestId(page, "pos-order-metadata-grid")).toBeVisible(); if (mobile) { await expect(getVisibleTestId(page, "pos-order-mobile-tabs")).toBeVisible(); } } async function waitForOrderDetailMetadata(page) { await expect(getVisibleTestId(page, "pos-order-metadata-license-plates")).toContainText("EC21235", { timeout: 10_000, }); await expect(getVisibleTestId(page, "pos-order-metadata-customer-wishes")).toContainText("Reference", { timeout: 10_000, }); } async function expectClippedLocatorScreenshot(page, locator, snapshotName, { width, height, maxDiffPixels = 300 }) { await locator.scrollIntoViewIfNeeded(); const box = await locator.boundingBox(); if (!box) { throw new Error(`Unable to capture screenshot for ${snapshotName}: locator has no bounding box.`); } const screenshot = await page.screenshot({ clip: { x: Math.floor(box.x), y: Math.floor(box.y), width, height, }, scale: "css", }); expect(screenshot).toMatchSnapshot(snapshotName, { maxDiffPixels }); } function getVisibleTestId(page, testId) { return page.locator(`[data-testid="${testId}"]:visible`).first(); } function buildTodayTimestamp(time = "10:00:00.000Z") { const today = new Date().toISOString().split("T")[0]; return `${today}T${time}`; } 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: "", }, }, }); } test.describe("POS visuals", () => { test("desktop step 1 snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); const posFixture = createPosFixture({ orderBookings: [ { id: 77, reg_1: "AB12345", reg_2: "", }, ], }); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: posFixture, }); await primeSession(page, "pos-visual-desktop-step-1-token"); await page.goto("/admin/12/modules/pos"); const stepOne = page.getByTestId("pos-step-1"); await expect(stepOne).toBeVisible(); await expectPosCardTabsDividerSpacing(stepOne); await expect(page.getByTestId("pos-recent-scan-row-801")).toBeVisible(); await expect(stepOne.getByText(/^Booket$/)).toBeVisible(); await expect(stepOne).not.toContainText("Booket (fremt. opdat.)"); await expect(stepOne).toHaveScreenshot("pos-step-1-desktop.png", { maxDiffPixels: 300, }); }); test("desktop step 1 expanded recent scan snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); const posFixture = createPosFixture({ orderBookings: [ { id: 77, reg_1: "AB12345", reg_2: "", }, ], }); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: posFixture, }); await primeSession(page, "pos-visual-desktop-step-1-expanded-token"); await page.goto("/admin/12/modules/pos"); await expect(page.getByTestId("pos-step-1")).toBeVisible(); await expect(page.getByTestId("pos-recent-scan-details-801")).toHaveCount(0); await page.getByTestId("pos-recent-scan-row-801").click(); await expect(page.locator("#reg_1")).toHaveValue("AB12345"); await expect(page.getByTestId("pos-recent-scan-details-801")).toContainText("RENAULT"); await expect(page.getByTestId("pos-recent-scan-details-801")).toContainText("Captur"); await expect(page.getByTestId("pos-recent-scan-details-801")).toContainText("Kundenummer"); await expect(page.getByTestId("pos-step-1")).toHaveScreenshot("pos-step-1-desktop-expanded-scan.png", { maxDiffPixels: 1000, }); }); test("desktop step 1 customer snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await primeSession(page, "pos-visual-desktop-step-1-customer-token"); 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 expectPosCardTabsDividerSpacing(page.getByTestId("pos-step-1")); await expectCustomerTabsDividerSpacing(page.getByTestId("pos-step-1")); await expect(page.getByTestId("pos-step-1")).toHaveScreenshot("pos-step-1-customer-desktop.png", { maxDiffPixels: 2000, }); }); test("desktop step 1 duplicate warning snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); const posFixture = createPosFixture(); posFixture.ordersById[9206] = { id: 9206, customer_id: 12345679, department_id: 12, reference: "DUP-VISUAL", po: "", safety_seal: "", notes: "", reg_1: "AB12345", reg_2: "", reg_3: "", invoice_collection_id: null, booking_id: null, completed_at: null, closed_at: null, created_at: buildTodayTimestamp(), include_in_invoice: null, }; posFixture.orderItemsByOrderId[9206] = []; await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: posFixture, }); await primeSession(page, "pos-visual-desktop-step-1-duplicate-warning-token"); await page.goto("/admin/12/modules/pos"); await expect(page.getByTestId("pos-step-1")).toBeVisible(); await commitDesktopReg1ByBlur(page, "AB12345"); const duplicateWarning = page.getByTestId("pos-desktop-duplicate-warning-inline"); await expect(duplicateWarning).toBeVisible({ timeout: 10_000 }); await duplicateWarning.getByTestId("pos-desktop-duplicate-warning-toggle-details").click(); await expect(page.getByTestId("pos-desktop-duplicate-warning-details-inline")).toBeVisible({ timeout: 10_000, }); await expect(duplicateWarning).toHaveScreenshot("pos-step-1-desktop-duplicate-warning.png", { maxDiffPixels: 2000, }); }); test("desktop step 1 keeps customer notes visible without refetching on blur", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); const customerNoteRequests = []; page.on("request", (request) => { if (request.method() === "GET" && request.url().includes("/customer/notes?customer_number=12345679")) { customerNoteRequests.push(request.url()); } }); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture({ customerNotesByNumber: { 12345679: [ { id: 401, created_at: "2026-04-09 09:15:00", note: "Ring inden vask", cashier_id: null, }, ], }, }), }); await primeSession(page, "pos-visual-desktop-step-1-notes-token"); await page.goto("/admin/12/modules/pos"); await expect(page.getByTestId("pos-step-1")).toBeVisible(); const stepOne = page.getByTestId("pos-step-1"); const registrationInput = page.locator("#reg_1"); const referenceInput = page.locator("#reference"); const noteRow = stepOne.getByTestId("pos-customer-note-401"); await registrationInput.fill("EC21235"); await expect(noteRow).toBeVisible(); await expect(noteRow).toContainText("Ring inden vask"); await expect.poll(() => customerNoteRequests.length).toBe(1); await referenceInput.click(); await expect(noteRow).toBeVisible(); await expect(noteRow).toContainText("Ring inden vask"); await page.waitForTimeout(250); expect(customerNoteRequests).toHaveLength(1); }); test("desktop order detail snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await primeSession(page, "pos-visual-desktop-token"); await page.goto("/admin/12/modules/pos/orders/54518"); await expect(page.getByTestId("pos-order-detail")).toBeVisible(); await expect(page.locator('[data-auto-excel-export-button="1"]')).toHaveCount(0); await expect(page.getByTestId("pos-order-registration-add-2")).toBeVisible(); await expect(page.getByTestId("pos-order-note-empty-state")).toBeVisible(); await expect(page.getByTestId("pos-order-item-edit-9101")).toBeVisible(); await expect(page.getByTestId("pos-order-add-item")).toBeVisible(); await expect(page.getByTestId("pos-order-total")).toContainText("1372 DKK"); await expect(page.getByTestId("pos-order-detail")).toHaveScreenshot("pos-order-detail-desktop.png", { maxDiffPixels: 300, }); }); test("desktop order detail required warnings snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createRequiredWarningsPosFixture(), }); await primeSession(page, "pos-visual-desktop-required-warnings-token"); await page.goto("/admin/12/modules/pos/orders/54518"); await expect(page.getByTestId("pos-order-detail")).toBeVisible(); await expect(page.getByTestId("pos-order-customer-wishes-reference-control")).toHaveAttribute( "data-warning-state", "danger" ); await expect(page.getByTestId("pos-order-customer-wishes-po-control")).toHaveAttribute( "data-warning-state", "warning" ); await expect(page.getByTestId("pos-order-detail")).toHaveScreenshot( "pos-order-detail-desktop-required-warnings.png", { maxDiffPixels: 300, } ); }); test("desktop step 2 shared workspace snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await primeSession(page, "pos-visual-desktop-step-2-title-token"); 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.locator('[data-testid="pos-next-step"]:visible').click(); const stepTwo = page.getByTestId("pos-step-2"); const primaryAction = stepTwo.getByTestId("pos-next-step"); const clearAllAction = stepTwo.getByRole("button", { name: /Slet alle/i }); await expect(stepTwo).toBeVisible(); await expect(stepTwo.getByTestId("pos-order-add-items-panel")).toBeVisible(); await expect(stepTwo.getByTestId("pos-order-panel-cart")).toBeVisible(); await expect(stepTwo.getByTestId("pos-order-metadata-grid")).toBeVisible(); await expect(primaryAction).toBeVisible(); await expect(clearAllAction).toBeVisible(); await expectPrimaryActionAboveClearAll(primaryAction, clearAllAction); await expect(stepTwo).toHaveScreenshot("pos-step-2-desktop.png", { maxDiffPixels: 300, }); }); test("desktop step 3 shared workspace snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); test.skip(testInfo.project.name === "firefox-desktop", "Step 3 workspace height is unstable on Firefox desktop."); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await primeSession(page, "pos-visual-desktop-step-3-token"); const customerResponse = page.waitForResponse((response) => { return ( response.request().method() === "GET" && response.url().includes("/users/customer?customer_number=12345679") ); }); await page.goto("/admin/12/modules/pos?id=54518&customer_id=12345679&step=3"); await customerResponse; const stepThree = page.getByTestId("pos-step-3"); const workspace = stepThree.getByTestId("pos-order-workspace"); const primaryAction = stepThree.getByTestId("pos-next-step"); const clearAllAction = stepThree.getByRole("button", { name: /Slet alle/i }); await expect(stepThree).toBeVisible(); await expect(workspace).toBeVisible(); await expect(stepThree.getByTestId("pos-order-panel-cart")).toBeVisible(); await expect(stepThree.getByTestId("pos-order-metadata-grid")).toBeVisible(); await expect(stepThree.getByTestId("pos-order-rail")).toBeVisible(); await expect(stepThree).toContainText("Tilføj flere varer"); await expect(primaryAction).toBeVisible(); await expect(clearAllAction).toBeVisible(); await expect(stepThree.getByRole("button", { name: /Fuldfør/i })).toBeVisible(); await expectPrimaryActionAboveClearAll(primaryAction, clearAllAction); const stepThreeCartPanel = stepThree.getByTestId("pos-order-panel-cart"); const clipByProject = { "chromium-desktop": { width: 460, height: 369 }, "webkit-desktop": { width: 452, height: 367 }, }; await expectClippedLocatorScreenshot(page, stepThreeCartPanel, "pos-step-3-desktop.png", { ...(clipByProject[testInfo.project.name] || { width: 460, height: 369 }), maxDiffPixels: 2500, }); }); test("desktop step 4 shared workspace snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); test.skip(testInfo.project.name === "firefox-desktop", "Step 4 workspace height is unstable on Firefox desktop."); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await primeSession(page, "pos-visual-desktop-step-4-token"); const customerResponse = page.waitForResponse((response) => { return ( response.request().method() === "GET" && response.url().includes("/users/customer?customer_number=12345679") ); }); await page.goto("/admin/12/modules/pos?id=54518&customer_id=12345679&step=4"); await customerResponse; const stepFour = page.getByTestId("pos-step-4"); const workspace = stepFour.getByTestId("pos-order-workspace"); const primaryAction = stepFour.getByTestId("pos-next-step"); const clearAllAction = stepFour.getByRole("button", { name: /Slet alle/i }); await expect(stepFour).toBeVisible(); await expect(workspace).toBeVisible(); await expect(stepFour.getByTestId("pos-order-panel-cart")).toBeVisible(); await expect(stepFour.getByTestId("pos-order-metadata-grid")).toBeVisible(); await expect(stepFour.getByTestId("pos-order-rail")).toBeVisible(); await expect(stepFour).toContainText("Tilføj flere varer"); await expect(primaryAction).toBeVisible(); await expect(clearAllAction).toBeVisible(); await expect(stepFour.getByRole("button", { name: /Fuldfør/i })).toBeVisible(); await expectPrimaryActionAboveClearAll(primaryAction, clearAllAction); const detailItemsTable = stepFour.getByTestId("pos-order-detail-items-table"); const clipByProject = { "chromium-desktop": { width: 423, height: 288 }, "webkit-desktop": { width: 415, height: 287 }, }; await expectClippedLocatorScreenshot(page, detailItemsTable, "pos-step-4-desktop.png", { ...(clipByProject[testInfo.project.name] || { width: 423, height: 288 }), maxDiffPixels: 2500, }); }); test("desktop step 3 guest stripe action order", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); const guestStripeCustomer = { id: 2, customerNumber: 999, name: "Card Terminal Customer", address: "Terminal Street 9", zip: "2630", city: "Taastrup", mobilePhone: "87654321", email: "card@example.com", corporateIdentificationNumber: "99999999", economic_customer: 999, barred: false, }; await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture({ customersByNumber: { 999: guestStripeCustomer, 12345679: guestStripeCustomer, }, ordersById: { 54518: { id: 54518, customer_id: 999, department_id: 12, reference: "CARD-REF-54518", notes: "Paid at terminal", reg_1: "ZZ99999", reg_2: "", reg_3: "", invoice_collection_id: null, booking_id: null, completed_at: null, closed_at: null, created_at: "2026-04-08 08:44:07", }, }, }), }); await primeSession(page, "pos-visual-desktop-step-3-guest-token"); await page.goto("/admin/12/modules/pos?id=54518&customer_id=999&step=3"); const stepThree = page.getByTestId("pos-step-3"); const primaryAction = stepThree .locator( '[data-testid="pos-stripe-create-intent"], [data-testid="pos-stripe-capture-intent"], [data-testid="pos-stripe-no-readers"], [data-testid="pos-stripe-payment-in-progress"], [data-testid="pos-stripe-payment-succeeded"], [data-testid="pos-stripe-error"]' ) .first(); const clearAllAction = stepThree.getByRole("button", { name: /Slet alle/i }); await expect(stepThree).toBeVisible(); await expect(stepThree.getByTestId("pos-order-panel-cart")).toBeVisible(); await expect(stepThree.getByTestId("pos-order-metadata-grid")).toBeVisible(); await expect(stepThree.getByTestId("pos-order-rail")).toBeVisible(); await expect(primaryAction).toBeVisible(); await expect(clearAllAction).toBeVisible(); await expectPrimaryActionAboveClearAll(primaryAction, clearAllAction); }); test("desktop order detail add-items workspace snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await primeSession(page, "pos-visual-desktop-add-items-token"); await openOrderDetailAddItems(page); await expect(getVisibleTestId(page, "pos-order-workspace")).toContainText("Transaktion #54518"); await expect(getVisibleTestId(page, "pos-product-card-53")).toBeVisible(); await expect(getVisibleTestId(page, "pos-order-item-edit-9101")).toBeVisible(); await expect(getVisibleTestId(page, "pos-order-total")).toContainText("1372 DKK"); await expect(getVisibleTestId(page, "pos-order-workspace")).toHaveScreenshot( "pos-order-detail-add-items-desktop.png", { maxDiffPixels: 300, } ); }); test("desktop order detail edit modal snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop only"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await primeSession(page, "pos-visual-desktop-edit-modal-token"); await page.goto("/admin/12/modules/pos/orders/54518"); await expect(page.getByTestId("pos-order-detail")).toBeVisible(); await page.getByTestId("pos-order-item-edit-9101").click(); await expect(page.getByTestId("pos-order-item-edit-modal")).toBeVisible(); await expect(page.getByTestId("pos-order-item-edit-modal")).toHaveScreenshot( "pos-order-detail-edit-modal-desktop.png", { maxDiffPixels: 300, } ); }); test("mobile step 2 snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("mobile"), "Mobile only"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await seedMobileStepTwoState(page); await primeSession(page, "pos-visual-mobile-step-2-token"); await page.goto("/admin/12/modules/pos?id=54518&customer_id=12345679&step=2"); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible(); await expect(page.getByTestId("pos-mobile-step-2")).toHaveScreenshot("pos-mobile-step-2.png", { maxDiffPixels: 300, }); }); test("mobile step 1 selected customer CTA snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("mobile"), "Mobile only"); test.skip(testInfo.project.name === "webkit-mobile", "Seeded customer CTA is unreliable on WebKit mobile."); const longCustomerName = "(TEST) Pleno Vognmandsforretning"; const fixture = createMobilePosFixture({ customersByNumber: { [REGULAR_CUSTOMER_ID]: { id: REGULAR_CUSTOMER_ID, customerNumber: REGULAR_CUSTOMER_ID, name: longCustomerName, address: "Demo Street 1", zip: "2630", city: "Taastrup", mobilePhone: "12345678", email: "pos-mobile@example.com", corporateIdentificationNumber: "00012345", economic_customer: REGULAR_CUSTOMER_ID, barred: false, }, }, }); await setupMobilePosPage(page, fixture, { token: "pos-visual-mobile-step-1-customer-token", permissions: ["admin", "department_access_1"], seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "EC21235", reference: "EC21233 - Test Ref. / Intern nummer", includePrimaryItem: false, vehicleType: null, lastOrderId: null, }, route: { step: 1, }, }); await expect(page.getByTestId("pos-mobile-step-1")).toBeVisible(); const nextStepButton = page.getByTestId("pos-mobile-next-step"); const hasSelectedCustomer = await nextStepButton .textContent() .then((value) => String(value || "").includes(longCustomerName)) .catch(() => false); if (!hasSelectedCustomer) { await nextStepButton.click(); await page.getByTestId(`pos-mobile-customer-option-${REGULAR_CUSTOMER_ID}`).first().click(); } await expect(nextStepButton).toContainText(longCustomerName, { timeout: 10_000 }); await expect(page.getByTestId("pos-mobile-fixed-actions")).toHaveScreenshot("pos-mobile-step-1-fixed-actions.png", { maxDiffPixels: 300, }); }); test("mobile transaction history actions keep text left and icons right", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("mobile"), "Mobile only"); const fixture = createMobilePosFixture(); await setupMobilePosPage(page, fixture, { token: "pos-visual-mobile-transaction-history-token", permissions: ["admin", "department_access_1"], seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "AB12345", includePrimaryItem: false, transactionHistoryView: true, lastOrderId: 9201, }, route: { step: 1, }, }); await expect(page.getByTestId("pos-mobile-transaction-history")).toBeVisible(); const reloadAction = page.getByTestId("pos-mobile-transaction-history-reload-action"); const reloadLabel = page.getByTestId("pos-mobile-transaction-history-reload-label"); const reloadIcon = page.getByTestId("pos-mobile-transaction-history-reload-icon"); const closeAction = page.getByTestId("pos-mobile-transaction-history-close-action"); const closeLabel = page.getByTestId("pos-mobile-transaction-history-close-label"); const closeIcon = page.getByTestId("pos-mobile-transaction-history-close-icon"); const [ reloadActionBounds, reloadLabelBounds, reloadIconBounds, closeActionBounds, closeLabelBounds, closeIconBounds, ] = await Promise.all([ getHorizontalBounds(reloadAction), getHorizontalBounds(reloadLabel), getHorizontalBounds(reloadIcon), getHorizontalBounds(closeAction), getHorizontalBounds(closeLabel), getHorizontalBounds(closeIcon), ]); expect(reloadIconBounds.left).toBeGreaterThan(reloadLabelBounds.right + 8); expect(reloadActionBounds.right - reloadIconBounds.right).toBeLessThan(24); expect( Math.abs( (reloadIconBounds.top + reloadIconBounds.bottom) / 2 - (reloadActionBounds.top + reloadActionBounds.bottom) / 2 ) ).toBeLessThan(6); expect(closeIconBounds.left).toBeGreaterThan(closeLabelBounds.right + 8); expect(closeActionBounds.right - closeIconBounds.right).toBeLessThan(24); expect( Math.abs( (closeIconBounds.top + closeIconBounds.bottom) / 2 - (closeActionBounds.top + closeActionBounds.bottom) / 2 ) ).toBeLessThan(6); }); test("mobile attachment view actions keep the same side inset", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("mobile"), "Mobile only"); test.skip( testInfo.project.name === "webkit-mobile", "Attachment view controls do not render consistently on WebKit mobile." ); const fixture = createMobilePosFixture(); await setupMobilePosPage(page, fixture, { token: "pos-visual-mobile-attachment-view-token", permissions: ["admin", "department_access_1"], seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "AB12345", includePrimaryItem: false, lastOrderId: null, }, route: { step: 1, }, }); await expect(page.getByTestId("pos-mobile-step-1")).toBeVisible(); await page.getByTestId("pos-mobile-attachments-toggle").click(); const closeAction = page.getByTestId("pos-mobile-attachment-view-close-action"); if (!(await closeAction.isVisible().catch(() => false))) { await page.getByTestId("pos-mobile-attachments-toggle").click({ force: true }); } await expect(closeAction).toBeVisible({ timeout: 10_000 }); const takePictureAction = page.getByTestId("pos-mobile-attachment-view-take-picture-action"); if (!(await takePictureAction.isVisible().catch(() => false))) { test.skip(testInfo.project.name.includes("webkit"), "Take-picture action is unavailable on WebKit mobile."); } const takePictureLabel = page.getByTestId("pos-mobile-attachment-view-take-picture-label"); const takePictureIcon = page.getByTestId("pos-mobile-attachment-view-take-picture-icon"); const closeLabel = page.getByTestId("pos-mobile-attachment-view-close-label"); const closeIcon = page.getByTestId("pos-mobile-attachment-view-close-icon"); const [ takePictureActionBounds, takePictureLabelBounds, takePictureIconBounds, closeActionBounds, closeLabelBounds, closeIconBounds, ] = await Promise.all([ getHorizontalBounds(takePictureAction), getHorizontalBounds(takePictureLabel), getHorizontalBounds(takePictureIcon), getHorizontalBounds(closeAction), getHorizontalBounds(closeLabel), getHorizontalBounds(closeIcon), ]); expect(takePictureIconBounds.left).toBeGreaterThan(takePictureLabelBounds.right + 8); expect(takePictureActionBounds.right - takePictureIconBounds.right).toBeLessThan(24); expect( Math.abs( (takePictureIconBounds.top + takePictureIconBounds.bottom) / 2 - (takePictureActionBounds.top + takePictureActionBounds.bottom) / 2 ) ).toBeLessThan(6); expect(closeIconBounds.left).toBeGreaterThan(closeLabelBounds.right + 8); expect(closeActionBounds.right - closeIconBounds.right).toBeLessThan(24); expect( Math.abs( (closeIconBounds.top + closeIconBounds.bottom) / 2 - (closeActionBounds.top + closeActionBounds.bottom) / 2 ) ).toBeLessThan(6); }); test("mobile order detail add-items workspace snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("mobile"), "Mobile only"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture(), }); await primeSession(page, "pos-visual-mobile-add-items-token"); await openOrderDetailAddItems(page, { mobile: true }); await waitForOrderDetailMetadata(page); await expect(getVisibleTestId(page, "pos-order-panel-cart")).toHaveScreenshot( "pos-order-detail-add-items-mobile.png", { maxDiffPixels: 300, } ); }); test("mobile step 3 snapshot", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("mobile"), "Mobile only"); await mockApi(page, { authenticated: true, permissions: POS_PERMISSIONS, edgeGateways: false, pos: createPosFixture({ ordersById: { 54518: { id: 54518, customer_id: 999, department_id: 12, reference: "CARD-REF-54518", notes: "Paid at terminal", reg_1: "EC21235", reg_2: "", reg_3: "", invoice_collection_id: null, booking_id: null, completed_at: null, closed_at: null, created_at: "2026-04-08 08:44:07", }, }, paymentIntentsByOrderId: { 54518: { id: "pi_54518", amount: 123400, amount_capturable: 123400, amount_received: 0, currency: "dkk", status: "requires_capture", metadata: { order_id: "54518", reader_id: "reader_online_1", tax_percentage: "25", }, }, }, }), }); await seedMobileStepTwoState(page, { customerId: 999 }); await primeSession(page, "pos-visual-mobile-step-3-token"); await page.goto("/admin/12/modules/pos?id=54518&customer_id=999&step=3"); await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible(); await expect(page.getByTestId("pos-mobile-step-3")).toHaveScreenshot("pos-mobile-step-3.png", { maxDiffPixels: 300, }); }); });