- playwright.config.visual.ts: dedicated config, viewport 1280x720, Chromium only, snapshots in e2e/tests/visual/__snapshots__ - e2e/tests/visual/visual-regression.spec.ts: login, register, dashboard (full/header/sidebar), player bar, playlists, 404, mobile/tablet viewports; dark theme + reduceMotion - scripts/visual-diff.js: optional pixelmatch script to generate diff image from two PNGs - docs/VISUAL_TESTING_STRATEGY.md: strategy, commands, CI, workflow - npm scripts: test:visual, test:visual:update, test:visual:report - deps: pixelmatch, pngjs; @playwright/test aligned to 1.58.1 - baseline snapshots added for login, dashboard, playlists, 404, viewports Co-authored-by: Cursor <cursoragent@cursor.com>
167 lines
6 KiB
TypeScript
167 lines
6 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: MAX_DIFF_PIXELS,
|
|
});
|
|
});
|
|
});
|
|
|
|
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,
|
|
});
|
|
});
|
|
});
|
|
});
|