Files
ITWorx_MCP_Hub-Public/apps/web/tests/e2e/workflows.spec.ts
T

233 lines
13 KiB
TypeScript

import { expect, loginAs, test } from "./fixtures";
test.describe("management workflows, driven end to end from the browser", () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, "hub-admin");
});
test("tool catalog pagination is real: Next/Previous move through disjoint, server-issued pages", async ({
page,
}) => {
await page.goto("/tools");
const firstRowId = () => page.locator("table tbody tr").first().locator("small").first().textContent();
const firstPageFirstId = await firstRowId();
await expect(page.getByRole("button", { name: "Previous page" })).toBeDisabled();
await page.getByRole("button", { name: "Next page" }).click();
await expect(page.locator("table tbody tr").first().locator("small").first()).not.toHaveText(firstPageFirstId ?? "");
const secondPageFirstId = await firstRowId();
await page.getByRole("button", { name: "Previous page" }).click();
await expect(page.locator("table tbody tr").first().locator("small").first()).toHaveText(firstPageFirstId ?? "");
await page.getByRole("button", { name: "Next page" }).click();
await expect(page.locator("table tbody tr").first().locator("small").first()).toHaveText(secondPageFirstId ?? "");
});
test("project detail renders real registered environments and connector bindings, not a fixed 'prod'", async ({
page,
}) => {
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.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.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.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 }) => {
await page.goto("/projects/does-not-exist");
await expect(page.getByRole("alert")).toBeVisible();
await expect(page.getByText(/is not registered/)).toBeVisible();
});
test("policy simulation is wired to the real engine: a known-allow case explains its decision", async ({
page,
}) => {
await page.goto("/policies");
await page.getByLabel("Project").selectOption({ label: "Ludarium" });
await page.getByLabel("Environment").selectOption("prod");
await page.getByLabel("Tool").selectOption({ label: "Knowledge Document Search" });
await page.getByRole("button", { name: "Explain decision" }).click();
const decisionCard = page.getByRole("status").filter({ hasText: "Matched:" });
await expect(decisionCard).toBeVisible();
await expect(decisionCard.getByText(/Trace [0-9a-f-]{8,}/)).toBeVisible();
await expect(decisionCard.getByText("Matched: none")).toHaveCount(0);
});
test("catalog preview surfaces a real validation result, driven by a real project selector", async ({ page }) => {
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({ 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();
}
});
test("connector state is communicated as visible text, not color alone", async ({ page }) => {
await page.goto("/connectors");
await expect(page.locator("table tbody tr").first()).toBeVisible();
const statusCells = page.locator("table tbody tr td .status");
const count = await statusCells.count();
expect(count).toBeGreaterThan(0);
for (let index = 0; index < count; index += 1) {
const text = (await statusCells.nth(index).innerText()).trim();
expect(["Healthy", "Degraded", "Unavailable", "Unknown"]).toContain(text);
}
});
test("table density toggle changes row padding without changing row count or data", async ({ page }) => {
await page.goto("/tools");
await expect(page.locator("table tbody tr").first()).toBeVisible();
const rowCountBefore = await page.locator("table tbody tr").count();
await page.getByRole("button", { name: "Compact" }).click();
await expect(page.locator(".table-wrap")).toHaveClass(/density-compact/);
expect(await page.locator("table tbody tr").count()).toBe(rowCountBefore);
await page.getByRole("button", { name: "Comfortable" }).click();
await expect(page.locator(".table-wrap")).toHaveClass(/density-comfortable/);
});
test("density toggle exposes which option is selected, not just the applied state", async ({ page }) => {
// Regression test for a real bug found during WP-235's visual polish
// pass: the Stitch retokenization made .button's default look a solid
// accent pill, which is also what the "active" class resolved to --
// Comfortable and Compact rendered identically, so neither the sighted
// UI nor assistive tech could tell which density was selected.
await page.goto("/tools");
const comfortable = page.getByRole("button", { name: "Comfortable" });
const compact = page.getByRole("button", { name: "Compact" });
await expect(comfortable).toHaveAttribute("aria-pressed", "true");
await expect(compact).toHaveAttribute("aria-pressed", "false");
await expect(compact).toHaveClass(/button-secondary/);
await compact.click();
await expect(comfortable).toHaveAttribute("aria-pressed", "false");
await expect(compact).toHaveAttribute("aria-pressed", "true");
await expect(comfortable).toHaveClass(/button-secondary/);
});
test("policy simulation renders a step-by-step decision trace, not just the top-line result", async ({ page }) => {
await page.goto("/policies");
await page.getByLabel("Project").selectOption({ label: "Ludarium" });
await page.getByLabel("Environment").selectOption("prod");
await page.getByLabel("Tool").selectOption({ label: "Knowledge Document Search" });
await page.getByRole("button", { name: "Explain decision" }).click();
await expect(page.getByText("Decision logic trace")).toBeVisible();
const steps = page.locator(".trace-step");
await expect(steps.first()).toBeVisible();
expect(await steps.count()).toBeGreaterThan(0);
// Every rendered step must carry a real phase name from the engine's
// trace, not an empty placeholder row.
const firstStepPhase = await steps.first().locator("strong").innerText();
expect(firstStepPhase.trim().length).toBeGreaterThan(0);
});
test("generated client configuration never renders secret material into the DOM", async ({ page }) => {
await page.goto("/clients");
await page.getByLabel("Name").fill("e2e-config-check");
await page.getByRole("button", { name: "Create pending client" }).click();
await expect(page.getByText(/Client created: e2e-config-check/)).toBeVisible();
await page.reload();
const targetRow = page.getByRole("row", { name: /e2e-config-check/ });
await expect(targetRow).toBeVisible();
await targetRow.getByRole("button", { name: "Config" }).click();
await expect(page.getByText(/Generated configuration for/)).toBeVisible();
const bodyText = await page.locator("body").innerText();
expect(bodyText).not.toMatch(/"secret"|client_secret|bearer [a-z0-9]{20,}/i);
expect(bodyText).toContain("Secret material is write-only");
});
test("client onboarding does not claim that the Hub provisions an Authentik identity", async ({ page }) => {
await page.goto("/clients");
await expect(page.getByText(/External identity provisioning is a separate administrator task/)).toBeVisible();
await expect(page.getByText(/Authentik hand-off reference/)).toHaveCount(0);
});
test("Operations page renders the same real drift/backup/denial data as the dashboard, not a fabricated summary", async ({ page }) => {
await page.goto("/");
const dashboardDrift = await page.locator(".metric-card", { hasText: "Reconcile drift" }).locator("strong").innerText();
await page.goto("/operations");
await expect(page.getByRole("heading", { name: "Operations" })).toBeVisible();
const operationsDrift = await page.locator(".metric-card", { hasText: "Reconcile drift" }).locator("strong").innerText();
expect(operationsDrift).toBe(dashboardDrift);
});
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({ 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({ timeout: 15_000 });
await page.getByLabel("Filter records").fill("Fleet Ops Operations Summary");
const row = page.getByRole("row", { name: /Fleet Ops Operations Summary/ });
await row.getByRole("button", { name: "Readiness" }).click();
await page.getByRole("dialog").getByLabel("Project").selectOption({ label: "Fleet Ops" });
await page.getByRole("dialog").getByLabel("Environment").selectOption("prod");
await page.getByRole("button", { name: "Run readiness check" }).click();
await expect(page.getByRole("dialog").getByText("Ready", { exact: true })).toBeVisible();
await expect(page.getByRole("dialog").getByText(/no MCP tool invocation was performed/i)).toBeVisible();
await expect(page.getByRole("dialog").getByText(/healthy/i).first()).toBeVisible();
});
test("closing a Modal returns keyboard focus to the element that opened it, not document.body", async ({ page }) => {
// Regression test for a real bug found during WP-235 Task 12's manual
// accessibility pass: Modal moved focus into the dialog on open but
// never restored it on close, silently dropping keyboard users back to
// document.body. Uses the tools "Readiness" button's Modal as an isolated,
// side-effect-free trigger.
await page.goto("/tools");
const trigger = page.locator("table tbody tr").first().getByRole("button", { name: "Readiness" });
await trigger.click();
await expect(page.getByRole("dialog")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog")).toHaveCount(0);
await expect(trigger).toBeFocused();
});
test("an open Modal locks background scroll and traps keyboard focus", async ({ page }) => {
await page.goto("/tools");
await page.locator("table tbody tr").first().getByRole("button", { name: "Readiness" }).click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await expect.poll(() => page.evaluate(() => document.body.style.overflow)).toBe("hidden");
await page.keyboard.press("Control+K");
await expect(page.getByRole("dialog")).toHaveCount(1);
await expect(page.getByRole("dialog", { name: "Quick jump" })).toHaveCount(0);
const close = dialog.getByRole("button", { name: "Close dialog" });
const readiness = dialog.getByRole("button", { name: "Run readiness check" });
await expect(readiness).toBeEnabled();
await close.focus();
await page.keyboard.press("Shift+Tab");
await expect(readiness).toBeFocused();
await page.keyboard.press("Tab");
await expect(close).toBeFocused();
await close.click();
await expect.poll(() => page.evaluate(() => document.body.style.overflow)).toBe("");
});
test("Operations page shows an honest placeholder for approvals, not a fabricated queue", async ({ page }) => {
await page.goto("/operations");
await expect(page.getByRole("heading", { name: "Approvals" })).toBeVisible();
await expect(page.getByText(/Approval workflow is not yet part of the authorized v1 management surface/)).toBeVisible();
});
});