import * as fs from 'fs'; import * as path from 'path'; import { chromium, FullConfig } from '@playwright/test'; import { TEST_CONFIG } from './utils/test-helpers'; // Load test user credentials from environment or use defaults const getTestUser = () => { const email = process.env.TEST_EMAIL || 'e2e@test.com'; const password = process.env.TEST_PASSWORD || 'Xk9$mP2#vL7@nQ4!wR8'; return { email, password }; }; /** * Global Setup for Playwright E2E Tests * * This setup runs ONCE before all tests to: * 1. Log in as a test user * 2. Save the authenticated session state to storageState.json * 3. All subsequent tests will use this saved state (no need to login again) * * This eliminates: * - Rate limiting issues (only 1 login instead of N logins) * - Test execution time (no login overhead per test) * - Flaky authentication failures */ async function globalSetup(config: FullConfig) { console.log('🔧 [GLOBAL SETUP] Starting global setup...'); const testUser = getTestUser(); console.log(`🔧 [GLOBAL SETUP] Using test user: ${testUser.email}`); // Use the first project's browser (usually chromium) // Use the first project's browser (usually chromium) const browser = await chromium.launch({ headless: true, }); const context = await browser.newContext(); const page = await context.newPage(); try { // Step 1: Navigate to frontend first (required for relative API URLs - fetch needs a base URL) console.log('🔧 [GLOBAL SETUP] Navigating to frontend...'); await page.goto(TEST_CONFIG.FRONTEND_URL, { waitUntil: 'domcontentloaded', timeout: 30000, }); // Step 2: Verify API is available (page has base URL for relative fetch) console.log('🔧 [GLOBAL SETUP] Verifying API availability...'); console.log(`🔧 [GLOBAL SETUP] API URL: ${TEST_CONFIG.API_URL}`); const healthCheckResult = await page.evaluate(async ({ apiUrl }) => { try { // When apiUrl is relative (e.g. /api/v1), health is at /api/v1/health (proxy forwards /api) const healthUrl = apiUrl.startsWith('/') ? `${apiUrl.replace(/\/$/, '')}/health` : `${apiUrl.replace(/\/api\/v1\/?$/, '')}/api/v1/health`; console.log(`[BROWSER] Health check: ${healthUrl}`); const healthResponse = await fetch(healthUrl, { method: 'GET', headers: { 'Content-Type': 'application/json' }, signal: AbortSignal.timeout(10000), // 10s timeout }); return { success: healthResponse.ok, status: healthResponse.status }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } }, { apiUrl: TEST_CONFIG.API_URL }); if (!healthCheckResult.success) { console.warn(`⚠️ [GLOBAL SETUP] API health check failed: ${healthCheckResult.error || `Status ${healthCheckResult.status}`}`); console.warn(`⚠️ [GLOBAL SETUP] Continuing anyway - API might be starting up...`); } else { console.log('✅ [GLOBAL SETUP] API is available'); } // Login via API directly in the browser context console.log('🔧 [GLOBAL SETUP] Attempting API login via browser...'); const loginResult = await page.evaluate(async ({ apiUrl, email, password }) => { try { console.log(`[BROWSER] Attempting login to: ${apiUrl}/auth/login`); const loginAttempt = async () => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout const response = await fetch(`${apiUrl}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, password, }), signal: controller.signal, }); clearTimeout(timeoutId); return response; }; let response = await loginAttempt(); // If login fails with 401, attempt to register the user if (response.status === 401) { console.warn(`[BROWSER] Login failed with 401. Attempting to register user: ${email}`); const registerResponse = await fetch(`${apiUrl}/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, password, password_confirmation: password, // Required by backend DTO username: email.split('@')[0], // Use email prefix as username first_name: 'E2E', last_name: 'Test', terms_accepted: true, }), }); if (!registerResponse.ok) { const errorText = await registerResponse.text(); console.error(`[BROWSER] Registration failed: HTTP ${registerResponse.status}: ${errorText}`); return { success: false, error: `Registration failed: HTTP ${registerResponse.status}: ${errorText}` }; } console.log(`[BROWSER] User ${email} registered successfully. Attempting login again.`); response = await loginAttempt(); // Try logging in again after registration } if (!response.ok) { const errorText = await response.text(); return { success: false, error: `HTTP ${response.status}: ${errorText}` }; } const data = await response.json(); const accessToken = data?.token?.access_token || data?.data?.token?.access_token || data?.access_token; const refreshToken = data?.token?.refresh_token || data?.data?.token?.refresh_token || data?.refresh_token; if (!accessToken) { return { success: false, error: 'No access token in response', data }; } // Store tokens in localStorage localStorage.setItem('veza_access_token', accessToken); if (refreshToken) { localStorage.setItem('veza_refresh_token', refreshToken); } // Also set auth-storage for Zustand const authStorage = { state: { isAuthenticated: true, accessToken, refreshToken, }, }; localStorage.setItem('auth-storage', JSON.stringify(authStorage)); return { success: true, accessToken, refreshToken }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`[BROWSER] Login error: ${errorMessage}`); // Check if it's a network error if (errorMessage.includes('Failed to fetch') || errorMessage.includes('NetworkError') || errorMessage.includes('aborted')) { return { success: false, error: `Network error: ${errorMessage}. Is the API running at ${apiUrl}?` }; } return { success: false, error: errorMessage }; } }, { apiUrl: TEST_CONFIG.API_URL, email: testUser.email, password: testUser.password }); if (!loginResult.success) { const errorMsg = loginResult.error || 'Unknown error'; console.warn(`⚠️ [GLOBAL SETUP] API login failed: ${errorMsg}`); console.warn(`⚠️ [GLOBAL SETUP] Make sure Backend API is running at ${TEST_CONFIG.API_URL} and test user exists: ${testUser.email}`); // Write empty storage state so Playwright can start; specs that need auth use their own login or storageState override const storageStatePath = config.projects[0]?.use?.storageState as string || 'e2e/.auth/user.json'; fs.mkdirSync(path.dirname(storageStatePath), { recursive: true }); await context.storageState({ path: storageStatePath }); console.warn(`⚠️ [GLOBAL SETUP] Saved empty auth state to ${storageStatePath}. Tests requiring API will fail until backend is running.`); await browser.close(); return; } console.log('✅ [GLOBAL SETUP] API login successful!'); console.log(`✅ [GLOBAL SETUP] Access token: ${loginResult.accessToken?.substring(0, 20)}...`); // Verify tokens are stored const storedToken = await page.evaluate(() => localStorage.getItem('veza_access_token')); if (!storedToken) { throw new Error('Token not stored in localStorage'); } // Save the authenticated state const storageStatePath = config.projects[0]?.use?.storageState as string || 'e2e/.auth/user.json'; console.log(`💾 [GLOBAL SETUP] Saving authenticated state to: ${storageStatePath}`); await context.storageState({ path: storageStatePath }); console.log('✅ [GLOBAL SETUP] Global setup completed successfully!'); } catch (error) { console.error('❌ [GLOBAL SETUP] Global setup failed:', error); throw error; } finally { await browser.close(); } } export default globalSetup;