- 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>
264 lines
9.3 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
});
|
|
});
|