Files
pleno-vue/tests/e2e/admin-pos-orders.spec.ts
T
Jeppe Bundgaard e8bd2f5478 Add employee fixtures, mobile POS customer note popup, and alignment updates:
- Introduced employee fixtures in `mobilePos.js` for testing workflows involving employee data.
- Enhanced `PosDepartmentStep1MobileCustomerNotes.vue` with translated labels and data-test attributes.
- Added e2e tests for customer notes popup and translated label validation.
- Improved alignment and responsiveness for `PaginationDisplayGeneralSearchReload.vue` and related e2e tests.
- Updated button logic and styles in `GenericButton.vue` and other mobile POS components for consistent UI behavior.
2026-04-09 14:47:43 +02:00

835 lines
38 KiB
TypeScript

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