176 lines
8.4 KiB
TypeScript
176 lines
8.4 KiB
TypeScript
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);
|
|
});
|
|
});
|