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>
312 lines
10 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
});
|
|
});
|