PracticeQAAutomation logoPracticeQAAutomation

Visual Testing

Catch what functional tests miss: a button moved 2px, a colour change, a broken image. Take a baseline screenshot of the card, plant a bug, and see your test fail — with Playwright's toHaveScreenshot or Cypress. Every recipe is run against this page in our test suite.

The card under test

Take a screenshot of this card as your baseline, then plant one of the bugs below and see if your test catches it. The viewer count and countdown change on their own — mask them or freeze time.

HeadphonesBestseller

Noise-cancelling Headphones

★★★★★ 4.7 (2,318)

₹7,999 ₹9,999

Plant a visual bug

Or open the page with ?bug=shift (any id above) so your test can load it directly.

Show code for

1. Take a baseline, then catch a bug

The first run saves a baseline image. Every later run compares against it. Plant a bug with ?bug=shift and run again: the test fails and saves a diff image showing exactly what moved.

Playwright
import { test, expect } from '@playwright/test';

test('product card looks right', async ({ page }) => {
  await page.goto('/misc/visual-testing');               // then try '/misc/visual-testing?bug=shift'
  const card = page.getByTestId('vt-card');
  await expect(card).toHaveScreenshot('product-card.png', {
    mask: [card.getByTestId('vt-viewers'), card.getByTestId('vt-clock')],  // parts that change every second
    animations: 'disabled',                                              // stop the pulsing badge
  });
});

// First run: npx playwright test --update-snapshots   (saves the baseline)
// Later runs: npx playwright test                      (compares; diff images go to test-results/)

2. Freeze time instead of masking

The viewer count and the countdown are worked out from the current time. Freeze the page's clock and they become the same on every run — nothing needs hiding.

Playwright
import { test, expect } from '@playwright/test';

test('card with frozen time', async ({ page }) => {
  await page.clock.setFixedTime(new Date('2026-01-15T10:00:00'));
  await page.goto('/misc/visual-testing');
  await expect(page.getByTestId('vt-clock')).toHaveText('Offer ends in 14:00:00');
  await expect(page.getByTestId('vt-card')).toHaveScreenshot('card-frozen.png', { animations: 'disabled' });
});

3. Thresholds: how different is “different”?

Surprise: with the default settings, ?bug=colour (purple → indigo) passes recipe 1! A pixel only counts as changed if its colour moves more than the threshold (0.2 by default). Set threshold to 0 to catch both colour bugs — and allow a few changed pixels for anti-aliasing noise instead.

Playwright
import { test, expect } from '@playwright/test';

test('strict colour check', async ({ page }) => {
  await page.clock.setFixedTime(new Date('2026-01-15T10:00:00'));
  await page.goto('/misc/visual-testing');                // then try '/misc/visual-testing?bug=colour'
  await expect(page.getByTestId('vt-card')).toHaveScreenshot('card-strict.png', {
    animations: 'disabled',
    threshold: 0,          // any colour change counts…
    maxDiffPixels: 10,     // …but tolerate a handful of noisy pixels
  });
});

4. Check it on a phone-sized screen

Layout bugs often only appear at small widths. Take a separate baseline per screen size.

Playwright
import { test, expect } from '@playwright/test';

test.use({ viewport: { width: 375, height: 800 } });

test('card on a phone', async ({ page }) => {
  await page.clock.setFixedTime(new Date('2026-01-15T10:00:00'));
  await page.goto('/misc/visual-testing');
  await expect(page.getByTestId('vt-card')).toHaveScreenshot('card-phone.png', { animations: 'disabled' });
});

Good to know

Visual tests are powerful but picky.

  • Baselines are per machine. Fonts and anti-aliasing differ between macOS, Windows and Linux, so create and compare baselines on the same OS — in CI, generate them in the same Docker image the tests run in.
  • Screenshot the component, not the whole page, so an unrelated change elsewhere doesn't fail every test.
  • Review diffs before updating baselines. Running with --update-snapshots accepts whatever is on screen — including real bugs.
  • Hosted services (Percy, Applitools, Chromatic) add review workflows and cross-browser rendering on top of the same idea.