diff --git a/src/composables/useWashDepartments.js b/src/composables/useWashDepartments.js
index d9e6cbfa..8d8fd17b 100644
--- a/src/composables/useWashDepartments.js
+++ b/src/composables/useWashDepartments.js
@@ -153,8 +153,16 @@ export function useWashDepartments(options = {}) {
}
});
- nearestDepartment.value = currentNearestDepartment.id ? currentNearestDepartment : null;
- departmentSelectionStrategy.value = currentNearestDepartment.id ? "distance" : null;
+ if (currentNearestDepartment.id) {
+ nearestDepartment.value = currentNearestDepartment;
+ return nearestDepartment.value;
+ }
+
+ const fallbackDepartment =
+ guestDepartments.value.find((department) => department.self_serve_enabled === true) || guestDepartments.value[0];
+ if (!nearestDepartment.value && fallbackDepartment) {
+ nearestDepartment.value = toDepartmentViewModel(fallbackDepartment);
+ }
return nearestDepartment.value;
};
diff --git a/src/views/dashboards/userDashboard/wash/MyWashStart.vue b/src/views/dashboards/userDashboard/wash/MyWashStart.vue
index d31e0df4..6010e149 100644
--- a/src/views/dashboards/userDashboard/wash/MyWashStart.vue
+++ b/src/views/dashboards/userDashboard/wash/MyWashStart.vue
@@ -125,6 +125,7 @@ const {
enableMachineRelay,
downloadAttachment,
answerQuestion,
+ removeAnswer,
} = useSelfServeLogic();
const {
@@ -1204,6 +1205,7 @@ const onSelectVehicleType = (selection: VehicleTypeTemplate) => {
markStartFormUserInput();
vehicleStepError.value = null;
washActionError.value = null;
+ answerSyncError.value = null;
vehicleTypeSelect.value = selection.id;
};
@@ -1211,6 +1213,7 @@ const onUpdateCustomerNumber = (value: string) => {
markStartFormUserInput();
vehicleStepError.value = null;
washActionError.value = null;
+ answerSyncError.value = null;
const authenticatedCustomerNumber = getAuthenticatedCustomerNumber();
customerNumberInput.value = authenticatedCustomerNumber ? String(authenticatedCustomerNumber) : value;
};
@@ -1219,18 +1222,21 @@ const onUpdateRegistrationNumber = (value: string) => {
markStartFormUserInput();
vehicleStepError.value = null;
washActionError.value = null;
+ answerSyncError.value = null;
licensePlateInput.value = normalizeLicensePlate(value);
};
const onUpdateSelectedLaneId = (laneId: number) => {
markStartFormUserInput();
washActionError.value = null;
+ answerSyncError.value = null;
radioLaneOption.value = laneId;
};
const onUpdateWashType = (washType: string) => {
markStartFormUserInput();
washActionError.value = null;
+ answerSyncError.value = null;
radioWashType.value = washType;
};
@@ -1407,6 +1413,7 @@ const onCloseCompleted = () => {
editAnswers.value = false;
vehicleStepError.value = null;
washActionError.value = null;
+ answerSyncError.value = null;
hasStartFormUserInput.value = false;
markQuestionReviewRequired();
resetGuidedWashStep();
@@ -1836,6 +1843,15 @@ watch(
@toggle-edit="toggleEditAnswers"
@answer-question="submitQuestionAnswer"
/>
+
+
+ {{ answerSyncError }}
+
{
+ 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 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: [],
+ vehicleTypes: [],
+ 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("/modules/xlvask/internal/vehicle-types")) requests.vehicleTypes.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: ["user"],
+ 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-tasks-step")).toBeVisible({ timeout: 10_000 });
+ 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({ 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.vehicleTypes.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("machine wash enables the relay after START and rolls back when relay enable fails", 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: ["user"],
+ sessionData: { customer_number: 12345679 },
+ selfServe: {
+ relayResponses: [{ success: true }, { success: false, data: { message: "Machine relay unavailable" } }],
+ },
+ });
+
+ await primeMockSession(page, { token: "user-my-wash-machine-token", bootPath: "/user/wash/start" });
+ 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-tasks-step")).toBeVisible({ timeout: 10_000 });
+
+ expect(requests.commands.map((request) => request.body?.command)).toContain("START");
+ expect(requests.relays).toHaveLength(1);
+ const firstStartIndex = requests.commands.findIndex((request) => request.body?.command === "START");
+ expect(firstStartIndex).toBeGreaterThanOrEqual(0);
+
+ await page.getByTestId("self-serve-nav-previous").click();
+ await expect(page.getByTestId("self-serve-lane-step")).toBeVisible();
+ 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-live-elapsed")).toBeHidden();
+ expect(requests.relays).toHaveLength(2);
+ 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: ["user"],
+ 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 moves local in-progress state to completed", async ({ 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: ["user"],
+ 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(page.getByTestId("self-serve-completed-step")).toBeVisible({ timeout: 15_000 });
+ await expect(page.getByTestId("self-serve-live-elapsed")).toBeHidden();
+ });
+
+ test("answer-sync failure shows an error and blocks progression", async ({ page }) => {
+ await mockApi(page, {
+ authenticated: true,
+ permissions: ["user"],
+ 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.getByTestId("self-serve-answer-sync-error")).toContainText("Answer sync failed");
+ 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: ["user"],
+ sessionData: { customer_number: 12345679 },
+ selfServe: true,
+ });
+
+ await primeMockSession(page, { token: "user-my-wash-no-geolocation-token", bootPath: "/user/wash/start" });
+
+ await expect(page.getByTestId("self-serve-department-name")).toContainText("Roskilde", { timeout: 15_000 });
+ await fillRegistration(page);
+ await selectVehicleType(page, 2);
+ await expect(page.getByTestId("self-serve-nav-next")).toBeEnabled();
+ expect(requests.departments[0]?.url.searchParams.get("include_lanes")).toBe("true");
+
+ await context.close();
+ });
+});