How to fix Playwright screenshot tests that fail because of timestamps
Make Playwright screenshot tests deterministic by freezing the page clock, controlling timers, or excluding timestamps that are not part of the visual assertion.
A timestamp that changes between test runs can make a Playwright screenshot differ even when the rest of the page is correct. If the displayed time is part of the behavior under test, freeze it before navigation with page.clock.setFixedTime(). If it is irrelevant to the assertion, mask or hide that region instead.
import { test, expect } from '@playwright/test';
test('renders a stable timestamp', async ({ page }) => {
await page.clock.setFixedTime(new Date('2024-02-02T10:00:00'));
await page.goto('http://localhost:3000');
await expect(page).toHaveScreenshot();
});
Choose the remedy based on what the test is meant to prove: stable displayed date, controlled timer behavior, or a screenshot that deliberately ignores a volatile region. See the Playwright Clock guide and visual comparison documentation.
1. Decide whether the timestamp belongs in the assertion
Before changing the test, inspect the failing screenshot diff and answer this question: should this test verify the timestamp’s displayed value?
- Yes: set a deterministic clock so the page renders the intended date every run.
- The timestamp behavior matters, and timers must advance: install Playwright’s clock before the page loads, then advance or pause it as needed.
- No: mask the timestamp or hide it with screenshot-only styling so the assertion covers the rest of the page.
Do not mask a date that is part of the feature being tested. A mask hides visual differences in its covered bounding box, so a broken date display inside that region could pass unnoticed.
2. Freeze the displayed date with setFixedTime
For a page that needs a stable current date while ordinary timers continue running, call page.clock.setFixedTime() before page.goto(). Playwright documents that this fixes Date.now() and new Date() without stopping timers.
import { test, expect } from '@playwright/test';
test('shows the expected date in the header', async ({ page }) => {
await page.clock.setFixedTime(new Date('2024-02-02T10:00:00'));
await page.goto('http://localhost:3000');
await expect(page.getByTestId('last-updated')).toHaveText('February 2, 2024');
await expect(page).toHaveScreenshot();
});
Use an explicit date that makes the intended output clear. Set the clock before navigation so application code that runs during page startup sees the fixed time. If the display depends on locale or timezone, make those inputs consistent in the test and application setup as well; the clock alone does not establish which locale or timezone your application uses.
With Playwright Test, the page fixture is provided by @playwright/test. This example assumes the application is already available at http://localhost:3000 and that the page contains a test ID named last-updated; adapt the URL and locator to your app.
3. Control timers when time must progress
Use page.clock.install() when a test needs control over time-driven behavior such as a countdown, scheduled refresh, or timeout-driven message. Install the clock before navigation and before other clock-related calls. Playwright warns that calling install() after other clock methods can lead to undefined behavior.
import { test, expect } from '@playwright/test';
test('updates a countdown after controlled time passes', async ({ page }) => {
await page.clock.install({ time: new Date('2024-02-02T09:59:00') });
await page.goto('http://localhost:3000');
// Use the clock method that matches the behavior under test.
// For example, pause time and advance it deliberately:
await page.clock.pauseAt(new Date('2024-02-02T10:00:00'));
await expect(page.getByTestId('countdown')).toBeVisible();
await page.clock.runFor(1_000);
await expect(page).toHaveScreenshot();
});
The example uses the documented clock API to illustrate the setup and controlled progression; match the pause or advancement method to the app’s timer behavior. The Clock guide recommends initializing slightly before the intended test time so page-load timers can run normally. If the test only needs a fixed date and timers should keep going, prefer setFixedTime() over installing a fully controlled clock.
setSystemTime() is an advanced option for changing the perceived system time without triggering timers. Choose among these APIs according to whether the test needs a fixed displayed date, controlled timer progression, or a system-time shift. See the official Clock API guide for the available methods and their behavior.
4. Exclude a timestamp that is outside the test’s scope
Mask the timestamp locator
Pass the volatile element as a mask to toHaveScreenshot(). The mask covers its bounding box in the screenshot, which keeps that changing area from causing a diff.
import { test, expect } from '@playwright/test';
test('checks the page layout without asserting the live timestamp', async ({ page }) => {
await page.goto('http://localhost:3000');
const timestamp = page.getByTestId('current-time');
await expect(page).toHaveScreenshot({ mask: [timestamp] });
});
Use a precise locator. If it matches more than one element, narrow it to the intended timestamp so unrelated page content is not hidden from the comparison.
Hide it with screenshot-only CSS
When the timestamp should be omitted from the captured image, use a stylesheet through the screenshot assertion’s stylePath option. This stylesheet applies for the screenshot operation, rather than changing the page’s normal behavior.
/* tests/screenshot.css */
[data-testid="current-time"] {
visibility: hidden !important;
}
import { test, expect } from '@playwright/test';
test('compares the page without the volatile time label', async ({ page }) => {
await page.goto('http://localhost:3000');
await expect(page).toHaveScreenshot({ stylePath: 'tests/screenshot.css' });
});
Masking and screenshot-only styling solve different visual needs: a mask replaces a region in the captured image, while CSS can hide or otherwise adjust volatile elements during capture. Keep either approach limited to content that is intentionally excluded from the assertion. Refer to the screenshot assertion options and visual comparison guide.
5. Diagnose a failure that remains
- Inspect the actual, expected, and diff images. Confirm the changed pixels are the timestamp and check whether other regions also moved or changed.
- Check test ordering. A fixed or installed clock must be configured before navigation and before app code reads time.
- Check where the date comes from. A server-rendered value or API response may already contain a changing timestamp before browser-side clock control takes effect. Make test data deterministic at its source or exclude the region if it is outside the assertion’s scope.
- Check locale and timezone inputs. The same instant can display differently under different formatting settings. Keep those inputs stable when the displayed format is under test.
- Check the rendering environment. Browser version, operating system, settings, hardware, power source, and headless mode can affect screenshots. Playwright recommends generating and comparing baselines in the same environment, and using browser- or platform-specific baselines where appropriate.
- Use Trace Viewer for context. It can show screenshot diffs, actual and expected images, action details, and logs that help pinpoint where the page diverged.
- Update the baseline only after review. If the changed appearance is intentional, update snapshots with
--update-snapshotsafter confirming the diff. Updating a baseline does not fix an unintended source of nondeterminism.
Playwright’s screenshot assertion waits for two consecutive screenshots to match before comparing against the stored expectation. That helps with unstable captures, but it does not make a changing timestamp constant. The official guidance is to run visual comparisons in the same environment used to generate their baselines. Sources: Visual comparisons and Trace Viewer.
6. Troubleshooting common timestamp failures
| Symptom | Likely cause | Fix |
|---|---|---|
The date still changes after calling setFixedTime(). |
The call happens after navigation or the displayed value comes from server-rendered or fetched data. | Set the clock before goto(). Make server or API test data deterministic if it supplies the value. |
| A countdown no longer advances. | The test uses a clock setup that does not let the relevant timers progress as expected. | Use install() and advance time deliberately, or use setFixedTime() when timers should continue naturally. Consult the Clock guide for the specific timer method. |
| The screenshot passes but the timestamp is wrong. | The timestamp is masked or hidden, so the visual assertion cannot check it. | Assert its text or behavior separately, then keep the mask only if visual changes in that region are intentionally excluded. |
| The timestamp looks different across machines despite the same fixed instant. | Locale, timezone, browser, or host rendering settings differ. | Standardize formatting inputs and run screenshot generation and comparison in the same browser and host environment. |
| The diff contains more than the time label. | There may be another dynamic value or an environment-dependent rendering difference. | Inspect the complete diff and trace. Control each relevant input rather than masking a large area without review. |
| Installing the clock produces unexpected behavior. | The clock was installed after another clock operation or after page initialization. | Install it first, before navigation and other clock-related calls. |
7. Performance, reliability, and cost
Freezing time or masking a small locator has no external service cost; it adds only test setup or screenshot styling. The main reliability benefit comes from controlling the input that makes the image nondeterministic. Screenshot retries can wait out transient capture instability, but they cannot substitute for deterministic dates, data, locale, timezone, or browser environment.
Keep screenshot tests focused: assert the timestamp’s value in a targeted text assertion when it matters, and use a screenshot for the visual layout you intend to protect. This makes failures easier to interpret and avoids accepting a changed baseline just to silence a recurring time-dependent diff.
Or skip the browser setup
For a standalone website screenshot, ScreenshotNeo is a website screenshot API and MCP server for developers. Its one-call GET API returns a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed. Response headers identify the page verdict and billing status.
- An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
- 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
FAQ
Should I freeze time or mask the timestamp?
Freeze time if the date or its formatting is part of the behavior being tested. Mask or hide it only when that region is intentionally outside the visual assertion.
Does waiting for two matching screenshots solve a changing timestamp?
No. It reduces capture instability by waiting for consecutive matching images, but a value that changes between runs still needs a deterministic input or an explicit exclusion.
When should I use setSystemTime?
It is an advanced choice for changing perceived system time without triggering timers. For a stable displayed date use setFixedTime(); for timer progression under test control use install() and the appropriate clock methods.
Should I update the snapshot after the test fails?
Only after checking that the visual change is intended. A refreshed snapshot records the new output; it does not explain or correct a changing timestamp.


