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