🎨 **True Light/Dark Mode** - Implemented proper light mode with inverted color scheme - Smooth theme transitions (0.3s ease) - Light mode colors: white backgrounds, dark text, vibrant accents - System theme detection with proper class application 🌈 **Enhanced Theme System** - 4 color themes work in both light and dark modes - Cyber (cyan/magenta), Ocean (blue/teal), Forest (green/lime), Sunset (orange/purple) - Theme-specific glassmorphism effects - Proper contrast in light mode ✨ **Premium Animations** - Float, glow-pulse, slide-in, scale-in, rotate-in animations - Smooth page transitions - Hover effects with depth (lift, glow, scale) - Micro-interactions on all interactive elements 🎯 **Visual Polish** - Enhanced glassmorphism for light/dark modes - Custom scrollbar with theme colors - Beautiful text selection - Focus indicators for accessibility - Premium utility classes 🔧 **Technical Improvements** - Updated UIStore to properly apply light/dark classes - Added data-theme attribute for CSS targeting - Smooth scroll behavior - Optimized transitions The app is now a visual masterpiece with perfect light/dark mode support!
181 lines
6.8 KiB
TypeScript
181 lines
6.8 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' },
|
|
// eslint-disable-next-line no-undef
|
|
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`);
|
|
// eslint-disable-next-line no-undef
|
|
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);
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
|