762 lines
25 KiB
JavaScript
762 lines
25 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
|
|
|
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: {},
|
|
})
|
|
);
|
|
});
|
|
}
|
|
|
|
async function installReplicationMock(page) {
|
|
await page.route(/\/superuser\/replication\/databases\/77\/provision$/, async (route) => {
|
|
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\/databases$/, async (route) => {
|
|
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\/compose-template$/, async (route) => {
|
|
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) => {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
ok: true,
|
|
host: { id: 0, kind: "database", label: "mariadb-replica-1", host: "db.example.test", port: 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.",
|
|
},
|
|
},
|
|
],
|
|
replication: {
|
|
status: "degraded",
|
|
min_percent: 42.5,
|
|
average_percent: 42.5,
|
|
replicas: [{ id: 2, label: "mysql-replica-1", replication_percent: 42.5 }],
|
|
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."],
|
|
},
|
|
},
|
|
write_freeze: {},
|
|
},
|
|
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" },
|
|
],
|
|
},
|
|
},
|
|
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: "ok",
|
|
status_reason: "License Plate Recognizer connectivity confirmed.",
|
|
status_reason_key: "http_ok",
|
|
status_reason_params: { label: "License Plate Recognizer" },
|
|
checked_at: "2026-04-08T08:45:00.000Z",
|
|
},
|
|
{
|
|
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("@smoke 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: 20_000 });
|
|
await expect(page.getByTestId("status-card-database")).toContainText("truckwash", { timeout: 20_000 });
|
|
await expect(page.getByTestId("status-card-database-replication")).toContainText("100.0% replikering");
|
|
await expect(page.locator("body")).toContainText("Acme Logistics");
|
|
await expect(page.locator("body")).toContainText("Kunde 1001");
|
|
await expect(page.locator("body")).toContainText("Bruger");
|
|
await expect(page.locator("body")).toContainText("Computer");
|
|
await expect(page.locator("body")).toContainText("/superuser/vehicles");
|
|
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();
|
|
await expect(page.getByTestId("database-status-card")).toContainText("truckwash");
|
|
await expect(page.getByTestId("database-status-card")).toContainText("8.0.36");
|
|
});
|
|
|
|
test("@smoke superuser replication management page renders topology and percentages", async ({ page }) => {
|
|
await bootSuperuser(page);
|
|
await installReplicationMock(page);
|
|
|
|
await page.goto("/superuser/system/replication");
|
|
|
|
await expect(page).toHaveURL(/\/superuser\/system\/replication$/);
|
|
await expect(page.getByTestId("replication-management-page")).toBeVisible();
|
|
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-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")).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");
|
|
await expect(page.getByTestId("replication-compose-generator")).toContainText("Generér Docker Compose");
|
|
await page.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 page.getByLabel("Vært").first().fill("db.example.test");
|
|
await page.getByRole("button", { name: "Test forbindelse" }).click();
|
|
await expect(page.getByTestId("replication-credential-test-result")).toContainText(
|
|
"Forbindelsen blev testet korrekt."
|
|
);
|
|
await expect(page.getByRole("button", { name: "Gem og klargør" })).toBeVisible();
|
|
await page.getByRole("button", { name: "Gem og klargør" }).click();
|
|
await expect(page.getByTestId("replication-add-database")).toContainText("Replikaen er allerede sikkert klargjort");
|
|
await expect(page.getByTestId("replication-host-redis-4")).toContainText(
|
|
"Redis-replikaens forbindelse til den primære vært er ikke oppe."
|
|
);
|
|
});
|
|
|
|
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");
|
|
|
|
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();
|
|
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");
|
|
|
|
const card = page.getByTestId("module-card-licenseplaterecognizer");
|
|
const title = card.locator(".module-card__title");
|
|
const pill = card.locator(".status-pill");
|
|
|
|
await expect(card).toBeVisible();
|
|
await expect(title).toContainText("License Plate Recognizer");
|
|
|
|
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 expect(page.getByTestId("system-status-gateways")).toBeVisible();
|
|
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);
|
|
});
|
|
});
|