233 lines
13 KiB
TypeScript
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();
|
|
});
|
|
});
|