Visual Regression Testing with Screenshot Comparison — automated screenshot comparison for catching visual regressions.
Visual Regression Testing with Screenshot Comparison enables catching unintended visual changes automatically. Instead of manually reviewing UI after every change, screenshot comparison detects pixel-level differences and reports them in CI. This prevents visual regressions from shipping to users undetected.
1npm install --save-dev @playwright/test
2npx playwright install --with-deps chromium
3
4# Generate initial baseline screenshots:
5npx playwright test tests/visual/ --update-snapshots
6
7# Run visual comparison tests:
8npx playwright test tests/visual/
9
10# On failure, screenshots are in test-results/
11# The diff image shows exactly what changed1// playwright.config.ts
2export default defineConfig({
3 expect: {
4 toHaveScreenshot: {
5 maxDiffPixelRatio: 0.02, // 2% pixel difference allowed
6 threshold: 0.2, // per-pixel color tolerance
7 animations: 'disabled', // freeze CSS animations
8 },
9 },
10 snapshotDir: './tests/visual/__screenshots__',
11});1await expect(page).toHaveScreenshot('page.png', {
2 // Mask elements that change between test runs
3 mask: [
4 page.locator('[data-testid="timestamp"]'),
5 page.locator('[data-testid="user-avatar"]'),
6 page.locator('.advertisement'),
7 ],
8 animations: 'disabled',
9});
10
11// Freeze dynamic content before screenshot:
12await page.evaluate(() => {
13 document.querySelectorAll('[data-testid="live-count"]')
14 .forEach(el => el.textContent = '1,234');
15 const style = document.createElement('style');
16 style.textContent = '*, *::before, *::after { transition-duration: 0s !important; }';
17 document.head.appendChild(style);
18});1- name: Run visual tests
2 run: npx playwright test tests/visual/
3 continue-on-error: true
4
5- name: Upload visual test artifacts
6 uses: actions/upload-artifact@v4
7 if: always()
8 with:
9 name: visual-results
10 path: |
11 playwright-report/
12 test-results/
13 retention-days: 30Workflow for baseline updates:
npx playwright test --update-snapshots locally1test('dark mode visual consistency', async ({ page }) => {
2 // Light mode baseline
3 await page.goto('/courses');
4 await expect(page).toHaveScreenshot('courses-light.png');
5
6 // Dark mode — emulate prefers-color-scheme: dark
7 await page.emulateMedia({ colorScheme: 'dark' });
8 await page.reload();
9 await expect(page).toHaveScreenshot('courses-dark.png');
10
11 // Verify dark mode is actually applied
12 const bodyBg = await page.evaluate(() =>
13 getComputedStyle(document.body).backgroundColor
14 );
15 // Dark backgrounds have low luminance
16 const [r, g, b] = bodyBg.match(/\d+/g)?.map(Number) || [];
17 const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
18 expect(luminance).toBeLessThan(128); // dark background
19});1const BREAKPOINTS = [
2 { name: 'mobile', width: 375, height: 812 },
3 { name: 'tablet', width: 768, height: 1024 },
4 { name: 'desktop', width: 1280, height: 800 },
5];
6
7for (const { name, width, height } of BREAKPOINTS) {
8 test(`homepage at ${name}`, async ({ page }) => {
9 await page.setViewportSize({ width, height });
10 await page.goto('/');
11 await expect(page).toHaveScreenshot(`homepage-${name}.png`, {
12 fullPage: true,
13 animations: 'disabled',
14 });
15 });
16}
17
18// Also check for horizontal overflow:
19test('no horizontal scroll on mobile', async ({ page }) => {
20 await page.setViewportSize({ width: 375, height: 812 });
21 await page.goto('/');
22 const hasScroll = await page.evaluate(() =>
23 document.documentElement.scrollWidth > document.documentElement.clientWidth
24 );
25 expect(hasScroll).toBe(false);
26});Production visual regression tests with Playwright covering pages, components, dark mode, and responsive breakpoints.
1// Playwright visual regression — baseline screenshot comparison
2import { test, expect } from '@playwright/test';
3
4test.describe('Visual Regression Tests', () => {
5 test('homepage matches baseline', async ({ page }) => {
6 await page.goto('/');
7 await page.waitForLoadState('networkidle');
8 await expect(page).toHaveScreenshot('homepage.png', {
9 maxDiffPixelRatio: 0.02,
10 mask: [page.locator('[data-testid="timestamp"]')],
11 });
12 });
13
14 test('course card component visual', async ({ page }) => {
15 await page.goto('/courses');
16 const firstCard = page.locator('[data-testid="course-card"]').first();
17 await expect(firstCard).toHaveScreenshot('course-card.png', {
18 maxDiffPixelRatio: 0.01,
19 });
20 });
21
22 test('dark mode homepage matches baseline', async ({ page }) => {
23 await page.emulateMedia({ colorScheme: 'dark' });
24 await page.goto('/');
25 await expect(page).toHaveScreenshot('homepage-dark.png');
26 });
27
28 // Run at multiple breakpoints
29 for (const vp of [
30 { width: 375, height: 812, name: 'mobile' },
31 { width: 768, height: 1024, name: 'tablet' },
32 { width: 1280, height: 800, name: 'desktop' },
33 ]) {
34 test('homepage at ' + vp.name + ' (' + vp.width + 'px)', async ({ page }) => {
35 await page.setViewportSize({ width: vp.width, height: vp.height });
36 await page.goto('/');
37 await expect(page).toHaveScreenshot('homepage-' + vp.name + '.png');
38 });
39 }
40});This visual test implementation covers visual regression testing with screenshot comparison with Playwright's built-in screenshot comparison. Baseline screenshots are stored in __screenshots__ and compared on every test run. The 2% maxDiffPixelRatio allows for minor rendering differences across environments. CI uploads failure artifacts for review, and developers update baselines by running npx playwright test --update-snapshots when changes are intentional.
Continue with the next visual regression topic to build comprehensive screenshot coverage across your entire UI.
Your progress is saved in this browser, on this device only.