Blog
Updated 5 min read

Playwright toHaveScreenshot or archive-replay: which to pick

Built-in toHaveScreenshot with committed PNGs, or an archive of your Playwright tests rendered and diffed off your repo: what each costs, and which fits.

Igor LuchenkovIgor LuchenkovBuilding UI Verify
PlaywrightVisual testingtoHaveScreenshotDeterminismCoding agents
Three screenshots of the same checkout page: the committed expected PNG, the actual render with the Pay button lower and no card border, and Playwright's diff image marking 17,887 changed pixels.
On this page

If you already drive real pages with Playwright, you are one decision away from visual testing them: catch the UI changes your assertions never look at, like a shifted button, a broken token, or a restyled card, on every pull request. The decision is which of two approaches to use, and it comes down to how big the surface is and where you want the review to happen.

What a Playwright visual test catches

A Playwright visual test screenshots the pages your Playwright tests render and diffs each image against a saved baseline, so a change in the rendered pixels fails the check even when every functional assertion still passes. A normal Playwright test asserts on the DOM: a heading exists, a button is enabled, a URL changed. None of that notices when the same button renders twenty pixels too low or a card loses its border. A visual test closes that gap by comparing the picture, not the markup, so the first run records a baseline and every run after it flags what moved for a human, or a coding agent, to judge.

Our demo checkout page after a two-line CSS change. The functional test still finds an enabled "Pay $49.00" button and passes. Only the screenshot notices the missing border and the button 20 px lower.

The reason Playwright is a natural place to do this is that your tests already navigate to the states worth checking. The cart page, the settings modal, the empty state you set up three lines of fixtures to reach: each is a page in a known condition, which is exactly what a visual baseline needs. You are reusing the setup you already wrote rather than authoring capture targets from scratch.

Two ways to do Playwright visual testing

There are two approaches, and they differ mainly in where the work lives: in your repo and CI, or off them. Playwright ships its own visual assertion, toHaveScreenshot, which renders and diffs inside your suite against a PNG you commit. The alternative records an archive of the real pages your tests drive and hands the render, diff, and review to a service. Both compare pixels the same way; what changes is who stores the baselines and where the diff work happens.

Built-in toHaveScreenshot

Playwright's toHaveScreenshot screenshots the page and diffs it against a baseline PNG committed next to the test. Locally it does a lot right: animations are off by default, mask hides the regions you cannot pin, threshold and maxDiffPixelRatio set your noise budget, and the HTML report stacks the expected, actual, and diff images for you. Two properties shape how it behaves once a team leans on it. The baselines are binary PNGs that live in your repo, keyed to the operating system that produced them (-chromium-darwin on a Mac, -chromium-linux on a CI box), so teams pin a Docker image to keep the two in sync. And it re-renders and re-diffs every test on every run inside your CI. The friction shows up when a check goes red in CI and the images are zipped in a build artifact you have to download to see, which is the whole subject of Stop downloading Playwright screenshot artifacts.

visual.spec.ts
import { test, expect } from "@playwright/test";

test("home page", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveScreenshot(); // diffs a committed baseline PNG
});
The first run of that test on a Mac fails, writes the baseline, and names it for the OS that drew it. Commit it and the PNG lives in git next to the spec.

Archive-replay with @uiverify/playwright

The archive-replay approach keeps your tests as the capture targets but moves the render, the baselines, and the review off your repo. You swap one import so every test records an archive of what its page actually rendered, then upload that archive. UI Verify replays it in a single fixed environment and diffs it there, so the pixels match no matter which machine ran the test, nothing binary lands in your repo, and the baseline is resolved from git, one per branch. On top of the diff it runs an AI judge that classifies a change as an intended change or a regression and exposes the cropped before-and-after over MCP, so your coding agent can review the pixels it changed without leaving the pull request. The full step-by-step is in the Playwright setup guide, and Playwright visual regression testing compares it with Chromatic and Argos too.

visual.spec.ts
import { test, expect } from "@uiverify/playwright";

test("home page", async ({ page }) => {
  await page.goto("/");
  // final state is auto-archived; UI Verify replays and diffs it
});
Our own dashboard's Playwright specs on one pull request. All nine pass as functional tests; the upload then renders the archived pages in one environment and reports the four that changed. No PNG was committed to get here.

How the two approaches compare

Built-in toHaveScreenshotArchive-replay (@uiverify/playwright)
BaselinesBinary PNGs committed in your repo, OS-keyedOff-repo, one per branch, resolved from git
Where pixels renderWherever the test runs; pin a Docker image to match CIOne fixed cloud environment, same pixels everywhere
Where you see the diffA downloaded report artifactA check and the diff on the pull request
What rendersEvery test, every run, in your CIEvery captured page, every run, off your CI
Updating a baselineRegenerate the PNG on the right OS and commit itAccept in the UI; a merge advances it
Review helpNone built inAI judge plus agent review over MCP
The two Playwright visual testing approaches at a glance.

Which approach fits your team?

Reach for built-in toHaveScreenshot when the suite is small, everyone runs the same OS or you already pin a Docker image for CI, and a committed PNG per page is a cost you are happy to carry. It has no service to sign up for and no data leaving your machine, which is the right trade for a handful of critical pages or a library where the whole team shares one render environment. The archive-replay path pays off as the surface grows: when PNGs in git start bloating history and conflicting, when Mac-versus-Linux pixel drift makes baselines a chore, when re-diffing the whole suite on every run gets slow, or when you want the diff and a verdict on the pull request instead of a downloaded artifact. Team size and how much the review loop costs you decide it more than any single feature.

Both approaches depend on determinism

Neither path saves you from the one thing that makes visual tests hard: a screenshot is a pure function of its inputs, so any input you leave moving between runs turns into a diff that fires without a real change. The clock, live API data, a feature flag, an animation caught on a different frame, a font that swaps in late: each one flakes a visual test whether you diff with toHaveScreenshot or with an archive. You freeze those in the test either way. Fix flaky Playwright screenshot tests walks the reframe, and the determinism checklist is the symptom-by-symptom list of levers.

The choice of tool is the second decision. The first is pinning your inputs. A page that renders the same pixels twice in a row is testable by either approach; a page that does not will flake under both, so start there before you pick.

Playwright visual tests, minus the PNG upkeep

UI Verify runs Playwright's kind of screenshot comparison over an archive your tests already capture, then puts the diff and an AI verdict on the pull request. Baselines stay off your repo and off your CI.

Start for free

No credit card required.

ShareXLinkedIn
Related skill

Deterministic Playwright captures

Stop real-page diffs that flake without a real change.