Resource library

QA How-To

How to Use Playwright clock API (2026)

Learn how to use the Playwright clock API in 2026 for timers, fixed dates, time jumps, polling, debounce, time zones, CI, and reliable boundary tests.

15 min read | 2,420 words

TL;DR

Install page.clock before navigation or timer creation, set an explicit instant, and advance the browser with the method matching the product scenario. Clock controls page time, not backend or database time.

Key Takeaways

  • Install Clock before application code creates the timers you need to control.
  • Use runFor() when each scheduled occurrence matters and fastForward() for a time jump.
  • Use setFixedTime() for stable Date values without full fake-timer progression.
  • Use setSystemTime() for a wall-clock adjustment, not for normal timer elapsed time.
  • Pin the browser context time zone and locale for calendar and formatting tests.
  • Test server-owned expiry through a separate controllable service boundary.

The playwright clock API lets a test control browser time without waiting for real minutes, hours, or dates to pass. Install the clock before application timers are created, choose a known starting instant, then use runFor(), fastForward(), pauseAt(), resume(), setFixedTime(), or setSystemTime() according to the behavior under test.

This makes countdowns, scheduled refreshes, session warnings, debounced input, and date-sensitive screens deterministic. It does not change server time, database time, or an external identity provider's clock. This guide explains that boundary and gives runnable Playwright Test examples for 2026 projects.

TL;DR

Goal Clock method Key behavior
Install controlled time and fake timers page.clock.install({ time }) Call before the page creates timers
Keep Date.now() fixed while real timers continue page.clock.setFixedTime(time) Useful for stable display dates without timer simulation
Advance and execute every scheduled interval occurrence page.clock.runFor(duration) Models elapsed time step by step
Jump ahead, firing due timers at most once page.clock.fastForward(duration) Useful for reaching a future state quickly
Stop at a specific instant page.clock.pauseAt(time) Freezes time at the target after advancing
Continue clock progression page.clock.resume() Lets installed time advance again
Simulate a system clock adjustment page.clock.setSystemTime(time) Changes current time without advancing timer schedules

1. What the Playwright clock API controls

Browser applications obtain time from several related APIs. They create wall-clock values with Date, schedule callbacks with setTimeout() and setInterval(), and often use animation or performance timing. Playwright's Clock is designed to control the browser-side time environment so a test can make those behaviors repeatable.

The controlled boundary is the browser context. Although the API is reached through page.clock, Clock is installed for the complete context, so its pages and iframes share the same controlled time. If the browser calls /api/session and the backend compares a token with the backend host's current time, advancing page.clock does not move that host clock. Likewise, a server-defined expiry, a database job, and a third-party service continue according to their own environments. Good test design identifies which clock owns the decision.

A basic controlled-time test looks like this:

import { test, expect } from '@playwright/test';

test('shows a session warning after five minutes', async ({ page }) => {
  await page.clock.install({
    time: new Date('2026-07-13T09:00:00Z')
  });

  await page.setContent(`
    <p role="status">Session active</p>
    <script>
      setTimeout(() => {
        document.querySelector('[role=status]').textContent =
          'Session expires soon';
      }, 5 * 60 * 1000);
    </script>
  `);

  await page.clock.runFor(5 * 60 * 1000);
  await expect(page.getByRole('status')).toHaveText('Session expires soon');
});

The test runs in seconds while the page experiences five controlled minutes. The assertion remains web-first and describes the user-visible result. The clock moves time, but the expected status proves the behavior.

2. Installing the clock at the right time

Install the clock before navigating to application code that creates timers. If a timer is captured before Clock installation, the test can end up with a mixture of native and controlled timing. That creates failures that are difficult to reproduce and defeats the reason for using fake time.

Use a full ISO timestamp with an explicit Z or numeric offset. A string such as 2026-07-13 may be interpreted differently than a reader expects, and a local timestamp without an offset depends on time zone context.

test('starts on a controlled renewal date', async ({ page }) => {
  await page.clock.install({
    time: new Date('2026-07-13T23:59:50Z')
  });

  await page.setContent(`
    <output id="time"></output>
    <script>
      const output = document.querySelector('#time');
      output.textContent = new Date().toISOString();
      setTimeout(() => {
        output.textContent = new Date().toISOString();
      }, 10_000);
    </script>
  `);

  await expect(page.locator('#time'))
    .toHaveText('2026-07-13T23:59:50.000Z');

  await page.clock.runFor(10_000);
  await expect(page.locator('#time'))
    .toHaveText('2026-07-14T00:00:00.000Z');
});

Place installation in the test when only a few cases need it. A shared fixture can be appropriate for an entire time-driven suite, but hidden fake-time setup can surprise tests that expect ordinary timer behavior. Make the controlled instant easy to find in the test or fixture name.

Do not install the clock globally merely to reduce date variability. If the page has no time-sensitive behavior, ordinary Playwright auto-waiting is simpler. Clock is a test input, not a general synchronization feature.

3. Choosing runFor() versus fastForward()

The distinction matters most for intervals. runFor() advances through the duration and executes timers according to their schedule. fastForward() jumps the clock, with timers due during that jump firing at most once. Use the method that matches the product question.

Scenario runFor() fastForward()
Five interval ticks should produce five effects Preferred Can collapse missed interval occurrences
Reach a delayed final state without replaying every tick More work than needed Preferred
Test animation or incremental progress Preferred Usually too coarse
Simulate a suspended tab returning later Sometimes Often a closer model

This runnable comparison demonstrates the behavior:

test('runFor executes each interval occurrence', async ({ page }) => {
  await page.clock.install();
  await page.setContent(`
    <output>0</output>
    <script>
      let count = 0;
      setInterval(() => {
        document.querySelector('output').textContent = String(++count);
      }, 1000);
    </script>
  `);

  await page.clock.runFor(5_000);
  await expect(page.locator('output')).toHaveText('5');
});

test('fastForward reaches the future without replaying every interval',
  async ({ page }) => {
    await page.clock.install();
    await page.setContent(`
      <output>0</output>
      <script>
        let count = 0;
        setInterval(() => {
          document.querySelector('output').textContent = String(++count);
        }, 1000);
      </script>
    `);

    await page.clock.fastForward(5_000);
    await expect(page.locator('output')).toHaveText('1');
  }
);

Do not switch methods only to make an assertion green. Decide whether the user should experience each intermediate timer callback or the application should reconcile after a jump. That decision often exposes product requirements around background tabs and sleep recovery.

4. Using duration formats safely

Clock advancement methods accept milliseconds as numbers and human-readable clock strings. Numeric values are unambiguous when constants name the unit:

const SECOND = 1_000;
const MINUTE = 60 * SECOND;

await page.clock.runFor(30 * SECOND);
await page.clock.fastForward(15 * MINUTE);

Clock strings are convenient for reviews:

await page.clock.runFor('00:30');
await page.clock.fastForward('01:15:00');

Use hh:mm:ss, mm:ss, or a numeric millisecond value according to the supported duration grammar. Do not pass informal strings such as '5 minutes' and assume they are parsed. Named numeric constants are usually easiest when a duration participates in calculations.

Keep boundary values visible. If a warning appears at exactly 30 minutes, test just before and at the threshold:

test('warns at the exact inactivity boundary', async ({ page }) => {
  const warningAt = 30 * 60 * 1000;

  await page.clock.install({ time: new Date('2026-07-13T10:00:00Z') });
  await page.setContent(`
    <p role="status">Active</p>
    <script>
      setTimeout(() => {
        document.querySelector('[role=status]').textContent = 'Warning';
      }, ${warningAt});
    </script>
  `);

  await page.clock.runFor(warningAt - 1);
  await expect(page.getByRole('status')).toHaveText('Active');

  await page.clock.runFor(1);
  await expect(page.getByRole('status')).toHaveText('Warning');
});

This is stronger than testing only a point far beyond the threshold. It detects an off-by-one error and documents whether the boundary is inclusive.

5. Fixed time, paused time, and resumed time

setFixedTime() gives the page a stable current time while allowing timers to continue in real time. It is useful when a test needs predictable Date.now() or formatted dates but does not need to simulate the timer queue.

test('renders a stable generated date', async ({ page }) => {
  await page.clock.setFixedTime(new Date('2026-07-13T12:00:00Z'));
  await page.setContent(`
    <time></time>
    <script>
      document.querySelector('time').textContent = new Date().toISOString();
    </script>
  `);

  await expect(page.locator('time'))
    .toHaveText('2026-07-13T12:00:00.000Z');
});

pauseAt() moves to a target instant and freezes there. It is useful when the application should settle at a date before the test performs several checks.

await page.clock.install({ time: new Date('2026-07-13T08:00:00Z') });
await page.setContent('<output></output>');
await page.clock.pauseAt(new Date('2026-07-13T09:30:00Z'));

const current = await page.evaluate(() => new Date().toISOString());
expect(current).toBe('2026-07-13T09:30:00.000Z');

Call resume() when the controlled clock should continue advancing. Because the test then depends on real passage again, assert only behavior that can tolerate it, or follow resume with another controlled operation.

await page.clock.resume();
await expect.poll(() => page.evaluate(() => Date.now())).toBeGreaterThan(
  Date.parse('2026-07-13T09:30:00Z')
);

Do not confuse fixed time with paused fake timers. If the test needs to trigger a five-minute setTimeout() immediately, install the clock and advance it. Waiting for a real timer after setFixedTime() defeats the speed benefit.

6. Simulating system time changes

setSystemTime() changes the page's perception of system time without advancing scheduled timers. This models a user or operating system adjusting the wall clock. It is different from letting one hour elapse.

Consider an application that records a timestamp when Save is clicked:

test('records the adjusted system time on the next action', async ({ page }) => {
  await page.clock.install({ time: new Date('2026-07-13T10:00:00Z') });
  await page.setContent(`
    <button>Save</button>
    <output></output>
    <script>
      document.querySelector('button').addEventListener('click', () => {
        document.querySelector('output').textContent =
          new Date().toISOString();
      });
    </script>
  `);

  await page.clock.setSystemTime(new Date('2026-07-13T11:00:00Z'));
  await page.getByRole('button', { name: 'Save' }).click();

  await expect(page.locator('output'))
    .toHaveText('2026-07-13T11:00:00.000Z');
});

Use this for clock correction, manual date changes, or logic comparing a previously stored timestamp with the current wall clock. Do not use it as a substitute for runFor() when timer callbacks must execute. Jumping system time does not mean the timer queue experienced an hour of execution.

System-time changes can expose product defects. Remaining-duration logic should often use a monotonic source rather than repeated wall-clock subtraction, because wall time can move backward. Authentication decisions may deliberately use absolute time. Clarify the requirement and test both forward and backward adjustments when that risk matters.

7. Time zones, locales, and date boundaries

Clock controls the instant, while the browser context's time zone determines how local date APIs format and decompose that instant. Locale affects presentation. Configure both inputs instead of relying on the machine running the test.

import { test, expect } from '@playwright/test';

test.use({
  timezoneId: 'America/New_York',
  locale: 'en-US'
});

test('shows the local date for a controlled instant', async ({ page }) => {
  await page.clock.setFixedTime(new Date('2026-07-13T14:30:00Z'));
  await page.setContent(`
    <output></output>
    <script>
      document.querySelector('output').textContent =
        new Intl.DateTimeFormat('en-US', {
          timeZone: 'America/New_York',
          dateStyle: 'medium',
          timeStyle: 'short'
        }).format(new Date());
    </script>
  `);

  await expect(page.locator('output')).toHaveText(/Jul 13, 2026/);
});

Avoid asserting an entire localized string unless copy and formatting are the contract. Punctuation, spacing, and time-zone abbreviations can vary with the platform's internationalization data. For a business rule, assert a stable machine value or targeted date parts. For presentation, pin locale, time zone, and browser version, then review the intended string.

High-value boundaries include midnight, month-end, leap day, daylight-saving transitions, and year-end. Use explicit instants on both sides of the boundary. Do not add 24 hours and assume that means the same local time on a daylight-saving transition day.

The Playwright cheat sheet covers context configuration and assertion choices that complement clock control.

8. Testing debounce, polling, and countdown behavior

Different timer patterns need different proof. Debounce should run once after the last input. Polling should perform repeated work at a defined cadence. A countdown should display the correct derived value and stop at its terminal state.

test('debounces search after the last keystroke', async ({ page }) => {
  await page.clock.install();
  await page.setContent(`
    <label>Search <input></label>
    <output>0 searches</output>
    <script>
      let timer;
      let count = 0;
      const input = document.querySelector('input');
      const output = document.querySelector('output');
      input.addEventListener('input', () => {
        clearTimeout(timer);
        timer = setTimeout(() => {
          output.textContent = String(++count) + ' search';
        }, 300);
      });
    </script>
  `);

  const search = page.getByLabel('Search');
  await search.fill('play');
  await page.clock.runFor(299);
  await expect(page.locator('output')).toHaveText('0 searches');

  await search.fill('playwright');
  await page.clock.runFor(300);
  await expect(page.locator('output')).toHaveText('1 search');
});

For polling, mock or observe the network boundary and use runFor() so interval occurrences execute. Assert the meaningful terminal UI rather than only counting timer callbacks. For countdowns, check the initial value, one intermediate point, and zero. Testing every second adds little value unless each tick performs business logic.

If asynchronous control flow is unfamiliar, review async and await patterns for QA tests. Clock advancement returns a promise and must be awaited like other Playwright operations.

9. Integrating Clock with routes and server behavior

A deterministic end-to-end-like test may control browser time and mock the server response. Keep the ownership boundary explicit: Clock drives the page's scheduler, while page.route() supplies a known backend state.

test('refreshes dashboard data every minute', async ({ page }) => {
  await page.clock.install({ time: new Date('2026-07-13T09:00:00Z') });

  let calls = 0;
  await page.route('https://api.example.test/metric', async route => {
    calls += 1;
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      headers: { 'access-control-allow-origin': '*' },
      body: JSON.stringify({ value: calls })
    });
  });

  await page.setContent(`
    <output>Loading</output>
    <script>
      async function refresh() {
        const response = await fetch('https://api.example.test/metric');
        const data = await response.json();
        document.querySelector('output').textContent = String(data.value);
      }
      refresh();
      setInterval(refresh, 60_000);
    </script>
  `);

  await expect(page.locator('output')).toHaveText('1');
  await page.clock.runFor(2 * 60_000);
  await expect(page.locator('output')).toHaveText('3');
  expect(calls).toBe(3);
});

When the actual backend must be tested, do not fake its clock accidentally. Supply a supported test clock to the service, create records with explicit expiry values, or use an environment designed for date testing. Browser Clock can still control client-side warnings, but the service needs its own deterministic input.

Avoid testing implementation counts alone. A polling test that asserts three calls but ignores displayed data can pass while parsing is broken. Assert the user result, then use call counts as supporting evidence for cadence or duplicate-request risk.

10. Playwright clock API strategy for CI and framework design

Put time control behind a small domain fixture only when it adds meaning. For example, subscriptionClock.startBeforeRenewal() can be useful if it holds one product-wide convention. A generic wrapper that renames every Clock method creates indirection without reducing risk.

Run time-sensitive tests in the normal browser matrix if the feature is important across engines. Keep the Playwright package and browser binaries aligned through the lockfile and installation process. On upgrades, run controlled-time tests deliberately because timing emulation can gain fixes and new coverage.

Failure evidence should include the starting instant, the advancement performed, browser project, time zone, locale, and application state. A test step can make those transitions visible in reports:

await test.step('advance to the renewal boundary', async () => {
  await page.clock.runFor(10_000);
});

Do not mix uncontrolled real waiting with fake-time steps unless that is explicitly the scenario. A test that calls runFor() and later waits for a native service timer can be confusing. Separate client scheduling tests from full integration tests where possible.

Use retries as diagnostic evidence, not as the cure. If a Clock test is intermittent, inspect installation order, promises created inside callbacks, network completion, shared test data, and assumptions about fastForward() versus runFor(). The Playwright timeout error guide helps separate application readiness from assertion timeout configuration.

Document the time model beside the test. A short comment or test step should say whether the scenario represents ordinary elapsed time, a background-page jump, a fixed reporting timestamp, or a manual device-clock correction. That label prevents a later maintainer from replacing runFor() with fastForward() because both appear to move the clock forward. It also helps product owners review whether the automated scenario matches the requirement.

Measure suite value through deterministic coverage, not through the largest simulated duration. Moving the browser forward a year is easy, but a useful test still needs meaningful boundaries and realistic state. Prefer cases at expiry, renewal, midnight, or a documented retry limit. Remove Clock from tests where it does not control the behavior, because unnecessary fake time adds another environment assumption without adding confidence.

Interview Questions and Answers

Q: What problem does Playwright Clock solve?

It makes browser-side date and timer behavior deterministic. A test can begin at a known instant and advance time without waiting in real time, which is useful for countdowns, inactivity warnings, polling, debounce, and scheduled UI changes.

Q: When should page.clock.install() run?

It should run before page code creates the timers the test needs to control. I normally install it before goto() or setContent(). Installing late can leave a mix of native and controlled timers.

Q: How do runFor() and fastForward() differ?

runFor() advances through time and executes scheduled timer occurrences as they become due. fastForward() jumps to the target and fires timers due in the jump at most once. The choice depends on whether intermediate ticks are part of the behavior.

Q: Does advancing Clock expire a backend token?

Not by itself. Clock controls the browser context environment, while a backend validates against its own time source. A complete token-expiry test needs a controllable backend clock, an explicit expiry fixture, or an appropriate test environment.

Q: What is setFixedTime() for?

It makes the page observe a stable current time while timers continue according to real passage. It is useful for deterministic date rendering when the test does not need fake-timer advancement.

Q: When would you call setSystemTime()?

I use it to simulate a wall-clock adjustment, such as the operating system time moving forward or backward. It changes current time without treating the difference as normally elapsed timer time.

Q: Why configure a time zone in addition to Clock?

Clock defines the instant, while the browser context time zone affects local date fields and formatting. Without both inputs, a test can display different dates around midnight or daylight-saving boundaries on different environments.

Q: How do you verify a timer-driven feature without overfitting to implementation?

I advance the relevant duration and assert a user-observable state, such as a warning, refreshed value, or disabled action. Timer callback counts can support the test when cadence matters, but they should not replace the product outcome.

Common Mistakes

  • Installing Clock after application timers have already been created.
  • Using fastForward() when every interval occurrence is expected to run.
  • Using runFor() when the scenario is specifically a suspended page jumping ahead.
  • Assuming page time also changes backend, database, cookie issuer, or third-party time.
  • Passing local timestamps without an explicit offset and ignoring context time zone.
  • Advancing far past a boundary without checking the instant just before it.
  • Forgetting to await Clock methods and the UI assertion that follows.
  • Combining fake time with arbitrary real sleeps.
  • Asserting only timer counts rather than user-visible results.
  • Hiding controlled time in a broad fixture that surprises unrelated tests.

Conclusion

To use the playwright clock API well, identify the owner of time, install control before timers start, and choose the method that represents the scenario. Use runFor() for scheduled occurrences, fastForward() for a jump, setFixedTime() for stable wall time, pauseAt() and resume() for controlled progression, and setSystemTime() for clock adjustments.

Begin with one slow timer test. Give it an explicit UTC instant, test the exact boundary, and assert the user's result. Then add time zone and server-side coverage only where the risk requires it. That produces fast tests without pretending that one browser clock controls the entire system.

Interview Questions and Answers

What problem does Playwright Clock solve?

It makes browser-side date and timer behavior deterministic. Tests can begin at a known instant and advance minutes or hours without real waiting. This is useful for countdowns, inactivity warnings, polling, debounce, and calendar boundaries.

Why must Clock be installed early?

Application code may capture or create timers during startup. Installing before navigation ensures those timers use the controlled environment. Installing later can leave a mixture of native and fake timers that is hard to reason about.

How do runFor and fastForward differ for intervals?

runFor executes each due occurrence while progressing through time. fastForward jumps, with a timer due in the jump firing at most once. A countdown usually needs runFor, while a suspended-tab recovery scenario may need fastForward.

What does setSystemTime simulate?

It simulates the current wall clock being adjusted to another instant. It does not mean the timer queue experienced the intervening duration normally. I use it for device-clock corrections and absolute-time comparisons.

What is the role of setFixedTime?

It gives Date a stable current value while timers continue in real time. This fits tests that need deterministic date rendering but not fake-timer execution. For a delayed callback, I install Clock and advance it instead.

How do time zones interact with Clock?

Clock establishes an instant, while the context time zone controls local date parts and formatting. Locale affects presentation as another input. I pin all relevant values for calendar tests, especially near midnight and daylight-saving changes.

Can Clock prove a token expired on the server?

No. It can prove the browser reacted to its own time or an explicit expiry value. Server validation uses the server's time source, so I need controlled service data, a supported backend test clock, or a separate integration environment.

How do you debug a flaky Clock test?

I check installation order, awaited promises, async work started inside callbacks, network completion, and the chosen advancement method. I also record starting time, time zone, browser, and data identity. I do not begin by adding a real sleep.

Frequently Asked Questions

What is the Playwright Clock API?

It is Playwright's browser-side time control for dates, timeouts, intervals, and related timing behavior. It lets a test use a known instant and move time without waiting for real duration.

When should I install page.clock?

Install it before the application creates the timers you need to control, normally before goto() or setContent(). Late installation can mix native and controlled timer behavior.

What is the difference between runFor and fastForward?

runFor() executes scheduled timers as controlled time progresses, including repeated interval occurrences. fastForward() jumps ahead and fires timers due during the jump at most once.

Does Playwright Clock change server time?

No. It controls the browser page's time environment. Backends, databases, queues, identity providers, and third-party services require their own test clock or explicit expiry data.

When should I use setFixedTime?

Use it when the page must observe a stable Date value while timers continue with real passage. It is useful for deterministic generated dates and display timestamps without simulated timer advancement.

How do I test midnight in another time zone?

Set the Playwright context timezoneId, install an explicit UTC instant just before local midnight, and advance across the boundary. Assert a stable local date rule rather than depending on the CI host's time zone.

Can Clock replace Playwright auto-waiting?

No. Clock moves browser time, while locator actions and web-first assertions synchronize with observable page state. Use Clock to trigger behavior, then use normal assertions to prove its result.

Related Guides