How would you frame a Playwright screenshot baseline for a bank statement page that scrolls for thousands of rows?
answer
- Frame decides what can break the check
- Assert on a locator, not the page
- Full page scales with the data
- Clip is coordinates, so layout shifts hurt
- A capture-time stylesheet collapses variable content
basics
~10 sAssert on a locator rather than the page: expect(page.getByTestId('balance-widget')).toHaveScreenshot() scrolls that element into view and captures only its box. Full page and clip are the alternatives when a region cannot be located.
solid answer
~40 sThere are four framings and they are not interchangeable. `expect(locator).toHaveScreenshot()` captures one element, scrolling it into view and cropping to its bounding box — the right default, because the reference then depends only on that component. `expect(page).toHaveScreenshot()` captures the viewport, which couples the baseline to viewport size and scroll position. Adding `fullPage: true` captures the whole scrollable document, so on a statement with thousands of rows the reference becomes enormous and any change anywhere breaks it. `clip: { x, y, width, height }` crops to a fixed rectangle in CSS pixels, useful only when the region has no stable element to locate, since a layout shift moves the content out from under the coordinates. `stylePath` complements all of them by applying a stylesheet during the capture, for example hiding a volatile footer.
code
typescript · 10 lines// Just the widget: scrolled into view, cropped to its box.
await expect(page.getByTestId('balance-widget')).toHaveScreenshot('balance.png');
// The whole scrollable statement, rows included.
await expect(page).toHaveScreenshot('statement-full.png', { fullPage: true });
// A fixed rectangle, when no element can be located.
await expect(page).toHaveScreenshot('chart.png', {
clip: { x: 0, y: 120, width: 640, height: 320 },
});go deeper
Know that you can pass a locator to the screenshot matcher instead of the page, and that it captures only that element after scrolling it into view.
Explain what each framing captures and what invalidates it, including that a full page capture grows with the data the page renders.
Show judgment about scope: the smallest frame that protects the thing you care about, a locator rather than coordinates, and a capture-time stylesheet to collapse variable content.
Own the coverage gap the narrow frames leave. Element-scoped baselines never see the space between components, so someone has to decide where the wider frames live and what they cost.
## The choice is what the baseline depends on Every screenshot reference is a contract, and framing decides how much of the page that contract covers. The narrower the frame, the fewer unrelated changes can break it — and the fewer real regressions it can catch. | Framing | Captures | Breaks when | |---|---|---| | `expect(locator).toHaveScreenshot()` | One element, cropped to its box | That component changes | | `expect(page).toHaveScreenshot()` | The current viewport | Anything above the fold changes, or the viewport does | | `fullPage: true` | The whole scrollable document | Anything on the page changes, at any scroll depth | | `clip: { x, y, width, height }` | A fixed rectangle in css pixels | The content under those coordinates moves | ## Locator-scoped capture, the usual answer `expect(page.getByTestId('balance-widget')).toHaveScreenshot('balance.png')` scrolls the element into view and captures its bounding box. On a statement page this is what you want for the balance widget or the export button: - The reference is small, so a human can actually review it in a diff. - The rest of the page — a thousand rows of transactions, a footer with a relative timestamp — cannot break it. - It is stable against layout changes elsewhere, because the crop follows the element rather than a coordinate. The cost is coverage. A locator-scoped baseline says nothing about how that widget sits in the page, so spacing and alignment regressions outside its box go unseen. ## When full page is the wrong tool `fullPage: true` is tempting because it feels thorough. On a long statement it is usually a mistake: 1. The image is as tall as the document, so review and storage costs scale with the data the page happens to render. 2. Every row is part of the contract, so a change in test data breaks a check that was meant to be about layout. 3. Lazy-loaded content below the fold may or may not have rendered when the capture runs, so the image varies between runs. It earns its place when the whole document really is the subject — a printable statement or a short marketing page — and not otherwise. ## When clip is the right tool `clip` takes `{ x, y, width, height }` in css pixels and crops the capture to that rectangle. It is the fallback for a region with no addressable element: a canvas-rendered chart, a third-party widget in a wrapper you do not control, or a slice of a larger element. Its weakness is that the coordinates are absolute — anything that shifts the layout moves the subject out of the box, and the assertion then fails with a picture of the wrong thing, which is a confusing failure to debug. ## Complementary, not alternative: stylePath `stylePath` points at one or more CSS files applied only while the capture is taken. Where a mask paints over a region and leaves its geometry intact, a stylesheet can change the geometry: ```css /* tests/screenshot.css */ [data-testid="last-updated"] { visibility: hidden; } .transaction-row:nth-child(n + 6) { display: none; } ``` ```typescript await expect(page).toHaveScreenshot('statement.png', { stylePath: './tests/screenshot.css', }); ``` That second rule is the trick that makes a long page screenshottable at all: render the first few rows, hide the rest, and the baseline stops depending on how much data the fixture happened to load. The stylesheet is applied identically when generating the reference and when comparing, so both images agree. ## How to decide - Start at the smallest frame that contains the thing you are protecting, and widen only for a specific reason. - Prefer a locator to coordinates whenever an element exists, because a locator survives layout changes and coordinates do not. - Reach for `stylePath` before widening the frame, since collapsing variable content is cheaper than accepting a baseline that changes with the data. - Remember that changing the framing of an existing assertion invalidates its reference: the next run finds a different-sized image and fails until the file is regenerated.
- Why is a clip rectangle a fragile way to frame a screenshot baseline?The coordinates are absolute. Anything that shifts the layout above the rectangle — a wrapped heading, an extra banner — moves the subject out from under it, and the assertion then compares a picture of something else, which reads as a visual regression rather than a framing problem.
- Your locator-scoped baselines are all green, yet the statement page ships visibly misaligned. How is that possible?An element-scoped reference only covers the element's own box. Spacing, alignment and stacking between components live outside every box, so nothing in the suite is comparing them. Covering that needs at least one wider frame, deliberately chosen and kept small.
saying these in an interview costs you the question
- Reaches for full page as the default framing
- Uses clip coordinates when a locator exists
- Thinks changing the framing keeps the existing reference valid
- Assumes a locator capture also covers surrounding layout
- Never considers hiding variable rows before capturing