Files
pleno-vue/tests/e2e/superuser-system-status.smoke.spec.js
T

585 lines
20 KiB
JavaScript

import { expect, test } from "@playwright/test";
import { mockApi, seedAuthenticatedState } from "./support/network.js";
const PAGE_READY_TIMEOUT = 60_000;
function json(body, status = 200) {
return {
status,
contentType: "application/json",
body: JSON.stringify(body),
};
}
async function primeSuperuserSession(page) {
const token = "superuser-system-status-token";
await seedAuthenticatedState(page, token);
}
async function bootSuperuser(page, { permissions = ["superuser", "user"], edgeGateways = false } = {}) {
await mockApi(page, {
authenticated: true,
permissions,
edgeGateways,
});
await primeSuperuserSession(page);
}
async function installSystemStatusMock(page, snapshot) {
await page.route(/\/superuser\/system\/status(?:\?.*)?$/, async (route) => {
await route.fulfill(
json({
success: true,
data: snapshot,
meta: {},
includes: {},
})
);
});
}
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",
refresh_after_seconds: 30,
runtime: {
cpu: { status: "ok", usage_percent: 34.5, source: "proc_stat", checked_at: "2026-04-08T08:45:00.000Z" },
memory: {
status: "degraded",
usage_percent: 91.3,
used_bytes: 6442450944,
total_bytes: 8589934592,
source: "cgroup_v2",
checked_at: "2026-04-08T08:45:00.000Z",
},
disk: {
status: "ok",
usage_percent: 56.2,
used_bytes: 53687091200,
total_bytes: 107374182400,
path: "/var/www/html",
checked_at: "2026-04-08T08:45:00.000Z",
},
},
dependencies: {
database: {
status: "ok",
latency_ms: 4.2,
database: "truckwash",
server_version: "8.0.36",
checked_at: "2026-04-08T08:45:00.000Z",
replication: {
status: "ok",
min_percent: 100,
average_percent: 100,
replicas: [{ id: 2, label: "mysql-replica-1", replication_percent: 100 }],
blockers: [],
},
},
redis: {
status: "down",
latency_ms: 12.8,
database: 0,
checked_at: "2026-04-08T08:45:00.000Z",
error: "Connection refused",
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: {
status: "degraded",
latency_ms: 7.1,
endpoint: "http://minio:9000",
checked_at: "2026-04-08T08:45:00.000Z",
buckets: [
{ name: "attachments", status: "ok" },
{ name: "backups", status: "down", error: "Missing bucket" },
],
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."],
},
},
},
modules: [
{
key: "openai",
enabled: true,
configured: true,
probe_supported: true,
status: "ok",
status_reason: "OpenAI API connectivity confirmed.",
status_reason_key: "http_ok",
status_reason_params: { label: "OpenAI API" },
checked_at: "2026-04-08T08:45:00.000Z",
},
{
key: "bird",
enabled: true,
configured: true,
probe_supported: true,
status: "degraded",
status_reason: "Bird API returned HTTP 503.",
status_reason_key: "http_status",
status_reason_params: { label: "Bird API", httpStatus: 503 },
checked_at: "2026-04-08T08:45:00.000Z",
},
],
sessions: {
active_window_minutes: 15,
active_users: 3,
active_sessions: 4,
recent_sessions: [
{
session_kind: "user",
principal_id: 101,
display_name: "Acme Logistics",
context_label: "Customer 1001",
context_label_key: "customer_number",
context_label_params: { number: 1001 },
customer_number_context: 1001,
device_type: "desktop",
user_agent: "Mozilla/5.0",
last_route: "/superuser/vehicles",
first_seen_at: "2026-04-08T08:20:00.000Z",
last_seen_at: "2026-04-08T08:44:00.000Z",
active: true,
},
],
},
warnings: ["Redis is unavailable; module probe caching is bypassed."],
warning_entries: [
{
key: "redis_cache_bypass",
params: {},
message: "Redis is unavailable; module probe caching is bypassed.",
},
],
};
const databaseCompatibilitySnapshot = {
...degradedSnapshot,
overall_status: "ok",
runtime: {
...degradedSnapshot.runtime,
memory: {
status: "ok",
usage_percent: 48.2,
used_bytes: 4294967296,
total_bytes: 8589934592,
source: "cgroup_v2",
checked_at: "2026-04-08T08:45:00.000Z",
},
},
dependencies: {
...degradedSnapshot.dependencies,
redis: {
status: "ok",
latency_ms: 1.9,
database: 0,
checked_at: "2026-04-08T08:45:00.000Z",
},
minio: {
status: "ok",
latency_ms: 5.4,
endpoint: "http://minio:9000",
checked_at: "2026-04-08T08:45:00.000Z",
buckets: [
{ name: "attachments", status: "ok" },
{ name: "backups", status: "ok" },
],
},
},
warnings: [],
};
const longReasonSnapshot = {
...degradedSnapshot,
modules: [
{
key: "shelly",
enabled: true,
configured: true,
probe_supported: true,
status: "configured",
status_reason:
"Shelly cloud credentials are configured, but no known device id is available for a safe read-only probe.",
status_reason_key: "shelly_no_device_id",
status_reason_params: {},
checked_at: "2026-04-08T08:45:00.000Z",
},
],
};
const longTitleSnapshot = {
...degradedSnapshot,
modules: [
{
key: "licenseplaterecognizer",
enabled: true,
configured: true,
probe_supported: true,
status: "degraded",
status_reason: "License Plate Recognizer quota usage is near the limit.",
status_reason_key: "licenseplaterecognizer_quota_near_limit",
status_reason_params: {
used: "2250",
quota: "2500",
remaining: "250",
percent: "90.0",
},
checked_at: "2026-04-08T08:45:00.000Z",
usage: {
provider: "licenseplaterecognizer",
calls_used: 2250,
quota_calls: 2500,
calls_remaining: 250,
usage_percent: 90,
version: "1.54.0",
},
},
{
key: "virkdata",
enabled: true,
configured: true,
probe_supported: false,
status: "configured",
status_reason: "Configuration is present, but no safe read-only probe is available.",
status_reason_key: "safe_probe_unavailable",
status_reason_params: {},
checked_at: "2026-04-08T08:45:00.000Z",
},
],
};
test.describe("Superuser system status smoke", () => {
test.describe.configure({ timeout: 120_000 });
test("@smoke @pr superuser status dashboard renders Danish translations for degraded infrastructure and recent sessions", async ({
page,
}) => {
await bootSuperuser(page);
await installSystemStatusMock(page, degradedSnapshot);
await page.goto("/superuser");
await expect(page).toHaveURL(/\/superuser$/);
await expect(page.getByTestId("system-status-dashboard")).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
await expect(page.getByTestId("system-status-dashboard-tabs")).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
await expect(page.getByTestId("status-card-database")).toContainText("truckwash", { 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 page.getByTestId("system-status-tab-sessions").click();
await expect(page.locator("body")).toContainText("Acme Logistics");
await expect(page.locator("body")).toContainText("Kunde");
await expect(page.locator("body")).toContainText("Bruger");
await expect(page.locator("body")).toContainText("Computer");
await expect(page.locator("body")).toContainText("/superuser/vehicles");
await page.getByTestId("system-status-tab-modules").click();
await expect(page.locator("body")).toContainText("Forbindelse til OpenAI API bekræftet.");
await expect(page.locator("body")).toContainText("Redis er utilgængelig; cache til modulprober omgås.");
await expect(page.locator("body")).not.toContainText("OpenAI API connectivity confirmed.");
await expect(page.locator("body")).not.toContainText("Redis is unavailable; module probe caching is bypassed.");
await expect(page.locator("body")).not.toContainText("Customer 1001");
await expect(page.locator('a[href="/superuser/configuration/openai"]').last()).toBeVisible();
});
test("@smoke superuser database compatibility page renders shared database status snapshot", async ({ page }) => {
await bootSuperuser(page);
await installSystemStatusMock(page, databaseCompatibilitySnapshot);
await page.goto("/superuser/system/database");
await expect(page).toHaveURL(/\/superuser\/system\/database$/);
await expect(page.getByTestId("database-status-card")).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
await expect(page.getByTestId("database-status-card")).toContainText("truckwash");
await expect(page.getByTestId("database-status-card")).toContainText("8.0.36");
});
test("@smoke @pr superuser replication route redirects to the system dashboard", async ({ page }) => {
await bootSuperuser(page);
await installSystemStatusMock(page, degradedSnapshot);
await page.goto("/superuser/system/replication");
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 dashboard link preserves infrastructure snapshot", async ({ page }) => {
await bootSuperuser(page);
await installSystemStatusMock(page, degradedSnapshot);
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$/);
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 }) => {
await bootSuperuser(page);
await installSystemStatusMock(page, longReasonSnapshot);
await page.goto("/superuser");
await page.getByTestId("system-status-tab-modules").click();
const card = page.getByTestId("module-card-shelly");
const title = card.locator(".module-card__title");
const pill = card.locator(".status-pill");
const reason = page.getByTestId("module-reason-shelly");
await expect(card).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
await expect(reason).toContainText("der findes ikke et kendt device-id");
const [titleBox, pillBox, reasonBox] = await Promise.all([
title.boundingBox(),
pill.boundingBox(),
reason.boundingBox(),
]);
expect(titleBox).not.toBeNull();
expect(pillBox).not.toBeNull();
expect(reasonBox).not.toBeNull();
const headerBottom = Math.max(titleBox.y + titleBox.height, pillBox.y + pillBox.height);
expect(reasonBox.y).toBeGreaterThanOrEqual(headerBottom - 1);
});
test("@smoke superuser module headers keep long titles and status pills inside their own cards", async ({ page }) => {
await bootSuperuser(page);
await installSystemStatusMock(page, longTitleSnapshot);
await page.goto("/superuser");
await page.getByTestId("system-status-tab-modules").click();
const card = page.getByTestId("module-card-licenseplaterecognizer");
const title = card.locator(".module-card__title");
const pill = card.locator(".status-pill");
await expect(card).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
await expect(title).toContainText("License Plate Recognizer");
await expect(page.getByTestId("module-reason-licenseplaterecognizer")).toContainText(
"License Plate Recognizer-kvoten er næsten opbrugt"
);
const usage = page.getByTestId("module-usage-licenseplaterecognizer");
await expect(usage).toContainText("Kvoteudnyttelse");
await expect(usage).toContainText("90.0%");
await expect(usage).toContainText("Brugte kald");
await expect(usage).toContainText("2.250");
await expect(usage).toContainText("Kald tilbage");
await expect(usage).toContainText("250");
await expect(usage).toContainText("Version: 1.54.0");
const [cardBox, titleBox, pillBox] = await Promise.all([
card.boundingBox(),
title.boundingBox(),
pill.boundingBox(),
]);
expect(cardBox).not.toBeNull();
expect(titleBox).not.toBeNull();
expect(pillBox).not.toBeNull();
expect(titleBox.x).toBeGreaterThanOrEqual(cardBox.x - 1);
expect(titleBox.x + titleBox.width).toBeLessThanOrEqual(cardBox.x + cardBox.width + 1);
expect(pillBox.x).toBeGreaterThanOrEqual(cardBox.x - 1);
expect(pillBox.x + pillBox.width).toBeLessThanOrEqual(cardBox.x + cardBox.width + 1);
const overlapWidth = Math.max(
0,
Math.min(titleBox.x + titleBox.width, pillBox.x + pillBox.width) - Math.max(titleBox.x, pillBox.x)
);
const overlapHeight = Math.max(
0,
Math.min(titleBox.y + titleBox.height, pillBox.y + pillBox.height) - Math.max(titleBox.y, pillBox.y)
);
expect(overlapWidth * overlapHeight).toBeLessThanOrEqual(1);
});
test("@smoke superuser status dashboard renders gateway health cards and gateway links", async ({ page }) => {
const gatewayRows = [
createGateway(301, {
label: "Gateway Atlas",
department_id: 11,
status: "OFFLINE",
discovery_status: "FAILED",
last_heartbeat_at: "2026-04-08T08:00:00.000Z",
error_state: { message: "MQTT disconnected" },
}),
createGateway(302, {
label: "Gateway Bering",
department_id: 12,
status: "OFFLINE",
discovery_status: "FAILED",
last_heartbeat_at: "2026-04-08T08:05:00.000Z",
}),
createGateway(303, {
label: "Gateway Carls",
department_id: 11,
status: "OFFLINE",
discovery_status: "STALE",
last_heartbeat_at: "2026-04-08T08:10:00.000Z",
}),
createGateway(304, {
label: "Gateway Delta",
department_id: 12,
status: "DEGRADED",
discovery_status: "READY",
last_heartbeat_at: "2026-04-08T08:15:00.000Z",
active_operation: {
summary: { label: "Udruller opdatering" },
},
}),
createGateway(305, {
label: "Gateway Echo",
department_id: 11,
status: "DEGRADED",
discovery_status: "STALE",
last_heartbeat_at: "2026-04-08T08:20:00.000Z",
diagnostics: [{ message: "Discovery har ikke rapporteret i 30 minutter." }],
}),
createGateway(306, {
label: "Gateway Fjord",
department_id: 12,
status: "DEGRADED",
discovery_status: "PENDING",
last_heartbeat_at: "2026-04-08T08:25:00.000Z",
}),
createGateway(307, {
label: "",
hostname: "gw-307.truckwash.test",
department_id: 11,
status: "ONLINE",
discovery_status: "READY",
last_heartbeat_at: "2026-04-08T08:30:00.000Z",
}),
createGateway(308, {
label: "Gateway Haven",
department_id: 12,
status: "ONLINE",
discovery_status: "READY",
last_heartbeat_at: "2026-04-08T08:35:00.000Z",
}),
createGateway(309, {
label: "Gateway Ist",
department_id: 11,
status: "ONLINE",
discovery_status: "READY",
last_heartbeat_at: "2026-04-08T08:40:00.000Z",
}),
createGateway(310, {
label: "Gateway Jutland",
department_id: 12,
status: "ONLINE",
discovery_status: "READY",
last_heartbeat_at: "2026-04-08T08:45:00.000Z",
}),
];
await bootSuperuser(page, {
permissions: ["superuser", "user", "modules_shelly_config"],
edgeGateways: false,
});
await installSystemStatusMock(page, degradedSnapshot);
await installGatewayFleetMock(page, {
rows: gatewayRows,
departments: [
{ id: 11, name: "Odense" },
{ id: 12, name: "Aarhus" },
],
});
await page.goto("/superuser");
await page.getByTestId("system-status-tab-gateways").click();
await expect(page.getByTestId("system-status-gateways")).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
await expect(page.getByTestId("gateway-summary-card-total")).toContainText("10");
await expect(page.getByTestId("gateway-summary-card-online")).toContainText("4");
await expect(page.getByTestId("gateway-summary-card-degraded")).toContainText("3");
await expect(page.getByTestId("gateway-summary-card-offline")).toContainText("3");
await expect(page.locator('[data-testid="system-status-gateways"] .section-link')).toBeVisible();
const gatewayCards = page.locator('[data-testid^="gateway-card-"]');
await expect(gatewayCards).toHaveCount(8);
await expect(gatewayCards.nth(0)).toContainText("Gateway Atlas");
await expect(gatewayCards.nth(0)).toContainText("Odense");
await expect(gatewayCards.nth(0)).toContainText("MQTT disconnected");
await expect(gatewayCards.nth(3)).toContainText("Udruller opdatering");
await expect(gatewayCards.nth(4)).toContainText("Discovery har ikke rapporteret i 30 minutter.");
await expect(gatewayCards.nth(6)).toContainText("gw-307.truckwash.test");
await expect(page.locator('a[href="/superuser/selfserve/edge-agents/301/overview"]')).toBeVisible();
await expect(page.locator('[data-testid="gateway-card-309"]')).toHaveCount(0);
});
});