Resolve recommended-profile Critical and High findings, update vulnerable dependencies, restore invoice queue E2E authentication setup, and clear the remaining frontend Qodana findings.
2089 lines
76 KiB
JavaScript
2089 lines
76 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
|
|
|
const removedMachineUnavailableGuidanceText =
|
|
"Maskinvask er ikke tilgængelig for den valgte bane. Vælg manuel vask eller en anden ledig bane.";
|
|
|
|
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 primeSession(page, { token }) {
|
|
await seedAuthenticatedState(page, token);
|
|
}
|
|
|
|
async function mockSelectedSubuserGrant(
|
|
page,
|
|
{ customerNumber = 12345679, permissions = ["SELFSERVE_LIST", "SELFSERVE_ADD"] } = {}
|
|
) {
|
|
await page.route("**/subusers/me", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
data: {
|
|
id: 71,
|
|
username: "driver-user",
|
|
name: "Driver User",
|
|
email: "driver@example.test",
|
|
phone_country_code: 45,
|
|
phone: 12345678,
|
|
grants: [
|
|
{
|
|
id: 7101,
|
|
billing_customer_number: customerNumber,
|
|
permissions,
|
|
},
|
|
],
|
|
created_at: "2026-01-01T00:00:00.000Z",
|
|
updated_at: "2026-01-01T00:00:00.000Z",
|
|
suspended_at: null,
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
}
|
|
|
|
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/);
|
|
}
|
|
|
|
async function advanceGuidedWashToLastStep(page) {
|
|
for (let index = 0; index < 5; index += 1) {
|
|
await page.getByTestId("self-serve-guided-next").click();
|
|
}
|
|
}
|
|
|
|
async function waitForLaneCommandRequest(page, command) {
|
|
const request = await page.waitForRequest((candidateRequest) => {
|
|
if (!candidateRequest.url().includes("/modules/self-serve/lane/command")) {
|
|
return false;
|
|
}
|
|
if (candidateRequest.method() !== "POST") {
|
|
return false;
|
|
}
|
|
|
|
const body = candidateRequest.postDataJSON?.() || {};
|
|
return body.command === command;
|
|
});
|
|
await request.response();
|
|
return request;
|
|
}
|
|
|
|
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);
|
|
|
|
try {
|
|
await page.evaluate((savedProgress) => {
|
|
window.localStorage.setItem("mywash_progress_v6", JSON.stringify(savedProgress));
|
|
}, payload);
|
|
} catch {}
|
|
}
|
|
|
|
async function expectFinishingOrCompletedWash(page) {
|
|
const finishing = page.getByTestId("self-serve-finishing-wash");
|
|
const completed = page.getByTestId("self-serve-completed-step");
|
|
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
if (await finishing.isVisible().catch(() => false)) {
|
|
return "finishing";
|
|
}
|
|
if (await completed.isVisible().catch(() => false)) {
|
|
return "completed";
|
|
}
|
|
return "pending";
|
|
},
|
|
{
|
|
message: "expected the wash to show the finishing state or complete",
|
|
timeout: 15_000,
|
|
}
|
|
)
|
|
.toMatch(/^(finishing|completed)$/);
|
|
|
|
if (await finishing.isVisible().catch(() => false)) {
|
|
await expect(finishing).toContainText("Afslutter vask, porten åbnes automatisk");
|
|
}
|
|
}
|
|
|
|
function captureSelfServeGatewayRequests(page) {
|
|
const captured = {
|
|
previews: [],
|
|
summaries: [],
|
|
answers: [],
|
|
allowedServices: [],
|
|
commands: [],
|
|
dynamicImages: [],
|
|
};
|
|
|
|
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);
|
|
} else if (url.pathname.endsWith("/department/lanes/dynamic-image")) {
|
|
captured.dynamicImages.push(entry);
|
|
}
|
|
});
|
|
|
|
return captured;
|
|
}
|
|
|
|
async function roundedBoundingBox(locator) {
|
|
const box = await locator.boundingBox();
|
|
expect(box).not.toBeNull();
|
|
|
|
return {
|
|
width: Math.round(box.width),
|
|
height: Math.round(box.height),
|
|
};
|
|
}
|
|
|
|
function waitForQuestionAnswerRequest(page, question, value) {
|
|
return page.waitForRequest((request) => {
|
|
const url = new URL(request.url());
|
|
if (!url.pathname.endsWith("/department/selfserve/vehicle/conditions") || request.method() !== "POST") {
|
|
return false;
|
|
}
|
|
|
|
const body = request.postDataJSON?.() || {};
|
|
return Number(body.question) === Number(question) && body.value === value;
|
|
});
|
|
}
|
|
|
|
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,
|
|
}) => {
|
|
const api = await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: {
|
|
customer_number: 12345679,
|
|
},
|
|
selfServe: true,
|
|
});
|
|
api.selfServe.commandResponseDelayMs = [500, 0];
|
|
await primeSession(page, {
|
|
token: "self-serve-user-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash");
|
|
|
|
await expect(page.getByRole("link", { name: "Start vask" })).toBeVisible({ timeout: 30_000 });
|
|
await expect(page.getByText("Roskilde").first()).toBeVisible();
|
|
|
|
const orderedDepartmentNames = await page
|
|
.locator(".card-header-title.has-text-grey")
|
|
.evaluateAll((elements) => elements.map((element) => element.textContent?.trim()));
|
|
expect(orderedDepartmentNames).toEqual(["Odense", "Aarhus"]);
|
|
|
|
await page.getByRole("link", { name: "Start vask" }).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);
|
|
|
|
api.selfServe.inProgressByLaneId[7] = {
|
|
lane_id: 7,
|
|
in_progress: true,
|
|
session: {
|
|
id: 601,
|
|
lane_id: 7,
|
|
department_id: 6,
|
|
reg: "ZZ00000",
|
|
customer_number: 12345679,
|
|
vehicle_type_id: 2,
|
|
machine_relay_enabled: false,
|
|
wash_started_at: "2026-04-28 11:00:00",
|
|
status: "IN_PROGRESS",
|
|
},
|
|
customer: { customer_number: 12345679 },
|
|
vehicle: { reg: "ZZ00000", type: 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-inline-progress")).toContainText("ZZ00000", { timeout: 20_000 });
|
|
|
|
await advanceGuidedWashToLastStep(page);
|
|
await expect(page.getByTestId("self-serve-guided-next")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-nav-complete")).toBeVisible();
|
|
const stopCommandRequestPromise = waitForLaneCommandRequest(page, "STOP");
|
|
const exitGateCommandRequestPromise = waitForLaneCommandRequest(page, "OPEN_PROPERTY_EXIT_GATE");
|
|
await page.getByTestId("self-serve-nav-complete").click();
|
|
await expectFinishingOrCompletedWash(page);
|
|
const stopCommandRequest = await stopCommandRequestPromise;
|
|
const exitGateCommandRequest = await exitGateCommandRequestPromise;
|
|
expect(stopCommandRequest.postDataJSON?.()).toMatchObject({
|
|
lane_id: 7,
|
|
command: "STOP",
|
|
});
|
|
expect(exitGateCommandRequest.postDataJSON?.()).toMatchObject({
|
|
lane_id: 7,
|
|
command: "OPEN_PROPERTY_EXIT_GATE",
|
|
});
|
|
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible({ timeout: 15_000 });
|
|
await page.getByTestId("self-serve-nav-close").click();
|
|
await expect(page.getByTestId("self-serve-completed-step")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-vehicle-step")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-nav-complete")).toBeHidden();
|
|
});
|
|
|
|
test("start route lets regular users choose a department when geolocation permission is denied", async ({
|
|
page,
|
|
context,
|
|
}) => {
|
|
await context.clearPermissions();
|
|
await page.addInitScript(() => {
|
|
Object.defineProperty(navigator, "geolocation", {
|
|
configurable: true,
|
|
value: {
|
|
getCurrentPosition: (_success, error) =>
|
|
error?.({ code: 1, message: "User denied Geolocation", PERMISSION_DENIED: 1 }),
|
|
watchPosition: (_success, error) => {
|
|
error?.({ code: 1, message: "User denied Geolocation", PERMISSION_DENIED: 1 });
|
|
return 1;
|
|
},
|
|
clearWatch: () => {},
|
|
},
|
|
});
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: {
|
|
customer_number: 12345679,
|
|
},
|
|
selfServe: true,
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-denied-geolocation-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start", { waitUntil: "domcontentloaded" });
|
|
|
|
await expect(page.getByTestId("self-serve-start-page")).toBeVisible({ timeout: 15_000 });
|
|
await expect(page.getByTestId("self-serve-department-name")).toContainText("Roskilde");
|
|
await expect(page.getByTestId("self-serve-vehicle-step")).toBeVisible({ timeout: 15_000 });
|
|
|
|
await page.getByTestId("self-serve-department-name").click();
|
|
await expect(page.getByTestId("self-serve-department-search")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("self-serve-department-search").fill("Odense");
|
|
await page.getByText("Odense", { exact: true }).click();
|
|
|
|
await expect(page.getByTestId("self-serve-department-name")).toContainText("Odense");
|
|
});
|
|
|
|
test("start route resumes the authenticated customer's active server wash from another device", async ({ page }) => {
|
|
const requests = captureSelfServeGatewayRequests(page);
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: {
|
|
customer_number: 12345679,
|
|
},
|
|
selfServe: {
|
|
inProgressByLaneId: {
|
|
9: {
|
|
lane_id: 9,
|
|
in_progress: true,
|
|
session: {
|
|
id: 704,
|
|
lane_id: 9,
|
|
department_id: 2,
|
|
reg: "CD67890",
|
|
customer_number: 12345679,
|
|
vehicle_type_id: 2,
|
|
machine_relay_enabled: true,
|
|
wash_started_at: "2026-04-28 10:15:00",
|
|
status: "MACHINE_STARTED",
|
|
},
|
|
customer: { customer_number: 12345679 },
|
|
vehicle: { reg: "CD67890", type: 2 },
|
|
},
|
|
},
|
|
summaryBySessionId: {
|
|
704: {
|
|
session: {
|
|
id: 704,
|
|
lane_id: 9,
|
|
reg: "CD67890",
|
|
status: "MACHINE_STARTED",
|
|
allowed: true,
|
|
vehicle_type_id: 2,
|
|
},
|
|
lane: { id: 9, name: "9", department: 2 },
|
|
questions: [],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [],
|
|
events: [{ id: 7041, type: "MACHINE_STARTED", created_at: "2026-04-28T10:15:00.000Z" }],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-active-server-wash-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("self-serve-department-name")).toContainText("Odense");
|
|
await expect(page.getByTestId("self-serve-nav-help")).toContainText("Har du brug for hjælp?");
|
|
expect(requests.commands).toHaveLength(0);
|
|
|
|
const bottomActions = page.getByTestId("self-serve-bottom-actions");
|
|
const guidedActions = bottomActions.getByTestId("self-serve-guided-bottom-actions");
|
|
const sessionActions = bottomActions.getByTestId("self-serve-session-bottom-actions");
|
|
const gateActions = bottomActions.getByTestId("self-serve-property-gate-actions");
|
|
await expect(bottomActions).toBeVisible();
|
|
await expect
|
|
.poll(async () =>
|
|
bottomActions.evaluate((element) =>
|
|
Array.from(element.children).map((child) => child.getAttribute("data-testid"))
|
|
)
|
|
)
|
|
.toEqual([
|
|
"self-serve-session-bottom-actions",
|
|
"self-serve-property-gate-actions",
|
|
"self-serve-guided-bottom-actions",
|
|
]);
|
|
await expect(guidedActions.getByTestId("self-serve-guided-prev")).toBeVisible();
|
|
await expect(guidedActions.getByTestId("self-serve-guided-next")).toBeVisible();
|
|
await expect(sessionActions.getByTestId("self-serve-nav-help")).toContainText("Har du brug for hjælp?");
|
|
await expect(sessionActions.getByTestId("self-serve-nav-complete")).toBeHidden();
|
|
await expect(guidedActions.getByTestId("self-serve-nav-complete")).toBeHidden();
|
|
await expect(gateActions).toBeVisible();
|
|
await expect(gateActions.getByTestId("self-serve-nav-open-property-access-gate")).toBeVisible();
|
|
await expect(gateActions.getByTestId("self-serve-nav-open-property-exit-gate")).toBeVisible();
|
|
|
|
await advanceGuidedWashToLastStep(page);
|
|
const guidedStepLabel = page.getByTestId("self-serve-guided-step-label");
|
|
const guidedStepTitle = page.getByTestId("self-serve-guided-step-title");
|
|
const guidedStepCounter = page.getByTestId("self-serve-guided-counter");
|
|
const guidedStepContent = page.getByTestId("self-serve-guided-content");
|
|
await expect(guidedStepTitle).toHaveText("6. Spot Free-skylning");
|
|
await expect(guidedStepCounter).toHaveText("6/6");
|
|
await expect(guidedStepLabel).toContainText("6/6");
|
|
await expect(page.getByText("6 / 6")).toHaveCount(0);
|
|
const [titleBox, counterBox, contentBox] = await Promise.all([
|
|
guidedStepTitle.boundingBox(),
|
|
guidedStepCounter.boundingBox(),
|
|
guidedStepContent.boundingBox(),
|
|
]);
|
|
expect(titleBox).not.toBeNull();
|
|
expect(counterBox).not.toBeNull();
|
|
expect(contentBox).not.toBeNull();
|
|
expect(Math.abs(titleBox.y + titleBox.height / 2 - (counterBox.y + counterBox.height / 2))).toBeLessThan(4);
|
|
expect(counterBox.x).toBeGreaterThan(titleBox.x + titleBox.width);
|
|
expect(Math.abs(counterBox.x + counterBox.width - (contentBox.x + contentBox.width))).toBeLessThan(8);
|
|
await expect(guidedActions.getByTestId("self-serve-guided-next")).toBeHidden();
|
|
await expect(guidedActions.getByTestId("self-serve-nav-complete")).toBeVisible();
|
|
|
|
if ((page.viewportSize()?.width || 0) <= 768) {
|
|
await expect(bottomActions).toHaveCSS("position", "fixed");
|
|
const footerBox = await page.locator(".fixed-bottom-footer").boundingBox();
|
|
const actionsBox = await bottomActions.boundingBox();
|
|
|
|
expect(footerBox).not.toBeNull();
|
|
expect(actionsBox).not.toBeNull();
|
|
expect(actionsBox.y + actionsBox.height).toBeLessThanOrEqual(footerBox.y + 2);
|
|
}
|
|
|
|
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: 9,
|
|
command: "OPEN_PROPERTY_ACCESS_GATE",
|
|
});
|
|
});
|
|
|
|
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("start route skips an empty question set and waits for lane confirmation", async ({ page }) => {
|
|
const requests = captureSelfServeGatewayRequests(page);
|
|
const api = await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: true,
|
|
});
|
|
api.selfServe.answerResponseDelayMs = 1000;
|
|
api.selfServe.previewByKey["7:AB12345"] = {
|
|
...api.selfServe.previewByKey["7:AB12345"],
|
|
questions: [],
|
|
tasks: [],
|
|
};
|
|
api.selfServe.summaryBySessionId[501] = {
|
|
...api.selfServe.summaryBySessionId[501],
|
|
questions: [],
|
|
tasks: [],
|
|
};
|
|
api.selfServe.summaryByKey["7:AB12345"] = {
|
|
...api.selfServe.summaryByKey["7:AB12345"],
|
|
questions: [],
|
|
tasks: [],
|
|
};
|
|
await primeSession(page, {
|
|
token: "self-serve-empty-questions-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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-lane-step")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("self-serve-question-11")).toHaveCount(0);
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toContainText(/start/i);
|
|
expect(requests.commands).toHaveLength(0);
|
|
|
|
const startCommandRequestPromise = waitForLaneCommandRequest(page, "START");
|
|
await page.getByTestId("self-serve-nav-confirm").click();
|
|
const startCommandRequest = await startCommandRequestPromise;
|
|
expect(startCommandRequest.postDataJSON?.()).toMatchObject({
|
|
lane_id: 7,
|
|
command: "START",
|
|
wash_type: "Manual",
|
|
});
|
|
});
|
|
|
|
test("start route reviews prefilled answered questions before lane confirmation", async ({ page }) => {
|
|
const api = await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: true,
|
|
});
|
|
const answeredQuestion = {
|
|
...api.selfServe.previewByKey["7:AB12345"].questions[0],
|
|
answer: true,
|
|
};
|
|
api.selfServe.previewByKey["7:AB12345"] = {
|
|
...api.selfServe.previewByKey["7:AB12345"],
|
|
questions: [answeredQuestion],
|
|
tasks: [],
|
|
};
|
|
api.selfServe.summaryBySessionId[501] = {
|
|
...api.selfServe.summaryBySessionId[501],
|
|
questions: [answeredQuestion],
|
|
tasks: [],
|
|
};
|
|
api.selfServe.summaryByKey["7:AB12345"] = {
|
|
...api.selfServe.summaryByKey["7:AB12345"],
|
|
questions: [answeredQuestion],
|
|
tasks: [],
|
|
};
|
|
await primeSession(page, {
|
|
token: "self-serve-prefilled-questions-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start", { waitUntil: "domcontentloaded" });
|
|
await fillRegistration(page, "ab12345");
|
|
await selectVehicleType(page, 2);
|
|
await page.getByTestId("self-serve-nav-next").click();
|
|
|
|
await expect(page.getByTestId("self-serve-questions-step")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("self-serve-question-11-yes")).toHaveClass(/is-success/);
|
|
await expect(page.getByTestId("self-serve-lane-step")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-steps")).toHaveCSS("padding-bottom", "0px");
|
|
await expect(page.getByTestId("self-serve-prewash-bottom-actions")).toHaveCSS("position", "static");
|
|
|
|
await page.getByTestId("self-serve-nav-confirm").click();
|
|
|
|
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
test("vehicle next button is disabled while self-serve data loads", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: {
|
|
previewResponseDelayMs: 1000,
|
|
summaryResponseDelayMs: 1000,
|
|
},
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-next-loading-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
await expect(page.getByTestId("self-serve-vehicle-step")).toBeVisible({ timeout: 30_000 });
|
|
await fillRegistration(page, "ab12345");
|
|
await selectVehicleType(page, 2);
|
|
|
|
const nextButton = page.getByTestId("self-serve-nav-next");
|
|
await expect(nextButton).toBeEnabled({ timeout: 10_000 });
|
|
await expect(nextButton).not.toHaveClass(/is-loading/);
|
|
|
|
await nextButton.click();
|
|
|
|
await expect(nextButton).toBeDisabled();
|
|
await expect(page.getByTestId("self-serve-questions-step")).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
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: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: true,
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-direct-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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-questions-step")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled();
|
|
await page.getByTestId("self-serve-question-21-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 expect(page.getByTestId("self-serve-lane-option-7")).toContainText("Tilgængelig");
|
|
await expect(page.getByTestId("self-serve-lane-option-8")).toContainText("Vaskebanen er ikke tilgængelig");
|
|
await expect(page.getByRole("radio", { name: /Maskine/i })).toBeEnabled();
|
|
await expect(page.getByTestId("self-serve-machine-unavailable-guidance")).toHaveCount(0);
|
|
await expect(page.locator("body")).not.toContainText(removedMachineUnavailableGuidanceText);
|
|
|
|
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("hides disabled lanes from customer self-serve selection", async ({ page }) => {
|
|
await seedSavedProgress(page, {
|
|
currentStep: 2,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
radioWashType: "Manual",
|
|
customerNumberInput: "12345679",
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: {
|
|
departments: [
|
|
{
|
|
id: 6,
|
|
name: "Roskilde",
|
|
address: "Industrivej 45, 4000 Roskilde",
|
|
latitude: 55.6415,
|
|
longitude: 12.0803,
|
|
self_serve_enabled: true,
|
|
lanes: [
|
|
{
|
|
id: 7,
|
|
department: 6,
|
|
name: "7",
|
|
status: "AVAILABLE",
|
|
selfserve_enabled: false,
|
|
machine_available: false,
|
|
products: [2, 3],
|
|
relay_machine_id: "M-7",
|
|
},
|
|
{
|
|
id: 8,
|
|
department: 6,
|
|
name: "8",
|
|
status: "AVAILABLE",
|
|
selfserve_enabled: true,
|
|
machine_available: true,
|
|
products: [2, 3],
|
|
relay_machine_id: "M-8",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
previewByKey: {
|
|
"8:AB12345": {
|
|
allowed: true,
|
|
machine_available: true,
|
|
lane: { id: 8, name: "8" },
|
|
questions: [],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [],
|
|
allowed_services: ["MACHINE"],
|
|
},
|
|
},
|
|
summaryByKey: {
|
|
"8:AB12345": {
|
|
session: { id: 801, lane_id: 8, reg: "AB12345", status: "IN_PROGRESS", allowed: true, vehicle_type_id: 2 },
|
|
lane: { id: 8, name: "8" },
|
|
questions: [],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [],
|
|
events: [{ id: 8010, type: "SESSION_SYNCED", created_at: "2026-04-29T08:00:00.000Z" }],
|
|
},
|
|
},
|
|
laneAllowedServicesByLane: {
|
|
8: ["MACHINE"],
|
|
},
|
|
},
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-disabled-lane-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("self-serve-lane-option-7")).toHaveCount(0);
|
|
await expect(page.getByTestId("self-serve-lane-option-8")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled({ timeout: 10_000 });
|
|
});
|
|
|
|
test("disables lanes that the API marks unavailable even when raw status is available", async ({ page }) => {
|
|
await seedSavedProgress(page, {
|
|
currentStep: 2,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
radioWashType: "Manual",
|
|
customerNumberInput: "12345679",
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: {
|
|
departments: [
|
|
{
|
|
id: 6,
|
|
name: "Roskilde",
|
|
address: "Industrivej 45, 4000 Roskilde",
|
|
latitude: 55.6415,
|
|
longitude: 12.0803,
|
|
self_serve_enabled: true,
|
|
lanes: [
|
|
{
|
|
id: 7,
|
|
department: 6,
|
|
name: "7",
|
|
status: "AVAILABLE",
|
|
selfserve_enabled: true,
|
|
selfserve_available: false,
|
|
selfserve_unavailable_reason: "lane_temporarily_unavailable",
|
|
machine_available: false,
|
|
products: [2, 3],
|
|
relay_machine_id: "M-7",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-unavailable-lane-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("self-serve-lane-option-7")).toContainText("Vaskebanen er ikke tilgængelig");
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled();
|
|
});
|
|
|
|
test("moves a stale saved lane selection to the first API-available lane", async ({ page }) => {
|
|
await seedSavedProgress(page, {
|
|
currentStep: 2,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
radioWashType: "Manual",
|
|
customerNumberInput: "12345679",
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: {
|
|
departments: [
|
|
{
|
|
id: 6,
|
|
name: "Roskilde",
|
|
address: "Industrivej 45, 4000 Roskilde",
|
|
latitude: 55.6415,
|
|
longitude: 12.0803,
|
|
self_serve_enabled: true,
|
|
lanes: [
|
|
{
|
|
id: 7,
|
|
department: 6,
|
|
name: "7",
|
|
status: "AVAILABLE",
|
|
selfserve_enabled: true,
|
|
selfserve_available: false,
|
|
selfserve_unavailable_reason: "lane_temporarily_unavailable",
|
|
machine_available: false,
|
|
products: [2, 3],
|
|
relay_machine_id: "M-7",
|
|
},
|
|
{
|
|
id: 8,
|
|
department: 6,
|
|
name: "8",
|
|
status: "AVAILABLE",
|
|
selfserve_enabled: true,
|
|
selfserve_available: true,
|
|
selfserve_unavailable_reason: null,
|
|
machine_available: false,
|
|
products: [2, 3],
|
|
relay_machine_id: "M-8",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
previewByKey: {
|
|
"8:AB12345": {
|
|
allowed: true,
|
|
machine_available: false,
|
|
lane: { id: 8, name: "8", selfserve_available: true },
|
|
questions: [],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [],
|
|
allowed_services: [],
|
|
},
|
|
},
|
|
summaryByKey: {
|
|
"8:AB12345": {
|
|
session: { id: 801, lane_id: 8, reg: "AB12345", status: "IN_PROGRESS", allowed: true, vehicle_type_id: 2 },
|
|
lane: { id: 8, name: "8", selfserve_available: true },
|
|
questions: [],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [],
|
|
events: [{ id: 8010, type: "SESSION_SYNCED", created_at: "2026-04-29T08:00:00.000Z" }],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-stale-unavailable-lane-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("self-serve-lane-option-7")).toContainText("Vaskebanen er ikke tilgængelig");
|
|
await expect(page.getByTestId("self-serve-lane-option-8")).toContainText("Tilgængelig");
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled({ 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: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: {
|
|
customer_number: 12345679,
|
|
runtime_config: {
|
|
economic: {
|
|
transaction_draft_customer_number: 777,
|
|
},
|
|
},
|
|
},
|
|
selfServe: true,
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-customer-number-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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,
|
|
wash_type: "Manual",
|
|
defer_relay_side_effects: true,
|
|
});
|
|
await page.waitForTimeout(250);
|
|
expect(orderFilters.some((filters) => filters.includes("customer_id:777"))).toBe(false);
|
|
});
|
|
|
|
test("subuser selected customer grant can start self-serve wash", async ({ page }) => {
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem("is_subuser", "true");
|
|
window.localStorage.setItem("selected_customer_number", "12345679");
|
|
});
|
|
await seedSavedProgress(page, {
|
|
currentStep: 2,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
radioWashType: "Manual",
|
|
customerNumberInput: "",
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: {
|
|
customer_number: null,
|
|
},
|
|
selfServe: true,
|
|
});
|
|
await mockSelectedSubuserGrant(page);
|
|
await primeSession(page, {
|
|
token: "self-serve-subuser-grant-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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,
|
|
wash_type: "Manual",
|
|
defer_relay_side_effects: true,
|
|
});
|
|
expect(startCommandRequest.headers()["x-customer-number"]).toBe("12345679");
|
|
});
|
|
|
|
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",
|
|
});
|
|
|
|
const api = await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
sessionData: {
|
|
customer_number: 12345679,
|
|
},
|
|
selfServe: {
|
|
commandResponses: [
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Edge gateway command timed out",
|
|
},
|
|
},
|
|
{ success: true },
|
|
],
|
|
},
|
|
});
|
|
api.selfServe.previewByKey["7:ZZ00000"] = {
|
|
...api.selfServe.previewByKey["7:ZZ00000"],
|
|
questions: [],
|
|
tasks: [],
|
|
};
|
|
api.selfServe.summaryBySessionId[601] = {
|
|
...api.selfServe.summaryBySessionId[601],
|
|
questions: [],
|
|
tasks: [],
|
|
};
|
|
api.selfServe.summaryByKey["7:ZZ00000"] = {
|
|
session: api.selfServe.previewByKey["7:ZZ00000"].session,
|
|
lane: api.selfServe.previewByKey["7:ZZ00000"].lane,
|
|
questions: [],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [],
|
|
events: [{ id: 4, type: "STARTED", created_at: "2026-01-01T11:01:00.000Z" }],
|
|
};
|
|
await primeSession(page, {
|
|
token: "self-serve-start-retry-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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("stop command failure shows the completed state after backend cleanup", async ({ page }) => {
|
|
await seedSavedProgress(page, {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 30_000,
|
|
currentStep: 4,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
radioWashType: "Manual",
|
|
customerNumberInput: "12345679",
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: {
|
|
commandResponses: [
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Stop command failed",
|
|
},
|
|
},
|
|
],
|
|
inProgressByLaneId: {
|
|
7: {
|
|
lane_id: 7,
|
|
in_progress: true,
|
|
session: {
|
|
id: 705,
|
|
lane_id: 7,
|
|
department_id: 6,
|
|
reg: "AB12345",
|
|
customer_number: 12345679,
|
|
vehicle_type_id: 2,
|
|
machine_relay_enabled: true,
|
|
wash_started_at: "2026-04-28 10:15:00",
|
|
status: "MACHINE_STARTED",
|
|
},
|
|
customer: { customer_number: 12345679 },
|
|
vehicle: { reg: "AB12345", type: 2 },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-stop-failure-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await advanceGuidedWashToLastStep(page);
|
|
const stopCommandRequestPromise = waitForLaneCommandRequest(page, "STOP");
|
|
await page.getByTestId("self-serve-nav-complete").click();
|
|
const stopCommandRequest = await stopCommandRequestPromise;
|
|
|
|
expect(stopCommandRequest.postDataJSON?.()).toMatchObject({
|
|
lane_id: 7,
|
|
command: "STOP",
|
|
});
|
|
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-nav-complete")).toBeHidden();
|
|
});
|
|
|
|
test("already-ended stop response completes stale in-progress progress", async ({ page }) => {
|
|
await seedSavedProgress(page, {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 30_000,
|
|
currentStep: 4,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
radioWashType: "Manual",
|
|
customerNumberInput: "12345679",
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: {
|
|
commandResponseDelayMs: [500, 0],
|
|
commandResponses: [
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Failed to execute command: Cannot stop lane: Lane is not occupied.",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-stop-already-ended-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await advanceGuidedWashToLastStep(page);
|
|
const stopCommandRequestPromise = waitForLaneCommandRequest(page, "STOP");
|
|
const exitGateCommandRequestPromise = waitForLaneCommandRequest(page, "OPEN_PROPERTY_EXIT_GATE");
|
|
await page.getByTestId("self-serve-nav-complete").click();
|
|
await expectFinishingOrCompletedWash(page);
|
|
const stopCommandRequest = await stopCommandRequestPromise;
|
|
const exitGateCommandRequest = await exitGateCommandRequestPromise;
|
|
|
|
expect(stopCommandRequest.postDataJSON?.()).toMatchObject({
|
|
lane_id: 7,
|
|
command: "STOP",
|
|
});
|
|
expect(exitGateCommandRequest.postDataJSON?.()).toMatchObject({
|
|
lane_id: 7,
|
|
command: "OPEN_PROPERTY_EXIT_GATE",
|
|
});
|
|
await expect(page.getByTestId("self-serve-action-error")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible({ timeout: 15_000 });
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeHidden();
|
|
});
|
|
|
|
test("edge gateway preview and summary refreshes stay read-only until confirmation", async ({ page }) => {
|
|
const requests = captureSelfServeGatewayRequests(page);
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: true,
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-edge-readonly-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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 expect(page.getByTestId("self-serve-toggle-debug")).toHaveCount(0);
|
|
await expect(page.getByTestId("self-serve-debug-panel")).toHaveCount(0);
|
|
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("question answers stay clickable while answer sync refreshes in the background", async ({ page }) => {
|
|
const requests = captureSelfServeGatewayRequests(page);
|
|
|
|
const api = await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: true,
|
|
});
|
|
api.selfServe.answerResponseDelayMs = 1000;
|
|
const followUpQuestion = {
|
|
id: 21,
|
|
question: "Are the side mirrors folded?",
|
|
description: "Can be answered while the first answer syncs.",
|
|
answer: null,
|
|
order_priority: 2,
|
|
};
|
|
const preview = api.selfServe.previewByKey["7:AB12345"];
|
|
const initialQuestions = [...preview.questions, followUpQuestion];
|
|
preview.questions = initialQuestions;
|
|
api.selfServe.summaryByKey["7:AB12345"].questions = initialQuestions;
|
|
api.selfServe.answerResponseByKey["7:AB12345:11:true"].questions = [
|
|
{ ...initialQuestions[0], answer: true },
|
|
followUpQuestion,
|
|
];
|
|
api.selfServe.answerResponseByKey["7:AB12345:21:false"] = {
|
|
...api.selfServe.answerResponseByKey["7:AB12345:11:true"],
|
|
questions: [initialQuestions[0], { ...followUpQuestion, answer: false }],
|
|
};
|
|
await primeSession(page, {
|
|
token: "self-serve-background-answer-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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 expect(page.getByTestId("self-serve-question-21")).toBeVisible();
|
|
|
|
const firstAnswerRequest = waitForQuestionAnswerRequest(page, 11, true);
|
|
await page.getByTestId("self-serve-question-11-yes").click();
|
|
await firstAnswerRequest;
|
|
|
|
await expect(page.getByTestId("self-serve-questions-inline-loading")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-question-11-no")).toBeEnabled();
|
|
await expect(page.getByTestId("self-serve-question-21-no")).toBeEnabled();
|
|
|
|
const secondAnswerRequest = waitForQuestionAnswerRequest(page, 21, false);
|
|
await page.getByTestId("self-serve-question-21-no").click();
|
|
await secondAnswerRequest;
|
|
|
|
await expect.poll(() => requests.answers.length).toBe(2);
|
|
expect(requests.answers.map((entry) => entry.body)).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ question: 11, value: true }),
|
|
expect.objectContaining({ question: 21, value: false }),
|
|
])
|
|
);
|
|
});
|
|
|
|
test("question cards keep their position when answer refreshes return reordered questions", async ({ page }) => {
|
|
const api = await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: true,
|
|
});
|
|
api.selfServe.answerResponseDelayMs = 1000;
|
|
const preview = api.selfServe.previewByKey["7:AB12345"];
|
|
const primaryQuestion = preview.questions[0];
|
|
const followUpQuestion = {
|
|
id: 21,
|
|
question: "Are the side mirrors folded?",
|
|
description: "Keeps the list tall enough to detect jumps.",
|
|
answer: null,
|
|
order_priority: 2,
|
|
};
|
|
const initialQuestions = [primaryQuestion, followUpQuestion];
|
|
|
|
preview.questions = initialQuestions;
|
|
api.selfServe.summaryBySessionId[501].questions = initialQuestions;
|
|
api.selfServe.summaryByKey["7:AB12345"].questions = initialQuestions;
|
|
|
|
await primeSession(page, {
|
|
token: "self-serve-stable-question-order-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
await fillRegistration(page, "ab12345");
|
|
await selectVehicleType(page, 2);
|
|
await page.getByTestId("self-serve-nav-next").click();
|
|
|
|
const questionCards = page.locator(
|
|
'[data-testid="self-serve-question-cards"] > [data-testid^="self-serve-question-"]'
|
|
);
|
|
await expect(questionCards).toHaveCount(2, { timeout: 10_000 });
|
|
await expect
|
|
.poll(async () => questionCards.evaluateAll((cards) => cards.map((card) => card.getAttribute("data-testid"))))
|
|
.toEqual(["self-serve-question-11", "self-serve-question-21"]);
|
|
|
|
const firstQuestion = page.getByTestId("self-serve-question-11");
|
|
let stableQuestionBoxSamples = 0;
|
|
let previousQuestionBoxKey = "";
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const [box, viewport] = await Promise.all([firstQuestion.boundingBox(), page.viewportSize()]);
|
|
if (!box || !viewport || box.x < 0 || box.x >= viewport.width / 2) {
|
|
stableQuestionBoxSamples = 0;
|
|
previousQuestionBoxKey = "";
|
|
return false;
|
|
}
|
|
|
|
const boxKey = [box.x, box.y, box.width, box.height].map((value) => Math.round(value)).join(":");
|
|
stableQuestionBoxSamples = boxKey === previousQuestionBoxKey ? stableQuestionBoxSamples + 1 : 0;
|
|
previousQuestionBoxKey = boxKey;
|
|
return stableQuestionBoxSamples >= 2;
|
|
},
|
|
{ timeout: 10_000 }
|
|
)
|
|
.toBe(true);
|
|
|
|
const firstQuestionBefore = await firstQuestion.boundingBox();
|
|
expect(firstQuestionBefore).not.toBeNull();
|
|
|
|
const reorderedAnsweredQuestions = [{ ...followUpQuestion }, { ...primaryQuestion, answer: false }];
|
|
const reorderedSummary = {
|
|
...api.selfServe.answerResponseByKey["7:AB12345:11:true"],
|
|
questions: reorderedAnsweredQuestions,
|
|
};
|
|
api.selfServe.answerResponseByKey["7:AB12345:11:false"] = reorderedSummary;
|
|
api.selfServe.previewByKey["7:AB12345"].questions = reorderedAnsweredQuestions;
|
|
api.selfServe.summaryBySessionId[501].questions = reorderedAnsweredQuestions;
|
|
api.selfServe.summaryByKey["7:AB12345"].questions = reorderedAnsweredQuestions;
|
|
|
|
const answerRequest = waitForQuestionAnswerRequest(page, 11, false);
|
|
const noButton = page.getByTestId("self-serve-question-11-no");
|
|
await noButton.click();
|
|
|
|
await expect(noButton).toHaveClass(/is-danger/);
|
|
await expect(noButton).not.toHaveClass(/is-light/);
|
|
await expect(page.getByTestId("self-serve-questions-inline-loading")).toBeVisible();
|
|
const firstQuestionWhileLoading = await firstQuestion.boundingBox();
|
|
expect(firstQuestionWhileLoading).not.toBeNull();
|
|
expect(Math.abs((firstQuestionWhileLoading?.y || 0) - (firstQuestionBefore?.y || 0))).toBeLessThanOrEqual(4);
|
|
|
|
await answerRequest;
|
|
await expect(page.getByTestId("self-serve-questions-inline-loading")).toBeHidden({ timeout: 10_000 });
|
|
await expect
|
|
.poll(async () => questionCards.evaluateAll((cards) => cards.map((card) => card.getAttribute("data-testid"))))
|
|
.toEqual(["self-serve-question-11", "self-serve-question-21"]);
|
|
});
|
|
|
|
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: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
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: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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("global machine wash disable keeps questions visible and disables machine wash selection", async ({ page }) => {
|
|
const requests = captureSelfServeGatewayRequests(page);
|
|
|
|
const api = await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: {
|
|
laneAllowedServices: [],
|
|
},
|
|
});
|
|
for (const payload of [
|
|
api.selfServe.previewByKey["7:AB12345"],
|
|
api.selfServe.answerResponseByKey["7:AB12345:11:true"],
|
|
api.selfServe.summaryBySessionId[501],
|
|
api.selfServe.summaryByKey["7:AB12345"],
|
|
]) {
|
|
payload.allowed_services = [];
|
|
payload.machine_available = false;
|
|
payload.machine_wash_enabled = false;
|
|
payload.tasks = [];
|
|
}
|
|
await primeSession(page, {
|
|
token: "self-serve-machine-globally-disabled-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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-lane-step")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByRole("radio", { name: /Maskine/i })).toBeDisabled();
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled({ timeout: 10_000 });
|
|
expect(requests.commands).toHaveLength(0);
|
|
});
|
|
|
|
test("@dynamic-image tasks flow renders dynamic image, requires completion, and restores after reload", async ({
|
|
page,
|
|
}) => {
|
|
const requests = captureSelfServeGatewayRequests(page);
|
|
|
|
await seedSavedProgress(page, {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 30_000,
|
|
currentStep: 3,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
radioWashType: "Machine",
|
|
customerNumberInput: "12345679",
|
|
completedTasks: {},
|
|
});
|
|
|
|
const api = await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: true,
|
|
});
|
|
api.selfServe.dynamicImageDelayMs = [750, 700, 700];
|
|
const answeredSummary = api.selfServe.answerResponseByKey["7:AB12345:11:true"];
|
|
answeredSummary.allowed_services = ["PROGRAM_PICKER", "MACHINE"];
|
|
answeredSummary.tasks = [
|
|
{
|
|
...answeredSummary.tasks[0],
|
|
condition_id: null,
|
|
gate_type: "ALWAYS",
|
|
gate_ref_id: null,
|
|
services: ["PROGRAM_PICKER"],
|
|
buttons: [],
|
|
dynamic_images_vehicle_type: 4,
|
|
},
|
|
{
|
|
id: 9002,
|
|
task: "Machine access",
|
|
description: "Enable the wash machine relay.",
|
|
order_priority: 2,
|
|
services: ["MACHINE"],
|
|
condition_id: null,
|
|
gate_type: "ALWAYS",
|
|
gate_ref_id: null,
|
|
buttons: [0, 2, "start", 5],
|
|
dynamic_images_vehicle_type: 3,
|
|
attachments: [],
|
|
},
|
|
];
|
|
api.selfServe.previewByKey["7:AB12345"].questions = answeredSummary.questions;
|
|
api.selfServe.previewByKey["7:AB12345"].tasks = answeredSummary.tasks;
|
|
api.selfServe.summaryBySessionId[501] = answeredSummary;
|
|
api.selfServe.summaryByKey["7:AB12345"] = answeredSummary;
|
|
await primeSession(page, {
|
|
token: "self-serve-tasks-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start", { waitUntil: "domcontentloaded" });
|
|
|
|
await expect(page.getByTestId("self-serve-tasks-step")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("self-serve-dynamic-image-skeleton")).toBeHidden({ timeout: 10_000 });
|
|
await expect(page.getByTestId("self-serve-dynamic-image")).toBeVisible();
|
|
const dynamicImageFrame = page.getByTestId("self-serve-dynamic-image-frame");
|
|
const loadedDynamicImageFrameBox = await roundedBoundingBox(dynamicImageFrame);
|
|
await expect(page.getByTestId("self-serve-task-9002")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-task-9002")).toContainText("Machine access");
|
|
await expect(page.getByTestId("self-serve-task-9002")).not.toContainText("#3 Machine access");
|
|
await expect
|
|
.poll(() => JSON.parse(requests.dynamicImages.at(-1)?.url.searchParams.get("buttons") || "[]"))
|
|
.toEqual(["program_picker", 0, 2, "start", 5]);
|
|
const initialDynamicImageUrl = requests.dynamicImages.at(-1).url;
|
|
expect(JSON.parse(initialDynamicImageUrl.searchParams.get("buttons"))).toEqual([
|
|
"program_picker",
|
|
0,
|
|
2,
|
|
"start",
|
|
5,
|
|
]);
|
|
expect(initialDynamicImageUrl.searchParams.get("current_step")).toBe("0");
|
|
expect(initialDynamicImageUrl.searchParams.get("thumb_position")).toBe("4");
|
|
await expect(page.getByTestId("self-serve-task-9001-attachment-302")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-task-9001")).toContainText("Prepare the truck #4");
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled();
|
|
|
|
await page.getByTestId("self-serve-task-9001-toggle").click();
|
|
await expect(page.getByTestId("self-serve-dynamic-image-skeleton")).toBeVisible({ timeout: 2_000 });
|
|
await expect.poll(() => roundedBoundingBox(dynamicImageFrame)).toEqual(loadedDynamicImageFrameBox);
|
|
await expect(page.getByTestId("self-serve-dynamic-image-skeleton")).toBeHidden({ timeout: 10_000 });
|
|
await expect.poll(() => requests.dynamicImages.at(-1)?.url.searchParams.get("current_step")).toBe("1");
|
|
expect(JSON.parse(requests.dynamicImages.at(-1).url.searchParams.get("buttons"))).toEqual([
|
|
"program_picker",
|
|
0,
|
|
2,
|
|
"start",
|
|
5,
|
|
]);
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled();
|
|
|
|
await page.getByTestId("self-serve-task-9002-toggle").click();
|
|
await expect.poll(() => requests.dynamicImages.at(-1)?.url.searchParams.get("current_step")).toBe("5");
|
|
expect(JSON.parse(requests.dynamicImages.at(-1).url.searchParams.get("buttons"))).toEqual([
|
|
"program_picker",
|
|
0,
|
|
2,
|
|
"start",
|
|
5,
|
|
]);
|
|
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("machine start keeps task instructions after a stale post-start summary", async ({ page }) => {
|
|
const requests = captureSelfServeGatewayRequests(page);
|
|
|
|
const api = await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: true,
|
|
});
|
|
const answeredSummary = api.selfServe.answerResponseByKey["7:AB12345:11:true"];
|
|
const machineTasks = [
|
|
{
|
|
...answeredSummary.tasks[0],
|
|
services: ["MACHINE"],
|
|
buttons: [1],
|
|
dynamic_images_vehicle_type: 2,
|
|
},
|
|
{
|
|
id: 9002,
|
|
task: "Machine access",
|
|
description: "Enable the wash machine relay.",
|
|
order_priority: 2,
|
|
services: ["MACHINE"],
|
|
condition_id: null,
|
|
gate_type: "ALWAYS",
|
|
gate_ref_id: null,
|
|
buttons: [2, "start"],
|
|
dynamic_images_vehicle_type: 3,
|
|
attachments: [],
|
|
},
|
|
];
|
|
const activeMachineSummary = {
|
|
...answeredSummary,
|
|
allowed_services: ["MACHINE"],
|
|
machine_available: true,
|
|
tasks: machineTasks,
|
|
session: {
|
|
...answeredSummary.session,
|
|
status: "IN_PROGRESS",
|
|
allowed: true,
|
|
},
|
|
};
|
|
|
|
api.selfServe.answerResponseByKey["7:AB12345:11:true"] = activeMachineSummary;
|
|
api.selfServe.summaryBySessionId[501] = activeMachineSummary;
|
|
api.selfServe.summaryByKey["7:AB12345"] = {
|
|
...activeMachineSummary,
|
|
allowed_services: [],
|
|
tasks: [],
|
|
};
|
|
|
|
await primeSession(page, {
|
|
token: "self-serve-machine-start-stale-summary-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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-lane-step")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("self-serve-lane-option-7").click();
|
|
await page.getByTestId("self-serve-wash-type-machine").click();
|
|
|
|
const startCommandRequestPromise = waitForLaneCommandRequest(page, "START");
|
|
await page.getByTestId("self-serve-nav-confirm").click();
|
|
const startCommandRequest = await startCommandRequestPromise;
|
|
expect(startCommandRequest.postDataJSON?.()).toMatchObject({
|
|
lane_id: 7,
|
|
command: "START",
|
|
wash_type: "Machine",
|
|
});
|
|
|
|
await expect
|
|
.poll(() =>
|
|
requests.summaries.some(
|
|
(entry) => entry.url.searchParams.get("lane_id") === "7" && entry.url.searchParams.get("reg") === "AB12345"
|
|
)
|
|
)
|
|
.toBe(true);
|
|
await expect(page.getByTestId("self-serve-tasks-step")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("self-serve-task-9002")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-tasks-step")).not.toContainText("Spørgsmål besvaret");
|
|
});
|
|
|
|
test("manual wash hides machine tasks when the machine service is allowed", async ({ page }) => {
|
|
await seedSavedProgress(page, {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 30_000,
|
|
currentStep: 3,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
radioWashType: "Manual",
|
|
customerNumberInput: "12345679",
|
|
completedTasks: {},
|
|
});
|
|
|
|
const api = await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: true,
|
|
});
|
|
const answeredSummary = api.selfServe.answerResponseByKey["7:AB12345:11:true"];
|
|
answeredSummary.tasks = [
|
|
...answeredSummary.tasks,
|
|
{
|
|
id: 9004,
|
|
task: "Press reset",
|
|
description: "Dynamic image button task without the MACHINE service.",
|
|
order_priority: 4,
|
|
services: [],
|
|
buttons: ["reset"],
|
|
dynamic_images_vehicle_type: 2,
|
|
},
|
|
{
|
|
id: 9005,
|
|
task: "Tryk reset",
|
|
description: "Legacy machine button task without structured button metadata.",
|
|
order_priority: 5,
|
|
services: [],
|
|
buttons: [],
|
|
dynamic_images_vehicle_type: null,
|
|
},
|
|
];
|
|
api.selfServe.previewByKey["7:AB12345"].questions = answeredSummary.questions;
|
|
api.selfServe.previewByKey["7:AB12345"].tasks = answeredSummary.tasks;
|
|
api.selfServe.summaryBySessionId[501] = answeredSummary;
|
|
api.selfServe.summaryByKey["7:AB12345"] = answeredSummary;
|
|
await primeSession(page, {
|
|
token: "self-serve-manual-task-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-task-9001")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-task-9002")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-task-9004")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-task-9005")).toBeHidden();
|
|
await expect(page.getByText("Press reset")).toBeHidden();
|
|
await expect(page.getByText("Tryk reset")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled();
|
|
});
|
|
|
|
test("machine-unavailable question result hides dynamic image button tasks", async ({ page }) => {
|
|
await seedSavedProgress(page, {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 30_000,
|
|
currentStep: 3,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
radioWashType: "Machine",
|
|
customerNumberInput: "12345679",
|
|
completedTasks: {},
|
|
});
|
|
|
|
const api = await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: true,
|
|
});
|
|
const buttonTask = {
|
|
id: 9004,
|
|
task: "Press reset",
|
|
description: "Dynamic image button task without the MACHINE service.",
|
|
order_priority: 4,
|
|
services: [],
|
|
buttons: ["reset"],
|
|
dynamic_images_vehicle_type: 2,
|
|
};
|
|
const legacyButtonTask = {
|
|
id: 9005,
|
|
task: "Tryk reset",
|
|
description: "Legacy machine button task without structured button metadata.",
|
|
order_priority: 5,
|
|
services: [],
|
|
buttons: [],
|
|
dynamic_images_vehicle_type: null,
|
|
};
|
|
const blockedSummary = {
|
|
...api.selfServe.answerResponseByKey["7:AB12345:11:true"],
|
|
session: {
|
|
...api.selfServe.answerResponseByKey["7:AB12345:11:true"].session,
|
|
allowed: false,
|
|
status: "MACHINE_NOT_ALLOWED",
|
|
},
|
|
questions: [
|
|
{
|
|
id: 11,
|
|
question: "Is the tarp removed?",
|
|
description: "Required before machine wash.",
|
|
answer: false,
|
|
order_priority: 1,
|
|
},
|
|
],
|
|
tasks: [buttonTask, legacyButtonTask],
|
|
};
|
|
api.selfServe.previewByKey["7:AB12345"] = {
|
|
...api.selfServe.previewByKey["7:AB12345"],
|
|
allowed: false,
|
|
allowed_services: [],
|
|
questions: blockedSummary.questions,
|
|
tasks: blockedSummary.tasks,
|
|
};
|
|
api.selfServe.summaryBySessionId[501] = blockedSummary;
|
|
api.selfServe.summaryByKey["7:AB12345"] = blockedSummary;
|
|
await primeSession(page, {
|
|
token: "self-serve-machine-blocked-task-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-task-9004")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-task-9005")).toBeHidden();
|
|
await expect(page.getByText("Press reset")).toBeHidden();
|
|
await expect(page.getByText("Tryk reset")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled();
|
|
});
|
|
|
|
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: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: true,
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-property-gates-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
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: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
selfServe: true,
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-mobile-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
|
|
await expect(page.getByTestId("self-serve-start-page")).toBeVisible({ timeout: 45_000 });
|
|
const selfServeSteps = page.getByTestId("self-serve-steps");
|
|
await expect(selfServeSteps.locator(".steps.mobile-compact")).toBeVisible();
|
|
await expect.poll(async () => selfServeSteps.locator(".step-item:visible").count()).toBeGreaterThanOrEqual(4);
|
|
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");
|
|
|
|
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-lane-step")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("self-serve-lane-option-7").click();
|
|
await page.getByTestId("self-serve-wash-type-manual").click();
|
|
|
|
const prewashActions = page.getByTestId("self-serve-prewash-bottom-actions");
|
|
const prewashNavigation = prewashActions.getByTestId("self-serve-prewash-navigation-actions");
|
|
await expect(prewashActions).toBeVisible();
|
|
await expect(prewashActions).toHaveCSS("position", "fixed");
|
|
await expect(prewashNavigation.getByTestId("self-serve-nav-previous")).toBeVisible();
|
|
await expect(prewashNavigation.getByTestId("self-serve-nav-confirm")).toBeVisible();
|
|
await expect(prewashNavigation.getByTestId("self-serve-nav-confirm")).toBeEnabled();
|
|
|
|
const footerBox = await page.locator(".fixed-bottom-footer").boundingBox();
|
|
const actionsBox = await prewashActions.boundingBox();
|
|
expect(footerBox).not.toBeNull();
|
|
expect(actionsBox).not.toBeNull();
|
|
expect(actionsBox.y + actionsBox.height).toBeLessThanOrEqual(footerBox.y + 2);
|
|
|
|
await prewashNavigation.getByTestId("self-serve-nav-previous").click();
|
|
await expect(page.getByTestId("self-serve-questions-step")).toBeVisible();
|
|
await page.getByTestId("self-serve-nav-confirm").click();
|
|
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible();
|
|
});
|
|
|
|
test("mocked polling keeps active wash local until the customer completes when backend reports it ended", async ({
|
|
page,
|
|
}) => {
|
|
await seedSavedProgress(page, {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 30_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: {
|
|
id: 801,
|
|
lane_id: 7,
|
|
reg: "AB12345",
|
|
customer_number: 12345679,
|
|
vehicle_type_id: 2,
|
|
status: "COMPLETED",
|
|
},
|
|
customer: { customer_number: 12345679 },
|
|
vehicle: { reg: "AB12345", type: 2 },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-polling-completed-token",
|
|
permissions: CUSTOMER_SELF_SERVE_PERMISSIONS,
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
|
|
await expect(page.getByTestId("self-serve-bottom-actions")).toBeVisible({ timeout: 15_000 });
|
|
await expect(page.getByTestId("self-serve-completed-step")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible();
|
|
await expect
|
|
.poll(async () => page.evaluate(() => window.localStorage.getItem("mywash_progress_v6")))
|
|
.not.toBeNull();
|
|
|
|
await advanceGuidedWashToLastStep(page);
|
|
const stopCommandRequestPromise = waitForLaneCommandRequest(page, "STOP");
|
|
await page.getByTestId("self-serve-nav-complete").click();
|
|
await stopCommandRequestPromise;
|
|
|
|
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible({ timeout: 15_000 });
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeHidden();
|
|
await expect.poll(async () => page.evaluate(() => window.localStorage.getItem("mywash_progress_v6"))).toBeNull();
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|