362 lines
13 KiB
JavaScript
362 lines
13 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, primeMockSession } from "./support/network.js";
|
|
|
|
const EDGE_GATEWAY_WORKSPACE_CACHE_KEY = "truckwash.edgeGatewayWorkspace.cache.v1";
|
|
|
|
async function primeSuperuserSession(page) {
|
|
await primeMockSession(page, { token: "superuser-edge-gateway-token" });
|
|
}
|
|
|
|
async function seedEdgeGatewayWorkspaceCache(page) {
|
|
const cachedGateway = {
|
|
id: 701,
|
|
department_id: 1,
|
|
label: "Cached CPH Edge 01",
|
|
hostname: "cph-edge-01",
|
|
status: "ONLINE",
|
|
discovery_status: "READY",
|
|
department_transport_mode: "gateway",
|
|
installed_version: "php-agent-v1",
|
|
target_version: "php-agent-v1.1",
|
|
last_heartbeat_at: "2026-04-08 08:15:00",
|
|
last_seen_ip: "10.1.0.14",
|
|
inventory_summary: {
|
|
total: 1,
|
|
online: 1,
|
|
offline: 0,
|
|
},
|
|
binding_summary: {
|
|
total: 1,
|
|
fallback_overrides: 0,
|
|
},
|
|
recent_operations_summary: {
|
|
pending: 0,
|
|
in_progress: 0,
|
|
},
|
|
backlog_depth: {
|
|
commands: 0,
|
|
operations: 0,
|
|
},
|
|
channel_status: {
|
|
broker: {
|
|
connected: true,
|
|
},
|
|
},
|
|
container_health: {
|
|
state: "ONLINE",
|
|
summary: "2/2 containers healthy",
|
|
services: [
|
|
{ name: "edge-agent", status: "healthy" },
|
|
{ name: "lan-worker", status: "healthy" },
|
|
],
|
|
},
|
|
outbox_status: {
|
|
state: "IN_SYNC",
|
|
queued: 0,
|
|
summary: "Outbox is empty",
|
|
last_replayed_at: "2026-04-08 08:14:56",
|
|
},
|
|
last_sync_at: "2026-04-08 08:14:56",
|
|
update_window: "02:00-04:00",
|
|
staged_version: {
|
|
target_version: "php-agent-v1.1",
|
|
release_channel: "stable",
|
|
queued_at: "2026-04-08 01:45:00",
|
|
},
|
|
rollback_status: {
|
|
state: "IDLE",
|
|
reason: null,
|
|
rolled_back_to: null,
|
|
at: null,
|
|
},
|
|
metadata: {
|
|
broker_connected: true,
|
|
system_metrics: {
|
|
latency_ms: 184,
|
|
cpu_usage_pct: 27,
|
|
memory_usage_pct: 61,
|
|
disk_usage_pct: 58,
|
|
},
|
|
},
|
|
inventory: [{ id: 1, device_id: "shelly-plus-01", local_ip: "10.1.0.31", model: "Shelly Plus 2PM", online: true }],
|
|
bindings: [
|
|
{
|
|
id: 1,
|
|
relay_id: "M-7",
|
|
device_id: "shelly-plus-01",
|
|
local_ip: "10.1.0.31",
|
|
channel: 0,
|
|
fallback_mode: "PREFER_LOCAL",
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
audit_logs: [],
|
|
};
|
|
const cachedAt = Date.now();
|
|
const workspaceCache = {
|
|
departments: {
|
|
cachedAt,
|
|
data: [
|
|
{ id: 1, name: "Copenhagen" },
|
|
{ id: 2, name: "Odense" },
|
|
],
|
|
},
|
|
lists: {
|
|
'{"departmentId":null,"view":"summary"}': {
|
|
params: { departmentId: null, view: "summary" },
|
|
rows: [cachedGateway],
|
|
meta: {},
|
|
cachedAt,
|
|
},
|
|
},
|
|
details: {
|
|
701: {
|
|
cachedAt,
|
|
data: cachedGateway,
|
|
},
|
|
},
|
|
};
|
|
|
|
await page.addInitScript(
|
|
({ cacheKey, cache }) => {
|
|
window.localStorage.setItem(cacheKey, JSON.stringify(cache));
|
|
},
|
|
{
|
|
cacheKey: EDGE_GATEWAY_WORKSPACE_CACHE_KEY,
|
|
cache: workspaceCache,
|
|
}
|
|
);
|
|
}
|
|
|
|
test.describe("Edge gateway management smoke", () => {
|
|
test.describe.configure({ mode: "serial" });
|
|
|
|
test("@smoke shows technical request details for backend errors", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await page.route(/\/edge-gateways(?:\?.*)?$/i, async (route, request) => {
|
|
if (request.method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 500,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: false,
|
|
data: {
|
|
message: "Internal server error: SQLSTATE[42S22]: Column not found",
|
|
error_code: "EDGE_GATEWAY_VALIDATION_FAILED",
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/gateways");
|
|
|
|
await expect(page.getByTestId("gateway-error-banner")).toContainText("Column not found");
|
|
await page.getByTestId("gateway-error-details").click();
|
|
await expect(page.getByTestId("gateway-error-details")).toContainText("Request:");
|
|
await expect(page.getByTestId("gateway-error-details")).toContainText("Status: HTTP 500");
|
|
});
|
|
|
|
test("@smoke exposes a copy action for generated installer commands", async ({ page }) => {
|
|
await page.addInitScript(() => {
|
|
Object.defineProperty(navigator, "clipboard", {
|
|
configurable: true,
|
|
value: {
|
|
writeText: async (text) => {
|
|
window.__copiedInstallerCommand = text;
|
|
},
|
|
},
|
|
});
|
|
window.__copiedInstallerCommand = "";
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
edgeGateways: {
|
|
empty: true,
|
|
claimPollsRemaining: 99,
|
|
},
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/gateways");
|
|
|
|
await page.getByTestId("gateway-installer-department").selectOption("1");
|
|
await page.getByTestId("gateway-installer-label").fill("Copy Test Pi");
|
|
await page.getByTestId("gateway-installer-generate").click();
|
|
|
|
await expect(page.getByTestId("gateway-install-command")).toHaveValue(/install\.sh\?token=edge-install-token/);
|
|
await page.getByTestId("gateway-installer-copy").click();
|
|
|
|
await expect(page.locator("body")).toContainText("Installer command copied.");
|
|
await expect(page.getByTestId("gateway-installer-copy")).toContainText("Copied");
|
|
await expect
|
|
.poll(() => page.evaluate(() => window.__copiedInstallerCommand))
|
|
.toContain("install.sh?token=edge-install-token");
|
|
});
|
|
|
|
test("@smoke shows fleet usage statistics on the landing page", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/gateways");
|
|
|
|
await expect(page.getByTestId("gateway-fleet-usage")).toBeVisible();
|
|
await expect(page.getByTestId("gateway-fleet-usage-coverage")).toContainText("2 gateways · 1 online");
|
|
await expect(page.getByTestId("gateway-fleet-usage-devices")).toContainText("1 online · 1 offline");
|
|
await expect(page.getByTestId("gateway-fleet-usage-bindings")).toContainText("1 overrides · 1 cloud only");
|
|
await expect(page.getByTestId("gateway-fleet-usage-runtime")).toContainText("298 ms");
|
|
});
|
|
|
|
test("@smoke renders cached gateway data before slow revalidation finishes", async ({ page }) => {
|
|
await seedEdgeGatewayWorkspaceCache(page);
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await page.route(/\/edge-gateways(?:\/701)?(?:\?.*)?$/i, async (route, request) => {
|
|
if (request.method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await page.waitForTimeout(1800);
|
|
await route.fallback();
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/gateways/701/overview");
|
|
|
|
await expect(page.getByTestId("gateway-detail-header")).toContainText("Cached CPH Edge 01", { timeout: 1200 });
|
|
await expect(page.getByTestId("gateway-overview-container-health")).toContainText("ONLINE");
|
|
await expect(page.getByTestId("gateway-overview-outbox")).toContainText("IN_SYNC");
|
|
await expect(page.getByTestId("gateway-detail-header")).toContainText("CPH Edge 01", { timeout: 5000 });
|
|
});
|
|
|
|
test("@smoke keeps watching long enough to open a delayed first gateway claim", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
edgeGateways: {
|
|
empty: true,
|
|
claimPollsRemaining: 8,
|
|
},
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/gateways");
|
|
|
|
await expect(page.getByTestId("gateway-fleet-landing")).toBeVisible();
|
|
await expect(page.getByTestId("gateway-empty-state")).toContainText("No gateways");
|
|
|
|
await page.getByTestId("gateway-installer-department").selectOption("1");
|
|
await page.getByTestId("gateway-installer-label").fill("Canary Pi");
|
|
await page.getByTestId("gateway-installer-generate").click();
|
|
|
|
await expect(page).toHaveURL(/\/superuser\/gateways\/703\/overview$/, { timeout: 20_000 });
|
|
await expect(page.getByTestId("gateway-detail-header")).toContainText("Canary Pi");
|
|
await expect(page.locator("body")).toContainText("Gateway connected.");
|
|
});
|
|
|
|
test("@smoke reopens an existing gateway when the installer reconnects it", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
edgeGateways: {
|
|
reuseClaimGatewayId: 701,
|
|
claimPollsRemaining: 8,
|
|
},
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/gateways");
|
|
await expect(page.getByTestId("gateway-fleet-landing")).toBeVisible();
|
|
|
|
await page.getByTestId("gateway-installer-department").selectOption("1");
|
|
await page.getByTestId("gateway-installer-label").fill("CPH Edge 01");
|
|
await page.getByTestId("gateway-installer-generate").click();
|
|
|
|
await expect(page).toHaveURL(/\/superuser\/gateways\/701\/overview$/, { timeout: 20_000 });
|
|
await expect(page.getByTestId("gateway-detail-header")).toContainText("CPH Edge 01");
|
|
await expect(page.locator("body")).toContainText("Gateway reconnected.");
|
|
});
|
|
|
|
test("@smoke updates metadata and rotates credentials from the manage view", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/gateways/701/overview");
|
|
await expect(page.getByTestId("gateway-detail-header")).toContainText("CPH Edge 01");
|
|
|
|
await page.getByTestId("gateway-tab-manage").click();
|
|
await page.getByTestId("gateway-metadata-label").fill("CPH Edge Prime");
|
|
await page.getByTestId("gateway-metadata-save").click();
|
|
|
|
await expect(page.locator("body")).toContainText("Gateway metadata updated.");
|
|
await expect(page.getByTestId("gateway-detail-header")).toContainText("CPH Edge Prime");
|
|
|
|
await page.getByTestId("gateway-rotate-confirmation").fill("ROTATE");
|
|
await page.getByTestId("gateway-rotate-credentials").click();
|
|
await expect(page.getByTestId("gateway-credential-bundle")).toContainText("rotated-edge-agent-token");
|
|
await expect(page.getByTestId("gateway-credential-bundle")).toContainText("truckwash-edge-gateway-stack.service");
|
|
await expect(page.locator("body")).toContainText("Gateway credentials rotated.");
|
|
});
|
|
|
|
test("@smoke manages discovery, bindings, operations, uninstall, and delete", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/gateways/701/inventory");
|
|
|
|
await page.getByTestId("gateway-discovery-trigger").click();
|
|
await expect(page.locator("body")).toContainText("Gateway operation DISCOVERY queued.");
|
|
|
|
await page.getByTestId("gateway-binding-add").click();
|
|
await page.locator('[data-testid^="gateway-binding-relay-"]').last().fill("M-8");
|
|
await page.locator('[data-testid^="gateway-binding-device-"]').last().selectOption("shelly-plus-01");
|
|
await page.locator('[data-testid^="gateway-binding-channel-"]').last().selectOption("0");
|
|
await page.locator('[data-testid^="gateway-binding-fallback-"]').last().selectOption("CLOUD_ONLY");
|
|
await page.getByTestId("gateway-bindings-save").click();
|
|
await expect(page.locator("body")).toContainText("Relay bindings saved.");
|
|
|
|
await page.getByTestId("gateway-tab-operations").click();
|
|
await page.getByTestId("gateway-update-target-version").fill("php-agent-v2.0");
|
|
await page.getByTestId("gateway-operation-update").click();
|
|
await expect(page.getByTestId("gateway-operations-list")).toContainText("UPDATE");
|
|
await expect(page.getByTestId("gateway-operation-events")).toContainText("Operation completed successfully");
|
|
await page.getByTestId("gateway-tab-overview").click();
|
|
await expect(page.getByTestId("gateway-overview-rollout")).toContainText("php-agent-v2.0");
|
|
await expect(page.getByTestId("gateway-overview-update-window")).toContainText("02:00-04:00");
|
|
await expect(page.getByTestId("gateway-overview-container-health")).toContainText("ONLINE");
|
|
await expect(page.getByTestId("gateway-overview-outbox")).toContainText("IN_SYNC");
|
|
|
|
await page.getByTestId("gateway-tab-operations").click();
|
|
await page.getByTestId("gateway-operation-uninstall").click();
|
|
await expect(page.getByTestId("gateway-operations-list")).toContainText("UNINSTALL");
|
|
|
|
await page.getByTestId("gateway-tab-manage").click();
|
|
await page.getByTestId("gateway-delete-confirmation").fill("CPH Edge 01");
|
|
await page.getByTestId("gateway-delete").click();
|
|
|
|
await expect(page).toHaveURL(/\/superuser\/gateways$/, { timeout: 8_000 });
|
|
await expect(page.locator("body")).toContainText("Gateway deleted.");
|
|
});
|
|
});
|