import { expect, type Page, test } from "./utils/test"; import { getTenantCredentials, loginAsTenantUser, tenantSettingsAccountPath, tenantSettingsProfilePath, tenantSettingsSubscriptionPath } from "./utils/auth"; import { expectNoCriticalA11yViolations } from "./utils/axe"; import { compactMobileViewport, expectNoHorizontalOverflow, narrowMobileViewport } from "./utils/mobile"; import { escapeRegExp, skipIfUnavailable } from "./utils/navigation"; import { skipOrFailWhenE2eFixtureMissing, skipOrFailWhenE2eInputsMissing, skipOrFailWhenE2eReadinessOptInMissing } from "./utils/readiness"; const credentials = getTenantCredentials(); const dashboardPath = process.env.E2E_TENANT_DASHBOARD_PATH ?? "/app/dashboard"; const pluginSettingsPath = process.env.E2E_PLUGIN_SETTINGS_PATH; const workflowBuilderPath = process.env.E2E_WORKFLOW_BUILDER_PATH; const workflowRunManualPath = process.env.E2E_WORKFLOW_RUN_MANUAL_PATH; const mobileSidebarSelector = '[data-ui-id="ui.surface.app.sidebar.mobile"]'; type MobileShellRouteCheck = { name: string; path: string; readySelector?: string; }; function requireMobileShellInputs(reason: string): void { skipOrFailWhenE2eInputsMissing(["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD"], reason); } async function expectFocusInsideMobileSidebar(page: Page, stepName: string): Promise { await expect .poll( async () => page.locator(mobileSidebarSelector).evaluate((sidebar) => { const activeElement = document.activeElement; return Boolean(activeElement && sidebar.contains(activeElement)); }), { message: `focus remains inside the mobile sidebar after ${stepName}` } ) .toBe(true); } async function countMobileSidebarFocusableItems(page: Page): Promise { return page.locator(mobileSidebarSelector).evaluate((sidebar) => { const focusableSelector = [ "a[href]", "button:not([disabled])", "input:not([disabled])", "select:not([disabled])", "textarea:not([disabled])", '[tabindex]:not([tabindex="-1"])', ].join(","); return Array.from(sidebar.querySelectorAll(focusableSelector)).filter((element) => { const style = window.getComputedStyle(element); return style.visibility !== "hidden" && style.display !== "none" && element.offsetParent !== null; }).length; }); } async function expectMobileShellControlsUsable(page: Page): Promise { const sidebarTrigger = page.locator('[data-ui-id="ui.action.shell.sidebarTrigger"]'); const moreTrigger = page.locator('[data-ui-id="ui.action.shell.more"]'); const mobileSidebar = page.locator(mobileSidebarSelector); await expect(sidebarTrigger).toBeVisible(); await expect(moreTrigger).toBeVisible(); await expectNoHorizontalOverflow(page); // A route change fires the sidebar's setOpenMobile(false) effect; a single // click can race that close and leave the drawer shut. Retry-open, only // clicking while it's closed, so the toggle never clobbers an open drawer. await expect(async () => { if (!(await mobileSidebar.isVisible())) { await sidebarTrigger.click(); } await expect(mobileSidebar).toBeVisible({ timeout: 2000 }); }).toPass({ timeout: 15000 }); await expect(mobileSidebar.locator('[data-ui-id="ui.nav.app.dashboard"]').first()).toBeVisible(); await expect(mobileSidebar.locator('[data-ui-id="ui.nav.app.settings"]').first()).toBeVisible(); await page.keyboard.press("Escape"); await expect(mobileSidebar).toBeHidden(); } async function expectAuthenticatedMobileShellRoute(page: Page, routeCheck: MobileShellRouteCheck): Promise { await expect(page).toHaveURL(pathPattern(routeCheck.path)); await expect(page).not.toHaveURL(/\/login(?:[?#].*)?$/u); if (routeCheck.readySelector) { await expect(page.locator(routeCheck.readySelector).first()).toBeVisible({ timeout: 10_000 }); } await expectMobileShellControlsUsable(page); } async function gotoMobileShellRoute(page: Page, routeCheck: MobileShellRouteCheck): Promise { const response = await page.goto(routeCheck.path, { waitUntil: "domcontentloaded" }); expect(response?.status() ?? 200).toBeLessThan(500); await skipIfUnavailable(page, response, `${routeCheck.name} path ${routeCheck.path}`); await expectAuthenticatedMobileShellRoute(page, routeCheck); } async function expectCrossSurfaceHistoryResizeReload(page: Page, originRoute: MobileShellRouteCheck, targetRoute: MobileShellRouteCheck): Promise { await gotoMobileShellRoute(page, originRoute); await gotoMobileShellRoute(page, targetRoute); await page.goBack({ waitUntil: "domcontentloaded" }); await expectAuthenticatedMobileShellRoute(page, originRoute); await page.goForward({ waitUntil: "domcontentloaded" }); await expectAuthenticatedMobileShellRoute(page, targetRoute); await page.setViewportSize(compactMobileViewport); await expectAuthenticatedMobileShellRoute(page, targetRoute); await page.setViewportSize(narrowMobileViewport); const response = await page.reload({ waitUntil: "domcontentloaded" }); expect(response?.status() ?? 200).toBeLessThan(500); await skipIfUnavailable(page, response, `${targetRoute.name} reload ${targetRoute.path}`); await expectAuthenticatedMobileShellRoute(page, targetRoute); } function pathPattern(path: string): RegExp { return new RegExp(`${escapeRegExp(path)}(?:[?#].*)?$`, "u"); } test.describe("authenticated mobile shell flow", () => { test.use({ viewport: narrowMobileViewport, isMobile: true, hasTouch: true }); test.beforeEach(() => { requireMobileShellInputs("Authenticated mobile shell flow inputs not configured"); }); test("opens the mobile sidebar and navigates from dashboard to settings", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); const mobileSidebar = page.locator('[data-ui-id="ui.surface.app.sidebar.mobile"], [data-mobile="true"]'); await expect(mobileSidebar).toBeHidden(); await page.locator('[data-ui-id="ui.action.shell.sidebarTrigger"]').click(); await expect(mobileSidebar).toBeVisible(); await expect(mobileSidebar).toHaveAttribute("role", "dialog"); await expect(mobileSidebar).toHaveAccessibleName(/.+/u); await expect(mobileSidebar).toHaveAccessibleDescription(/.+/u); const settingsLink = mobileSidebar.locator('[data-ui-id="ui.nav.app.settings"]').first(); await expect(settingsLink).toBeVisible(); await settingsLink.click(); const settingsProfilePath = tenantSettingsProfilePath(dashboardPath); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(settingsProfilePath)}(?:[?#].*)?$`, "u")); await expect(mobileSidebar).toBeHidden(); await expectNoHorizontalOverflow(page); }); test("closes the mobile sidebar with Escape without losing shell navigation", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); const trigger = page.locator('[data-ui-id="ui.action.shell.sidebarTrigger"]'); const mobileSidebar = page.locator('[data-ui-id="ui.surface.app.sidebar.mobile"], [data-mobile="true"]'); await expect(mobileSidebar).toBeHidden(); await trigger.click(); await expect(mobileSidebar).toBeVisible(); await expect(mobileSidebar.locator('[data-ui-id="ui.nav.app.dashboard"]').first()).toBeVisible(); await expect(mobileSidebar.locator('[data-ui-id="ui.nav.app.settings"]').first()).toBeVisible(); await page.keyboard.press("Escape"); await expect(mobileSidebar).toBeHidden(); await expect(trigger).toBeVisible(); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); }); test("traps keyboard focus inside the mobile sidebar and returns focus to the trigger", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); const trigger = page.locator('[data-ui-id="ui.action.shell.sidebarTrigger"]'); const mobileSidebar = page.locator(mobileSidebarSelector); await expect(mobileSidebar).toBeHidden(); await trigger.focus(); await expect(trigger).toBeFocused(); await page.keyboard.press("Enter"); await expect(mobileSidebar).toBeVisible(); await expectFocusInsideMobileSidebar(page, "opening"); const focusableItemCount = await countMobileSidebarFocusableItems(page); expect(focusableItemCount).toBeGreaterThan(0); for (let index = 0; index < focusableItemCount + 2; index += 1) { await page.keyboard.press("Tab"); await expectFocusInsideMobileSidebar(page, `Tab ${index + 1}`); } await page.keyboard.press("Escape"); await expect(mobileSidebar).toBeHidden(); await expect(trigger).toBeVisible(); await expect(trigger).toBeFocused(); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); }); test("keyboard shortcut toggles the mobile sidebar without changing route", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); const mobileSidebar = page.locator('[data-mobile="true"]'); await expect(mobileSidebar).toBeHidden(); await page.keyboard.press("Control+e"); await expect(mobileSidebar).toBeVisible(); await expect(mobileSidebar.locator('[data-ui-id="ui.nav.app.dashboard"]').first()).toBeVisible(); await expect(mobileSidebar.locator('[data-ui-id="ui.nav.app.settings"]').first()).toBeVisible(); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await page.keyboard.press("Control+e"); await expect(mobileSidebar).toBeHidden(); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); }); test("mobile more menu exposes profile and sign-out contracts without submitting", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); await page.locator('[data-ui-id="ui.action.shell.more"]').click(); const profileTarget = page.locator('[data-ui-id="ui.action.shell.profile"]').first(); await expect(profileTarget).toBeVisible(); await expect(profileTarget.locator("xpath=ancestor::a[1]")).toHaveAttribute("href", /settings\/profile/u); const signOut = page.locator('[data-ui-id="ui.action.shell.signOut"]').first(); await expect(signOut).toBeVisible(); await expect(signOut.locator("xpath=ancestor::form[1]")).toHaveAttribute("action", /\/logout$/u); await expectNoCriticalA11yViolations(page); await expectNoHorizontalOverflow(page); }); test("mobile more menu closes with Escape and returns control to the trigger", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); const trigger = page.locator('[data-ui-id="ui.action.shell.more"]'); await trigger.click(); const signOut = page.locator('[data-ui-id="ui.action.shell.signOut"]').first(); await expect(signOut).toBeVisible(); await page.keyboard.press("Escape"); await expect(signOut).toBeHidden(); await expect(trigger).toBeVisible(); await expect(trigger).toBeFocused(); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); }); test("mobile command palette focuses search and closes with Escape", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); const trigger = page.locator('[data-ui-id="ui.action.shell.search"]'); await trigger.click(); const palette = page.locator('[data-ui-id="ui.surface.commandPalette.portal"]'); const searchInput = page.locator('[data-ui-id="ui.surface.commandPalette.search"]'); await expect(palette).toBeVisible(); await expect(searchInput).toBeVisible(); await expect(searchInput).toBeFocused(); await searchInput.fill("settings"); await page.keyboard.press("Escape"); await expect(palette).toBeHidden(); await expect(trigger).toBeVisible(); await expect(trigger).toBeFocused(); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); }); test("mobile command palette navigates from a selected result", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); await page.locator('[data-ui-id="ui.action.shell.search"]').click(); const palette = page.locator('[data-ui-id="ui.surface.commandPalette.portal"]'); const searchInput = page.locator('[data-ui-id="ui.surface.commandPalette.search"]'); const rows = page.locator('[data-ui-id="ui.surface.commandPalette.row"]'); await expect(palette).toBeVisible(); await expect(searchInput).toBeFocused(); await searchInput.fill("settings"); await expect(rows.first()).toBeVisible(); await rows.first().click(); await expect(palette).toBeHidden(); await expect(page).toHaveURL(/\/settings(?:\/[^?#]+)?(?:[?#].*)?$/u); await expectNoCriticalA11yViolations(page); await expectNoHorizontalOverflow(page); }); test("mobile command palette supports keyboard-only result selection", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); await page.keyboard.press("Control+k"); const palette = page.locator('[data-ui-id="ui.surface.commandPalette.portal"]'); const searchInput = page.locator('[data-ui-id="ui.surface.commandPalette.search"]'); const rows = page.locator('[data-ui-id="ui.surface.commandPalette.row"]'); await expect(palette).toBeVisible(); await expect(searchInput).toBeVisible(); await expect(searchInput).toBeFocused(); await searchInput.fill("settings"); await expect(rows.first()).toBeVisible(); await page.keyboard.press("Enter"); await expect(palette).toBeHidden(); await expect(page).toHaveURL(/\/settings(?:\/[^?#]+)?(?:[?#].*)?$/u); await expectNoCriticalA11yViolations(page); await expectNoHorizontalOverflow(page); }); test("signs out from the mobile more menu and protects the dashboard", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await page.locator('[data-ui-id="ui.action.shell.more"]').click(); const signOut = page.locator('[data-ui-id="ui.action.shell.signOut"]').first(); await expect(signOut).toBeVisible(); await signOut.click(); // Sign-out posts /logout.data via single-fetch and client-redirects to /login. // Wait for that landing so the cleared session cookie is committed before the // dashboard re-check — otherwise the next navigation races the logout response. await page.waitForURL(/\/login(?:[?#].*)?/u, { timeout: 15_000 }); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(/\/login(?:[?#].*)?/u); expect(new URL(page.url()).searchParams.get("redirectTo") ?? "").toContain(dashboardPath); }); test("keeps account settings accessible on mobile and returns to dashboard", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(tenantSettingsAccountPath(dashboardPath), { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(tenantSettingsAccountPath(dashboardPath))}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); await expectNoCriticalA11yViolations(page); const mobileSidebar = page.locator('[data-mobile="true"]'); await expect(mobileSidebar).toBeHidden(); await page.locator('[data-ui-id="ui.action.shell.sidebarTrigger"]').click(); await expect(mobileSidebar).toBeVisible(); const dashboardLink = mobileSidebar.locator('[data-ui-id="ui.nav.app.dashboard"]').first(); await expect(dashboardLink).toBeVisible(); await dashboardLink.click(); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expect(mobileSidebar).toBeHidden(); await expectNoHorizontalOverflow(page); }); test("keeps profile settings selectors stable on mobile without submitting", async ({ page }) => { await loginAsTenantUser(page, credentials!); const settingsProfilePath = tenantSettingsProfilePath(dashboardPath); await page.goto(settingsProfilePath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(settingsProfilePath)}(?:[?#].*)?$`, "u")); await expect(page.locator('[data-ui-id="ui.settings.profile.page"]').first()).toBeVisible(); const form = page.locator('[data-ui-id="ui.settings.profile.form"]').first(); await expect(form).toBeVisible(); await expect(form.locator('input[name="action"][value="profile"]')).toHaveCount(1); const email = form.locator('[data-ui-id="ui.settings.profile.email"]').first(); await expect(email).toBeVisible(); await expect(email).toBeDisabled(); await expect(email).not.toHaveValue(""); await expect(form.locator('[data-ui-id="ui.settings.profile.firstName"]').first()).toBeVisible(); await expect(form.locator('[data-ui-id="ui.settings.profile.lastName"]').first()).toBeVisible(); await expect(form.locator('[data-ui-id="ui.settings.profile.save"]').first()).toBeVisible(); await expect(page.locator('[data-ui-id="ui.settings.profile.session.panel"]').first()).toBeVisible(); await expect(page.locator('[data-ui-id="ui.settings.profile.danger.delete"]').first()).toBeVisible(); await expectNoHorizontalOverflow(page); }); test("keeps the mobile shell usable after hard reload on nested authenticated routes", async ({ page }) => { await loginAsTenantUser(page, credentials!); const routeChecks = [ { name: "account settings", path: tenantSettingsAccountPath(dashboardPath) }, { name: "profile settings", path: tenantSettingsProfilePath(dashboardPath) }, { name: "subscription settings", path: tenantSettingsSubscriptionPath(dashboardPath), readySelector: '[data-ui-id="ui.subscription.page"]' }, ...(pluginSettingsPath ? [{ name: "plugin settings", path: pluginSettingsPath }] : []), ...(workflowBuilderPath ? [{ name: "workflow builder", path: workflowBuilderPath }] : []), ]; for (const routeCheck of routeChecks) { await test.step(routeCheck.name, async () => { await page.goto(routeCheck.path, { waitUntil: "domcontentloaded" }); await expect(page).not.toHaveURL(/\/login(?:[?#].*)?$/u); if ("readySelector" in routeCheck) { await expect(page.locator(routeCheck.readySelector).first()).toBeVisible(); } const response = await page.reload({ waitUntil: "domcontentloaded" }); expect(response?.status() ?? 200).toBeLessThan(500); await expect(page).not.toHaveURL(/\/login(?:[?#].*)?$/u); if ("readySelector" in routeCheck) { await expect(page.locator(routeCheck.readySelector).first()).toBeVisible(); } const sidebarTrigger = page.locator('[data-ui-id="ui.action.shell.sidebarTrigger"]'); const moreTrigger = page.locator('[data-ui-id="ui.action.shell.more"]'); const mobileSidebar = page.locator(mobileSidebarSelector); await expect(sidebarTrigger).toBeVisible(); await expect(moreTrigger).toBeVisible(); await expectNoHorizontalOverflow(page); await sidebarTrigger.click(); await expect(mobileSidebar).toBeVisible(); await expect(mobileSidebar.locator('[data-ui-id="ui.nav.app.dashboard"]').first()).toBeVisible(); await expect(mobileSidebar.locator('[data-ui-id="ui.nav.app.settings"]').first()).toBeVisible(); await expectNoHorizontalOverflow(page); await page.keyboard.press("Escape"); await expect(mobileSidebar).toBeHidden(); await moreTrigger.click(); await expect(page.locator('[data-ui-id="ui.action.shell.signOut"]').first()).toBeVisible(); await page.keyboard.press("Escape"); await expect(moreTrigger).toBeFocused(); await expectNoCriticalA11yViolations(page); }); } }); test("keeps authenticated new-account creation form accessible on mobile without submitting", async ({ page }) => { await loginAsTenantUser(page, credentials!); const newAccountPath = process.env.E2E_NEW_ACCOUNT_PATH ?? "/new-account"; await page.goto(newAccountPath, { waitUntil: "domcontentloaded" }); await expect(page).not.toHaveURL(/\/login(?:[?#].*)?$/u); await expect(page.locator('form input[name="name"]').first()).toBeVisible(); await expect(page.locator('form input[name="action"][value="create"]').first()).toHaveCount(1); const csrfInput = page.locator('form input[name="csrf"]').first(); if ((await csrfInput.count()) > 0) { expect(await csrfInput.inputValue()).not.toBe(""); } await expect(page.locator('form button[type="submit"]').first()).toBeVisible(); await expect(page.locator('form button[type="submit"]').first()).toBeEnabled(); await expectNoHorizontalOverflow(page); await expectNoCriticalA11yViolations(page); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); }); test("submits new-account form contract without creating a tenant", async ({ page }) => { await loginAsTenantUser(page, credentials!); const newAccountPath = process.env.E2E_NEW_ACCOUNT_PATH ?? "/new-account"; await page.goto(newAccountPath, { waitUntil: "domcontentloaded" }); await expect(page).not.toHaveURL(/\/login(?:[?#].*)?$/u); const form = page.locator("form").first(); const nameInput = form.locator('input[name="name"]').first(); const actionInput = form.locator('input[name="action"][value="create"]').first(); const csrfInput = form.locator('input[name="csrf"]').first(); const submit = form.locator('button[type="submit"]').first(); if ((await nameInput.count()) === 0 || (await submit.count()) === 0) { skipOrFailWhenE2eFixtureMissing("New-account creation form is not rendered in this environment"); } await expect(nameInput).toBeVisible(); await expect(actionInput).toHaveCount(1); await expect(csrfInput).not.toHaveValue(""); await expect(submit).toBeEnabled(); const currentPathname = new URL(page.url()).pathname; let resolveSubmitted!: (formData: URLSearchParams) => void; const submitted = new Promise((resolve) => { resolveSubmitted = resolve; }); await page.route("**/*", async (route) => { const request = route.request(); const requestUrl = new URL(request.url()); // React Router single-fetch posts form actions to `${pathname}.data`. const matchesRoute = requestUrl.pathname === currentPathname || requestUrl.pathname === `${currentPathname}.data`; if (request.method() !== "POST" || !matchesRoute) { await route.continue(); return; } resolveSubmitted(new URLSearchParams(request.postData() ?? "")); await route.fulfill({ status: 204, body: "", }); }); await nameInput.fill("E2E Contract Account"); await submit.click(); const formData = await submitted; expect(formData.get("action")).toBe("create"); expect(formData.get("name")).toBe("E2E Contract Account"); expect(formData.get("csrf") ?? "").not.toBe(""); }); test("skip link moves keyboard focus to the main app region", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await page.keyboard.press("Tab"); const skipLink = page.getByTestId("skip-to-content"); await expect(skipLink).toBeVisible(); await expect(skipLink).toBeFocused(); await page.keyboard.press("Enter"); await expect(page.locator('[data-ui-id="ui.surface.shell.main"]')).toBeFocused(); await expectNoHorizontalOverflow(page); await expectNoCriticalA11yViolations(page); }); }); test.describe("authenticated mobile shell browser history slice", () => { test.use({ viewport: narrowMobileViewport, isMobile: true, hasTouch: true }); test.beforeEach(() => { requireMobileShellInputs("Authenticated mobile shell browser history inputs not configured"); skipOrFailWhenE2eReadinessOptInMissing("E2E_BROWSER_MATRIX", "skipping opt-in mobile shell browser history slice"); }); test("keeps shell usable across sidebar navigation, browser history, resize, and reload", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(pathPattern(dashboardPath)); await expectMobileShellControlsUsable(page); const settingsProfilePath = tenantSettingsProfilePath(dashboardPath); const mobileSidebar = page.locator(mobileSidebarSelector); await page.locator('[data-ui-id="ui.action.shell.sidebarTrigger"]').click(); await expect(mobileSidebar).toBeVisible(); await mobileSidebar.locator('[data-ui-id="ui.nav.app.settings"]').first().click(); await expect(page).toHaveURL(pathPattern(settingsProfilePath)); await expect(mobileSidebar).toBeHidden(); await expectMobileShellControlsUsable(page); await page.goBack({ waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(pathPattern(dashboardPath)); await expectMobileShellControlsUsable(page); await page.goForward({ waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(pathPattern(settingsProfilePath)); await expectMobileShellControlsUsable(page); await page.setViewportSize(compactMobileViewport); await expectMobileShellControlsUsable(page); await page.setViewportSize(narrowMobileViewport); const response = await page.reload({ waitUntil: "domcontentloaded" }); expect(response?.status() ?? 200).toBeLessThan(500); await expect(page).toHaveURL(pathPattern(settingsProfilePath)); await expectMobileShellControlsUsable(page); const originRoute = { name: "profile settings", path: settingsProfilePath, readySelector: '[data-ui-id="ui.settings.profile.page"]' }; const crossSurfaceRoutes = [ { name: "subscription settings", path: tenantSettingsSubscriptionPath(dashboardPath), readySelector: '[data-ui-id="ui.subscription.page"]' }, ...(pluginSettingsPath ? [{ name: "plugin settings", path: pluginSettingsPath, readySelector: '[data-ui-id="ui.plugins.page"]' }] : []), ...(workflowBuilderPath ? [{ name: "workflow builder", path: workflowBuilderPath, readySelector: '[data-ui-id="ui.workflows.builder.canvas"]' }] : []), ...(workflowRunManualPath ? [{ name: "workflow manual run", path: workflowRunManualPath, readySelector: '[data-ui-id="ui.workflows.run.manual.form"]' }] : []), ]; for (const targetRoute of crossSurfaceRoutes) { await test.step(`keeps shell usable across ${targetRoute.name} history and resize`, async () => { await expectCrossSurfaceHistoryResizeReload(page, originRoute, targetRoute); }); } await expectNoCriticalA11yViolations(page); }); }); test.describe("authenticated compact mobile shell flow", () => { test.use({ viewport: compactMobileViewport, isMobile: true, hasTouch: true }); test.beforeEach(() => { requireMobileShellInputs("Authenticated compact mobile shell flow inputs not configured"); }); test("keeps dashboard and settings shell controls usable at 320px", async ({ page }) => { await loginAsTenantUser(page, credentials!); await page.goto(dashboardPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")); await expectNoHorizontalOverflow(page); const sidebarTrigger = page.locator('[data-ui-id="ui.action.shell.sidebarTrigger"]'); const mobileSidebar = page.locator(mobileSidebarSelector); await sidebarTrigger.click(); await expect(mobileSidebar).toBeVisible(); await expect(mobileSidebar.locator('[data-ui-id="ui.nav.app.dashboard"]').first()).toBeVisible(); await expect(mobileSidebar.locator('[data-ui-id="ui.nav.app.settings"]').first()).toBeVisible(); await expectNoHorizontalOverflow(page); await page.keyboard.press("Escape"); await expect(mobileSidebar).toBeHidden(); await expect(sidebarTrigger).toBeFocused(); await expectNoHorizontalOverflow(page); const moreTrigger = page.locator('[data-ui-id="ui.action.shell.more"]'); await moreTrigger.click(); const signOut = page.locator('[data-ui-id="ui.action.shell.signOut"]').first(); await expect(signOut).toBeVisible(); await expectNoHorizontalOverflow(page); await page.keyboard.press("Escape"); await expect(signOut).toBeHidden(); await expect(moreTrigger).toBeFocused(); await expectNoHorizontalOverflow(page); const searchTrigger = page.locator('[data-ui-id="ui.action.shell.search"]'); await searchTrigger.click(); const palette = page.locator('[data-ui-id="ui.surface.commandPalette.portal"]'); const searchInput = page.locator('[data-ui-id="ui.surface.commandPalette.search"]'); await expect(palette).toBeVisible(); await expect(searchInput).toBeFocused(); await searchInput.fill("settings"); await expect(page.locator('[data-ui-id="ui.surface.commandPalette.row"]').first()).toBeVisible(); await expectNoCriticalA11yViolations(page); await expectNoHorizontalOverflow(page); await page.keyboard.press("Escape"); await expect(palette).toBeHidden(); await expect(searchTrigger).toBeFocused(); await expectNoHorizontalOverflow(page); const settingsProfilePath = tenantSettingsProfilePath(dashboardPath); await sidebarTrigger.click(); await expect(mobileSidebar).toBeVisible(); await mobileSidebar.locator('[data-ui-id="ui.nav.app.settings"]').first().click(); await expect(page).toHaveURL(pathPattern(settingsProfilePath)); await expect(mobileSidebar).toBeHidden(); await expectNoHorizontalOverflow(page); // Reopening the drawer right after the settings SPA navigation can race the // closing Sheet's state, dropping the first trigger click. Retry the open // (only clicking while it is still closed, so we never toggle it back shut). await expect(async () => { if (!(await mobileSidebar.isVisible())) { await sidebarTrigger.click(); } await expect(mobileSidebar).toBeVisible({ timeout: 2000 }); }).toPass({ timeout: 15000 }); await page.keyboard.press("Escape"); await expect(mobileSidebar).toBeHidden(); await expect(sidebarTrigger).toBeFocused(); await expectNoHorizontalOverflow(page); await expectNoCriticalA11yViolations(page); }); });