244 lines
10 KiB
TypeScript
244 lines
10 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
|
|
|
const PASSWORD = 'Sup3rSecure!123';
|
|
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
function createDeferred() {
|
|
let resolve: (() => void) | null = null;
|
|
const promise = new Promise<void>((done) => {
|
|
resolve = done;
|
|
});
|
|
|
|
return {
|
|
promise,
|
|
resolve: () => resolve?.()
|
|
};
|
|
}
|
|
|
|
function uniqueEmail(prefix: string) {
|
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@example.com`;
|
|
}
|
|
|
|
async function gotoAuthPage(page: Page, path: string) {
|
|
await page.goto(path, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
|
|
async function signUp(page: Page, email: string, path = '/auth/signup') {
|
|
await gotoAuthPage(page, path);
|
|
await page.locator('input[autocomplete="name"]').fill('Playwright User');
|
|
await page.locator('input[autocomplete="email"]').fill(email);
|
|
await page.locator('input[autocomplete="new-password"]').first().fill(PASSWORD);
|
|
await page.locator('input[autocomplete="new-password"]').nth(1).fill(PASSWORD);
|
|
await page.getByRole('button', { name: 'Create account' }).click();
|
|
}
|
|
|
|
async function signIn(page: Page, email: string, path = '/auth/signin') {
|
|
await gotoAuthPage(page, path);
|
|
await page.locator('input[autocomplete="email"]').fill(email);
|
|
await page.locator('input[autocomplete="current-password"]').fill(PASSWORD);
|
|
await page.getByRole('button', { name: 'Sign in with password' }).click();
|
|
}
|
|
|
|
async function expectStableDashboard(page: Page) {
|
|
await expect(page.getByRole('heading', { name: 'Command Center' })).toBeVisible({ timeout: 30_000 });
|
|
await expect(page).toHaveURL(/\/$/, { timeout: 30_000 });
|
|
await page.waitForTimeout(1_000);
|
|
expect(page.url()).not.toContain('/auth/signin');
|
|
}
|
|
|
|
async function expectStableProtectedRoute(page: Page, pattern: RegExp) {
|
|
await expect(page).toHaveURL(pattern, { timeout: 30_000 });
|
|
await page.waitForTimeout(1_000);
|
|
expect(page.url()).not.toContain('/auth/signin');
|
|
}
|
|
|
|
async function signOut(page: Page) {
|
|
await page.getByRole('button', { name: 'Sign out' }).first().click();
|
|
await expect(page).toHaveURL(/\/auth\/signin/, { timeout: 30_000 });
|
|
}
|
|
|
|
test('preserves the return path while switching between auth screens and shows the expected controls', async ({ page }) => {
|
|
await gotoAuthPage(page, '/auth/signin?next=%2Fanalysis%3Fticker%3DNVDA');
|
|
|
|
await expect(page.getByRole('heading', { name: 'Secure Sign In' })).toBeVisible();
|
|
await expect(page.getByText('Use email/password or request a magic link.')).toBeVisible();
|
|
await expect(page.locator('input[autocomplete="email"]')).toBeVisible();
|
|
await expect(page.locator('input[autocomplete="current-password"]')).toBeVisible();
|
|
await expect(page.getByRole('button', { name: 'Sign in with password' })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: 'Send magic link' })).toBeVisible();
|
|
await expect(page.getByRole('link', { name: 'Create one' })).toHaveAttribute('href', '/auth/signup?next=%2Fanalysis%3Fticker%3DNVDA');
|
|
|
|
await page.getByRole('link', { name: 'Create one' }).click();
|
|
|
|
await expect(page).toHaveURL(/\/auth\/signup\?next=%2Fanalysis%3Fticker%3DNVDA$/);
|
|
await expect(page.getByRole('heading', { name: 'Create Account' })).toBeVisible();
|
|
await expect(page.getByText('Set up your operator profile to access portfolio and filings intelligence.')).toBeVisible();
|
|
await expect(page.locator('input[autocomplete="name"]')).toBeVisible();
|
|
await expect(page.locator('input[autocomplete="email"]')).toBeVisible();
|
|
await expect(page.locator('input[autocomplete="new-password"]').first()).toBeVisible();
|
|
await expect(page.getByRole('button', { name: 'Create account' })).toBeVisible();
|
|
await expect(page.getByRole('link', { name: 'Sign in' })).toHaveAttribute('href', '/auth/signin?next=%2Fanalysis%3Fticker%3DNVDA');
|
|
});
|
|
|
|
test('shows client-side validation when signup passwords do not match', async ({ page }) => {
|
|
await gotoAuthPage(page, '/auth/signup');
|
|
|
|
await page.locator('input[autocomplete="name"]').fill('Playwright User');
|
|
await page.locator('input[autocomplete="email"]').fill('mismatch@example.com');
|
|
await page.locator('input[autocomplete="new-password"]').first().fill(PASSWORD);
|
|
await page.locator('input[autocomplete="new-password"]').nth(1).fill('NotTheSame123!');
|
|
await page.getByRole('button', { name: 'Create account' }).click();
|
|
|
|
await expect(page.getByText('Passwords do not match.')).toBeVisible();
|
|
});
|
|
|
|
test('shows loading affordances while sign-in is in flight', async ({ page }) => {
|
|
const gate = createDeferred();
|
|
|
|
await page.route('**/api/auth/sign-in/email', async (route) => {
|
|
await gate.promise;
|
|
await route.fulfill({
|
|
status: 401,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ message: 'Invalid credentials' })
|
|
});
|
|
});
|
|
|
|
await gotoAuthPage(page, '/auth/signin');
|
|
await page.locator('input[autocomplete="email"]').fill('playwright@example.com');
|
|
await page.locator('input[autocomplete="current-password"]').fill(PASSWORD);
|
|
|
|
const submitButton = page.getByRole('button', { name: 'Sign in with password' });
|
|
const magicLinkButton = page.getByRole('button', { name: 'Send magic link' });
|
|
|
|
await submitButton.click();
|
|
|
|
await expect(page.getByRole('button', { name: 'Signing in...' })).toBeDisabled();
|
|
await expect(magicLinkButton).toBeDisabled();
|
|
|
|
gate.resolve();
|
|
|
|
await expect(page.getByText('Invalid credentials')).toBeVisible();
|
|
});
|
|
|
|
test('shows loading affordances while sign-up is in flight', async ({ page }) => {
|
|
const gate = createDeferred();
|
|
|
|
await page.route('**/api/auth/sign-up/email', async (route) => {
|
|
await gate.promise;
|
|
await route.fulfill({
|
|
status: 409,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ message: 'Email already exists' })
|
|
});
|
|
});
|
|
|
|
await gotoAuthPage(page, '/auth/signup');
|
|
await page.locator('input[autocomplete="name"]').fill('Playwright User');
|
|
await page.locator('input[autocomplete="email"]').fill(uniqueEmail('playwright-loading'));
|
|
await page.locator('input[autocomplete="new-password"]').first().fill(PASSWORD);
|
|
await page.locator('input[autocomplete="new-password"]').nth(1).fill(PASSWORD);
|
|
await page.getByRole('button', { name: 'Create account' }).click();
|
|
|
|
await expect(page.getByRole('button', { name: 'Creating account...' })).toBeDisabled();
|
|
|
|
gate.resolve();
|
|
|
|
await expect(page.getByText('Email already exists')).toBeVisible();
|
|
});
|
|
|
|
test('successful signup reaches the authenticated shell and stays there', async ({ page }) => {
|
|
await signUp(page, uniqueEmail('playwright-signup-success'));
|
|
await expectStableDashboard(page);
|
|
});
|
|
|
|
test('successful signup preserves the requested next path', async ({ page }) => {
|
|
await signUp(page, uniqueEmail('playwright-signup-next'), '/auth/signup?next=%2Fanalysis%3Fticker%3DNVDA');
|
|
await expectStableProtectedRoute(page, /\/analysis\?ticker=NVDA$/);
|
|
});
|
|
|
|
test('successful sign-in reaches the authenticated shell and stays there', async ({ page }) => {
|
|
const email = uniqueEmail('playwright-signin-success');
|
|
|
|
await signUp(page, email);
|
|
await expectStableDashboard(page);
|
|
await signOut(page);
|
|
|
|
await signIn(page, email);
|
|
await expectStableDashboard(page);
|
|
});
|
|
|
|
test('authenticated users are redirected away from auth pages with hard navigation', async ({ page }) => {
|
|
await signUp(page, uniqueEmail('playwright-authenticated-redirect'));
|
|
await expectStableDashboard(page);
|
|
|
|
await page.goto('/auth/signin', { waitUntil: 'domcontentloaded' });
|
|
await expectStableDashboard(page);
|
|
|
|
await page.goto('/auth/signup?next=%2Fanalysis%3Fticker%3DNVDA', { waitUntil: 'domcontentloaded' });
|
|
await expectStableProtectedRoute(page, /\/analysis\?ticker=NVDA$/);
|
|
});
|
|
|
|
test('shows the handoff state while waiting for the session to become visible', async ({ page }) => {
|
|
const gate = createDeferred();
|
|
let holdSession = false;
|
|
|
|
await page.route('**/api/auth/get-session**', async (route) => {
|
|
if (holdSession) {
|
|
await gate.promise;
|
|
}
|
|
|
|
await route.continue();
|
|
});
|
|
|
|
await gotoAuthPage(page, '/auth/signup');
|
|
holdSession = true;
|
|
|
|
await page.locator('input[autocomplete="name"]').fill('Playwright User');
|
|
await page.locator('input[autocomplete="email"]').fill(uniqueEmail('playwright-handoff'));
|
|
await page.locator('input[autocomplete="new-password"]').first().fill(PASSWORD);
|
|
await page.locator('input[autocomplete="new-password"]').nth(1).fill(PASSWORD);
|
|
await page.getByRole('button', { name: 'Create account' }).click();
|
|
|
|
await expect(page.getByRole('button', { name: 'Finishing sign-in...' })).toBeDisabled();
|
|
await expect(page.getByText('Establishing your session and opening the workspace...')).toBeVisible();
|
|
|
|
gate.resolve();
|
|
|
|
await expectStableDashboard(page);
|
|
});
|
|
|
|
test('shows recovery guidance if session establishment never completes', async ({ page }) => {
|
|
let forceMissingSession = false;
|
|
|
|
await page.route('**/api/auth/get-session**', async (route) => {
|
|
if (!forceMissingSession) {
|
|
await route.continue();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: 'null'
|
|
});
|
|
});
|
|
|
|
await gotoAuthPage(page, '/auth/signup');
|
|
forceMissingSession = true;
|
|
|
|
await page.locator('input[autocomplete="name"]').fill('Playwright User');
|
|
await page.locator('input[autocomplete="email"]').fill(uniqueEmail('playwright-timeout'));
|
|
await page.locator('input[autocomplete="new-password"]').first().fill(PASSWORD);
|
|
await page.locator('input[autocomplete="new-password"]').nth(1).fill(PASSWORD);
|
|
await page.getByRole('button', { name: 'Create account' }).click();
|
|
|
|
await expect(page.getByRole('button', { name: 'Finishing sign-in...' })).toBeDisabled();
|
|
await expect(page.getByText('Establishing your session and opening the workspace...')).toBeVisible();
|
|
await expect(page.getByText('Authentication completed, but the session was not established on this device. Please sign in again.')).toBeVisible({ timeout: 15_000 });
|
|
await expect(page).toHaveURL(/\/auth\/signup$/);
|
|
await expect(page.getByRole('button', { name: 'Create account' })).toBeEnabled();
|
|
});
|