- Introduced `.prettierrc.json` to enforce consistent code formatting across the project. - Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
336 lines
10 KiB
JavaScript
336 lines
10 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);
|
|
await page.goto("/login");
|
|
await page.evaluate(async (sessionToken) => {
|
|
const sessionModule = await import("/src/components/session/token/SessionUser.vue");
|
|
window.localStorage.setItem("token", sessionToken);
|
|
sessionModule.SessionUser.token.value = sessionToken;
|
|
sessionModule.SessionUser.authenticated.value = true;
|
|
sessionModule.SessionUser.permissions.value = ["superuser", "user"];
|
|
sessionModule.SessionUser.initiated.value = true;
|
|
}, token);
|
|
}
|
|
|
|
async function installSystemStatusMock(page, snapshot) {
|
|
await page.route(/\/superuser\/system\/status(?:\?.*)?$/, async (route) => {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: snapshot,
|
|
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",
|
|
},
|
|
redis: {
|
|
status: "down",
|
|
latency_ms: 12.8,
|
|
database: 0,
|
|
checked_at: "2026-04-08T08:45:00.000Z",
|
|
error: "Connection refused",
|
|
},
|
|
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.beforeEach(async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
});
|
|
|
|
test("@smoke superuser status dashboard renders Danish translations for degraded infrastructure and recent sessions", async ({
|
|
page,
|
|
}) => {
|
|
await installSystemStatusMock(page, degradedSnapshot);
|
|
|
|
await page.goto("/superuser");
|
|
|
|
await expect(page).toHaveURL(/\/superuser$/);
|
|
await expect(page.getByTestId("system-status-dashboard")).toBeVisible();
|
|
await expect(page.getByTestId("status-card-database")).toContainText("truckwash");
|
|
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 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 module cards keep long Danish status reasons below the header row", async ({ 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 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);
|
|
});
|
|
});
|