Files
pleno-vue/tests/e2e/self-serve-wash.spec.js
T

621 lines
22 KiB
JavaScript

import { expect, test } from "@playwright/test";
import { mockApi, primeMockSession } from "./support/network.js";
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 primeSession(page, { token, permissions, sessionData = {} }) {
await primeMockSession(page, { token });
}
async function seedGeolocation(context) {
await context.grantPermissions(["geolocation"]);
await context.setGeolocation({
latitude: 55.6415,
longitude: 12.0803,
});
}
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: "12345679",
answers: {},
completedTasks: {},
nearestDepartmentId: 6,
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);
}
function captureSelfServeGatewayRequests(page) {
const captured = {
previews: [],
summaries: [],
answers: [],
allowedServices: [],
commands: [],
};
page.on("request", (request) => {
const url = new URL(request.url());
let body = null;
if (request.method() !== "GET") {
try {
body = request.postDataJSON?.() || null;
} catch {
body = null;
}
}
const entry = {
method: request.method(),
url,
body,
};
if (url.pathname.endsWith("/department/selfserve/vehicle/allowed")) {
captured.previews.push(entry);
} else if (url.pathname.endsWith("/department/selfserve/washes/summary")) {
captured.summaries.push(entry);
} else if (url.pathname.endsWith("/department/selfserve/vehicle/conditions")) {
captured.answers.push(entry);
} else if (url.pathname.endsWith("/modules/self-serve/lane/services/allowed")) {
captured.allowedServices.push(entry);
} else if (url.pathname.endsWith("/modules/self-serve/lane/command")) {
captured.commands.push(entry);
}
});
return captured;
}
test.describe("Self-serve wash", () => {
test.describe.configure({ timeout: 120_000 });
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("Roskilde");
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("Roskilde");
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({ waitUntil: "domcontentloaded" });
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("Roskilde");
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("Roskilde");
});
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: "12345679",
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({ waitUntil: "domcontentloaded" });
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("customer session uses authenticated customer number instead of stale local values", async ({ page }) => {
const orderFilters = [];
page.on("request", (request) => {
const url = new URL(request.url());
if (url.pathname.endsWith("/orders")) {
orderFilters.push(url.searchParams.get("filters") || "");
}
});
await seedSavedProgress(page, {
currentStep: 2,
licensePlateInput: "ZZ00000",
vehicleTypeSelect: 2,
radioLaneOption: 7,
radioWashType: "Manual",
customerNumberInput: "777",
});
await mockApi(page, {
authenticated: true,
permissions: ["user"],
sessionData: {
customer_number: 12345679,
runtime_config: {
economic: {
transaction_draft_customer_number: 777,
},
},
},
selfServe: true,
});
await primeSession(page, {
token: "self-serve-customer-number-token",
permissions: ["user"],
});
await page.goto("/user/wash/start");
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled({ timeout: 10_000 });
const startCommandRequestPromise = waitForLaneCommandRequest(page, "START");
await page.getByTestId("self-serve-nav-confirm").click();
const startCommandRequest = await startCommandRequestPromise;
expect(startCommandRequest.postDataJSON?.()).toMatchObject({
command: "START",
customer_number: 12345679,
});
await page.waitForTimeout(250);
expect(orderFilters.some((filters) => filters.includes("customer_id:777"))).toBe(false);
});
test("start command failure stays retryable without entering in-progress state", async ({ page }) => {
await seedSavedProgress(page, {
currentStep: 2,
licensePlateInput: "ZZ00000",
vehicleTypeSelect: 2,
radioLaneOption: 7,
radioWashType: "Manual",
customerNumberInput: "777",
});
await mockApi(page, {
authenticated: true,
permissions: ["user"],
sessionData: {
customer_number: 12345679,
},
selfServe: {
commandResponses: [
{
success: false,
data: {
message: "Edge gateway command timed out",
},
},
{ success: true },
],
},
});
await primeSession(page, {
token: "self-serve-start-retry-token",
permissions: ["user"],
});
await page.goto("/user/wash/start");
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("self-serve-nav-confirm").click();
await expect(page.getByTestId("self-serve-action-error")).toContainText("Edge gateway command timed out");
await expect(page.getByTestId("self-serve-live-elapsed")).toBeHidden();
await page.getByTestId("self-serve-action-retry").click();
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
});
test("edge gateway preview and summary refreshes stay read-only until confirmation", async ({ page }) => {
const requests = captureSelfServeGatewayRequests(page);
await mockApi(page, {
authenticated: true,
permissions: ["user"],
selfServe: true,
});
await primeSession(page, {
token: "self-serve-edge-readonly-token",
permissions: ["user"],
});
await page.goto("/user/wash/start");
await fillRegistration(page, "ab12345");
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.getByTestId("self-serve-nav-confirm")).toBeEnabled({ timeout: 10_000 });
await expect(page.getByTestId("self-serve-runtime-error")).toBeHidden();
await expect(page.getByTestId("self-serve-action-error")).toBeHidden();
expect(requests.answers).toHaveLength(1);
expect(requests.answers[0].body).toMatchObject({
lane: 7,
question: 11,
value: true,
vehicle_type: 2,
activate_machine: false,
sync_relay_state: false,
});
expect(requests.allowedServices).toHaveLength(0);
expect(requests.commands).toHaveLength(0);
expect(requests.previews.some((entry) => entry.url.searchParams.get("vehicle_type") === "2")).toBe(true);
expect(requests.summaries.some((entry) => entry.url.searchParams.get("session_id") === "501")).toBe(true);
expect(requests.summaries.some((entry) => entry.url.searchParams.get("vehicle_type") === "2")).toBe(true);
await page.getByTestId("self-serve-nav-confirm").click();
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 });
expect(requests.allowedServices).toHaveLength(1);
expect(requests.allowedServices[0].body).toMatchObject({
lane_id: 7,
task_ids: [9001],
});
expect(requests.commands).toHaveLength(0);
});
test("allowed-services edge gateway timeout stays on questions and can be retried", async ({ page }) => {
const requests = captureSelfServeGatewayRequests(page);
await mockApi(page, {
authenticated: true,
permissions: ["user"],
selfServe: {
laneAllowedServiceResponses: [
{
success: false,
data: {
message: "Edge gateway command timed out",
},
},
{
success: true,
data: {
allowed_services: ["MACHINE"],
},
},
],
},
});
await primeSession(page, {
token: "self-serve-edge-services-retry-token",
permissions: ["user"],
});
await page.goto("/user/wash/start");
await fillRegistration(page, "ab12345");
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.getByTestId("self-serve-nav-confirm")).toBeEnabled({ timeout: 10_000 });
await page.getByTestId("self-serve-nav-confirm").click();
await expect(page.getByTestId("self-serve-runtime-error")).toContainText("Edge gateway command timed out");
await expect(page.getByTestId("self-serve-questions-step")).toBeVisible();
await expect(page.getByTestId("self-serve-lane-step")).toBeHidden();
expect(requests.allowedServices).toHaveLength(1);
expect(requests.commands).toHaveLength(0);
await page.getByTestId("self-serve-nav-confirm").click();
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("self-serve-runtime-error")).toBeHidden();
expect(requests.allowedServices).toHaveLength(2);
expect(requests.commands).toHaveLength(0);
});
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: "12345679",
completedTasks: {},
});
const api = await mockApi(page, {
authenticated: true,
permissions: ["user"],
selfServe: true,
});
const answeredSummary = api.selfServe.answerResponseByKey["7:AB12345:11:true"];
api.selfServe.previewByKey["7:AB12345"].questions = answeredSummary.questions;
api.selfServe.summaryBySessionId[501] = answeredSummary;
api.selfServe.summaryByKey["7:AB12345"] = answeredSummary;
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: "12345679",
});
await page.reload({ waitUntil: "domcontentloaded" });
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: "12345679",
});
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_6"],
selfServe: true,
});
await primeSession(page, {
token: "self-serve-admin-token",
permissions: ["admin", "department_access_6"],
});
await page.goto("/admin/6/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("SESSION_SYNCED")).toBeVisible();
const answeredQuestion = page.locator("li", { hasText: "Is the tarp removed?:" }).last();
await expect(answeredQuestion).toBeVisible();
await expect(answeredQuestion.locator("strong")).toHaveText("Ja");
});
});