- 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.
835 lines
38 KiB
TypeScript
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);
|
|
});
|
|
});
|