veza/tests/e2e/23-visual-regression.spec.ts
senke 6fad0ad68d fix: stabilize frontend — 98 TS errors to 0, align API endpoints, optimize bundle
- Fix 98 TypeScript errors across 37 files:
  - Service layer double-unwrapping (subscriptionService, distributionService, gearService)
  - Self-referencing variables in SearchPageResults
  - FeedView/ExploreView .posts→.items alignment
  - useQueueSync Zustand subscribe API
  - AdminAuditLogsView missing interface fields
  - Toast proxy type, interceptor type narrowing
  - 22 unused imports/variables removed
  - 5 storybook mock data fixes

- Align frontend API calls with backend endpoints:
  - Analytics: useAnalyticsView now calls /creator/analytics/dashboard (was /analytics)
  - Chat: chatService uses /conversations (was mock data), WS URL from backend token
  - Dashboard StatsSection: uses real /dashboard API data (was hardcoded zeros)
  - Settings: suppress 2FA toast error when endpoint unavailable

- Fix marketplace products: seed uses 'active' status (was 'published')
- Enrich seed: admin follows all creators (feed has content)

- Optimize bundle: vendor catch-all 793KB→318KB gzip (-60%)
  Split into vendor-charts, vendor-emoji, vendor-swagger, vendor-media, etc.

- Clean repo: remove ~100 orphaned screenshots, audit reports, logs from root

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

264 lines
9.3 KiB
TypeScript

import { test, expect } from '@chromatic-com/playwright';
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);
// Extra wait to ensure all fonts and lazy-loaded elements settle
await page.waitForTimeout(ANIMATION_SETTLE_MS + 500);
// Mask dynamic elements (e.g., cursor blink, timestamps) to avoid flaky diffs
await expect(page).toHaveScreenshot('register-page.png', {
fullPage: true,
maxDiffPixelRatio: 0.25,
mask: [
page.locator('input'), // Mask inputs (cursor blink)
page.locator('time'), // Mask any time elements
],
});
});
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 }) => {
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 }) => {
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);
await expect(header).toHaveScreenshot('dashboard-header.png', {
maxDiffPixelRatio: 0.15,
});
});
test('dashboard sidebar only', async ({ page }) => {
await navigateTo(page, '/dashboard');
const sidebar = page.getByTestId('app-sidebar').or(page.locator('aside')).first();
await sidebar
.waitFor({ state: 'visible', timeout: 15000 })
.catch(() => {});
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
await expect(sidebar).toHaveScreenshot('dashboard-sidebar.png', {
maxDiffPixelRatio: 0.15,
});
});
// The global player bar is always rendered (shows idle state when no track is playing).
test('global player bar renders in idle state', async ({ page }) => {
await navigateTo(page, '/dashboard');
await disableAnimations(page);
await ensureDarkTheme(page);
await page.waitForTimeout(ANIMATION_SETTLE_MS);
// The player bar should always be present (idle or playing)
const playerBar = page.getByTestId('global-player');
await expect(playerBar).toBeVisible({ timeout: 10000 });
await expect(playerBar).toHaveScreenshot('player-bar-idle.png', {
maxDiffPixelRatio: 0.15,
});
});
test('profile page visual snapshot', async ({ page }) => {
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 }) => {
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);
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 }) => {
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 }) => {
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 }) => {
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,
});
});
});
});