Visual testing detects unintended UI changes by comparing screenshots against baselines. It catches CSS regressions that traditional tests miss.
Why Visual Testing?
- Detects visual regressions
- Validates responsive design
- Tests across browsers/devices
- Complements functional tests
- Catches CSS framework updates
Tools
Percy
typescript
// percy.config.ts
import { defineConfig } from '@percy/cli';
export default defineConfig({
snapshot: {
widths: [375, 768, 1280],
minHeight: 1024,
percyCSS: `body { max-width: 1400px; margin: 0 auto; }`
}
});typescript
// tests/homepage.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Homepage Visual Tests', () => {
test('matches baseline', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png');
});
test('responsive designs', async ({ page }) => {
await page.goto('/');
// Desktop
await page.setViewportSize({ width: 1280, height: 720 });
await expect(page).toHaveScreenshot('homepage-desktop.png');
// Tablet
await page.setViewportSize({ width: 768, height: 1024 });
await expect(page).toHaveScreenshot('homepage-tablet.png');
// Mobile
await page.setViewportSize({ width: 375, height: 667 });
await expect(page).toHaveScreenshot('homepage-mobile.png');
});
});Playwright Screenshots
typescript
import { test, expect } from '@playwright/test';
test('full page screenshot', async ({ page }) => {
await page.goto('/');
await page.screenshot({
path: 'screenshots/homepage.png',
fullPage: true
});
});
test('element screenshot', async ({ page }) => {
await page.goto('/');
const header = page.locator('header');
await header.screenshot({ path: 'screenshots/header.png' });
});
test('visual regression with mask', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
mask: [page.locator('.dynamic-content')]
});
});Best Practices
- 1Mask dynamic content
- 2Test responsive breakpoints
- 3Use consistent viewports
- 4Update baselines intentionally
- 5Run in CI/CD pipeline
Handling Dynamic Content
typescript
test('handle timestamps', async ({ page }) => {
await page.goto('/');
// Freeze time
await page.addInitScript(() => {
Date.now = () => new Date('2024-01-01').getTime();
});
await expect(page).toHaveScreenshot('frozen-time.png');
});Conclusion
Visual testing catches UI regressions that functional tests miss. Integrate it into your testing strategy for comprehensive UI quality assurance.