Add unit tests and new components for edge gateway workflows:
- Introduced unit tests for edge gateway workflow helpers, including workflow step resolution, incident action mapping, relay health row formatting, and workspace state merging. - Added new components for advanced operations, configuration panel, context panel, fleet rail, and health summary. - Enhanced gateway management UI with support for advanced actions, fallback operations, relay health visualization, and device binding features.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { mockApi, primeMockSession } from "./support/network.js";
|
||||
|
||||
async function primeSuperuserSession(page) {
|
||||
@@ -6,8 +6,21 @@ async function primeSuperuserSession(page) {
|
||||
await primeMockSession(page, { token });
|
||||
}
|
||||
|
||||
async function openRecoverStep(page) {
|
||||
await page.getByTestId("gateway-step-recover").click();
|
||||
}
|
||||
|
||||
async function openConfigureStep(page) {
|
||||
await page.getByTestId("gateway-step-configure").click();
|
||||
}
|
||||
|
||||
async function openAdvancedOperations(page) {
|
||||
await page.getByTestId("gateway-step-primary-recover").click();
|
||||
await expect(page.getByTestId("gateway-advanced-operations")).toContainText("Break-glass");
|
||||
}
|
||||
|
||||
test.describe("Edge gateway management smoke", () => {
|
||||
test("@smoke manages installers, bindings, queued updates, and shell approval", async ({ page }) => {
|
||||
test("@smoke manages the guided workflow across health, local control, maintenance, and shell approval", async ({ page }) => {
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["superuser", "user"],
|
||||
@@ -18,9 +31,8 @@ test.describe("Edge gateway management smoke", () => {
|
||||
|
||||
await expect(page).toHaveURL(/\/superuser\/gateways$/);
|
||||
await expect(page.locator("body")).toContainText("Edge gateways");
|
||||
await expect(page.locator("body")).toContainText("Valgt gateway");
|
||||
await expect(page.locator("body")).toContainText("CPH Edge 01");
|
||||
await expect(page.locator("body")).toContainText("Shelly Plus 2PM");
|
||||
await expect(page.getByTestId("gateway-step-shell-nav")).toContainText("Kategorier");
|
||||
await expect(page.getByTestId("gateway-detail-header")).toContainText("CPH Edge 01");
|
||||
await expect(page.locator("body")).toContainText("Latens");
|
||||
await expect(page.locator("body")).toContainText("184 ms");
|
||||
await expect(page.locator("body")).toContainText("CPU");
|
||||
@@ -30,12 +42,16 @@ test.describe("Edge gateway management smoke", () => {
|
||||
await expect(page.locator("body")).toContainText("Disk");
|
||||
await expect(page.locator("body")).toContainText("58%");
|
||||
|
||||
const bindingRows = page.locator(".edge-table--editor tbody tr");
|
||||
const primaryBindingRow = bindingRows.nth(0);
|
||||
const legacyBindingRow = bindingRows.nth(1);
|
||||
const primaryDeviceSelect = primaryBindingRow.locator("select").first();
|
||||
const primaryChannelSelect = primaryBindingRow.locator("select").nth(1);
|
||||
const primaryIpInput = primaryBindingRow.locator('input[type="text"]').nth(1);
|
||||
await openConfigureStep(page);
|
||||
await expect(page.getByTestId("gateway-step-configure-panel")).toContainText("Shelly Plus 2PM");
|
||||
|
||||
const bindingCards = page.locator(".edge-binding-card");
|
||||
const primaryBindingCard = bindingCards.nth(0);
|
||||
const legacyBindingCard = bindingCards.nth(1);
|
||||
const primaryDeviceSelect = primaryBindingCard.locator("select").nth(0);
|
||||
const primaryChannelSelect = primaryBindingCard.locator("select").nth(1);
|
||||
const primaryFallbackSelect = primaryBindingCard.locator("select").nth(2);
|
||||
const primaryIpInput = primaryBindingCard.getByLabel("IP");
|
||||
|
||||
const primaryOnlineOption = primaryDeviceSelect.locator("option").nth(1);
|
||||
await expect(primaryOnlineOption).toContainText("Shelly Plus 2PM");
|
||||
@@ -52,7 +68,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
await expect(primaryOfflineOption).toContainText("Gen 3");
|
||||
await expect(primaryOfflineOption).toContainText("Offline");
|
||||
|
||||
const staleOption = legacyBindingRow.locator("select").first().locator("option").last();
|
||||
const staleOption = legacyBindingCard.getByLabel("Shelly-device").locator("option").last();
|
||||
await expect(staleOption).toContainText("Ukendt Shelly-device");
|
||||
await expect(staleOption).toContainText("shelly-missing-legacy");
|
||||
await expect(staleOption).toContainText("10.1.0.99");
|
||||
@@ -61,36 +77,46 @@ test.describe("Edge gateway management smoke", () => {
|
||||
|
||||
await primaryChannelSelect.selectOption("1");
|
||||
await primaryDeviceSelect.selectOption("shelly-mini-offline");
|
||||
await primaryFallbackSelect.selectOption("LOCAL_ONLY");
|
||||
await expect(primaryIpInput).toHaveValue("10.1.0.34");
|
||||
await expect(primaryChannelSelect).toHaveValue("0");
|
||||
await expect(primaryFallbackSelect).toHaveValue("LOCAL_ONLY");
|
||||
|
||||
const relayBindingInput = primaryBindingRow.locator('input[type="text"]').first();
|
||||
const relayBindingInput = primaryBindingCard.getByLabel("Relæ-id");
|
||||
await relayBindingInput.fill("M-7-CANARY");
|
||||
await page.getByRole("button", { name: "Gem bindinger" }).click();
|
||||
await expect(relayBindingInput).toHaveValue("M-7-CANARY");
|
||||
await expect(primaryDeviceSelect).toHaveValue("shelly-mini-offline");
|
||||
await expect(primaryIpInput).toHaveValue("10.1.0.34");
|
||||
await expect(primaryChannelSelect).toHaveValue("0");
|
||||
await expect(primaryFallbackSelect).toHaveValue("LOCAL_ONLY");
|
||||
|
||||
await page.getByLabel("Installer afdeling").selectOption("1");
|
||||
await page.locator('input[placeholder="F.eks. Roskilde Pi 01"]').fill("Canary Pi");
|
||||
await page.getByRole("button", { name: /installer/i }).click();
|
||||
await expect(page.getByLabel("Installationskommando")).toHaveValue(/curl -fsSL/);
|
||||
|
||||
await page.getByRole("button", { name: /discovery/i }).click();
|
||||
await page.getByTestId("gateway-discovery-action").click();
|
||||
await expect(page.locator("body")).toContainText("Discovery er");
|
||||
await expect(page.locator("body")).toContainText("Afventer");
|
||||
await expect(page.locator("body")).toContainText("shelly-plus-new");
|
||||
|
||||
await openRecoverStep(page);
|
||||
await page.getByLabel("Installer afdeling").selectOption("1");
|
||||
await page.locator('input[placeholder="F.eks. Roskilde Pi 01"]').fill("Canary Pi");
|
||||
await page.getByTestId("gateway-installer-generate").click();
|
||||
await expect(page.getByLabel("Installationskommando")).toHaveValue(/curl -fsSL/);
|
||||
|
||||
await page.locator('input[placeholder="F.eks. 1.3.0"]').fill("1.3.0");
|
||||
await page.getByRole("button", { name: /opdatering/i }).click();
|
||||
await page.getByTestId("gateway-update-queue").click();
|
||||
await expect(page.locator("body")).toContainText("Opdatering er");
|
||||
const latestUpdate = page.getByTestId("gateway-update-item").first();
|
||||
await expect(latestUpdate).toContainText("1.3.0");
|
||||
await expect(latestUpdate).toContainText("Verificerer genstart");
|
||||
await expect(latestUpdate).toContainText("Fuldført");
|
||||
|
||||
await page.getByRole("button", { name: /root shell/i }).click();
|
||||
await openAdvancedOperations(page);
|
||||
await page.getByRole("button", { name: "Vis tabel" }).click();
|
||||
await expect(page.getByTestId("gateway-relay-health-table")).toContainText("M-7-LEGACY");
|
||||
await expect(page.getByTestId("gateway-relay-health-table")).toContainText("Cloud fallback");
|
||||
await expect(page.getByTestId("gateway-relay-health-table")).toContainText("Kun cloud");
|
||||
|
||||
await page.getByTestId("gateway-shell-open").click();
|
||||
await page.getByRole("textbox", { name: /begrundelse/i }).fill("Investigate offline relay");
|
||||
await page.getByRole("button", { name: "Godkend root shell" }).click();
|
||||
|
||||
@@ -98,6 +124,9 @@ test.describe("Edge gateway management smoke", () => {
|
||||
await expect(page.locator("body")).toContainText("Live");
|
||||
await expect(terminalBuffer).toContainText("[Live shell forbundet]");
|
||||
await expect(terminalBuffer).toContainText("root@pi:~#");
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.getSelection?.()?.toString() ?? ""))
|
||||
.toBe("");
|
||||
|
||||
const terminalSurface = page.locator('[aria-label="Root shell terminal"]');
|
||||
await terminalSurface.focus();
|
||||
@@ -106,7 +135,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
await terminalSurface.press("Enter");
|
||||
await expect(terminalBuffer).toContainText("agent.mjs");
|
||||
|
||||
await page.getByRole("button", { name: "Terminate session" }).click();
|
||||
await page.getByRole("button", { name: "Afslut session" }).click();
|
||||
await expect(page.locator("body")).toContainText("Lukket");
|
||||
await expect(terminalBuffer).toContainText("Session lukket");
|
||||
});
|
||||
@@ -124,19 +153,68 @@ test.describe("Edge gateway management smoke", () => {
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/gateways");
|
||||
await page.getByRole("button", { name: /root shell/i }).click();
|
||||
await openRecoverStep(page);
|
||||
await openAdvancedOperations(page);
|
||||
await page.getByTestId("gateway-shell-open").click();
|
||||
await page.getByRole("textbox", { name: /begrundelse/i }).fill("Test broken shell");
|
||||
await page.getByRole("button", { name: "Godkend root shell" }).click();
|
||||
|
||||
await expect(page.locator("body")).toContainText("Fejl");
|
||||
await expect(page.getByRole("button", { name: "Terminate session" })).toBeDisabled();
|
||||
await expect(page.locator("body")).toContainText("Fejlet");
|
||||
await expect(page.getByRole("button", { name: "Afslut session" })).toBeDisabled();
|
||||
await expect(page.locator('[aria-label="Root shell buffer"]')).toContainText("Gateway-shell blev lukket");
|
||||
});
|
||||
|
||||
test("@smoke applies department cutover mode from department page", async ({ page }) => {
|
||||
test("@smoke applies department cutover mode from the department page", async ({ page }) => {
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["superuser", "user"],
|
||||
edgeGateways: {
|
||||
gatewayOverrides: [
|
||||
{
|
||||
id: 701,
|
||||
metadata: {
|
||||
broker_connected: false,
|
||||
system_metrics: {
|
||||
latency_ms: 184,
|
||||
cpu_usage_pct: 27,
|
||||
memory_usage_pct: 61,
|
||||
memory_used_bytes: 2576980377,
|
||||
memory_total_bytes: 4294967296,
|
||||
disk_usage_pct: 58,
|
||||
disk_used_bytes: 249108103168,
|
||||
disk_total_bytes: 429496729600,
|
||||
disk_mount: "/",
|
||||
},
|
||||
},
|
||||
channel_status: {
|
||||
command: {
|
||||
preferred: "API_POLLING",
|
||||
active: "API_POLLING",
|
||||
state: "DEGRADED",
|
||||
backlog_depth: 0,
|
||||
last_success_at: "2026-04-08 08:15:00",
|
||||
},
|
||||
shell: {
|
||||
preferred: "API_POLLING",
|
||||
active: "API_POLLING",
|
||||
state: "ONLINE",
|
||||
backlog_depth: 0,
|
||||
last_success_at: "2026-04-08 08:15:00",
|
||||
},
|
||||
broker: {
|
||||
connected: false,
|
||||
state: "OFFLINE",
|
||||
last_error: "Broker unavailable",
|
||||
},
|
||||
},
|
||||
transport_health: {
|
||||
status: "DEGRADED",
|
||||
summary: "API polling er aktiv som fallback",
|
||||
recommended_action: "review_binding_override",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
@@ -144,13 +222,17 @@ test.describe("Edge gateway management smoke", () => {
|
||||
|
||||
await expect(page).toHaveURL(/\/superuser\/departments\/1\/gateways$/);
|
||||
await expect(page.locator("body")).toContainText("Gateway for afdelingen");
|
||||
await expect(page.getByTestId("gateway-open-full-page")).toBeVisible();
|
||||
|
||||
await openRecoverStep(page);
|
||||
await openAdvancedOperations(page);
|
||||
await page.getByLabel("Transporttilstand").selectOption("cloud");
|
||||
await page.getByRole("button", { name: "Anvend cutover" }).click();
|
||||
await expect(page.getByLabel("Transporttilstand")).toHaveValue("cloud");
|
||||
await expect(page.locator("body")).toContainText("Cloud fallback");
|
||||
});
|
||||
|
||||
test("@smoke shows API polling as the control channel while discovery still works", async ({ page }) => {
|
||||
test("@smoke keeps control-channel state visible in the health step while discovery still works", async ({ page }) => {
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["superuser", "user"],
|
||||
@@ -159,8 +241,9 @@ test.describe("Edge gateway management smoke", () => {
|
||||
|
||||
await page.goto("/superuser/gateways");
|
||||
|
||||
await expect(page.locator("body")).toContainText("API polling");
|
||||
await page.getByRole("button", { name: /discovery/i }).click();
|
||||
await expect(page.locator("body")).toContainText("Broker fast path");
|
||||
await openConfigureStep(page);
|
||||
await page.getByTestId("gateway-discovery-action").click();
|
||||
await expect(page.locator("body")).toContainText("Discovery er");
|
||||
await expect(page.locator("body")).toContainText("shelly-plus-new");
|
||||
});
|
||||
@@ -173,9 +256,10 @@ test.describe("Edge gateway management smoke", () => {
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/gateways/701");
|
||||
await openRecoverStep(page);
|
||||
|
||||
await page.locator('input[placeholder="F.eks. 1.3.0"]').fill("1.3.0-rollback");
|
||||
await page.getByRole("button", { name: /opdatering/i }).click();
|
||||
await page.getByTestId("gateway-update-queue").click();
|
||||
|
||||
const latestUpdate = page.getByTestId("gateway-update-item").first();
|
||||
await expect(latestUpdate).toContainText("1.3.0-rollback");
|
||||
|
||||
Reference in New Issue
Block a user