skip to content

In Playwright, what does the mask option of toHaveScreenshot() accept, and what does it do to the image?

level: middleimportance: should knowfreq 52%

answer

  1. Takes locators, not selector strings
  2. Solid box over the bounding box
  3. Bright pink unless maskColor says otherwise
  4. Applied while capturing, so both images carry it
  5. Layout still shifts if the region resizes

basics

~20 s

It accepts an array of locators. Each matched element is painted over with a solid box covering its bounding box, bright pink by default, changeable with maskColor. The box is applied while capturing, so both the reference and the actual carry it.

solid answer

~40 s

`mask` takes `Locator[]`, not selector strings, so anything you can build with `page.getByRole()` or `page.getByTestId()` can be masked. Every element the locators resolve to is overlaid with a solid rectangle over its bounding box, `#FF00FF` by default and any CSS colour through `maskColor`. The overlay happens at capture time, which is the key mechanical point: the reference image contains the box too, so the comparison is between two masked images rather than a masked image and a clean one. Equally important, masking paints over the element without removing it from layout. If the current balance grows from four digits to seven, the masked box grows with it, everything after it shifts, and the assertion still fails — masking hides the content of a region, never its size or position.

code

typescript · 7 lines
typescript
await expect(page).toHaveScreenshot('statement.png', {
  mask: [
    page.getByTestId('current-balance'),
    page.getByTestId('last-updated'),
  ],
  maskColor: '#000000',
});

go deeper

for a junior

Know that mask takes an array of locators and paints a solid box over each matched element, and that maskColor changes the colour of that box.

for a middle

Explain that the box is applied while capturing, so both images carry it and adding or removing a mask invalidates the stored reference.

for a senior

Show that you know masking hides content but not geometry, so a region that changes size still breaks the comparison and needs different framing rather than another mask.

for a principal

Own the line between hiding a region and stabilising it, since every mask is a piece of the page a visual check no longer protects and that debt is invisible in a green run.

## What the option takes `mask` is an array of **locators**, passed to the matcher alongside the snapshot name: ```typescript await expect(page).toHaveScreenshot('statement.png', { mask: [page.getByTestId('current-balance'), page.getByTestId('last-updated')], }); ``` A locator, not a CSS string and not a rectangle. That has practical consequences: - A locator that resolves to several elements masks all of them, so `page.getByRole('cell')` covers every cell rather than raising a strict-mode error. - A locator that resolves to nothing masks nothing and does not fail the assertion, so a stale test id silently stops masking. - Locators are evaluated at capture time, which means an element that has not rendered yet is not masked. ## What it draws Each matched element is overlaid with a **solid rectangle** covering its bounding box. The default fill is the bright pink `#FF00FF`, chosen to be obvious in a diff, and `maskColor` accepts any CSS colour string when that pink clashes with the design being captured. | Aspect | Behaviour | |---|---| | Shape | The element's bounding box, so a non-rectangular element is over-covered | | Default colour | `#FF00FF`, overridden by `maskColor` | | Applied when | During capture, so both reference and actual contain the box | | Effect on layout | None: the element still occupies its space | ## The two mechanics interviewers probe 1. **The mask is in both images.** Because it is applied while capturing, a reference generated with a mask already has the box baked in. Add a mask to a test whose reference was generated without one and the two images differ everywhere the box lands, so the assertion fails until the reference is regenerated. Remove a mask and the same thing happens in reverse. 2. **The mask hides content, not geometry.** The element keeps its box in the layout. On a statement page, masking the current balance stops the digits from breaking the comparison, but if the balance widget changes width or height the coloured box changes with it, everything below shifts, and the comparison fails on the geometry even though the volatile text was hidden. ## Where the boundary of the option sits Masking is a framing parameter of the capture. It answers the question "which pixels should not participate in this comparison", and it answers it by painting over them before either image exists. - It cannot make a region partially count; a masked box is opaque. - It cannot follow an element that moves; it covers wherever the locator resolves at capture time. - It is not a tolerance mechanism, and it does not interact with how the remaining pixels are compared. ## Choosing a mask colour The default pink is deliberate: on a diff image, a pink rectangle reads instantly as "a human decided to ignore this". Two reasons to override it with `maskColor`: - The page itself uses that pink, so a masked region is indistinguishable from real content. - A masked region sits next to a decorative element and a neutral colour makes the resulting image easier to review, for example `maskColor: '#000000'` over a dark table header. The colour has no effect on the comparison beyond being identical in both images, so any consistent value works. ## A concrete pattern On a bank statement page, three things move between runs: the current balance, the relative timestamp under it, and the transaction ids in the table. All three are locatable, so one masked capture covers the layout, the typography and the chrome of the whole page while ignoring exactly the values that change. The export button, the column headers and the table borders — the parts a visual check exists to protect — stay fully compared.

  • A test adds a mask to an assertion that already had a committed reference image. What happens on the next run?
    It fails. The stored reference has no box, the new capture has one, so every pixel under the mask differs. The reference has to be regenerated once the mask is in place, and the same applies in reverse when a mask is removed.
  • The masked balance region still breaks the comparison from time to time. What is the likely cause?
    The value changed length and the element resized, so the coloured box changed size and the content after it moved. A mask hides pixels inside a bounding box; it does not stabilise the box itself, so the region needs a fixed size or a different framing.

Like taping a card over the address window before photocopying a form: every copy carries the tape, including the one you compare against, and the card does not shrink the window.

saying these in an interview costs you the question

  • Thinks mask accepts css selector strings or coordinates
  • Believes the reference image is stored without the mask box
  • Assumes a masked element is removed from the layout
  • Says masking is a way to loosen the pixel comparison
  • Expects a locator that matches nothing to raise an error