diff --git a/tests/e2e/self-serve-wash.spec.js b/tests/e2e/self-serve-wash.spec.js index 8e0c3168..fd4ee3d3 100644 --- a/tests/e2e/self-serve-wash.spec.js +++ b/tests/e2e/self-serve-wash.spec.js @@ -4,6 +4,12 @@ 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 = () => { @@ -27,7 +33,10 @@ async function primeSession(page, { token, permissions, sessionData = {} }) { await seedAuthenticatedState(page, token); } -async function mockSelectedSubuserGrant(page, { customerNumber = 12345679, permissions = ["SELFSERVE_LIST"] } = {}) { +async function mockSelectedSubuserGrant( + page, + { customerNumber = 12345679, permissions = ["SELFSERVE_LIST", "SELFSERVE_ADD"] } = {} +) { await page.route("**/subusers/me", async (route) => { await route.fulfill({ status: 200, @@ -243,7 +252,7 @@ test.describe("Self-serve wash", () => { }) => { const api = await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679, }, @@ -252,7 +261,7 @@ test.describe("Self-serve wash", () => { api.selfServe.commandResponseDelayMs = [500, 0]; await primeSession(page, { token: "self-serve-user-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash"); @@ -350,7 +359,7 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679, }, @@ -358,7 +367,7 @@ test.describe("Self-serve wash", () => { }); await primeSession(page, { token: "self-serve-denied-geolocation-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start", { waitUntil: "domcontentloaded" }); @@ -380,7 +389,7 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679, }, @@ -426,7 +435,7 @@ test.describe("Self-serve wash", () => { }); await primeSession(page, { token: "self-serve-active-server-wash-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -533,7 +542,7 @@ test.describe("Self-serve wash", () => { const requests = captureSelfServeGatewayRequests(page); const api = await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: true, }); api.selfServe.answerResponseDelayMs = 1000; @@ -554,7 +563,7 @@ test.describe("Self-serve wash", () => { }; await primeSession(page, { token: "self-serve-empty-questions-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -580,7 +589,7 @@ test.describe("Self-serve wash", () => { test("start route reviews prefilled answered questions before lane confirmation", async ({ page }) => { const api = await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: true, }); const answeredQuestion = { @@ -604,7 +613,7 @@ test.describe("Self-serve wash", () => { }; await primeSession(page, { token: "self-serve-prefilled-questions-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start", { waitUntil: "domcontentloaded" }); @@ -626,7 +635,7 @@ test.describe("Self-serve wash", () => { test("vehicle next button is disabled while self-serve data loads", async ({ page }) => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: { previewResponseDelayMs: 1000, summaryResponseDelayMs: 1000, @@ -634,7 +643,7 @@ test.describe("Self-serve wash", () => { }); await primeSession(page, { token: "self-serve-next-loading-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -670,12 +679,12 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: true, }); await primeSession(page, { token: "self-serve-direct-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -725,7 +734,7 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: { departments: [ { @@ -789,7 +798,7 @@ test.describe("Self-serve wash", () => { }); await primeSession(page, { token: "self-serve-disabled-lane-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -811,7 +820,7 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: { departments: [ { @@ -841,7 +850,7 @@ test.describe("Self-serve wash", () => { }); await primeSession(page, { token: "self-serve-unavailable-lane-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -862,7 +871,7 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: { departments: [ { @@ -927,7 +936,7 @@ test.describe("Self-serve wash", () => { }); await primeSession(page, { token: "self-serve-stale-unavailable-lane-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -957,7 +966,7 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679, runtime_config: { @@ -970,7 +979,7 @@ test.describe("Self-serve wash", () => { }); await primeSession(page, { token: "self-serve-customer-number-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1007,7 +1016,7 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: null, }, @@ -1016,7 +1025,7 @@ test.describe("Self-serve wash", () => { await mockSelectedSubuserGrant(page); await primeSession(page, { token: "self-serve-subuser-grant-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1048,7 +1057,7 @@ test.describe("Self-serve wash", () => { const api = await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679, }, @@ -1085,7 +1094,7 @@ test.describe("Self-serve wash", () => { }; await primeSession(page, { token: "self-serve-start-retry-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1114,7 +1123,7 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: { commandResponses: [ { @@ -1147,7 +1156,7 @@ test.describe("Self-serve wash", () => { }); await primeSession(page, { token: "self-serve-stop-failure-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1181,7 +1190,7 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: { commandResponseDelayMs: [500, 0], commandResponses: [ @@ -1196,7 +1205,7 @@ test.describe("Self-serve wash", () => { }); await primeSession(page, { token: "self-serve-stop-already-ended-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1228,12 +1237,12 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: true, }); await primeSession(page, { token: "self-serve-edge-readonly-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1280,7 +1289,7 @@ test.describe("Self-serve wash", () => { const api = await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: true, }); api.selfServe.answerResponseDelayMs = 1000; @@ -1305,7 +1314,7 @@ test.describe("Self-serve wash", () => { }; await primeSession(page, { token: "self-serve-background-answer-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1340,7 +1349,7 @@ test.describe("Self-serve wash", () => { test("question cards keep their position when answer refreshes return reordered questions", async ({ page }) => { const api = await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: true, }); api.selfServe.answerResponseDelayMs = 1000; @@ -1361,7 +1370,7 @@ test.describe("Self-serve wash", () => { await primeSession(page, { token: "self-serve-stable-question-order-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1435,7 +1444,7 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: { laneAllowedServiceResponses: [ { @@ -1455,7 +1464,7 @@ test.describe("Self-serve wash", () => { }); await primeSession(page, { token: "self-serve-edge-services-retry-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1488,7 +1497,7 @@ test.describe("Self-serve wash", () => { const api = await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: { laneAllowedServices: [], }, @@ -1506,7 +1515,7 @@ test.describe("Self-serve wash", () => { } await primeSession(page, { token: "self-serve-machine-globally-disabled-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1545,7 +1554,7 @@ test.describe("Self-serve wash", () => { const api = await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: true, }); api.selfServe.dynamicImageDelayMs = [750, 700, 700]; @@ -1581,7 +1590,7 @@ test.describe("Self-serve wash", () => { api.selfServe.summaryByKey["7:AB12345"] = answeredSummary; await primeSession(page, { token: "self-serve-tasks-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start", { waitUntil: "domcontentloaded" }); @@ -1657,7 +1666,7 @@ test.describe("Self-serve wash", () => { const api = await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: true, }); const answeredSummary = api.selfServe.answerResponseByKey["7:AB12345:11:true"]; @@ -1704,7 +1713,7 @@ test.describe("Self-serve wash", () => { await primeSession(page, { token: "self-serve-machine-start-stale-summary-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1758,7 +1767,7 @@ test.describe("Self-serve wash", () => { const api = await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: true, }); const answeredSummary = api.selfServe.answerResponseByKey["7:AB12345:11:true"]; @@ -1789,7 +1798,7 @@ test.describe("Self-serve wash", () => { api.selfServe.summaryByKey["7:AB12345"] = answeredSummary; await primeSession(page, { token: "self-serve-manual-task-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1821,7 +1830,7 @@ test.describe("Self-serve wash", () => { const api = await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: true, }); const buttonTask = { @@ -1871,7 +1880,7 @@ test.describe("Self-serve wash", () => { api.selfServe.summaryByKey["7:AB12345"] = blockedSummary; await primeSession(page, { token: "self-serve-machine-blocked-task-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1899,12 +1908,12 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: true, }); await primeSession(page, { token: "self-serve-property-gates-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -1934,12 +1943,12 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, selfServe: true, }); await primeSession(page, { token: "self-serve-mobile-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); @@ -2000,7 +2009,7 @@ test.describe("Self-serve wash", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679, }, @@ -2025,7 +2034,7 @@ test.describe("Self-serve wash", () => { }); await primeSession(page, { token: "self-serve-polling-completed-token", - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, }); await page.goto("/user/wash/start"); diff --git a/tests/e2e/superuser-system-status.smoke.spec.js b/tests/e2e/superuser-system-status.smoke.spec.js index 6d7e969b..65bb7f15 100644 --- a/tests/e2e/superuser-system-status.smoke.spec.js +++ b/tests/e2e/superuser-system-status.smoke.spec.js @@ -25,44 +25,6 @@ async function bootSuperuser(page, { permissions = ["superuser", "user"], edgeGa await primeSuperuserSession(page); } -async function waitForLayoutFrame(page) { - await page.evaluate( - () => - new Promise((resolve) => { - requestAnimationFrame(() => requestAnimationFrame(resolve)); - }) - ); -} - -async function getActionMenuVerticalPlacement(trigger, menu) { - const [triggerBox, menuBox] = await Promise.all([trigger.boundingBox(), menu.boundingBox()]); - - expect(triggerBox).not.toBeNull(); - expect(menuBox).not.toBeNull(); - - const belowGap = menuBox.y - (triggerBox.y + triggerBox.height); - const aboveGap = triggerBox.y - (menuBox.y + menuBox.height); - const isBelowButton = belowGap >= -2 && belowGap <= 16; - const isAboveButton = aboveGap >= -2 && aboveGap <= 16; - - expect(isBelowButton || isAboveButton).toBe(true); - expect(menuBox.y >= triggerBox.y + triggerBox.height - 2 || menuBox.y + menuBox.height <= triggerBox.y + 2).toBe( - true - ); - - return isBelowButton ? "below" : "above"; -} - -async function getSettledActionMenuVerticalPlacement(page, trigger, menu) { - await waitForLayoutFrame(page); - const firstPlacement = await getActionMenuVerticalPlacement(trigger, menu); - await waitForLayoutFrame(page); - const secondPlacement = await getActionMenuVerticalPlacement(trigger, menu); - expect(secondPlacement).toBe(firstPlacement); - - return secondPlacement; -} - async function installSystemStatusMock(page, snapshot) { await page.route(/\/superuser\/system\/status(?:\?.*)?$/, async (route) => { await route.fulfill( @@ -76,531 +38,6 @@ async function installSystemStatusMock(page, snapshot) { }); } -async function installReplicationMock(page) { - const requests = { - composeKinds: [], - credentialTestKinds: [], - addKinds: [], - provisionKinds: [], - hostTestKinds: [], - }; - - await page.route(/\/superuser\/replication\/(databases|redis|minio)\/\d+\/test$/, async (route) => { - const match = route - .request() - .url() - .match(/\/superuser\/replication\/(databases|redis|minio)\//); - const pathKind = match?.[1] || "databases"; - requests.hostTestKinds.push(pathKind === "databases" ? "database" : pathKind); - await route.fulfill( - json({ - success: true, - data: { - ok: true, - status: { status: "ok", replication_percent: 100, blockers: [] }, - }, - meta: {}, - includes: {}, - }) - ); - }); - - await page.route(/\/superuser\/replication\/databases\/77\/provision$/, async (route) => { - requests.provisionKinds.push("database"); - await route.fulfill( - json({ - success: true, - data: { - ok: true, - message: "MariaDB replication was configured with GTID slave_pos.", - replication_percent: 100, - blockers: [], - host: { id: 77, kind: "database", label: "mariadb-replica-1", role: "replica" }, - }, - meta: {}, - includes: {}, - }) - ); - }); - - await page.route(/\/superuser\/replication\/redis\/88\/provision$/, async (route) => { - requests.provisionKinds.push("redis"); - await route.fulfill( - json({ - success: true, - data: { - ok: true, - message: "Redis replication was configured.", - replication_percent: 100, - blockers: [], - host: { id: 88, kind: "redis", label: "redis-replica-1", role: "replica" }, - }, - meta: {}, - includes: {}, - }) - ); - }); - - await page.route(/\/superuser\/replication\/minio\/99\/provision$/, async (route) => { - requests.provisionKinds.push("minio"); - await route.fulfill( - json({ - success: true, - data: { - ok: true, - message: "MinIO bucket replication was configured.", - replication_percent: 100, - blockers: [], - host: { id: 99, kind: "minio", label: "minio-replica-1", role: "replica" }, - }, - meta: {}, - includes: {}, - }) - ); - }); - - await page.route(/\/superuser\/replication\/databases$/, async (route) => { - requests.addKinds.push("database"); - await route.fulfill( - json( - { - success: true, - data: { - id: 77, - kind: "database", - label: "mariadb-replica-1", - host: "db.example.test", - port: 3307, - role: "replica", - status: "unknown", - replication_percent: 0, - }, - meta: {}, - includes: {}, - }, - 201 - ) - ); - }); - - await page.route(/\/superuser\/replication\/redis$/, async (route) => { - requests.addKinds.push("redis"); - await route.fulfill( - json( - { - success: true, - data: { - id: 88, - kind: "redis", - label: "redis-replica-1", - host: "redis-replica.example.test", - port: 6380, - role: "replica", - status: "unknown", - replication_percent: 0, - }, - meta: {}, - includes: {}, - }, - 201 - ) - ); - }); - - await page.route(/\/superuser\/replication\/minio$/, async (route) => { - requests.addKinds.push("minio"); - await route.fulfill( - json( - { - success: true, - data: { - id: 99, - kind: "minio", - label: "minio-replica-1", - host: "minio-replica.example.test", - port: 9010, - role: "replica", - status: "unknown", - replication_percent: 0, - }, - meta: {}, - includes: {}, - }, - 201 - ) - ); - }); - - await page.route(/\/superuser\/replication\/compose-template$/, async (route) => { - const payload = JSON.parse(route.request().postData() || "{}"); - requests.composeKinds.push(payload.kind || "database"); - if (payload.kind === "minio") { - await route.fulfill( - json({ - success: true, - data: { - kind: "minio", - engine: "minio", - role: "replica", - service_name: "minio-replica-1", - host_port: 9010, - console_port: 9011, - compose: - 'services:\n minio-replica-1:\n image: "minio/minio:latest"\n command:\n - server\n - /data\n minio-replica-1-setup:\n image: "minio/mc:latest"\n', - env: "MINIO_ROOT_USER=twminio0123456789abcdef01234567\nMINIO_ROOT_PASSWORD=generated-minio-secret\nMINIO_SERVER_URL=http://minio-replica.example.test:9010\nMINIO_BROWSER_REDIRECT_URL=http://minio-replica.example.test:9011\nMINIO_BUCKETS=attachments,uploads\nMINIO_PRIMARY_ENDPOINT=https://minio-primary.internal:9000\nMINIO_PRIMARY_ACCESS_KEY=primary-access\nMINIO_PRIMARY_SECRET_KEY=primary-secret\n", - credentials: { - label: "minio-replica-1", - host: "", - port: 9010, - scheme: "http", - endpoint: "", - buckets: ["attachments", "uploads"], - console_port: 9011, - username: "twminio0123456789abcdef01234567", - password: "generated-minio-secret", - space_headroom_percent: 20, - }, - steps: [], - }, - meta: {}, - includes: {}, - }) - ); - return; - } - if (payload.kind === "redis") { - await route.fulfill( - json({ - success: true, - data: { - kind: "redis", - engine: "redis", - role: "replica", - service_name: "redis-replica-1", - host_port: 6380, - compose: - 'services:\n redis-replica-1:\n image: "redis:7"\n command:\n - /bin/sh\n - -ec\n - |\n if [ ! -f /data/redis.conf ]; then\n echo "replicaof $$REDIS_PRIMARY_HOST $${REDIS_PRIMARY_PORT:-6379}" > /data/redis.conf\n fi\n exec redis-server /data/redis.conf\n', - env: "REDIS_PASSWORD=generated-redis-password\nREDIS_PRIMARY_HOST=redis-primary\nREDIS_PRIMARY_PORT=6379\nREDIS_PRIMARY_PASSWORD=\nREDIS_PRIMARY_USERNAME=\n", - credentials: { - label: "redis-replica-1", - host: "", - port: 6380, - database: 0, - username: "", - password: "generated-redis-password", - }, - steps: [], - }, - meta: {}, - includes: {}, - }) - ); - return; - } - - await route.fulfill( - json({ - success: true, - data: { - kind: "database", - engine: "mariadb", - role: "replica", - service_name: "mariadb-replica-1", - host_port: 3307, - server_id: 2, - compose: - 'services:\n mariadb-replica-1:\n image: "mariadb:11"\n command:\n - "--log-bin=/var/lib/mysql/mariadb-bin"\n mariadb-replica-1-seed:\n image: "mariadb:11"\n', - env: "MARIADB_ROOT_PASSWORD=generated-root-password\nMARIADB_PASSWORD=generated-app-password\nMARIADB_PRIMARY_ADMIN_PASSWORD=\n", - seed_command: - "mariadb-dump --gtid --master-data=2 --databases nnks_db | mariadb --host=db.example.test --port=3307", - credentials: { - label: "mariadb-replica-1", - host: "", - port: 3307, - database: "nnks_db", - username: "nnks_db_user", - password: "generated-app-password", - admin_username: "root", - admin_password: "generated-root-password", - replication_username: "replication", - replication_password: "generated-replication-password", - ssl_mode: "DISABLED", - allow_preseeded_replica: true, - }, - steps: [], - }, - meta: {}, - includes: {}, - }) - ); - }); - - await page.route(/\/superuser\/replication\/test-credentials$/, async (route) => { - const payload = JSON.parse(route.request().postData() || "{}"); - const kind = payload.kind || "database"; - requests.credentialTestKinds.push(kind); - await route.fulfill( - json({ - success: true, - data: { - ok: true, - host: { - id: 0, - kind, - label: kind === "redis" ? "redis-replica-1" : kind === "minio" ? "minio-replica-1" : "mariadb-replica-1", - host: - kind === "redis" - ? "redis-replica.example.test" - : kind === "minio" - ? "minio-replica.example.test" - : "db.example.test", - port: kind === "redis" ? 6380 : kind === "minio" ? 9010 : 3307, - }, - status: { status: "ok", replication_percent: 100, blockers: [] }, - }, - meta: {}, - includes: {}, - }) - ); - }); - - await page.route(/\/superuser\/replication(?:\?.*)?$/, async (route) => { - await route.fulfill( - json({ - success: true, - data: { - generated_at: "2026-04-08T08:45:00.000Z", - database: { - primary: { - id: 1, - kind: "database", - label: "mysql-primary", - host: "mysql-primary", - port: 3306, - role: "primary", - status: "ok", - replication_percent: 100, - }, - hosts: [ - { - id: 1, - kind: "database", - label: "mysql-primary", - host: "mysql-primary", - port: 3306, - role: "primary", - status: "ok", - replication_percent: 100, - last_status: { blockers: [] }, - }, - { - id: 2, - kind: "database", - label: "mysql-replica-1", - host: "mysql-replica-1", - port: 3306, - role: "replica", - status: "provisioning", - replication_percent: 42.5, - last_checked_at: "2026-04-08T08:45:00.000Z", - last_status: { - blockers: [ - "Replica seed includes data for schema-only tables: nnks_db.edge_gateway_log_entries. Re-run provisioning to rebuild the replica without operational log data.", - ], - replication_percent: 42.5, - status: "provisioning", - }, - active_operation: { - id: 99, - operation: "provision", - status: "running", - progress_percent: 42.5, - message: "Copying replica data for bookings.", - }, - }, - { - id: 7, - kind: "database", - label: "mysql-replica-caught-up", - host: "mysql-replica-caught-up", - port: 3306, - role: "replica", - status: "ok", - replication_percent: 100, - last_checked_at: "2026-04-08T08:45:00.000Z", - last_status: { blockers: [], replication_percent: 100, status: "ok" }, - }, - ], - replication: { - status: "degraded", - min_percent: 42.5, - average_percent: 42.5, - replicas: [ - { id: 2, label: "mysql-replica-1", replication_percent: 42.5 }, - { id: 7, label: "mysql-replica-caught-up", replication_percent: 100 }, - ], - blockers: [], - }, - }, - redis: { - primary: { - id: 3, - kind: "redis", - label: "redis-primary", - host: "redis-primary", - port: 6379, - role: "primary", - status: "ok", - replication_percent: 100, - }, - hosts: [ - { - id: 3, - kind: "redis", - label: "redis-primary", - host: "redis-primary", - port: 6379, - role: "primary", - status: "ok", - replication_percent: 100, - last_status: { blockers: [] }, - }, - { - id: 4, - kind: "redis", - label: "redis-replica-1", - host: "redis-replica-1", - port: 6379, - role: "replica", - status: "degraded", - replication_percent: 81.2, - last_checked_at: "2026-04-08T08:45:00.000Z", - last_status: { blockers: ["Redis replica link to primary is not up."], replication_percent: 81.2 }, - }, - ], - replication: { - status: "degraded", - min_percent: 81.2, - average_percent: 81.2, - replicas: [{ id: 4, label: "redis-replica-1", replication_percent: 81.2 }], - blockers: ["Redis replica link to primary is not up."], - }, - }, - minio: { - primary: { - id: 5, - kind: "minio", - label: "minio-primary", - host: "minio-primary", - port: 9000, - endpoint: "http://minio-primary:9000", - role: "primary", - status: "ok", - replication_percent: 100, - }, - hosts: [ - { - id: 5, - kind: "minio", - label: "minio-primary", - host: "minio-primary", - port: 9000, - endpoint: "http://minio-primary:9000", - role: "primary", - status: "ok", - replication_percent: 100, - last_status: { blockers: [] }, - }, - { - id: 6, - kind: "minio", - label: "minio-replica-1", - host: "minio-replica-1", - port: 9010, - endpoint: "http://minio-replica-1:9000", - role: "replica", - status: "ok", - replication_percent: 67.4, - last_checked_at: "2026-04-08T08:45:00.000Z", - last_status: { - status: "ok", - blockers: ["MinIO replica has not caught up."], - replication_percent: 67.4, - }, - }, - ], - replication: { - status: "degraded", - min_percent: 67.4, - average_percent: 67.4, - replicas: [{ id: 6, label: "minio-replica-1", replication_percent: 67.4 }], - blockers: ["MinIO replica has not caught up."], - }, - }, - write_freeze: {}, - }, - meta: {}, - includes: {}, - }) - ); - }); - - return requests; -} - -function createGateway(id, overrides = {}) { - return { - id, - label: `Gateway ${id}`, - hostname: `gateway-${id}.truckwash.test`, - department_id: 11, - status: "ONLINE", - discovery_status: "READY", - last_heartbeat_at: "2026-04-08T09:00:00.000Z", - active_operation: null, - diagnostics: [], - error_state: null, - ...overrides, - }; -} - -function createGatewayFleetUsage(rows) { - return { - gateways: { - total: rows.length, - online: rows.filter((gateway) => gateway.status === "ONLINE").length, - degraded: rows.filter((gateway) => gateway.status === "DEGRADED").length, - offline: rows.filter((gateway) => gateway.status === "OFFLINE").length, - }, - }; -} - -async function installGatewayFleetMock(page, { rows, departments }) { - await page.route(/\/edge-gateways(?:\?.*)?$/, async (route) => { - await route.fulfill( - json({ - success: true, - data: rows, - meta: { - fleet_usage: createGatewayFleetUsage(rows), - }, - includes: {}, - }) - ); - }); - - await page.route(/\/departments(?:\?.*)?$/, async (route) => { - await route.fulfill( - json({ - success: true, - data: departments, - meta: {}, - includes: {}, - }) - ); - }); -} - const degradedSnapshot = { overall_status: "degraded", generated_at: "2026-04-08T08:45:00.000Z", @@ -853,190 +290,33 @@ test.describe("Superuser system status smoke", () => { await expect(page.getByTestId("database-status-card")).toContainText("8.0.36"); }); - test("@smoke @pr superuser replication action menu keeps stable vertical placement", async ({ page }) => { + test("@smoke @pr superuser replication route redirects to the system dashboard", async ({ page }) => { await bootSuperuser(page); - await installReplicationMock(page); + await installSystemStatusMock(page, degradedSnapshot); await page.goto("/superuser/system/replication"); - await expect(page.getByTestId("replication-management-page")).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); - await expect(page.getByTestId("replication-host-database-2")).toContainText("mysql-replica-1"); - - const replicaActions = page.getByTestId("replication-actions-database-2"); - const replicaActionTrigger = replicaActions.locator(".action-settings-wheel-trigger"); - const replicaActionMenu = replicaActions.locator(".dropdown-content"); - - await expect(replicaActionTrigger).toBeVisible(); - await replicaActionTrigger.click(); - await expect(replicaActions.locator(".dropdown-item-action:visible")).toHaveCount(4); - const initialPlacement = await getSettledActionMenuVerticalPlacement(page, replicaActionTrigger, replicaActionMenu); - - await page.keyboard.press("Escape"); - await expect(replicaActionTrigger).not.toHaveClass(/action-settings-wheel-trigger--active/); - - await replicaActionTrigger.click(); - await expect(replicaActions.locator(".dropdown-item-action:visible")).toHaveCount(4); - const reopenedPlacement = await getSettledActionMenuVerticalPlacement( - page, - replicaActionTrigger, - replicaActionMenu - ); - - expect(reopenedPlacement).toBe(initialPlacement); + await expect(page).toHaveURL(/\/superuser$/); + await expect(page.getByTestId("system-status-dashboard")).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); + await expect(page.getByTestId("status-card-database-replication")).toContainText("100.0% replikering"); + await expect(page.getByTestId("status-card-minio-replication")).toContainText("67.4% replikering"); + await expect(page.locator('a[href="/superuser/system/replication"]')).toBeVisible(); + await expect(page.getByTestId("replication-management-page")).toHaveCount(0); }); - test("@smoke superuser replication management page renders topology and percentages", async ({ page }) => { + test("@smoke superuser replication dashboard link preserves infrastructure snapshot", async ({ page }) => { await bootSuperuser(page); - const replicationRequests = await installReplicationMock(page); + await installSystemStatusMock(page, degradedSnapshot); - await page.goto("/superuser/system/replication"); + await page.goto("/superuser"); + await expect(page.getByTestId("system-status-dashboard")).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); + await page.locator('a[href="/superuser/system/replication"]').click(); - await expect(page).toHaveURL(/\/superuser\/system\/replication$/); - await expect(page.getByTestId("replication-management-page")).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); - await expect(page.getByTestId("replication-summary-database")).toContainText("42.5%"); - await expect(page.getByTestId("replication-summary-redis")).toContainText("81.2%"); - await expect(page.getByTestId("replication-summary-minio")).toContainText("67.4%"); - await expect(page.getByTestId("replication-host-database-2")).toContainText("mysql-replica-1"); - await expect(page.getByTestId("replication-host-database-2")).toContainText("42.5%"); - await expect(page.getByTestId("replication-host-database-2")).toContainText("Copying replica data for bookings."); - await expect( - page.getByTestId("replication-host-database-2").getByTestId("replication-host-progress") - ).toHaveAttribute("value", /^42\.5$/); - await expect(page.getByTestId("replication-host-minio-6").getByTestId("replication-host-progress")).toHaveAttribute( - "value", - /^67\.4$/ - ); - await expect(page.getByTestId("replication-host-database-7")).toContainText("mysql-replica-caught-up"); - await expect(page.getByTestId("replication-host-database-7").getByTestId("replication-host-progress")).toHaveCount( - 0 - ); - await expect(page.getByTestId("replication-host-minio-6").locator(".status-pill")).toContainText("Nedsat"); - await expect(page.getByTestId("replication-host-redis-4")).toContainText( - "Venter på forbindelsen til den primære Redis-vært." - ); - await expect(page.getByTestId("replication-host-minio-6")).toContainText("Synkroniserer i baggrunden."); - await expect(page.getByTestId("replication-host-database-2")).toContainText( - "Replikaen indeholder data i tabeller, hvor kun skemaet skal kopieres" - ); - await expect(page.getByTestId("replication-host-database-2")).not.toContainText("Replica seed includes data"); - const primaryActions = page.getByTestId("replication-actions-database-1"); - const replicaActions = page.getByTestId("replication-actions-database-2"); - await expect(page.getByTestId("replication-host-database-2").locator(".buttons.are-small")).toHaveCount(0); - await expect(primaryActions.locator(".action-settings-wheel-trigger")).toBeVisible(); - await primaryActions.locator(".action-settings-wheel-trigger").click(); - await expect(primaryActions.locator(".dropdown-item-action:visible")).toHaveCount(2); - await expect(primaryActions.getByRole("button", { name: "Test" })).toBeVisible(); - await expect(primaryActions.getByRole("button", { name: "Omdøb" })).toBeVisible(); - await page.keyboard.press("Escape"); - await expect(replicaActions.locator(".action-settings-wheel-trigger")).toBeVisible(); - const replicaActionTrigger = replicaActions.locator(".action-settings-wheel-trigger"); - await replicaActionTrigger.click(); - await expect(replicaActions.locator(".dropdown-item-action:visible")).toHaveCount(4); - await expect(replicaActions.getByRole("button", { name: "Omdøb" })).toBeVisible(); - await getSettledActionMenuVerticalPlacement( - page, - replicaActionTrigger, - replicaActions.locator(".dropdown-content") - ); - await replicaActions.getByRole("button", { name: "Test" }).click(); - expect(replicationRequests.hostTestKinds).toContain("database"); - const addHostTabs = page.getByTestId("replication-add-host-tabs"); - await expect(addHostTabs).toContainText("Database"); - await expect(addHostTabs).toContainText("Redis"); - await expect(addHostTabs).toContainText("MinIO"); - const databasePanel = page.getByTestId("replication-add-database"); - await expect(databasePanel).toBeVisible(); - await expect(page.getByTestId("replication-compose-generator")).toContainText("Generér Docker Compose"); - await databasePanel.getByRole("button", { name: "Generér compose" }).click(); - await expect(page.getByTestId("replication-compose-result").locator("textarea").first()).toHaveValue( - /mariadb-replica-1/ - ); - await expect(page.locator('input[type="password"]').nth(0)).toHaveValue("generated-app-password"); - await expect(page.getByTestId("replication-compose-result").locator("textarea").nth(2)).toHaveValue(/mariadb-dump/); - await expect(page.getByTestId("replication-compose-result")).toContainText("Brug et unikt server-id"); - await databasePanel.getByLabel("Vært").first().fill("db.example.test"); - await databasePanel.getByRole("button", { name: "Test forbindelse" }).click(); - await expect(page.getByTestId("replication-credential-test-result")).toContainText( - "Forbindelsen blev testet korrekt." - ); - await expect(databasePanel.getByRole("button", { name: "Gem og klargør" })).toBeVisible(); - await databasePanel.getByRole("button", { name: "Gem og klargør" }).click(); - await expect(page.getByTestId("replication-add-database")).toContainText("Replikaen er allerede sikkert klargjort"); - const databaseCountsAfterDatabaseFlow = { - credentialTests: replicationRequests.credentialTestKinds.filter((kind) => kind === "database").length, - adds: replicationRequests.addKinds.filter((kind) => kind === "database").length, - provisions: replicationRequests.provisionKinds.filter((kind) => kind === "database").length, - }; - await addHostTabs.locator(".tabs").getByText("Redis", { exact: true }).click(); - const redisPanel = page.getByTestId("replication-add-redis"); - await expect(redisPanel).toBeVisible(); - await expect(page.getByTestId("replication-redis-compose-generator")).toContainText("Generér Docker Compose"); - await redisPanel.getByRole("button", { name: "Generér compose" }).click(); - await expect(page.getByTestId("replication-redis-compose-result").locator("textarea").first()).toHaveValue( - /redis-replica-1/ - ); - await expect(page.getByTestId("replication-redis-compose-result").locator("textarea").first()).toHaveValue( - /\/data\/redis\.conf/ - ); - await expect(page.getByTestId("replication-redis-compose-result").locator("textarea").nth(1)).toHaveValue( - /REDIS_PASSWORD=generated-redis-password/ - ); - await expect(redisPanel.locator('input[type="password"]')).toHaveValue("generated-redis-password"); - await redisPanel.getByLabel("Vært", { exact: true }).fill("redis-replica.example.test"); - await redisPanel.getByRole("button", { name: "Test forbindelse" }).click(); - await expect(page.getByTestId("replication-redis-credential-test-result")).toContainText( - "Forbindelsen blev testet korrekt." - ); - await expect(redisPanel.getByRole("button", { name: "Gem og klargør" })).toBeVisible(); - await redisPanel.getByRole("button", { name: "Gem og klargør" }).click(); - await addHostTabs.locator(".tabs").getByText("MinIO", { exact: true }).click(); - const minioPanel = page.getByTestId("replication-add-minio"); - await expect(minioPanel).toBeVisible(); - await expect(page.getByTestId("replication-minio-compose-generator")).toContainText("Generér Docker Compose"); - await minioPanel.getByRole("button", { name: "Generér compose" }).click(); - await expect(page.getByTestId("replication-minio-compose-result").locator("textarea").first()).toHaveValue( - /minio-replica-1/ - ); - await expect(page.getByTestId("replication-minio-compose-result").locator("textarea").nth(1)).toHaveValue( - /MINIO_ROOT_PASSWORD=generated-minio-secret/ - ); - await expect(minioPanel.locator('input[type="password"]')).toHaveValue("generated-minio-secret"); - await minioPanel.getByLabel("Vært", { exact: true }).fill("minio-replica.example.test"); - await minioPanel.getByRole("button", { name: "Test forbindelse" }).click(); - await expect(page.getByTestId("replication-minio-credential-test-result")).toContainText( - "Forbindelsen blev testet korrekt." - ); - await expect(minioPanel.getByRole("button", { name: "Gem og klargør" })).toBeVisible(); - const minioProvisionResponse = page.waitForResponse(/\/superuser\/replication\/minio\/99\/provision$/); - await minioPanel.getByRole("button", { name: "Gem og klargør" }).click(); - await minioProvisionResponse; - expect(replicationRequests.composeKinds).toContain("database"); - expect(replicationRequests.composeKinds).toContain("redis"); - expect(replicationRequests.composeKinds).toContain("minio"); - expect(replicationRequests.credentialTestKinds).toContain("database"); - expect(replicationRequests.credentialTestKinds).toContain("redis"); - expect(replicationRequests.credentialTestKinds).toContain("minio"); - expect(replicationRequests.addKinds).toContain("database"); - expect(replicationRequests.addKinds).toContain("redis"); - expect(replicationRequests.addKinds).toContain("minio"); - expect(replicationRequests.provisionKinds).toContain("database"); - expect(replicationRequests.provisionKinds).toContain("redis"); - expect(replicationRequests.provisionKinds).toContain("minio"); - expect(replicationRequests.credentialTestKinds.filter((kind) => kind === "database")).toHaveLength( - databaseCountsAfterDatabaseFlow.credentialTests - ); - expect(replicationRequests.addKinds.filter((kind) => kind === "database")).toHaveLength( - databaseCountsAfterDatabaseFlow.adds - ); - expect(replicationRequests.provisionKinds.filter((kind) => kind === "database")).toHaveLength( - databaseCountsAfterDatabaseFlow.provisions - ); - await expect(page.getByTestId("replication-host-redis-4")).toContainText( - "Redis-replikaens forbindelse til den primære vært er ikke oppe." - ); - await expect(page.getByTestId("replication-host-minio-6")).toContainText( - "MinIO-replikaen er ikke indhentet endnu." - ); + await expect(page).toHaveURL(/\/superuser$/); + await expect(page.getByTestId("system-status-dashboard")).toBeVisible({ timeout: PAGE_READY_TIMEOUT }); + await expect(page.getByTestId("system-status-panel-infrastructure")).toBeVisible(); + await expect(page.getByTestId("status-card-database")).toContainText("truckwash"); + await expect(page.getByTestId("status-card-database-replication")).toContainText("100.0% replikering"); }); test("@smoke superuser module cards keep long Danish status reasons below the header row", async ({ page }) => { diff --git a/tests/e2e/userMyWashStartFlow.spec.ts b/tests/e2e/userMyWashStartFlow.spec.ts index 5c29e622..b7570d05 100644 --- a/tests/e2e/userMyWashStartFlow.spec.ts +++ b/tests/e2e/userMyWashStartFlow.spec.ts @@ -1,6 +1,12 @@ 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 = () => { @@ -133,7 +139,7 @@ test.describe("User MyWashStart mocked flow", () => { const requests = collectSelfServeRequests(page); await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679 }, selfServe: true, }); @@ -177,7 +183,7 @@ test.describe("User MyWashStart mocked flow", () => { test("registration autocomplete does not cover vehicle type selection", async ({ page }) => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679 }, selfServe: true, }); @@ -204,7 +210,7 @@ test.describe("User MyWashStart mocked flow", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679 }, selfServe: { relayResponses: [{ success: false, data: { message: "Machine relay unavailable" } }], @@ -239,7 +245,7 @@ test.describe("User MyWashStart mocked flow", () => { const requests = collectSelfServeRequests(page); await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679 }, selfServe: { inProgressByLaneId: { @@ -301,7 +307,7 @@ test.describe("User MyWashStart mocked flow", () => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679 }, selfServe: { inProgressByLaneId: { @@ -325,7 +331,7 @@ test.describe("User MyWashStart mocked flow", () => { test("answer-sync failure shows an error and blocks progression", async ({ page }) => { await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679 }, selfServe: true, }); @@ -360,7 +366,7 @@ test.describe("User MyWashStart mocked flow", () => { const requests = collectSelfServeRequests(page); await mockApi(page, { authenticated: true, - permissions: ["user"], + permissions: CUSTOMER_SELF_SERVE_PERMISSIONS, sessionData: { customer_number: 12345679 }, selfServe: true, });