feat: Visual masterpiece - true light mode & premium UI
🎨 **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!
2026-01-11 01:32:21 +00:00
|
|
|
|
2026-02-14 13:02:13 +00:00
|
|
|
import * as fs from 'fs';
|
|
|
|
|
import * as path from 'path';
|
2025-12-22 21:00:50 +00:00
|
|
|
import { chromium, FullConfig } from '@playwright/test';
|
2026-01-07 18:39:21 +00:00
|
|
|
import { TEST_CONFIG } from './utils/test-helpers';
|
2025-12-22 21:00:50 +00:00
|
|
|
|
2025-12-26 16:28:43 +00:00
|
|
|
// Load test user credentials from environment or use defaults
|
|
|
|
|
const getTestUser = () => {
|
2025-12-26 16:38:16 +00:00
|
|
|
const email = process.env.TEST_EMAIL || 'e2e@test.com';
|
|
|
|
|
const password = process.env.TEST_PASSWORD || 'Xk9$mP2#vL7@nQ4!wR8';
|
2025-12-26 16:28:43 +00:00
|
|
|
return { email, password };
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-22 21:00:50 +00:00
|
|
|
/**
|
|
|
|
|
* 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...');
|
|
|
|
|
|
2025-12-26 16:38:16 +00:00
|
|
|
const testUser = getTestUser();
|
|
|
|
|
console.log(`🔧 [GLOBAL SETUP] Using test user: ${testUser.email}`);
|
|
|
|
|
|
2025-12-22 21:00:50 +00:00
|
|
|
// Use the first project's browser (usually chromium)
|
2026-01-07 18:39:21 +00:00
|
|
|
// Use the first project's browser (usually chromium)
|
2025-12-22 21:00:50 +00:00
|
|
|
const browser = await chromium.launch({
|
|
|
|
|
headless: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const context = await browser.newContext();
|
|
|
|
|
const page = await context.newPage();
|
|
|
|
|
|
|
|
|
|
try {
|
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 15:43:21 +00:00
|
|
|
// 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)
|
2025-12-27 15:03:00 +00:00
|
|
|
console.log('🔧 [GLOBAL SETUP] Verifying API availability...');
|
|
|
|
|
console.log(`🔧 [GLOBAL SETUP] API URL: ${TEST_CONFIG.API_URL}`);
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2025-12-27 15:03:00 +00:00
|
|
|
const healthCheckResult = await page.evaluate(async ({ apiUrl }) => {
|
|
|
|
|
try {
|
2026-02-16 09:52:56 +00:00
|
|
|
// 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`;
|
2025-12-27 15:03:00 +00:00
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 16:38:16 +00:00
|
|
|
// 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`);
|
2026-01-16 11:19:06 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
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;
|
|
|
|
|
};
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
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,
|
|
|
|
|
}), });
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
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
|
|
|
|
|
}
|
2025-12-26 16:38:16 +00:00
|
|
|
|
|
|
|
|
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,
|
2025-12-28 15:07:02 +00:00
|
|
|
accessToken,
|
|
|
|
|
refreshToken,
|
2025-12-26 16:38:16 +00:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
localStorage.setItem('auth-storage', JSON.stringify(authStorage));
|
|
|
|
|
|
|
|
|
|
return { success: true, accessToken, refreshToken };
|
|
|
|
|
} catch (error) {
|
2025-12-27 15:03:00 +00:00
|
|
|
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 };
|
2025-12-26 16:38:16 +00:00
|
|
|
}
|
|
|
|
|
}, { apiUrl: TEST_CONFIG.API_URL, email: testUser.email, password: testUser.password });
|
|
|
|
|
|
|
|
|
|
if (!loginResult.success) {
|
2025-12-27 15:03:00 +00:00
|
|
|
const errorMsg = loginResult.error || 'Unknown error';
|
2026-02-14 13:02:13 +00:00
|
|
|
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;
|
2025-12-22 21:00:50 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-26 16:38:16 +00:00
|
|
|
console.log('✅ [GLOBAL SETUP] API login successful!');
|
|
|
|
|
console.log(`✅ [GLOBAL SETUP] Access token: ${loginResult.accessToken?.substring(0, 20)}...`);
|
2025-12-22 21:00:50 +00:00
|
|
|
|
2025-12-26 16:38:16 +00:00
|
|
|
// Verify tokens are stored
|
|
|
|
|
const storedToken = await page.evaluate(() => localStorage.getItem('veza_access_token'));
|
|
|
|
|
if (!storedToken) {
|
|
|
|
|
throw new Error('Token not stored in localStorage');
|
2025-12-22 21:00:50 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-26 16:38:16 +00:00
|
|
|
// Save the authenticated state
|
2025-12-22 21:00:50 +00:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-22 21:56:37 +00:00
|
|
|
|
2025-12-23 00:36:04 +00:00
|
|
|
|
2025-12-24 10:19:05 +00:00
|
|
|
|