Component-Level Visual Regression Tests — automated screenshot comparison for catching visual regressions.
Component-Level Visual Regression Tests 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// Component visual tests using page.locator
2import { test, expect } from '@playwright/test';
3
4test.describe('Component visual tests', () => {
5 test('Button variants all match baseline', async ({ page }) => {
6 await page.goto('/design-system/buttons');
7 const buttonGrid = page.locator('[data-testid="button-showcase"]');
8 await expect(buttonGrid).toHaveScreenshot('button-variants.png');
9 });
10
11 test('CourseCard light and dark mode', async ({ page }) => {
12 // Light mode
13 await page.goto('/design-system/course-card');
14 const card = page.locator('[data-testid="course-card-demo"]');
15 await expect(card).toHaveScreenshot('course-card-light.png');
16
17 // Dark mode
18 await page.emulateMedia({ colorScheme: 'dark' });
19 await expect(card).toHaveScreenshot('course-card-dark.png');
20 });
21
22 test('Form error state visual', async ({ page }) => {
23 await page.goto('/login');
24 await page.click('[type="submit"]'); // trigger validation
25 await page.waitForSelector('[role="alert"]');
26 const form = page.locator('form');
27 await expect(form).toHaveScreenshot('login-form-error.png');
28 });
29});This visual test implementation covers component-level visual regression tests 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.