import { expect, test } from "@playwright/test"; import { mockApi, primeMockSession } from "./support/network.js"; async function primeSuperuserSession(page) { const token = "superuser-edge-gateway-token"; await primeMockSession(page, { token }); } test.describe("Edge gateway management smoke", () => { test("@smoke manages installers, bindings, queued updates, and shell approval", async ({ page }) => { await mockApi(page, { authenticated: true, permissions: ["superuser", "user"], }); await primeSuperuserSession(page); await page.goto("/superuser/gateways"); 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.locator("body")).toContainText("Latens"); await expect(page.locator("body")).toContainText("184 ms"); await expect(page.locator("body")).toContainText("CPU"); await expect(page.locator("body")).toContainText("27%"); await expect(page.locator("body")).toContainText("RAM"); await expect(page.locator("body")).toContainText("61%"); 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); const primaryOnlineOption = primaryDeviceSelect.locator("option").nth(1); await expect(primaryOnlineOption).toContainText("Shelly Plus 2PM"); await expect(primaryOnlineOption).toContainText("shelly-plus-01"); await expect(primaryOnlineOption).toContainText("10.1.0.31"); await expect(primaryOnlineOption).toContainText("2 kanaler"); await expect(primaryOnlineOption).toContainText("Gen 2"); const primaryOfflineOption = primaryDeviceSelect.locator("option").nth(2); await expect(primaryOfflineOption).toContainText("Shelly Mini 1"); await expect(primaryOfflineOption).toContainText("shelly-mini-offline"); await expect(primaryOfflineOption).toContainText("10.1.0.34"); await expect(primaryOfflineOption).toContainText("1 kanal"); await expect(primaryOfflineOption).toContainText("Gen 3"); await expect(primaryOfflineOption).toContainText("Offline"); const staleOption = legacyBindingRow.locator("select").first().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"); await expect(staleOption).toContainText("2 kanaler"); await expect(staleOption).toContainText("Mangler i discovery"); await primaryChannelSelect.selectOption("1"); await primaryDeviceSelect.selectOption("shelly-mini-offline"); await expect(primaryIpInput).toHaveValue("10.1.0.34"); await expect(primaryChannelSelect).toHaveValue("0"); const relayBindingInput = primaryBindingRow.locator('input[type="text"]').first(); 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 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 expect(page.locator("body")).toContainText("Discovery er"); await expect(page.locator("body")).toContainText("Afventer"); await expect(page.locator("body")).toContainText("shelly-plus-new"); await page.locator('input[placeholder="F.eks. 1.3.0"]').fill("1.3.0"); await page.getByRole("button", { name: /opdatering/i }).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 page.getByRole("textbox", { name: /begrundelse/i }).fill("Investigate offline relay"); await page.getByRole("button", { name: "Godkend root shell" }).click(); const terminalBuffer = page.locator('[aria-label="Root shell buffer"]'); await expect(page.locator("body")).toContainText("Live"); await expect(terminalBuffer).toContainText("[Live shell forbundet]"); await expect(terminalBuffer).toContainText("root@pi:~#"); const terminalSurface = page.locator('[aria-label="Root shell terminal"]'); await terminalSurface.focus(); await terminalSurface.press("l"); await terminalSurface.press("s"); await terminalSurface.press("Enter"); await expect(terminalBuffer).toContainText("agent.mjs"); await page.getByRole("button", { name: "Terminate session" }).click(); await expect(page.locator("body")).toContainText("Lukket"); await expect(terminalBuffer).toContainText("Session lukket"); }); test("@smoke shows a shell connection error when the API-polled shell closes before open", async ({ page }) => { await mockApi(page, { authenticated: true, permissions: ["superuser", "user"], edgeGateways: { shellSocket: { mode: "fail-before-open", }, }, }); await primeSuperuserSession(page); await page.goto("/superuser/gateways"); await page.getByRole("button", { name: /root shell/i }).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('[aria-label="Root shell buffer"]')).toContainText("Gateway-shell blev lukket"); }); test("@smoke applies department cutover mode from department page", async ({ page }) => { await mockApi(page, { authenticated: true, permissions: ["superuser", "user"], }); await primeSuperuserSession(page); await page.goto("/superuser/departments/1/gateways"); await expect(page).toHaveURL(/\/superuser\/departments\/1\/gateways$/); await expect(page.locator("body")).toContainText("Gateway for afdelingen"); 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 }) => { await mockApi(page, { authenticated: true, permissions: ["superuser", "user"], }); await primeSuperuserSession(page); 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("Discovery er"); await expect(page.locator("body")).toContainText("shelly-plus-new"); }); test("@smoke shows a rolled-back update when restart verification fails", async ({ page }) => { await mockApi(page, { authenticated: true, permissions: ["superuser", "user"], }); await primeSuperuserSession(page); await page.goto("/superuser/gateways/701"); await page.locator('input[placeholder="F.eks. 1.3.0"]').fill("1.3.0-rollback"); await page.getByRole("button", { name: /opdatering/i }).click(); const latestUpdate = page.getByTestId("gateway-update-item").first(); await expect(latestUpdate).toContainText("1.3.0-rollback"); await expect(latestUpdate).toContainText("Verificerer genstart"); await expect(latestUpdate).toContainText("Rullet tilbage"); await expect(latestUpdate).toContainText("Startup verification timed out"); }); });