- Introduced `.prettierrc.json` to enforce consistent code formatting across the project. - Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
439 lines
15 KiB
JavaScript
439 lines
15 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
|
|
|
async function suppressVueDevtoolsOverlay(page) {
|
|
await page.addInitScript(() => {
|
|
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; }";
|
|
document.documentElement.appendChild(style);
|
|
});
|
|
}
|
|
|
|
async function primeSession(page, { token, permissions, sessionData = {} }) {
|
|
await seedAuthenticatedState(page, token);
|
|
await page.goto("/login");
|
|
await page.evaluate(
|
|
async ({ sessionToken, sessionPermissions, data }) => {
|
|
const sessionModule = await import("/src/components/session/token/SessionUser.vue");
|
|
window.localStorage.setItem("token", sessionToken);
|
|
sessionModule.SessionUser.token.value = sessionToken;
|
|
sessionModule.SessionUser.authenticated.value = true;
|
|
sessionModule.SessionUser.permissions.value = sessionPermissions;
|
|
sessionModule.SessionUser.initiated.value = true;
|
|
sessionModule.SessionUser.user.customer_number.value = data.customer_number ?? 12345;
|
|
sessionModule.SessionUser.user.display_name.value = data.display_name ?? "E2E User";
|
|
},
|
|
{
|
|
sessionToken: token,
|
|
sessionPermissions: permissions,
|
|
data: sessionData,
|
|
}
|
|
);
|
|
}
|
|
|
|
async function seedGeolocation(context) {
|
|
await context.grantPermissions(["geolocation"]);
|
|
await context.setGeolocation({
|
|
latitude: 55.6761,
|
|
longitude: 12.5683,
|
|
});
|
|
}
|
|
|
|
async function fillRegistration(page, value) {
|
|
const registration = page.locator('input[placeholder*="registreringsnummer"]').first();
|
|
await registration.fill(value);
|
|
await expect(registration).toHaveValue(value.toUpperCase());
|
|
await registration.press("Escape");
|
|
}
|
|
|
|
async function selectVehicleType(page, vehicleTypeId = 2) {
|
|
await page.getByTestId(`self-serve-vehicle-type-${vehicleTypeId}`).click();
|
|
await expect(page.getByTestId("self-serve-vehicle-selector-description")).toContainText(/Truck|Van|Car/);
|
|
}
|
|
|
|
function waitForLaneCommandRequest(page, command) {
|
|
return page.waitForRequest((request) => {
|
|
if (!request.url().includes("/modules/self-serve/lane/command")) {
|
|
return false;
|
|
}
|
|
if (request.method() !== "POST") {
|
|
return false;
|
|
}
|
|
|
|
const body = request.postDataJSON?.() || {};
|
|
return body.command === command;
|
|
});
|
|
}
|
|
|
|
function buildSavedProgress(overrides = {}) {
|
|
return {
|
|
washInProgress: false,
|
|
washLaneId: null,
|
|
washStartTime: null,
|
|
currentStep: 0,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: "12345",
|
|
answers: {},
|
|
completedTasks: {},
|
|
nearestDepartmentId: 1,
|
|
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);
|
|
}
|
|
|
|
test.describe("Self-serve wash", () => {
|
|
test.skip(
|
|
({ browserName }) => browserName !== "chromium",
|
|
"Self-serve coverage is maintained on Chromium desktop/mobile."
|
|
);
|
|
|
|
test.beforeEach(async ({ page, context }) => {
|
|
await suppressVueDevtoolsOverlay(page);
|
|
await seedGeolocation(context);
|
|
});
|
|
|
|
test("@smoke landing route sorts departments, loads vehicle selection, and restores direct wash progress", async ({
|
|
page,
|
|
}) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["user"],
|
|
selfServe: true,
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-user-token",
|
|
permissions: ["user"],
|
|
});
|
|
|
|
await page.goto("/user/wash");
|
|
|
|
await expect(page.getByTestId("self-serve-wash-home")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-home-nearest-name")).toContainText("Copenhagen");
|
|
|
|
const orderedDepartmentIds = await page
|
|
.locator('[data-testid^="self-serve-home-department-"]')
|
|
.evaluateAll((elements) => elements.map((element) => element.getAttribute("data-testid")));
|
|
expect(orderedDepartmentIds).toEqual(["self-serve-home-department-2", "self-serve-home-department-3"]);
|
|
|
|
await page.getByTestId("self-serve-home-start").click();
|
|
await expect(page).toHaveURL(/\/user\/wash\/start$/);
|
|
await expect(page.getByTestId("self-serve-department-name")).toContainText("Copenhagen");
|
|
|
|
await fillRegistration(page, "ab12345");
|
|
await selectVehicleType(page, 2);
|
|
|
|
await seedSavedProgress(page, {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 20_000,
|
|
currentStep: 4,
|
|
licensePlateInput: "ZZ00000",
|
|
answers: {
|
|
21: true,
|
|
},
|
|
});
|
|
|
|
await page.reload();
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("self-serve-nav-complete").click();
|
|
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible();
|
|
await page.getByTestId("self-serve-nav-close").click();
|
|
await expect(page.getByTestId("self-serve-completed-step")).toBeHidden();
|
|
await expect(page.getByTestId("self-serve-nav-complete")).toBeVisible();
|
|
});
|
|
|
|
test("superuser can force a department and clear the override", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["user", "superuser"],
|
|
selfServe: true,
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-superuser-token",
|
|
permissions: ["user", "superuser"],
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
|
|
await expect(page.getByTestId("self-serve-department-name")).toContainText("Copenhagen");
|
|
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("Copenhagen");
|
|
});
|
|
|
|
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: "12345",
|
|
savedAt: Date.now() - 3 * 60 * 60 * 1000,
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["user"],
|
|
selfServe: true,
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-direct-token",
|
|
permissions: ["user"],
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
|
|
await expect(page.getByTestId("self-serve-vehicle-step")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-nav-complete")).toBeHidden();
|
|
await expect.poll(async () => page.evaluate(() => window.localStorage.getItem("mywash_progress_v6"))).toBeNull();
|
|
|
|
await seedSavedProgress(page, {
|
|
currentStep: 2,
|
|
licensePlateInput: "ZZ00000",
|
|
radioLaneOption: 8,
|
|
radioWashType: "Machine",
|
|
});
|
|
await page.reload();
|
|
|
|
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByRole("radio", { name: /Maskine/i })).toBeDisabled();
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled();
|
|
|
|
await page.getByTestId("self-serve-lane-option-7").click();
|
|
await page.getByTestId("self-serve-wash-type-manual").click();
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("self-serve-nav-confirm").click();
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
test("tasks flow renders dynamic image, requires completion, and restores after reload", async ({ page }) => {
|
|
await seedSavedProgress(page, {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 30_000,
|
|
currentStep: 3,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
customerNumberInput: "12345",
|
|
completedTasks: {},
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["user"],
|
|
selfServe: {
|
|
previewByKey: {
|
|
"7:AB12345": {
|
|
allowed: true,
|
|
machine_available: true,
|
|
lane: { id: 7, name: "7" },
|
|
session: { id: 501, status: "IN_PROGRESS", allowed: true },
|
|
questions: [
|
|
{
|
|
id: 11,
|
|
question: "Is the tarp removed?",
|
|
description: "Required before machine wash.",
|
|
answer: true,
|
|
order_priority: 1,
|
|
},
|
|
],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [
|
|
{
|
|
id: 9001,
|
|
task: "Prepare the truck",
|
|
description: "Complete the pre-wash checks.",
|
|
order_priority: 1,
|
|
services: ["MACHINE"],
|
|
buttons: [1],
|
|
},
|
|
],
|
|
allowed_services: ["MACHINE"],
|
|
},
|
|
},
|
|
summaryBySessionId: {
|
|
501: {
|
|
session: { id: 501, status: "IN_PROGRESS", allowed: true },
|
|
lane: { id: 7, name: "7" },
|
|
questions: [
|
|
{
|
|
id: 11,
|
|
question: "Is the tarp removed?",
|
|
description: "Required before machine wash.",
|
|
answer: true,
|
|
order_priority: 1,
|
|
},
|
|
],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [
|
|
{
|
|
id: 9001,
|
|
task: "Prepare the truck",
|
|
description: "Complete the pre-wash checks.",
|
|
order_priority: 1,
|
|
services: ["MACHINE"],
|
|
buttons: [1],
|
|
},
|
|
],
|
|
events: [{ id: 3, type: "QUESTION_ANSWERED", created_at: "2026-01-01T10:01:00.000Z" }],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-tasks-token",
|
|
permissions: ["user"],
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
|
|
await expect(page.getByTestId("self-serve-tasks-step")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("self-serve-dynamic-image")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-task-9001-attachment-302")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled();
|
|
|
|
await page.getByTestId("self-serve-task-9001-toggle").click();
|
|
await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled();
|
|
await page.getByTestId("self-serve-nav-confirm").click();
|
|
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible();
|
|
await seedSavedProgress(page, {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 45_000,
|
|
currentStep: 4,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
customerNumberInput: "12345",
|
|
});
|
|
await page.reload();
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
test("in-progress flow opens property access and exit gates via lane command endpoint", async ({ page }) => {
|
|
await seedSavedProgress(page, {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 30_000,
|
|
currentStep: 4,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioLaneOption: 7,
|
|
customerNumberInput: "12345",
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["user"],
|
|
selfServe: true,
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-property-gates-token",
|
|
permissions: ["user"],
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
|
|
|
|
const accessCommandRequestPromise = waitForLaneCommandRequest(page, "OPEN_PROPERTY_ACCESS_GATE");
|
|
await page.getByTestId("self-serve-nav-open-property-access-gate").click();
|
|
const accessCommandRequest = await accessCommandRequestPromise;
|
|
expect(accessCommandRequest.postDataJSON?.()).toMatchObject({
|
|
lane_id: 7,
|
|
command: "OPEN_PROPERTY_ACCESS_GATE",
|
|
});
|
|
|
|
const exitCommandRequestPromise = waitForLaneCommandRequest(page, "OPEN_PROPERTY_EXIT_GATE");
|
|
await page.getByTestId("self-serve-nav-open-property-exit-gate").click();
|
|
const exitCommandRequest = await exitCommandRequestPromise;
|
|
expect(exitCommandRequest.postDataJSON?.()).toMatchObject({
|
|
lane_id: 7,
|
|
command: "OPEN_PROPERTY_EXIT_GATE",
|
|
});
|
|
|
|
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible();
|
|
});
|
|
|
|
test("mobile layout keeps primary controls usable", async ({ page }, testInfo) => {
|
|
test.skip(!testInfo.project.name.includes("mobile"), "Mobile sanity runs on the mobile project only.");
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["user"],
|
|
selfServe: true,
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-mobile-token",
|
|
permissions: ["user"],
|
|
});
|
|
|
|
await page.goto("/user/wash/start");
|
|
|
|
await expect(page.getByTestId("self-serve-start-page")).toBeVisible();
|
|
await fillRegistration(page, "ab12345");
|
|
await selectVehicleType(page, 2);
|
|
await expect(page.getByTestId("self-serve-nav-next")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-vehicle-selector-description")).toContainText("Truck");
|
|
});
|
|
|
|
test("admin preview modal reuses shared question/task rendering", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["admin", "department_access_1"],
|
|
selfServe: true,
|
|
});
|
|
await primeSession(page, {
|
|
token: "self-serve-admin-token",
|
|
permissions: ["admin", "department_access_1"],
|
|
});
|
|
|
|
await page.goto("/admin/1/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("QUESTION_ANSWERED")).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
});
|