Publish curated ITWorx_MCP_Hub source from 6c7434368ce2a594005983c5a6601a31a639e7f1
This commit is contained in:
1 parent
77946d62c4
commit
6d40ab975c
86 files changed
+6784
-1478
No files matched your search
@@ -16,8 +16,10 @@ test.describe("authentication", () => {
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
await expect(page.getByText("Authenticated", { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Governed MCP, with the current signal first." })).toBeVisible();
|
||||
// Real backend data, not a placeholder: Ludarium is the seeded catalog project.
|
||||
await expect(page.getByText("Ludarium", { exact: true })).toBeVisible();
|
||||
// Assert a real bounded dashboard row without depending on catalog ordering.
|
||||
const snapshot = await (await page.request.get("/api/dashboard")).json();
|
||||
expect(snapshot.projects.length).toBeGreaterThan(0);
|
||||
await expect(page.locator(`[href="/projects/${snapshot.projects[0].slug}"]`).first()).toContainText(snapshot.projects[0].name);
|
||||
});
|
||||
|
||||
test("an authenticated user can sign out and stale management data disappears", async ({ page }) => {
|
||||
|
||||
@@ -18,7 +18,8 @@ test.describe("mobile navigation drawer", () => {
|
||||
|
||||
await trigger.click();
|
||||
await expect(page.locator(".sidebar.sidebar-open")).toHaveCount(1);
|
||||
await expect(trigger).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(page.locator(".nav-trigger")).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(page.getByRole("dialog", { name: "Application navigation" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("closes via its own close button", async ({ page }) => {
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
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();
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { expect, loginAs, test } from "./fixtures";
|
||||
|
||||
test.describe("operator review regressions", () => {
|
||||
test.beforeEach(async ({ page }) => { await loginAs(page, "hub-admin"); });
|
||||
|
||||
test("filter changes retain the URL project and environment at the API boundary", async ({ page }) => {
|
||||
await page.goto("/connectors?project=ludarium&environment=prod");
|
||||
await expect(page.getByLabel("Filter records")).toBeVisible();
|
||||
const response = page.waitForResponse((value) => {
|
||||
const url = new URL(value.url());
|
||||
return url.pathname === "/api/connectors" && url.searchParams.get("q") === "knowledge-default";
|
||||
});
|
||||
await page.getByLabel("Filter records").fill("knowledge-default");
|
||||
const actual = new URL((await response).url());
|
||||
expect(actual.searchParams.get("project")).toBe("ludarium");
|
||||
expect(actual.searchParams.get("environment")).toBe("prod");
|
||||
await expect.poll(() => new URL(page.url()).searchParams.get("q")).toBe("knowledge-default");
|
||||
expect(new URL(page.url()).searchParams.get("project")).toBe("ludarium");
|
||||
expect(new URL(page.url()).searchParams.get("environment")).toBe("prod");
|
||||
await expect(page.getByRole("link", { name: "Knowledge Default", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("catalog scope stays locked until an explicit apply finishes", async ({ page }) => {
|
||||
let release!: () => void;
|
||||
const pending = new Promise<void>((resolve) => { release = resolve; });
|
||||
await page.route("**/api/catalog/preview", (route) => route.fulfill({ json: { valid: true, changes: ["Review fixture change"], errors: [] } }));
|
||||
await page.route("**/api/catalog/apply", async (route) => { await pending; await route.fulfill({ json: { accepted: true, message: "Reviewed catalog apply completed" } }); });
|
||||
await page.goto("/tools?project=ludarium");
|
||||
const workflow = page.locator(".workflow-panel").filter({ has: page.getByRole("heading", { name: "Catalog validation and preview" }) });
|
||||
await workflow.getByRole("button", { name: "Validate and preview" }).click();
|
||||
await workflow.getByRole("button", { name: "Apply approved catalog" }).click();
|
||||
await page.getByRole("button", { name: "Confirm apply" }).click();
|
||||
await expect(workflow.getByRole("combobox", { name: "Project", exact: true })).toBeDisabled();
|
||||
await expect(workflow.getByRole("button", { name: "Applying…" })).toBeDisabled();
|
||||
release();
|
||||
await expect(workflow.getByText("Reviewed catalog apply completed")).toBeVisible();
|
||||
await expect(workflow.getByRole("combobox", { name: "Project", exact: true })).toBeEnabled();
|
||||
});
|
||||
|
||||
test("a pending client created after SPA navigation retains a visible resume path", async ({ page }) => {
|
||||
let release!: () => void;
|
||||
const pending = new Promise<void>((resolve) => { release = resolve; });
|
||||
let createdId = "";
|
||||
await page.route("**/api/client-management", async (route) => {
|
||||
if (route.request().method() !== "POST") { await route.continue(); return; }
|
||||
const response = await route.fetch();
|
||||
createdId = ((await response.json()) as { client: { id: string } }).client.id;
|
||||
await pending;
|
||||
await route.fulfill({ response });
|
||||
});
|
||||
await page.goto("/clients");
|
||||
const name = `creation-navigation-${Date.now()}`;
|
||||
await page.getByLabel("Name", { exact: true }).fill(name);
|
||||
await page.getByRole("button", { name: "Create pending client" }).click();
|
||||
await expect.poll(() => createdId).not.toBe("");
|
||||
await page.getByRole("navigation", { name: "Primary navigation" }).getByRole("link", { name: "Operations", exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "Operations", level: 1 })).toBeVisible();
|
||||
release();
|
||||
const notice = page.locator(".client-created-notice");
|
||||
await expect(notice).toContainText(name);
|
||||
expect(new URL(page.url()).pathname).toBe("/operations");
|
||||
await expect(notice.getByRole("link", { name: "Resume onboarding" })).toHaveAttribute("href", `/clients?onboard=${createdId}`);
|
||||
await notice.getByRole("link", { name: "Resume onboarding" }).click();
|
||||
await expect(page.getByText(/Resumable Hub identity/)).toBeVisible();
|
||||
expect(new URL(page.url()).searchParams.get("onboard")).toBe(createdId);
|
||||
});
|
||||
|
||||
test("operation-check results link to registered connector identities", async ({ page }) => {
|
||||
await page.route("**/api/operations/check", (route) => route.fulfill({ json: {
|
||||
generatedAt: new Date().toISOString(), healthy: true,
|
||||
connectors: { "itworx-knowledge": { reachable: true, status: "healthy", detail: "Bounded contract fixture", connectorRefs: [{ id: "knowledge-default", name: "Knowledge Default" }], connectorRefsTruncated: false } },
|
||||
} }));
|
||||
await page.goto("/operations");
|
||||
await page.getByRole("button", { name: "Run live checks" }).click();
|
||||
const registration = page.locator(".connector-checks").getByRole("link", { name: "Knowledge Default", exact: true });
|
||||
await expect(registration).toHaveAttribute("href", "/connectors/knowledge-default");
|
||||
await registration.click();
|
||||
await expect(page.getByRole("heading", { name: "Knowledge Default", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Quick Jump sends an ordinary client to its filtered records instead of an unavailable onboarding workflow", async ({ page }) => {
|
||||
await page.route("**/api/clients?*", (route) => route.fulfill({ json: {
|
||||
kind: "clients", generatedAt: new Date().toISOString(),
|
||||
rows: [{ id: "ordinary-fixture-client", name: "Ordinary fixture client", status: "healthy", summary: "Existing client", project: "ludarium", environment: "prod" }],
|
||||
page: { total: 1, nextCursor: null },
|
||||
} }));
|
||||
await page.getByRole("button", { name: "Open quick jump" }).click();
|
||||
const dialog = page.getByRole("dialog", { name: "Quick jump" });
|
||||
await dialog.getByRole("combobox").fill("ordinary-fixture-client");
|
||||
await dialog.getByRole("group", { name: "Clients", exact: true }).getByRole("option", { name: /Ordinary fixture client/ }).click();
|
||||
await expect(page).toHaveURL(/\/clients\?q=ordinary-fixture-client$/);
|
||||
await expect(page.locator(".desktop-data-table").getByText("Existing client", { exact: true })).toBeVisible();
|
||||
const desktopRecord = page.locator(".desktop-data-table").getByRole("link", { name: "Ordinary fixture client", exact: true });
|
||||
await expect(desktopRecord).toHaveAttribute("href", "/clients?q=ordinary-fixture-client");
|
||||
await desktopRecord.click();
|
||||
await expect(page).toHaveURL(/\/clients\?q=ordinary-fixture-client$/);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
const mobileRecord = page.locator(".mobile-record-list").getByRole("link", { name: "Ordinary fixture client", exact: true });
|
||||
await expect(mobileRecord).toHaveAttribute("href", "/clients?q=ordinary-fixture-client");
|
||||
await mobileRecord.click();
|
||||
await expect(page).toHaveURL(/\/clients\?q=ordinary-fixture-client$/);
|
||||
await expect(page.getByText(/Resumable Hub identity/)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Quick Jump retains the selected audit trace as its destination filter", async ({ page }) => {
|
||||
await page.route("**/api/audit?*", (route) => route.fulfill({ json: {
|
||||
kind: "audit", generatedAt: new Date().toISOString(),
|
||||
rows: [{ id: "audit-fixture", name: "tool.invocation", status: "healthy", summary: "Fixture event", traceId: "review-fixture-trace", project: "ludarium" }],
|
||||
page: { total: 1, nextCursor: null },
|
||||
} }));
|
||||
await page.getByRole("button", { name: "Open quick jump" }).click();
|
||||
const dialog = page.getByRole("dialog", { name: "Quick jump" });
|
||||
await dialog.getByRole("combobox").fill("review-fixture-trace");
|
||||
await dialog.getByRole("group", { name: "Audit", exact: true }).getByRole("option", { name: /tool.invocation/ }).click();
|
||||
await expect(page).toHaveURL(/\/audit\?q=review-fixture-trace$/);
|
||||
await expect(page.getByLabel("Filter records")).toHaveValue("review-fixture-trace");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { expect, loginAs, test } from "./fixtures";
|
||||
|
||||
test.use({ viewport: { width: 390, height: 844 } });
|
||||
|
||||
test.describe("mobile operator shell", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAs(page, "hub-viewer");
|
||||
await page.goto("/");
|
||||
});
|
||||
|
||||
test("navigation isolates the page, traps focus, locks scroll, and restores focus on Escape", async ({ page }, testInfo) => {
|
||||
const trigger = page.getByRole("button", { name: "Open navigation" });
|
||||
const main = page.locator("main");
|
||||
await trigger.focus();
|
||||
await trigger.press("Enter");
|
||||
|
||||
const drawer = page.getByRole("dialog", { name: "Application navigation" });
|
||||
const close = drawer.getByRole("button", { name: "Close navigation" });
|
||||
await expect(drawer).toBeVisible();
|
||||
await expect(drawer).toHaveAttribute("aria-modal", "true");
|
||||
await expect(close).toBeFocused();
|
||||
if (process.env.SOL_VISUAL_REVIEW === "1") await page.screenshot({ path: testInfo.outputPath("mobile-drawer.png"), animations: "disabled" });
|
||||
await expect.poll(() => main.evaluate((element) => (element as HTMLElement).inert)).toBe(true);
|
||||
await expect.poll(() => page.evaluate(() => document.body.style.overflow)).toBe("hidden");
|
||||
|
||||
await drawer.getByRole("link", { name: "ITWorx MCP Hub" }).focus();
|
||||
await page.keyboard.press("Shift+Tab");
|
||||
await expect(drawer.getByRole("link", { name: "Platform" })).toBeFocused();
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(drawer.getByRole("link", { name: "ITWorx MCP Hub" })).toBeFocused();
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(drawer).toHaveCount(0);
|
||||
await expect(trigger).toBeFocused();
|
||||
await expect.poll(() => main.evaluate((element) => (element as HTMLElement).inert)).toBe(false);
|
||||
await expect.poll(() => page.evaluate(() => document.body.style.overflow)).toBe("");
|
||||
});
|
||||
|
||||
test("short phones can scroll the full drawer while the page stays locked", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 320, height: 520 });
|
||||
await page.getByRole("button", { name: "Open navigation" }).click();
|
||||
const drawer = page.getByRole("dialog", { name: "Application navigation" });
|
||||
await expect(drawer).toBeVisible();
|
||||
const metrics = await drawer.evaluate((element) => ({
|
||||
height: element.getBoundingClientRect().height,
|
||||
clientHeight: element.clientHeight,
|
||||
scrollHeight: element.scrollHeight,
|
||||
overflowY: getComputedStyle(element).overflowY,
|
||||
}));
|
||||
expect(metrics.height).toBeLessThanOrEqual(521);
|
||||
expect(metrics.overflowY).toBe("auto");
|
||||
expect(metrics.scrollHeight).toBeGreaterThan(metrics.clientHeight);
|
||||
await drawer.evaluate((element) => { element.scrollTop = element.scrollHeight; });
|
||||
await expect(drawer.getByRole("link", { name: "Platform" })).toBeInViewport();
|
||||
});
|
||||
|
||||
test("Quick Jump searches grouped entities with keyboard control and never requests audit for a viewer", async ({ page }, testInfo) => {
|
||||
const auditRequests: string[] = [];
|
||||
page.on("request", (request) => {
|
||||
if (new URL(request.url()).pathname === "/api/audit") auditRequests.push(request.url());
|
||||
});
|
||||
await page.getByRole("button", { name: "Open quick jump" }).click();
|
||||
const dialog = page.getByRole("dialog", { name: "Quick jump" });
|
||||
const input = dialog.getByRole("combobox", { name: "Find a workspace or resource" });
|
||||
await expect(input).toBeFocused();
|
||||
await expect(dialog).toHaveAttribute("aria-modal", "true");
|
||||
await expect.poll(() => page.locator("main").evaluate((element) => Boolean(element.closest("[inert]")))).toBe(true);
|
||||
await input.fill("Ludarium");
|
||||
|
||||
const project = dialog.getByRole("group", { name: "Projects", exact: true }).getByRole("option", { name: /Ludarium/i });
|
||||
await expect(project).toBeVisible();
|
||||
expect((await project.boundingBox())!.height).toBeLessThanOrEqual(120);
|
||||
if (process.env.SOL_VISUAL_REVIEW === "1") await page.screenshot({ path: testInfo.outputPath("mobile-quick-jump.png"), animations: "disabled" });
|
||||
await expect(dialog.getByRole("group", { name: "Projects" })).toBeVisible();
|
||||
await input.press("ArrowDown");
|
||||
await expect(input).toHaveAttribute("aria-activedescendant", /quick-jump-option-/);
|
||||
await input.press("Enter");
|
||||
await expect(page).toHaveURL(/\/projects\/ludarium$/);
|
||||
expect(auditRequests).toEqual([]);
|
||||
});
|
||||
|
||||
test("Quick Jump keeps recent navigation in memory and shows search failures", async ({ page }) => {
|
||||
await page.getByRole("button", { name: "Open quick jump" }).click();
|
||||
let dialog = page.getByRole("dialog", { name: "Quick jump" });
|
||||
await dialog.getByRole("option", { name: "Catalog" }).click();
|
||||
await expect(page).toHaveURL(/\/tools$/);
|
||||
|
||||
await page.getByRole("button", { name: "Open quick jump" }).click();
|
||||
dialog = page.getByRole("dialog", { name: "Quick jump" });
|
||||
await expect(dialog.getByRole("group", { name: "Recent destinations" }).getByRole("option", { name: "Catalog" })).toBeVisible();
|
||||
|
||||
await page.route("**/api/projects?*", (route) => route.fulfill({ status: 503, body: "unavailable" }));
|
||||
const input = dialog.getByRole("combobox", { name: "Find a workspace or resource" });
|
||||
await input.fill("Ludarium");
|
||||
await expect(dialog.getByRole("alert")).toHaveText("One resource type could not be searched. Results may be incomplete.");
|
||||
});
|
||||
|
||||
test("sign-out remains explicit and reports its busy state on a narrow screen", async ({ page }) => {
|
||||
await page.route("**/auth/logout", async (route) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
await route.fulfill({ status: 503, body: "unavailable" });
|
||||
});
|
||||
const signOut = page.getByRole("button", { name: "Sign out" });
|
||||
await expect(signOut).toBeVisible();
|
||||
await expect(signOut).toHaveText("Sign out");
|
||||
await signOut.click();
|
||||
const busyButton = page.getByRole("button", { name: "Signing out…" });
|
||||
await expect(busyButton).toBeDisabled();
|
||||
await expect(page.getByRole("alert")).toContainText("control API is unavailable");
|
||||
await expect(page.getByRole("button", { name: "Sign out" })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("mobile workflow dialog", () => {
|
||||
test.use({ viewport: { width: 390, height: 844 } });
|
||||
|
||||
test("large tool workflow fills the dynamic viewport and scrolls inside the dialog", async ({ page }, testInfo) => {
|
||||
await loginAs(page, "hub-operator");
|
||||
await page.goto("/tools");
|
||||
const trigger = page.locator(".mobile-record-list").getByRole("button", { name: "Readiness" }).first();
|
||||
await trigger.click();
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: /Tool console/ });
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect.poll(() => page.evaluate(() => document.body.style.overflow)).toBe("hidden");
|
||||
await expect.poll(() => page.locator("main").evaluate((element) => Boolean(element.closest("[inert]")))).toBe(true);
|
||||
const layout = await dialog.evaluate((element) => ({
|
||||
height: element.getBoundingClientRect().height,
|
||||
overflow: getComputedStyle(element).overflowY,
|
||||
bodyOverflow: getComputedStyle(element.querySelector(".modal-body")!).overflowY,
|
||||
}));
|
||||
expect(layout.height).toBeGreaterThan(800);
|
||||
expect(layout.overflow).toBe("hidden");
|
||||
expect(layout.bodyOverflow).toBe("auto");
|
||||
if (process.env.SOL_VISUAL_REVIEW === "1") {
|
||||
await expect(dialog.getByRole("button", { name: "Run readiness check" })).toBeEnabled();
|
||||
await page.screenshot({ path: testInfo.outputPath("mobile-tool-workflow.png"), animations: "disabled" });
|
||||
}
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(dialog).toHaveCount(0);
|
||||
await expect(trigger).toBeFocused();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,329 @@
|
||||
import { expect, loginAs, test } from "./fixtures";
|
||||
|
||||
function projectDetailFixture(slug: string) {
|
||||
return {
|
||||
slug,
|
||||
name: `Project ${slug.toUpperCase()}`,
|
||||
description: `Delayed route fixture for ${slug}.`,
|
||||
environments: [{ name: "prod", classification: "production" }],
|
||||
capabilityPacks: [],
|
||||
connectorBindings: [],
|
||||
mcpServers: [],
|
||||
knowledgeSources: [],
|
||||
virtualServerCount: 0,
|
||||
clientCount: 0,
|
||||
activeClientCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function connectorDetailFixture(id: string) {
|
||||
return {
|
||||
id,
|
||||
name: `Connector ${id.toUpperCase()}`,
|
||||
connectorType: "fixture",
|
||||
description: `Delayed route fixture for ${id}.`,
|
||||
registration: { state: "registered" as const, detail: "Fixture registration." },
|
||||
readiness: { reachable: null, status: "unknown", detail: "No probe was requested.", observedAt: null, source: "unavailable" as const },
|
||||
bindings: [],
|
||||
relatedTools: [],
|
||||
};
|
||||
}
|
||||
|
||||
test.describe("operator workflow details", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAs(page, "hub-admin");
|
||||
});
|
||||
|
||||
test("client onboarding resumes from its durable identity and never treats a Hub check as MCP proof", async ({ page }) => {
|
||||
await page.goto("/clients");
|
||||
const clientName = `onboarding-resume-${Date.now()}`;
|
||||
await page.getByLabel("Name").fill(clientName);
|
||||
await page.getByLabel("Client profile").selectOption("native-2026");
|
||||
const createResponsePromise = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === "/api/client-management";
|
||||
});
|
||||
await page.getByRole("button", { name: "Create pending client" }).click();
|
||||
const created = await (await createResponsePromise).json() as {
|
||||
client: { id: string };
|
||||
bootstrap: { reference: string };
|
||||
config: { kind: string; protocolVersion: string; transport: string; credentialReference: string };
|
||||
provisioning: { oauthUsername: string; issuer: string; tokenEndpoint: string; oauthClientId: string };
|
||||
};
|
||||
await expect(page.getByText(`Client created: ${clientName}`)).toBeVisible();
|
||||
await expect.poll(() => page.url()).toMatch(/\/clients\?onboard=[0-9a-f-]+$/i);
|
||||
const savedUrl = page.url();
|
||||
expect(new URL(savedUrl).searchParams.get("onboard")).toBe(created.client.id);
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByText(/Resumable Hub identity/)).toBeVisible();
|
||||
const handoff = page.locator(".onboarding-config-auth");
|
||||
const configPanel = page.locator(".onboarding-config");
|
||||
const configValue = (label: string) => configPanel.locator("dl > div").filter({ hasText: label }).locator("dd");
|
||||
await expect(configValue("Client profile")).toHaveText(created.config.kind);
|
||||
await expect(configValue("Protocol version")).toHaveText(created.config.protocolVersion);
|
||||
await expect(configValue("Transport")).toHaveText(created.config.transport);
|
||||
await expect(configPanel.getByText("Credential environment variable", { exact: true })).toHaveCount(0);
|
||||
await expect(configPanel.getByText("Bridge command", { exact: true })).toHaveCount(0);
|
||||
await expect(handoff).toContainText(`External account: ${created.provisioning.oauthUsername}`);
|
||||
await expect(handoff).toContainText(`Issuer: ${created.provisioning.issuer}`);
|
||||
await expect(handoff).toContainText(`Token endpoint: ${created.provisioning.tokenEndpoint}`);
|
||||
await expect(handoff).toContainText(`OAuth client ID: ${created.provisioning.oauthClientId}`);
|
||||
const resumedText = await page.locator("body").innerText();
|
||||
expect(resumedText).not.toContain(created.bootstrap.reference);
|
||||
expect(resumedText).not.toContain(created.config.credentialReference);
|
||||
await expect(page.getByText("OAuth scopes", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText(/External verification still required/)).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Record Hub checks" })).toBeEnabled();
|
||||
await page.getByRole("button", { name: "Record Hub checks" }).click();
|
||||
|
||||
await expect(page.locator(".onboarding-check-recorded")).toContainText("Hub checks were recorded");
|
||||
await expect(page.getByText(/does not mark external OAuth grants or MCP discovery as complete/)).toBeVisible();
|
||||
await expect(page.locator(".onboarding-identity-bar small")).toContainText("· pending");
|
||||
expect(page.url()).toBe(savedUrl);
|
||||
});
|
||||
|
||||
test("a delayed verify response cannot overwrite the onboarding identity after the route changes", async ({ page }) => {
|
||||
const onboardingStatus = (clientId: string, subjectDetail: string, verified = false) => ({
|
||||
clientId,
|
||||
project: "ludarium",
|
||||
environment: "prod",
|
||||
clientStatus: "pending",
|
||||
provisioning: {
|
||||
status: "pending-authentik-service-account",
|
||||
subject: null,
|
||||
issuer: "https://identity.example.invalid",
|
||||
oauthUsername: `svc-mcp-codex-ludarium-${clientId}`,
|
||||
tokenEndpoint: "https://identity.example.invalid/application/o/token/",
|
||||
oauthClientId: "hub-fixture-client",
|
||||
steps: ["Complete external account setup with the administrator."],
|
||||
},
|
||||
checks: {
|
||||
externalSubject: { state: "pending", detail: subjectDetail },
|
||||
externalScope: { state: "unavailable", detail: "External OAuth grants are not visible to the Hub." },
|
||||
hubBinding: { state: "pending", detail: "The client remains disabled." },
|
||||
catalog: { state: "unknown", detail: "Catalog check has not passed." },
|
||||
connector: { state: "unknown", detail: "Connector state is unknown." , observedAt: null },
|
||||
mcpProof: { state: "unavailable", detail: "MCP discovery and invocation must be confirmed externally." },
|
||||
},
|
||||
verification: { checkedAt: verified ? "2026-10-01T10:00:00.000Z" : null, checkedByManagement: verified, verifiedByManagement: verified, hubChecksPassed: verified, detail: verified ? "Client A checks passed." : "No verification record." },
|
||||
});
|
||||
let releaseVerify!: () => void;
|
||||
const verifyGate = new Promise<void>((resolve) => { releaseVerify = resolve; });
|
||||
await page.route("**/api/client-management/a/onboarding-status", (route) => route.fulfill({ json: onboardingStatus("a", "Client A is awaiting its external subject.") }));
|
||||
await page.route("**/api/client-management/a/config", (route) => route.fulfill({ json: { clientId: "a", endpoint: "https://hub.example.invalid/mcp", scopes: ["project:ludarium:read"] } }));
|
||||
await page.route("**/api/client-management/b/onboarding-status", (route) => route.fulfill({ json: onboardingStatus("b", "Client B is awaiting its external subject.") }));
|
||||
await page.route("**/api/client-management/b/config", (route) => route.fulfill({ json: { clientId: "b", endpoint: "https://hub.example.invalid/mcp", scopes: ["project:ludarium:read"] } }));
|
||||
await page.route("**/api/client-management/a/verify", async (route) => {
|
||||
await verifyGate;
|
||||
await route.fulfill({ json: onboardingStatus("a", "Client A is verified.", true) });
|
||||
});
|
||||
|
||||
await page.goto("/clients?onboard=a");
|
||||
await expect(page.getByText("Client A is awaiting its external subject.")).toBeVisible();
|
||||
const verifyRequest = page.waitForRequest((request) => request.url().endsWith("/api/client-management/a/verify"));
|
||||
await page.getByRole("button", { name: "Record Hub checks" }).click();
|
||||
await verifyRequest;
|
||||
await page.evaluate(() => {
|
||||
window.history.pushState({}, "", "/clients?onboard=b");
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
});
|
||||
await expect(page.getByText("Client B is awaiting its external subject.")).toBeVisible();
|
||||
|
||||
releaseVerify();
|
||||
await expect(page.getByText("Client B is awaiting its external subject.")).toBeVisible();
|
||||
await expect(page.getByText("Client A is verified.")).toHaveCount(0);
|
||||
await expect(page.getByText(/Hub checks passed/)).toHaveCount(0);
|
||||
await expect(page).toHaveURL(/\/clients\?onboard=b$/);
|
||||
});
|
||||
|
||||
test("a delayed catalog preview for the previous project cannot be shown or applied", async ({ page }) => {
|
||||
await page.route((url) => url.pathname === "/api/projects" && url.searchParams.has("limit"), (route) => route.fulfill({ json: {
|
||||
kind: "projects",
|
||||
generatedAt: "2026-10-01T10:00:00.000Z",
|
||||
rows: [
|
||||
{ id: "ludarium", name: "Ludarium", status: "healthy", summary: "Fixture project A" },
|
||||
{ id: "forgeflow", name: "ForgeFlow", status: "healthy", summary: "Fixture project B" },
|
||||
],
|
||||
page: { total: 2 },
|
||||
} }));
|
||||
|
||||
let releasePreview!: () => void;
|
||||
const previewGate = new Promise<void>((resolve) => { releasePreview = resolve; });
|
||||
const previewProjects: string[] = [];
|
||||
await page.route("**/api/catalog/preview", async (route) => {
|
||||
const body = route.request().postDataJSON() as { project: string };
|
||||
previewProjects.push(body.project);
|
||||
if (body.project === "ludarium") {
|
||||
await previewGate;
|
||||
await route.fulfill({ json: { valid: true, changes: ["Project A private preview"], errors: [] } });
|
||||
return;
|
||||
}
|
||||
await route.fulfill({ json: { valid: true, changes: ["Project B preview"], errors: [] } });
|
||||
});
|
||||
|
||||
await page.goto("/tools");
|
||||
const workflow = page.locator(".workflow-panel").filter({ has: page.getByRole("heading", { name: "Catalog validation and preview" }) });
|
||||
const project = workflow.getByLabel("Project");
|
||||
await expect(project.locator("option")).toHaveCount(2);
|
||||
await project.selectOption("ludarium");
|
||||
|
||||
const previewRequest = page.waitForRequest((request) => new URL(request.url()).pathname === "/api/catalog/preview");
|
||||
await workflow.getByRole("button", { name: "Validate and preview" }).click();
|
||||
await previewRequest;
|
||||
try {
|
||||
await project.selectOption("forgeflow");
|
||||
await expect(workflow.locator(".diff-box")).toHaveCount(0);
|
||||
releasePreview();
|
||||
await expect(workflow.getByText("Project A private preview")).toHaveCount(0);
|
||||
await expect(workflow.getByRole("button", { name: "Apply approved catalog" })).toHaveCount(0);
|
||||
expect(previewProjects).toEqual(["ludarium"]);
|
||||
} finally {
|
||||
releasePreview();
|
||||
}
|
||||
});
|
||||
|
||||
test("a delayed project detail response is ignored after navigating to another project", async ({ page }) => {
|
||||
let releaseProjectA!: () => void;
|
||||
const projectGate = new Promise<void>((resolve) => { releaseProjectA = resolve; });
|
||||
await page.route("**/api/projects/a", async (route) => {
|
||||
await projectGate;
|
||||
await route.fulfill({ json: projectDetailFixture("a") });
|
||||
});
|
||||
await page.route("**/api/projects/b", (route) => route.fulfill({ json: projectDetailFixture("b") }));
|
||||
|
||||
const projectARequest = page.waitForRequest((request) => new URL(request.url()).pathname === "/api/projects/a");
|
||||
await page.goto("/projects/a");
|
||||
await projectARequest;
|
||||
await page.evaluate(() => {
|
||||
window.history.pushState({}, "", "/projects/b");
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
});
|
||||
try {
|
||||
await expect(page.getByRole("heading", { name: "Project B", exact: true })).toBeVisible();
|
||||
releaseProjectA();
|
||||
await expect(page.getByRole("heading", { name: "Project A", exact: true })).toHaveCount(0);
|
||||
await expect(page).toHaveURL(/\/projects\/b$/);
|
||||
} finally {
|
||||
releaseProjectA();
|
||||
}
|
||||
});
|
||||
|
||||
test("a delayed connector detail response is ignored after navigating to another connector", async ({ page }) => {
|
||||
let releaseConnectorA!: () => void;
|
||||
const connectorGate = new Promise<void>((resolve) => { releaseConnectorA = resolve; });
|
||||
await page.route("**/api/connectors/a", async (route) => {
|
||||
await connectorGate;
|
||||
await route.fulfill({ json: connectorDetailFixture("a") });
|
||||
});
|
||||
await page.route("**/api/connectors/b", (route) => route.fulfill({ json: connectorDetailFixture("b") }));
|
||||
|
||||
const connectorARequest = page.waitForRequest((request) => new URL(request.url()).pathname === "/api/connectors/a");
|
||||
await page.goto("/connectors/a");
|
||||
await connectorARequest;
|
||||
await page.evaluate(() => {
|
||||
window.history.pushState({}, "", "/connectors/b");
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
});
|
||||
try {
|
||||
await expect(page.getByRole("heading", { name: "Connector B", exact: true })).toBeVisible();
|
||||
releaseConnectorA();
|
||||
await expect(page.getByRole("heading", { name: "Connector A", exact: true })).toHaveCount(0);
|
||||
await expect(page).toHaveURL(/\/connectors\/b$/);
|
||||
} finally {
|
||||
releaseConnectorA();
|
||||
}
|
||||
});
|
||||
|
||||
test("typed tool arguments reach the readiness API and recent runs retain metadata only", async ({ page }) => {
|
||||
const capturedRequests: Array<Record<string, unknown>> = [];
|
||||
await page.route(/\/api\/tools(?:\?.*)?$/, (route) => route.fulfill({ json: {
|
||||
kind: "tools",
|
||||
generatedAt: "2026-10-01T10:00:00.000Z",
|
||||
rows: [{ id: "typed-tool", name: "Typed Tool", status: "healthy", summary: "Typed schema fixture", project: "ludarium", environment: "prod", risk: "R1", canonicalName: "test.typed" }],
|
||||
page: { total: 1 },
|
||||
} }));
|
||||
await page.route("**/api/tool-definitions/test.typed", (route) => route.fulfill({ json: {
|
||||
canonicalName: "test.typed",
|
||||
description: "Typed schema fixture",
|
||||
readOnly: true,
|
||||
riskClass: "R1",
|
||||
inputSchema: { type: "object", required: ["count", "enabled", "mode", "tags", "filters"], properties: {
|
||||
count: { type: "integer", title: "Count", minimum: 1, maximum: 10 },
|
||||
enabled: { type: "boolean", title: "Enabled" },
|
||||
mode: { type: "string", title: "Mode", enum: ["safe", "full"] },
|
||||
tags: { type: "array", title: "Tags" },
|
||||
filters: { type: "object", title: "Filters" },
|
||||
} },
|
||||
limits: { timeoutMs: 1000, maxOutputBytes: 2048, maxItems: 10 },
|
||||
} }));
|
||||
await page.route("**/api/tools/test", async (route) => {
|
||||
capturedRequests.push(route.request().postDataJSON() as Record<string, unknown>);
|
||||
await route.fulfill({ json: {
|
||||
ready: true,
|
||||
tool: "test.typed",
|
||||
project: "ludarium",
|
||||
environment: "prod",
|
||||
note: "Readiness only; no MCP tool invocation was performed.",
|
||||
checks: {
|
||||
catalog: { valid: true, readOnly: true, connectorType: "fixture" },
|
||||
input: { valid: true, errors: [] },
|
||||
binding: { valid: true, detail: "Fixture binding" },
|
||||
policy: { allowed: true, decision: "allow", reasonCode: "FIXTURE_ALLOWED", traceId: "ready-trace" },
|
||||
connector: { reachable: true, status: "healthy", detail: "Fixture route" },
|
||||
},
|
||||
} });
|
||||
});
|
||||
await page.route("**/api/tools/invoke", (route) => route.fulfill({ json: {
|
||||
ok: false,
|
||||
traceId: "trace-typed-001",
|
||||
tool: "test.typed",
|
||||
content: [{ type: "text", text: "controlled fixture result" }],
|
||||
error: { code: "controlled", message: "The fixture returned a controlled error." },
|
||||
} }));
|
||||
|
||||
await page.goto("/tools");
|
||||
await page.getByRole("button", { name: "Readiness" }).click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog.getByRole("button", { name: "Run readiness check" })).toBeDisabled();
|
||||
await dialog.getByLabel("Project").selectOption("ludarium");
|
||||
await dialog.getByLabel("Environment").selectOption("prod");
|
||||
await dialog.getByLabel("Count").fill("4");
|
||||
await dialog.getByLabel("Enabled").selectOption("true");
|
||||
await dialog.getByLabel("Mode").selectOption({ label: "full" });
|
||||
await dialog.getByLabel("Tags").fill('["never-store-tags"]');
|
||||
await dialog.getByLabel("Filters").fill('{"author":"never-store-filter"}');
|
||||
await dialog.getByRole("button", { name: "Run readiness check" }).click();
|
||||
await expect(dialog.getByText("Ready", { exact: true })).toBeVisible();
|
||||
expect(capturedRequests[0]).toMatchObject({
|
||||
project: "ludarium",
|
||||
environment: "prod",
|
||||
tool: "test.typed",
|
||||
arguments: { count: 4, enabled: true, mode: "full", tags: ["never-store-tags"], filters: { author: "never-store-filter" } },
|
||||
});
|
||||
|
||||
await dialog.getByLabel("Count").fill("5");
|
||||
await expect(dialog.getByText("Ready", { exact: true })).toHaveCount(0);
|
||||
await expect(dialog.getByRole("button", { name: "Run tool" })).toBeDisabled();
|
||||
await dialog.getByRole("button", { name: "Run readiness check" }).click();
|
||||
await expect(dialog.getByText("Ready", { exact: true })).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Run tool" }).click();
|
||||
await expect(dialog.getByRole("heading", { name: "Controlled error" })).toBeVisible();
|
||||
|
||||
const storedHistory = await page.evaluate(() => sessionStorage.getItem("itworx-tool-history-v1"));
|
||||
expect(storedHistory).toContain("trace-typed-001");
|
||||
expect(storedHistory).not.toContain("never-store-tags");
|
||||
expect(storedHistory).not.toContain("never-store-filter");
|
||||
expect(storedHistory).not.toContain("controlled fixture result");
|
||||
await dialog.getByRole("button", { name: "Close dialog" }).click();
|
||||
|
||||
const recentRuns = page.getByRole("region", { name: "Recent Tool Runs" });
|
||||
await expect(recentRuns.getByText("Controlled error")).toBeVisible();
|
||||
await expect(recentRuns.getByText("trace-typed-001")).toBeVisible();
|
||||
await recentRuns.getByRole("button", { name: "Open tool context" }).click();
|
||||
await expect(page).toHaveURL(/\/tools\?reopen=test\.typed&project=ludarium&environment=prod/);
|
||||
await expect(page.getByRole("dialog", { name: /Tool console/ })).toBeVisible();
|
||||
await expect(page.getByRole("dialog").getByLabel("Project")).toHaveValue("ludarium");
|
||||
await expect(page.getByRole("dialog").getByLabel("Environment")).toHaveValue("prod");
|
||||
});
|
||||
});
|
||||
@@ -33,12 +33,12 @@ test.describe("product-quality interaction and composition", () => {
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Quick jump" });
|
||||
await expect(dialog).toBeVisible();
|
||||
const search = dialog.getByLabel("Find a workspace");
|
||||
const search = dialog.getByRole("combobox", { name: "Find a workspace or resource" });
|
||||
await expect(search).toBeFocused();
|
||||
await expect(dialog.getByRole("button", { name: /Audit/ })).toHaveCount(0);
|
||||
await expect(dialog.getByRole("option", { name: /Audit/ })).toHaveCount(0);
|
||||
|
||||
await search.fill("catalog");
|
||||
await dialog.getByRole("button", { name: /Catalog/ }).click();
|
||||
await dialog.getByRole("group", { name: "Build", exact: true }).getByRole("option", { name: /Catalog/ }).click();
|
||||
await expect(page).toHaveURL(/\/tools$/);
|
||||
await expect(page.getByRole("heading", { name: "Catalog", level: 2 })).toBeVisible();
|
||||
});
|
||||
@@ -48,7 +48,7 @@ test.describe("product-quality interaction and composition", () => {
|
||||
await page.goto("/tools");
|
||||
const filter = page.getByLabel("Filter records");
|
||||
await filter.fill(" Fleet Ops ");
|
||||
await expect(page).toHaveURL(/\/tools\?q=Fleet%20Ops$/);
|
||||
await expect.poll(() => new URL(page.url()).searchParams.get("q")).toBe("Fleet Ops");
|
||||
await expect(page.getByText("Updating results…")).toHaveCount(0);
|
||||
|
||||
await filter.fill(" ");
|
||||
@@ -79,7 +79,7 @@ test.describe("product-quality interaction and composition", () => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
|
||||
await page.goto("/tools");
|
||||
await expect(page.getByRole("heading", { name: "Catalog", level: 2 })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Catalog", level: 2 })).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByLabel("Filter records")).toBeVisible();
|
||||
await expectNoPageOverflow(page);
|
||||
if (captureReview) await page.screenshot({ path: testInfo.outputPath("catalog-desktop.png"), fullPage: false });
|
||||
|
||||
@@ -29,13 +29,13 @@ test.describe("management workflows, driven end to end from the browser", () =>
|
||||
}) => {
|
||||
await page.goto("/projects/ludarium?tab=Environments");
|
||||
await expect(page.getByLabel("Project detail sections").getByRole("link", { name: "Environments" })).toHaveAttribute("aria-current", "page");
|
||||
await expect(page.getByRole("cell", { name: "prod", exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel("Project environments", { exact: true }).getByRole("heading", { name: "prod", exact: true })).toBeVisible();
|
||||
// ludarium never registered a staging environment (docs/known-state, and
|
||||
// tests/test_wp230_ui_completion.py's equivalent backend assertion).
|
||||
await expect(page.getByRole("cell", { name: "staging" })).toHaveCount(0);
|
||||
await expect(page.getByLabel("Project environments", { exact: true }).getByRole("heading", { name: "staging", exact: true })).toHaveCount(0);
|
||||
|
||||
await page.getByLabel("Project detail sections").getByRole("link", { name: "Connectors" }).click();
|
||||
await expect(page.getByRole("cell", { name: "knowledge", exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel("Connector bindings", { exact: true }).getByRole("heading", { name: "knowledge", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("an unregistered project is a real 404, not a fabricated default", async ({ page }) => {
|
||||
@@ -63,12 +63,14 @@ test.describe("management workflows, driven end to end from the browser", () =>
|
||||
await page.goto("/tools");
|
||||
await page.getByRole("button", { name: "Validate and preview" }).click();
|
||||
const outcome = page.getByText(/Valid preview|Validation blocked/);
|
||||
await expect(outcome).toBeVisible();
|
||||
await expect(outcome).toBeVisible({ timeout: 15_000 });
|
||||
const applyButton = page.getByRole("button", { name: "Apply approved catalog" });
|
||||
if (await outcome.filter({ hasText: "Valid preview" }).count()) {
|
||||
await expect(applyButton).toBeEnabled();
|
||||
await applyButton.click();
|
||||
await expect(page.getByRole("dialog")).toContainText("All registered project routes may be updated");
|
||||
await page.getByRole("button", { name: "Confirm apply" }).click();
|
||||
await expect(page.getByText(/Catalog reconciliation completed and verified/)).toBeVisible({ timeout: 15_000 });
|
||||
} else {
|
||||
await expect(applyButton).toBeDisabled();
|
||||
}
|
||||
@@ -167,10 +169,10 @@ test.describe("management workflows, driven end to end from the browser", () =>
|
||||
test("tool readiness validates the full registered path without invoking the tool", async ({ page }) => {
|
||||
await page.goto("/tools");
|
||||
await page.getByRole("button", { name: "Validate and preview" }).click();
|
||||
await expect(page.getByText("Valid preview")).toBeVisible();
|
||||
await expect(page.getByText("Valid preview")).toBeVisible({ timeout: 15_000 });
|
||||
await page.getByRole("button", { name: "Apply approved catalog" }).click();
|
||||
await page.getByRole("button", { name: "Confirm apply" }).click();
|
||||
await expect(page.getByText(/Catalog reconciliation completed and verified/)).toBeVisible();
|
||||
await expect(page.getByText(/Catalog reconciliation completed and verified/)).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await page.getByLabel("Filter records").fill("Fleet Ops Operations Summary");
|
||||
const row = page.getByRole("row", { name: /Fleet Ops Operations Summary/ });
|
||||
|
||||
Reference in new issue
Block a user