QA How-To
Playwright toHaveScreenshot: Examples and Best Practices
Explore Playwright toHaveScreenshot examples for pages, components, masks, responsive states, CI baselines, debugging, and visual test design in 2026.
22 min read | 2,707 words
TL;DR
The strongest Playwright toHaveScreenshot examples first assert a deterministic business-ready state, then compare the smallest useful page or locator boundary. Give every baseline a stable name, control viewport and dynamic inputs, use masks or `stylePath` narrowly, and review snapshot updates as product changes.
Key Takeaways
- Build each visual assertion around a named business state, not an arbitrary moment in a workflow.
- Use locator screenshots for focused components and a small number of page snapshots for composition risks.
- Control responsive viewport, theme, locale, data, fonts, and time as explicit test inputs.
- Prefer fixed test data, then narrow masks or screenshot-only styles when data cannot be controlled.
- Keep browser-project baselines separate and generate them in the same environment used for comparison.
- Diagnose expected, actual, and diff images before accepting any snapshot update.
These Playwright toHaveScreenshot examples cover the patterns that working SDET teams need: a basic viewport baseline, a focused component, responsive projects, full-page capture, masking, screenshot-only CSS, and controlled comparison thresholds. Every pattern uses current Playwright Test APIs and makes the state under comparison explicit.
The code statement is the easy part. The hard part is deciding what the image means, making its inputs repeatable, and keeping baseline review trustworthy. The examples below are organized as small recipes, followed by the design and maintenance rules that prevent a visual suite from becoming flaky or expensive.
TL;DR
| Scenario | Pattern | Why it works |
|---|---|---|
| Page composition | expect(page).toHaveScreenshot(name) |
Protects relationships across the viewport |
| Isolated component | expect(locator).toHaveScreenshot(name) |
Limits noise and produces a focused diff |
| Long static document | { fullPage: true } |
Captures content below the fold |
| Volatile third-party value | { mask: [locator] } |
Excludes only the irrelevant pixels |
| Dynamic widget behavior | { stylePath: file } |
Applies reviewable CSS during capture |
| Small known raster variance | maxDiffPixels or maxDiffPixelRatio |
Encodes a bounded exception |
| Responsive contract | Separate named projects and baselines | Keeps viewports and expectations explicit |
Use these patterns as templates, but do not paste a snapshot at the end of every functional test. Add a visual assertion when the requirement concerns layout, styling, clipping, overlap, responsive behavior, or another pixel-level risk.
1. Basic Playwright toHaveScreenshot Examples
The first example is intentionally self-contained. It renders fixed HTML and takes a named viewport snapshot after a semantic readiness assertion:
import { test, expect } from '@playwright/test';
test('empty cart has the approved call to action', async ({ page }) => {
await page.setContent(`
<style>
body { font: 16px Arial, sans-serif; background: #f8fafc; }
main { max-width: 640px; margin: 80px auto; text-align: center; }
a { display: inline-block; padding: 12px 18px; color: white; background: #2563eb; }
</style>
<main>
<h1>Your cart is empty</h1>
<p>Add a course to continue learning.</p>
<a href='/catalog'>Browse courses</a>
</main>
`);
await expect(page.getByRole('heading', { name: 'Your cart is empty' }))
.toBeVisible();
await expect(page).toHaveScreenshot('empty-cart.png');
});
A stable name such as empty-cart.png describes the state even if the test title later changes. The first run creates the expected snapshot. Inspect that baseline, confirm it represents the correct page, and commit it with the test. The next run compares the current viewport against it.
The assertion waits for two consecutive identical captures before pixel comparison. That handles brief visual settling, but it does not replace domain synchronization. The heading assertion proves that the page reached the expected branch. In a real application, you might also assert that the cart request completed, the loading skeleton disappeared, or the signed-in customer is correct.
Use explicit names wherever a test has multiple checkpoints. before-submit.png and validation-errors.png are more useful than generated sequential names in reports and code review.
2. Locator Screenshot for a Component Contract
A component snapshot is usually the best default because unrelated page changes do not affect it. This runnable test captures an alert after the user submits invalid input:
import { test, expect } from '@playwright/test';
test('validation summary renders consistently', async ({ page }) => {
await page.setContent(`
<form aria-label='Registration'>
<label>Email <input type='email' name='email'></label>
<button type='submit'>Create account</button>
</form>
<script>
document.querySelector('form').addEventListener('submit', event => {
event.preventDefault();
const alert = document.createElement('div');
alert.setAttribute('role', 'alert');
alert.innerHTML = '<strong>Check your details</strong><p>Email is required.</p>';
alert.style.cssText = 'border:2px solid #b91c1c;padding:16px;color:#7f1d1d;width:320px';
event.currentTarget.prepend(alert);
});
</script>
`);
await page.getByRole('button', { name: 'Create account' }).click();
const summary = page.getByRole('alert');
await expect(summary).toContainText('Email is required.');
await expect(summary).toHaveScreenshot('required-email-alert.png');
});
The locator must resolve to the intended element. Prefer roles, labels, text, and stable test IDs over CSS structure. If the component moves to a sidebar but its own rendering remains correct, this test still passes. Add a separate page snapshot only if its placement relative to other elements is an acceptance criterion.
Component snapshots pair well with semantic checks. The screenshot detects borders, spacing, wrapping, icons, and clipping. toContainText makes the required message obvious when reviewing code and provides a clearer failure if the content is absent. See Playwright locator strategies for durable target design.
3. Responsive Playwright toHaveScreenshot Examples
Responsive visual tests should express viewports as projects, not resize the same page at random points. Separate projects create separate expected images and make reports clear:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
projects: [
{
name: 'desktop-1280',
use: {
...devices['Desktop Chrome'],
viewport: { width: 1280, height: 720 },
},
},
{
name: 'mobile-390',
use: {
...devices['iPhone 13'],
viewport: { width: 390, height: 844 },
},
},
],
});
The test can use the same snapshot name because Playwright's project-aware snapshot path keeps expected images distinct:
import { test, expect } from '@playwright/test';
test('pricing page responds to the configured viewport', async ({ page }) => {
await page.goto('/pricing');
await expect(page.getByRole('heading', { name: 'Choose your plan' }))
.toBeVisible();
await expect(page).toHaveScreenshot('pricing-layout.png');
});
Choose viewports from product breakpoints and supported devices, not an exhaustive collection of nearly identical widths. A desktop, a narrow phone, and perhaps a boundary width may cover the meaningful layout transformations. Verify navigation behavior or card order semantically when those requirements matter.
Do not set viewport: null for baseline comparisons. That delegates dimensions to the host window, which makes output nondeterministic. Keep theme, locale, and device scale consistent too, because responsive code may branch on more than width.
4. Full-Page and Clipped Screenshot Patterns
fullPage: true captures the full scrollable page rather than only the viewport. It fits static documents such as receipts, audit reports, and content pages where below-the-fold layout matters:
await page.goto('/orders/INV-1042/print');
await expect(page.getByText('Invoice INV-1042')).toBeVisible();
await expect(page.getByText('Total $184.00')).toBeVisible();
await expect(page).toHaveScreenshot('invoice-print-view.png', {
fullPage: true,
});
Full-page snapshots need extra care. Lazy-loaded images may not exist until scrolled into view. Sticky headers can be captured in unexpected positions. Infinite feeds have no stable end. For these surfaces, prefer focused sections or provide a dedicated deterministic print route.
The page assertion also accepts clip coordinates:
await expect(page).toHaveScreenshot('seat-map-canvas.png', {
clip: { x: 80, y: 140, width: 720, height: 480 },
});
Clipping can be valid for a canvas or another coordinate-owned surface, but it is coupled to page geometry. A locator screenshot is more resilient when an element can be selected. If a toolbar grows by eight pixels, a coordinate clip may capture the wrong area without conveying why.
Before taking a long-page image, assert the final record count and wait for all required images or fonts. Avoid a general hard delay. Readiness should be based on the document's actual contract, such as a visible footer and no remaining skeletons.
5. Mask Dynamic Values With Narrow Locators
Use mask when a region must remain in the layout but its exact pixels are irrelevant and cannot be controlled. A mask paints a solid rectangle over every selected locator before comparison. This example masks a rotating support avatar and a server-generated reference while preserving the rest of the confirmation panel:
const confirmation = page.getByRole('region', { name: 'Booking confirmation' });
await expect(confirmation).toHaveScreenshot('booking-confirmation.png', {
mask: [
confirmation.getByTestId('support-avatar'),
confirmation.getByTestId('generated-reference'),
],
maskColor: '#475569',
});
Prefer deterministic data whenever possible. A fixed booking reference gives you more coverage than a mask. Freezing the clock keeps a date visible and testable. Masking is a fallback for data that the test does not own, such as a third-party media thumbnail.
Be precise about visibility. Playwright can mask invisible matched elements too. Use a locator such as .avatar:visible if a page contains hidden responsive copies that should not expand a mask unexpectedly. Never mask a parent container merely because one child changes. The mask's rectangular footprint participates in comparison, but all pixels beneath it are excluded, including defects.
Add semantic assertions for excluded information when the values still matter functionally. For example, assert that the booking reference matches the required format before masking its variable characters in the screenshot.
6. Apply Screenshot-Only CSS With stylePath
stylePath is useful when a volatile element cannot be selected cleanly with a mask or when you need to normalize behavior specifically for capture. Create a small reviewed stylesheet:
/* tests/visual.css */
[data-testid='rotating-promo'] {
visibility: hidden !important;
}
[data-testid='map-pulse'] {
animation: none !important;
opacity: 0.35 !important;
}
Pass that file to the assertion:
await page.goto('/store-locator');
await expect(page.getByRole('heading', { name: 'Find a store' })).toBeVisible();
await expect(page).toHaveScreenshot('store-locator.png', {
stylePath: 'tests/visual.css',
});
Playwright applies the stylesheet while taking the screenshot, including inside Shadow DOM and inner frames. That reach can be valuable for embedded widgets, but it also means a broad selector can modify more than expected. Use dedicated test attributes and document the excluded behavior. The path may also be an array when separate focused stylesheets are easier to own.
Do not use screenshot CSS to make the product look correct only under test. A rule that fixes overflow, font size, spacing, or contrast can conceal the defect the snapshot should detect. The acceptable use is normalization of known irrelevant volatility, while production layout remains untouched. Keep semantic checks for the hidden widget's existence or alternative representation when it is user-important.
7. Control Color, Theme, Fonts, and Time
Visual baselines depend on rendering inputs that functional tests sometimes ignore. Configure colorScheme, locale, timezoneId, viewport, and device profile in the project. Install exactly the fonts used by the application in the canonical test image. Wait for document.fonts.ready before capturing text-heavy layouts.
A theme-specific suite can use two named projects:
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'chromium-light',
use: { colorScheme: 'light', viewport: { width: 1280, height: 720 } },
},
{
name: 'chromium-dark',
use: { colorScheme: 'dark', viewport: { width: 1280, height: 720 } },
},
],
});
If the application stores theme selection in local storage rather than using media queries, set that application state before navigation or through a reusable fixture. Do not assume the emulation option overrides custom business logic.
For dates, freeze application time with Playwright's Clock API before loading the page that reads the clock. For random data, seed the generator or provide fixtures. For network content, fulfill a stable response only when the test's purpose is presentation. Keep a separate integration test for the real service path.
This separation is a testing strength: one visual test proves a known model renders correctly, while other tests prove the backend returns a valid model. Trying to prove both through an unstable end-to-end screenshot often produces ambiguous failures.
8. Configure Comparison Policy at the Right Scope
Set stable suite defaults in playwright.config.ts, then use local overrides only for justified surfaces:
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
timeout: 8_000,
toHaveScreenshot: {
animations: 'disabled',
caret: 'hide',
scale: 'css',
maxDiffPixels: 10,
},
},
});
| Option | Meaning | Good use | Warning |
|---|---|---|---|
threshold |
Per-pixel perceived color distance | Small raster color variance | Higher values can ignore color defects |
maxDiffPixels |
Absolute number of differing pixels | Fixed-size component with tiny known noise | Does not scale with image size |
maxDiffPixelRatio |
Differing pixels divided by all pixels | Legitimately variable dimensions | Can allow many pixels on large pages |
scale: 'css' |
One output pixel per CSS pixel | Portable, smaller baselines | Device-pixel defects are not represented |
animations: 'disabled' |
Controls supported browser animations | Stable end-state capture | Does not freeze every dynamic media source |
Use threshold and amount limits deliberately. If a card shifts by four pixels, relaxing color distance is irrelevant. If text edges differ due to environment rasterization, maxDiffPixels might make the test pass, but environment alignment is still the first fix.
Avoid copying a permissive exception into global config. Keep it beside the chart, map, or canvas that requires it, with a comment describing evidence and ownership.
9. CI Baseline and Artifact Workflow
A good CI workflow compares against committed baselines in a versioned environment. Pin the Playwright package through the lockfile, install the matching browser binaries, and use the same container or runner image used to generate expected output. Cache dependencies carefully, but do not reuse stale browser binaries from a different Playwright release.
Run comparisons without update flags:
npx playwright install --with-deps chromium
npx playwright test tests/visual --project=chromium-desktop
Publish the HTML report and test result directory when a job fails. Reviewers need the expected, actual, and diff images, not only a text message saying pixels changed. Retain artifacts long enough for the owning team to investigate, while applying normal privacy rules because screenshots can contain personal data.
Baseline generation should be a separate, explicit action. A developer can run a focused --update-snapshots=changed command in the canonical container, review images, and commit them. CI then verifies that the checked-in contract matches. Do not let the comparison job rewrite baselines or upload generated expected images directly into the main branch.
For multiple browsers, keep distinct baselines. Start with the browser and surfaces that carry the most visual risk, then expand only if the additional engine catches useful differences. Pair this workflow with GitHub Actions for Playwright when you need matrix and artifact patterns.
10. Debugging Playwright toHaveScreenshot Examples
When a snapshot fails, classify the diff before editing code or baselines. The pattern usually points toward the cause:
| Diff pattern | Likely cause | Next check |
|---|---|---|
| Whole page shifted | Viewport, font, scrollbar, or layout change | Compare project settings and font load |
| One block missing | Wrong state, failed request, or late render | Inspect trace and network |
| Text edges sparkle | Font or rasterization mismatch | Reproduce in canonical OS image |
| Date or badge changes | Volatile data or time | Freeze clock or seed response |
| Theme colors invert | colorScheme or stored preference |
Verify context and app state |
| Sticky content repeats | Full-page capture behavior | Use a locator or print surface |
Run one test with one worker and a trace:
npx playwright test tests/visual/pricing.spec.ts \
--project=desktop-1280 --workers=1 --trace=on
Open npx playwright show-report, inspect all three comparison images, and use the trace to verify the page state before capture. If CI alone fails, reproduce inside its image. If the actual image changes across repeated runs, identify the uncontrolled input rather than averaging it away with a tolerance.
The Playwright trace viewer guide is especially useful when the screenshot shows a wrong page state but not the failed request or action that caused it.
11. Best Practices for a Maintainable Visual Suite
Organize tests by owned surface and risk. A design-system team may own component snapshots, while product teams own a few critical page compositions. Name every baseline by business state. Keep image scope small and keep normal assertions for content, accessibility, and interaction. Visual testing complements those checks, it does not replace them.
Use a review checklist for new snapshots:
- Does the test prove the intended state before capture?
- Is the target the smallest boundary that covers the visual risk?
- Are viewport, theme, locale, fonts, time, and data controlled?
- Are masks and styles narrow, documented, and semantically safe?
- Is any tolerance supported by repeat-run evidence?
- Was the baseline generated with the canonical browser and OS image?
- Can a reviewer understand the expected product change from the pull request?
Measure suite value through defects caught and review clarity, not image count. Delete snapshots that duplicate stronger coverage or fail frequently without revealing product risk. When ownership changes, transfer the baseline review responsibility explicitly. A visual failure that nobody understands or owns becomes background noise.
Interview Questions and Answers
Q: Show a minimal Playwright screenshot assertion.
I import expect from @playwright/test, wait for a business-ready state, and call await expect(page).toHaveScreenshot('state.png'). The first run creates the baseline for review. Later runs compare against it and attach expected, actual, and diff images on failure.
Q: How would you test a responsive component visually?
I create named projects for meaningful product viewports and run the same focused locator assertion in each. Project-specific baselines keep mobile and desktop expectations separate. I also assert semantic behavior such as menu accessibility or card order where pixels alone are ambiguous.
Q: What should be masked in a screenshot?
Only pixels whose precise value is irrelevant and cannot reasonably be made deterministic, such as an external rotating avatar. I mask the narrowest locator and keep semantic validation for any important value. I do not mask errors, prices, or containers that own meaningful layout.
Q: When is fullPage: true a bad choice?
It is a poor fit for infinite feeds, lazy content, pages with problematic sticky elements, or workflows where only one component has visual risk. A full-page diff can be large and hard to diagnose. I prefer a deterministic print route or focused locator snapshots for those cases.
Q: How do you choose a screenshot tolerance?
I first eliminate environment and data variation, then repeat the test in the canonical environment and inspect diff patterns. If a small harmless variance remains, I choose the smallest option and scope that covers it. I document why that component needs an exception.
Q: What belongs in a visual-test pull request?
The test, approved baseline, reason for the visual contract, and evidence of any expected baseline change belong together. The reviewer should know the generation environment and be able to compare before and after. Snapshot updates should never be unexplained binary churn.
Q: Why combine visual and semantic assertions?
A screenshot can show that a component looks right but may not explain that a button is disabled, an accessible name is wrong, or a value came from the wrong record. Semantic assertions express those requirements directly. Together they provide clearer failures and stronger coverage.
Common Mistakes
- Adding
toHaveScreenshot()to every functional test without identifying visual risk. - Capturing a loading state because navigation completed but application rendering did not.
- Using one shared baseline across incompatible browsers, viewports, themes, or platforms.
- Resizing the host browser nondeterministically instead of using named projects.
- Masking an entire card when only one third-party image changes.
- Using screenshot-only CSS to fix a real production overflow or contrast problem.
- Raising a global tolerance to accommodate one noisy canvas.
- Taking full-page snapshots of infinite or heavily lazy-loaded screens.
- Updating baselines inside CI verification and treating generated output as approval.
- Reviewing the diff thumbnail without opening expected and actual images at full resolution.
- Ignoring font packages and browser revision when local and CI output differs.
Conclusion
Good Playwright toHaveScreenshot examples are state-driven, focused, and reproducible. Start with a semantic readiness check, compare a named page or locator boundary, and control every rendering input that affects the contract. Use responsive projects, masks, styles, and tolerances as explicit design choices, not emergency fixes.
Choose one high-risk component, implement the locator pattern, run it repeatedly in your canonical environment, and review the first baseline with the component owner. That small disciplined loop is the foundation of a visual suite that engineers trust.
Interview Questions and Answers
Give an example of a focused visual test in Playwright.
I locate a component by role or test ID, assert its expected content, then call `await expect(locator).toHaveScreenshot('business-state.png')`. This isolates the component and yields a small diff. A separate page snapshot covers placement only when that relationship is important.
How do you design visual coverage for responsive layouts?
I select viewports that represent supported breakpoints and configure them as named projects. The same visual test runs under each project with separate baselines. I avoid dozens of near-duplicate widths and add semantic checks for behavior that pixels cannot explain.
How do mask and stylePath differ?
A mask covers selected locator rectangles with a solid color while preserving their footprint. `stylePath` applies CSS during capture and can change visibility or presentation, including inside frames and Shadow DOM. A mask is easier to bound, while styles require careful selector review because they can affect layout broadly.
How do you handle a canvas chart in a screenshot test?
I provide fixed chart data, set a deterministic viewport and device scale, wait for a product-defined rendered state, and capture the chart locator. I also assert its title, legend, or accessible data semantically. Any tolerance is local and based on repeat-run evidence.
What causes cross-platform screenshot failures?
Browser revisions, font availability, operating system rasterization, graphics libraries, viewport, device scale, and locale are common causes. I generate and compare baselines in one versioned environment. Cross-platform equality should not be assumed.
What is your baseline update policy?
I require the owner to explain the intended UI change, run a focused update in the canonical environment, and inspect before and after images. Baselines are committed with the related code. Normal CI compares only and never approves its own output.
Why can a broad screenshot suite become ineffective?
Broad images produce noisy diffs, slow review, and frequent unrelated failures. Teams then update snapshots mechanically and stop treating failures as defects. Focused risk-based snapshots, clear ownership, and semantic assertions preserve signal.
Frequently Asked Questions
What is a basic Playwright toHaveScreenshot example?
After reaching a deterministic state, use `await expect(page).toHaveScreenshot('approved-state.png')`. The first run creates the expected PNG, and subsequent runs compare the current viewport with that reviewed baseline.
Can Playwright compare a screenshot of one element?
Yes. Resolve the component with a locator and call `await expect(locator).toHaveScreenshot('component.png')`. This usually creates a clearer and more maintainable visual contract than capturing the entire page.
How do I take a full-page visual snapshot in Playwright?
Pass `{ fullPage: true }` to the page screenshot assertion. Use it for finite, deterministic documents, and be careful with lazy loading, sticky elements, and infinite content.
How can I ignore a timestamp in a Playwright screenshot?
The best approach is to freeze time or seed a fixed value. If the exact timestamp is irrelevant and cannot be controlled, pass its narrow locator in the `mask` array and keep a semantic assertion for required format or presence.
What does stylePath do in toHaveScreenshot?
It applies a CSS file while Playwright captures the comparison image, including within Shadow DOM and frames. Use it narrowly to neutralize irrelevant volatility, never to conceal a production layout defect.
How should responsive screenshot tests be organized?
Create named Playwright projects for meaningful supported viewports and let each project keep its own baseline. This makes dimensions reproducible and test reports understandable.
How do I debug a Playwright screenshot mismatch?
Compare expected, actual, and diff images, classify the visual pattern, and inspect a trace for page state and network causes. Verify browser revision, OS, fonts, viewport, locale, theme, and data before changing tolerance.
Related Guides
- Playwright drag and drop: Examples and Best Practices
- Playwright mock date and time: Examples and Best Practices
- Playwright popup and new tab handling: Examples and Best Practices
- Playwright tag and grep filters: Examples and Best Practices
- Playwright APIRequestContext: Examples and Best Practices
- Playwright aria snapshot: Examples and Best Practices