| |

Playwright Visual Testing: Day 9 Tutorial

Playwright visual testing Day 9 featured image with screenshot baselines and diff review

Playwright visual testing is where your automation suite starts checking the page the way a user actually experiences it. On Day 9, I want you to stop treating screenshots as decoration and start using them as a controlled regression signal with TypeScript, baselines, masks, thresholds, and CI review.

Table of Contents

Contents

Why Playwright visual testing matters

Most UI automation checks text, buttons, URLs, and API responses. That is useful, but it misses a large class of bugs: layout shifts, broken spacing, invisible text, wrong colors, clipped cards, missing icons, and modals that render behind the overlay. I see teams ship these bugs even when every locator assertion is green.

Playwright’s official visual comparison docs say Playwright Test can create screenshots with await expect(page).toHaveScreenshot(), generate a reference screenshot on first run, and compare later runs against that reference. That is the core idea. The first run creates the baseline. Every serious run after that asks one question: did the UI drift in a way we did not approve?

For this series, Day 8 covered Playwright API Testing. API checks are excellent for data correctness. Visual checks are excellent for presentation correctness. A stable suite needs both because users do not see JSON payloads. They see forms, dashboards, errors, tables, receipts, and empty states.

Where visual testing catches bugs normal assertions miss

  • A pricing card moves below the fold on laptop width.
  • A disabled button looks enabled because the CSS token changed.
  • A table column overlaps after a long customer name arrives from the API.
  • A banner hides the first input field on mobile.
  • A new icon font fails to load in CI but passes locally.

None of these are exotic bugs. They appear in normal sprint work. In Indian product teams, I see this often in release weeks when backend changes, design system changes, and payment flow changes land together. A good Playwright visual testing layer gives the SDET a second signal before production users complain.

What the ecosystem data says

The npm downloads API reported 157,752,437 downloads for @playwright/test during the last-month window ending 2026-06-15, and the npm registry lists the current package version as 1.61.0. GitHub’s repository API also shows the Microsoft Playwright repository as a large active open-source project. I do not use these numbers to claim visual tests are always required. I use them to make a practical point: Playwright is mature enough that visual regression should not be treated as a third-party afterthought.

If you are targeting ₹25-40 LPA SDET roles, this topic matters. Interviewers expect you to go beyond clicking buttons. They want to hear how you control flakiness, review diffs, protect CI time, and decide which screens deserve screenshot coverage.

How Playwright screenshot assertions work

Playwright visual testing uses snapshot files. When you call toHaveScreenshot() for the first time, Playwright creates a golden image. On later runs, it captures a fresh image and compares the two. If the difference is above the configured tolerance, the test fails and Playwright stores actual, expected, and diff artifacts.

The mental model

  1. Pick a stable state of the page.
  2. Freeze or control dynamic data.
  3. Capture a screenshot baseline.
  4. Commit the baseline to source control.
  5. Review every diff like a code change.

This is the part beginners skip. They run screenshots on a live dashboard with changing dates, charts, random names, ads, and animations. Then they blame Playwright when the test fails every morning. The tool is not the problem. The state is the problem.

Baseline location

By default, Playwright stores snapshots near your test files with a name that includes browser and platform information. That is good because a Chromium screenshot on Linux can differ from WebKit on macOS. Fonts, antialiasing, and rendering engines are not identical.

tests/
  visual.spec.ts
  visual.spec.ts-snapshots/
    landing-page-chromium-linux.png
    pricing-card-chromium-linux.png

Do not manually rename these files until you understand your project configuration. A broken snapshot path is a silly reason to waste 30 minutes in CI.

Project setup for stable visual tests

Start with a dedicated visual project in playwright.config.ts. I prefer this over mixing visual tests with every smoke test. Visual checks are valuable, but they are heavier than a locator assertion. They deserve their own timeout, retry policy, and artifact rules.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  expect: {
    timeout: 5_000,
    toHaveScreenshot: {
      maxDiffPixelRatio: 0.01,
      animations: 'disabled',
      caret: 'hide'
    }
  },
  use: {
    baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure'
  },
  projects: [
    {
      name: 'chromium-visual',
      use: {
        ...devices['Desktop Chrome'],
        viewport: { width: 1365, height: 768 },
        deviceScaleFactor: 1
      },
      testMatch: /.*\.visual\.spec\.ts/
    },
    {
      name: 'mobile-visual',
      use: {
        ...devices['Pixel 7'],
      },
      testMatch: /.*\.visual\.spec\.ts/
    }
  ]
});

Why I separate visual tests

A login smoke test should fail fast. A visual checkout review can afford more diagnostics. Mixing both creates confusion because developers do not know whether a red build means the app is unusable or a 12-pixel icon moved. Separate projects make the signal clean.

Install browser dependencies in CI

On Linux CI, install Playwright browsers and dependencies explicitly. This removes a common screenshot mismatch source.

npm ci
npx playwright install --with-deps chromium
npx playwright test --project=chromium-visual

Screenshot description: in VS Code, you should see a tests/visual.spec.ts-snapshots folder beside your visual spec. In the Playwright HTML report, a failed visual assertion shows expected, actual, and diff attachments. That report is the review surface for the team.

Your first visual regression test

Let us test a course landing page. The key is to navigate to a stable route, wait for meaningful content, and assert the screenshot after the page settles.

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

test.describe('course landing visual checks', () => {
  test('landing page hero remains stable', async ({ page }) => {
    await page.goto('/courses/playwright-automation');

    await expect(page.getByRole('heading', { name: /playwright automation/i }))
      .toBeVisible();

    await expect(page).toHaveScreenshot('playwright-course-hero.png', {
      fullPage: false
    });
  });
});

Run it once locally to create the baseline:

npx playwright test tests/course-landing.visual.spec.ts --update-snapshots

Then run it normally:

npx playwright test tests/course-landing.visual.spec.ts

What to commit

Commit the spec and the generated snapshot. If you only commit the spec, CI will create a baseline in an environment nobody reviewed. That defeats the point. Baselines are test data, not temporary files.

Element-level screenshot

Full page screenshots are noisy. I use element screenshots for components that matter: price card, checkout summary, learner dashboard card, certificate preview, and error alert.

test('pricing card stays readable', async ({ page }) => {
  await page.goto('/pricing');

  const proPlan = page.getByTestId('plan-pro');
  await expect(proPlan).toBeVisible();

  await expect(proPlan).toHaveScreenshot('pro-plan-card.png');
});

This is the first rule I teach beginners: screenshot the smallest meaningful area. The smaller the area, the easier it is to review the diff.

Handling dynamic UI without fake confidence

Dynamic UI is the main reason Playwright visual testing gets a bad reputation. Dates, random avatars, charts, ad slots, timers, spinners, caret blinking, and skeleton loaders can break the baseline without a product bug.

Mask changing regions

Use masks for areas you intentionally do not want to compare. A mask is honest when the changing content is irrelevant to the visual contract.

test('dashboard shell stays stable', async ({ page }) => {
  await page.goto('/dashboard');

  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();

  await expect(page).toHaveScreenshot('dashboard-shell.png', {
    mask: [
      page.getByTestId('realtime-clock'),
      page.getByTestId('support-chat-widget'),
      page.getByTestId('random-testimonial')
    ],
    fullPage: true
  });
});

Do not mask the exact area you are trying to protect. If the price number is the risk, do not mask the price number. If the animated support widget is not part of the risk, mask it.

Freeze time and network data

For pages with dates or charts, prefer controlled API responses. Day 8 already introduced API setup and network mocking. Use that skill here.

test('invoice preview layout does not regress', async ({ page }) => {
  await page.route('**/api/invoices/INV-1001', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        id: 'INV-1001',
        customer: 'Aarav Sharma',
        amount: 24999,
        currency: 'INR',
        dueDate: '2026-06-30',
        status: 'PAID'
      })
    });
  });

  await page.goto('/invoices/INV-1001');
  await expect(page.getByText('Aarav Sharma')).toBeVisible();

  await expect(page.getByTestId('invoice-preview'))
    .toHaveScreenshot('paid-invoice-preview.png');
});

This is why visual testing and API mocking belong together. You do not need production data to verify layout. You need realistic, controlled data.

Disable animations

Playwright supports screenshot options that disable animations and hide the caret. Use them globally when the product allows it. CSS animations look nice for users, but they create random frames for screenshots.

Component screenshots vs full page screenshots

There are two healthy levels for Playwright visual testing: page-level smoke and component-level contract. Page-level checks catch broken layout across a user journey. Component-level checks catch changes in a reusable block.

When to use full page screenshots

  • Landing page above the fold after a redesign.
  • Checkout page before payment.
  • Admin dashboard summary after data load.
  • Mobile navigation open state.
  • Error page for 404 and payment failure.

Full page screenshots are useful when the relationship between sections matters. They are poor for pages with infinite feeds, live counters, or ad slots.

When to use component screenshots

Component screenshots are better for design system confidence. A single card, alert, modal, table row, or receipt block has a smaller visual surface. Smaller surface means fewer false failures.

test('payment failure alert visual contract', async ({ page }) => {
  await page.goto('/checkout?paymentStatus=failed');

  const alert = page.getByRole('alert').filter({ hasText: 'Payment failed' });
  await expect(alert).toBeVisible();

  await expect(alert).toHaveScreenshot('payment-failure-alert.png');
});

In a real project, I put component screenshots close to the feature team. The payments team owns payment screenshots. The learning team owns course dashboard screenshots. Ownership reduces the “who approved this diff?” drama.

ARIA snapshots for structure checks

Playwright also documents snapshot testing for the accessibility tree. This is different from pixel visual testing. A screenshot asks, “does it look the same?” An ARIA snapshot asks, “does the user-facing structure stay the same?”

That matters because a button can look correct while its accessible name disappears. A heading can remain visible but change hierarchy. A form can render perfectly but lose labels. Visual tests do not replace accessibility checks.

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

test('checkout form keeps accessible structure', async ({ page }) => {
  await page.goto('/checkout');

  await expect(page.locator('form')).toMatchAriaSnapshot(`
    - form:
      - heading "Complete your purchase"
      - textbox "Email address"
      - textbox "Coupon code"
      - button "Apply coupon"
      - button "Pay securely"
  `);
});

Where ARIA snapshots help

  • Checkout and login forms.
  • Navigation menus.
  • Accessible modals.
  • Course lesson sidebars.
  • Complex tables with headings and actions.

I do not run ARIA snapshots for every page. I use them where semantic structure has business value. If a checkout form loses the correct button name, that is a real issue for keyboard and screen reader users.

CI review workflow for visual diffs

A Playwright visual testing setup is only as good as its review workflow. If every diff gets blindly approved, you have created screenshot theater. If every small diff blocks the team for hours, people will delete the tests.

A simple GitHub Actions workflow

name: visual-regression

on:
  pull_request:
    paths:
      - 'src/**'
      - 'tests/**'
      - 'playwright.config.ts'

jobs:
  visual:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npm run build
      - run: npx playwright test --project=chromium-visual
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-visual-report
          path: |
            playwright-report/
            test-results/

Reviewers should open the HTML report artifact and inspect the diff. A red diff is not automatically bad. It can be an approved design change. The question is whether the change is intentional.

Approval rule

I use this rule with teams: no visual baseline update without a matching product or design explanation in the pull request. That explanation can be one sentence. “Updated pricing card spacing from 16px to 24px based on design ticket QA-431” is enough. “Updated snapshots” is not enough.

Updating snapshots safely

# Run against the same browser and OS target used for baselines
npx playwright test --project=chromium-visual --update-snapshots

# Review changed PNG files before committing
git status
git diff --stat

Do not update snapshots from a random local machine if CI is Linux and your baseline is Linux. Use a container, Codespace, or CI update job if your team keeps hitting font differences.

Common pitfalls in Playwright visual testing

Pitfall 1: Taking screenshots too early

Never take a screenshot immediately after page.goto() unless you know the page is fully ready. Wait for the heading, API result, table row, or stable app signal. Playwright’s auto-waiting helps actions and assertions, but your screenshot moment still needs intent.

Pitfall 2: Comparing live production data

Production data changes. If the screenshot depends on a live offer, live order count, or rotating testimonial, the baseline becomes noisy. Mock the data or use a seeded test environment.

Pitfall 3: Using one giant full-page screenshot

A 12,000-pixel page screenshot can fail because a footer link moved. That may not be worth blocking a release. Break coverage into meaningful zones: hero, pricing card, checkout summary, error alert, and mobile menu.

Pitfall 4: Ignoring fonts

CI machines may not have your product font. If the font is part of the visual contract, install it. If it is not, use a stable fallback in the test environment.

Pitfall 5: Treating visual tests as a replacement for assertions

Visual tests complement locator assertions. Keep explicit checks for text, roles, API responses, and URLs. A screenshot diff can tell you something changed. A semantic assertion tells you exactly what changed.

Pitfall 6: Not linking failures to Trace Viewer

Day 7 covered Playwright Trace Viewer. Use it when a visual check fails in CI. The trace helps you inspect DOM, network, console messages, and the exact screenshot moment. That is faster than guessing from a PNG alone.

Key takeaways and Day 9 homework

Playwright visual testing gives you confidence that the product still looks right after code changes. It works best when you keep the state controlled, the screenshot area small, and the review process strict.

  • Use toHaveScreenshot() for controlled visual baselines.
  • Prefer component screenshots when the full page is noisy.
  • Mask dynamic regions only when they are outside the visual contract.
  • Mock API data for deterministic pages.
  • Review visual diffs like code, not like disposable artifacts.

Your Day 9 homework is simple. Pick one business-critical page from your app and add three checks: one hero screenshot, one component screenshot, and one ARIA snapshot. If you are following the full series, connect this with Day 5 Page Object Model and Day 2 locators and assertions. That combination gives you readable tests, semantic checks, and visual coverage.

FAQ

Is Playwright visual testing enough for complete UI validation?

No. It is one layer. Keep locator assertions, API checks, accessibility checks, and manual exploratory testing for risky changes. Screenshots are strong for layout regressions, but they do not explain every failure by themselves.

Should I run visual tests on every pull request?

Run a small, high-value visual pack on pull requests. Run the larger pack nightly or before release. If your PR visual suite takes 30 minutes, developers will hate it and bypass it.

How many screenshots should a project start with?

Start with 5 to 10 screenshots across critical flows. For example: landing hero, pricing card, login error, checkout summary, payment failure, dashboard card, mobile menu, and certificate preview. Expand only after the review workflow works.

Do visual tests work with design systems?

Yes, but keep ownership clear. Component teams should own component baselines. Product teams should own page-level baselines. This avoids one QA engineer becoming the screenshot approval bottleneck.

What should I learn next after visual testing?

Next, learn parallelism, sharding, retries, and CI scaling. Visual tests are powerful, but they need a disciplined pipeline when the suite grows beyond a few screens.

Sources: Playwright Visual Comparisons documentation, Playwright Snapshot Testing documentation, Playwright Assertions documentation, npm downloads API for @playwright/test, npm registry package metadata, and GitHub API repository metadata for Microsoft Playwright.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.