Update self-serve mocked customer permissions for the guarded wash routes and replace removed replication-management E2E coverage with dashboard redirect assertions.
385 lines
15 KiB
TypeScript
385 lines
15 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, primeMockSession } from "./support/network.js";
|
|
|
|
const CUSTOMER_SELF_SERVE_PERMISSIONS = [
|
|
"user",
|
|
"list_own_department_selfserve_vehicle_conditions",
|
|
"add_own_department_selfserve_vehicle_conditions",
|
|
];
|
|
|
|
async function suppressVueDevtoolsOverlay(page) {
|
|
await page.addInitScript(() => {
|
|
const appendStyle = () => {
|
|
const parent = document.documentElement || document.head || document.body;
|
|
if (!parent) {
|
|
document.addEventListener("DOMContentLoaded", appendStyle, { once: true });
|
|
return;
|
|
}
|
|
|
|
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; }";
|
|
parent.appendChild(style);
|
|
};
|
|
|
|
appendStyle();
|
|
});
|
|
}
|
|
|
|
async function seedGeolocation(context) {
|
|
await context.grantPermissions(["geolocation"]);
|
|
await context.setGeolocation({ latitude: 55.6415, longitude: 12.0803 });
|
|
}
|
|
|
|
function buildSavedProgress(overrides = {}) {
|
|
return {
|
|
washInProgress: false,
|
|
washLaneId: null,
|
|
washStartTime: null,
|
|
currentStep: 0,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: "12345679",
|
|
answers: {},
|
|
completedTasks: {},
|
|
nearestDepartmentId: 6,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
isForcingNearestDepartment: false,
|
|
savedAt: Date.now(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function seedSavedProgress(page, overrides = {}) {
|
|
await page.addInitScript((savedProgress) => {
|
|
window.localStorage.setItem("mywash_progress_v6", JSON.stringify(savedProgress));
|
|
}, buildSavedProgress(overrides));
|
|
}
|
|
|
|
async function fillRegistration(page, value = "ab12345") {
|
|
const registration = page.locator('input[placeholder*="registreringsnummer"]').first();
|
|
await registration.fill(value);
|
|
await expect(registration).toHaveValue(value.toUpperCase());
|
|
await registration.press("Escape");
|
|
}
|
|
|
|
async function fillRegistrationWithoutClosingAutocomplete(page, value = "ab12345") {
|
|
const registration = page.locator('input[placeholder*="registreringsnummer"]').first();
|
|
await registration.fill(value);
|
|
await expect(registration).toHaveValue(value.toUpperCase());
|
|
}
|
|
|
|
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/);
|
|
}
|
|
|
|
async function answerRequiredQuestions(page) {
|
|
await expect(page.getByTestId("self-serve-question-11")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("self-serve-question-11-yes").click();
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled({ timeout: 10_000 });
|
|
}
|
|
|
|
async function completeGuidedWash(page) {
|
|
for (let index = 0; index < 5; index += 1) {
|
|
await page.getByTestId("self-serve-guided-next").click();
|
|
}
|
|
}
|
|
|
|
function collectSelfServeRequests(page) {
|
|
const requests = {
|
|
departments: [],
|
|
vehicleTypeProducts: [],
|
|
customerVehicles: [],
|
|
allowed: [],
|
|
conditions: [],
|
|
commands: [],
|
|
relays: [],
|
|
summaries: [],
|
|
activeWash: [],
|
|
inProgress: [],
|
|
};
|
|
|
|
page.on("request", (request) => {
|
|
const url = new URL(request.url());
|
|
const entry = {
|
|
method: request.method(),
|
|
url,
|
|
body: request.method() === "GET" ? null : request.postDataJSON?.() || null,
|
|
};
|
|
|
|
if (url.pathname.endsWith("/guest/departments")) requests.departments.push(entry);
|
|
if (url.pathname.endsWith("/products")) requests.vehicleTypeProducts.push(entry);
|
|
if (url.pathname.endsWith("/vehicles")) requests.customerVehicles.push(entry);
|
|
if (url.pathname.endsWith("/department/selfserve/vehicle/allowed")) requests.allowed.push(entry);
|
|
if (url.pathname.endsWith("/department/selfserve/vehicle/conditions")) requests.conditions.push(entry);
|
|
if (url.pathname.endsWith("/modules/self-serve/lane/command")) requests.commands.push(entry);
|
|
if (url.pathname.endsWith("/modules/self-serve/lane/relay/machine/enable")) requests.relays.push(entry);
|
|
if (url.pathname.endsWith("/department/selfserve/washes/summary")) requests.summaries.push(entry);
|
|
if (url.pathname.endsWith("/modules/self-serve/lane/wash/my-active-wash")) requests.activeWash.push(entry);
|
|
if (url.pathname.endsWith("/modules/self-serve/lane/wash/in-progress")) requests.inProgress.push(entry);
|
|
});
|
|
|
|
return requests;
|
|
}
|
|
|
|
test.describe("User MyWashStart mocked flow", () => {
|
|
test.describe.configure({ timeout: 120_000 });
|
|
|
|
test.skip(({ browserName }) => browserName !== "chromium", "MyWashStart flow coverage is maintained on Chromium.");
|
|
|
|
test.beforeEach(async ({ page, context }) => {
|
|
await suppressVueDevtoolsOverlay(page);
|
|
await seedGeolocation(context);
|
|
});
|
|
|
|
test("manual wash selects vehicle, answers questions, starts, stops, and shows completed step", async ({ page }) => {
|
|
const requests = collectSelfServeRequests(page);
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: { customer_number: 12345679 },
|
|
selfServe: true,
|
|
});
|
|
|
|
await primeMockSession(page, { token: "user-my-wash-manual-token", bootPath: "/user/wash/start" });
|
|
|
|
await expect(page.getByTestId("self-serve-department-name")).toContainText("Roskilde", { timeout: 15_000 });
|
|
await fillRegistration(page);
|
|
await selectVehicleType(page, 2);
|
|
await page.getByTestId("self-serve-nav-next").click();
|
|
await answerRequiredQuestions(page);
|
|
await page.getByTestId("self-serve-nav-confirm").click();
|
|
|
|
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("self-serve-lane-option-7").click();
|
|
await page.getByTestId("self-serve-wash-type-manual").click();
|
|
await page.getByTestId("self-serve-nav-confirm").click();
|
|
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await completeGuidedWash(page);
|
|
await expect(page.getByTestId("self-serve-nav-complete")).toBeVisible();
|
|
await page.getByTestId("self-serve-nav-complete").click();
|
|
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible({ timeout: 15_000 });
|
|
|
|
expect(requests.departments[0]?.url.searchParams.get("include_lanes")).toBe("true");
|
|
expect(requests.vehicleTypeProducts.length).toBeGreaterThan(0);
|
|
expect(requests.customerVehicles.length).toBeGreaterThan(0);
|
|
expect(requests.allowed.length).toBeGreaterThan(0);
|
|
expect(requests.conditions.some((request) => request.body?.question === 11 && request.body?.value === true)).toBe(
|
|
true
|
|
);
|
|
expect(requests.summaries.length).toBeGreaterThan(0);
|
|
expect(requests.activeWash.length).toBeGreaterThan(0);
|
|
expect(requests.inProgress.length).toBeGreaterThan(0);
|
|
expect(requests.commands.map((request) => request.body?.command)).toEqual(
|
|
expect.arrayContaining(["START", "STOP", "OPEN_PROPERTY_EXIT_GATE"])
|
|
);
|
|
});
|
|
|
|
test("registration autocomplete does not cover vehicle type selection", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: { customer_number: 12345679 },
|
|
selfServe: true,
|
|
});
|
|
|
|
await primeMockSession(page, { token: "user-my-wash-registration-overlay-token", bootPath: "/user/wash/start" });
|
|
|
|
await expect(page.getByTestId("self-serve-department-name")).toContainText("Roskilde", { timeout: 15_000 });
|
|
await fillRegistrationWithoutClosingAutocomplete(page);
|
|
await selectVehicleType(page, 2);
|
|
await expect(page.getByTestId("self-serve-nav-next")).toBeEnabled();
|
|
});
|
|
|
|
test("machine wash rolls back when relay enable fails after START", async ({ page }) => {
|
|
const requests = collectSelfServeRequests(page);
|
|
await seedSavedProgress(page, {
|
|
currentStep: 2,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
radioWashType: "Machine",
|
|
customerNumberInput: "12345679",
|
|
answers: { 11: true },
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: { customer_number: 12345679 },
|
|
selfServe: {
|
|
relayResponses: [{ success: false, data: { message: "Machine relay unavailable" } }],
|
|
},
|
|
});
|
|
|
|
await primeMockSession(page, { token: "user-my-wash-machine-token", bootPath: "/user/wash/start" });
|
|
const needsQuestionReview = await page
|
|
.getByTestId("self-serve-question-11")
|
|
.waitFor({ state: "visible", timeout: 10_000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
if (needsQuestionReview) {
|
|
await page.getByTestId("self-serve-question-11-yes").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-lane-step")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("self-serve-wash-type-machine").click();
|
|
await page.getByTestId("self-serve-nav-confirm").click();
|
|
|
|
await expect(page.getByTestId("self-serve-action-error")).toContainText("Machine relay unavailable");
|
|
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeHidden();
|
|
expect(requests.relays).toHaveLength(1);
|
|
expect(requests.commands.map((request) => request.body?.command)).toEqual(
|
|
expect.arrayContaining(["START", "STOP"])
|
|
);
|
|
});
|
|
|
|
test("active wash restore reloads start route and resumes at WASH_IN_PROGRESS", async ({ page }) => {
|
|
const requests = collectSelfServeRequests(page);
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: { customer_number: 12345679 },
|
|
selfServe: {
|
|
inProgressByLaneId: {
|
|
7: {
|
|
lane_id: 7,
|
|
in_progress: true,
|
|
session: {
|
|
id: 801,
|
|
lane_id: 7,
|
|
department_id: 6,
|
|
reg: "AB12345",
|
|
customer_number: 12345679,
|
|
vehicle_type_id: 2,
|
|
machine_relay_enabled: false,
|
|
wash_started_at: "2026-06-02 08:00:00",
|
|
status: "IN_PROGRESS",
|
|
},
|
|
customer: { customer_number: 12345679 },
|
|
vehicle: { reg: "AB12345", type: 2 },
|
|
},
|
|
},
|
|
summaryBySessionId: {
|
|
801: {
|
|
session: { id: 801, lane_id: 7, reg: "AB12345", status: "IN_PROGRESS", vehicle_type_id: 2 },
|
|
lane: { id: 7, name: "7", department: 6 },
|
|
questions: [],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [],
|
|
events: [{ id: 8011, type: "STARTED", created_at: "2026-06-02T08:00:00.000Z" }],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
await primeMockSession(page, { token: "user-my-wash-active-token", bootPath: "/user/wash/start" });
|
|
await page.reload({ waitUntil: "domcontentloaded" });
|
|
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 15_000 });
|
|
await expect(page.getByTestId("self-serve-department-name")).toContainText("Roskilde");
|
|
await expect(page.getByTestId("self-serve-vehicle-step")).toBeHidden();
|
|
expect(requests.activeWash.length).toBeGreaterThan(0);
|
|
expect(requests.commands).toHaveLength(0);
|
|
});
|
|
|
|
test("server-completed wash refresh keeps local in-progress state until customer completion", async ({ page }) => {
|
|
const requests = collectSelfServeRequests(page);
|
|
await seedSavedProgress(page, {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 60_000,
|
|
currentStep: 4,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
radioWashType: "Manual",
|
|
customerNumberInput: "12345679",
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: { customer_number: 12345679 },
|
|
selfServe: {
|
|
inProgressByLaneId: {
|
|
7: {
|
|
lane_id: 7,
|
|
in_progress: false,
|
|
session: null,
|
|
customer: null,
|
|
vehicle: null,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
await primeMockSession(page, { token: "user-my-wash-server-completed-token", bootPath: "/user/wash/start" });
|
|
await expect.poll(() => requests.inProgress.length, { timeout: 15_000 }).toBeGreaterThan(0);
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-completed-step")).toBeHidden();
|
|
});
|
|
|
|
test("answer-sync failure shows an error and blocks progression", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: { customer_number: 12345679 },
|
|
selfServe: true,
|
|
});
|
|
await page.route(/\/department\/selfserve\/vehicle\/conditions$/i, async (route) => {
|
|
if (route.request().method() === "POST") {
|
|
await route.fulfill({
|
|
status: 500,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ message: "Answer sync failed" }),
|
|
});
|
|
return;
|
|
}
|
|
await route.fallback();
|
|
});
|
|
|
|
await primeMockSession(page, { token: "user-my-wash-answer-sync-token", bootPath: "/user/wash/start" });
|
|
await fillRegistration(page);
|
|
await selectVehicleType(page, 2);
|
|
await page.getByTestId("self-serve-nav-next").click();
|
|
await expect(page.getByTestId("self-serve-question-11")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("self-serve-question-11-yes").click();
|
|
|
|
await expect(page.getByText("Answer sync failed").first()).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled();
|
|
await expect(page.getByTestId("self-serve-lane-step")).toBeHidden();
|
|
});
|
|
|
|
test("geolocation unavailable still provides a department-selection fallback", async ({ browser }) => {
|
|
const context = await browser.newContext({ permissions: [] });
|
|
const page = await context.newPage();
|
|
await suppressVueDevtoolsOverlay(page);
|
|
const requests = collectSelfServeRequests(page);
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: { customer_number: 12345679 },
|
|
selfServe: true,
|
|
});
|
|
|
|
await primeMockSession(page, { token: "user-my-wash-no-geolocation-token", bootPath: "/user/wash/start" });
|
|
|
|
await expect(page.getByTestId("self-serve-department-name")).toContainText("Roskilde", { timeout: 15_000 });
|
|
await fillRegistration(page);
|
|
await selectVehicleType(page, 2);
|
|
await expect(page.getByTestId("self-serve-nav-next")).toBeEnabled();
|
|
expect(requests.departments[0]?.url.searchParams.get("include_lanes")).toBe("true");
|
|
|
|
await context.close();
|
|
});
|
|
});
|