import { expect, test } from "@playwright/test"; import { mockApi, seedAuthenticatedState } from "./support/network.js"; async function suppressVueDevtoolsOverlay(page) { await page.addInitScript(() => { const style = document.createElement("style"); style.textContent = "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }"; document.documentElement.appendChild(style); }); } async function primeSession(page, { token, permissions, sessionData = {} }) { await seedAuthenticatedState(page, token); await page.goto("/login"); await page.evaluate( async ({ sessionToken, sessionPermissions, data }) => { const sessionModule = await import("/src/components/session/token/SessionUser.vue"); window.localStorage.setItem("token", sessionToken); sessionModule.SessionUser.token.value = sessionToken; sessionModule.SessionUser.authenticated.value = true; sessionModule.SessionUser.permissions.value = sessionPermissions; sessionModule.SessionUser.initiated.value = true; sessionModule.SessionUser.user.customer_number.value = data.customer_number ?? 12345; sessionModule.SessionUser.user.display_name.value = data.display_name ?? "E2E User"; }, { sessionToken: token, sessionPermissions: permissions, data: sessionData, } ); } async function seedGeolocation(context) { await context.grantPermissions(["geolocation"]); await context.setGeolocation({ latitude: 55.6761, longitude: 12.5683, }); } async function fillRegistration(page, value) { const registration = page.locator('input[placeholder*="registreringsnummer"]').first(); await registration.fill(value); await expect(registration).toHaveValue(value.toUpperCase()); await registration.press("Escape"); } async function selectVehicleType(page, vehicleTypeId = 2) { await page.getByTestId(`self-serve-vehicle-type-${vehicleTypeId}`).click(); await expect(page.getByTestId("self-serve-vehicle-selector-description")).toContainText(/Truck|Van|Car/); } function waitForLaneCommandRequest(page, command) { return page.waitForRequest((request) => { if (!request.url().includes("/modules/self-serve/lane/command")) { return false; } if (request.method() !== "POST") { return false; } const body = request.postDataJSON?.() || {}; return body.command === command; }); } function buildSavedProgress(overrides = {}) { return { washInProgress: false, washLaneId: null, washStartTime: null, currentStep: 0, licensePlateInput: "AB12345", vehicleTypeSelect: 2, radioWashType: "Manual", radioLaneOption: 7, customerNumberInput: "12345", answers: {}, completedTasks: {}, nearestDepartmentId: 1, forceNearestDepartmentEvaluationId: 0, isForcingNearestDepartment: false, savedAt: Date.now(), ...overrides, }; } async function seedSavedProgress(page, overrides = {}) { const payload = buildSavedProgress(overrides); await page.addInitScript((savedProgress) => { window.localStorage.setItem("mywash_progress_v6", JSON.stringify(savedProgress)); }, payload); } test.describe("Self-serve wash", () => { test.skip( ({ browserName }) => browserName !== "chromium", "Self-serve coverage is maintained on Chromium desktop/mobile." ); test.beforeEach(async ({ page, context }) => { await suppressVueDevtoolsOverlay(page); await seedGeolocation(context); }); test("@smoke landing route sorts departments, loads vehicle selection, and restores direct wash progress", async ({ page, }) => { await mockApi(page, { authenticated: true, permissions: ["user"], selfServe: true, }); await primeSession(page, { token: "self-serve-user-token", permissions: ["user"], }); await page.goto("/user/wash"); await expect(page.getByTestId("self-serve-wash-home")).toBeVisible(); await expect(page.getByTestId("self-serve-home-nearest-name")).toContainText("Copenhagen"); const orderedDepartmentIds = await page .locator('[data-testid^="self-serve-home-department-"]') .evaluateAll((elements) => elements.map((element) => element.getAttribute("data-testid"))); expect(orderedDepartmentIds).toEqual(["self-serve-home-department-2", "self-serve-home-department-3"]); await page.getByTestId("self-serve-home-start").click(); await expect(page).toHaveURL(/\/user\/wash\/start$/); await expect(page.getByTestId("self-serve-department-name")).toContainText("Copenhagen"); await fillRegistration(page, "ab12345"); await selectVehicleType(page, 2); await seedSavedProgress(page, { washInProgress: true, washLaneId: 7, washStartTime: Date.now() - 20_000, currentStep: 4, licensePlateInput: "ZZ00000", answers: { 21: true, }, }); await page.reload(); await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("self-serve-nav-complete").click(); await expect(page.getByTestId("self-serve-completed-step")).toBeVisible(); await page.getByTestId("self-serve-nav-close").click(); await expect(page.getByTestId("self-serve-completed-step")).toBeHidden(); await expect(page.getByTestId("self-serve-nav-complete")).toBeVisible(); }); test("superuser can force a department and clear the override", async ({ page }) => { await mockApi(page, { authenticated: true, permissions: ["user", "superuser"], selfServe: true, }); await primeSession(page, { token: "self-serve-superuser-token", permissions: ["user", "superuser"], }); await page.goto("/user/wash/start"); await expect(page.getByTestId("self-serve-department-name")).toContainText("Copenhagen"); await page.getByTestId("self-serve-department-name").click(); const departmentSearch = page.locator('input[placeholder*="afdelinger"]').first(); await departmentSearch.fill("Odense"); await page.getByRole("button", { name: "Odense" }).click(); await expect(page.getByTestId("self-serve-department-name")).toContainText("Odense"); await expect(page.getByTestId("self-serve-department-clear")).toBeVisible(); await page.getByTestId("self-serve-department-clear").click(); await expect(page.getByTestId("self-serve-department-name")).toContainText("Copenhagen"); }); test("lane flow gates invalid lanes, direct wash path auto-starts, and expired progress is discarded", async ({ page, }) => { await seedSavedProgress(page, { washInProgress: true, washLaneId: 7, washStartTime: Date.now() - 30_000, currentStep: 4, licensePlateInput: "STALE01", vehicleTypeSelect: 2, radioWashType: "Manual", radioLaneOption: 7, customerNumberInput: "12345", savedAt: Date.now() - 3 * 60 * 60 * 1000, }); await mockApi(page, { authenticated: true, permissions: ["user"], selfServe: true, }); await primeSession(page, { token: "self-serve-direct-token", permissions: ["user"], }); await page.goto("/user/wash/start"); await expect(page.getByTestId("self-serve-vehicle-step")).toBeVisible(); await expect(page.getByTestId("self-serve-nav-complete")).toBeHidden(); await expect.poll(async () => page.evaluate(() => window.localStorage.getItem("mywash_progress_v6"))).toBeNull(); await seedSavedProgress(page, { currentStep: 2, licensePlateInput: "ZZ00000", radioLaneOption: 8, radioWashType: "Machine", }); await page.reload(); await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 }); await expect(page.getByRole("radio", { name: /Maskine/i })).toBeDisabled(); await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled(); await page.getByTestId("self-serve-lane-option-7").click(); await page.getByTestId("self-serve-wash-type-manual").click(); await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled({ timeout: 10_000 }); await page.getByTestId("self-serve-nav-confirm").click(); await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 }); }); test("tasks flow renders dynamic image, requires completion, and restores after reload", async ({ page }) => { await seedSavedProgress(page, { washInProgress: true, washLaneId: 7, washStartTime: Date.now() - 30_000, currentStep: 3, licensePlateInput: "AB12345", vehicleTypeSelect: 2, radioLaneOption: 7, customerNumberInput: "12345", completedTasks: {}, }); await mockApi(page, { authenticated: true, permissions: ["user"], selfServe: { previewByKey: { "7:AB12345": { allowed: true, machine_available: true, lane: { id: 7, name: "7" }, session: { id: 501, status: "IN_PROGRESS", allowed: true }, questions: [ { id: 11, question: "Is the tarp removed?", description: "Required before machine wash.", answer: true, order_priority: 1, }, ], conditions: [], rules: [], tasks: [ { id: 9001, task: "Prepare the truck", description: "Complete the pre-wash checks.", order_priority: 1, services: ["MACHINE"], buttons: [1], }, ], allowed_services: ["MACHINE"], }, }, summaryBySessionId: { 501: { session: { id: 501, status: "IN_PROGRESS", allowed: true }, lane: { id: 7, name: "7" }, questions: [ { id: 11, question: "Is the tarp removed?", description: "Required before machine wash.", answer: true, order_priority: 1, }, ], conditions: [], rules: [], tasks: [ { id: 9001, task: "Prepare the truck", description: "Complete the pre-wash checks.", order_priority: 1, services: ["MACHINE"], buttons: [1], }, ], events: [{ id: 3, type: "QUESTION_ANSWERED", created_at: "2026-01-01T10:01:00.000Z" }], }, }, }, }); await primeSession(page, { token: "self-serve-tasks-token", permissions: ["user"], }); await page.goto("/user/wash/start"); await expect(page.getByTestId("self-serve-tasks-step")).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId("self-serve-dynamic-image")).toBeVisible(); await expect(page.getByTestId("self-serve-task-9001-attachment-302")).toBeVisible(); await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled(); await page.getByTestId("self-serve-task-9001-toggle").click(); await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled(); await page.getByTestId("self-serve-nav-confirm").click(); await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible(); await seedSavedProgress(page, { washInProgress: true, washLaneId: 7, washStartTime: Date.now() - 45_000, currentStep: 4, licensePlateInput: "AB12345", vehicleTypeSelect: 2, radioLaneOption: 7, customerNumberInput: "12345", }); await page.reload(); await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 }); }); test("in-progress flow opens property access and exit gates via lane command endpoint", async ({ page }) => { await seedSavedProgress(page, { washInProgress: true, washLaneId: 7, washStartTime: Date.now() - 30_000, currentStep: 4, licensePlateInput: "AB12345", vehicleTypeSelect: 2, radioLaneOption: 7, customerNumberInput: "12345", }); await mockApi(page, { authenticated: true, permissions: ["user"], selfServe: true, }); await primeSession(page, { token: "self-serve-property-gates-token", permissions: ["user"], }); await page.goto("/user/wash/start"); await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 }); const accessCommandRequestPromise = waitForLaneCommandRequest(page, "OPEN_PROPERTY_ACCESS_GATE"); await page.getByTestId("self-serve-nav-open-property-access-gate").click(); const accessCommandRequest = await accessCommandRequestPromise; expect(accessCommandRequest.postDataJSON?.()).toMatchObject({ lane_id: 7, command: "OPEN_PROPERTY_ACCESS_GATE", }); const exitCommandRequestPromise = waitForLaneCommandRequest(page, "OPEN_PROPERTY_EXIT_GATE"); await page.getByTestId("self-serve-nav-open-property-exit-gate").click(); const exitCommandRequest = await exitCommandRequestPromise; expect(exitCommandRequest.postDataJSON?.()).toMatchObject({ lane_id: 7, command: "OPEN_PROPERTY_EXIT_GATE", }); await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible(); }); test("mobile layout keeps primary controls usable", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("mobile"), "Mobile sanity runs on the mobile project only."); await mockApi(page, { authenticated: true, permissions: ["user"], selfServe: true, }); await primeSession(page, { token: "self-serve-mobile-token", permissions: ["user"], }); await page.goto("/user/wash/start"); await expect(page.getByTestId("self-serve-start-page")).toBeVisible(); await fillRegistration(page, "ab12345"); await selectVehicleType(page, 2); await expect(page.getByTestId("self-serve-nav-next")).toBeVisible(); await expect(page.getByTestId("self-serve-vehicle-selector-description")).toContainText("Truck"); }); test("admin preview modal reuses shared question/task rendering", async ({ page }) => { await mockApi(page, { authenticated: true, permissions: ["admin", "department_access_1"], selfServe: true, }); await primeSession(page, { token: "self-serve-admin-token", permissions: ["admin", "department_access_1"], }); await page.goto("/admin/1/modules/wash-lanes/7"); await expect(page.getByTestId("department-wash-lane-page")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("self-serve-admin-open-try-modal").first().click(); await expect(page.getByTestId("self-serve-try-modal")).toBeVisible(); await page.getByTestId("self-serve-try-reg").fill("ab12345"); await page.getByTestId("self-serve-try-refresh").click(); await expect(page.getByTestId("self-serve-question-11")).toBeVisible(); await page.getByTestId("self-serve-question-11-yes").click(); await expect(page.getByText("Prepare the truck")).toBeVisible(); await expect(page.getByText("QUESTION_ANSWERED")).toBeVisible({ timeout: 10_000 }); }); });