Add unit tests for Orders navigation count refresh and fallback customer caching, update POS mobile accessibility labels:

- Introduced `orders-draft-count-refresh.spec.js` for testing `Orders` navigation hooks on customer changes and order deletions.
- Enhanced `use-draft-transaction-customer.spec.js` with tests for clearing cached fallback customers.
- Updated POS mobile components with accessibility labels and localization for image viewer and customer suggestion buttons.
- Added support for `vehicleCustomerSuggestionsGet` handling in e2e mocks and tests.
This commit is contained in:
Jeppe Bundgaard
2026-04-21 15:20:50 +02:00
parent 44f77eadc1
commit 817c174e0e
19 changed files with 575 additions and 50 deletions
+271 -8
View File
@@ -116,6 +116,12 @@ function buildTodayTimestamp(time = "08:00:00.000Z") {
return `${todayIsoDate}T${time}`;
}
async function forceLocale(page, locale = "en") {
await page.addInitScript((value) => {
window.localStorage.setItem("locale", value);
}, locale);
}
function createMultiBookingFixture({ reg, vehicle = {}, bookings = [] }) {
const baseFixture = createMobilePosFixture();
return createMobilePosFixture({
@@ -436,7 +442,7 @@ test("mobile customer popup exposes the draft quick action and selects the confi
seedState: {
customerId: null,
includePrimaryItem: false,
reg: "AB12345",
reg: "FREE123",
reference: "MOBILE-DRAFT",
},
route: {
@@ -469,6 +475,94 @@ test("mobile customer popup exposes the draft quick action and selects the confi
await expect(page.getByTestId("pos-mobile-customer-name")).toContainText(MOBILE_DRAFT_TRANSACTION_CUSTOMER_NAME);
});
test("mobile customer popup applies a previous-customer suggestion", async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "chromium-mobile", "Mobile POS order suite is scoped to chromium-mobile.");
const suggestedCustomerId = 55667788;
const suggestedCustomerName = "Suggestion Logistics";
const secondarySuggestedCustomerId = 66778899;
const fixture = createMobilePosFixture({
customersByNumber: {
[suggestedCustomerId]: {
id: suggestedCustomerId,
customerNumber: suggestedCustomerId,
name: suggestedCustomerName,
address: "Suggestion Street 8",
zip: "2630",
city: "Taastrup",
mobilePhone: "55667788",
email: "suggestion@example.com",
corporateIdentificationNumber: "55667788",
barred: false,
economic_customer: suggestedCustomerId,
},
[secondarySuggestedCustomerId]: {
id: secondarySuggestedCustomerId,
customerNumber: secondarySuggestedCustomerId,
name: "Fallback Suggestion",
address: "Fallback Street 9",
zip: "2630",
city: "Taastrup",
mobilePhone: "66778899",
email: "fallback@example.com",
corporateIdentificationNumber: "66778899",
barred: false,
economic_customer: secondarySuggestedCustomerId,
},
},
vehicleCustomerSuggestionsByReg: {
FREE123: [
{
id: 901,
customer_number: suggestedCustomerId,
customer_name: suggestedCustomerName,
barred: false,
},
{
id: 902,
customer_number: secondarySuggestedCustomerId,
customer_name: "Fallback Suggestion",
barred: false,
},
],
},
});
await setupMobilePosPage(page, fixture, {
token: "pos-mobile-previous-customer-suggestion-token",
seedState: {
customerId: null,
includePrimaryItem: false,
reg: "FREE123",
reference: "MOBILE-SUGGESTION",
lastOrderId: null,
},
route: {
step: 1,
},
});
await openCustomerPopupFromStep1(page);
await expect(page.getByTestId("pos-customer-suggestions")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId(`pos-customer-suggestion-select-${suggestedCustomerId}`)).toBeVisible({
timeout: 10_000,
});
await page.getByTestId(`pos-customer-suggestion-select-${suggestedCustomerId}`).click();
await expect(page.getByTestId("pos-mobile-customer-popup")).toBeHidden({ timeout: 10_000 });
await expect
.poll(async () => {
const snapshot = await getStoredPosSnapshot(page);
return snapshot?.metadata?.customerId ?? null;
})
.toBe(suggestedCustomerId);
await waitForMobileNextStepCooldown(page);
await page.getByTestId("pos-mobile-next-step").click();
await waitForMobileStepTwoReady(page);
await expect(page.getByTestId("pos-mobile-customer-name")).toContainText(suggestedCustomerName);
});
test("mobile customer popup defaults to customer invoice mode and keeps customer search visible", async ({
page,
}, testInfo) => {
@@ -998,6 +1092,74 @@ test.describe("POS mobile order flow", () => {
await waitForStepReset(page);
});
test("stored step 1 vehicles without status normalize to unknown without Vue prop warnings", async ({ page }) => {
const fixture = createMobilePosFixture();
const consoleProblems = [];
const pageErrors = [];
page.on("console", (message) => {
if (message.type() === "error" || message.type() === "warning" || message.text().includes('prop "status"')) {
consoleProblems.push(`${message.type()}: ${message.text()}`);
}
});
page.on("pageerror", (error) => {
pageErrors.push(error.stack || error.message);
});
await setupMobilePosPage(page, fixture, {
token: "stored-step1-missing-status-token",
seedState: false,
route: {
step: 1,
},
});
await page.evaluate(() => {
const storedValue = window.localStorage.getItem("pos");
const snapshot = storedValue ? JSON.parse(storedValue) : {};
snapshot.vehicles = snapshot.vehicles || {};
snapshot.vehicles.vehicle_1 = {
reg: "EC2123",
customer_id: null,
type: null,
booking_id: null,
booking_matches: [],
wash_subscription: false,
barred: false,
reference: "",
last_order_id: null,
};
snapshot.vehicles.vehicle_2 = null;
snapshot.vehicles.vehicle_3 = null;
snapshot.vehicles.activeVehicleIndex = 1;
window.localStorage.setItem("pos", JSON.stringify(snapshot));
});
await page.reload();
await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 });
await expect
.poll(
async () => {
const snapshot = await getStoredPosSnapshot(page);
return {
reg: snapshot?.vehicles?.vehicle_1?.reg ?? null,
status: snapshot?.vehicles?.vehicle_1?.status ?? null,
};
},
{ timeout: 10_000 }
)
.toEqual({
reg: "EC2123",
status: "unknown",
});
const combinedProblems = [...consoleProblems, ...pageErrors].join("\n");
expect(combinedProblems).not.toContain('Invalid prop: type check failed for prop "status"');
expect(combinedProblems).not.toContain('Expected String with value "undefined"');
});
test("matched vehicle manual input seeds the step 2 reference and primary product defaults", async ({ page }) => {
const fixture = createMobilePosFixture();
await createOrderFromStep1(page, fixture, {
@@ -2292,6 +2454,8 @@ test.describe("POS mobile order flow", () => {
},
});
await forceLocale(page, "en");
await setupMobilePosPage(page, fixture, {
token: "mobile-action-metadata-token",
seedState: {
@@ -2317,7 +2481,7 @@ test.describe("POS mobile order flow", () => {
await expect(clearAllButton).toHaveAttribute("data-action-key", "pos-mobile-clear-all");
await expect(completeButton).toHaveAttribute("data-copy-key", "complete");
await expect(clearAllButton).toHaveAttribute("data-copy-key", "clear_all");
await expect(completeButton).toContainText(/Afslut|Fuldf/);
await expect(completeButton).toContainText(/Complete|Afslut|Fuldf/);
await expectCleanActionText(completeButton);
await expectCleanActionText(clearAllButton);
@@ -2329,21 +2493,63 @@ test.describe("POS mobile order flow", () => {
const attachmentsToggle = page.getByTestId("pos-mobile-attachments-toggle");
await expect(attachmentsToggle).toBeVisible({ timeout: 10_000 });
await expect(attachmentsToggle).toHaveAttribute("data-action-key", "pos-mobile-attachments-toggle");
await expect(attachmentsToggle).toContainText("Attachments");
await expectCleanActionText(attachmentsToggle);
await attachmentsToggle.click();
for (const actionKey of [
"pos-mobile-attachment-view-take-picture",
"pos-mobile-attachment-view-close",
"pos-mobile-attachments-upload-file",
"pos-mobile-attachments-wash-certificate",
]) {
const expectedLabelsByActionKey = {
"pos-mobile-attachment-view-take-picture": "Take picture",
"pos-mobile-attachment-view-close": "Close",
"pos-mobile-attachments-upload-file": "Upload",
"pos-mobile-attachments-wash-certificate": "Wash certificate",
};
for (const [actionKey, label] of Object.entries(expectedLabelsByActionKey)) {
const action = getByActionKey(page, actionKey);
await expect(action).toBeVisible({ timeout: 10_000 });
await expect(action).toHaveAttribute("data-action-key", actionKey);
await expect(action).toContainText(label);
await expectCleanActionText(action);
}
await expect(page.locator("body")).not.toContainText("admin.pos.wash_certificate");
});
test("mobile image viewer uses localized controls", async ({ page }) => {
const imageAttachment = {
filename: "damage.svg",
base64String:
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSJyZWQiLz48L3N2Zz4=",
};
await forceLocale(page, "en");
await setupMobilePosPage(page, createMobilePosFixture(), {
token: "mobile-image-viewer-locale-token",
seedState: {
customerId: REGULAR_CUSTOMER_ID,
reg: "AB12345",
includePrimaryItem: false,
attachmentsBase64: [imageAttachment],
lastOrderId: null,
},
route: {
step: 1,
},
});
await expect(page.getByTestId("pos-mobile-step-1")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-mobile-attachments-toggle").click();
const thumbnail = page.getByAltText("damage.svg");
await expect(thumbnail).toBeVisible({ timeout: 10_000 });
await thumbnail.click();
await expect(page.getByTestId("pos-mobile-image-viewer-zoom-in")).toContainText("Zoom", { timeout: 10_000 });
await expect(page.getByTestId("pos-mobile-image-viewer-zoom-out")).toContainText("Zoom out");
await expect(page.getByTestId("pos-mobile-image-viewer-reset")).toContainText("Reset");
await expect(page.getByTestId("pos-mobile-image-viewer-close")).toContainText("Close");
});
test("step 2 sync is idempotent when the order already matches the local transaction", async ({ page }) => {
@@ -2649,6 +2855,63 @@ test.describe("POS mobile order flow", () => {
await waitForStepReset(page);
});
test("booking completion popup reuses the step 2 safety seal without asking for it again", async ({ page }) => {
const orderId = 9412;
const fixture = createMobilePosFixture({
ordersById: {
[orderId]: buildRegularOrder(orderId, {
reg_1: "SEAL321",
reference: "",
}),
},
orderItemsByOrderId: {
[orderId]: [],
},
});
await setupMobilePosPage(page, fixture, {
token: "mobile-booking-prefilled-seal-token",
seedState: {
customerId: REGULAR_CUSTOMER_ID,
reg: "SEAL321",
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],
});
const safetySealInput = page.getByTestId("pos-mobile-safety-seal-step-2-input");
await expect(safetySealInput).toBeVisible({ timeout: 10_000 });
await safetySealInput.fill("5150");
await page.getByTestId("pos-mobile-next-step").click();
const popupSafetySealInput = page.getByTestId("pos-mobile-booking-safety-seal-input");
await expect(page.getByTestId("pos-mobile-popup")).toBeVisible({ timeout: 10_000 });
await expect(popupSafetySealInput).toHaveValue("5150");
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("5150");
await waitForStepReset(page);
});
test("booking completion failure keeps the mobile booking popup open for retry", async ({ page }) => {
const orderId = 9410;
const fixture = createMobilePosFixture({