IKivan-kozenko -aqa
Try yourself as a QA tester
All topics·Advanced·1 / 24

Visual comparisons

Visual snapshot testing: first run generates the reference, every run after compares pixel-by-pixel. I use it for catching accidental CSS regressions — a layout that looks fine in code but breaks visually. The main challenge is flakiness from dynamic content like timestamps and ads — mask those with stylePath or mask option.

How screenshot comparison works

First run always fails and generates the reference — commit that file, subsequent runs compare against it

First run: no reference exists, so Playwright generates it and writes the file to disk. The test fails on the first run — that's expected. Commit the generated file. Every run after that: Playwright takes a fresh screenshot and compares it pixel-by-pixel to the saved reference.

Screenshot files are named with browser and OS in the name: orders-page-1-chromium-darwin.png. That's because rendering differs between browsers and platforms — you need separate references for each. If you run tests on Linux CI but generate references on macOS, the comparison will fail.

ts
test('orders page looks correct', async ({ page }) => {
  await page.goto('/orders')
  await page.waitForLoadState('networkidle')

  // Порівняти весь page
  await expect(page).toHaveScreenshot()

  // Або з явним іменем (рекомендую — легше знайти файл)
  await expect(page).toHaveScreenshot('orders-page.png')
})

test('order card component', async ({ page }) => {
  await page.goto('/orders/42')

  // Порівняти тільки конкретний елемент
  const card = page.getByTestId('order-card')
  await expect(card).toHaveScreenshot('order-card.png')
})

Generating and updating references

When the page design changes intentionally, you need to update the reference. Use --update-snapshots to regenerate all references. Review the diff in git before committing — this is the checkpoint where you confirm the change is intentional.

bash
# Перший запуск — згенерувати еталони
npx playwright test --update-snapshots

# Оновити еталон після навмисних змін дизайну
npx playwright test --update-snapshots orders.spec.ts

# Переглянути що змінилося
git diff tests/orders.spec.ts-snapshots/

Tolerance — allow minor pixel differences

Anti-aliasing, font rendering, and subpixel differences cause minor pixel variations between runs. I set maxDiffPixelRatio: 0.01 globally — allows 1% of pixels to differ without failing. For individual assertions I use maxDiffPixels when a specific component is known to have micro-rendering differences.

ts
// playwright.config.ts
export default defineConfig({
  expect: {
    toHaveScreenshot: {
      maxDiffPixelRatio: 0.01, // допустити до 1% різниці пікселів
    },
  },
})
ts
// Або для конкретної перевірки
await expect(page).toHaveScreenshot('dashboard.png', {
  maxDiffPixels: 100, // точна кількість пікселів що можуть відрізнятися
})

Masking dynamic content — timestamps, avatars, ads

Dynamic content like timestamps, user avatars, or live counters will always differ between runs and make visual tests flaky. Two ways to handle it: mask option (Playwright overlays a colored box) or stylePath (inject CSS that hides elements).

ts
test('order list - visual', async ({ page }) => {
  await page.goto('/orders')

  // Маскувати елементи що змінюються між запусками
  await expect(page).toHaveScreenshot('orders.png', {
    mask: [
      page.getByTestId('created-at-column'),   // часові мітки
      page.getByTestId('user-avatar'),          // аватари
      page.getByTestId('live-counter'),         // лічильники
    ],
  })
})
css
/* screenshot.css — ховати dynamic елементи глобально */
[data-testid="created-at-column"],
[data-testid="user-avatar"],
.ad-banner,
.live-indicator {
  visibility: hidden !important;
}
ts
// playwright.config.ts — застосувати CSS до всіх snapshot-тестів
export default defineConfig({
  expect: {
    toHaveScreenshot: {
      stylePath: './screenshot.css',
    },
  },
})

Text snapshots — compare API responses and text content

Beyond screenshots, toMatchSnapshot() works for any text or binary data. I use it for API response structure snapshots — when I want to catch unexpected field changes in an endpoint response.

ts
test('order page title snapshot', async ({ page }) => {
  await page.goto('/orders/42')
  const title = await page.getByRole('heading').textContent()
  expect(title).toMatchSnapshot('order-title.txt')
})

// Snapshot API-відповіді
test('orders API response structure', async ({ request }) => {
  const response = await request.get('/api/orders?limit=1')
  const body = await response.json()
  // Перший запуск збереже структуру; наступні будуть порівнювати
  expect(JSON.stringify(body, null, 2)).toMatchSnapshot('orders-api.json')
})