Files
HiveGrid-Wireframes/hivegrid-cloud/tests/e2e/mobile-auth.spec.ts
T

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