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(); }); });