Merge pull request #111 from copenhagentruckwash/create-playwright-test-suite-for-mywashstart-6chtnh
Add mocked Playwright tests for MyWashStart flow and improve answer-sync & department fallback
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
<b-message
|
||||
v-if="answerSyncError"
|
||||
type="is-danger"
|
||||
:aria-close-label="$t('common.close')"
|
||||
data-testid="self-serve-answer-sync-error"
|
||||
>
|
||||
{{ answerSyncError }}
|
||||
</b-message>
|
||||
</b-step-item>
|
||||
|
||||
<b-step-item
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { mockApi, primeMockSession } from "./support/network.js";
|
||||
|
||||
async function suppressVueDevtoolsOverlay(page) {
|
||||
await page.addInitScript(() => {
|
||||
const appendStyle = () => {
|
||||
const parent = document.documentElement || document.head || document.body;
|
||||
if (!parent) {
|
||||
document.addEventListener("DOMContentLoaded", appendStyle, { once: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent =
|
||||
"#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
|
||||
parent.appendChild(style);
|
||||
};
|
||||
|
||||
appendStyle();
|
||||
});
|
||||
}
|
||||
|
||||
async function 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user