veza/apps/web/e2e/tests/visual/visual-regression.spec.ts
senke 37c5acc302 style(ui): Spotify-like palette, player, sidebar and dashboard polish
- Dark palette: background 0.11, card 0.14, sidebar 0.08 (Spotify #121212 feel)
- MiniPlayer: h-16, thinner progress bar, compact cover/times, rounded actions
- Sidebar: minimal header, lighter section labels, clean nav hover (no heavy border)
- Header: h-16, rounded search pill, compact user pill and dropdown
- Dashboard: pt-20 to match header, StatCard typography and spacing tweaks
- Visual: register-page snapshot + small maxDiffPixels tolerance for font variance

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 20:19:59 +01:00

167 lines
6.1 KiB
TypeScript

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,
});
});
});
});