145 lines
7.5 KiB
TypeScript
145 lines
7.5 KiB
TypeScript
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();
|
|
});
|
|
});
|