195 lines
10 KiB
TypeScript
195 lines
10 KiB
TypeScript
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();
|
|
});
|