skip to content

Which capture defaults does Playwright's toHaveScreenshot() apply that page.screenshot() does not?

level: middleimportance: should knowfreq 45%

answer

  1. Comparison needs a pinned page
  2. Animations default differs from a plain screenshot
  3. Finite forward, infinite cancelled
  4. One image pixel per css pixel
  5. The matcher re-captures until it matches

basics

~20 s

The matcher defaults to animations disabled and scale css, where a plain page screenshot allows animations and captures at device resolution. Both hide the text caret. The matcher also re-captures until the image matches or the assertion times out.

solid answer

~40 s

`toHaveScreenshot()` exists to produce a comparable image, so its defaults differ from `page.screenshot()` on two options. `animations` defaults to `'disabled'` rather than `'allow'`: finite CSS animations, transitions and Web Animations are fast-forwarded to their end state and still fire their end events, while infinite ones are cancelled to their initial frame and resumed after the capture. `scale` defaults to `'css'` rather than `'device'`, so one image pixel is one CSS pixel and the file does not double in size on a high-density display. `caret` defaults to `'hide'` in both. On top of the options, the matcher is a retrying assertion: it keeps re-capturing until the image matches the reference or the assertion timeout expires, so a page still settling does not fail on the first frame.

code

typescript · 5 lines
typescript
await expect(page.getByTestId('balance-widget')).toHaveScreenshot('balance.png', {
  animations: 'disabled',
  caret: 'hide',
  scale: 'css',
});

go deeper

for a junior

Know that the matcher already disables animations and hides the caret for you, so a baseline does not need a manual wait for a transition to end.

for a middle

Explain the mechanics: finite animations are fast-forwarded to their end state, infinite ones are cancelled to the first frame, and the css scale keeps one image pixel per css pixel.

for a senior

Demonstrate that you know the matcher retries the capture until it matches or times out, and can read a slow screenshot failure as an unstable region rather than a wrong reference.

for a principal

Set the defaults once at the config level so every suite captures comparably, rather than leaving each test to rediscover which options make an image reproducible.

## Two different jobs, two sets of defaults `page.screenshot()` produces an image for a human to look at. `expect(page).toHaveScreenshot()` produces an image for a machine to compare byte by byte with a stored reference. The second job needs the page pinned, so the matcher ships with stricter defaults. | Option | `toHaveScreenshot()` | `page.screenshot()` | |---|---|---| | `animations` | `'disabled'` | `'allow'` | | `scale` | `'css'` | `'device'` | | `caret` | `'hide'` | `'hide'` | The caret row is the one candidates get wrong in the other direction: hiding the blinking text cursor is already the default for both, so it is not a difference, just a shared piece of determinism. ## What animations disabled actually does It is not a blanket freeze. The treatment depends on the animation: - **Finite** CSS animations, CSS transitions and Web Animations are **fast-forwarded to completion**, so the page shows the end state and the corresponding end events still fire. - **Infinite** animations are **cancelled back to their initial state**, and played again after the capture is taken. That distinction matters for a statement page whose balance widget slides in on load and whose refresh icon spins forever: the slide lands in its final position rather than mid-flight, and the spinner is captured at frame zero rather than wherever it happened to be. ## What scale css buys On a display with a device pixel ratio of two, `'device'` produces an image twice as wide and twice as tall as the CSS layout. 1. `'css'` keeps one image pixel per CSS pixel, so the file is smaller and its dimensions follow the viewport rather than the hardware. 2. That makes the reference dimensionally identical across machines with different pixel densities, which is the whole point of a stored baseline. 3. `'device'` is the better choice when you deliberately want to inspect rendering at full hardware resolution, which is a debugging goal rather than a comparison goal. ## The retry behaviour that is not an option at all `toHaveScreenshot()` is a web-first assertion. It does not take one capture and judge it; it re-captures until the image matches the reference or the assertion timeout expires. Two things follow: - A page that is still settling — a late web font, a chart drawing itself — usually resolves on its own without any explicit wait in the test. - A page that never settles burns the whole assertion timeout before reporting, so a permanently animating region shows up as a slow failure rather than an instant one. ## Options you can still pass explicitly The defaults are a starting point, not a policy. The same call accepts, among others: - `animations: 'allow'` when the whole point of the baseline is a mid-animation state you have otherwise pinned. - `caret: 'initial'` when the caret is genuinely part of what you are asserting, such as a focused amount field. - `omitBackground: true` to drop the default white backdrop and capture transparency. - `stylePath` to apply a stylesheet only while capturing, which is how a volatile region is neutralised through CSS rather than through the test body. ## The habit to take away Write the defaults out explicitly the first time you set a suite up, read what each one does, then delete the ones that match the default. It costs one pass and it stops the two classic surprises: a baseline captured mid-animation because the code borrowed options from a plain screenshot call, and a reference that doubles in size the day someone runs the suite on a high-density laptop.

  • A card on the statement page animates in over 300 ms. What does the default animations setting capture?
    Its end state. A finite animation is fast-forwarded to completion before the capture, and its end events still fire, so the baseline shows the settled card rather than a frame partway through the transition.
  • Why can a screenshot assertion take several seconds to fail on a page with a permanent spinner?
    The matcher retries the capture until it matches the reference or the assertion timeout expires. An infinite animation is cancelled to its initial frame, but any genuinely unstable region keeps producing a different image, so the assertion spends its whole budget before reporting.

saying these in an interview costs you the question

  • Thinks the matcher takes exactly one capture and judges it
  • Assumes the screenshot options are identical to a plain page screenshot
  • Says disabling animations freezes every animation at the current frame
  • Believes the caret setting differs between the two calls
  • Ignores pixel density and wonders why an image doubled in size