Files
pleno-vue/tests/e2e/pos.visual.spec.js
T

1315 lines
48 KiB
JavaScript

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",
];
const POS_STEP_TIMEOUT = 20_000;
const visualSnapshotOptions = (maxDiffPixels = 300, maxDiffPixelRatio = 0.02) =>
process.platform === "win32" ? { maxDiffPixels } : { maxDiffPixelRatio, threshold: 0.35 };
const VISUAL_SNAPSHOT_OPTIONS = visualSnapshotOptions();
const LINUX_CONTAINER_DESKTOP_VISUAL_SNAPSHOT_OPTIONS = visualSnapshotOptions(3500, 0.04);
const LINUX_CONTAINER_MOBILE_POS_VISUAL_SNAPSHOT_OPTIONS = visualSnapshotOptions(3000, 0.04);
const RELAXED_VISUAL_SNAPSHOT_OPTIONS = visualSnapshotOptions(1000, 0.03);
const MOBILE_PAYMENT_VISUAL_SNAPSHOT_OPTIONS = visualSnapshotOptions(300, 0.05);
async function primeSession(page, token = "pos-visual-token") {
await seedAuthenticatedState(page, token);
await page.goto("/login", { waitUntil: "domcontentloaded" });
}
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 expectVehicleEmptyStateMatchesLastWashContentHeight(page) {
const vehicleEmptyState = page.getByTestId("pos-desktop-vehicle-empty-state");
const lastWashList = page.getByTestId("pos-desktop-last-wash-section").locator(".pos-step-one-insight-list");
const lastWashCopyButton = page.getByTestId("pos-desktop-last-wash-copy");
await expect(vehicleEmptyState).toBeVisible();
await expect(lastWashList).toBeVisible();
await expect(lastWashCopyButton).toBeVisible();
const [emptyStateBox, lastWashListBox, lastWashCopyButtonBox] = await Promise.all([
vehicleEmptyState.boundingBox(),
lastWashList.boundingBox(),
lastWashCopyButton.boundingBox(),
]);
expect(emptyStateBox).not.toBeNull();
expect(lastWashListBox).not.toBeNull();
expect(lastWashCopyButtonBox).not.toBeNull();
const emptyStateBottom = emptyStateBox.y + emptyStateBox.height;
const lastWashContentBottom = lastWashCopyButtonBox.y + lastWashCopyButtonBox.height;
expect(Math.abs(emptyStateBottom - lastWashContentBottom)).toBeLessThanOrEqual(3);
expect(emptyStateBox.height).toBeGreaterThan(lastWashCopyButtonBox.height);
}
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, maxDiffPixelRatio = 0.02, resetScroll = false, expandViewportForClip = false }
) {
await locator.scrollIntoViewIfNeeded();
if (resetScroll) {
await resetScrollableAncestor(locator);
}
let box = await locator.boundingBox();
if (!box) {
throw new Error(`Unable to capture screenshot for ${snapshotName}: locator has no bounding box.`);
}
const viewportSize = page.viewportSize();
if (expandViewportForClip && viewportSize && box.y + height > viewportSize.height) {
await page.setViewportSize({
width: viewportSize.width,
height: Math.ceil(box.y + height),
});
if (resetScroll) {
await resetScrollableAncestor(locator);
}
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(resolve)));
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, visualSnapshotOptions(maxDiffPixels, maxDiffPixelRatio));
}
async function resetScrollableAncestor(locator) {
await locator.evaluate((element) => {
const scrollParent = element.closest(".independent-scroll");
if (scrollParent) {
scrollParent.scrollTop = 0;
scrollParent.scrollLeft = 0;
}
});
}
async function stabilizeElementScreenshotHeight(locator, height) {
await locator.evaluate((element, targetHeight) => {
element.style.height = `${targetHeight}px`;
element.style.overflow = "hidden";
}, height);
}
async function stabilizeElementScreenshotMinHeight(locator, height) {
await locator.evaluate((element, targetHeight) => {
element.style.minHeight = `${targetHeight}px`;
}, height);
}
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,
department: 12,
reg_1: "AB12345",
reg_2: "",
order_id: null,
status: "pending",
},
],
});
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({ timeout: POS_STEP_TIMEOUT });
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.)");
const isWebKitDesktop = testInfo.project.name === "webkit-desktop";
const isFirefoxDesktop = testInfo.project.name === "firefox-desktop";
await expectClippedLocatorScreenshot(page, stepOne, "pos-step-1-desktop.png", {
width: 860,
height: isFirefoxDesktop ? 496 : isWebKitDesktop ? 492 : 490,
maxDiffPixels: 3500,
maxDiffPixelRatio: 0.04,
expandViewportForClip: true,
});
});
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,
department: 12,
reg_1: "AB12345",
reg_2: "",
order_id: null,
status: "pending",
},
],
});
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({ timeout: POS_STEP_TIMEOUT });
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");
if (testInfo.project.name === "firefox-desktop") {
const stepOne = page.getByTestId("pos-step-1");
await stabilizeElementScreenshotHeight(stepOne, 652);
await expect(stepOne).toHaveScreenshot(
"pos-step-1-desktop-expanded-scan.png",
visualSnapshotOptions(30000, 0.05)
);
} else if (testInfo.project.name === "webkit-desktop") {
await expectClippedLocatorScreenshot(
page,
page.getByTestId("pos-step-1"),
"pos-step-1-desktop-expanded-scan.png",
{
width: 852,
height: 646,
maxDiffPixels: 3000,
maxDiffPixelRatio: 0.05,
expandViewportForClip: true,
}
);
} else {
await expect(page.getByTestId("pos-step-1")).toHaveScreenshot(
"pos-step-1-desktop-expanded-scan.png",
RELAXED_VISUAL_SNAPSHOT_OPTIONS
);
}
});
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");
const stepOne = page.getByTestId("pos-step-1");
await expect(stepOne).toBeVisible({ timeout: POS_STEP_TIMEOUT });
await page.locator("#reg_1").fill("EC21235");
await expect(stepOne.locator(".pos-card-tabs > .tabs li.is-active")).toContainText("Kunde");
await expectPosCardTabsDividerSpacing(stepOne);
await expectCustomerTabsDividerSpacing(stepOne);
await expect(page.getByTestId("pos-desktop-vehicle-summary")).toBeVisible();
await expect(page.getByTestId("pos-desktop-last-wash")).toBeVisible();
await expect(page.getByTestId("pos-desktop-last-wash")).toContainText("Indvendig vask Forvogn");
await expect(page.getByTestId("pos-desktop-secondary-block")).toBeVisible();
await expect(page.getByTestId("pos-desktop-last-wash-copy")).toBeEnabled();
const normalizeFirefoxClip = testInfo.project.name === "firefox-desktop";
await expectClippedLocatorScreenshot(page, stepOne, "pos-step-1-customer-desktop.png", {
width: 860,
height: 663,
maxDiffPixels: 12000,
maxDiffPixelRatio: 0.04,
resetScroll: normalizeFirefoxClip,
expandViewportForClip: normalizeFirefoxClip,
});
});
test("desktop step 1 stretches vehicle empty state beside last wash content", async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "chromium-desktop", "Layout assertion is covered on chromium desktop.");
await page.setViewportSize({ width: 1920, height: 1080 });
const posFixture = createPosFixture();
posFixture.vehicles = posFixture.vehicles.map((vehicle) =>
vehicle.reg === "EC21235"
? {
...vehicle,
type: null,
wash_subscription: false,
addons: { enabled: 0, available: 0, list: [] },
last_order_id: 54518,
}
: vehicle
);
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: posFixture,
});
await primeSession(page, "pos-visual-desktop-empty-state-height-token");
await page.goto("/admin/12/modules/pos");
const stepOne = page.getByTestId("pos-step-1");
await expect(stepOne).toBeVisible({ timeout: POS_STEP_TIMEOUT });
await page.locator("#reg_1").fill("EC21235");
await expect(page.getByTestId("pos-desktop-vehicle-summary")).toBeVisible();
await expect(page.getByTestId("pos-desktop-last-wash")).toBeVisible();
await expect(page.getByTestId("pos-desktop-vehicle-empty-state")).toContainText(
"Ingen abonnement- eller tilvalgsdata"
);
await expectVehicleEmptyStateMatchesLastWashContentHeight(page);
});
test("desktop step 1 required reference warning snapshot", async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "chromium-desktop", "Covered on chromium desktop.");
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: createRequiredWarningsPosFixture(),
});
await page.addInitScript(() => {
window.localStorage.setItem("locale", "da");
});
await primeSession(page, "pos-visual-desktop-step-1-required-reference-token");
await page.goto("/admin/12/modules/pos", { waitUntil: "domcontentloaded" });
const stepOne = page.getByTestId("pos-step-1");
await expect(stepOne).toBeVisible({ timeout: POS_STEP_TIMEOUT });
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 expect(stepOne.getByTestId("pos-desktop-step-1-reference-control")).toHaveAttribute(
"data-warning-state",
"danger"
);
const referenceWarningIcon = stepOne.getByTestId("pos-desktop-step-1-reference-warning-icon");
await expect(referenceWarningIcon).toBeVisible();
await expect(
stepOne
.locator(".pos-selected-customer__title")
.filter({ hasText: /\(TEST\) Pleno Vognmandsforretning/ })
.first()
).toBeVisible();
await referenceWarningIcon.hover();
await expect(page.getByTestId("pos-desktop-step-1-reference-warning-tooltip")).toHaveText(
"Kunden kræver en reference."
);
await expectClippedLocatorScreenshot(page, stepOne, "pos-step-1-required-reference-desktop.png", {
width: 860,
height: 663,
maxDiffPixels: 500,
});
});
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", { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("pos-step-1")).toBeVisible({ timeout: POS_STEP_TIMEOUT });
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",
LINUX_CONTAINER_DESKTOP_VISUAL_SNAPSHOT_OPTIONS
);
});
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({ timeout: POS_STEP_TIMEOUT });
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");
const orderDetail = page.getByTestId("pos-order-detail");
await expect(orderDetail).toBeVisible({ timeout: POS_STEP_TIMEOUT });
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(orderDetail.locator(".pos-selected-customer--order-detail")).toContainText(
"(TEST) Pleno Vognmandsforretning"
);
await expect(orderDetail.getByRole("button", { name: /Kvittering/i })).toBeVisible();
if (testInfo.project.name === "firefox-desktop") {
await stabilizeElementScreenshotMinHeight(orderDetail, 986);
} else if (testInfo.project.name === "webkit-desktop") {
await stabilizeElementScreenshotHeight(orderDetail, 973);
}
await expect(orderDetail).toHaveScreenshot(
"pos-order-detail-desktop.png",
testInfo.project.name === "firefox-desktop"
? visualSnapshotOptions(80000, 0.1)
: testInfo.project.name === "webkit-desktop"
? LINUX_CONTAINER_DESKTOP_VISUAL_SNAPSHOT_OPTIONS
: VISUAL_SNAPSHOT_OPTIONS
);
});
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");
const orderDetail = page.getByTestId("pos-order-detail");
await expect(orderDetail).toBeVisible({ timeout: POS_STEP_TIMEOUT });
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(orderDetail.locator(".pos-selected-customer--order-detail")).toContainText(
"(TEST) Pleno Vognmandsforretning"
);
await expect(orderDetail.getByRole("button", { name: /Kvittering/i })).toBeVisible();
if (testInfo.project.name === "webkit-desktop" || testInfo.project.name === "firefox-desktop") {
await stabilizeElementScreenshotHeight(orderDetail, testInfo.project.name === "firefox-desktop" ? 902 : 893);
await expect(orderDetail).toHaveScreenshot(
"pos-order-detail-desktop-required-warnings.png",
testInfo.project.name === "firefox-desktop"
? visualSnapshotOptions(60000, 0.09)
: LINUX_CONTAINER_DESKTOP_VISUAL_SNAPSHOT_OPTIONS
);
} else {
await expectClippedLocatorScreenshot(page, orderDetail, "pos-order-detail-desktop-required-warnings.png", {
width: 860,
height: 888,
maxDiffPixels: 3500,
maxDiffPixelRatio: 0.04,
expandViewportForClip: true,
});
}
});
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({ timeout: POS_STEP_TIMEOUT });
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 resetScrollableAncestor(stepTwo);
if (testInfo.project.name === "webkit-desktop" || testInfo.project.name === "firefox-desktop") {
await stabilizeElementScreenshotHeight(stepTwo, testInfo.project.name === "firefox-desktop" ? 832 : 819);
}
await expect(stepTwo).toHaveScreenshot(
"pos-step-2-desktop.png",
testInfo.project.name === "webkit-desktop" || testInfo.project.name === "firefox-desktop"
? LINUX_CONTAINER_DESKTOP_VISUAL_SNAPSHOT_OPTIONS
: VISUAL_SNAPSHOT_OPTIONS
);
});
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: 3500,
maxDiffPixelRatio: 0.04,
});
});
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");
const orderWorkspace = getVisibleTestId(page, "pos-order-workspace");
if (testInfo.project.name === "webkit-desktop" || testInfo.project.name === "firefox-desktop") {
await stabilizeElementScreenshotHeight(orderWorkspace, testInfo.project.name === "firefox-desktop" ? 1052 : 1249);
}
await expect(getVisibleTestId(page, "pos-order-workspace")).toHaveScreenshot(
"pos-order-detail-add-items-desktop.png",
testInfo.project.name === "webkit-desktop" || testInfo.project.name === "firefox-desktop"
? LINUX_CONTAINER_DESKTOP_VISUAL_SNAPSHOT_OPTIONS
: VISUAL_SNAPSHOT_OPTIONS
);
});
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",
{
...VISUAL_SNAPSHOT_OPTIONS,
}
);
});
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();
const customerBannerHeight = await page
.getByTestId("pos-mobile-customer-banner")
.evaluate((element) => element.getBoundingClientRect().height);
expect(customerBannerHeight).toBeLessThan(84);
const isWebKitMobile = testInfo.project.name === "webkit-mobile";
await page.getByTestId("pos-mobile-step-2").evaluate(
(element, height) => {
element.style.height = `${height}px`;
element.style.overflow = "hidden";
},
isWebKitMobile ? 1292 : 1299
);
await expect(page.getByTestId("pos-mobile-step-2")).toHaveScreenshot(
"pos-mobile-step-2.png",
LINUX_CONTAINER_MOBILE_POS_VISUAL_SNAPSHOT_OPTIONS
);
});
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",
VISUAL_SNAPSHOT_OPTIONS
);
});
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",
LINUX_CONTAINER_MOBILE_POS_VISUAL_SNAPSHOT_OPTIONS
);
});
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",
MOBILE_PAYMENT_VISUAL_SNAPSHOT_OPTIONS
);
});
});