Stop dynamic content from breaking your visual tests
Timestamps, ads, A/B tests, and live data create meaningless screenshot diffs. Here's how to handle them in Playwright.
What counts as dynamic content
Dynamic content is anything that changes between test runs without corresponding code changes. It's the primary source of false positives in visual testing.
Timestamps and dates
"5 minutes ago", "Today at 3:42 PM", copyright years. These change on every run and create meaningless diffs.
Ads and third-party embeds
Ad networks serve different content constantly. Embedded widgets from external services change without your control.
A/B tests and feature flags
If your visual tests hit different experiment variants, screenshots will never match.
Carousels and rotating content
Auto-advancing slides, random hero images, and content rotators produce different screenshots each run.
Live data from APIs
Stock prices, weather, user counts, notifications. Anything fetched from a live backend will vary.
User-specific content
Profile pictures, names, personalized recommendations. Tests using real accounts see real variation.
If you're seeing visual test failures with no apparent cause, start by auditing your pages for these content types.
Strategy 1: Mock API routes
The most powerful technique. Intercept network requests and return controlled responses. Your UI renders exactly what you specify.
// Intercept API calls and return fixed data
await page.route('**/api/users', async (route) => {
await route.fulfill({
json: {
name: 'Test User',
avatar: '/test-avatar.png',
joinedAt: '2024-01-15T00:00:00Z',
},
});
});
// Intercept all external resources
await page.route('**/*', (route) => {
if (route.request().url().includes('analytics')) {
return route.abort();
}
return route.continue();
});Mock at the network level rather than in your application code. This ensures tests reflect real rendering paths.
Strategy 2: Disable animations
Animations captured mid-transition produce inconsistent screenshots. Disable them entirely in test mode.
// Inject CSS to disable all animations
await page.addStyleTag({
content: `
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
`,
});
// Or use Playwright's reduced motion emulation
await page.emulateMedia({ reducedMotion: 'reduce' });The CSS injection approach is more aggressive but catches JavaScript animations too.
Strategy 3: Freeze time
Playwright's clock API lets you control the system time. All date-based rendering becomes deterministic.
// Set a fixed time before navigation
await page.clock.setFixedTime(new Date('2024-06-15T10:00:00Z'));
// Navigate and take screenshot - dates will be consistent
await page.goto('/dashboard');
await expect(page).toHaveScreenshot();Set the time before navigation to ensure all components render with the frozen timestamp.
Strategy 4: Mask dynamic elements
When you can't control content, hide it from comparison. Playwright's mask option replaces elements with solid color blocks.
// Mask specific elements during screenshot
await expect(page).toHaveScreenshot({
mask: [
page.locator('.ad-banner'),
page.locator('[data-testid="live-clock"]'),
page.locator('.user-avatar'),
],
});
// Mask with a specific color
await expect(page).toHaveScreenshot({
mask: [page.locator('.dynamic-content')],
maskColor: '#ff00ff', // Magenta makes masks obvious
});Use masking sparingly. Over-masking defeats the purpose of visual testing. Prefer mocking when possible.
Strategy 5: Wait for layout stability
Screenshots captured before the page stabilizes will differ. Wait for fonts, images, and network requests.
// Wait for network to settle
await page.goto('/page', { waitUntil: 'networkidle' });
// Wait for fonts to load
await page.evaluate(() => document.fonts.ready);
// Wait for specific content to appear
await page.waitForSelector('[data-loaded="true"]');
// Then capture
await expect(page).toHaveScreenshot();Combine waits as needed. Different pages may need different stability conditions. For more on this, see reducing visual testing flakiness.
Combining strategies
Real-world pages need multiple techniques. Here's a complete example combining several approaches:
test('dashboard visual test', async ({ page }) => {
// Freeze time
await page.clock.setFixedTime(new Date('2024-06-15T10:00:00Z'));
// Mock API data
await page.route('**/api/**', async (route) => {
const url = route.request().url();
if (url.includes('/user')) {
return route.fulfill({ json: mockUser });
}
return route.continue();
});
// Block analytics and ads
await page.route('**/*', (route) => {
const url = route.request().url();
if (url.match(/analytics|ads|tracking/)) {
return route.abort();
}
return route.continue();
});
// Navigate and wait for stability
await page.goto('/dashboard', { waitUntil: 'networkidle' });
// Disable animations
await page.addStyleTag({
content: '*, *::before, *::after { animation: none !important; transition: none !important; }',
});
// Wait for fonts
await page.evaluate(() => document.fonts.ready);
// Screenshot with remaining dynamic elements masked
await expect(page).toHaveScreenshot({
mask: [page.locator('.third-party-widget')],
});
});When mocking isn't enough
Some content genuinely can't be controlled: third-party widgets with unpredictable loading, iframes from external domains, or content from live services you can't mock.
For these cases, masking is appropriate. But if you're masking large portions of your page, reconsider what you're testing. Visual tests work best when most of the page is deterministic.
For a complete approach to CI stability, see visual testing in CI pipelines and Playwright visual testing best practices.
Before trusting screenshots in CI
- Identify all sources of dynamic content in your test pages
- Mock API responses for deterministic data
- Freeze time using Playwright's clock API
- Disable CSS animations and transitions
- Mask third-party embeds and ads that can't be controlled
- Wait for network idle and font loading before capture
- Use consistent test accounts with predictable data
- Run tests in a containerized environment for consistency
Related guides
Want less noise in your visual tests? Join the waitlist
Get early access