- Added unit tests for `applyPosAddCustomerSearchResult` to handle autofill field updates, manual overrides, and stale response rejection. - Implemented a new e2e scenario for preserving manual edits during autofill updates and ignoring stale delayed responses. - Refactored `mobilePos.js` fixture setup to include CVR search and customer registration mock logic. - Added and integrated `PosDesktopCustomerConflictModal.vue` for UI handling of customer conflicts.
1825 lines
79 KiB
TypeScript
1825 lines
79 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",
|
|
"edit_order_items",
|
|
"get_user",
|
|
"list_customer_notes",
|
|
"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 selectStepOneCustomer(
|
|
page: Page,
|
|
customerNumber: number | string,
|
|
expectedName: string | RegExp = /\(TEST\) Pleno Vognmandsforretning/
|
|
) {
|
|
await page.locator("#pos_select_customer_input").fill(String(customerNumber));
|
|
await expect(page.locator(".customer-drop-down-select").first()).toBeVisible();
|
|
await page.locator(".customer-drop-down-select").first().click();
|
|
await expect(page.locator(".field.has-addons input[disabled]").last()).toHaveValue(expectedName);
|
|
}
|
|
|
|
async function clickVisibleTestId(page: Page, testId: string) {
|
|
const locator = page.getByTestId(testId);
|
|
const count = await locator.count();
|
|
|
|
for (let index = 0; index < count; index += 1) {
|
|
const candidate = locator.nth(index);
|
|
if (await candidate.isVisible()) {
|
|
await candidate.click();
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw new Error(`No visible element found for test id "${testId}"`);
|
|
}
|
|
|
|
async function 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 getOrderSettingsField(page: Page, field: string) {
|
|
return page.locator(`[data-testid="pos-order-settings-field-${field}"]:visible`).first();
|
|
}
|
|
|
|
function getOrderSettingsEditButton(page: Page, field: string) {
|
|
return page.locator(`[data-testid="pos-order-settings-edit-${field}"]:visible`).first();
|
|
}
|
|
|
|
function getOrderDetailField(page: Page, field: string) {
|
|
return page.locator(`[data-testid="pos-order-detail-${field}"]:visible`).first();
|
|
}
|
|
|
|
function getOrderDetailInput(page: Page, field: string) {
|
|
return getOrderDetailField(page, field).locator("input, textarea").first();
|
|
}
|
|
|
|
async function openOrderDetailsTab(page: Page) {
|
|
await clickVisibleTestId(page, "pos-order-tab-details");
|
|
await expect(page.getByTestId("pos-order-panel-details")).toBeVisible();
|
|
}
|
|
|
|
async function submitOrderSettingModal(page: Page, field: string, value: string, type: "input" | "select" = "input") {
|
|
await getOrderSettingsEditButton(page, field).click();
|
|
|
|
const popup = page.locator(".swal2-popup");
|
|
await expect(popup).toBeVisible();
|
|
|
|
const control = popup.locator(`#${field}`).first();
|
|
await expect(control).toBeVisible();
|
|
|
|
if (type === "select") {
|
|
await control.selectOption(value);
|
|
} else {
|
|
await control.fill(value);
|
|
}
|
|
|
|
await popup.locator(".swal2-confirm").click();
|
|
await expect(popup).toBeHidden({ timeout: 10000 });
|
|
}
|
|
|
|
async function createInvoiceCollectionFromPicker(page: Page, closedAt: string) {
|
|
const popup = page.locator(".swal2-popup");
|
|
await expect(popup).toBeVisible();
|
|
await expect(popup.locator(".tabs")).toBeVisible({ timeout: 10000 });
|
|
|
|
const createTab = popup.locator(".tabs li a").last();
|
|
await expect(createTab).toBeVisible({ timeout: 10000 });
|
|
await createTab.click();
|
|
|
|
const dateInput = popup.locator('input[type="date"]').first();
|
|
await expect(dateInput).toBeVisible({ timeout: 10000 });
|
|
await dateInput.fill(closedAt);
|
|
|
|
const createRequest = page.waitForRequest((request) => {
|
|
return request.method() === "POST" && request.url().includes("/collected-invoices");
|
|
});
|
|
const createResponse = page.waitForResponse((response) => {
|
|
return response.request().method() === "POST" && response.url().includes("/collected-invoices");
|
|
});
|
|
const createButton = popup.locator("button.button.is-primary").first();
|
|
await expect(createButton).toBeEnabled({ timeout: 10000 });
|
|
await createButton.click();
|
|
await createRequest;
|
|
await createResponse;
|
|
}
|
|
|
|
async function waitForOrderInvoiceCollectionSuccess(page: Page) {
|
|
const successPopup = page.locator(".swal2-popup");
|
|
await expect(successPopup).toContainText(/Faktura samling ændret/i, { timeout: 10000 });
|
|
await expect(successPopup).toBeHidden({ timeout: 4000 });
|
|
}
|
|
|
|
async function changeOrderCustomer(page: Page, customerNumber: string, closedAt: string) {
|
|
await getOrderSettingsEditButton(page, "customer_id").click();
|
|
|
|
const customerPopup = page.locator(".swal2-popup");
|
|
await expect(customerPopup).toBeVisible();
|
|
await customerPopup.locator("#pos_select_customer_input").fill(customerNumber);
|
|
|
|
const customerResult = customerPopup.locator(".customer-drop-down-select").first();
|
|
await expect(customerResult).toBeVisible();
|
|
await customerResult.click();
|
|
await expect(customerPopup.locator("#pos_select_customer_input")).toHaveCount(0, { timeout: 10000 });
|
|
|
|
await createInvoiceCollectionFromPicker(page, closedAt);
|
|
await waitForOrderInvoiceCollectionSuccess(page);
|
|
}
|
|
|
|
async function changeOrderInvoiceCollection(page: Page, closedAt: string) {
|
|
await getOrderSettingsEditButton(page, "invoice_collection_id").click();
|
|
await createInvoiceCollectionFromPicker(page, closedAt);
|
|
await waitForOrderInvoiceCollectionSuccess(page);
|
|
}
|
|
|
|
async function openOrderDetail(page: Page, orderId = 54518) {
|
|
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());
|
|
});
|
|
}
|
|
|
|
async function waitForOrderMutation(
|
|
page: Page,
|
|
method: "PUT" | "POST" | "DELETE",
|
|
endpoint: "/order" | "/orders",
|
|
predicate: (body: Record<string, unknown>) => boolean
|
|
) {
|
|
return page.waitForRequest((request) => {
|
|
if (request.method() !== method || !request.url().includes(endpoint)) {
|
|
return false;
|
|
}
|
|
|
|
const body = (request.postDataJSON?.() || {}) as Record<string, unknown>;
|
|
return predicate(body);
|
|
});
|
|
}
|
|
|
|
function createRequiredWarningsPosFixture() {
|
|
const customerNumber = 12345679;
|
|
const baseFixture = createPosFixture();
|
|
|
|
return createPosFixture({
|
|
customerAttributesByNumber: {
|
|
[customerNumber]: [
|
|
{
|
|
id: 11,
|
|
customer_number: customerNumber,
|
|
attribute: "requiresReferenceNumber",
|
|
},
|
|
{
|
|
id: 12,
|
|
customer_number: customerNumber,
|
|
attribute: "usePONumbers",
|
|
},
|
|
],
|
|
},
|
|
ordersById: {
|
|
54518: {
|
|
...baseFixture.ordersById[54518],
|
|
reference: "",
|
|
po: "",
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
function createCustomerConflictPosFixture() {
|
|
const baseFixture = createPosFixture();
|
|
const defaultCustomer = baseFixture.customersByNumber[12345679];
|
|
const vehicleCustomerNumber = 22334455;
|
|
const vehicleCustomerName = "(TEST) Conflict Fleet";
|
|
|
|
return createPosFixture({
|
|
customersByNumber: {
|
|
[vehicleCustomerNumber]: {
|
|
...defaultCustomer,
|
|
id: 3,
|
|
customerNumber: vehicleCustomerNumber,
|
|
economic_customer: vehicleCustomerNumber,
|
|
name: vehicleCustomerName,
|
|
email: "conflict@example.com",
|
|
mobilePhone: "22334455",
|
|
},
|
|
},
|
|
vehicles: [
|
|
...baseFixture.vehicles.filter((vehicle) => vehicle.reg !== "CONFLICT1"),
|
|
{
|
|
id: 7701,
|
|
reg: "CONFLICT1",
|
|
customer_id: vehicleCustomerNumber,
|
|
customer_name: vehicleCustomerName,
|
|
type: 53,
|
|
status: "verified",
|
|
barred: false,
|
|
wash_subscription: false,
|
|
addons: {
|
|
enabled: 0,
|
|
available: 0,
|
|
list: [],
|
|
},
|
|
reference: "CONFLICT-VEHICLE-REF",
|
|
last_order_id: null,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
test.describe("Admin POS Orders - desktop settings", () => {
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only order settings coverage");
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: POS_PERMISSIONS,
|
|
edgeGateways: false,
|
|
pos: createPosFixture(),
|
|
});
|
|
await primeOperatorSession(page);
|
|
});
|
|
|
|
test("loads order detail metadata without order_id prop/setup errors on first render", async ({ page }) => {
|
|
const consoleMessages: string[] = [];
|
|
const pageErrors: string[] = [];
|
|
|
|
page.on("console", (message) => {
|
|
if (message.type() === "warning" || message.type() === "error") {
|
|
consoleMessages.push(message.text());
|
|
}
|
|
});
|
|
page.on("pageerror", (error) => {
|
|
pageErrors.push(String(error));
|
|
});
|
|
|
|
await openOrderDetail(page);
|
|
await expect(page.getByTestId("pos-order-registration-1")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-metadata-note")).toBeVisible();
|
|
|
|
const combinedMessages = [...consoleMessages, ...pageErrors].join("\n");
|
|
expect(combinedMessages).not.toContain("Missing required prop: order_id");
|
|
expect(combinedMessages).not.toContain('Invalid prop: type check failed for prop "order_id"');
|
|
expect(combinedMessages).not.toContain("Unhandled error during execution of setup function");
|
|
});
|
|
|
|
test("renders order detail metadata and item actions without the Excel export affordance", async ({ page }) => {
|
|
await openOrderDetail(page);
|
|
await expect(page.getByTestId("pos-order-header-export-actions")).toHaveCount(0);
|
|
await expect(page.locator('[data-auto-excel-export-button="1"]')).toHaveCount(0);
|
|
|
|
const inlineDepartment = page.getByTestId("pos-order-inline-department");
|
|
const inlineCreated = page.getByTestId("pos-order-inline-created");
|
|
|
|
await expect(inlineDepartment).toBeVisible();
|
|
await expect(inlineCreated).toBeVisible();
|
|
await expect(inlineDepartment).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
|
await expect(inlineCreated).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
|
await expect(inlineDepartment).toHaveCSS("padding-left", "0px");
|
|
await expect(inlineCreated).toHaveCSS("padding-left", "0px");
|
|
const transactionLabel = page.getByTestId("pos-order-transaction-label");
|
|
await expect(transactionLabel).toBeVisible();
|
|
|
|
const priceHeader = page.getByTestId("pos-order-price-header");
|
|
const actionsHeader = page.getByTestId("pos-order-actions-header");
|
|
await expect(priceHeader).toContainText("Pris (DKK)");
|
|
await expect(actionsHeader).toContainText("Handlinger");
|
|
await expect(actionsHeader).not.toContainText("table.actions");
|
|
|
|
const [transactionFontSize, inlineDepartmentFontSize, inlineCreatedFontSize] = await Promise.all([
|
|
transactionLabel.evaluate((element) => window.getComputedStyle(element).fontSize),
|
|
inlineDepartment.evaluate((element) => window.getComputedStyle(element).fontSize),
|
|
inlineCreated.evaluate((element) => window.getComputedStyle(element).fontSize),
|
|
]);
|
|
expect(inlineDepartmentFontSize).toBe(transactionFontSize);
|
|
expect(inlineCreatedFontSize).toBe(transactionFontSize);
|
|
|
|
const [priceHeaderStyles, actionsHeaderStyles] = await Promise.all([
|
|
priceHeader.evaluate((element) => {
|
|
const target = element.querySelector(".th-wrap") || element;
|
|
const styles = window.getComputedStyle(target);
|
|
return {
|
|
justifyContent: styles.justifyContent,
|
|
textAlign: styles.textAlign,
|
|
};
|
|
}),
|
|
actionsHeader.evaluate((element) => {
|
|
const target = element.querySelector(".th-wrap") || element;
|
|
const styles = window.getComputedStyle(target);
|
|
return {
|
|
justifyContent: styles.justifyContent,
|
|
textAlign: styles.textAlign,
|
|
};
|
|
}),
|
|
]);
|
|
expect(priceHeaderStyles.justifyContent).toBe("flex-end");
|
|
expect(priceHeaderStyles.textAlign).toBe("right");
|
|
expect(actionsHeaderStyles.justifyContent).toBe("flex-end");
|
|
expect(actionsHeaderStyles.textAlign).toBe("right");
|
|
await expect(page.getByTestId("pos-order-item-price-9101")).toHaveCSS("text-align", "right");
|
|
|
|
const licensePlatesSection = page.getByTestId("pos-order-metadata-license-plates");
|
|
const customerWishesSection = page.getByTestId("pos-order-metadata-customer-wishes");
|
|
const noteSection = page.getByTestId("pos-order-metadata-note");
|
|
const metadataSections = [licensePlatesSection, customerWishesSection, noteSection];
|
|
|
|
for (const section of metadataSections) {
|
|
await expect(section).toBeVisible();
|
|
const box = await section.boundingBox();
|
|
expect(box).not.toBeNull();
|
|
expect(box?.width ?? 0).toBeGreaterThan(120);
|
|
expect(box?.height ?? 0).toBeGreaterThan(70);
|
|
}
|
|
|
|
await expect(licensePlatesSection.locator(".skeleton-lines")).toHaveCount(0);
|
|
const reg1 = page.getByTestId("pos-order-registration-1");
|
|
const reg2Add = page.getByTestId("pos-order-registration-add-2");
|
|
const reg3Add = page.getByTestId("pos-order-registration-add-3");
|
|
const customerWishesReference = page.getByTestId("pos-order-customer-wishes-reference");
|
|
const customerWishesPo = page.getByTestId("pos-order-customer-wishes-po");
|
|
await expect(reg1).toBeVisible();
|
|
await expect(reg2Add).toBeVisible();
|
|
await expect(reg3Add).toBeVisible();
|
|
await expect(customerWishesReference).toBeVisible();
|
|
await expect(customerWishesPo).toBeVisible();
|
|
await expect(customerWishesReference).toContainText("EC21233 - Test Ref. / Intern nummer");
|
|
await expect(customerWishesPo).toContainText("+ Tilføj");
|
|
const noteEmptyState = page.getByTestId("pos-order-note-empty-state");
|
|
await expect(noteEmptyState).toBeVisible();
|
|
await expect(noteEmptyState.locator(".pos-order-field__empty-pill")).toHaveText(/^\+\s+\S+/);
|
|
await expect(noteEmptyState).not.toContainText(/Ingen data/i);
|
|
await expect(noteEmptyState.locator(".pos-order-note-editor__empty-copy")).toHaveCount(0);
|
|
|
|
const reg1Box = await reg1.boundingBox();
|
|
const reg2AddBox = await reg2Add.boundingBox();
|
|
const licensePlatesBox = await licensePlatesSection.boundingBox();
|
|
const customerWishesSectionBox = await customerWishesSection.boundingBox();
|
|
const noteSectionBox = await noteSection.boundingBox();
|
|
const customerWishesReferenceBox = await customerWishesReference.boundingBox();
|
|
const customerWishesPoBox = await customerWishesPo.boundingBox();
|
|
expect(reg1Box).not.toBeNull();
|
|
expect(reg2AddBox).not.toBeNull();
|
|
expect(licensePlatesBox).not.toBeNull();
|
|
expect(customerWishesSectionBox).not.toBeNull();
|
|
expect(noteSectionBox).not.toBeNull();
|
|
expect(customerWishesReferenceBox).not.toBeNull();
|
|
expect(customerWishesPoBox).not.toBeNull();
|
|
expect(Math.abs((reg1Box?.height ?? 0) - (reg2AddBox?.height ?? 0))).toBeLessThanOrEqual(2);
|
|
expect(Math.abs((licensePlatesBox?.height ?? 0) - (customerWishesSectionBox?.height ?? 0))).toBeLessThanOrEqual(2);
|
|
expect(customerWishesSectionBox?.x ?? 0).toBeGreaterThan(licensePlatesBox?.x ?? 0);
|
|
expect(noteSectionBox?.y ?? 0).toBeGreaterThan(customerWishesSectionBox?.y ?? 0);
|
|
expect(Math.abs((licensePlatesBox?.x ?? 0) - (noteSectionBox?.x ?? 0))).toBeLessThanOrEqual(2);
|
|
expect(noteSectionBox?.width ?? 0).toBeGreaterThan(customerWishesSectionBox?.width ?? 0);
|
|
|
|
const noteControlBox = await noteSection.locator(".pos-order-field__control").boundingBox();
|
|
expect(noteControlBox).not.toBeNull();
|
|
expect(
|
|
Math.abs((customerWishesReferenceBox?.height ?? 0) - (customerWishesPoBox?.height ?? 0))
|
|
).toBeLessThanOrEqual(2);
|
|
expect(noteControlBox?.height ?? 0).toBeLessThan(100);
|
|
await expect(noteEmptyState).not.toContainText("pos.add_note_placeholder");
|
|
|
|
await expect(page.getByTestId("pos-order-item-edit-9101")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-item-delete-9101")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-add-item")).toBeVisible();
|
|
await expectOrderTotal(page, 1372);
|
|
await page.getByTestId("pos-order-add-item").click();
|
|
await expect(page.getByTestId("pos-order-add-items-panel").first()).toBeVisible();
|
|
await expect(page.getByTestId("pos-product-card-53").first()).toBeVisible();
|
|
});
|
|
|
|
test("keeps a readable split layout in add-items mode and restores the normal shell after backing out", async ({
|
|
page,
|
|
}) => {
|
|
await openOrderDetail(page);
|
|
|
|
const main = getVisibleTestId(page, "pos-order-main");
|
|
const beforeMainBox = await main.boundingBox();
|
|
const beforeRailBox = await getVisibleTestId(page, "pos-order-rail").boundingBox();
|
|
expect(beforeMainBox).not.toBeNull();
|
|
expect(beforeRailBox).not.toBeNull();
|
|
|
|
await openAddItemsPanel(page);
|
|
await expect(page.locator('[data-testid="pos-order-rail"]:visible')).toHaveCount(0);
|
|
|
|
const workspace = getVisibleTestId(page, "pos-order-workspace");
|
|
const productPanel = page.locator('[data-testid="pos-order-add-items-panel"]:visible').first();
|
|
const cartPanel = page.locator('[data-testid="pos-order-panel-cart"]:visible').first();
|
|
const addItemsBackButton = page.locator('[data-testid="pos-order-add-items-back"]:visible').first();
|
|
const afterMainBox = await main.boundingBox();
|
|
const workspaceBox = await workspace.boundingBox();
|
|
const productPanelBox = await productPanel.boundingBox();
|
|
const cartPanelBox = await cartPanel.boundingBox();
|
|
const addItemsBackButtonBox = await addItemsBackButton.boundingBox();
|
|
expect(afterMainBox).not.toBeNull();
|
|
expect(workspaceBox).not.toBeNull();
|
|
expect(productPanelBox).not.toBeNull();
|
|
expect(cartPanelBox).not.toBeNull();
|
|
expect(addItemsBackButtonBox).not.toBeNull();
|
|
expect((afterMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0)).toBeGreaterThan(200);
|
|
expect(workspaceBox?.width ?? 0).toBeGreaterThan(800);
|
|
const productPanelWidth = productPanelBox?.width ?? 0;
|
|
const cartPanelWidth = cartPanelBox?.width ?? 0;
|
|
const workspaceWidth = workspaceBox?.width ?? 0;
|
|
expect(productPanelWidth).toBeGreaterThan(470);
|
|
expect(cartPanelWidth).toBeGreaterThan(300);
|
|
expect(cartPanelWidth).toBeLessThan(420);
|
|
expect(productPanelWidth).toBeGreaterThan(cartPanelWidth);
|
|
expect(productPanelWidth / workspaceWidth).toBeGreaterThan(0.55);
|
|
expect(cartPanelWidth / workspaceWidth).toBeLessThan(0.42);
|
|
expect(productPanelBox?.x ?? 0).toBeLessThan(cartPanelBox?.x ?? 0);
|
|
expect(addItemsBackButtonBox?.y ?? 0).toBeGreaterThan((cartPanelBox?.y ?? 0) + (cartPanelBox?.height ?? 0) - 2);
|
|
expect(Math.abs((addItemsBackButtonBox?.x ?? 0) - (cartPanelBox?.x ?? 0))).toBeLessThanOrEqual(8);
|
|
|
|
await expect(getVisibleTestId(page, "pos-order-total")).toBeVisible();
|
|
await expect(getVisibleTestId(page, "pos-order-item-edit-9101")).toBeVisible();
|
|
await expect(getVisibleTestId(page, "pos-order-registration-1")).toBeVisible();
|
|
await expect(getVisibleTestId(page, "pos-order-metadata-customer-wishes")).toBeVisible();
|
|
|
|
const categoryTabs = productPanel.locator(".tabs li");
|
|
await expect(categoryTabs).toHaveCount(3);
|
|
await categoryTabs.nth(2).click();
|
|
await expect(productPanel.getByTestId("pos-product-card-64")).toBeVisible();
|
|
await expect(productPanel.getByTestId("pos-product-card-53")).toHaveCount(0);
|
|
|
|
await productPanel.getByTestId("pos-product-card-64").click();
|
|
const addRequest = waitForOrderItemMutation(page, "POST", "/order/items");
|
|
await productPanel.getByTestId("pos-add-to-cart-64").click();
|
|
await addRequest;
|
|
|
|
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
|
|
await expect(getVisibleTestId(page, "pos-order-item-name-9200")).toContainText("Vaskecertifikat");
|
|
await expectOrderTotal(page, 1397);
|
|
|
|
await page.locator('[data-testid="pos-order-add-items-back"]:visible').first().click();
|
|
await expect(page.locator('[data-testid="pos-order-add-items-panel"]:visible')).toHaveCount(0);
|
|
await expect(getVisibleTestId(page, "pos-order-rail")).toBeVisible();
|
|
|
|
const restoredMainBox = await main.boundingBox();
|
|
const restoredRailBox = await getVisibleTestId(page, "pos-order-rail").boundingBox();
|
|
expect(restoredMainBox).not.toBeNull();
|
|
expect(restoredRailBox).not.toBeNull();
|
|
expect(Math.abs((restoredMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0))).toBeLessThanOrEqual(20);
|
|
expect(Math.abs((beforeRailBox?.width ?? 0) - (restoredRailBox?.width ?? 0))).toBeLessThanOrEqual(20);
|
|
|
|
await page.reload();
|
|
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
|
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
|
|
await expectOrderTotal(page, 1397);
|
|
});
|
|
|
|
test("loads step 2 customer context after reload and keeps actions directly below the basket card", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/admin/12/modules/pos");
|
|
await expect(page.getByTestId("pos-step-1")).toBeVisible();
|
|
await page.locator("#reg_1").fill("EC21235");
|
|
await expect(page.getByTestId("pos-step-1").locator(".pos-card-tabs > .tabs li.is-active")).toContainText("Kunde");
|
|
await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click();
|
|
await expect(page).toHaveURL(/\/admin\/12\/modules\/pos\?id=\d+&customer_id=12345679&step=2/);
|
|
|
|
const stepTwo = page.getByTestId("pos-step-2");
|
|
const orderCard = stepTwo.getByTestId("pos-order-card");
|
|
const actions = stepTwo.getByTestId("pos-step-2-actions");
|
|
const customerName = () => page.locator('[data-testid="pos-order-customer-name"]:visible').first();
|
|
const licensePlatesCard = stepTwo.getByTestId("pos-order-metadata-license-plates");
|
|
const customerWishesCard = stepTwo.getByTestId("pos-order-metadata-customer-wishes");
|
|
const noteCard = stepTwo.getByTestId("pos-order-metadata-note");
|
|
|
|
await expect(stepTwo).toBeVisible();
|
|
await expect(customerName()).toHaveText(/\(TEST\) Pleno Vognmandsforretning/, { timeout: 10000 });
|
|
await expect(actions.getByTestId("pos-next-step")).toBeVisible();
|
|
await expect(licensePlatesCard).toBeVisible();
|
|
await expect(customerWishesCard).toBeVisible();
|
|
await expect(noteCard).toBeVisible();
|
|
|
|
const orderCardBox = await orderCard.boundingBox();
|
|
const actionPanelBox = await actions.boundingBox();
|
|
const licensePlatesCardBox = await licensePlatesCard.boundingBox();
|
|
const customerWishesCardBox = await customerWishesCard.boundingBox();
|
|
expect(orderCardBox).not.toBeNull();
|
|
expect(actionPanelBox).not.toBeNull();
|
|
expect(licensePlatesCardBox).not.toBeNull();
|
|
expect(customerWishesCardBox).not.toBeNull();
|
|
const orderCardBottom = (orderCardBox?.y ?? 0) + (orderCardBox?.height ?? 0);
|
|
expect(actionPanelBox?.y ?? 0).toBeGreaterThan(orderCardBottom - 2);
|
|
expect((actionPanelBox?.y ?? 0) - orderCardBottom).toBeLessThanOrEqual(40);
|
|
expect(Math.abs((actionPanelBox?.x ?? 0) - (orderCardBox?.x ?? 0))).toBeLessThanOrEqual(2);
|
|
expect(Math.abs((actionPanelBox?.width ?? 0) - (orderCardBox?.width ?? 0))).toBeLessThanOrEqual(2);
|
|
expect(customerWishesCardBox?.y ?? 0).toBeGreaterThan(
|
|
(licensePlatesCardBox?.y ?? 0) + (licensePlatesCardBox?.height ?? 0) - 2
|
|
);
|
|
expect(Math.abs((customerWishesCardBox?.x ?? 0) - (licensePlatesCardBox?.x ?? 0))).toBeLessThanOrEqual(2);
|
|
|
|
await page.reload();
|
|
await expect(stepTwo).toBeVisible();
|
|
await expect(page).toHaveURL(/\/admin\/12\/modules\/pos\?id=\d+&customer_id=12345679&step=2/);
|
|
await expect(getVisibleTestId(page, "pos-order-registration-1")).toContainText("EC21235");
|
|
await expect(getVisibleTestId(page, "pos-order-metadata-customer-wishes")).toContainText(
|
|
"EC21233 - Test Ref. / Intern nummer"
|
|
);
|
|
await expect(stepTwo.getByTestId("pos-next-step")).toBeVisible();
|
|
});
|
|
|
|
test("autosaves customer wishes and note metadata after a typing pause without blur", async ({ page }) => {
|
|
await openOrderDetail(page);
|
|
|
|
const referenceValue = "AUTOSAVE-REF-2026";
|
|
const poValue = "AUTOSAVE-PO-2026";
|
|
const noteValue = "Autosaved desktop note";
|
|
|
|
const referenceRequest = waitForOrderMutation(
|
|
page,
|
|
"PUT",
|
|
"/orders",
|
|
(body) => Number(body.id) === 54518 && body.reference === referenceValue
|
|
);
|
|
await page.getByTestId("pos-order-customer-wishes-reference").click();
|
|
const referenceInput = page.getByTestId("pos-order-customer-wishes-reference-input");
|
|
await referenceInput.fill(referenceValue);
|
|
|
|
const capturedReferenceRequest = await referenceRequest;
|
|
expect(capturedReferenceRequest.postDataJSON()).toMatchObject({
|
|
id: 54518,
|
|
reference: referenceValue,
|
|
});
|
|
await expect(referenceInput).toBeFocused();
|
|
|
|
const poRequest = waitForOrderMutation(
|
|
page,
|
|
"PUT",
|
|
"/orders",
|
|
(body) => Number(body.id) === 54518 && body.po === poValue
|
|
);
|
|
await page.getByTestId("pos-order-customer-wishes-po").click();
|
|
const poInput = page.getByTestId("pos-order-customer-wishes-po-input");
|
|
await poInput.fill(poValue);
|
|
|
|
const capturedPoRequest = await poRequest;
|
|
expect(capturedPoRequest.postDataJSON()).toMatchObject({
|
|
id: 54518,
|
|
po: poValue,
|
|
});
|
|
await expect(poInput).toBeFocused();
|
|
|
|
const noteRequest = waitForOrderMutation(
|
|
page,
|
|
"PUT",
|
|
"/orders",
|
|
(body) => Number(body.id) === 54518 && body.notes === noteValue
|
|
);
|
|
await page.getByTestId("pos-order-metadata-note").locator("button").click();
|
|
const noteInput = page.getByTestId("pos-order-note-textarea");
|
|
await noteInput.fill(noteValue);
|
|
|
|
const capturedNoteRequest = await noteRequest;
|
|
expect(capturedNoteRequest.postDataJSON()).toMatchObject({
|
|
id: 54518,
|
|
notes: noteValue,
|
|
});
|
|
await expect(noteInput).toBeFocused();
|
|
|
|
await page.reload();
|
|
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toContainText(referenceValue);
|
|
await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toContainText(poValue);
|
|
await expect(page.getByTestId("pos-order-metadata-note")).toContainText(noteValue);
|
|
});
|
|
|
|
test("shows empty-state pills for cleared customer wishes fields", async ({ page }) => {
|
|
await openOrderDetail(page);
|
|
|
|
const clearReferenceRequest = waitForOrderMutation(
|
|
page,
|
|
"PUT",
|
|
"/orders",
|
|
(body) => Number(body.id) === 54518 && body.reference === ""
|
|
);
|
|
await page.getByTestId("pos-order-customer-wishes-reference").click();
|
|
const referenceInput = page.getByTestId("pos-order-customer-wishes-reference-input");
|
|
await referenceInput.fill("");
|
|
const capturedReferenceRequest = await clearReferenceRequest;
|
|
expect(capturedReferenceRequest.postDataJSON()).toMatchObject({
|
|
id: 54518,
|
|
reference: "",
|
|
});
|
|
|
|
await page.getByTestId("pos-order-customer-wishes-po").click();
|
|
const poInput = page.getByTestId("pos-order-customer-wishes-po-input");
|
|
const setPoRequest = waitForOrderMutation(
|
|
page,
|
|
"PUT",
|
|
"/orders",
|
|
(body) => Number(body.id) === 54518 && body.po === "temp-po"
|
|
);
|
|
await poInput.fill("temp-po");
|
|
const capturedSetPoRequest = await setPoRequest;
|
|
expect(capturedSetPoRequest.postDataJSON()).toMatchObject({
|
|
id: 54518,
|
|
po: "temp-po",
|
|
});
|
|
|
|
const clearPoRequest = waitForOrderMutation(
|
|
page,
|
|
"PUT",
|
|
"/orders",
|
|
(body) => Number(body.id) === 54518 && body.po === ""
|
|
);
|
|
await poInput.fill("");
|
|
const capturedPoRequest = await clearPoRequest;
|
|
expect(capturedPoRequest.postDataJSON()).toMatchObject({
|
|
id: 54518,
|
|
po: "",
|
|
});
|
|
|
|
await page.reload();
|
|
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-customer-wishes-reference")).toContainText("+ Tilføj");
|
|
await expect(page.getByTestId("pos-order-customer-wishes-po")).toContainText("+ Tilføj");
|
|
});
|
|
|
|
test("autosaves inline registration metadata and reloads normalized values", async ({ page }) => {
|
|
await openOrderDetail(page);
|
|
|
|
const rawReg1 = "xy-12 34";
|
|
const rawReg2 = "tr/56 78";
|
|
const rawReg3 = "no_90 12";
|
|
|
|
const reg1Request = waitForOrderMutation(
|
|
page,
|
|
"PUT",
|
|
"/orders",
|
|
(body) => Number(body.id) === 54518 && body.reg_1 === "XY-12 34"
|
|
);
|
|
await page.getByTestId("pos-order-registration-1").click();
|
|
const reg1Input = page.getByTestId("pos-order-registration-input-1");
|
|
await reg1Input.fill(rawReg1);
|
|
const capturedReg1Request = await reg1Request;
|
|
expect(capturedReg1Request.postDataJSON()).toMatchObject({
|
|
id: 54518,
|
|
reg_1: "XY-12 34",
|
|
});
|
|
await expect(reg1Input).toHaveValue("XY1234", { timeout: 10000 });
|
|
|
|
const reg2Request = waitForOrderMutation(
|
|
page,
|
|
"PUT",
|
|
"/orders",
|
|
(body) => Number(body.id) === 54518 && body.reg_2 === "TR/56 78"
|
|
);
|
|
await page.getByTestId("pos-order-registration-add-2").click();
|
|
const reg2Input = page.getByTestId("pos-order-registration-input-2");
|
|
await reg2Input.fill(rawReg2);
|
|
const capturedReg2Request = await reg2Request;
|
|
expect(capturedReg2Request.postDataJSON()).toMatchObject({
|
|
id: 54518,
|
|
reg_2: "TR/56 78",
|
|
});
|
|
await expect(reg2Input).toHaveValue("TR5678", { timeout: 10000 });
|
|
|
|
const reg3Request = waitForOrderMutation(
|
|
page,
|
|
"PUT",
|
|
"/orders",
|
|
(body) => Number(body.id) === 54518 && body.reg_3 === "NO_90 12"
|
|
);
|
|
await page.getByTestId("pos-order-registration-add-3").click();
|
|
const reg3Input = page.getByTestId("pos-order-registration-input-3");
|
|
await reg3Input.fill(rawReg3);
|
|
const capturedReg3Request = await reg3Request;
|
|
expect(capturedReg3Request.postDataJSON()).toMatchObject({
|
|
id: 54518,
|
|
reg_3: "NO_90 12",
|
|
});
|
|
await expect(reg3Input).toHaveValue("NO9012", { timeout: 10000 });
|
|
|
|
await page.reload();
|
|
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-registration-1")).toContainText("XY1234");
|
|
await expect(page.getByTestId("pos-order-registration-2")).toContainText("TR5678");
|
|
await expect(page.getByTestId("pos-order-registration-3")).toContainText("NO9012");
|
|
});
|
|
|
|
test("edits a primary order item in the Buefy modal and persists after reload", async ({ page }) => {
|
|
await openOrderDetail(page);
|
|
await expectOrderTotal(page, 1372);
|
|
|
|
await openOrderItemEditModal(page, 9101);
|
|
await page.getByTestId("pos-order-item-edit-price").fill("700");
|
|
await page.getByTestId("pos-order-item-edit-quantity").fill("2");
|
|
await page.getByTestId("pos-order-item-edit-notes").fill("Primary item note");
|
|
await page.getByTestId("pos-order-item-edit-reference").fill("PRIMARY-REF");
|
|
|
|
const editRequest = waitForOrderItemMutation(page, "PUT", "/order/items");
|
|
await page.getByTestId("pos-order-item-edit-save").click();
|
|
await editRequest;
|
|
|
|
await expect(page.getByTestId("pos-order-item-edit-modal")).toBeHidden();
|
|
await expect(page.getByTestId("pos-order-item-note-trigger-9101")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-item-reference-trigger-9101")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-item-quantity-9101")).toContainText("2");
|
|
await expect(page.getByTestId("pos-order-item-price-9101")).toContainText("1400");
|
|
await expectOrderTotal(page, 2123);
|
|
|
|
await 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(getOrderSettingsField(page, "customer_id")).toBeVisible();
|
|
await expect(getOrderSettingsField(page, "department_id")).toBeVisible();
|
|
await expect(getOrderSettingsField(page, "created_at")).toBeVisible();
|
|
await expect(getOrderSettingsField(page, "include_in_invoice")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-settings-include-helper")).not.toHaveText(/^\s*$/);
|
|
await expect(getOrderSettingsEditButton(page, "reference")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-settings-delete")).toBeVisible();
|
|
});
|
|
|
|
test("persists scalar metadata edits across settings and details tabs", async ({ page }) => {
|
|
const { orderId } = await createDisposableOrder(page);
|
|
const submittedValues = {
|
|
reference: "UPDATED-REF-2026",
|
|
reg_1: " zz-99 881 ",
|
|
reg_2: " tr/99-882 ",
|
|
reg_3: " tl_99 883 ",
|
|
notes: "Updated order note",
|
|
po: "PO-9001",
|
|
lane: "7",
|
|
wash_id: "WASH-77",
|
|
booking_id: "8801",
|
|
};
|
|
const expectedValues = {
|
|
...submittedValues,
|
|
reg_1: "ZZ99881",
|
|
reg_2: "TR99882",
|
|
reg_3: "TL99883",
|
|
};
|
|
|
|
await openOrderSettings(page, orderId);
|
|
|
|
await submitOrderSettingModal(page, "reference", submittedValues.reference);
|
|
await submitOrderSettingModal(page, "reg_1", submittedValues.reg_1);
|
|
await submitOrderSettingModal(page, "reg_2", submittedValues.reg_2);
|
|
await submitOrderSettingModal(page, "reg_3", submittedValues.reg_3);
|
|
await submitOrderSettingModal(page, "notes", submittedValues.notes);
|
|
await submitOrderSettingModal(page, "po", submittedValues.po);
|
|
await submitOrderSettingModal(page, "lane", submittedValues.lane);
|
|
await submitOrderSettingModal(page, "wash_id", submittedValues.wash_id);
|
|
await submitOrderSettingModal(page, "booking_id", submittedValues.booking_id);
|
|
|
|
await reloadOrderSettings(page, orderId);
|
|
await expect(getOrderSettingsField(page, "reference")).toContainText(expectedValues.reference);
|
|
await expect(getOrderSettingsField(page, "reg_1")).toContainText(expectedValues.reg_1);
|
|
await expect(getOrderSettingsField(page, "reg_2")).toContainText(expectedValues.reg_2);
|
|
await expect(getOrderSettingsField(page, "reg_3")).toContainText(expectedValues.reg_3);
|
|
await expect(getOrderSettingsField(page, "notes")).toContainText(expectedValues.notes);
|
|
await expect(getOrderSettingsField(page, "po")).toContainText(expectedValues.po);
|
|
await expect(getOrderSettingsField(page, "lane")).toContainText(expectedValues.lane);
|
|
await expect(getOrderSettingsField(page, "wash_id")).toContainText(expectedValues.wash_id);
|
|
await expect(getOrderSettingsField(page, "booking_id")).toContainText(expectedValues.booking_id);
|
|
|
|
await openOrderDetailsTab(page);
|
|
await expect(getOrderDetailInput(page, "reference")).toHaveValue(expectedValues.reference);
|
|
await expect(getOrderDetailInput(page, "reg_1")).toHaveValue(expectedValues.reg_1);
|
|
await expect(getOrderDetailInput(page, "reg_2")).toHaveValue(expectedValues.reg_2);
|
|
await expect(getOrderDetailInput(page, "reg_3")).toHaveValue(expectedValues.reg_3);
|
|
await expect(getOrderDetailInput(page, "notes")).toHaveValue(expectedValues.notes);
|
|
await expect(getOrderDetailInput(page, "po")).toHaveValue(expectedValues.po);
|
|
await expect(getOrderDetailInput(page, "lane")).toHaveValue(expectedValues.lane);
|
|
await expect(getOrderDetailInput(page, "wash_id")).toHaveValue(expectedValues.wash_id);
|
|
await expect(getOrderDetailInput(page, "booking_id")).toHaveValue(expectedValues.booking_id);
|
|
});
|
|
|
|
test("persists department, created_at, and invoice inclusion changes through per-field modals", async ({ page }) => {
|
|
const { orderId } = await createDisposableOrder(page);
|
|
const updatedCreatedAt = "2026-04-09T13:37";
|
|
const helper = page.getByTestId("pos-order-settings-include-helper");
|
|
|
|
await openOrderSettings(page, orderId);
|
|
const initialHelper = ((await helper.textContent()) || "").trim();
|
|
expect(initialHelper).not.toBe("");
|
|
|
|
await submitOrderSettingModal(page, "department_id", "1", "select");
|
|
await submitOrderSettingModal(page, "created_at", updatedCreatedAt);
|
|
await submitOrderSettingModal(page, "include_in_invoice", "exclude", "select");
|
|
|
|
await reloadOrderSettings(page, orderId);
|
|
await expect(getOrderSettingsField(page, "department_id")).toContainText("Taastrup");
|
|
await expect(getOrderSettingsField(page, "created_at")).toContainText("2026-04-09 13:37:00");
|
|
await expect(getOrderSettingsField(page, "include_in_invoice")).toContainText(/Ekskluder|Exclude/);
|
|
const excludedHelper = ((await helper.textContent()) || "").trim();
|
|
expect(excludedHelper).not.toBe("");
|
|
expect(excludedHelper).not.toBe(initialHelper);
|
|
|
|
await submitOrderSettingModal(page, "include_in_invoice", "include", "select");
|
|
await reloadOrderSettings(page, orderId);
|
|
await expect(getOrderSettingsField(page, "include_in_invoice")).toContainText(/Inkluder|Include/);
|
|
const includedHelper = ((await helper.textContent()) || "").trim();
|
|
expect(includedHelper).not.toBe("");
|
|
expect(includedHelper).not.toBe(excludedHelper);
|
|
|
|
await submitOrderSettingModal(page, "include_in_invoice", "use_department", "select");
|
|
await reloadOrderSettings(page, orderId);
|
|
await expect(getOrderSettingsField(page, "include_in_invoice")).toContainText(/Brug afdeling|Use department/);
|
|
const inheritedExcludedHelper = ((await helper.textContent()) || "").trim();
|
|
expect(inheritedExcludedHelper).not.toBe("");
|
|
expect(inheritedExcludedHelper).not.toBe(includedHelper);
|
|
|
|
await openOrderDetailsTab(page);
|
|
await expect(getOrderDetailInput(page, "department_id")).toHaveValue("Taastrup");
|
|
await expect(getOrderDetailInput(page, "created_at")).toHaveValue("2026-04-09 13:37:00");
|
|
await expect(getOrderDetailInput(page, "include_in_invoice")).toHaveValue(/Ekskluder|Exclude/);
|
|
});
|
|
|
|
test("updates customer and invoice collection through the dedicated modal flows without a manual reload", async ({
|
|
page,
|
|
}) => {
|
|
const { orderId } = await createDisposableOrder(page);
|
|
|
|
await openOrderSettings(page, orderId);
|
|
const initialUrl = page.url();
|
|
|
|
await changeOrderCustomer(page, "999", "2026-04-30");
|
|
await expect(page).toHaveURL(initialUrl);
|
|
await expect(getOrderSettingsField(page, "customer_id")).toContainText("999");
|
|
await expect(getOrderSettingsField(page, "invoice_collection_id")).toContainText("300");
|
|
|
|
await openOrderDetailsTab(page);
|
|
await expect(getOrderDetailInput(page, "customer_id")).toHaveValue("999");
|
|
await expect(getOrderDetailInput(page, "invoice_collection_id")).toHaveValue("300");
|
|
|
|
await clickVisibleTestId(page, "pos-order-tab-settings");
|
|
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
|
|
|
|
await changeOrderInvoiceCollection(page, "2026-05-01");
|
|
await expect(page).toHaveURL(initialUrl);
|
|
await expect(getOrderSettingsField(page, "invoice_collection_id")).toContainText("301");
|
|
|
|
await openOrderDetailsTab(page);
|
|
await expect(getOrderDetailInput(page, "invoice_collection_id")).toHaveValue("301");
|
|
});
|
|
|
|
test("shows a paperclip attachment control, all attachment actions, and a hover preview when the order has attachments", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/admin/12/modules/pos/orders");
|
|
|
|
const attachmentDropdown = getVisibleTestId(page, "pos-order-list-attachments-54518");
|
|
const attachmentAction = attachmentDropdown.locator(".dropdown-trigger button");
|
|
const settingsAction = getVisibleTestId(page, "pos-order-list-settings-54518").locator(".dropdown-trigger button");
|
|
|
|
await expect(attachmentAction).toBeVisible();
|
|
await expect(settingsAction).toBeVisible();
|
|
|
|
const attachmentBox = await attachmentAction.boundingBox();
|
|
const settingsBox = await settingsAction.boundingBox();
|
|
|
|
expect(attachmentBox).not.toBeNull();
|
|
expect(settingsBox).not.toBeNull();
|
|
expect(attachmentBox?.x ?? 0).toBeLessThan(settingsBox?.x ?? 0);
|
|
await expect(attachmentAction).toHaveClass(/is-dark/);
|
|
await expect(attachmentAction.locator(".fa-paperclip")).toBeVisible();
|
|
|
|
await attachmentAction.click();
|
|
const attachmentItems = attachmentDropdown.locator(".dropdown-content button.dropdown-item-action");
|
|
await expect(attachmentItems.nth(0)).toContainText(/vaskecertifikat|wash certificate/i);
|
|
await expect(attachmentItems.nth(1)).toContainText(/upload/i);
|
|
await expect(attachmentItems.nth(2)).toContainText(/safety-seal\.pdf/i);
|
|
|
|
const hoveredAttachmentItem = page.getByTestId("pos-order-list-attachment-item-54518-301");
|
|
await hoveredAttachmentItem.hover();
|
|
|
|
const dropdownContent = attachmentDropdown.locator(".dropdown-content");
|
|
const previewPanel = page.getByTestId("pos-order-list-attachment-preview-54518");
|
|
await expect(previewPanel).toBeVisible();
|
|
await expect(previewPanel.locator("iframe")).toBeVisible();
|
|
|
|
const dropdownContentBox = await dropdownContent.boundingBox();
|
|
const previewPanelBox = await previewPanel.boundingBox();
|
|
expect(dropdownContentBox).not.toBeNull();
|
|
expect(previewPanelBox).not.toBeNull();
|
|
expect(previewPanelBox?.x ?? 0).toBeLessThan(dropdownContentBox?.x ?? 0);
|
|
});
|
|
|
|
test("shows an icon-only plus attachment control and outlined dropdown when the order has no attachments", async ({
|
|
page,
|
|
}) => {
|
|
const { orderId } = await createDisposableOrder(page);
|
|
|
|
await page.goto("/admin/12/modules/pos/orders");
|
|
|
|
const paperclipAction = getVisibleTestId(page, "pos-order-list-attachments-54518").locator(
|
|
".dropdown-trigger button"
|
|
);
|
|
const attachmentDropdown = getVisibleTestId(page, `pos-order-list-attachments-${orderId}`);
|
|
const attachmentAction = attachmentDropdown.locator(".dropdown-trigger button");
|
|
const settingsAction = getVisibleTestId(page, `pos-order-list-settings-${orderId}`).locator(
|
|
".dropdown-trigger button"
|
|
);
|
|
|
|
await expect(attachmentAction).toBeVisible();
|
|
await expect(settingsAction).toBeVisible();
|
|
await expect(attachmentAction).toHaveClass(/action-settings-wheel-trigger--text/);
|
|
await expect(attachmentAction).not.toHaveClass(/is-text/);
|
|
await expect(attachmentAction).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
|
await expect(attachmentAction).toHaveCSS("text-decoration-line", "none");
|
|
await expect(attachmentAction).toHaveCSS("justify-content", "center");
|
|
await expect(attachmentAction.locator(".fa-plus")).toBeVisible();
|
|
await expect(attachmentAction.locator(".ml-2")).toHaveCount(0);
|
|
|
|
const paperclipBox = await paperclipAction.boundingBox();
|
|
const attachmentBox = await attachmentAction.boundingBox();
|
|
const settingsBox = await settingsAction.boundingBox();
|
|
const plusIconBox = await attachmentAction.locator(".fa-plus").boundingBox();
|
|
|
|
expect(paperclipBox).not.toBeNull();
|
|
expect(attachmentBox).not.toBeNull();
|
|
expect(settingsBox).not.toBeNull();
|
|
expect(plusIconBox).not.toBeNull();
|
|
expect(Math.abs((attachmentBox?.width ?? 0) - (paperclipBox?.width ?? 0))).toBeLessThanOrEqual(1);
|
|
expect(Math.abs((attachmentBox?.height ?? 0) - (paperclipBox?.height ?? 0))).toBeLessThanOrEqual(1);
|
|
expect(
|
|
Math.abs(
|
|
(plusIconBox?.x ?? 0) +
|
|
(plusIconBox?.width ?? 0) / 2 -
|
|
((attachmentBox?.x ?? 0) + (attachmentBox?.width ?? 0) / 2)
|
|
)
|
|
).toBeLessThanOrEqual(1);
|
|
expect(
|
|
Math.abs(
|
|
(plusIconBox?.y ?? 0) +
|
|
(plusIconBox?.height ?? 0) / 2 -
|
|
((attachmentBox?.y ?? 0) + (attachmentBox?.height ?? 0) / 2)
|
|
)
|
|
).toBeLessThanOrEqual(1);
|
|
expect(attachmentBox?.x ?? 0).toBeLessThan(settingsBox?.x ?? 0);
|
|
|
|
await attachmentAction.click();
|
|
const dropdownContent = attachmentDropdown.locator(".dropdown-content");
|
|
const attachmentItems = dropdownContent.locator("button.dropdown-item-action");
|
|
await expect(dropdownContent).toBeVisible();
|
|
await expect(dropdownContent).toHaveCSS("border-top-width", "1px");
|
|
await expect(dropdownContent).toHaveCSS("border-right-width", "1px");
|
|
await expect(dropdownContent).toHaveCSS("border-bottom-width", "1px");
|
|
await expect(dropdownContent).toHaveCSS("border-left-width", "1px");
|
|
await expect(dropdownContent).toHaveCSS("border-top-style", "solid");
|
|
await expect(attachmentItems.nth(0)).toContainText(/vaskecertifikat|wash certificate/i);
|
|
await expect(attachmentItems.nth(1)).toContainText(/upload/i);
|
|
});
|
|
});
|
|
|
|
test.describe("Admin POS wash certificate completion", () => {
|
|
test("shows and autosaves the safety seal field when the order contains a wash certificate item", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only wash certificate metadata coverage");
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: POS_PERMISSIONS,
|
|
edgeGateways: false,
|
|
pos: createPosFixture(),
|
|
});
|
|
await primeOperatorSession(page, "pos-safety-seal-token");
|
|
|
|
await openOrderDetail(page);
|
|
|
|
const safetySealField = page.getByTestId("pos-order-customer-wishes-safety-seal");
|
|
const referenceControl = page.getByTestId("pos-order-customer-wishes-reference-control");
|
|
const poControl = page.getByTestId("pos-order-customer-wishes-po-control");
|
|
const safetySealControl = page.getByTestId("pos-order-customer-wishes-safety-seal-control");
|
|
await expect(safetySealField).toBeVisible();
|
|
await expect(referenceControl).toBeVisible();
|
|
await expect(poControl).toBeVisible();
|
|
await expect(safetySealControl).toBeVisible();
|
|
|
|
const [referenceBox, poBox, safetySealBox] = await Promise.all([
|
|
referenceControl.boundingBox(),
|
|
poControl.boundingBox(),
|
|
safetySealControl.boundingBox(),
|
|
]);
|
|
|
|
expect(referenceBox).not.toBeNull();
|
|
expect(poBox).not.toBeNull();
|
|
expect(safetySealBox).not.toBeNull();
|
|
expect(safetySealBox!.width).toBeGreaterThan(referenceBox!.width * 1.8);
|
|
expect(safetySealBox!.width).toBeGreaterThan(poBox!.width * 1.8);
|
|
|
|
const updateRequest = waitForOrderMutation(
|
|
page,
|
|
"PUT",
|
|
"/orders",
|
|
(body) => Number(body.id) === 54518 && body.safety_seal === "SEAL-5566"
|
|
);
|
|
|
|
await safetySealField.click();
|
|
const safetySealInput = page.getByTestId("pos-order-customer-wishes-safety-seal-input");
|
|
await expect(safetySealInput).toBeVisible();
|
|
await safetySealInput.fill("SEAL-5566");
|
|
|
|
const capturedRequest = await updateRequest;
|
|
expect(capturedRequest.postDataJSON()).toMatchObject({
|
|
id: 54518,
|
|
safety_seal: "SEAL-5566",
|
|
});
|
|
await expect(safetySealInput).toBeFocused();
|
|
await expect(safetySealInput).toHaveValue("SEAL-5566");
|
|
});
|
|
|
|
test("defers material desktop completion to step 4 and auto-attaches the wash certificate on final completion", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only completion sequencing coverage");
|
|
|
|
const baseFixture = createPosFixture();
|
|
const materialOrderId = 56001;
|
|
const fixture = createPosFixture({
|
|
ordersById: {
|
|
...baseFixture.ordersById,
|
|
[materialOrderId]: {
|
|
...baseFixture.ordersById[54518],
|
|
id: materialOrderId,
|
|
reference: "AUTO-SEAL-REF",
|
|
completed_at: null,
|
|
},
|
|
},
|
|
orderItemsByOrderId: {
|
|
...baseFixture.orderItemsByOrderId,
|
|
[materialOrderId]: [
|
|
{
|
|
...baseFixture.orderItemsByOrderId[54518][0],
|
|
id: 9801,
|
|
order_id: materialOrderId,
|
|
related_item_id: null,
|
|
},
|
|
{
|
|
...baseFixture.orderItemsByOrderId[54518][2],
|
|
id: 9802,
|
|
order_id: materialOrderId,
|
|
related_item_id: 9801,
|
|
},
|
|
],
|
|
},
|
|
attachmentsByOrderId: {
|
|
...baseFixture.attachmentsByOrderId,
|
|
[materialOrderId]: [],
|
|
},
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: POS_PERMISSIONS,
|
|
edgeGateways: false,
|
|
pos: fixture,
|
|
});
|
|
await primeOperatorSession(page, "pos-material-sequencing-token");
|
|
|
|
await page.goto(`/admin/12/modules/pos?id=${materialOrderId}&customer_id=12345679&step=3`);
|
|
await expect(page.getByTestId("pos-step-3")).toBeVisible();
|
|
|
|
await clickVisibleTestId(page, "pos-next-step");
|
|
await expect(page.getByTestId("pos-step-4")).toBeVisible();
|
|
expect(fixture.ordersById[materialOrderId]?.completed_at).toBeNull();
|
|
await expect(page.getByTestId("pos-step-4").getByTestId("pos-next-step")).toBeVisible({ timeout: 10_000 });
|
|
|
|
const completionRequest = page.waitForRequest((request) => {
|
|
return request.method() === "POST" && request.url().includes("/orders/mark_as_completed");
|
|
});
|
|
await clickVisibleTestId(page, "pos-next-step");
|
|
await completionRequest;
|
|
|
|
await expect.poll(() => Boolean(fixture.ordersById[materialOrderId]?.completed_at), { timeout: 10_000 }).toBe(true);
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
Boolean(
|
|
fixture.attachmentsByOrderId[materialOrderId]?.some(
|
|
(attachment) => String(attachment?.content?.other || "").toUpperCase() === "WASH_CERTIFICATE"
|
|
)
|
|
),
|
|
{ timeout: 10_000 }
|
|
)
|
|
.toBe(true);
|
|
});
|
|
});
|
|
|
|
test.describe("Admin POS Orders - desktop required warning states", () => {
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only required warning coverage");
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: POS_PERMISSIONS,
|
|
edgeGateways: false,
|
|
pos: createRequiredWarningsPosFixture(),
|
|
});
|
|
await primeOperatorSession(page, "pos-orders-required-warnings-token");
|
|
});
|
|
|
|
test("shows required reference and po warnings in order detail and clears them without changing field size", async ({
|
|
page,
|
|
}) => {
|
|
await openOrderDetail(page);
|
|
|
|
const referenceControl = page.getByTestId("pos-order-customer-wishes-reference-control");
|
|
const poControl = page.getByTestId("pos-order-customer-wishes-po-control");
|
|
|
|
await expect(referenceControl).toHaveAttribute("data-warning-state", "danger");
|
|
await expect(poControl).toHaveAttribute("data-warning-state", "warning");
|
|
await expect(page.getByTestId("pos-order-customer-wishes-reference-warning-icon")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-customer-wishes-po-warning-icon")).toBeVisible();
|
|
|
|
const warningReferenceBox = await referenceControl.boundingBox();
|
|
const warningPoBox = await poControl.boundingBox();
|
|
expect(warningReferenceBox).not.toBeNull();
|
|
expect(warningPoBox).not.toBeNull();
|
|
expect(Math.abs((warningReferenceBox?.height ?? 0) - (warningPoBox?.height ?? 0))).toBeLessThanOrEqual(2);
|
|
|
|
const referenceValue = "REF1";
|
|
const poValue = "PO1";
|
|
|
|
const referenceRequest = waitForOrderMutation(
|
|
page,
|
|
"PUT",
|
|
"/orders",
|
|
(body) => Number(body.id) === 54518 && body.reference === referenceValue
|
|
);
|
|
await page.getByTestId("pos-order-customer-wishes-reference").click();
|
|
const referenceInput = page.getByTestId("pos-order-customer-wishes-reference-input");
|
|
await referenceInput.fill(referenceValue);
|
|
await expect(referenceControl).not.toHaveAttribute("data-warning-state", "danger");
|
|
await expect(page.getByTestId("pos-order-customer-wishes-reference-warning-icon")).toHaveCount(0);
|
|
await referenceInput.press("Enter");
|
|
await expect(referenceInput).toHaveCount(0);
|
|
await referenceRequest;
|
|
await expect(referenceControl).not.toHaveClass(/is-loading/);
|
|
|
|
const poRequest = waitForOrderMutation(
|
|
page,
|
|
"PUT",
|
|
"/orders",
|
|
(body) => Number(body.id) === 54518 && body.po === poValue
|
|
);
|
|
await page.getByTestId("pos-order-customer-wishes-po").click();
|
|
const poInput = page.getByTestId("pos-order-customer-wishes-po-input");
|
|
await poInput.fill(poValue);
|
|
await expect(poControl).not.toHaveAttribute("data-warning-state", "warning");
|
|
await expect(page.getByTestId("pos-order-customer-wishes-po-warning-icon")).toHaveCount(0);
|
|
await poInput.press("Enter");
|
|
await expect(poInput).toHaveCount(0);
|
|
await poRequest;
|
|
await expect(poControl).not.toHaveClass(/is-loading/);
|
|
|
|
await expect(page.getByTestId("pos-order-customer-wishes-reference")).toContainText(referenceValue);
|
|
await expect(page.getByTestId("pos-order-customer-wishes-po")).toContainText(poValue);
|
|
await expect(referenceControl).not.toHaveClass(/is-loading/);
|
|
await expect(poControl).not.toHaveClass(/is-loading/);
|
|
|
|
const clearedReferenceBox = await referenceControl.boundingBox();
|
|
const clearedPoBox = await poControl.boundingBox();
|
|
expect(clearedReferenceBox).not.toBeNull();
|
|
expect(clearedPoBox).not.toBeNull();
|
|
expect(Math.abs((clearedReferenceBox?.height ?? 0) - (clearedPoBox?.height ?? 0))).toBeLessThanOrEqual(2);
|
|
});
|
|
|
|
test("shows the same required warnings in the shared desktop step 2 workspace", async ({ page }) => {
|
|
await page.goto("/admin/12/modules/pos?step=1");
|
|
await expect(page.getByTestId("pos-step-1")).toBeVisible();
|
|
|
|
await page.locator("#reg_1").fill("WARN123");
|
|
await page.locator("#pos_select_customer_input").fill("12345679");
|
|
await expect(page.locator(".customer-drop-down-select").first()).toBeVisible();
|
|
await page.locator(".customer-drop-down-select").first().click();
|
|
|
|
await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click();
|
|
const stepTwo = page.getByTestId("pos-step-2");
|
|
await expect(stepTwo).toBeVisible();
|
|
|
|
await expect(stepTwo.getByTestId("pos-order-customer-wishes-reference-control")).toHaveAttribute(
|
|
"data-warning-state",
|
|
"danger"
|
|
);
|
|
await expect(stepTwo.getByTestId("pos-order-customer-wishes-po-control")).toHaveAttribute(
|
|
"data-warning-state",
|
|
"warning"
|
|
);
|
|
await expect(stepTwo.getByTestId("pos-order-customer-wishes-reference-warning-icon")).toBeVisible();
|
|
await expect(stepTwo.getByTestId("pos-order-customer-wishes-po-warning-icon")).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe("Admin POS Orders - desktop step 1 customer and vehicle ownership", () => {
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only step 1 customer ownership coverage");
|
|
});
|
|
|
|
test("preserves a manually selected required-reference customer and manual reference while editing reg_1", async ({
|
|
page,
|
|
}) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: POS_PERMISSIONS,
|
|
edgeGateways: false,
|
|
pos: createRequiredWarningsPosFixture(),
|
|
});
|
|
await primeOperatorSession(page, "pos-orders-manual-customer-reg-edit-token");
|
|
|
|
await page.goto("/admin/12/modules/pos?step=1");
|
|
await expect(page.getByTestId("pos-step-1")).toBeVisible();
|
|
|
|
await page.locator("#reg_1").fill("MAN");
|
|
await selectStepOneCustomer(page, 12345679);
|
|
|
|
const referenceInput = page.locator("#reference");
|
|
await referenceInput.fill("MANUAL-REF-001");
|
|
|
|
await page.locator("#reg_1").fill("MANUAL1");
|
|
await expect(page.locator(".pos-selected-customer__title").filter({ hasText: /\(TEST\) Pleno/ }).first()).toBeVisible();
|
|
await expect(referenceInput).toHaveValue("MANUAL-REF-001");
|
|
|
|
await page.locator("#reg_1").fill("");
|
|
await expect(page.locator(".pos-selected-customer__title").filter({ hasText: /\(TEST\) Pleno/ }).first()).toBeVisible();
|
|
await expect(referenceInput).toHaveValue("MANUAL-REF-001");
|
|
|
|
await page.locator("#reg_1").fill("MANUAL2");
|
|
await expect(page.locator(".pos-selected-customer__title").filter({ hasText: /\(TEST\) Pleno/ }).first()).toBeVisible();
|
|
await expect(referenceInput).toHaveValue("MANUAL-REF-001");
|
|
});
|
|
|
|
test("asks whether to keep the selected customer when an exact plate belongs to another customer", async ({
|
|
page,
|
|
}) => {
|
|
const fixture = createCustomerConflictPosFixture();
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: POS_PERMISSIONS,
|
|
edgeGateways: false,
|
|
pos: fixture,
|
|
});
|
|
await primeOperatorSession(page, "pos-orders-customer-conflict-keep-token");
|
|
|
|
await page.goto("/admin/12/modules/pos?step=1");
|
|
await expect(page.getByTestId("pos-step-1")).toBeVisible();
|
|
|
|
await page.locator("#reg_1").fill("CON");
|
|
await selectStepOneCustomer(page, 12345679);
|
|
await page.locator("#reference").fill("KEEP-REF");
|
|
|
|
await page.locator("#reg_1").fill("CONFLICT1");
|
|
const activeConflictModal = page.locator('[data-testid="pos-desktop-customer-conflict-modal"].is-active');
|
|
await expect(activeConflictModal).toBeVisible();
|
|
|
|
await activeConflictModal.getByTestId("pos-desktop-customer-conflict-keep").click();
|
|
await expect(activeConflictModal).toHaveCount(0);
|
|
|
|
await expect(page.locator(".pos-selected-customer__title").filter({ hasText: /\(TEST\) Pleno/ }).first()).toBeVisible();
|
|
await expect(page.locator("#reference")).toHaveValue("KEEP-REF");
|
|
await expect(page.locator("#reg_1")).toHaveValue("CONFLICT1");
|
|
|
|
await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click();
|
|
await expect(page).toHaveURL(/\/admin\/12\/modules\/pos\?id=\d+&customer_id=12345679&step=2/);
|
|
});
|
|
|
|
test("can switch to the matched vehicle customer from the conflict modal", async ({ page }) => {
|
|
const fixture = createCustomerConflictPosFixture();
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: POS_PERMISSIONS,
|
|
edgeGateways: false,
|
|
pos: fixture,
|
|
});
|
|
await primeOperatorSession(page, "pos-orders-customer-conflict-use-vehicle-token");
|
|
|
|
await page.goto("/admin/12/modules/pos?step=1");
|
|
await expect(page.getByTestId("pos-step-1")).toBeVisible();
|
|
|
|
await page.locator("#reg_1").fill("CON");
|
|
await selectStepOneCustomer(page, 12345679);
|
|
await page.locator("#reference").fill("MANUAL-REF");
|
|
|
|
await page.locator("#reg_1").fill("CONFLICT1");
|
|
const activeConflictModal = page.locator('[data-testid="pos-desktop-customer-conflict-modal"].is-active');
|
|
await expect(activeConflictModal).toBeVisible();
|
|
|
|
await activeConflictModal.getByTestId("pos-desktop-customer-conflict-use-vehicle").click();
|
|
await expect(activeConflictModal).toHaveCount(0);
|
|
|
|
await expect(page.locator(".pos-selected-customer__title").filter({ hasText: /\(TEST\) Conflict Fleet/ }).first()).toBeVisible();
|
|
await expect(page.locator("#reference")).toHaveValue("CONFLICT-VEHICLE-REF");
|
|
await expect(page.locator("#reg_1")).toHaveValue("CONFLICT1");
|
|
|
|
await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click();
|
|
await expect(page).toHaveURL(/\/admin\/12\/modules\/pos\?id=\d+&customer_id=22334455&step=2/);
|
|
});
|
|
});
|
|
|
|
test.describe("Admin POS Orders - desktop attachment discovery", () => {
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only attachment discovery coverage");
|
|
|
|
const posFixture = createPosFixture();
|
|
posFixture.ordersById[54518] = {
|
|
...posFixture.ordersById[54518],
|
|
attachments: [],
|
|
has_attachments: false,
|
|
attachments_count: 0,
|
|
attachment_count: 0,
|
|
attachmentsCount: 0,
|
|
};
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: POS_PERMISSIONS,
|
|
edgeGateways: false,
|
|
pos: posFixture,
|
|
});
|
|
await primeOperatorSession(page, "pos-orders-attachment-discovery-token");
|
|
});
|
|
|
|
test("discovers fetched attachments and switches the list trigger from plus to paperclip", async ({ page }) => {
|
|
await page.goto("/admin/12/modules/pos/orders");
|
|
|
|
const attachmentDropdown = getVisibleTestId(page, "pos-order-list-attachments-54518");
|
|
const attachmentAction = attachmentDropdown.locator(".dropdown-trigger button");
|
|
|
|
await expect.poll(async () => attachmentAction.locator(".fa-paperclip").count(), { timeout: 10000 }).toBe(1);
|
|
await expect(attachmentAction).toHaveClass(/is-dark/);
|
|
await expect(attachmentAction.locator(".fa-plus")).toHaveCount(0);
|
|
|
|
await attachmentAction.click();
|
|
const attachmentItems = attachmentDropdown.locator(".dropdown-content button.dropdown-item-action");
|
|
await expect(attachmentItems.nth(0)).toContainText(/vaskecertifikat|wash certificate/i);
|
|
await expect(attachmentItems.nth(1)).toContainText(/upload/i);
|
|
await expect(attachmentItems.nth(2)).toContainText(/safety-seal\.pdf/i);
|
|
});
|
|
});
|
|
|
|
test.describe("Admin POS Orders - desktop locked states", () => {
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only order lock coverage");
|
|
});
|
|
|
|
test("hides item mutation controls when edit permission is missing", async ({ page }) => {
|
|
const limitedPermissions = POS_PERMISSIONS.filter((permission) => permission !== "edit_order_items");
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: limitedPermissions,
|
|
edgeGateways: false,
|
|
pos: createPosFixture(),
|
|
});
|
|
await primeOperatorSession(page, "pos-orders-no-edit-token", limitedPermissions);
|
|
await openOrderDetail(page);
|
|
|
|
await expect(page.getByTestId("pos-order-item-edit-9101")).toHaveCount(0);
|
|
await expect(page.getByTestId("pos-order-item-delete-9101")).toHaveCount(0);
|
|
await expect(page.getByTestId("pos-order-add-item")).toHaveCount(0);
|
|
await expectOrderTotal(page, 1372);
|
|
});
|
|
|
|
test("hides item mutation controls when the order is already invoiced", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: POS_PERMISSIONS,
|
|
edgeGateways: false,
|
|
pos: createPosFixture({
|
|
economicModuleOrdersByOrderId: {
|
|
54518: {
|
|
invoice_id: 99101,
|
|
invoice_draft_id: null,
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
await primeOperatorSession(page, "pos-orders-invoiced-token");
|
|
await openOrderDetail(page);
|
|
|
|
await expect(page.getByTestId("pos-order-item-edit-9101")).toHaveCount(0);
|
|
await expect(page.getByTestId("pos-order-item-delete-9101")).toHaveCount(0);
|
|
await expect(page.getByTestId("pos-order-add-item")).toHaveCount(0);
|
|
await expectOrderTotal(page, 1372);
|
|
});
|
|
});
|
|
|
|
test.describe("Admin POS Orders - mobile smoke", () => {
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
test.skip(!testInfo.project.name.toLowerCase().includes("mobile"), "Mobile-only order settings smoke");
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: POS_PERMISSIONS,
|
|
edgeGateways: false,
|
|
pos: createPosFixture(),
|
|
});
|
|
await primeOperatorSession(page, "pos-orders-mobile-token");
|
|
});
|
|
|
|
test("opens the settings tab and renders the new controls", async ({ page }) => {
|
|
await page.goto("/admin/12/modules/pos/orders/54518");
|
|
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
|
|
|
await clickVisibleTestId(page, "pos-order-tab-settings");
|
|
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
|
|
await expect(getOrderSettingsField(page, "department_id")).toBeVisible();
|
|
await expect(getOrderSettingsField(page, "created_at")).toBeVisible();
|
|
await expect(getOrderSettingsField(page, "include_in_invoice")).toBeVisible();
|
|
});
|
|
|
|
test("shows the wash certificate action in the attachments tab", async ({ page }) => {
|
|
await openOrderAttachments(page);
|
|
await openAddAttachmentCard(page);
|
|
});
|
|
|
|
test("keeps horizontal gutters around the mobile order tabs", async ({ page }) => {
|
|
await openOrderDetail(page);
|
|
|
|
const noteEmptyState = page.getByTestId("pos-order-note-empty-state");
|
|
await expect(noteEmptyState).toBeVisible();
|
|
await expect(noteEmptyState.locator(".pos-order-field__empty-pill")).toHaveText(/^\+\s+\S+/);
|
|
|
|
const tabsContainer = getVisibleTestId(page, "pos-order-mobile-tabs");
|
|
const cartTab = getVisibleTestId(page, "pos-order-tab-cart");
|
|
const containerBox = await tabsContainer.boundingBox();
|
|
const cartTabBox = await cartTab.boundingBox();
|
|
|
|
expect(containerBox).not.toBeNull();
|
|
expect(cartTabBox).not.toBeNull();
|
|
expect(cartTabBox!.x - containerBox!.x).toBeGreaterThanOrEqual(10);
|
|
expect(containerBox!.x + containerBox!.width - (cartTabBox!.x + cartTabBox!.width)).toBeGreaterThanOrEqual(10);
|
|
});
|
|
|
|
test("supports mobile item edit, add-panel access, and delete control visibility", async ({ page }) => {
|
|
await openOrderDetail(page);
|
|
await expectOrderTotal(page, 1372);
|
|
|
|
await openOrderItemEditModal(page, 9101);
|
|
await page.getByTestId("pos-order-item-edit-quantity").fill("2");
|
|
|
|
const editRequest = waitForOrderItemMutation(page, "PUT", "/order/items");
|
|
await page.getByTestId("pos-order-item-edit-save").click();
|
|
await editRequest;
|
|
|
|
await expect(page.getByTestId("pos-order-item-edit-modal")).toBeHidden();
|
|
await expectOrderTotal(page, 2021);
|
|
|
|
await page.getByTestId("pos-order-add-item").click();
|
|
await expect(page.getByTestId("pos-order-add-items-panel").first()).toBeVisible();
|
|
await page.getByTestId("pos-order-item-delete-9101").first().scrollIntoViewIfNeeded();
|
|
await expect(page.getByTestId("pos-order-item-delete-9101").first()).toBeVisible();
|
|
});
|
|
|
|
test("keeps the mobile search and reload controls inline and shows a clear card boundary", async ({ page }) => {
|
|
await page.goto("/admin/12/modules/pos/orders");
|
|
|
|
const searchInput = page.getByTestId("pagination-search-input");
|
|
const reloadActions = page.getByTestId("pagination-reload-actions");
|
|
const orderCard = page.getByTestId("pos-order-list-card-54518");
|
|
|
|
await expect(searchInput).toBeVisible();
|
|
await expect(reloadActions).toBeVisible();
|
|
await expect(orderCard).toBeVisible();
|
|
|
|
const searchBox = await searchInput.boundingBox();
|
|
const reloadBox = await reloadActions.boundingBox();
|
|
|
|
expect(searchBox).not.toBeNull();
|
|
expect(reloadBox).not.toBeNull();
|
|
const searchCenterY = (searchBox?.y ?? 0) + (searchBox?.height ?? 0) / 2;
|
|
const reloadCenterY = (reloadBox?.y ?? 0) + (reloadBox?.height ?? 0) / 2;
|
|
expect(Math.abs(searchCenterY - reloadCenterY)).toBeLessThanOrEqual(4);
|
|
expect(searchBox?.x ?? 0).toBeLessThan(reloadBox?.x ?? 0);
|
|
expect((searchBox?.x ?? 0) + (searchBox?.width ?? 0) - (reloadBox?.x ?? 0)).toBeLessThanOrEqual(4);
|
|
expect(searchBox?.width ?? 0).toBeGreaterThan(reloadBox?.width ?? 0);
|
|
|
|
await expect(orderCard).toHaveCSS("border-top-width", "1px");
|
|
await expect(orderCard).toHaveCSS("border-right-width", "1px");
|
|
await expect(orderCard).toHaveCSS("border-bottom-width", "1px");
|
|
await expect(orderCard).toHaveCSS("border-left-width", "1px");
|
|
});
|
|
|
|
test("stacks the add-items workspace on mobile and still lets the user add products", async ({ page }) => {
|
|
await openOrderDetail(page);
|
|
|
|
const main = getVisibleTestId(page, "pos-order-main");
|
|
const beforeMainBox = await main.boundingBox();
|
|
const beforeRailBox = await getVisibleTestId(page, "pos-order-rail").boundingBox();
|
|
expect(beforeMainBox).not.toBeNull();
|
|
expect(beforeRailBox).not.toBeNull();
|
|
|
|
await openAddItemsPanel(page);
|
|
await expect(page.locator('[data-testid="pos-order-rail"]:visible')).toHaveCount(0);
|
|
|
|
const productPanel = page.locator('[data-testid="pos-order-add-items-panel"]:visible').first();
|
|
const cartPanel = page.locator('[data-testid="pos-order-panel-cart"]:visible').first();
|
|
const workspace = getVisibleTestId(page, "pos-order-workspace");
|
|
const productPanelBox = await productPanel.boundingBox();
|
|
const cartPanelBox = await cartPanel.boundingBox();
|
|
const workspaceBox = await workspace.boundingBox();
|
|
expect(productPanelBox).not.toBeNull();
|
|
expect(cartPanelBox).not.toBeNull();
|
|
expect(workspaceBox).not.toBeNull();
|
|
expect(workspaceBox?.width ?? 0).toBeGreaterThan(300);
|
|
expect(Math.abs((productPanelBox?.x ?? 0) - (cartPanelBox?.x ?? 0))).toBeLessThanOrEqual(12);
|
|
expect(Math.abs((productPanelBox?.width ?? 0) - (cartPanelBox?.width ?? 0))).toBeLessThanOrEqual(24);
|
|
expect(cartPanelBox?.y ?? 0).toBeGreaterThan((productPanelBox?.y ?? 0) + 100);
|
|
|
|
await productPanel.locator("select").first().selectOption("8");
|
|
await expect(productPanel.getByTestId("pos-product-card-64")).toBeVisible();
|
|
await expect(productPanel.getByTestId("pos-product-card-53")).toHaveCount(0);
|
|
|
|
await productPanel.getByTestId("pos-product-card-64").click();
|
|
const addRequest = waitForOrderItemMutation(page, "POST", "/order/items");
|
|
await productPanel.getByTestId("pos-add-to-cart-64").click();
|
|
await addRequest;
|
|
|
|
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
|
|
await expect(getVisibleTestId(page, "pos-order-item-name-9200")).toContainText("Vaskecertifikat");
|
|
await expectOrderTotal(page, 1397);
|
|
|
|
await page.locator('[data-testid="pos-order-add-items-back"]:visible').first().click();
|
|
await expect(page.locator('[data-testid="pos-order-add-items-panel"]:visible')).toHaveCount(0);
|
|
await expect(getVisibleTestId(page, "pos-order-rail")).toBeVisible();
|
|
|
|
const restoredMainBox = await main.boundingBox();
|
|
expect(restoredMainBox).not.toBeNull();
|
|
expect(Math.abs((restoredMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0))).toBeLessThanOrEqual(20);
|
|
|
|
await page.reload();
|
|
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
|
|
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
|
|
await expectOrderTotal(page, 1397);
|
|
});
|
|
});
|