veza/tests/e2e/23-visual-regression.spec.ts
senke 20a16f7cbe test: add comprehensive e2e test suite (34 spec files)
New tests/e2e/ suite covering:
- Auth, navigation, player, tracks, playlists
- Search, discover, social, marketplace, chat
- Accessibility, API, workflows, edge cases
- Routes coverage, forms validation, modals
- Empty states, responsive, network errors
- Error boundary, performance, visual regression
- Cross-browser, profile, smoke, upload
- Storybook, deep pages, visual bugs
- Includes fixtures, helpers, global setup/teardown
- Playwright config and coverage map

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:36:22 +01:00

312 lines
10 KiB
TypeScript

import { test, expect } from '@playwright/test';
import { loginViaAPI, CONFIG, navigateTo } from './helpers';
/**
* Visual Regression Tests @visual
*
* Lightweight visual regression tests that capture screenshots and verify
* pages render correctly. Combined from:
* - visual-complete.spec.ts
* - visual-regression.spec.ts
* - visual/sidebar.spec.ts
* - visual/visual-regression.spec.ts
*/
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);
}
async function disableAnimations(page: import('@playwright/test').Page) {
await page.addStyleTag({
content: `
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
`,
});
}
/**
* Check whether login succeeded (page is no longer on /login).
* Returns true if authenticated, false otherwise.
*/
function isLoggedIn(page: import('@playwright/test').Page): boolean {
return !page.url().includes('/login');
}
test.describe('VISUAL REGRESSION @visual', () => {
test.describe('Auth Pages (unauthenticated)', () => {
test('login page visual snapshot', async ({ page }) => {
await page.emulateMedia({ reducedMotion: 'reduce', colorScheme: 'dark' });
await page.goto('/login', { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle').catch(() => {});
// Wait for the actual login form to render
await page
.waitForSelector('[data-testid="login-form"], input[type="email"]', { timeout: 15000 })
.catch(() => {});
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
await expect(page).toHaveScreenshot('login-page.png', {
fullPage: true,
maxDiffPixelRatio: 0.15,
});
});
test('register page visual snapshot', async ({ page }) => {
await page.emulateMedia({ reducedMotion: 'reduce', colorScheme: 'dark' });
await page.goto('/register', { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle').catch(() => {});
await page
.waitForSelector('[data-testid="register-form"], form, input[type="email"]', {
timeout: 15000,
})
.catch(() => {});
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
await expect(page).toHaveScreenshot('register-page.png', {
fullPage: true,
maxDiffPixelRatio: 0.15,
});
});
test('404 page visual snapshot', async ({ page }) => {
await page.emulateMedia({ reducedMotion: 'reduce', colorScheme: 'dark' });
await page.goto('/non-existent-route-404', { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle').catch(() => {});
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
await expect(page).toHaveScreenshot('404-page.png', {
fullPage: true,
maxDiffPixelRatio: 0.15,
});
});
});
test.describe('Authenticated Pages', () => {
test.beforeEach(async ({ page }) => {
await page.emulateMedia({ reducedMotion: 'reduce', colorScheme: 'dark' });
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
});
test('dashboard full page', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/dashboard');
await page.waitForSelector('main, [role="main"]', { timeout: 15000 }).catch(() => {});
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
await expect(page).toHaveScreenshot('dashboard-full.png', {
fullPage: true,
maxDiffPixelRatio: 0.15,
});
});
test('dashboard header only', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/dashboard');
const header = page.locator('header').first();
await header.waitFor({ timeout: 15000 }).catch(() => {});
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
const headerVisible = await header.isVisible().catch(() => false);
if (!headerVisible) {
test.skip(true, 'Header not visible');
return;
}
await expect(header).toHaveScreenshot('dashboard-header.png', {
maxDiffPixelRatio: 0.15,
});
});
test('dashboard sidebar only', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/dashboard');
const sidebar = page.getByTestId('app-sidebar').or(page.locator('aside')).first();
const visible = await sidebar
.waitFor({ state: 'visible', timeout: 15000 })
.then(() => true)
.catch(() => false);
if (!visible) {
test.skip(true, 'Sidebar not visible (e.g. mobile layout)');
return;
}
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
await expect(sidebar).toHaveScreenshot('dashboard-sidebar.png', {
maxDiffPixelRatio: 0.15,
});
});
test('global player bar', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/dashboard');
await disableAnimations(page);
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 || !(await playerBar.isVisible().catch(() => false))) {
test.skip(true, 'Player bar not visible');
return;
}
await expect(playerBar).toHaveScreenshot('player-bar.png', {
maxDiffPixelRatio: 0.15,
});
});
test('profile page visual snapshot', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/profile');
await page.waitForSelector('main, [role="main"]', { timeout: 15000 }).catch(() => {});
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
await expect(page).toHaveScreenshot('profile-page.png', {
fullPage: true,
maxDiffPixelRatio: 0.15,
});
});
test('playlists page visual snapshot', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/playlists');
await page
.waitForSelector('main, [role="main"]', { timeout: 15000 })
.catch(() => {});
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
await expect(page).toHaveScreenshot('playlists-page.png', {
fullPage: true,
maxDiffPixelRatio: 0.15,
});
});
test('tracks list page visual snapshot', async ({ page }) => {
test.setTimeout(60_000);
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/tracks');
await page.waitForSelector('main, [role="main"]', { timeout: 20000 }).catch(() => {});
await page.waitForTimeout(3000);
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
await expect(page).toHaveScreenshot('tracks-list-page.png', {
fullPage: true,
maxDiffPixelRatio: 0.15,
});
});
test('library page visual snapshot', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/library');
await page
.waitForSelector('main, [role="main"]', { timeout: 15000 })
.catch(() => {});
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
await expect(page).toHaveScreenshot('library-page.png', {
fullPage: true,
maxDiffPixelRatio: 0.15,
});
});
});
test.describe('Responsive Viewports', () => {
test.beforeEach(async ({ page }) => {
await page.emulateMedia({ reducedMotion: 'reduce', colorScheme: 'dark' });
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
});
test('dashboard mobile 375x667', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await page.setViewportSize({ width: 375, height: 667 });
await page.waitForTimeout(200);
await navigateTo(page, '/dashboard');
await page.waitForSelector('main, [role="main"]', { timeout: 15000 }).catch(() => {});
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
await expect(page).toHaveScreenshot('dashboard-mobile.png', {
fullPage: true,
maxDiffPixelRatio: 0.15,
});
});
test('dashboard tablet 768x1024', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await page.setViewportSize({ width: 768, height: 1024 });
await page.waitForTimeout(200);
await navigateTo(page, '/dashboard');
await page.waitForSelector('main, [role="main"]', { timeout: 15000 }).catch(() => {});
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
await expect(page).toHaveScreenshot('dashboard-tablet.png', {
fullPage: true,
maxDiffPixelRatio: 0.15,
});
});
});
});