2025-12-25 17:45:44 +00:00
|
|
|
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<PerformanceMetrics> {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-16 14:18:28 +00:00
|
|
|
test.describe('Large Dataset Performance', () => {
|
2026-01-16 14:19:24 +00:00
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-16 14:18:28 +00:00
|
|
|
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');
|
|
|
|
|
});
|
2026-01-16 14:20:54 +00:00
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
});
|
2026-01-16 14:18:28 +00:00
|
|
|
});
|
|
|
|
|
|
2025-12-25 17:45:44 +00:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|