Add utilities and tests for mobile order management and Subuser navigation:

- Introduced `mobileOrderCompletion.js` for mobile order metadata handling with safety seal syncing, metadata persistence, and booking linking utilities.
- Added tests for Subuser navigation menu (`navigation-menu-items-user-subusers.spec.js`) to validate links and permission gating.
- Created unit and end-to-end tests for Subuser grant permissions and management, including permission nodes and invites (`subuser-grant-permission-nodes.spec.js`, `subuser-management.spec.ts`).
- Implemented `SubuserCompleteRegistrationPage.vue` with validation, token handling, and registration flow for Subuser setup.
- Enhanced Playwright e2e tests for Subuser features (`subuserCompleteRegistration.spec.ts`).
This commit is contained in:
Jeppe Bundgaard
2026-04-14 10:09:10 +02:00
parent a93b0dc954
commit c14f0cc1c4
46 changed files with 4258 additions and 674 deletions
+329 -35
View File
@@ -279,6 +279,24 @@ async function mockPosApi(page, fixture) {
}
}
if (filters.includes("reg_1:")) {
const reg1Filter = String(filters.split("reg_1:")[1]?.split(",")[0] || "").toUpperCase();
if (reg1Filter) {
bookings = bookings.filter((booking) => String(booking.reg_1 || "").toUpperCase() === reg1Filter);
}
}
if (filters.includes("reg_2:")) {
const reg2Filter = String(filters.split("reg_2:")[1]?.split(",")[0] || "").toUpperCase();
if (reg2Filter) {
bookings = bookings.filter((booking) => String(booking.reg_2 || "").toUpperCase() === reg2Filter);
}
}
if (filters.includes("order_id:null") || filters.includes("order_id:is null")) {
bookings = bookings.filter((booking) => String(booking.status || "pending").toLowerCase() !== "completed");
}
if (bookingId > 0) {
await route.fulfill(
json({
@@ -345,6 +363,9 @@ async function mockPosApi(page, fixture) {
status: "completed",
};
fixture.completedBookingIds.push(bookingId);
if (typeof fixture.onCompleteOrderBooking === "function") {
fixture.onCompleteOrderBooking(bookingId, fixture.orderBookings[bookingIndex], fixture);
}
}
await route.fulfill(
@@ -507,7 +528,9 @@ async function mockPosApi(page, fixture) {
}
if (createdTo) {
duplicateOrders = duplicateOrders.filter((order) => String(order?.created_at || "") <= `${createdTo}T23:59:59.999Z`);
duplicateOrders = duplicateOrders.filter(
(order) => String(order?.created_at || "") <= `${createdTo}T23:59:59.999Z`
);
}
await route.fulfill(
@@ -833,6 +856,25 @@ async function commitDesktopReg1ByBlur(page, value) {
});
}
async function openDesktopOrderBookingSettings(modal, bookingId) {
const settings = modal.getByTestId(`pos-desktop-order-booking-settings-${bookingId}`);
await expect(settings).toBeVisible({ timeout: 10_000 });
await settings.locator(".dropdown-trigger button").click();
const dropdown = settings.locator(".dropdown-content");
await expect(dropdown).toBeVisible({ timeout: 10_000 });
return dropdown;
}
async function completeDesktopOrderBookingFromSettings(page, modal, bookingId) {
const dropdown = await openDesktopOrderBookingSettings(modal, bookingId);
await dropdown.locator("button.dropdown-item-action").first().click();
const popup = page.locator(".swal2-popup");
await expect(popup).toBeVisible({ timeout: 10_000 });
await popup.locator(".swal2-deny").click();
await expect(popup).toBeHidden({ timeout: 10_000 });
}
test.describe("POS flow", () => {
test.beforeEach(async ({ page }) => {
await suppressVueDevtoolsOverlay(page);
@@ -1150,11 +1192,7 @@ test.describe("POS flow", () => {
await page.locator("#reg_1").fill("EC212");
await expect(page.getByTestId("desktop-booked-date-7102")).toContainText("20.03.2026", { timeout: 10_000 });
await page
.locator(".dropdown-item")
.filter({ hasText: "EC21234 - Pleno Logistics" })
.first()
.click();
await page.locator(".dropdown-item").filter({ hasText: "EC21234 - Pleno Logistics" }).first().click();
await expect(activeBookingSelector).toBeVisible({ timeout: 10_000 });
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-9102")).toBeVisible({
@@ -1349,14 +1387,50 @@ test.describe("POS flow", () => {
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8103")).toBeVisible({
timeout: 10_000,
});
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-settings-8102")).toBeVisible({
timeout: 10_000,
});
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-services-8102")).toContainText(
"Tankvogn",
{ timeout: 10_000 }
);
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-services-8102")).toContainText(
"Dolly",
{ timeout: 10_000 }
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-services-8102")).toContainText("Dolly", {
timeout: 10_000,
});
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-service-row-8102-0")).toContainText(
"1x",
{
timeout: 10_000,
}
);
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-service-row-8102-0")).toContainText(
"Tankvogn",
{
timeout: 10_000,
}
);
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-service-amount-8102-0")).toContainText(
"599",
{
timeout: 10_000,
}
);
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-service-amount-8102-1")).toContainText(
"275",
{
timeout: 10_000,
}
);
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-service-total-8102")).toContainText(
"874",
{
timeout: 10_000,
}
);
const bookingSettingsDropdown = await openDesktopOrderBookingSettings(activeBookingSelector, 8102);
await expect(bookingSettingsDropdown.locator("button.dropdown-item-action")).toHaveCount(4);
await page.keyboard.press("Escape");
await expect(bookingSettingsDropdown).toBeHidden({ timeout: 10_000 });
await activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8102").click();
await expect(page.locator("#reg_1")).toHaveValue("TRACTORA");
@@ -1388,6 +1462,244 @@ test.describe("POS flow", () => {
expect(fixture.completedBookingIds).not.toContain(8103);
});
test("desktop booking selector prioritizes and highlights bookings scheduled for today", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."
);
const fixture = createPosFixture();
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
fixture.vehicles = [
{
...fixture.vehicles[0],
reg: "TODAYFIRST",
reference: "REF-TODAYFIRST",
},
];
fixture.orderBookings = [
buildOrderBooking(8151, {
reg_1: "TRACTORTOMORROW",
reg_2: "TODAYFIRST",
datetime: tomorrow.toISOString(),
reference: "TOMORROW-BOOKING",
reference_number: "TOMORROW-BOOKING",
}),
buildOrderBooking(8150, {
reg_1: "TRACTORTODAY",
reg_2: "TODAYFIRST",
datetime: buildTodayTimestamp("08:00:00.000Z"),
reference: "TODAY-BOOKING",
reference_number: "TODAY-BOOKING",
}),
];
await setupDesktopPosPage(page, fixture, { token: "pos-desktop-booking-today-priority" });
const activeBookingSelector = getActiveDesktopModal(page);
await commitDesktopReg1ByBlur(page, "TODAYFIRST");
await expect(activeBookingSelector).toBeVisible({ timeout: 10_000 });
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8150")).toBeVisible({
timeout: 10_000,
});
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8151")).toBeVisible({
timeout: 10_000,
});
const orderedBookingCards = await activeBookingSelector
.locator('[data-testid^="pos-desktop-order-booking-option-"]')
.evaluateAll((elements) => elements.map((element) => element.getAttribute("data-testid")));
expect(orderedBookingCards[0]).toBe("pos-desktop-order-booking-option-8150");
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-today-8150")).toBeVisible({
timeout: 10_000,
});
const isTodayHighlighted = await activeBookingSelector
.getByTestId("pos-desktop-order-booking-option-8150")
.evaluate((element) => element.classList.contains("pos-desktop-order-booking-card--today"));
expect(isTodayHighlighted).toBe(true);
});
test("desktop booking selector refreshes after completing a booking from the action wheel and auto-resolves the last remaining match", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."
);
const fixture = createPosFixture();
fixture.vehicles = [
{
...fixture.vehicles[0],
reg: "WHEELREFRESH",
reference: "REF-WHEELREFRESH",
},
];
fixture.orderBookings = [
buildOrderBooking(8140, {
reg_1: "TRACTORX",
reg_2: "WHEELREFRESH",
datetime: "2026-01-01T07:00:00.000Z",
reference: "WHEEL-REFRESH-A",
reference_number: "WHEEL-REFRESH-A",
}),
buildOrderBooking(8141, {
reg_1: "TRACTORY",
reg_2: "WHEELREFRESH",
datetime: "2026-01-01T09:00:00.000Z",
reference: "WHEEL-REFRESH-B",
reference_number: "WHEEL-REFRESH-B",
}),
];
await setupDesktopPosPage(page, fixture, { token: "pos-desktop-booking-wheel-refresh" });
const activeBookingSelector = getActiveDesktopModal(page);
await commitDesktopReg1ByBlur(page, "WHEELREFRESH");
await expect(activeBookingSelector).toBeVisible({ timeout: 10_000 });
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8140")).toBeVisible({
timeout: 10_000,
});
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8141")).toBeVisible({
timeout: 10_000,
});
await completeDesktopOrderBookingFromSettings(page, activeBookingSelector, 8140);
await expect(page.locator('[data-testid="pos-desktop-order-booking-modal"].is-active')).toHaveCount(0, {
timeout: 10_000,
});
await expect(page.locator("#reg_1")).toHaveValue("TRACTORY");
await expect(page.locator("#reg_2")).toHaveValue("WHEELREFRESH");
await expect(page.locator("#reference")).toHaveValue("WHEEL-REFRESH-B");
await expect.poll(() => fixture.completedBookingIds, { timeout: 10_000 }).toContain(8140);
expect(fixture.completedBookingIds).not.toContain(8141);
});
test("desktop booking selector closes when a wheel action refresh removes all remaining matches", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."
);
const fixture = createPosFixture();
fixture.vehicles = [
{
...fixture.vehicles[0],
reg: "WHEELZERO",
reference: "REF-WHEELZERO",
},
];
fixture.orderBookings = [
buildOrderBooking(8142, {
reg_1: "TRACTORZEROA",
reg_2: "WHEELZERO",
datetime: "2026-01-01T07:00:00.000Z",
reference: "WHEEL-ZERO-A",
reference_number: "WHEEL-ZERO-A",
}),
buildOrderBooking(8143, {
reg_1: "TRACTORZEROB",
reg_2: "WHEELZERO",
datetime: "2026-01-01T09:00:00.000Z",
reference: "WHEEL-ZERO-B",
reference_number: "WHEEL-ZERO-B",
}),
];
fixture.onCompleteOrderBooking = (_bookingId, completedBooking, activeFixture) => {
const matchingPlate = String(completedBooking?.reg_2 || completedBooking?.reg_1 || "").toUpperCase();
activeFixture.orderBookings = activeFixture.orderBookings.map((booking) => {
const bookingPlate = String(booking?.reg_2 || booking?.reg_1 || "").toUpperCase();
if (bookingPlate !== matchingPlate) {
return booking;
}
return {
...booking,
status: "completed",
};
});
};
await setupDesktopPosPage(page, fixture, { token: "pos-desktop-booking-wheel-zero" });
const activeBookingSelector = getActiveDesktopModal(page);
await commitDesktopReg1ByBlur(page, "WHEELZERO");
await expect(activeBookingSelector).toBeVisible({ timeout: 10_000 });
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8142")).toBeVisible({
timeout: 10_000,
});
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8143")).toBeVisible({
timeout: 10_000,
});
await completeDesktopOrderBookingFromSettings(page, activeBookingSelector, 8142);
await expect(page.locator('[data-testid="pos-desktop-order-booking-modal"].is-active')).toHaveCount(0, {
timeout: 10_000,
});
await expect(page.locator("#reg_1")).toHaveValue("WHEELZERO");
await expect(page.locator("#reference")).toHaveValue("REF-WHEELZERO");
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: 10_000 });
await expect.poll(() => fixture.ordersById[9300]?.booking_id ?? null, { timeout: 10_000 }).toBe(null);
});
test("desktop does not reopen the booking selector after refreshing on a later POS step", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS reload behavior is validated on chromium-desktop."
);
const fixture = createPosFixture();
fixture.ordersById[9201] = {
...fixture.ordersById[9201],
reg_1: "STEPTHREE",
reference: "STEP-THREE-REF",
};
fixture.orderItemsByOrderId[9201] = [
buildOrderItem(fixture.products[0], { order_id: 9201, product_id: 53, quantity: 1 }, 9801),
];
fixture.orderBookings = [
buildOrderBooking(8160, {
reg_1: "STEPTHREE",
datetime: "2026-04-14T07:00:00.000Z",
reference: "STEP-THREE-A",
reference_number: "STEP-THREE-A",
}),
buildOrderBooking(8161, {
reg_1: "STEPTHREE",
datetime: "2026-04-14T09:00:00.000Z",
reference: "STEP-THREE-B",
reference_number: "STEP-THREE-B",
}),
];
await setupDesktopPosPage(page, fixture, { token: "pos-desktop-step-3-refresh" });
await page.goto("/admin/1/modules/pos?id=9201&customer_id=12345&step=3");
await expect(page.getByTestId("pos-step-3")).toBeVisible({ timeout: 10_000 });
await expect(page).toHaveURL(/id=9201&customer_id=12345&step=3/);
await expect(getActiveDesktopModal(page)).toHaveCount(0);
await page.reload();
await expect(page.getByTestId("pos-step-3")).toBeVisible({ timeout: 10_000 });
await expect(page).toHaveURL(/id=9201&customer_id=12345&step=3/);
await expect(page.getByTestId("pos-step-3")).toContainText("STEP-THREE-REF");
await expect(getActiveDesktopModal(page)).toHaveCount(0);
});
test("desktop shows booking selection before duplicate warning when multiple bookings need selection", async ({
page,
}, testInfo) => {
@@ -1545,9 +1857,7 @@ test.describe("POS flow", () => {
});
});
test("desktop duplicate warning cancel keeps the current step-1 state", async ({
page,
}, testInfo) => {
test("desktop duplicate warning cancel keeps the current step-1 state", async ({ page }, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."
@@ -1625,9 +1935,7 @@ test.describe("POS flow", () => {
await expect(page.locator("#reg_1")).toHaveValue("AB12345");
});
test("desktop blocks next until duplicate warning is resolved", async ({
page,
}, testInfo) => {
test("desktop blocks next until duplicate warning is resolved", async ({ page }, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."
@@ -1667,9 +1975,7 @@ test.describe("POS flow", () => {
await expect.poll(() => fixture.ordersById[9300]?.reg_1 || null, { timeout: 10_000 }).toBe("AB12345");
});
test("desktop keyboard selection opens the booking chooser for booked search results", async ({
page,
}, testInfo) => {
test("desktop keyboard selection opens the booking chooser for booked search results", async ({ page }, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."
@@ -1736,9 +2042,7 @@ test.describe("POS flow", () => {
await reg1Input.press("ArrowDown");
await expect(activeDropdownItems).toHaveCount(1);
await expect(activeDropdownItems.first()).toContainText("EC21234");
await expect
.poll(() => page.evaluate(() => window.getSelection()?.toString() || ""), { timeout: 10_000 })
.toBe("");
await expect.poll(() => page.evaluate(() => window.getSelection()?.toString() || ""), { timeout: 10_000 }).toBe("");
await reg1Input.press("Enter");
await expect(reg1Input).toHaveValue("EC21234");
@@ -1751,9 +2055,7 @@ test.describe("POS flow", () => {
});
});
test("desktop dropdown click opens the booking chooser for booked search results", async ({
page,
}, testInfo) => {
test("desktop dropdown click opens the booking chooser for booked search results", async ({ page }, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."
@@ -1803,16 +2105,10 @@ test.describe("POS flow", () => {
const reg1Input = page.locator("#reg_1");
await reg1Input.fill("EC21");
await page
.locator(".dropdown-item")
.filter({ hasText: "EC21234 - Pleno Logistics" })
.first()
.click();
await page.locator(".dropdown-item").filter({ hasText: "EC21234 - Pleno Logistics" }).first().click();
await expect(reg1Input).toHaveValue("EC21234");
await expect
.poll(() => page.evaluate(() => window.getSelection()?.toString() || ""), { timeout: 10_000 })
.toBe("");
await expect.poll(() => page.evaluate(() => window.getSelection()?.toString() || ""), { timeout: 10_000 }).toBe("");
await expect(activeBookingSelector).toBeVisible({ timeout: 10_000 });
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8124")).toBeVisible({
timeout: 10_000,
@@ -1822,9 +2118,7 @@ test.describe("POS flow", () => {
});
});
test("desktop blocks next until a multiple-booking chooser is resolved", async ({
page,
}, testInfo) => {
test("desktop blocks next until a multiple-booking chooser is resolved", async ({ page }, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"Desktop POS booking flow is validated on chromium-desktop."