Stability Controls
When to Use
Use this when screenshots produce inconsistent results across runs due to animations, fonts, dynamic content, or scroll state.
Pattern
Animations and caret (defaults — verify they are on)
expect: {
toHaveScreenshot: { animations: 'disabled', caret: 'hide' },
}
Wait for fonts
await page.evaluate(() => document.fonts.ready);
Standard CSS Font Loading API; not Playwright-specific. Without it, the first screenshot captures the fallback font; the second captures the web font — baselines drift.
Wait for network idle
await page.goto('/', { waitUntil: 'networkidle' });
// or after load:
await page.waitForLoadState('networkidle');
Caveat: hangs on long-poll/SSE connections. Use 'domcontentloaded' + explicit waitForResponse in those cases.
Mask volatile regions
await expect(page).toHaveScreenshot({
mask: [
page.locator('[data-testid="last-updated"]'),
page.locator('.user-avatar'),
page.locator('iframe'),
],
});
Masks paint solid #FF00FF over the bounding box (configurable with maskColor). Content inside cannot cause diffs.
stylePath — global stabilization CSS
tests/playwright/screenshot.css:
*, *::before, *::after {
animation: none !important;
transition: none !important;
caret-color: transparent !important;
}
iframe { visibility: hidden; }
.has-dynamic-counter { visibility: hidden; }
[data-vrt-mask] { visibility: hidden; }
expect: { toHaveScreenshot: { stylePath: './screenshot.css' } }
Pierces Shadow DOM and inner frames — works for SDC components and Olivero embedded content.
Scroll position normalization
await page.evaluate(() => window.scrollTo(0, 0));
// For element shots:
await locator.scrollIntoViewIfNeeded();
Lazy images / video posters
await page.evaluate(() =>
document.querySelectorAll('img[loading="lazy"]')
.forEach(img => (img.loading = 'eager'))
);
await page.waitForLoadState('networkidle');
Common Mistakes
- Wrong: Skipping
document.fonts.ready→ Right: first screenshot captures fallback font; baselines drift on every subsequent run - Wrong:
networkidleon long-poll/SSE pages → Right: hangs the test; use explicit waits - Wrong: Masks using CSS classes that are dynamically applied → Right: use stable attributes (
data-vrt-mask) so masking is consistent