PRACTICAL GUIDE / playwright screenshot different in CI

Debug Playwright Visual Snapshot Diffs That Appear Only in CI

Isolate Playwright CI screenshot differences across render environments, fonts, motion, data, and baselines without approving real visual regressions.

By The Testing AcademyUpdated July 11, 20268 min read
All field guides
In this guide9 sections
  1. Preserve the three images and the render contract
  2. Reproduce inside the CI renderer
  3. Follow the font and text path
  4. Follow the geometry and viewport path
  5. Follow the motion, time, and caret path
  6. Follow the application-state path
  7. Review thresholds as risk boundaries
  8. Make baseline updates a controlled decision
  9. Close the CI loop with retained evidence

What you will learn

  • Preserve the three images and the render contract
  • Reproduce inside the CI renderer
  • Follow the font and text path
  • Follow the geometry and viewport path

A screenshot that fails only in CI is an environment comparison until proven otherwise. Do not update the baseline or widen tolerance from the red pipeline. First reproduce the CI pixels, classify their shape, and identify which render input differs from the baseline contract.

The highest-value clue is the diff pattern. Uniform text halos suggest rasterization or fonts. A shifted page suggests viewport, scrollbar, or late layout. One dynamic region suggests data or time. A completely different state means the test captured the wrong application phase, not a pixel-comparison problem.

Preserve the three images and the render contract

Collect the expected, actual, and diff images from the same failed assertion. Keep their directory structure intact so the test and project remain identifiable. Add the trace and a small metadata attachment containing the browser name, viewport, locale, timezone, color scheme, platform, and relevant application build identity.

The official visual comparisons guide warns that rendering can vary with host operating system, settings, hardware, power source, and headless mode. It recommends comparing in the same environment where baselines were generated. That is the first boundary to verify, not an afterthought.

Animated field map

CI Visual Diff Root-Cause Flow

Retain the failed pixels, compare render inputs, control volatile sources, decide the baseline deliberately, and rerun reproducibly.

  1. 01 / ci pixel diff

    CI-only pixel diff

    Preserve expected, actual, diff, trace, and test identity.

  2. 02 / environment compare

    Environment comparison

    Compare OS image, browser, viewport, scale, locale, and data.

  3. 03 / volatile controls

    Font and motion controls

    Stabilize required assets, animations, time, and readiness.

  4. 04 / baseline decision

    Baseline decision

    Reject regression or review a candidate from the approved renderer.

  5. 05 / reproducible run

    Reproducible visual run

    Verify locally and in CI with identical render inputs.

Reproduce inside the CI renderer

Use the same container or machine image, dependency lockfile, Playwright browser installation, command, project, and environment variables as CI. Running headless on a different local operating system is not equivalent. If baselines are platform-specific, confirm the failing job selected the expected snapshot suffix and path.

Do not regenerate snapshots during this step. Mount or check out the existing references read-only, run the failing test, and compare artifact hashes and dimensions. A clean reproduction converts an intermittent remote symptom into a controlled debugging loop.

Record render inputs in a small helper. Avoid claiming that metadata alone explains pixels; it makes hidden differences reviewable.

TypeScript
import os from 'node:os';
import { test as base } from '@playwright/test';

export const test = base.extend<{ renderEvidence: void }>({
  renderEvidence: [async ({ page, browserName }, use, testInfo) => {
    await use();

    const evidence = {
      browserName,
      platform: os.platform(),
      release: os.release(),
      project: testInfo.project.name,
      viewport: page.viewportSize(),
      devicePixelRatio: await page.evaluate(() => window.devicePixelRatio),
      locale: await page.evaluate(() => navigator.language),
      fontsStatus: await page.evaluate(() => document.fonts.status),
    };

    await testInfo.attach('render-evidence.json', {
      body: Buffer.from(JSON.stringify(evidence, null, 2)),
      contentType: 'application/json',
    });
  }, { auto: true }],
});

If platform release details are sensitive in your environment, omit or normalize them. The important point is to capture enough inputs to compare jobs without leaking host inventory unnecessarily.

Follow the font and text path

Text-only diff noise deserves a font investigation before tolerance changes. Confirm the intended font files exist in the image, load without network errors, and are licensed for that distribution. A font-family declaration does not prove that the desired face rendered; the browser may use a fallback with different metrics.

Assert a product readiness signal that is set after critical assets load. During diagnosis, query document.fonts.check() for the exact family and style used by the target. Keep the screenshot focused on a stable component so unrelated text does not multiply a small rasterization difference.

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

test('account summary matches the approved layout', async ({ page }) => {
  await page.goto('/account/visual-fixture');
  await expect(page.getByTestId('account-summary')).toHaveAttribute(
    'data-render-state',
    'ready',
  );

  await expect.poll(() =>
    page.evaluate(() => document.fonts.check('16px "Inter"')),
  ).toBe(true);

  await expect(page.getByTestId('account-summary')).toHaveScreenshot(
    'account-summary.png',
  );
});

Replace the illustrative family with the application's actual required face. If typography is a visual requirement, substituting a test-only font removes coverage. If exact glyph rasterization cannot be aligned across platforms, maintain platform-specific baselines or run visual comparison on one canonical renderer rather than hiding large text regions.

Follow the geometry and viewport path

Large shifted blocks usually come from dimensions rather than colors. Compare viewport, device scale factor, mobile emulation, scrollbar behavior, browser project, and full-page versus element capture. A missing image or late web font can also move everything below it.

Set dimensions in the Playwright project instead of relying on a CI window default. Assert images and layout prerequisites before capture. For responsive pages, each baseline should name the intended project or viewport contract; one image should not float between desktop and mobile layouts.

Check whether a vertical scrollbar appears only in one environment because content wraps differently. Masking the edge will not fix the shifted content. Narrow the font or data cause, or make the target an element screenshot when page-level overflow is outside the claim.

Follow the motion, time, and caret path

Playwright screenshot assertions handle animations and carets according to their options, but application state can still vary through JavaScript timers, canvas drawing, rotating content, videos, and asynchronous transitions. Control the source that matters instead of sleeping until the frame looks convenient.

Use deterministic fixture data, a fixed application clock where time is part of rendering, and explicit readiness markers. A narrow mask is acceptable for a region outside the visual contract, but every mask creates an unreviewed rectangle. Pair it with another assertion for behavior hidden by that mask.

If motion itself is the feature, a static screenshot may be the wrong assertion. Test the state before and after the transition and use a focused visual reference only at a defined endpoint.

Follow the application-state path

A diff can faithfully report that CI rendered an error banner, empty state, experiment variant, or unauthenticated page. Inspect the screenshot as a product state before studying pixels. Trace network and console evidence often identifies a failed seed call, missing environment variable, expired authentication state, or feature flag mismatch.

Assert semantic prerequisites immediately before the image. The test should fail with "invoice fixture not loaded" rather than compare an empty panel against a populated reference. This also prevents a visually similar loading skeleton from becoming a candidate baseline.

Keep data values deterministic without making the component unrealistic. Names, dates, image URLs, and list lengths should be explicit fixture inputs. Random factories are useful elsewhere but make poor visual references unless seeded and stable across processes.

Review thresholds as risk boundaries

threshold, maxDiffPixels, and maxDiffPixelRatio solve different problems. Change only the dimension supported by measured evidence. A broad per-pixel threshold can hide subtle color regressions; a large allowed pixel count can hide a compact but important missing icon.

Begin from the strictest reproducible comparison. If the canonical renderer still produces a small known variation, document the observed shape and choose a narrow limit. Scope the screenshot to the component under test so the tolerance cannot be spent across unrelated page regions.

Never combine a new baseline, larger threshold, and new mask in one unexamined repair. Reviewers would be unable to tell which change made the failure disappear or how much coverage was lost.

Make baseline updates a controlled decision

Generate a candidate only in the approved render environment and only after the product change is understood. Review expected, actual, and diff images beside the source change. If the difference is a defect, fix the product or test state and keep the baseline. If it is intentional, approve the new reference as code.

Do not let a failed CI job automatically overwrite repository snapshots. That creates a path where an authentication error, missing font, or real regression becomes the accepted image. Keep candidate generation separate from comparison and require a human-readable reason for baseline changes.

Close the CI loop with retained evidence

Artifact upload should run after failed tests unless the job was cancelled. Preserve the snapshot directories, HTML report or trace, and render metadata under a name that includes project and shard identity. Confirm that the baseline files were present in the test job; an artifact from a later merge step cannot explain an earlier missing reference.

Before accepting a fix, verify:

  • Expected, actual, and diff images belong to the same test and project.
  • The baseline and actual use the same canonical OS and browser inputs.
  • Required fonts and images load before the named readiness state.
  • Viewport, device scale, locale, timezone, and feature flags are explicit.
  • Dynamic data, time, animation, and masks have documented ownership.
  • Any tolerance change matches measured noise and preserves defect sensitivity.

Rerun the candidate in the canonical local or container environment, then rerun the unchanged CI command. Close the incident only when both produce the same decision. A green visual test should mean the reviewed pixels are reproducible, not that the comparison has been loosened until CI stops objecting.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 11, 2026 / Reviewed July 11, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Playwright documentation

    Microsoft

    Canonical API, locator, fixture, browser, and test-runner behavior.

  2. 02
    Playwright best practices

    Microsoft

    Official guidance for resilient tests, isolation, and user-facing locators.

  3. 03
    WebDriver standard

    W3C

    The browser automation protocol specification used by major automation stacks.

FAQ / QUICK ANSWERS

Questions testers ask

Why does a Playwright screenshot match locally but differ in CI?

The baseline and actual image may come from different operating systems, browser builds, fonts, device scale factors, locale settings, or application data. Reproduce the CI render environment before changing tolerance.

Should I update visual snapshots after every CI-only difference?

No. Review expected, actual, and diff artifacts, classify the changed pixels, and reproduce them in the approved baseline environment. Updating first can bless a real regression or missing asset.

How can missing fonts be detected before a screenshot assertion?

Package the required fonts in the CI image, assert application readiness, and inspect document.fonts status or specific font loading during diagnosis. Do not rely on a fixed delay to make fallback text disappear.

Can a larger Playwright screenshot threshold solve font rendering noise?

It can suppress measured low-level variation, but it also broadens the blind spot across the image. First align the render environment and scope the snapshot; change one tolerance only with reviewed evidence.

What visual artifacts should CI retain on failure?

Keep expected, actual, and diff images together with the trace, project name, render metadata, and source revision. Reviewers need both the pixels and the environment that produced them.