Skip to content
All essays
CraftMarch 27, 202610 min

Visual Testing: Catching UI Regressions

Master visual testing and catch unintended UI changes before users do

Ü
Ümit Uz
Mobile & Full Stack Developer

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

  1. 1Mask dynamic content
  2. 2Test responsive breakpoints
  3. 3Use consistent viewports
  4. 4Update baselines intentionally
  5. 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.

Enjoyed this essay? Share it.

Related essays

Next essay
Contract Testing: Ensuring Service Integration