Align frontend smoke tests with current route permissions

Update self-serve mocked customer permissions for the guarded wash routes and replace removed replication-management E2E coverage with dashboard redirect assertions.
This commit is contained in:
Jeppe Bundgaard
2026-07-13 16:41:08 +02:00
parent 4452bd4088
commit dfe3163692
3 changed files with 95 additions and 800 deletions
+64 -55
View File
@@ -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");
+18 -738
View File
@@ -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 }) => {
+13 -7
View File
@@ -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,
});