- Introduced `.prettierrc.json` to enforce consistent code formatting across the project. - Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
1080 lines
36 KiB
JavaScript
1080 lines
36 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import {
|
|
DEFAULT_BOOKING_ID,
|
|
DEFAULT_DEPARTMENT_ID,
|
|
REGULAR_CUSTOMER_ID,
|
|
createAttachmentFile,
|
|
createMobilePosFixture,
|
|
getStoredPosSnapshot,
|
|
setupMobilePosPage,
|
|
suppressVueDevtoolsOverlay,
|
|
waitForMobileNextStepCooldown,
|
|
} from "./support/mobilePos.js";
|
|
|
|
function buildRegularOrder(orderId, overrides = {}) {
|
|
return {
|
|
id: orderId,
|
|
customer_id: REGULAR_CUSTOMER_ID,
|
|
department_id: DEFAULT_DEPARTMENT_ID,
|
|
reference: "STEP2-REF",
|
|
notes: "",
|
|
po: "",
|
|
reg_1: "AB12345",
|
|
reg_2: "",
|
|
reg_3: "",
|
|
invoice_collection_id: null,
|
|
booking_id: null,
|
|
completed_at: null,
|
|
created_at: "2026-01-01T10:00:00.000Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function selectCustomerFromPopup(page, customerNumber = REGULAR_CUSTOMER_ID) {
|
|
await expect(page.getByTestId("pos-mobile-customer-popup")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-customer-search-input").fill(String(customerNumber));
|
|
await expect(page.getByTestId("pos-mobile-customer-search-result-0")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-customer-search-result-0").click();
|
|
await expect(page.getByTestId("pos-mobile-customer-popup")).toBeHidden({ timeout: 10_000 });
|
|
}
|
|
|
|
async function waitForStepReset(page) {
|
|
await expect
|
|
.poll(
|
|
() => {
|
|
const currentUrl = new URL(page.url());
|
|
return currentUrl.searchParams.get("step") || "1";
|
|
},
|
|
{ timeout: 12_000 }
|
|
)
|
|
.toBe("1");
|
|
await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 });
|
|
}
|
|
|
|
async function expectHorizontallyCentered(container, target, tolerance = 8) {
|
|
const [containerBox, targetBox] = await Promise.all([container.boundingBox(), target.boundingBox()]);
|
|
|
|
expect(containerBox).not.toBeNull();
|
|
expect(targetBox).not.toBeNull();
|
|
|
|
const containerCenter = containerBox.x + containerBox.width / 2;
|
|
const targetCenter = targetBox.x + targetBox.width / 2;
|
|
|
|
expect(Math.abs(containerCenter - targetCenter)).toBeLessThanOrEqual(tolerance);
|
|
}
|
|
|
|
async function expectSameRow(left, right, tolerance = 8) {
|
|
const [leftBox, rightBox] = await Promise.all([left.boundingBox(), right.boundingBox()]);
|
|
|
|
expect(leftBox).not.toBeNull();
|
|
expect(rightBox).not.toBeNull();
|
|
|
|
const leftCenterY = leftBox.y + leftBox.height / 2;
|
|
const rightCenterY = rightBox.y + rightBox.height / 2;
|
|
|
|
expect(Math.abs(leftCenterY - rightCenterY)).toBeLessThanOrEqual(tolerance);
|
|
}
|
|
|
|
async function expectAboveFixedActions(page, target, tolerance = 8) {
|
|
const [targetBox, actionsBox] = await Promise.all([
|
|
target.boundingBox(),
|
|
page.getByTestId("pos-mobile-fixed-actions").boundingBox(),
|
|
]);
|
|
|
|
expect(targetBox).not.toBeNull();
|
|
expect(actionsBox).not.toBeNull();
|
|
|
|
expect(targetBox.y + targetBox.height).toBeLessThanOrEqual(actionsBox.y - tolerance);
|
|
}
|
|
|
|
async function selectPrimaryProduct(page, productId = 53) {
|
|
await expect(page.getByTestId("pos-mobile-vehicle-selection")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-mobile-category-4")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-category-4").click();
|
|
await page.waitForTimeout(800);
|
|
await expect(page.getByTestId(`pos-mobile-product-${productId}`)).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId(`pos-mobile-product-${productId}`).click();
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
}
|
|
|
|
async function waitForBookingHydration(page, { primaryId, addonProductIds = [] } = {}) {
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const snapshot = await getStoredPosSnapshot(page);
|
|
const addonIds = (snapshot?.transactionItems?.primaryItem?.addons || [])
|
|
.map((addon) => Number(addon?.product?.id ?? addon?.id))
|
|
.sort((left, right) => left - right);
|
|
return {
|
|
primaryId: snapshot?.transactionItems?.primaryItem?.id ?? null,
|
|
addonIds,
|
|
};
|
|
},
|
|
{ timeout: 10_000 }
|
|
)
|
|
.toEqual({
|
|
primaryId,
|
|
addonIds: [...addonProductIds].sort((left, right) => left - right),
|
|
});
|
|
}
|
|
|
|
async function createOrderFromStep1(
|
|
page,
|
|
fixture,
|
|
{
|
|
reg = "FREE123",
|
|
reference = "STEP1-REF",
|
|
manualInput = false,
|
|
customerNumber = REGULAR_CUSTOMER_ID,
|
|
extraSeedState = {},
|
|
} = {}
|
|
) {
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: `step1-${reg.toLowerCase()}-token`,
|
|
seedState: {
|
|
customerId: null,
|
|
reg,
|
|
reference,
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
lastOrderId: null,
|
|
...extraSeedState,
|
|
},
|
|
route: {
|
|
step: 1,
|
|
},
|
|
});
|
|
|
|
if (manualInput) {
|
|
await page.getByTestId("pos-mobile-manual-input-toggle").click();
|
|
await expect(page.getByTestId("pos-mobile-manual-input")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-reg-input-1").fill(reg.toLowerCase());
|
|
await expect(page.getByTestId("pos-mobile-reg-input-1")).toHaveValue(reg.toUpperCase());
|
|
await page.getByTestId("pos-mobile-manual-input-close").click();
|
|
await expect(page.getByTestId("pos-mobile-manual-input")).toBeHidden({ timeout: 10_000 });
|
|
}
|
|
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await selectCustomerFromPopup(page, customerNumber);
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
}
|
|
|
|
test.describe("POS mobile order flow", () => {
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
test.skip(testInfo.project.name !== "chromium-mobile", "Mobile POS order suite is scoped to chromium-mobile.");
|
|
await suppressVueDevtoolsOverlay(page);
|
|
});
|
|
|
|
test("scanner shell renders and prompts for customer selection", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-shell-token",
|
|
seedState: {
|
|
customerId: null,
|
|
reg: "FREE123",
|
|
reference: "SHELL-REF",
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
lastOrderId: null,
|
|
},
|
|
route: {
|
|
step: 1,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-mobile-step-1-title")).toContainText("Scan nummerplader");
|
|
await expect(page.getByTestId("pos-mobile-step-1-subtitle")).toContainText("Hold kameraet op mod nummerpladerne.");
|
|
await expectHorizontallyCentered(
|
|
page.getByTestId("pos-mobile-step-1-shell"),
|
|
page.getByTestId("pos-mobile-step-1-title")
|
|
);
|
|
await expectHorizontallyCentered(
|
|
page.getByTestId("pos-mobile-step-1-shell"),
|
|
page.getByTestId("pos-mobile-step-1-subtitle")
|
|
);
|
|
await expectHorizontallyCentered(
|
|
page.getByTestId("pos-mobile-step-1-shell"),
|
|
page.getByTestId("pos-mobile-manual-input-toggle")
|
|
);
|
|
|
|
const [registrationRowBox, referenceTriggerBox] = await Promise.all([
|
|
page.getByTestId("pos-mobile-step-1-registration-row").boundingBox(),
|
|
page.getByTestId("pos-mobile-step-1-reference-trigger").boundingBox(),
|
|
]);
|
|
|
|
expect(registrationRowBox).not.toBeNull();
|
|
expect(referenceTriggerBox).not.toBeNull();
|
|
expect(Math.abs((registrationRowBox?.width ?? 0) - (referenceTriggerBox?.width ?? 0))).toBeLessThanOrEqual(4);
|
|
expect(Math.abs((registrationRowBox?.x ?? 0) - (referenceTriggerBox?.x ?? 0))).toBeLessThanOrEqual(4);
|
|
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect(page.getByTestId("pos-mobile-customer-popup")).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
test("transaction history keeps each wash visually separated with a border", async ({ page }) => {
|
|
const fixture = createMobilePosFixture({
|
|
ordersById: {
|
|
9201: buildRegularOrder(9201, {
|
|
customer_name: "VOGNMAND JIMMY CHRISTENSEN ApS",
|
|
total_net_amount: 746,
|
|
pending_handheld: true,
|
|
created_at: "2026-04-09T12:08:00.000Z",
|
|
}),
|
|
9202: buildRegularOrder(9202, {
|
|
customer_name: "BYGMA Roskilde A/S",
|
|
total_net_amount: 507,
|
|
pending_handheld: true,
|
|
created_at: "2026-04-09T11:51:00.000Z",
|
|
}),
|
|
9203: buildRegularOrder(9203, {
|
|
customer_name: "VOLVO ENTREPRENØRMASKINER A/S",
|
|
total_net_amount: 512,
|
|
pending_handheld: true,
|
|
created_at: "2026-04-09T10:10:00.000Z",
|
|
}),
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-transaction-history-token",
|
|
seedState: {
|
|
customerId: null,
|
|
reg: "FREE123",
|
|
reference: "HISTORY-REF",
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
lastOrderId: null,
|
|
transactionHistoryView: true,
|
|
},
|
|
route: {
|
|
step: 1,
|
|
},
|
|
});
|
|
|
|
const cards = page.locator('[data-testid^="pos-mobile-transaction-history-card-"]');
|
|
await expect(cards.first()).toBeVisible({ timeout: 10_000 });
|
|
expect(await cards.count()).toBe(3);
|
|
await expect(cards.first()).toHaveCSS("border-top-style", "solid");
|
|
await expect(cards.first()).toHaveCSS("border-top-width", "1px");
|
|
});
|
|
|
|
test("customer notes popup shows the translated created-by label", async ({ page }) => {
|
|
const fixture = createMobilePosFixture({
|
|
customerNotesByNumber: {
|
|
[REGULAR_CUSTOMER_ID]: [
|
|
{
|
|
id: 401,
|
|
note: "test123",
|
|
created_at: "2026-04-09T06:17:00.000Z",
|
|
cashier_id: 7,
|
|
},
|
|
],
|
|
},
|
|
employees: [
|
|
{
|
|
id: 7,
|
|
display_name: "Jeppe",
|
|
},
|
|
],
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-customer-notes-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "AB12345",
|
|
reference: "NOTES-REF",
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
lastOrderId: null,
|
|
},
|
|
route: {
|
|
step: 1,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-customer-notes-trigger")).toBeVisible({ timeout: 10_000 });
|
|
|
|
const [registrationRowBox, notesTriggerBox] = await Promise.all([
|
|
page.getByTestId("pos-mobile-step-1-registration-row").boundingBox(),
|
|
page.getByTestId("pos-mobile-customer-notes-trigger").boundingBox(),
|
|
]);
|
|
|
|
expect(registrationRowBox).not.toBeNull();
|
|
expect(notesTriggerBox).not.toBeNull();
|
|
expect(Math.abs((registrationRowBox?.width ?? 0) - (notesTriggerBox?.width ?? 0))).toBeLessThanOrEqual(4);
|
|
expect(Math.abs((registrationRowBox?.x ?? 0) - (notesTriggerBox?.x ?? 0))).toBeLessThanOrEqual(4);
|
|
|
|
await page.getByTestId("pos-mobile-customer-notes-trigger").click();
|
|
|
|
const popup = page.locator('[data-testid="pos-mobile-popup"][data-popup-id="customer_notes"]');
|
|
await expect(popup).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-mobile-customer-note-created-by-401")).toContainText("Oprettet af Jeppe");
|
|
await expect(page.getByTestId("pos-mobile-customer-note-created-by-401")).not.toContainText("pos.created_by");
|
|
|
|
await popup.getByText("Tilføj note", { exact: true }).click();
|
|
await expect(page.locator("#input_field")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.locator("#input_field")).toHaveAttribute("placeholder", "Tilføj note...");
|
|
});
|
|
|
|
test("@smoke manual input happy path creates, completes, and resets", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await createOrderFromStep1(page, fixture, {
|
|
reg: "ZZ00000",
|
|
reference: "MOBILE-SMOKE-REF",
|
|
manualInput: true,
|
|
});
|
|
|
|
await selectPrimaryProduct(page, 53);
|
|
|
|
await waitForMobileNextStepCooldown(page);
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
|
|
await waitForStepReset(page);
|
|
});
|
|
|
|
test("required reference prompt retries and creates the order after confirmation", async ({ page }) => {
|
|
const fixture = createMobilePosFixture({
|
|
customerAttributesByNumber: {
|
|
[REGULAR_CUSTOMER_ID]: [
|
|
{
|
|
id: 9,
|
|
customer_number: REGULAR_CUSTOMER_ID,
|
|
attribute: "requiresReferenceNumber",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
await createOrderFromStep1(page, fixture, {
|
|
reg: "FREE123",
|
|
reference: "",
|
|
extraSeedState: {
|
|
reference: "",
|
|
},
|
|
});
|
|
|
|
await expect(page.locator(".swal2-popup")).toBeVisible({ timeout: 10_000 });
|
|
await page.locator(".swal2-input").fill("REQ-REF-001");
|
|
await page.locator(".swal2-confirm").click();
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
const orderId = Number(new URL(page.url()).searchParams.get("id"));
|
|
await expect.poll(() => fixture.ordersById[orderId]?.reference ?? "").toBe("REQ-REF-001");
|
|
expect(fixture.requestCounters.orderCreate).toBe(1);
|
|
});
|
|
|
|
test("invalid mobile POS route params fall back to step 1", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-invalid-route-token",
|
|
seedState: {
|
|
customerId: null,
|
|
reg: "FREE123",
|
|
reference: "INVALID-ROUTE-REF",
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
lastOrderId: null,
|
|
},
|
|
route: {
|
|
step: 1,
|
|
},
|
|
});
|
|
|
|
await page.goto(
|
|
`/admin/${fixture.departmentId ?? DEFAULT_DEPARTMENT_ID}/modules/pos?step=999&id=abc&customer_id=def`
|
|
);
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toHaveCount(0);
|
|
});
|
|
|
|
test("step 2 without an order id falls back to step 1", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-missing-order-id-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "FREE123",
|
|
reference: "MISSING-ORDER-ID-REF",
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
lastOrderId: null,
|
|
},
|
|
route: {
|
|
step: 1,
|
|
},
|
|
});
|
|
|
|
await page.goto(
|
|
`/admin/${fixture.departmentId ?? DEFAULT_DEPARTMENT_ID}/modules/pos?step=2&customer_id=${REGULAR_CUSTOMER_ID}`
|
|
);
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-mobile-next-step")).toContainText("Pleno Logistics", { timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toHaveCount(0);
|
|
});
|
|
|
|
test("step 1 attachments upload after order creation completes", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await createOrderFromStep1(page, fixture, {
|
|
reg: "ATT1234",
|
|
reference: "ATTACH-SUCCESS-REF",
|
|
extraSeedState: {
|
|
attachmentsBase64: [createAttachmentFile("damage-success.jpg")],
|
|
},
|
|
});
|
|
|
|
await selectPrimaryProduct(page, 53);
|
|
await waitForMobileNextStepCooldown(page);
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.attachmentUpload, { timeout: 10_000 }).toBe(1);
|
|
await expect.poll(() => fixture.requestLog.attachmentUploads.length, { timeout: 10_000 }).toBe(1);
|
|
await waitForStepReset(page);
|
|
});
|
|
|
|
test("stored pos_order_id restores step 2 without creating a duplicate order", async ({ page }) => {
|
|
const orderId = 9305;
|
|
const fixture = createMobilePosFixture({
|
|
ordersById: {
|
|
[orderId]: buildRegularOrder(orderId, {
|
|
reference: "RESTORE-REF",
|
|
reg_1: "AB12345",
|
|
}),
|
|
},
|
|
orderItemsByOrderId: {
|
|
[orderId]: [],
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-restore-token",
|
|
storedOrderId: orderId,
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "AB12345",
|
|
reference: "RESTORE-REF",
|
|
includePrimaryItem: false,
|
|
vehicleType: 53,
|
|
},
|
|
route: {
|
|
step: 1,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-next-step")).toContainText("Pleno Logistics", { timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
expect(fixture.requestCounters.orderCreate).toBe(0);
|
|
expect(fixture.requestCounters.orderGet).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("step 2 query bootstrap auto-loads the primary product and persists notes/reference", async ({ page }) => {
|
|
const orderId = 9401;
|
|
const fixture = createMobilePosFixture({
|
|
ordersById: {
|
|
[orderId]: buildRegularOrder(orderId, {
|
|
reference: "BOOTSTRAP-REF",
|
|
reg_1: "AB12345",
|
|
}),
|
|
},
|
|
orderItemsByOrderId: {
|
|
[orderId]: [],
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-step2-bootstrap-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "AB12345",
|
|
reference: "BOOTSTRAP-REF",
|
|
includePrimaryItem: false,
|
|
vehicleType: 53,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId,
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-mobile-last-order")).toBeVisible({ timeout: 10_000 });
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const snapshot = await getStoredPosSnapshot(page);
|
|
return snapshot?.transactionItems?.primaryItem?.id ?? null;
|
|
},
|
|
{ timeout: 10_000 }
|
|
)
|
|
.toBe(53);
|
|
|
|
await page.getByTestId("pos-mobile-notes-input").scrollIntoViewIfNeeded();
|
|
await expectAboveFixedActions(page, page.getByTestId("pos-mobile-notes-input"));
|
|
await page.getByTestId("pos-mobile-notes-input").fill("mobile-pos-notes");
|
|
await page.getByTestId("pos-mobile-notes-input").press("Tab");
|
|
|
|
await page.getByTestId("pos-mobile-reference-step-2-input").scrollIntoViewIfNeeded();
|
|
await expectAboveFixedActions(page, page.getByTestId("pos-mobile-reference-step-2-input"));
|
|
await page.getByTestId("pos-mobile-reference-step-2-input").fill("mobile-pos-reference");
|
|
await page.getByTestId("pos-mobile-reference-step-2-input").press("Tab");
|
|
|
|
await expect.poll(() => fixture.ordersById[orderId]?.notes ?? "").toBe("mobile-pos-notes");
|
|
await expect.poll(() => fixture.ordersById[orderId]?.reference ?? "").toBe("mobile-pos-reference");
|
|
});
|
|
|
|
test("manual step 2 selection supports addons and additional items", async ({ page }) => {
|
|
const orderId = 9402;
|
|
const fixture = createMobilePosFixture({
|
|
ordersById: {
|
|
[orderId]: buildRegularOrder(orderId, {
|
|
reference: "STEP2-MANUAL-REF",
|
|
reg_1: "ZZ00000",
|
|
}),
|
|
},
|
|
orderItemsByOrderId: {
|
|
[orderId]: [],
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-step2-manual-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "ZZ00000",
|
|
reference: "STEP2-MANUAL-REF",
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
lastOrderId: null,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId,
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await selectPrimaryProduct(page, 53);
|
|
await expect(page.getByTestId("pos-mobile-additional-items-header-title")).toBeVisible({ timeout: 10_000 });
|
|
await expectSameRow(
|
|
page.getByTestId("pos-mobile-additional-items-header-title"),
|
|
page.getByTestId("pos-mobile-additional-items-header-icon")
|
|
);
|
|
|
|
await expect(page.getByTestId("pos-mobile-addon-71")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-addon-71").click();
|
|
await expect(page.getByTestId("pos-mobile-addon-71-value")).toHaveText("1", { timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-addon-71-increase").click();
|
|
await expect(page.getByTestId("pos-mobile-addon-71-value")).toHaveText("2", { timeout: 10_000 });
|
|
|
|
await page.getByTestId("pos-mobile-additional-items-open").click();
|
|
await expect(page.getByTestId("pos-mobile-additional-items-selection")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-category-8").click();
|
|
await page.waitForTimeout(2_000);
|
|
await page.getByTestId("pos-mobile-product-91").click();
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const snapshot = await getStoredPosSnapshot(page);
|
|
return (snapshot?.transactionItems?.additionalItems || [])
|
|
.map((item) => Number(item?.id))
|
|
.sort((left, right) => left - right);
|
|
},
|
|
{ timeout: 10_000 }
|
|
)
|
|
.toEqual([91]);
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-mobile-addon-71-value")).toHaveText("2", { timeout: 10_000 });
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const snapshot = await getStoredPosSnapshot(page);
|
|
return (snapshot?.transactionItems?.additionalItems || []).map((item) => ({
|
|
id: Number(item?.id),
|
|
quantity: Number(item?.quantity ?? 0),
|
|
}));
|
|
},
|
|
{ timeout: 10_000 }
|
|
)
|
|
.toEqual([
|
|
{
|
|
id: 91,
|
|
quantity: 1,
|
|
},
|
|
]);
|
|
await page.waitForTimeout(500);
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(3);
|
|
await expect.poll(() => (fixture.orderItemsByOrderId[orderId] || []).length, { timeout: 10_000 }).toBe(3);
|
|
const createdProductIds = fixture.requestLog.orderItemCreates.map((entry) => Number(entry.product_id));
|
|
expect(createdProductIds).toEqual([53, 71, 91]);
|
|
});
|
|
|
|
test("clear all deletes the order and returns to the scanner", async ({ page }) => {
|
|
const orderId = 9403;
|
|
const fixture = createMobilePosFixture({
|
|
ordersById: {
|
|
[orderId]: buildRegularOrder(orderId),
|
|
},
|
|
orderItemsByOrderId: {
|
|
[orderId]: [],
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-clear-all-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "AB12345",
|
|
reference: "CLEAR-ALL-REF",
|
|
includePrimaryItem: true,
|
|
primaryItemId: 53,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId,
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-clear-all-button")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-clear-all-button").click({ force: true });
|
|
|
|
await expect.poll(() => fixture.deletedOrderIds.includes(orderId), { timeout: 10_000 }).toBe(true);
|
|
await waitForStepReset(page);
|
|
});
|
|
|
|
test("step 2 sync is idempotent when the order already matches the local transaction", async ({ page }) => {
|
|
const orderId = 9404;
|
|
const fixture = createMobilePosFixture({
|
|
ordersById: {
|
|
[orderId]: buildRegularOrder(orderId),
|
|
},
|
|
orderItemsByOrderId: {
|
|
[orderId]: [
|
|
{
|
|
id: 9901,
|
|
order_id: orderId,
|
|
product_id: 53,
|
|
product: fixtureProduct(53),
|
|
quantity: 1,
|
|
notes: "",
|
|
reference: "",
|
|
related_item_id: null,
|
|
price: 599,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-idempotent-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "AB12345",
|
|
reference: "IDEMPOTENT-REF",
|
|
includePrimaryItem: true,
|
|
primaryItemId: 53,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId,
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 2_500 }).toBe(0);
|
|
await expect.poll(() => fixture.requestCounters.orderItemsDelete, { timeout: 2_500 }).toBe(0);
|
|
await waitForStepReset(page);
|
|
});
|
|
|
|
test("booking hydration applies booking items, reference, notes, and po", async ({ page }) => {
|
|
const orderId = 9405;
|
|
const fixture = createMobilePosFixture({
|
|
ordersById: {
|
|
[orderId]: buildRegularOrder(orderId, {
|
|
reg_1: "BOOK123",
|
|
reference: "",
|
|
}),
|
|
},
|
|
orderItemsByOrderId: {
|
|
[orderId]: [],
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-booking-hydration-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "BOOK123",
|
|
reference: "",
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
bookingId: DEFAULT_BOOKING_ID,
|
|
vehicleStatus: "booked",
|
|
lastOrderId: null,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId,
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await waitForBookingHydration(page, {
|
|
primaryId: 53,
|
|
addonProductIds: [71],
|
|
});
|
|
|
|
await expect.poll(() => fixture.ordersById[orderId]?.reference ?? "").toBe("BOOKING-REF-8101");
|
|
await expect.poll(() => fixture.ordersById[orderId]?.notes ?? "").toBe("Booking notes from planner");
|
|
await expect.poll(() => fixture.ordersById[orderId]?.po ?? "").toBe("PO-8101");
|
|
});
|
|
|
|
test("non-wash booking falls back to the first wash product", async ({ page }) => {
|
|
const orderId = 9406;
|
|
const fixture = createMobilePosFixture({
|
|
ordersById: {
|
|
[orderId]: buildRegularOrder(orderId, {
|
|
reg_1: "NOWASH1",
|
|
reference: "",
|
|
}),
|
|
},
|
|
orderItemsByOrderId: {
|
|
[orderId]: [],
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-booking-fallback-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "NOWASH1",
|
|
reference: "",
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
bookingId: 8102,
|
|
vehicleStatus: "booked",
|
|
lastOrderId: null,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId,
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const snapshot = await getStoredPosSnapshot(page);
|
|
return snapshot?.transactionItems?.primaryItem?.id ?? null;
|
|
},
|
|
{ timeout: 10_000 }
|
|
)
|
|
.toBe(53);
|
|
});
|
|
|
|
test("booking completion without safety seal completes and resets", async ({ page }) => {
|
|
const orderId = 9407;
|
|
const fixture = createMobilePosFixture({
|
|
ordersById: {
|
|
[orderId]: buildRegularOrder(orderId, {
|
|
reg_1: "BOOK123",
|
|
reference: "",
|
|
}),
|
|
},
|
|
orderItemsByOrderId: {
|
|
[orderId]: [],
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-booking-complete-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "BOOK123",
|
|
reference: "",
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
bookingId: DEFAULT_BOOKING_ID,
|
|
vehicleStatus: "booked",
|
|
lastOrderId: null,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId,
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await waitForBookingHydration(page, {
|
|
primaryId: 53,
|
|
addonProductIds: [71],
|
|
});
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.bookingSetOrderId, { timeout: 10_000 }).toBe(1);
|
|
await expect.poll(() => fixture.requestCounters.bookingComplete, { timeout: 10_000 }).toBe(1);
|
|
await expect.poll(() => fixture.bookingsById[DEFAULT_BOOKING_ID]?.status ?? "").toBe("completed");
|
|
await waitForStepReset(page);
|
|
});
|
|
|
|
test("booking completion prompts when a safety seal is added to the basket", async ({ page }) => {
|
|
const orderId = 9409;
|
|
const fixture = createMobilePosFixture({
|
|
ordersById: {
|
|
[orderId]: buildRegularOrder(orderId, {
|
|
reg_1: "BOOK123",
|
|
reference: "",
|
|
}),
|
|
},
|
|
orderItemsByOrderId: {
|
|
[orderId]: [],
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-booking-basket-seal-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "BOOK123",
|
|
reference: "",
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
bookingId: DEFAULT_BOOKING_ID,
|
|
vehicleStatus: "booked",
|
|
lastOrderId: null,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId,
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await waitForBookingHydration(page, {
|
|
primaryId: 53,
|
|
addonProductIds: [71],
|
|
});
|
|
|
|
await page.getByTestId("pos-mobile-additional-items-open").click();
|
|
await expect(page.getByTestId("pos-mobile-additional-items-selection")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-category-8").click();
|
|
await page.waitForTimeout(2_000);
|
|
await page.getByTestId("pos-mobile-product-41").click();
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const snapshot = await getStoredPosSnapshot(page);
|
|
return (snapshot?.transactionItems?.additionalItems || [])
|
|
.map((item) => Number(item?.id))
|
|
.sort((left, right) => left - right);
|
|
},
|
|
{ timeout: 10_000 }
|
|
)
|
|
.toEqual([41]);
|
|
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await page.waitForTimeout(500);
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
|
|
await expect(page.getByTestId("pos-mobile-popup")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-booking-safety-seal-input").fill("9090");
|
|
await page.getByTestId("pos-mobile-booking-complete-with-certificate").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.bookingComplete, { timeout: 10_000 }).toBe(1);
|
|
await expect.poll(() => fixture.bookingsById[DEFAULT_BOOKING_ID]?.status ?? "").toBe("completed");
|
|
await expect.poll(() => String(fixture.bookingsById[DEFAULT_BOOKING_ID]?.safety_seal ?? "")).toBe("9090");
|
|
await waitForStepReset(page);
|
|
});
|
|
|
|
test("booking completion with safety seal popup submits the explicit seal", async ({ page }) => {
|
|
const orderId = 9408;
|
|
const fixture = createMobilePosFixture({
|
|
ordersById: {
|
|
[orderId]: buildRegularOrder(orderId, {
|
|
reg_1: "SEAL123",
|
|
reference: "",
|
|
}),
|
|
},
|
|
orderItemsByOrderId: {
|
|
[orderId]: [],
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-booking-seal-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "SEAL123",
|
|
reference: "",
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
bookingId: 8103,
|
|
vehicleStatus: "booked",
|
|
lastOrderId: null,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId,
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await waitForBookingHydration(page, {
|
|
primaryId: 53,
|
|
addonProductIds: [41],
|
|
});
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
|
|
await expect(page.getByTestId("pos-mobile-popup")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-booking-safety-seal-input").fill("4242");
|
|
await page.getByTestId("pos-mobile-booking-complete-with-certificate").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.bookingComplete, { timeout: 10_000 }).toBe(1);
|
|
await expect.poll(() => fixture.bookingsById[8103]?.status ?? "").toBe("completed");
|
|
await expect.poll(() => String(fixture.bookingsById[8103]?.safety_seal ?? "")).toBe("4242");
|
|
await waitForStepReset(page);
|
|
});
|
|
|
|
test("booking completion failure keeps the mobile booking popup open for retry", async ({ page }) => {
|
|
const orderId = 9410;
|
|
const fixture = createMobilePosFixture({
|
|
failureBudget: {
|
|
bookingComplete: 1,
|
|
},
|
|
ordersById: {
|
|
[orderId]: buildRegularOrder(orderId, {
|
|
reg_1: "SEAL123",
|
|
reference: "",
|
|
}),
|
|
},
|
|
orderItemsByOrderId: {
|
|
[orderId]: [],
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-booking-failure-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "SEAL123",
|
|
reference: "",
|
|
includePrimaryItem: false,
|
|
vehicleType: null,
|
|
bookingId: 8103,
|
|
vehicleStatus: "booked",
|
|
lastOrderId: null,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId,
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await waitForBookingHydration(page, {
|
|
primaryId: 53,
|
|
addonProductIds: [41],
|
|
});
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
|
|
await expect(page.getByTestId("pos-mobile-popup")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-booking-safety-seal-input").fill("1111");
|
|
await page.getByTestId("pos-mobile-booking-complete-with-certificate").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.bookingComplete, { timeout: 10_000 }).toBe(1);
|
|
await expect(page.getByTestId("pos-mobile-popup")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-mobile-step-1-shell")).toHaveCount(0);
|
|
await expect.poll(() => fixture.bookingsById[8103]?.status ?? "").toBe("pending");
|
|
});
|
|
|
|
test("order creation failure leaves the flow on step 1", async ({ page }) => {
|
|
const fixture = createMobilePosFixture({
|
|
failureBudget: {
|
|
orderCreate: 1,
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-order-create-failure-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "AB12345",
|
|
reference: "FAIL-CREATE-REF",
|
|
includePrimaryItem: false,
|
|
vehicleType: 53,
|
|
},
|
|
route: {
|
|
step: 1,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-next-step")).toContainText("Pleno Logistics", { timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.orderCreate, { timeout: 10_000 }).toBe(1);
|
|
await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toHaveCount(0);
|
|
});
|
|
|
|
test("attachment upload failure is recorded but does not block completion", async ({ page }) => {
|
|
const orderId = 9409;
|
|
const fixture = createMobilePosFixture({
|
|
failureBudget: {
|
|
attachmentUpload: 1,
|
|
},
|
|
ordersById: {
|
|
[orderId]: buildRegularOrder(orderId),
|
|
},
|
|
orderItemsByOrderId: {
|
|
[orderId]: [],
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-attachment-failure-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
reg: "AB12345",
|
|
reference: "ATTACHMENT-REF",
|
|
includePrimaryItem: true,
|
|
primaryItemId: 53,
|
|
attachmentsBase64: [createAttachmentFile("damage.jpg")],
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId,
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect.poll(() => fixture.requestCounters.attachmentUpload, { timeout: 10_000 }).toBe(1);
|
|
await waitForStepReset(page);
|
|
});
|
|
});
|
|
|
|
function fixtureProduct(productId) {
|
|
const fixture = createMobilePosFixture();
|
|
return fixture.products.find((product) => product.id === productId);
|
|
}
|