import AxeBuilder from "@axe-core/playwright"; import { expect, loginAs, test } from "./fixtures"; const darkReview = process.env.WP400_DARK_REVIEW === "1"; test.use({ colorScheme: darkReview ? "dark" : "light" }); const viewports = [ { width: 320, height: 568 }, { width: 360, height: 800 }, { width: 390, height: 844 }, { width: 430, height: 932 }, { width: 768, height: 1024 }, { width: 1280, height: 800 }, { width: 1440, height: 900 }, { width: 844, height: 390 }, ]; for (const viewport of viewports) { test(`operator layout and accessible surfaces at ${viewport.width}x${viewport.height}`, async ({ page }, info) => { test.setTimeout(180_000); await page.setViewportSize(viewport); await loginAs(page, "hub-admin"); for (const path of ["/", "/projects", "/servers", "/tools", "/connectors", "/clients", "/policies", "/audit", "/operations", "/settings", "/projects/ludarium", "/connectors/knowledge-default"]) { await page.goto(path); await expect(page.locator(".topbar")).toBeVisible(); if (viewport.width <= 840) { const target = await page.locator(".nav-trigger").boundingBox(); expect(target?.width).toBeGreaterThanOrEqual(44); expect(target?.height).toBeGreaterThanOrEqual(44); } await expect(page.locator(".loading-state")).toHaveCount(0, { timeout: 15_000 }); const dimensions = await page.evaluate(() => ({ document: document.documentElement.scrollWidth, viewport: innerWidth })); expect(dimensions.document, path).toBeLessThanOrEqual(dimensions.viewport + 1); if (path === "/" && viewport.width <= 430) { await expect(page.getByLabel("Project status records")).toBeVisible(); await expect(page.getByLabel("Project status table")).toBeHidden(); const grid = await page.locator(".primary-metrics").evaluate((element) => getComputedStyle(element).gridTemplateColumns); expect(grid.split(" ")).toHaveLength(1); } if (path === "/tools" && viewport.width <= 430) await expect(page.getByRole("group", { name: "Table density" })).toBeHidden(); if (viewport.width === 390 || viewport.width === 1440) { const result = await new AxeBuilder({ page }).analyze(); expect(result.violations.filter((violation) => ["serious", "critical"].includes(violation.impact ?? "")), path).toEqual([]); } if (process.env.SOL_VISUAL_REVIEW && ["/", "/tools", "/clients", "/operations", "/projects/ludarium", "/connectors/knowledge-default"].includes(path)) { const themeSuffix = darkReview ? "-dark" : ""; await page.screenshot({ path: info.outputPath(`${path.replaceAll("/", "_") || "dashboard"}-${viewport.width}${themeSuffix}.png`), fullPage: true }); await page.screenshot({ path: info.outputPath(`${path.replaceAll("/", "_") || "dashboard"}-${viewport.width}${themeSuffix}-viewport.png`) }); } } }); } test("mobile client actions require an explicit disclosure and confirmation", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await loginAs(page, "hub-admin"); await page.goto("/clients"); const name = `mobile-safety-${Date.now()}`; await page.getByLabel("Name", { exact: true }).fill(name); await page.getByRole("button", { name: "Create pending client" }).click(); const card = page.locator(".mobile-record").filter({ hasText: name }).first(); await expect(card.getByRole("button", { name: "Config", exact: true })).toBeVisible(); await expect(card.getByRole("button", { name: "Revoke", exact: true })).toBeHidden(); await card.locator("summary").click(); await card.getByRole("button", { name: "Revoke", exact: true }).click(); await expect(page.getByRole("dialog", { name: /Revoke/ })).toBeVisible(); await page.getByRole("button", { name: "Cancel", exact: true }).click(); }); test("desktop density persists while mobile cards have no inert density control", async ({ page }) => { await loginAs(page, "hub-viewer"); await page.setViewportSize({ width: 1280, height: 800 }); await page.goto("/tools"); await page.getByRole("button", { name: "Compact", exact: true }).click(); await page.reload(); await expect(page.getByRole("button", { name: "Compact", exact: true })).toHaveAttribute("aria-pressed", "true"); await page.setViewportSize({ width: 430, height: 932 }); await expect(page.getByRole("group", { name: "Table density" })).toBeHidden(); }); test("phone trend keeps outcome details and operations errors never imply no alerts", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await loginAs(page, "hub-viewer"); await page.route("**/api/operations/summary", async (route) => { const response = await route.fetch(); const summary = await response.json(); summary.trend = Array.from({ length: 24 }, (_, hour) => ({ hour: new Date(Date.UTC(2026, 9, 1, hour)).toISOString(), success: hour, denied: 1, error: 0, rate_limited: 0 })); await route.fulfill({ json: summary }); }); await page.goto("/operations"); const mobile = page.getByRole("group", { name: "Four-hour call buckets" }); await expect(mobile).toBeVisible(); await expect(mobile.getByRole("button")).toHaveCount(6); await mobile.getByRole("button").first().click(); await expect(page.locator(".mobile-trend .trend-detail")).toContainText("success"); await page.unroute("**/api/operations/summary"); await page.route("**/api/operations/summary", (route) => route.fulfill({ status: 503, body: "unavailable" })); await page.reload(); await expect(page.getByText("Alerts are unknown while the summary is unavailable.")).toBeVisible(); await expect(page.getByText("No alerts in the latest summary")).toHaveCount(0); }); test("status refresh backs off, reports disconnection and does not invoke probes", async ({ page }) => { await loginAs(page, "hub-viewer"); await page.clock.install(); await page.goto("/"); await expect(page.getByText("Current · auto refresh")).toBeVisible(); let reads = 0; let writes = 0; page.on("request", (request) => { if (request.method() === "POST" && request.url().includes("/api/")) writes += 1; }); await page.route("**/api/dashboard", (route) => { reads += 1; return route.fulfill({ status: 503, body: "unavailable" }); }); await page.clock.fastForward(31_000); await expect(page.getByText("Disconnected · retry available")).toBeVisible(); expect(reads).toBe(1); await page.clock.fastForward(30_000); expect(reads).toBe(1); await page.clock.fastForward(31_000); await expect.poll(() => reads).toBe(2); expect(writes).toBe(0); }); test("a hidden dashboard pauses scheduled refresh and never sends an automatic write", async ({ page }) => { await loginAs(page, "hub-viewer"); await page.clock.install(); await page.addInitScript(() => { Object.defineProperty(document, "hidden", { configurable: true, get: () => sessionStorage.getItem("e2e-document-hidden") !== "false", }); }); let dashboardReads = 0; let apiWrites = 0; page.on("request", (request) => { const url = new URL(request.url()); if (url.pathname === "/api/dashboard" && request.method() === "GET") dashboardReads += 1; if (url.pathname.startsWith("/api/") && request.method() === "POST") apiWrites += 1; }); await page.goto("/"); await expect(page.getByText("Auto refresh paused")).toBeVisible(); expect(dashboardReads).toBe(1); // The one initial snapshot; no timer-driven read. await page.clock.fastForward(120_000); expect(dashboardReads).toBe(1); expect(apiWrites).toBe(0); }); test("the dashboard action inbox starts with four signals and expands the rest on demand", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await loginAs(page, "hub-viewer"); await page.route("**/api/dashboard", async (route) => { const response = await route.fetch(); const snapshot = await response.json(); snapshot.generatedAt = new Date().toISOString(); snapshot.staleAfterSeconds = 3600; snapshot.health = { hub: "healthy", gateway: "healthy", detail: "Fixture is current." }; snapshot.connectors = Array.from({ length: 6 }, (_, index) => ({ id: `inbox-fixture-${index + 1}`, name: `Inbox fixture ${index + 1}`, project: "ludarium", state: "degraded", detail: "A controlled connector needs review.", registrationState: "registered", readiness: { state: "unknown", detail: "No live probe was requested.", observedAt: null, source: "not-probed" }, })); snapshot.drift = { count: 0, projects: [], known: true, detail: "No drift." }; snapshot.backup = { state: "healthy", lastSuccessfulAt: null, detail: "Fixture backup state." }; snapshot.denials = { count: 0, window: "24h" }; await route.fulfill({ json: snapshot }); }); await page.goto("/"); const inbox = page.getByRole("region", { name: "6 actionable signals" }); await expect(inbox.locator(".exception-item")).toHaveCount(4); await expect(inbox.getByRole("link", { name: /Inbox fixture 1/ })).toBeVisible(); await expect(inbox.getByRole("link", { name: /Inbox fixture 4/ })).toBeVisible(); await expect(inbox.getByRole("link", { name: /Inbox fixture 5/ })).toHaveCount(0); await inbox.getByRole("button", { name: "Show 2 more signals" }).click(); await expect(inbox.locator(".exception-item")).toHaveCount(6); await expect(inbox.getByRole("link", { name: /Inbox fixture 6/ })).toBeVisible(); await inbox.getByRole("button", { name: "Show fewer signals" }).click(); await expect(inbox.locator(".exception-item")).toHaveCount(4); }); test("project and connector detail links resolve to registered records", async ({ page }) => { await loginAs(page, "hub-admin"); await page.setViewportSize({ width: 1280, height: 800 }); await page.goto("/connectors/knowledge-default"); await expect(page.getByRole("heading", { name: "Knowledge Default", exact: true })).toBeVisible(); await page.goto("/projects/ludarium?tab=MCP%20Servers"); await expect(page.getByLabel("MCP servers", { exact: true }).getByRole("heading", { name: "ludarium-prod-readonly", exact: true })).toBeVisible(); await page.goto("/projects/ludarium?tab=Knowledge%20Sources"); await expect(page.getByLabel("Knowledge sources", { exact: true }).getByRole("heading", { name: "discovery://repository/ludarium", exact: true })).toBeVisible(); });