import { expect, test } from "./utils/test"; import { expectAllPasswordRequirementsMet, expectPasswordRequirementsVisible, expectSomePasswordRequirementsMissing } from "./utils/auth"; import { compactMobileViewport, expectNoHorizontalOverflow, narrowMobileViewport } from "./utils/mobile"; const loginEmailInput = '[data-ui-id="ui.auth.login.email"], input[type="email"], input[name="email"], input#email'; const loginPasswordInput = '[data-ui-id="ui.auth.login.password"], input[type="password"], input[name="password"], input#password'; const loginSubmitButton = '[data-ui-id="ui.auth.login.submit"], button[type="submit"]'; const forgotEmailInput = '[data-ui-id="ui.auth.forgot.email"], input[type="email"], input[name="email"], input#email'; const forgotSubmitButton = '[data-ui-id="ui.auth.forgot.submit"], button[type="submit"]'; const registerPasswordInput = '[data-ui-id="ui.auth.register.password"], input[type="password"], input[name="password"], input#password'; const resetEmailInput = '[data-ui-id="ui.auth.reset.email"], input[type="email"], input[name="email"], input#email'; const resetPasswordInput = '[data-ui-id="ui.auth.reset.password"], input[type="password"], input[name="password"], input#password'; const resetSubmitButton = '[data-ui-id="ui.auth.reset.submit"], button[type="submit"]'; // Unauthenticated deep links only redirect to /login for an existing tenant; a // non-existent slug resolves before auth and redirects home. Use the seeded // slug (fallback to the default-seed "acme"). "/app" is not a route here. const tenantSlug = process.env.E2E_TENANT_ACCOUNT_SLUG ?? "acme"; test.describe("mobile auth pages", () => { test.use({ viewport: narrowMobileViewport, isMobile: true, hasTouch: true }); for (const protectedPath of [`/${tenantSlug}/dashboard`, "/new-account"]) { test(`${protectedPath} redirects unauthenticated mobile users to login`, async ({ page }) => { await page.goto(protectedPath, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(/\/login(?:[?#].*)?$/); await expect(page.locator(loginEmailInput).first()).toBeVisible(); await expectNoHorizontalOverflow(page); }); } test("tenant protected deep links redirect unauthenticated mobile users to login with redirectTo", async ({ page }) => { const tenantProtectedPaths = [ `/${tenantSlug}/dashboard`, `/${tenantSlug}/settings/account`, `/${tenantSlug}/settings/plugins`, `/${tenantSlug}/workflow-engine`, `/${tenantSlug}/workflow-engine/workflows/example/run/manual`, ]; for (const protectedPath of tenantProtectedPaths) { await page.goto(protectedPath, { waitUntil: "domcontentloaded" }); const currentUrl = new URL(page.url()); expect(currentUrl.pathname).toBe("/login"); expect(currentUrl.searchParams.get("redirectTo")).toBe(protectedPath); await expect(page.locator(loginEmailInput).first()).toBeVisible(); await expectNoHorizontalOverflow(page); } }); test("/login fits a narrow mobile viewport", async ({ page }) => { await page.goto("/login", { waitUntil: "domcontentloaded" }); await expect(page.locator(loginEmailInput).first()).toBeVisible(); await expect(page.locator(loginPasswordInput).first()).toBeVisible(); await expect(page.locator(loginSubmitButton).first()).toBeVisible(); await expectNoHorizontalOverflow(page); }); test("/register fits a narrow mobile viewport and keeps password requirements live", async ({ page }) => { await page.goto("/register", { waitUntil: "domcontentloaded" }); if (/\/login(?:[?#].*)?$/.test(page.url())) { await expect(page.locator(loginEmailInput).first()).toBeVisible(); await expectNoHorizontalOverflow(page); return; } await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); await expectNoHorizontalOverflow(page); const passwordInput = page.locator(registerPasswordInput).first(); if ((await passwordInput.count()) === 0 || !(await passwordInput.isVisible())) { return; } const requirements = page.locator('[aria-live="polite"]').first(); await expectPasswordRequirementsVisible(requirements); await passwordInput.fill("short"); await expectSomePasswordRequirementsMissing(requirements); await passwordInput.fill("LongEnough1!"); await expectAllPasswordRequirementsMet(requirements); await expectNoHorizontalOverflow(page); }); test("/forgot-password fits a narrow mobile viewport", async ({ page }) => { await page.goto("/forgot-password", { waitUntil: "domcontentloaded" }); await expect(page.locator(forgotEmailInput).first()).toBeVisible(); await expect(page.locator(forgotSubmitButton).first()).toBeVisible(); await expectNoHorizontalOverflow(page); }); test("/reset without a token keeps password inputs disabled on mobile", async ({ page }) => { await page.goto("/reset", { waitUntil: "domcontentloaded" }); await expect(page.locator(resetEmailInput).first()).toBeVisible(); await expect(page.locator(resetPasswordInput).first()).toBeVisible(); await expect(page.locator(resetSubmitButton).first()).toBeDisabled(); await expectNoHorizontalOverflow(page); }); test("/reset with query data fits a narrow mobile viewport", async ({ page }) => { await page.goto("/reset?e=test%40example.com&t=dummy-token", { waitUntil: "domcontentloaded" }); await expect(page.locator(resetEmailInput).first()).toHaveValue("test@example.com"); await expect(page.locator(resetPasswordInput).first()).toBeEditable(); await expect(page.locator(resetSubmitButton).first()).toBeEnabled(); await expectNoHorizontalOverflow(page); }); test("/auth/magic-link invalid state fits a narrow mobile viewport", async ({ page }) => { await page.goto("/auth/magic-link", { waitUntil: "domcontentloaded" }); await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); await expect(page.getByRole("link", { name: /request a new.*link|neuen.*link/i })).toHaveAttribute("href", "/forgot-password"); await expectNoHorizontalOverflow(page); }); test("auth recovery links stay navigable on mobile", async ({ page }) => { await page.goto("/login", { waitUntil: "domcontentloaded" }); await page.locator('a[href="/forgot-password"]').click(); await expect(page).toHaveURL(/\/forgot-password(?:[?#].*)?$/); await expectNoHorizontalOverflow(page); await page.locator('a[href="/login"]').click(); await expect(page).toHaveURL(/\/login(?:[?#].*)?$/); await expectNoHorizontalOverflow(page); await page.goto("/auth/magic-link", { waitUntil: "domcontentloaded" }); await page.locator('a[href="/forgot-password"]').click(); await expect(page).toHaveURL(/\/forgot-password(?:[?#].*)?$/); await expectNoHorizontalOverflow(page); await page.goto("/reset", { waitUntil: "domcontentloaded" }); await page.locator('a[href="/login"]').click(); await expect(page).toHaveURL(/\/login(?:[?#].*)?$/); await expectNoHorizontalOverflow(page); }); }); test.describe("compact mobile auth pages", () => { test.use({ viewport: compactMobileViewport, isMobile: true, hasTouch: true }); for (const route of ["/login", "/register", "/forgot-password", "/reset", "/reset?e=test%40example.com&t=dummy-token", "/auth/magic-link"]) { test(`${route} fits a 320px mobile viewport`, async ({ page }) => { await page.goto(route, { waitUntil: "domcontentloaded" }); await expect(page.locator("body")).toBeVisible(); await expectNoHorizontalOverflow(page); }); } test("/reset without a token keeps controls disabled at 320px", async ({ page }) => { await page.goto("/reset", { waitUntil: "domcontentloaded" }); await expect(page.locator(resetEmailInput).first()).toBeVisible(); await expect(page.locator(resetPasswordInput).first()).toBeVisible(); await expect(page.locator(resetSubmitButton).first()).toBeDisabled(); await expectNoHorizontalOverflow(page); }); test("/reset preserves long email query data at 320px", async ({ page }) => { const email = "avery.long.recovery.email.address.for.mobile.testing@example-super-long-domain.test"; await page.goto(`/reset?e=${encodeURIComponent(email)}&t=dummy-token`, { waitUntil: "domcontentloaded" }); await expect(page.locator(resetEmailInput).first()).toHaveValue(email); await expect(page.locator(resetPasswordInput).first()).toBeEditable(); await expect(page.locator(resetSubmitButton).first()).toBeEnabled(); await expectNoHorizontalOverflow(page); }); });