import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from './utils/test-helpers'; /** * Performance Tests * * These tests measure page load times, render performance, and Core Web Vitals. * Performance metrics are captured using Playwright's performance API and * browser Performance Timing API. * * To run only performance tests: * - Run: npx playwright test performance * * Performance thresholds: * - Page load time: < 3 seconds * - First Contentful Paint (FCP): < 1.8 seconds * - Largest Contentful Paint (LCP): < 2.5 seconds * - Time to Interactive (TTI): < 3.8 seconds * - Total Blocking Time (TBT): < 300ms */ interface PerformanceMetrics { loadTime: number; domContentLoaded: number; firstPaint: number; firstContentfulPaint: number; largestContentfulPaint: number; timeToInteractive: number; totalBlockingTime: number; cumulativeLayoutShift: number; firstInputDelay: number; networkRequests: number; jsHeapSizeUsed: number; } /** * Capture performance metrics from the browser */ async function capturePerformanceMetrics(page: any): Promise { return await page.evaluate(() => { const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; const paint = performance.getEntriesByType('paint'); const measure = performance.getEntriesByType('measure'); // Calculate load time const loadTime = navigation.loadEventEnd - navigation.fetchStart; const domContentLoaded = navigation.domContentLoadedEventEnd - navigation.fetchStart; // Get paint metrics const firstPaint = paint.find((entry) => entry.name === 'first-paint')?.startTime || 0; const firstContentfulPaint = paint.find((entry) => entry.name === 'first-contentful-paint')?.startTime || 0; // Get LCP (Largest Contentful Paint) - approximate using load event const largestContentfulPaint = navigation.loadEventEnd - navigation.fetchStart; // Calculate TTI (Time to Interactive) - approximate const timeToInteractive = navigation.domInteractive - navigation.fetchStart; // Calculate TBT (Total Blocking Time) - approximate // This is a simplified calculation const totalBlockingTime = Math.max(0, navigation.domInteractive - navigation.domContentLoadedEventEnd); // Get CLS (Cumulative Layout Shift) - requires PerformanceObserver let cumulativeLayoutShift = 0; if ('PerformanceObserver' in window) { try { const clsEntries: any[] = []; const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!(entry as any).hadRecentInput) { clsEntries.push(entry); } } }); observer.observe({ type: 'layout-shift', buffered: true }); cumulativeLayoutShift = clsEntries.reduce((sum, entry: any) => sum + entry.value, 0); } catch (e) { // CLS not supported } } // Get FID (First Input Delay) - approximate const firstInputDelay = 0; // Would need PerformanceObserver for real measurement // Count network requests const networkRequests = performance.getEntriesByType('resource').length; // Get memory usage (if available) const memory = (performance as any).memory; const jsHeapSizeUsed = memory ? memory.usedJSHeapSize : 0; return { loadTime, domContentLoaded, firstPaint, firstContentfulPaint, largestContentfulPaint, timeToInteractive, totalBlockingTime, cumulativeLayoutShift, firstInputDelay, networkRequests, jsHeapSizeUsed, }; }); } /** * Wait for page to be fully loaded and stable */ async function waitForPageStable(page: any, timeout = 10000) { await page.waitForLoadState('networkidle', { timeout }); await page.waitForLoadState('domcontentloaded'); // Wait a bit more for any async operations await page.waitForTimeout(1000); } test.describe('Performance Tests', () => { // Use authenticated state for most tests test.use({ storageState: 'e2e/.auth/user.json' }); test.describe('Page Load Performance', () => { test('dashboard page load time should be acceptable', async ({ page }) => { const startTime = Date.now(); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await waitForPageStable(page); const endTime = Date.now(); const loadTime = endTime - startTime; const metrics = await capturePerformanceMetrics(page); // Log metrics for debugging console.log('Dashboard Performance Metrics:', { loadTime: `${loadTime}ms`, domContentLoaded: `${metrics.domContentLoaded.toFixed(2)}ms`, firstContentfulPaint: `${metrics.firstContentfulPaint.toFixed(2)}ms`, largestContentfulPaint: `${metrics.largestContentfulPaint.toFixed(2)}ms`, timeToInteractive: `${metrics.timeToInteractive.toFixed(2)}ms`, networkRequests: metrics.networkRequests, }); // Assertions - thresholds based on Core Web Vitals expect(loadTime).toBeLessThan(5000); // 5 seconds max expect(metrics.domContentLoaded).toBeLessThan(3000); // 3 seconds expect(metrics.firstContentfulPaint).toBeLessThan(1800); // 1.8 seconds (Good FCP) expect(metrics.largestContentfulPaint).toBeLessThan(2500); // 2.5 seconds (Good LCP) }); test('login page load time should be fast', async ({ page }) => { // Use unauthenticated state for login page await page.context().clearCookies(); const startTime = Date.now(); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/login`); await waitForPageStable(page); const endTime = Date.now(); const loadTime = endTime - startTime; const metrics = await capturePerformanceMetrics(page); console.log('Login Page Performance Metrics:', { loadTime: `${loadTime}ms`, firstContentfulPaint: `${metrics.firstContentfulPaint.toFixed(2)}ms`, networkRequests: metrics.networkRequests, }); // Login page should be very fast (no data loading) expect(loadTime).toBeLessThan(2000); // 2 seconds max expect(metrics.firstContentfulPaint).toBeLessThan(1000); // 1 second }); test('profile page load time should be acceptable', async ({ page }) => { const startTime = Date.now(); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/profile`); await waitForPageStable(page); const endTime = Date.now(); const loadTime = endTime - startTime; const metrics = await capturePerformanceMetrics(page); expect(loadTime).toBeLessThan(5000); expect(metrics.firstContentfulPaint).toBeLessThan(1800); }); test('tracks page load time should be acceptable', async ({ page }) => { const startTime = Date.now(); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/tracks`); await waitForPageStable(page); const endTime = Date.now(); const loadTime = endTime - startTime; const metrics = await capturePerformanceMetrics(page); expect(loadTime).toBeLessThan(5000); expect(metrics.firstContentfulPaint).toBeLessThan(1800); }); test('playlists page load time should be acceptable', async ({ page }) => { const startTime = Date.now(); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/playlists`); await waitForPageStable(page); const endTime = Date.now(); const loadTime = endTime - startTime; const metrics = await capturePerformanceMetrics(page); expect(loadTime).toBeLessThan(5000); expect(metrics.firstContentfulPaint).toBeLessThan(1800); }); }); test.describe('Render Performance', () => { test('dashboard should render main content quickly', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); // Measure time to render main content const renderStart = Date.now(); await page.waitForSelector('main, [role="main"]', { timeout: 10000 }); const renderEnd = Date.now(); const renderTime = renderEnd - renderStart; console.log(`Dashboard main content render time: ${renderTime}ms`); expect(renderTime).toBeLessThan(2000); // Should render in under 2 seconds }); test('navigation should be responsive', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await waitForPageStable(page); // Measure navigation time const navStart = Date.now(); await page.click('a[href="/profile"]', { timeout: 5000 }); await page.waitForURL('**/profile', { timeout: 5000 }); await waitForPageStable(page); const navEnd = Date.now(); const navTime = navEnd - navStart; console.log(`Navigation time (dashboard -> profile): ${navTime}ms`); expect(navTime).toBeLessThan(3000); // Navigation should be fast }); }); test.describe('Network Performance', () => { test('should minimize network requests on initial load', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await waitForPageStable(page); const metrics = await capturePerformanceMetrics(page); console.log(`Total network requests: ${metrics.networkRequests}`); // Should not have excessive network requests // This threshold may need adjustment based on actual usage expect(metrics.networkRequests).toBeLessThan(50); }); test('API requests should complete quickly', async ({ page }) => { const requestTimes: number[] = []; // Track API request times page.on('response', (response: any) => { const url = response.url(); if (url.includes('/api/')) { const timing = response.timing(); if (timing) { const requestTime = timing.responseEnd - timing.requestStart; requestTimes.push(requestTime); } } }); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await waitForPageStable(page); if (requestTimes.length > 0) { const avgRequestTime = requestTimes.reduce((a, b) => a + b, 0) / requestTimes.length; const maxRequestTime = Math.max(...requestTimes); console.log(`Average API request time: ${avgRequestTime.toFixed(2)}ms`); console.log(`Max API request time: ${maxRequestTime.toFixed(2)}ms`); // API requests should complete reasonably quickly expect(avgRequestTime).toBeLessThan(1000); // Average under 1 second expect(maxRequestTime).toBeLessThan(3000); // Max under 3 seconds } }); }); test.describe('Memory Performance', () => { test('should not have excessive memory usage', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await waitForPageStable(page); const metrics = await capturePerformanceMetrics(page); if (metrics.jsHeapSizeUsed > 0) { const heapSizeMB = metrics.jsHeapSizeUsed / (1024 * 1024); console.log(`JS Heap Size Used: ${heapSizeMB.toFixed(2)}MB`); // Should not use excessive memory (threshold: 100MB) expect(heapSizeMB).toBeLessThan(100); } }); }); test.describe('Large Dataset Performance', () => { test('should render large track lists (1000+ tracks) smoothly', async ({ page }) => { // Mock a large track list with 1000+ tracks const largeTrackList = Array.from({ length: 1200 }, (_, i) => ({ id: `track-${i + 1}`, title: `Track ${i + 1}`, artist: `Artist ${Math.floor(i / 10) + 1}`, duration: 180 + (i % 60), // Varying durations file_path: `/tracks/track-${i + 1}.mp3`, file_size: 5000000 + (i * 1000), format: 'mp3', is_public: true, play_count: Math.floor(Math.random() * 1000), like_count: Math.floor(Math.random() * 100), created_at: new Date().toISOString(), updated_at: new Date().toISOString(), creator_id: 'test-user', status: 'ready' as const, })); // Intercept tracks API call and return mocked data await page.route('**/api/v1/tracks**', async (route) => { if (route.request().method() === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, data: largeTrackList, total: largeTrackList.length, page: 1, limit: largeTrackList.length, }), }); } else { await route.continue(); } }); // Navigate to library page const renderStart = Date.now(); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/library`); // Wait for library content to be visible await page.waitForSelector('[data-testid="library-page"], .library-page, main', { timeout: 10000 }); // Wait for tracks to be rendered (check for virtualized list or track items) await page.waitForSelector( '[data-testid="track-list"], .track-list, [role="list"], table, [role="table"], [data-testid="virtualized-list"]', { timeout: 10000 } ).catch(() => { // If specific selector not found, wait for any content console.warn('⚠️ [PERF] Specific track list selector not found, waiting for general content'); }); const renderEnd = Date.now(); const renderTime = renderEnd - renderStart; // Measure performance metrics const metrics = await capturePerformanceMetrics(page); // Count rendered track items (virtualization may only render visible items) const trackCount = await page.evaluate(() => { const selectors = [ '[data-testid*="track"]', '[data-track-id]', '[role="listitem"]', 'tr[data-track-id]', '.track-item', 'li', ]; let count = 0; for (const selector of selectors) { const elements = document.querySelectorAll(selector); if (elements.length > 0) { count = elements.length; break; } } return count; }); // Check if virtualization is working (should render fewer items than total) const isVirtualized = trackCount < largeTrackList.length; console.log('Large Track List Performance Metrics:', { renderTime: `${renderTime}ms`, totalTracks: `${largeTrackList.length} tracks`, renderedTracks: `${trackCount} tracks rendered`, isVirtualized: isVirtualized ? 'Yes' : 'No', domContentLoaded: `${metrics.domContentLoaded.toFixed(2)}ms`, firstContentfulPaint: `${metrics.firstContentfulPaint.toFixed(2)}ms`, largestContentfulPaint: `${metrics.largestContentfulPaint.toFixed(2)}ms`, timeToInteractive: `${metrics.timeToInteractive.toFixed(2)}ms`, networkRequests: metrics.networkRequests, }); // Verify performance thresholds // Large track lists should render in reasonable time (8 seconds max for 1000+ tracks) expect(renderTime).toBeLessThan(8000); // Verify that tracks are being rendered (at least some tracks should be visible) expect(trackCount).toBeGreaterThan(0); // Verify smooth rendering - LCP should be acceptable for large lists expect(metrics.largestContentfulPaint).toBeLessThan(4000); // 4 seconds for very large lists // Verify virtualization is working (should not render all 1000+ tracks at once) if (isVirtualized) { console.log('✅ [PERF] Virtualization detected - only visible tracks rendered'); } else { console.warn('⚠️ [PERF] Virtualization may not be working - all tracks may be rendered'); } console.log('✅ [PERF] Large track list rendered smoothly'); }); test('should render large playlists (100+ tracks) smoothly', async ({ page }) => { // Mock a playlist with 100+ tracks const largePlaylist = { id: 'test-large-playlist', name: 'Large Playlist Test', description: 'Performance test with 100+ tracks', tracks: Array.from({ length: 120 }, (_, i) => ({ id: `track-${i + 1}`, title: `Track ${i + 1}`, artist: `Artist ${i + 1}`, duration: 180 + (i % 60), // Varying durations file_path: `/tracks/track-${i + 1}.mp3`, file_size: 5000000 + (i * 1000), format: 'mp3', is_public: true, play_count: Math.floor(Math.random() * 1000), like_count: Math.floor(Math.random() * 100), created_at: new Date().toISOString(), updated_at: new Date().toISOString(), creator_id: 'test-user', })), created_at: new Date().toISOString(), updated_at: new Date().toISOString(), creator_id: 'test-user', }; // Intercept playlist API call and return mocked data await page.route('**/api/v1/playlists/**', async (route) => { if (route.request().method() === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, data: largePlaylist, }), }); } else { await route.continue(); } }); // Navigate to playlist page const renderStart = Date.now(); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/playlists/${largePlaylist.id}`); // Wait for playlist content to be visible await page.waitForSelector('[data-testid="playlist-detail"], .playlist-detail, main', { timeout: 10000 }); // Wait for tracks to be rendered (check for track list or items) await page.waitForSelector( '[data-testid="playlist-tracks"], .playlist-tracks, [role="list"], table, [role="table"]', { timeout: 10000 } ).catch(() => { // If specific selector not found, wait for any content console.warn('⚠️ [PERF] Specific track list selector not found, waiting for general content'); }); const renderEnd = Date.now(); const renderTime = renderEnd - renderStart; // Measure performance metrics const metrics = await capturePerformanceMetrics(page); // Count rendered track items const trackCount = await page.evaluate(() => { const selectors = [ '[data-testid*="track"]', '[role="listitem"]', 'tr[data-track-id]', '.track-item', 'li', ]; let count = 0; for (const selector of selectors) { const elements = document.querySelectorAll(selector); if (elements.length > 0) { count = elements.length; break; } } return count; }); console.log('Large Playlist Performance Metrics:', { renderTime: `${renderTime}ms`, trackCount: `${trackCount} tracks rendered`, domContentLoaded: `${metrics.domContentLoaded.toFixed(2)}ms`, firstContentfulPaint: `${metrics.firstContentfulPaint.toFixed(2)}ms`, largestContentfulPaint: `${metrics.largestContentfulPaint.toFixed(2)}ms`, timeToInteractive: `${metrics.timeToInteractive.toFixed(2)}ms`, networkRequests: metrics.networkRequests, }); // Verify performance thresholds // Large playlists should render in reasonable time (5 seconds max for 100+ tracks) expect(renderTime).toBeLessThan(5000); // Verify that tracks are being rendered (at least some tracks should be visible) // Note: Virtualization might only render visible tracks, so we check for > 0 expect(trackCount).toBeGreaterThan(0); // Verify smooth rendering - LCP should be acceptable expect(metrics.largestContentfulPaint).toBeLessThan(3000); // 3 seconds for large lists console.log('✅ [PERF] Large playlist rendered smoothly'); }); test('should render many conversations (100+) smoothly', async ({ page }) => { // Mock a large conversation list with 100+ conversations const largeConversationList = Array.from({ length: 120 }, (_, i) => ({ id: `conversation-${i + 1}`, name: `Conversation ${i + 1}`, type: i % 3 === 0 ? 'direct' : 'channel', participants: i % 3 === 0 ? [`user-${i}`, `user-${i + 1}`] : [], unread_count: i % 5 === 0 ? Math.floor(Math.random() * 10) : 0, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), })); // Intercept conversations API call and return mocked data await page.route('**/api/v1/conversations**', async (route) => { if (route.request().method() === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, conversations: largeConversationList, }), }); } else { await route.continue(); } }); // Navigate to chat page const renderStart = Date.now(); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/chat`); // Wait for chat content to be visible await page.waitForSelector('[data-testid="chat-page"], .chat-page, main, [data-testid="chat-sidebar"]', { timeout: 10000 }); // Wait for conversations to be rendered (check for conversation list or items) await page.waitForSelector( '[data-testid="conversation-list"], .conversation-list, [role="list"], [data-testid*="conversation"]', { timeout: 10000 } ).catch(() => { // If specific selector not found, wait for any content console.warn('⚠️ [PERF] Specific conversation list selector not found, waiting for general content'); }); const renderEnd = Date.now(); const renderTime = renderEnd - renderStart; // Measure performance metrics const metrics = await capturePerformanceMetrics(page); // Count rendered conversation items const conversationCount = await page.evaluate(() => { const selectors = [ '[data-testid*="conversation"]', '[data-conversation-id]', '[role="listitem"]', '.conversation-item', 'li', ]; let count = 0; for (const selector of selectors) { const elements = document.querySelectorAll(selector); if (elements.length > 0) { count = elements.length; break; } } return count; }); console.log('Many Conversations Performance Metrics:', { renderTime: `${renderTime}ms`, totalConversations: `${largeConversationList.length} conversations`, renderedConversations: `${conversationCount} conversations rendered`, domContentLoaded: `${metrics.domContentLoaded.toFixed(2)}ms`, firstContentfulPaint: `${metrics.firstContentfulPaint.toFixed(2)}ms`, largestContentfulPaint: `${metrics.largestContentfulPaint.toFixed(2)}ms`, timeToInteractive: `${metrics.timeToInteractive.toFixed(2)}ms`, networkRequests: metrics.networkRequests, }); // Verify performance thresholds // Many conversations should render in reasonable time (5 seconds max for 100+ conversations) expect(renderTime).toBeLessThan(5000); // Verify that conversations are being rendered (at least some conversations should be visible) expect(conversationCount).toBeGreaterThan(0); // Verify smooth rendering - LCP should be acceptable expect(metrics.largestContentfulPaint).toBeLessThan(3000); // 3 seconds for large lists console.log('✅ [PERF] Many conversations rendered smoothly'); }); }); test.describe('Core Web Vitals', () => { test('should meet Core Web Vitals thresholds', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await waitForPageStable(page); const metrics = await capturePerformanceMetrics(page); // Core Web Vitals thresholds (Good) const coreWebVitals = { LCP: metrics.largestContentfulPaint, // Should be < 2.5s FID: metrics.firstInputDelay, // Should be < 100ms (not measured here) CLS: metrics.cumulativeLayoutShift, // Should be < 0.1 FCP: metrics.firstContentfulPaint, // Should be < 1.8s TBT: metrics.totalBlockingTime, // Should be < 300ms }; console.log('Core Web Vitals:', { LCP: `${coreWebVitals.LCP.toFixed(2)}ms (target: < 2500ms)`, FCP: `${coreWebVitals.FCP.toFixed(2)}ms (target: < 1800ms)`, TBT: `${coreWebVitals.TBT.toFixed(2)}ms (target: < 300ms)`, CLS: `${coreWebVitals.CLS.toFixed(4)} (target: < 0.1)`, }); // Assert Core Web Vitals thresholds expect(coreWebVitals.LCP).toBeLessThan(2500); expect(coreWebVitals.FCP).toBeLessThan(1800); expect(coreWebVitals.TBT).toBeLessThan(300); expect(coreWebVitals.CLS).toBeLessThan(0.1); }); }); });