861 lines
38 KiB
JavaScript
861 lines
38 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, primeMockSession } from "./support/network.js";
|
|
|
|
async function primeSuperuserSession(page) {
|
|
await primeMockSession(page, { token: "superuser-edge-gateway-token" });
|
|
}
|
|
|
|
const json = (body, status = 200) => ({
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
async function acceleratePageTimers(page, timerScale = 0.01) {
|
|
await page.addInitScript((scale) => {
|
|
const nativeSetTimeout = window.setTimeout.bind(window);
|
|
const nativeSetInterval = window.setInterval.bind(window);
|
|
|
|
window.setTimeout = (callback, delay = 0, ...args) =>
|
|
nativeSetTimeout(callback, Math.max(1, Number(delay || 0) * scale), ...args);
|
|
window.setInterval = (callback, delay = 0, ...args) =>
|
|
nativeSetInterval(callback, Math.max(1, Number(delay || 0) * scale), ...args);
|
|
}, timerScale);
|
|
}
|
|
|
|
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(/\/(?:modules\/)?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/configuration/edgegateway");
|
|
|
|
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 saves module defaults on the canonical module workspace", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/configuration/edgegateway");
|
|
|
|
await expect(page.getByTestId("edge-gateway-module-config")).toBeVisible();
|
|
await page.getByTestId("gateway-module-release-channel").selectOption("canary");
|
|
await page.getByTestId("gateway-module-update-window").fill("03:00-05:00");
|
|
await page.getByTestId("gateway-module-save").click();
|
|
|
|
await expect(page.getByTestId("gateway-module-release-channel")).toHaveValue("canary");
|
|
await expect(page.getByTestId("gateway-module-update-window")).toHaveValue("03:00-05:00");
|
|
});
|
|
|
|
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/configuration/edgegateway");
|
|
|
|
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/configuration/edgegateway");
|
|
|
|
await expect(page.getByTestId("gateway-fleet-usage")).toBeVisible();
|
|
await expect(page.getByTestId("gateway-fleet-usage-coverage")).toContainText("2 gateways");
|
|
await expect(page.getByTestId("gateway-fleet-usage-coverage")).toContainText("1 online");
|
|
await expect(page.getByTestId("gateway-fleet-usage-devices")).toContainText("1 online");
|
|
await expect(page.getByTestId("gateway-fleet-usage-devices")).toContainText("1 offline");
|
|
await expect(page.getByTestId("gateway-fleet-usage-bindings")).toContainText("1 overrides");
|
|
await expect(page.getByTestId("gateway-fleet-usage-bindings")).toContainText("1 cloud only");
|
|
await expect(page.getByTestId("gateway-fleet-usage-runtime")).toContainText("298 ms");
|
|
});
|
|
|
|
test("@smoke keeps watching slow installers until the first gateway claim completes", async ({ page }) => {
|
|
test.setTimeout(90_000);
|
|
await acceleratePageTimers(page);
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
edgeGateways: {
|
|
empty: true,
|
|
claimPollsRemaining: 65,
|
|
},
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/configuration/edgegateway");
|
|
|
|
await expect(page.getByTestId("gateway-fleet-landing")).toBeVisible();
|
|
await expect(page.getByTestId("gateway-onboarding")).toContainText("Install new gateway");
|
|
await expect(page.getByTestId("gateway-fleet-usage-coverage")).toContainText("0 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.getByTestId("gateway-installer-status")).toBeVisible();
|
|
await expect.poll(() => page.getByTestId("gateway-installer-status-state").textContent()).toContain("Running");
|
|
await expect
|
|
.poll(async () => (await page.getByTestId("gateway-installer-status-step").textContent())?.trim())
|
|
.not.toBe("Waiting to start");
|
|
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway\/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/configuration/edgegateway");
|
|
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.getByTestId("gateway-installer-status")).toBeVisible();
|
|
await expect.poll(() => page.getByTestId("gateway-installer-status-state").textContent()).toContain("Running");
|
|
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway\/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 surfaces pre-claim installer failures with diagnostics", async ({ page }) => {
|
|
test.setTimeout(90_000);
|
|
await acceleratePageTimers(page);
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
edgeGateways: {
|
|
empty: true,
|
|
claimPollsRemaining: 8,
|
|
installSessionFailure: {
|
|
step: "START_STACK",
|
|
failurePoll: 4,
|
|
message:
|
|
"Job for truckwash-edge-gateway-stack.service failed because the control process exited with error code.",
|
|
diagnostics: [
|
|
{
|
|
name: "systemctl status",
|
|
output:
|
|
"Job for truckwash-edge-gateway-stack.service failed because the control process exited with error code.",
|
|
},
|
|
{
|
|
name: "journalctl",
|
|
output: "Compose rollout failed during build/startup",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/configuration/edgegateway");
|
|
|
|
await page.getByTestId("gateway-installer-department").selectOption("1");
|
|
await page.getByTestId("gateway-installer-label").fill("Broken Pi");
|
|
await page.getByTestId("gateway-installer-generate").click();
|
|
|
|
await expect(page.getByTestId("gateway-installer-status")).toBeVisible();
|
|
await expect
|
|
.poll(() => page.getByTestId("gateway-installer-status-state").textContent(), { timeout: 15_000 })
|
|
.toContain("Failed");
|
|
await expect(page.getByTestId("gateway-installer-status-step")).toContainText("Starting stack");
|
|
await expect(page.getByTestId("gateway-installer-status-error")).toContainText(
|
|
"Job for truckwash-edge-gateway-stack.service failed"
|
|
);
|
|
await page.getByTestId("gateway-installer-status-diagnostics").locator("summary").click();
|
|
await expect(page.getByTestId("gateway-installer-status-diagnostics")).toContainText("systemctl status");
|
|
await expect(page.getByTestId("gateway-installer-status-diagnostics")).toContainText(
|
|
"Compose rollout failed during build/startup"
|
|
);
|
|
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway(?:\?.*)?$/);
|
|
await expect(page.getByTestId("gateway-fleet-landing")).toBeVisible();
|
|
await expect(page.getByTestId("gateway-onboarding")).toContainText("Install new gateway");
|
|
await expect(page.getByTestId("gateway-fleet-usage-coverage")).toContainText("0 gateways");
|
|
});
|
|
|
|
test("@smoke updates metadata and rotates credentials from the settings view", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/configuration/edgegateway/701/overview");
|
|
await expect(page.getByTestId("gateway-detail-header")).toContainText("CPH Edge 01");
|
|
|
|
await page.getByTestId("gateway-tab-settings").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 completes delayed installer onboarding without a local timeout", async ({ page }) => {
|
|
test.setTimeout(90_000);
|
|
await acceleratePageTimers(page);
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
edgeGateways: {
|
|
empty: true,
|
|
claimPollsRemaining: 65,
|
|
},
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/configuration/edgegateway");
|
|
await expect(page.getByTestId("gateway-fleet-landing")).toBeVisible();
|
|
|
|
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.getByTestId("gateway-install-command")).toHaveValue(/install\.sh\?token=edge-install-token/);
|
|
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway\/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 shows transport sync diagnostics on the statistics page", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
edgeGateways: {
|
|
gatewayOverrides: [
|
|
{
|
|
id: 701,
|
|
metadata: {
|
|
control_plane_status: {
|
|
last_successful_sync_at: "2026-04-08 08:15:00",
|
|
last_transport_failure_at: "2026-04-08 08:14:20",
|
|
last_transport_error:
|
|
"POST https://api.truckwash.test/edge-agent/gateways/701/heartbeat returned HTTP 502",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/configuration/edgegateway/701/overview");
|
|
await expect(page.getByTestId("gateway-detail-header")).toContainText("CPH Edge 01");
|
|
|
|
await page.getByTestId("gateway-tab-statistics").click();
|
|
await expect(page.getByTestId("gateway-statistics-page")).toBeVisible();
|
|
await expect(page.getByTestId("gateway-statistics-transport")).toContainText("Last successful sync");
|
|
await expect(page.getByTestId("gateway-statistics-transport")).toContainText("2026-04-08 08:15:00");
|
|
await expect(page.getByTestId("gateway-statistics-transport")).toContainText("Last transport failure");
|
|
await expect(page.getByTestId("gateway-statistics-transport")).toContainText("2026-04-08 08:14:20");
|
|
await expect(page.getByTestId("gateway-statistics-transport")).toContainText("returned HTTP 502");
|
|
});
|
|
|
|
test("@smoke manages discovery, bindings, tasks, logs, statistics, uninstall, and delete", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/configuration/edgegateway/701/inventory");
|
|
|
|
await page.getByTestId("gateway-discovery-trigger").click();
|
|
await expect(page.locator("body")).toContainText("Gateway operation DISCOVERY queued.");
|
|
await expect(page.getByTestId("gateway-inventory-table")).toContainText("shelly-plus-new", { timeout: 10_000 });
|
|
|
|
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-tasks").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-logs").click();
|
|
await expect(page.getByTestId("gateway-logs-page")).toBeVisible();
|
|
await expect(page.getByTestId("gateway-logs-timeline")).toContainText("GATEWAY_OPERATION_QUEUED");
|
|
|
|
await page.getByTestId("gateway-tab-statistics").click();
|
|
await expect(page.getByTestId("gateway-statistics-page")).toBeVisible();
|
|
await expect(page.getByTestId("gateway-statistics-version")).toContainText("php-agent-v2.0");
|
|
await expect(page.getByTestId("gateway-statistics-transport")).toContainText("Last successful sync");
|
|
await expect(page.getByTestId("gateway-statistics-transport")).toContainText("2026-04-08 08:14:56");
|
|
await expect(page.getByTestId("gateway-statistics-transport")).toContainText("Last transport error");
|
|
|
|
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-container-services")).toContainText("auto-updater");
|
|
await expect(page.getByTestId("gateway-overview-outbox")).toContainText("IN_SYNC");
|
|
|
|
await page.getByTestId("gateway-tab-tasks").click();
|
|
await page.getByTestId("gateway-operation-uninstall").click();
|
|
await expect(page.getByTestId("gateway-operations-list")).toContainText("UNINSTALL");
|
|
|
|
await page.getByTestId("gateway-tab-settings").click();
|
|
await page.getByTestId("gateway-delete-confirmation").fill("CPH Edge 01");
|
|
await page.getByTestId("gateway-delete").click();
|
|
|
|
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway$/, { timeout: 8_000 });
|
|
await expect(page.locator("body")).toContainText("Gateway deleted.");
|
|
});
|
|
|
|
test("@smoke updates the integrated workspace after binding and scanner assignment changes", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/departments/1/gateways?tab=lanes", { waitUntil: "domcontentloaded" });
|
|
|
|
await expect(page.getByTestId("department-hardware-panel-lanes")).toBeVisible();
|
|
await expect(page.getByTestId("department-lane-8")).toContainText("MISSING");
|
|
|
|
await page.getByTestId("department-hardware-tab-gateways").click();
|
|
await expect(page.getByTestId("edge-gateway-workspace")).toBeVisible();
|
|
|
|
await page.getByTestId("gateway-tab-inventory").click();
|
|
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("1");
|
|
await page.locator('[data-testid^="gateway-binding-fallback-"]').last().selectOption("PREFER_LOCAL");
|
|
await page.getByTestId("gateway-bindings-save").click();
|
|
await expect(page.locator("body")).toContainText("Relay bindings saved.");
|
|
|
|
await page.getByTestId("department-hardware-tab-lanes").click();
|
|
await page.getByTestId("department-hardware-refresh").click();
|
|
await expect(page.getByTestId("department-lane-8")).toContainText("READY");
|
|
|
|
await page.getByTestId("department-hardware-tab-scanners").click();
|
|
await expect(page.getByTestId("department-hardware-panel-scanners")).toBeVisible();
|
|
await page.getByTestId("department-scanner-lane-2").selectOption("8");
|
|
await page.getByTestId("department-scanner-save-2").click();
|
|
await expect(page.getByTestId("department-hardware-notice")).toContainText("lane assignment updated");
|
|
await expect(page.getByTestId("department-scanner-2")).toContainText("READY");
|
|
|
|
await page.getByTestId("department-scanner-rotate-2").click();
|
|
await expect(page.getByTestId("department-hardware-notice")).toContainText("API key rotated");
|
|
await expect(page.getByTestId("department-scanner-key-2")).toContainText("rotated-scanner-key-2");
|
|
});
|
|
|
|
test("@smoke shows all five hardware categories on department lane actions", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1600, height: 900 });
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/department/lanes", { waitUntil: "domcontentloaded" });
|
|
|
|
const laneRow = page.locator("tr", { hasText: "Lane 7" }).first();
|
|
await expect(laneRow).toBeVisible();
|
|
await laneRow.locator(".action-settings-wheel-trigger").click();
|
|
|
|
const sectionLabels = await page
|
|
.locator('[data-testid="action-settings-wheel-sections"] .action-settings-wheel-section-trigger__label')
|
|
.allTextContents();
|
|
expect(sectionLabels).toEqual(["Lane", "Self-serve Studio", "Gates", "Relays", "Gateways"]);
|
|
|
|
await page.getByTestId("action-settings-wheel-section-department-self-serve-studio").hover();
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-self-serve-studio")).toContainText(
|
|
"Open Studio"
|
|
);
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-self-serve-studio")).toContainText(
|
|
"Open Legacy Self-Serve"
|
|
);
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-self-serve-studio")).toContainText(
|
|
"Open Hardware Workspace (Lanes)"
|
|
);
|
|
|
|
await page.getByTestId("action-settings-wheel-section-department-gates").hover();
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-gates")).toContainText("Open Gates Tab");
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-gates")).toContainText("Open Legacy Gates");
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-gates")).toContainText("Add Gate");
|
|
|
|
await page.getByTestId("action-settings-wheel-section-department-relays").hover();
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-relays")).toContainText("Open Relays Tab");
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-relays")).toContainText(
|
|
"Open Legacy Relays"
|
|
);
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-relays")).toContainText("Add Relay");
|
|
|
|
await page.getByTestId("action-settings-wheel-section-department-gateways").hover();
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-gateways")).toContainText(
|
|
"Open Gateways Tab"
|
|
);
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-gateways")).toContainText(
|
|
"Open Fleet Landing"
|
|
);
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-gateways")).toContainText(
|
|
"Open Primary Gateway"
|
|
);
|
|
});
|
|
|
|
test("@smoke renders Danish hardware action labels without mojibake", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1600, height: 900 });
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem("locale", "da");
|
|
});
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/department/lanes", { waitUntil: "domcontentloaded" });
|
|
|
|
const laneRow = page.locator("tr", { hasText: "Lane 7" }).first();
|
|
await expect(laneRow).toBeVisible();
|
|
await laneRow.locator(".action-settings-wheel-trigger").click();
|
|
|
|
await expect(page.getByTestId("action-settings-wheel-section-department-self-serve-studio")).toContainText(
|
|
"Selvvask Studio"
|
|
);
|
|
await expect(page.getByTestId("action-settings-wheel-section-department-gates")).toContainText("Porte");
|
|
await expect(page.getByTestId("action-settings-wheel-section-department-relays")).toContainText("Relæer");
|
|
|
|
await page.getByTestId("action-settings-wheel-section-department-self-serve-studio").hover();
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-self-serve-studio")).toContainText(
|
|
"Åbn Studio"
|
|
);
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-self-serve-studio")).toContainText(
|
|
"Åbn ældre selvvask"
|
|
);
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-self-serve-studio")).toContainText(
|
|
"Åbn hardware-arbejdsområde (baner)"
|
|
);
|
|
|
|
await page.getByTestId("action-settings-wheel-section-department-relays").hover();
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-relays")).toContainText("Åbn relæ-fane");
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-relays")).toContainText("Åbn ældre relæer");
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-relays")).toContainText("Tilføj relæ");
|
|
|
|
await page.getByTestId("action-settings-wheel-section-department-gateways").hover();
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-gateways")).toContainText(
|
|
"Åbn gateway-fane"
|
|
);
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-gateways")).toContainText(
|
|
"Åbn gateway-oversigt"
|
|
);
|
|
await expect(page.getByTestId("action-settings-wheel-submenu-department-gateways")).toContainText(
|
|
"Åbn primær gateway"
|
|
);
|
|
});
|
|
|
|
test("@smoke surfaces relay contexts and self-serve CTAs in the integrated workspace", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/departments/1/gateways?tab=lanes", { waitUntil: "domcontentloaded" });
|
|
|
|
await expect(page.getByTestId("department-hardware-open-selfserve-studio")).toContainText("Open Studio");
|
|
await expect(page.getByTestId("department-hardware-open-legacy-selfserve")).toContainText("Open Legacy Self-Serve");
|
|
await expect(page.getByTestId("department-hardware-open-binding-inventory")).toBeVisible();
|
|
|
|
await page.getByTestId("department-hardware-tab-relays").click();
|
|
await expect(page.getByTestId("department-hardware-panel-relays")).toBeVisible();
|
|
await expect(page.getByTestId("department-hardware-add-relay")).toBeVisible();
|
|
await expect(page.getByTestId("department-hardware-open-legacy-relays")).toBeVisible();
|
|
await expect(page.getByTestId("department-relay-52")).toContainText("North machine relay");
|
|
await expect(page.getByTestId("department-relay-52")).toContainText("Lane 7");
|
|
await expect(page.getByTestId("department-relay-52")).toContainText("North Entrance");
|
|
await expect(page.getByTestId("department-relay-52")).toContainText("CPH Edge 01");
|
|
});
|
|
|
|
test("@smoke saves Shelly relay selections from the integrated workspace", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/departments/1/gateways?tab=lanes", { waitUntil: "domcontentloaded" });
|
|
|
|
const entryRelaySelect = page.getByTestId("department-lane-relay-7-relay_in_id");
|
|
const entryRelayStatus = page.getByTestId("department-lane-relay-status-7-relay_in_id");
|
|
await expect(entryRelaySelect).toBeVisible();
|
|
await expect(entryRelaySelect).toHaveValue("");
|
|
await expect(entryRelayStatus).toHaveAttribute("data-state", "Unknown");
|
|
await expect(entryRelayStatus.locator("i.fas.fa-circle")).toBeVisible();
|
|
await expect
|
|
.poll(() =>
|
|
entryRelaySelect.evaluate(
|
|
(select, expectedValue) => Array.from(select.options).some((option) => option.value === expectedValue),
|
|
"ENTRY-8"
|
|
)
|
|
)
|
|
.toBe(true);
|
|
|
|
await entryRelaySelect.selectOption("ENTRY-8");
|
|
await expect(entryRelayStatus).toHaveAttribute("data-state", "Green");
|
|
await expect(entryRelaySelect.locator("option:checked")).toContainText("Roskilde entry (Shelly Plus 1PM)");
|
|
await page.getByTestId("department-lane-save-7").click();
|
|
|
|
await expect(page.getByTestId("department-hardware-notice")).toContainText("Lane 7 relay bindings updated.");
|
|
await expect(entryRelaySelect).toHaveValue("ENTRY-8");
|
|
await expect(entryRelayStatus).toHaveAttribute("data-state", "Green");
|
|
await expect(page.getByTestId("department-lane-7")).toContainText("MISSING");
|
|
|
|
await entryRelaySelect.selectOption("");
|
|
await expect(entryRelayStatus).toHaveAttribute("data-state", "Unknown");
|
|
await page.getByTestId("department-lane-save-7").click();
|
|
|
|
await expect(page.getByTestId("department-hardware-notice")).toContainText("Lane 7 relay bindings updated.");
|
|
await expect(entryRelaySelect).toHaveValue("");
|
|
await expect(entryRelayStatus).toHaveAttribute("data-state", "Unknown");
|
|
await expect(page.getByTestId("department-lane-7")).toContainText("READY");
|
|
});
|
|
|
|
test("@smoke creates, edits, and deletes department gates from the integrated workspace", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
let nextGateId = 90;
|
|
let gates = [
|
|
{
|
|
id: 41,
|
|
department: 1,
|
|
name: "North Entrance",
|
|
is_entrance: true,
|
|
is_exit: false,
|
|
config: {
|
|
type: "RELAY",
|
|
relay_id: "M-7",
|
|
pulse_seconds: 1,
|
|
},
|
|
},
|
|
{
|
|
id: 42,
|
|
department: 1,
|
|
name: "Service Exit",
|
|
is_entrance: false,
|
|
is_exit: true,
|
|
config: {
|
|
type: "PHONE_CALL",
|
|
phone_number: "+4512345678",
|
|
call_duration_threshold: 3,
|
|
},
|
|
},
|
|
];
|
|
|
|
const buildGateWorkspace = () => {
|
|
const workspaceGates = gates.map((gate) => {
|
|
const transportType = String(gate?.config?.type || "PHONE_CALL").toUpperCase();
|
|
const relayId = String(gate?.config?.relay_id || "").trim();
|
|
|
|
return {
|
|
...gate,
|
|
transport_type: transportType,
|
|
config_complete:
|
|
transportType === "PHONE_CALL"
|
|
? Boolean(gate?.config?.phone_number) && Number(gate?.config?.call_duration_threshold || 0) > 0
|
|
: Boolean(relayId),
|
|
relay: relayId ? { relay_id: relayId } : null,
|
|
coverage:
|
|
transportType === "RELAY" && relayId
|
|
? {
|
|
covered: true,
|
|
primary_binding: {
|
|
gateway_label: "CPH Edge 01",
|
|
},
|
|
}
|
|
: null,
|
|
};
|
|
});
|
|
|
|
return {
|
|
department: {
|
|
id: 1,
|
|
name: "Copenhagen",
|
|
description: "Primary transport department",
|
|
order_priority: 1,
|
|
},
|
|
summary: {
|
|
department_id: 1,
|
|
department_name: "Copenhagen",
|
|
transport_mode: "cloud",
|
|
gateway_count: 1,
|
|
online_gateway_count: 1,
|
|
primary_gateway: {
|
|
id: 701,
|
|
label: "CPH Edge 01",
|
|
status: "ONLINE",
|
|
},
|
|
required_relay_count: workspaceGates.filter((gate) => gate.transport_type === "RELAY").length,
|
|
bound_relay_count: workspaceGates.filter((gate) => gate.transport_type === "RELAY").length,
|
|
missing_binding_count: 0,
|
|
gate_count: workspaceGates.length,
|
|
gate_transport_mix: {
|
|
relay: workspaceGates.filter((gate) => gate.transport_type === "RELAY").length,
|
|
phone_call: workspaceGates.filter((gate) => gate.transport_type === "PHONE_CALL").length,
|
|
},
|
|
scanner_count: 0,
|
|
assigned_scanner_count: 0,
|
|
issue_count: 0,
|
|
health: "READY",
|
|
},
|
|
gateways: [],
|
|
lanes: [],
|
|
self_serve: {
|
|
readiness_state: "DISABLED",
|
|
ready_lanes: 0,
|
|
lane_count: 0,
|
|
configured_task_count: 0,
|
|
configured_product_count: 0,
|
|
links: {
|
|
studio: "/admin/1/modules/self-serve/studio",
|
|
},
|
|
},
|
|
gates: workspaceGates,
|
|
scanners: [],
|
|
issues: [],
|
|
actions: [],
|
|
};
|
|
};
|
|
|
|
await page.route(
|
|
/https?:\/\/(?:api\.truckwash\.io(?::\d+)?)\/modules\/edge-gateways\/workspace\/departments\/1(?:\?.*)?$/i,
|
|
async (route) => {
|
|
await route.fulfill(json({ data: buildGateWorkspace() }));
|
|
}
|
|
);
|
|
|
|
await page.route(/https?:\/\/(?:api\.truckwash\.io(?::\d+)?)\/department\/gates(?:\?.*)?$/i, async (route) => {
|
|
const request = route.request();
|
|
const method = request.method();
|
|
|
|
if (method === "GET") {
|
|
await route.fulfill(json({ data: gates }));
|
|
return;
|
|
}
|
|
|
|
if (method === "POST") {
|
|
const payload = request.postDataJSON();
|
|
const createdGate = {
|
|
id: nextGateId++,
|
|
...payload,
|
|
};
|
|
gates = [...gates, createdGate];
|
|
await route.fulfill(json({ data: createdGate }));
|
|
return;
|
|
}
|
|
|
|
if (method === "PUT") {
|
|
const payload = request.postDataJSON();
|
|
gates = gates.map((gate) =>
|
|
Number(gate.id) === Number(payload.id)
|
|
? {
|
|
...gate,
|
|
...payload,
|
|
config: payload.config ?? gate.config,
|
|
}
|
|
: gate
|
|
);
|
|
const updatedGate = gates.find((gate) => Number(gate.id) === Number(payload.id));
|
|
await route.fulfill(json({ data: updatedGate }));
|
|
return;
|
|
}
|
|
|
|
if (method === "DELETE") {
|
|
const requestUrl = new URL(request.url());
|
|
const gateId = Number(requestUrl.searchParams.get("id") || 0);
|
|
gates = gates.filter((gate) => Number(gate.id) !== gateId);
|
|
await route.fulfill(json({ data: true }));
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(json({ data: [] }));
|
|
});
|
|
|
|
await page.goto("/superuser/departments/1/gateways?tab=gates");
|
|
|
|
await expect(page.getByTestId("department-hardware-panel-gates")).toBeVisible();
|
|
await expect(page.getByTestId("department-hardware-add-gate")).toBeVisible();
|
|
await expect(page.getByTestId("department-gate-edit-41")).toBeVisible();
|
|
await expect(page.getByTestId("department-gate-delete-42")).toBeVisible();
|
|
|
|
await page.getByTestId("department-hardware-add-gate").click();
|
|
|
|
let modal = page.locator(".swal2-popup");
|
|
await expect(modal).toBeVisible();
|
|
await modal.locator("#department-gate-name").fill("East Exit");
|
|
await modal.locator("#department-gate-is-exit").selectOption("true");
|
|
await modal.locator("#department-gate-transport-type").selectOption("RELAY");
|
|
await expect(modal.locator("#department-gate-relay-id")).toBeVisible();
|
|
await modal.locator("#department-gate-relay-id").fill("M-11");
|
|
await modal.locator("#department-gate-pulse-seconds").fill("2");
|
|
await modal.locator(".swal2-confirm").click();
|
|
|
|
await expect(page.getByTestId("department-hardware-notice")).toContainText("Gate created: East Exit.");
|
|
await expect(page.getByTestId("department-gate-90")).toContainText("East Exit");
|
|
await expect(page.getByTestId("department-gate-90")).toContainText("Relay M-11");
|
|
|
|
await page.getByTestId("department-gate-edit-42").click();
|
|
|
|
modal = page.locator(".swal2-popup");
|
|
await expect(modal).toBeVisible();
|
|
await modal.locator("#department-gate-name").fill("Service Exit Updated");
|
|
await modal.locator("#department-gate-phone-number").fill("+4599999999");
|
|
await modal.locator("#department-gate-call-duration-threshold").fill("45");
|
|
await modal.locator(".swal2-confirm").click();
|
|
|
|
await expect(page.getByTestId("department-hardware-notice")).toContainText("Gate updated: Service Exit Updated.");
|
|
await expect(page.getByTestId("department-gate-42")).toContainText("Service Exit Updated");
|
|
await expect(page.getByTestId("department-gate-42")).toContainText("+4599999999");
|
|
await expect(page.getByTestId("department-gate-42")).toContainText("threshold 45");
|
|
|
|
await page.getByTestId("department-gate-delete-90").click();
|
|
|
|
modal = page.locator(".swal2-popup");
|
|
await expect(modal).toBeVisible();
|
|
await modal.locator(".swal2-confirm").click();
|
|
|
|
await expect(page.getByTestId("department-hardware-notice")).toContainText("Gate deleted: East Exit.");
|
|
await expect(page.getByTestId("department-gate-90")).toHaveCount(0);
|
|
});
|
|
|
|
test("@smoke cancels an in-progress gateway task so a replacement task can be queued", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/configuration/edgegateway/701/tasks");
|
|
|
|
await page.getByTestId("gateway-operation-discovery").click();
|
|
await expect(page.getByTestId("gateway-operation-cancel")).toBeVisible();
|
|
await page.getByTestId("gateway-operation-cancel").click();
|
|
await expect(page.locator("body")).toContainText("DISCOVERY cancelled.");
|
|
await expect(page.getByTestId("gateway-operations-list")).toContainText("CANCELLED");
|
|
|
|
await page.getByTestId("gateway-update-target-version").fill("php-agent-v2.1");
|
|
await page.getByTestId("gateway-operation-update").click();
|
|
await expect(page.locator("body")).toContainText("Gateway operation UPDATE queued.");
|
|
});
|
|
|
|
test("@smoke opens the live terminal, streams output, and closes the shell session", async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
});
|
|
await primeSuperuserSession(page);
|
|
|
|
await page.goto("/superuser/configuration/edgegateway/701/terminal");
|
|
|
|
await expect(page.getByTestId("gateway-terminal-page")).toBeVisible();
|
|
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/connecting|open/i);
|
|
await expect(page.getByTestId("gateway-terminal-output")).toContainText("Connected to CPH Edge 01", {
|
|
timeout: 5_000,
|
|
});
|
|
|
|
await page.getByTestId("gateway-terminal-input").fill("pwd");
|
|
await page.getByTestId("gateway-terminal-send").click();
|
|
await expect(page.getByTestId("gateway-terminal-output")).toContainText("/opt/truckwash-edge-agent");
|
|
|
|
await page.getByTestId("gateway-terminal-close").click();
|
|
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/closed|idle/i);
|
|
});
|
|
});
|