import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from '../../utils/test-helpers'; /** Pixel-perfect visual regression: strict by default. Relax in CI if needed via VISUAL_MAX_DIFF_PIXELS. */ const MAX_DIFF_PIXELS = process.env.VISUAL_MAX_DIFF_PIXELS ? parseInt(process.env.VISUAL_MAX_DIFF_PIXELS, 10) : 0; const ANIMATION_SETTLE_MS = 800; async function ensureDarkTheme(page: import('@playwright/test').Page) { await page.evaluate(() => { document.documentElement.classList.add('dark'); document.documentElement.setAttribute('data-theme', 'dark'); }); await page.waitForTimeout(100); } test.describe('Visual regression (pixel-perfect)', () => { test.beforeEach(async ({ page }) => { await page.emulateMedia({ reducedMotion: 'reduce' }); }); test.describe('Auth pages (no storage)', () => { test.use({ storageState: { cookies: [], origins: [] } }); test('login page', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/login`); await page.waitForLoadState('networkidle'); await page.waitForSelector('form', { timeout: 10000 }); await ensureDarkTheme(page); await page.waitForTimeout(ANIMATION_SETTLE_MS); await expect(page).toHaveScreenshot('login-page.png', { fullPage: true, maxDiffPixels: MAX_DIFF_PIXELS, }); }); test('register page', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/register`); await page.waitForLoadState('networkidle'); await page.waitForSelector('form, [role="form"], input[type="email"]', { timeout: 15000 }).catch(() => {}); await ensureDarkTheme(page); await page.waitForTimeout(ANIMATION_SETTLE_MS); await expect(page).toHaveScreenshot('register-page.png', { fullPage: true, maxDiffPixels: Math.max(MAX_DIFF_PIXELS, 10), // allow minor font/subpixel variance }); }); }); test.describe('App shell (authenticated)', () => { test('dashboard full page', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); await page.waitForSelector('main, [role="main"]', { timeout: 15000 }); await ensureDarkTheme(page); await page.waitForTimeout(ANIMATION_SETTLE_MS); await expect(page).toHaveScreenshot('dashboard-full.png', { fullPage: true, maxDiffPixels: MAX_DIFF_PIXELS, }); }); test('dashboard header only', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); const header = page.locator('header').first(); await header.waitFor({ timeout: 10000 }); await ensureDarkTheme(page); await page.waitForTimeout(ANIMATION_SETTLE_MS); await expect(header).toHaveScreenshot('dashboard-header.png', { maxDiffPixels: MAX_DIFF_PIXELS, }); }); test('dashboard sidebar only', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); const sidebar = page.locator('aside').first(); const visible = await sidebar.waitFor({ state: 'visible', timeout: 12000 }).then(() => true).catch(() => false); if (!visible) { test.skip(true, 'Sidebar not visible (e.g. not authenticated or mobile layout)'); return; } await ensureDarkTheme(page); await page.waitForTimeout(ANIMATION_SETTLE_MS); await expect(sidebar).toHaveScreenshot('dashboard-sidebar.png', { maxDiffPixels: MAX_DIFF_PIXELS, }); }); test('global player bar', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); await ensureDarkTheme(page); await page.waitForTimeout(ANIMATION_SETTLE_MS); const playerBar = page.locator('div.fixed.bottom-0.left-0.right-0').first(); await playerBar.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {}); if ((await playerBar.count()) === 0) { test.skip(); return; } await expect(playerBar).toHaveScreenshot('player-bar.png', { maxDiffPixels: MAX_DIFF_PIXELS, }); }); }); test.describe('Key routes', () => { test('playlists page', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/playlists`); await page.waitForLoadState('networkidle'); await page.waitForSelector('main, [role="main"]', { timeout: 10000 }).catch(() => {}); await ensureDarkTheme(page); await page.waitForTimeout(ANIMATION_SETTLE_MS); await expect(page).toHaveScreenshot('playlists-page.png', { fullPage: true, maxDiffPixels: MAX_DIFF_PIXELS, }); }); test('404 page', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/non-existent-route-404`); await page.waitForLoadState('networkidle'); await ensureDarkTheme(page); await page.waitForTimeout(ANIMATION_SETTLE_MS); await expect(page).toHaveScreenshot('404-page.png', { fullPage: true, maxDiffPixels: MAX_DIFF_PIXELS, }); }); }); test.describe('Viewports', () => { test('dashboard mobile 375x667', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); await page.waitForTimeout(ANIMATION_SETTLE_MS); await ensureDarkTheme(page); await expect(page).toHaveScreenshot('dashboard-mobile.png', { fullPage: true, maxDiffPixels: MAX_DIFF_PIXELS, }); }); test('dashboard tablet 768x1024', async ({ page }) => { await page.setViewportSize({ width: 768, height: 1024 }); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); await page.waitForTimeout(ANIMATION_SETTLE_MS); await ensureDarkTheme(page); await expect(page).toHaveScreenshot('dashboard-tablet.png', { fullPage: true, maxDiffPixels: MAX_DIFF_PIXELS, }); }); }); });