[FE-TEST-014] fe-test: Add visual regression tests
This commit is contained in:
parent
f127e1e356
commit
4e1e414aa2
2 changed files with 294 additions and 3 deletions
|
|
@ -10227,8 +10227,10 @@
|
|||
"description": "Add screenshot tests for UI components",
|
||||
"owner": "frontend",
|
||||
"estimated_hours": 6,
|
||||
"status": "todo",
|
||||
"files_involved": [],
|
||||
"status": "completed",
|
||||
"files_involved": [
|
||||
"apps/web/e2e/visual-regression.spec.ts"
|
||||
],
|
||||
"implementation_steps": [
|
||||
{
|
||||
"step": 1,
|
||||
|
|
@ -10248,7 +10250,14 @@
|
|||
"Unit tests",
|
||||
"Integration tests"
|
||||
],
|
||||
"notes": ""
|
||||
"notes": "",
|
||||
"completed_at": "2025-12-25T18:45:00.281678",
|
||||
"validation": {
|
||||
"typescript_compilation": "No errors in new file",
|
||||
"linter": "No linting errors",
|
||||
"test_file_created": "e2e/visual-regression.spec.ts",
|
||||
"coverage": "Visual regression tests for pages and components"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "FE-TEST-015",
|
||||
|
|
|
|||
282
apps/web/e2e/visual-regression.spec.ts
Normal file
282
apps/web/e2e/visual-regression.spec.ts
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { loginAsUser, TEST_CONFIG } from './utils/test-helpers';
|
||||
|
||||
/**
|
||||
* Visual Regression Tests
|
||||
*
|
||||
* These tests capture screenshots of UI components and pages
|
||||
* to detect visual regressions. Screenshots are stored in:
|
||||
* - test-results/visual-regression.spec.ts-snapshots/
|
||||
*
|
||||
* To update screenshots after intentional changes:
|
||||
* - Run: npx playwright test --update-snapshots
|
||||
*
|
||||
* To run only visual tests:
|
||||
* - Run: npx playwright test visual-regression
|
||||
*/
|
||||
|
||||
test.describe('Visual Regression Tests', () => {
|
||||
// Use authenticated state for most tests
|
||||
test.use({ storageState: 'e2e/.auth/user.json' });
|
||||
|
||||
test.describe('Authentication Pages', () => {
|
||||
test('login page visual snapshot', async ({ page }) => {
|
||||
// Use unauthenticated state for login page
|
||||
await page.context().clearCookies();
|
||||
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/login`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for form to be fully rendered
|
||||
await page.waitForSelector('form', { timeout: 5000 });
|
||||
await page.waitForTimeout(500); // Allow animations to settle
|
||||
|
||||
await expect(page).toHaveScreenshot('login-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 100, // Allow small differences (fonts, anti-aliasing)
|
||||
});
|
||||
});
|
||||
|
||||
test('register page visual snapshot', async ({ page }) => {
|
||||
// Use unauthenticated state for register page
|
||||
await page.context().clearCookies();
|
||||
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/register`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for form to be fully rendered
|
||||
await page.waitForSelector('form', { timeout: 5000 });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(page).toHaveScreenshot('register-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 100,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Dashboard Pages', () => {
|
||||
test('dashboard page visual snapshot', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for main content to load
|
||||
await page.waitForSelector('main, [role="main"]', { timeout: 10000 });
|
||||
await page.waitForTimeout(1000); // Allow data to load
|
||||
|
||||
await expect(page).toHaveScreenshot('dashboard-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
});
|
||||
});
|
||||
|
||||
test('dashboard header visual snapshot', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for header
|
||||
const header = page.locator('header').first();
|
||||
await header.waitFor({ timeout: 5000 });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(header).toHaveScreenshot('dashboard-header.png', {
|
||||
maxDiffPixels: 50,
|
||||
});
|
||||
});
|
||||
|
||||
test('dashboard sidebar visual snapshot', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for sidebar
|
||||
const sidebar = page.locator('aside').first();
|
||||
await sidebar.waitFor({ timeout: 5000 });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(sidebar).toHaveScreenshot('dashboard-sidebar.png', {
|
||||
maxDiffPixels: 50,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Profile Page', () => {
|
||||
test('profile page visual snapshot', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/profile`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for profile content
|
||||
await page.waitForSelector('main, [role="main"]', { timeout: 10000 });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page).toHaveScreenshot('profile-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Tracks Pages', () => {
|
||||
test('tracks list page visual snapshot', async ({ page }) => {
|
||||
// Navigate to tracks page (adjust route as needed)
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/tracks`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for tracks list to load
|
||||
await page.waitForTimeout(2000); // Allow tracks to load
|
||||
|
||||
await expect(page).toHaveScreenshot('tracks-list-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Playlists Pages', () => {
|
||||
test('playlists page visual snapshot', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/playlists`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for playlists to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await expect(page).toHaveScreenshot('playlists-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('UI Components', () => {
|
||||
test('button variants visual snapshot', async ({ page }) => {
|
||||
// Create a test page with button variants
|
||||
await page.setContent(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/src/index.css">
|
||||
</head>
|
||||
<body style="padding: 20px; background: white;">
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<button class="bg-primary text-white px-4 py-2 rounded">Primary Button</button>
|
||||
<button class="bg-secondary text-white px-4 py-2 rounded">Secondary Button</button>
|
||||
<button class="border px-4 py-2 rounded">Outline Button</button>
|
||||
<button class="bg-destructive text-white px-4 py-2 rounded">Destructive Button</button>
|
||||
<button class="bg-primary text-white px-4 py-2 rounded" disabled>Disabled Button</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(page).toHaveScreenshot('button-variants.png', {
|
||||
maxDiffPixels: 50,
|
||||
});
|
||||
});
|
||||
|
||||
test('card component visual snapshot', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/src/index.css">
|
||||
</head>
|
||||
<body style="padding: 20px; background: white;">
|
||||
<div class="rounded-lg border bg-card shadow-sm p-6" style="max-width: 400px;">
|
||||
<h3 class="text-lg font-semibold mb-2">Card Title</h3>
|
||||
<p class="text-muted-foreground">This is a card component with some content.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(page).toHaveScreenshot('card-component.png', {
|
||||
maxDiffPixels: 50,
|
||||
});
|
||||
});
|
||||
|
||||
test('form elements visual snapshot', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/src/index.css">
|
||||
</head>
|
||||
<body style="padding: 20px; background: white;">
|
||||
<form style="max-width: 400px; display: flex; flex-direction: column; gap: 15px;">
|
||||
<div>
|
||||
<label class="block mb-1">Text Input</label>
|
||||
<input type="text" class="w-full px-3 py-2 border rounded" placeholder="Enter text">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1">Email Input</label>
|
||||
<input type="email" class="w-full px-3 py-2 border rounded" placeholder="email@example.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1">Password Input</label>
|
||||
<input type="password" class="w-full px-3 py-2 border rounded" placeholder="Password">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1">Textarea</label>
|
||||
<textarea class="w-full px-3 py-2 border rounded" rows="3" placeholder="Enter message"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="bg-primary text-white px-4 py-2 rounded">Submit</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(page).toHaveScreenshot('form-elements.png', {
|
||||
maxDiffPixels: 50,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Error States', () => {
|
||||
test('404 page visual snapshot', async ({ page }) => {
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/non-existent-page`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page).toHaveScreenshot('404-page.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 100,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Responsive Design', () => {
|
||||
test('mobile viewport dashboard snapshot', async ({ page }) => {
|
||||
// Set mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page).toHaveScreenshot('dashboard-mobile.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
});
|
||||
});
|
||||
|
||||
test('tablet viewport dashboard snapshot', async ({ page }) => {
|
||||
// Set tablet viewport
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page).toHaveScreenshot('dashboard-tablet.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 200,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in a new issue