veza/apps/web/e2e/global-setup.ts
senke b103a09a25 chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 16:43:21 +01:00

216 lines
8.8 KiB
TypeScript

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;