Skip to content

Virtual Clock

Test idle timeouts, trial expirations, and debounced inputs without sleep().

Why you need this

Three tests that are impossible — or flaky at best — without clock control:

  1. Auto-logout after 15 minutes idle. Without clock control you either wait 15 real minutes in CI, or stub Date inside the app code (the test no longer reflects production). With it: jump 15 min, assert the login modal.

  2. Trial banner that flips at midnight. Assert both "expires today" before midnight and "expired" right after, in the same CI run, deterministically.

  3. Debounced search input. Advance exactly 299 ms (no request), then 2 ms more (exactly one request). No setTimeout(400) flake.


Quick-start examples

1 — Idle logout after 15 minutes

ts
import { Browser } from 'craftdriver';

const browser = await Browser.launch();
await browser.clock.install({ time: '2026-01-01T09:00:00Z' });
await browser.navigateTo('http://localhost:3000/dashboard');
await browser.clock.fastForward('15:01');
await browser.expect('#login-modal').toBeVisible();
await browser.quit();

2 — Trial banner flipping at midnight

ts
import { Browser } from 'craftdriver';

const browser = await Browser.launch();

// Before midnight — banner says "expires today"
await browser.clock.setFixedTime('2026-06-15T23:59:00Z');
await browser.navigateTo('http://localhost:3000/billing');
await browser.expect('#trial-banner').toContainText('expires today');

// After midnight — banner says "expired"
await browser.clock.setFixedTime('2026-06-16T00:00:01Z');
await browser.reload();
await browser.expect('#trial-banner').toContainText('expired');

await browser.quit();

3 — Debounced search input

ts
import { Browser } from 'craftdriver';

const browser = await Browser.launch();
await browser.clock.install();
await browser.navigateTo('http://localhost:3000/search');
await browser.fill('#q', 'lap');

await browser.clock.tick(299); // debounce hasn't fired yet
// assert: no /search network request

await browser.clock.tick(2);  // total 301 ms — debounce fires
// assert: exactly one /search?q=lap request

await browser.quit();

Method reference

MethodWhen to use
clock.install(opts?)Full fake-timer suite: Date, performance.now, setTimeout, setInterval, rAF. Use when you need tick().
clock.uninstall()Restore real globals and remove the preload script.
clock.tick(ms)Advance virtual time by ms milliseconds and fire all due timers.
clock.fastForward(duration)Like tick() but also accepts "MM:SS" / "HH:MM:SS" strings.
clock.setFixedTime(time)Freeze Date.now() at a point in time. Does not fake timers.
clock.setSystemTime(time)Move the virtual clock to time without firing timers. Requires install().
clock.runFor(ms)Like tick() but yields between frames so async/microtask callbacks resolve.

clock.install(options?)

ts
await browser.clock.install();
await browser.clock.install({ time: '2026-01-01T09:00:00Z' });
await browser.clock.install({ time: 1_735_689_600_000 });   // ms since epoch
await browser.clock.install({ time: new Date('2026-01-01') });

Installs fake implementations of:

  • Date constructor and Date.now()
  • performance.now()
  • setTimeout / clearTimeout
  • setInterval / clearInterval
  • requestAnimationFrame / cancelAnimationFrame

Timers only fire when you explicitly call tick(), fastForward(), or runFor(). Also registers a preload script so the fake clock is automatically re-installed on every subsequent navigation for the duration of the browser session.

time defaults to the real current time (Date.now()) if omitted.

clock.uninstall()

ts
await browser.clock.uninstall();

Restores Date, setTimeout, setInterval, and all other faked globals to their originals. Removes the preload script so future navigations use the real clock again.

clock.tick(ms)

ts
await browser.clock.tick(500);   // advance 500 ms
await browser.clock.tick(0);     // flush zero-delay timers

Advances the virtual clock by ms milliseconds. Every timer whose deadline falls within the window is fired in order. setInterval timers reschedule themselves automatically.

Requires install().

clock.fastForward(duration)

ts
await browser.clock.fastForward(60_000);    // 60 seconds
await browser.clock.fastForward('15:00');   // 15 minutes
await browser.clock.fastForward('01:30:00'); // 1 hour 30 minutes

Same as tick() but accepts a human-readable duration string.

  • "MM:SS" — minutes and seconds (numbers can exceed 59: "90:00" = 90 min)
  • "HH:MM:SS" — hours, minutes, seconds

Requires install().

clock.setFixedTime(time)

ts
await browser.clock.setFixedTime('2026-06-15T23:59:00Z');
await browser.clock.setFixedTime(1_750_032_540_000);
await browser.clock.setFixedTime(new Date('2026-06-15T23:59:00Z'));

Freezes Date.now() and new Date() at the given instant. Does not install fake setTimeout or setInterval — real timers continue to fire.

Use this for simple date-dependent rendering where you don't need to control timer execution.

Also registers a preload script so the fixed date persists across navigations.

clock.setSystemTime(time)

ts
await browser.clock.install({ time: 0 });
await browser.clock.setSystemTime(Date.UTC(2026, 5, 16)); // jump to 2026-06-16
await browser.clock.tick(1000); // continue from here

Moves the virtual clock to time without advancing the timer queue or firing any callbacks. Subsequent tick() calls continue from the new position.

Requires install().

clock.runFor(ms)

ts
await browser.clock.runFor(1000);

Like tick() but yields between each ~16 ms frame so that microtask callbacks (e.g. Promise.then() chained to a timer) resolve in the correct order before the next frame is processed.

Requires install().


Accepted time formats

All methods that take a time argument accept:

TypeExampleNotes
number1_750_032_540_000Milliseconds since Unix epoch
string'2026-06-15T23:59:00Z'Anything new Date(string) accepts
Datenew Date('2026-01-01')Used as-is

fastForward additionally accepts a duration string ("MM:SS" or "HH:MM:SS"), not an absolute point in time.


Gotchas

What is and isn't faked

After install():

Faked ✓Not faked ✗
Date.now()Date.prototype.toLocaleString formatting
new Date() with no argsIntl.DateTimeFormat time zone offsets
performance.now()Real network request timing
setTimeout / clearTimeoutWorker timers
setInterval / clearIntervalSharedWorker / ServiceWorker timers
requestAnimationFrameCSS animation timing

After setFixedTime() only Date.now(), new Date(), and performance.now() are affected. All timers use real wall time.

Both install() and setFixedTime() register a BiDi preload script that re-applies the fake clock automatically on every navigation. You don't need to call them again after navigateTo() or reload().

uninstall() removes the preload script. After uninstalling, future navigations use the real clock.

Interaction with real network requests

Clock control affects only in-page JavaScript. Real HTTP requests still run at wall-clock speed. If your test asserts on a network response, make sure the request is made (i.e. a timer fires and calls fetch()) before asserting on the response.

install() is idempotent

Calling install() a second time resets the virtual time and the timer queue. Any timers registered before the second install() are discarded.

Released under the MIT License.