Playwright Clock API: Mock Time, Timers, and Dates in Tests
Time-dependent UI is a notorious source of flaky tests: a session-timeout banner that appears after 30 minutes, a countdown timer, a “good morning” greeting that flips at noon, or a token that silently expires mid-test. Waiting for real wall-clock time is slow and non-deterministic, and tests that pass at 11:59 fail at 12:01. In this guide you’ll learn how to use the Playwright clock API to mock time, fast-forward timers, freeze dates, and make every time-based assertion fully deterministic using real Playwright and TypeScript.
Contents
Why you need to mock time in Playwright tests
Browsers expose time through a handful of APIs: Date, Date.now(), performance.now(), and the timer functions setTimeout, setInterval, requestAnimationFrame, and requestIdleCallback. Application code reads from these to render relative timestamps (“2 minutes ago”), schedule polling, animate progress bars, and decide when something should expire. If your test cannot control these sources, it is at the mercy of the machine clock.
Before Playwright shipped a first-class clock implementation, teams reached for page.addInitScript hacks or bundled Sinon’s fake timers into the page. That worked but was fragile and easy to get wrong. The modern page.clock API, available since Playwright 1.45, gives you a controlled, deterministic clock that replaces the browser’s time sources before your page script runs. The result: tests that finish in milliseconds instead of minutes, and that produce the same outcome on every machine, in CI, and at any time of day.
- Determinism — the same input always yields the same assertion result.
- Speed — a 24-hour timeout becomes an instant
fastForwardcall. - Control — you decide exactly when timers fire and what
new Date()returns.
The Playwright clock API mock time methods at a glance
The clock fixture lives on the page object as page.clock. There are two distinct ways to engage it: installing a fake clock that you advance manually, or setting a fixed/system time while letting timers continue to run on their own. Knowing which method to reach for is the key to using the Playwright clock API mock time features correctly.
| Method | What it does | Timers auto-fire? |
|---|---|---|
clock.install({ time }) | Installs a controllable fake clock starting at the given time. Time only moves when you advance it. | No — you control them |
clock.fastForward(ms) | Jumps the clock forward, firing every timer scheduled in that window instantly. | Yes, immediately |
clock.runFor(ms) | Advances tick-by-tick over the duration, firing timers in real chronological order. | Yes, in order |
clock.pauseAt(time) | Fast-forwards to a target time, then freezes the clock there. | Up to that point |
clock.resume() | Resumes normal ticking after a pause. | Yes |
clock.setFixedTime(time) | Makes Date always return this value. Timers are untouched. | Yes (real timers) |
clock.setSystemTime(time) | Sets the current time without pausing; timers keep running. | Yes |
The most common pattern combines install to pin a starting moment with fastForward or runFor to drive timers forward. Always call your clock setup before page.goto() so the fake clock is in place before any page script reads the time.
Example 1: Install a clock and freeze the date
The simplest scenario is pinning the page to a known instant. Suppose your app renders a dashboard greeting and a “Today is…” label that depend on the current date. Install the clock at a fixed moment, navigate, and assert against a value you fully control.
import { test, expect } from '@playwright/test';
test('greeting reflects a frozen morning time', async ({ page }) => {
// Pin the clock BEFORE navigating so page scripts see the fake time.
await page.clock.install({ time: new Date('2026-06-27T09:00:00') });
await page.goto('https://your-app.example/dashboard');
// The app reads new Date() at load and shows a morning greeting.
await expect(page.getByTestId('greeting')).toHaveText('Good morning');
await expect(page.getByTestId('today')).toHaveText('Saturday, June 27, 2026');
});
Because the clock is installed, Date.now(), new Date(), and performance.now() inside the page all start from 2026-06-27T09:00:00 and will not advance until you tell them to. The test no longer cares what time it is in CI.
Here is the classic case that fastForward was built for. The app schedules a setTimeout that shows an “inactive session” warning after 5 minutes. Instead of waiting 300 seconds, jump the clock forward and let the timer fire instantly.
import { test, expect } from '@playwright/test';
test('inactivity banner appears after five minutes', async ({ page }) => {
await page.clock.install();
await page.goto('https://your-app.example/app');
const banner = page.getByRole('alert', { name: /session expiring/i });
await expect(banner).toBeHidden();
// Jump 5 minutes ahead. Any setTimeout/setInterval due in this
// window fires immediately, in one synchronous burst.
await page.clock.fastForward('05:00');
await expect(banner).toBeVisible();
});
Note the convenient '05:00' shorthand — fastForward accepts either a millisecond number or an "MM:SS" / "HH:MM:SS" string. Use fastForward when you only care about the end state. If your app schedules several staggered timers and the order of side effects matters, prefer runFor, which advances chronologically and fires each timer at its correct tick.
Example 3: Drive a countdown with runFor
A countdown timer that updates every second using setInterval is a perfect fit for runFor. Each tick fires the interval callback, so the DOM updates exactly as it would in production — only compressed into instant test time.
import { test, expect } from '@playwright/test';
test('checkout countdown ticks down second by second', async ({ page }) => {
await page.clock.install({ time: new Date('2026-06-27T10:00:00') });
await page.goto('https://your-app.example/checkout');
const timer = page.getByTestId('hold-timer');
await expect(timer).toHaveText('02:00');
// Advance 60 seconds, firing the 1-second interval 60 times in order.
await page.clock.runFor(60_000);
await expect(timer).toHaveText('01:00');
// Advance to the end and assert the expiry state.
await page.clock.runFor(60_000);
await expect(timer).toHaveText('00:00');
await expect(page.getByText('Your cart hold has expired')).toBeVisible();
});
If you used fastForward(120_000) here instead, the interval would still fire 120 times, but all at once at the end — you would never observe the intermediate '01:00' state. Choose runFor when you want to assert on the journey, and fastForward when you only need the destination.
Example 4: pauseAt for a precise boundary, then resume
Some bugs only appear at an exact boundary — midnight rollover, the stroke of a billing cutoff, or a daylight-saving edge. pauseAt fast-forwards to a target time and freezes there, letting you assert the boundary state before resuming normal flow.
import { test, expect } from '@playwright/test';
test('greeting flips exactly at noon', async ({ page }) => {
await page.clock.install({ time: new Date('2026-06-27T11:59:50') });
await page.goto('https://your-app.example/dashboard');
await expect(page.getByTestId('greeting')).toHaveText('Good morning');
// Pause precisely at noon and verify the boundary behavior.
await page.clock.pauseAt(new Date('2026-06-27T12:00:00'));
await expect(page.getByTestId('greeting')).toHaveText('Good afternoon');
// Resume normal ticking if later timers need to run.
await page.clock.resume();
});
While paused, Date is frozen at the target, which is ideal for screenshot or visual-comparison assertions where you need a stable, reproducible timestamp on screen.
setFixedTime vs setSystemTime: when timers should keep running
Not every test wants frozen timers. Sometimes you only need Date to lie about the current moment while real setTimeout behavior continues normally — for example, validating that a “created just now” relative timestamp renders correctly without taking manual control of the scheduler.
import { test, expect } from '@playwright/test';
test('relative timestamp shows just now', async ({ page }) => {
// Only Date is mocked; timers run at their real pace.
await page.clock.setFixedTime(new Date('2026-06-27T08:30:00'));
await page.goto('https://your-app.example/feed');
// The post carries a server timestamp equal to the fixed time.
await expect(page.getByTestId('post-age')).toHaveText('just now');
});
The difference is subtle but important. setFixedTime makes every Date read return the same constant value — the clock does not advance at all. setSystemTime sets the current time but lets it move forward naturally, so reading Date.now() twice can return different values. Reach for install only when you need to manually pump timers with fastForward or runFor.
| You want to… | Use |
|---|---|
Freeze Date to a constant, timers untouched | setFixedTime |
| Set current time but keep it ticking naturally | setSystemTime |
| Take manual control of timers and date | install + fastForward/runFor |
| Jump to a boundary and freeze there | install + pauseAt |
Combining the clock with network mocking
Time-based features often pair with API responses — a token’s expiresAt field, or a feed whose items carry server timestamps. Pin both the clock and the network so the entire scenario is deterministic. Use page.route to fulfil the request with a payload that lines up with your frozen clock.
import { test, expect } from '@playwright/test';
test('token expiry warning matches mocked API', async ({ page }) => {
await page.clock.install({ time: new Date('2026-06-27T10:00:00') });
await page.route('**/api/session', async (route) => {
await route.fulfill({
json: { expiresAt: '2026-06-27T10:10:00' }, // expires in 10 min
});
});
await page.goto('https://your-app.example/app');
await expect(page.getByTestId('session-status')).toHaveText('Active');
// Advance past the mocked expiry and confirm the UI reacts.
await page.clock.fastForward('10:30');
await expect(page.getByTestId('session-status')).toHaveText('Expired');
});
Because the API payload and the clock share the same frozen reference, the 10-minute expiry is exact. Advancing 10 minutes 30 seconds reliably crosses the boundary every run. If you prefer recorded fixtures over hand-written JSON, page.routeFromHAR replays a captured HAR file the same way while the clock stays under your control.
Best practices and gotchas
- Install before navigation. Call
page.clock.install()orsetFixedTime()beforepage.goto(). If you install after the page reads the time, early scripts already captured the real clock. - Pass explicit dates. Always seed with an ISO date you control. Be mindful of time zones —
new Date('2026-06-27T12:00:00')is interpreted in the runner’s local zone unless you append aZor offset. - fastForward vs runFor.
fastForwardcollapses all due timers to the end;runForfires them in chronological order. Pick based on whether intermediate states matter. - Use a fixture for shared setup. If most tests need the same frozen time, wrap
clock.installin a custom test fixture so you don’t repeat it. - It only mocks the browser. The clock controls page-side time, not your Node test code or backend — mock server time separately if needed.
Conclusion
Mastering the Playwright clock API to mock time turns your slowest, flakiest tests into the fastest and most reliable ones in the suite. Install a clock to pin a starting moment, use fastForward or runFor to drive timers, pauseAt to nail exact boundaries, and setFixedTime when you only need Date frozen. Combine it with page.route for end-to-end determinism, always set the clock before navigation, and you’ll never again wait on a real-world timeout or watch a test fail because the build ran past midnight.
FAQ
What Playwright version is required for the clock API?
The page.clock API was introduced in Playwright 1.45. Make sure your @playwright/test dependency is at least that version. Older projects relied on page.addInitScript with Sinon fake timers; migrating to the built-in clock is simpler and more reliable, so upgrade if you can.
What is the difference between fastForward and runFor?
Both advance the clock, but fastForward(ms) jumps straight to the end and fires every due timer in one burst, so you only observe the final state. runFor(ms) advances chronologically and fires each timer at its correct tick, letting you assert on intermediate states like a countdown updating each second. Use runFor when the journey matters and fastForward when only the destination matters.
Does the Playwright clock API affect my backend or Node code?
No. The clock API only mocks time inside the browser page — Date, Date.now(), performance.now(), and the timer functions. Your Node test runner and any real backend keep using the true system clock. If your scenario depends on server time, mock the API responses with page.route so the server-provided timestamps align with your frozen browser clock.
