You have been checking text, visibility, and attributes. But what about the visual appearance of the page? Did someone accidentally change the font? Is the logo misaligned? Did a CSS update break the layout? Text assertions cannot catch visual bugs. Snapshot and screenshot assertions can.
toHaveScreenshot takes a screenshot of the page or element and compares it pixel-by-pixel against a saved reference image. If the pixels differ beyond a threshold, the test fails. First run creates the reference. Subsequent runs compare against it.
import { test, expect } from '@playwright/test';
test('full page visual comparison', async ({ page }) => {
await page.goto('/banking/dashboard');
// Compare full page screenshot against reference
await expect(page).toHaveScreenshot();
// Named screenshot (useful when multiple screenshots per test)
await expect(page).toHaveScreenshot('dashboard-loaded.png');
// Allow small pixel differences (anti-aliasing, font rendering)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
});
// Allow percentage-based difference
await expect(page).toHaveScreenshot({
maxDiffPixelRatio: 0.01, // 1% of total pixels
});
});import { test, expect } from '@playwright/test';
test('component visual comparison', async ({ page }) => {
await page.goto('/banking/dashboard');
// Screenshot of a specific element
const balanceCard = page.getByTestId('balance-card');
await expect(balanceCard).toHaveScreenshot('balance-card.png');
// Navigation bar only
const nav = page.getByRole('navigation');
await expect(nav).toHaveScreenshot('navigation.png');
// Chart widget
await expect(page.getByTestId('spending-chart')).toHaveScreenshot(
'spending-chart.png',
{ maxDiffPixels: 50 }
);
});Run the test first time: npx playwright test -- reference screenshots are created automatically
Screenshots are saved in a __snapshots__ folder next to the test file
Commit the screenshots to git -- they are your baseline
Run tests again -- Playwright compares new screenshots against the baseline
If UI changes intentionally, update snapshots: npx playwright test --update-snapshots
Review updated screenshots in git diff before committing
# First run: creates reference screenshots
npx playwright test visual.spec.ts
# After intentional UI changes: update baselines
npx playwright test visual.spec.ts --update-snapshots
# Screenshots are saved here:
# tests/visual.spec.ts-snapshots/
# dashboard-loaded-chromium-linux.png
# dashboard-loaded-firefox-linux.png
# dashboard-loaded-webkit-linux.pngtoMatchSnapshot is for non-image data. Compare API responses, HTML structure, or any serializable value against a saved reference.
import { test, expect } from '@playwright/test';
test('API response snapshot', async ({ page }) => {
await page.goto('/banking/dashboard');
// Snapshot of accessibility tree
const snapshot = await page.accessibility.snapshot();
expect(snapshot).toMatchSnapshot('dashboard-a11y.json');
// Snapshot of element HTML
const html = await page.getByTestId('nav-menu').innerHTML();
expect(html).toMatchSnapshot('nav-menu.html');
});import { test, expect } from '@playwright/test';
test('screenshot with masked dynamic content', async ({ page }) => {
await page.goto('/banking/dashboard');
// Mask elements that change on every run (dates, times, balances)
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [
page.getByTestId('current-date'),
page.getByTestId('balance'),
page.getByTestId('last-login'),
],
});
// Masked elements are replaced with a colored box in the screenshot
// The rest of the page is compared pixel-by-pixel
});Visual tests are OS and browser dependent. A screenshot from macOS Chrome will not match Linux Chrome due to font rendering differences. Always run visual tests in a consistent environment -- Docker or CI with the same OS.
Start with element-level screenshots, not full-page. Full-page screenshots are brittle -- any tiny change anywhere fails the test. Element screenshots let you protect specific components: the header, the navigation, the data table. Much more stable.
Q: How do you do visual testing in Playwright?
A: Playwright has built-in visual comparison with toHaveScreenshot(). First run creates reference screenshots. Subsequent runs compare pixel-by-pixel. I use maxDiffPixels or maxDiffPixelRatio to allow minor rendering differences. For dynamic content like dates and balances, I use the mask option to exclude them from comparison. I prefer element-level screenshots over full-page because they are more stable. I run visual tests in Docker to ensure consistent font rendering across environments. When UI changes are intentional, I update baselines with --update-snapshots and review the diffs in git.
Key Point: toHaveScreenshot does pixel-level visual comparison. Use mask for dynamic content. Run in consistent environments. Start with element-level, not full-page screenshots.