import type { Page } from '@playwright/test'; export interface TestCredentials { username: string; password: string; email: string; } export interface TestUserResponse { id: string; username: string; email: string; role: 'Admin' | 'User'; } export interface E2ETestUser { credentials: TestCredentials; userResponse: TestUserResponse; token?: string; } export const E2E_TIMEOUTS = { login: 15000, navigation: 15000, api: 8000, userCreation: 20000, } as const; /** * E2E Test Auth Helper - Creates unique test users for each test run * Similar to the backend TestAuthHelper but for E2E browser tests */ export class E2ETestAuthHelper { constructor(private page: Page) {} /** * Create a unique test user via API call */ async createTestUser(): Promise { const uniqueId = this.generateUniqueId(); const credentials: TestCredentials = { username: `e2e_user_${uniqueId}`, email: `e2e_user_${uniqueId}@test.com`, password: 'testpass123' }; try { // Make API call to create user const response = await this.page.request.post('/api/auth/register', { data: { username: credentials.username, email: credentials.email, password: credentials.password }, timeout: E2E_TIMEOUTS.userCreation }); if (!response.ok()) { const errorText = await response.text(); console.warn(`Warning: Failed to create dynamic test user. Status: ${response.status()}, Body: ${errorText}`); // Fallback to seeded admin user (since no regular user is seeded) console.log('Falling back to seeded admin user...'); return { credentials: { username: 'admin', email: 'admin@test.com', password: 'readur2024' }, userResponse: { id: 'seeded-admin', username: 'admin', email: 'admin@test.com', role: 'Admin' } }; } const userResponse: TestUserResponse = await response.json(); return { credentials, userResponse, }; } catch (error) { console.error('❌ Failed to create E2E test user:', error); // Fallback to seeded admin user (since no regular user is seeded) console.log('Falling back to seeded admin user due to error...'); return { credentials: { username: 'admin', email: 'admin@test.com', password: 'readur2024' }, userResponse: { id: 'seeded-admin', username: 'admin', email: 'admin@test.com', role: 'Admin' } }; } } /** * Create a unique admin user via API call */ async createAdminUser(): Promise { const uniqueId = this.generateUniqueId(); const credentials: TestCredentials = { username: `e2e_admin_${uniqueId}`, email: `e2e_admin_${uniqueId}@test.com`, password: 'adminpass123' }; try { // Make API call to create admin user const response = await this.page.request.post('/api/auth/register', { data: { username: credentials.username, email: credentials.email, password: credentials.password, role: 'admin' }, timeout: E2E_TIMEOUTS.userCreation }); if (!response.ok()) { const errorText = await response.text(); console.warn(`Warning: Failed to create dynamic admin user. Status: ${response.status()}, Body: ${errorText}`); // Fallback to seeded admin user console.log('Falling back to seeded admin user...'); return { credentials: { username: 'admin', email: 'admin@test.com', password: 'readur2024' }, userResponse: { id: 'seeded-admin', username: 'admin', email: 'admin@test.com', role: 'Admin' } }; } const userResponse: TestUserResponse = await response.json(); return { credentials, userResponse, }; } catch (error) { console.error('❌ Failed to create E2E admin user:', error); // Fallback to seeded admin user console.log('Falling back to seeded admin user due to error...'); return { credentials: { username: 'admin', email: 'admin@test.com', password: 'readur2024' }, userResponse: { id: 'seeded-admin', username: 'admin', email: 'admin@test.com', role: 'Admin' } }; } } /** * Login a user via browser UI and return authentication status */ async loginUser(credentials: TestCredentials): Promise { try { // Go to home page and wait for it to load await this.page.goto('/'); await this.page.waitForLoadState('networkidle'); // Check if already logged in by looking for dashboard content const welcomeText = await this.page.locator('h4:has-text("Welcome back,")').isVisible().catch(() => false); if (welcomeText) { return true; } // Wait for login page to be ready - look for the distinctive login page content await this.page.waitForSelector('h3:has-text("Welcome to Readur")', { timeout: E2E_TIMEOUTS.login }); await this.page.waitForSelector('h5:has-text("Sign in to your account")', { timeout: E2E_TIMEOUTS.login }); // Material-UI creates input elements inside TextFields, but we need to wait for them to be ready // The inputs have the name attributes from react-hook-form register const usernameField = this.page.locator('input[name="username"]'); const passwordField = this.page.locator('input[name="password"]'); // Wait for both fields to be attached and visible await usernameField.waitFor({ state: 'attached', timeout: E2E_TIMEOUTS.login }); await passwordField.waitFor({ state: 'attached', timeout: E2E_TIMEOUTS.login }); // Browser-specific wait time const browserName = await this.page.context().browser()?.browserType().name() || ''; const isWebKit = browserName === 'webkit'; const isFirefox = browserName === 'firefox'; if (isWebKit) { console.log('WebKit browser detected - adding extra wait time'); await this.page.waitForTimeout(5000); } else if (isFirefox) { console.log('Firefox browser detected - adding extra wait time'); await this.page.waitForTimeout(3000); } // Clear any existing content and fill the fields await usernameField.clear(); await usernameField.fill(credentials.username); await passwordField.clear(); await passwordField.fill(credentials.password); // Browser-specific wait for form validation if (isWebKit) { await this.page.waitForTimeout(3000); } else if (isFirefox) { await this.page.waitForTimeout(2000); } // Click submit button - look for the sign in button specifically const signInButton = this.page.locator('button[type="submit"]:has-text("Sign in")'); await signInButton.waitFor({ state: 'visible', timeout: E2E_TIMEOUTS.login }); if (isWebKit || isFirefox) { // WebKit and Firefox specific approach: don't wait for API response, just click and wait for navigation await signInButton.click(); // Browser-specific wait before checking navigation await this.page.waitForTimeout(isWebKit ? 2000 : 1500); // Wait for navigation with longer timeout for WebKit/Firefox await this.page.waitForURL(/.*\/dashboard.*/, { timeout: 25000 }); console.log(`Successfully navigated to: ${this.page.url()}`); // Wait for dashboard content to load with extra time await this.page.waitForFunction(() => { return document.querySelector('h4') !== null && (document.querySelector('h4')?.textContent?.includes('Welcome') || document.querySelector('[role="main"]') !== null); }, { timeout: 20000 }); } else { // Standard approach for other browsers const loginPromise = this.page.waitForResponse(response => response.url().includes('/auth/login') && response.status() === 200, { timeout: E2E_TIMEOUTS.login } ); await signInButton.click(); const response = await loginPromise; // Wait for navigation to dashboard with more flexible URL pattern await this.page.waitForURL(/.*\/dashboard.*/, { timeout: E2E_TIMEOUTS.navigation }); console.log(`Successfully navigated to: ${this.page.url()}`); // Wait for dashboard content to load - be more flexible about the welcome message await this.page.waitForFunction(() => { return document.querySelector('h4') !== null && (document.querySelector('h4')?.textContent?.includes('Welcome') || document.querySelector('[role="main"]') !== null); }, { timeout: E2E_TIMEOUTS.navigation }); } console.log(`Login as ${credentials.username} completed successfully`); return true; } catch (error) { console.error(`Login as ${credentials.username} failed:`, error); // Take a screenshot for debugging await this.page.screenshot({ path: `test-results/login-failure-${credentials.username}-${Date.now()}.png`, fullPage: true }); return false; } } /** * Login a user via API and return authentication token */ async loginUserAPI(credentials: TestCredentials): Promise { console.log(`API login for E2E user: ${credentials.username}...`); try { const response = await this.page.request.post('/api/auth/login', { data: { username: credentials.username, password: credentials.password }, timeout: E2E_TIMEOUTS.api }); if (!response.ok()) { const errorText = await response.text(); throw new Error(`API login failed. Status: ${response.status()}, Body: ${errorText}`); } const loginResponse = await response.json(); const token = loginResponse.token; if (!token) { throw new Error('No token received from login response'); } console.log(`✅ API login successful for ${credentials.username}`); return token; } catch (error) { console.error(`❌ API login failed for ${credentials.username}:`, error); throw error; } } /** * Logout user via browser UI */ async logout(): Promise { try { // Look for logout button/link and click it const logoutButton = this.page.locator('[data-testid="logout"], button:has-text("Logout"), a:has-text("Logout")').first(); if (await logoutButton.isVisible({ timeout: 5000 })) { await logoutButton.click(); // Wait for redirect to login page await this.page.waitForFunction(() => window.location.pathname.includes('/login') || window.location.pathname === '/', { timeout: E2E_TIMEOUTS.navigation } ); console.log('✅ Logout successful'); return true; } else { console.log('⚠️ Logout button not found - may already be logged out'); return true; } } catch (error) { console.error('❌ Logout failed:', error); return false; } } /** * Ensure user is logged out */ async ensureLoggedOut(): Promise { await this.page.goto('/'); await this.page.waitForLoadState('networkidle'); // If we see a login form, we're already logged out const usernameInput = await this.page.locator('input[name="username"]').isVisible().catch(() => false); if (usernameInput) { console.log('Already logged out - login form visible'); return; } // Otherwise, try to logout await this.logout(); } /** * Generate a unique ID for test users to avoid collisions */ private generateUniqueId(): string { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); const processId = typeof process !== 'undefined' ? process.pid : Math.floor(Math.random() * 10000); return `${timestamp}_${processId}_${random}`; } /** * Clean up test user (optional - users are isolated per test run) */ async cleanupUser(userId: string): Promise { try { console.log(`Cleaning up E2E test user: ${userId}`); // This would require admin privileges or a special cleanup endpoint // For now, we rely on test isolation and database cleanup between test runs console.log(`⚠️ User cleanup not implemented - relying on test isolation`); return true; } catch (error) { console.error(`❌ Failed to cleanup user ${userId}:`, error); return false; } } } /** * Create an E2E test user and return credentials */ export async function createE2ETestUser(page: Page): Promise { const authHelper = new E2ETestAuthHelper(page); return await authHelper.createTestUser(); } /** * Create an E2E admin user and return credentials */ export async function createE2EAdminUser(page: Page): Promise { const authHelper = new E2ETestAuthHelper(page); return await authHelper.createAdminUser(); } /** * Login an E2E user via browser UI */ export async function loginE2EUser(page: Page, credentials: TestCredentials): Promise { const authHelper = new E2ETestAuthHelper(page); return await authHelper.loginUser(credentials); }