211 lines
8.3 KiB
TypeScript
211 lines
8.3 KiB
TypeScript
|
|
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: Verify API is available before attempting login
|
|
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 {
|
|
// Remove /api/v1 from URL for health check (health is usually at root)
|
|
const baseUrl = apiUrl.replace('/api/v1', '');
|
|
const healthUrl = `${baseUrl}/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');
|
|
}
|
|
|
|
// Navigate to frontend root (not /login to avoid routing issues)
|
|
console.log('🔧 [GLOBAL SETUP] Navigating to frontend...');
|
|
await page.goto(TEST_CONFIG.FRONTEND_URL, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
|
|
// 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.error(`❌ [GLOBAL SETUP] API login failed: ${errorMsg}`);
|
|
console.error(`❌ [GLOBAL SETUP] Make sure:`);
|
|
console.error(` - Backend API is running at ${TEST_CONFIG.API_URL}`);
|
|
console.error(` - Test user exists: ${testUser.email}`);
|
|
console.error(` - CORS is configured correctly`);
|
|
throw new Error(`API login failed: ${errorMsg}`);
|
|
}
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
|