Skip to content

Commit 9b1171f

Browse files
authored
feat(fake-timers): basic support for Temporal (#16128)
1 parent 69a28ba commit 9b1171f

14 files changed

Lines changed: 264 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
- `[jest-circus, jest-cli, jest-config, jest-core, jest-jasmine2, jest-types]` Add `--collect-tests` flag to discover and list tests without executing them ([#16006](https://github.com/jestjs/jest/pull/16006))
77
- `[jest-config, jest-runner, jest-worker]` Add `workerGracefulExitTimeout` config option to control how long workers are given to exit before being force-killed ([#15984](https://github.com/jestjs/jest/pull/15984))
88
- `[jest-config]` Add support for `jest.config.mts` as a valid configuration file ([#16005](https://github.com/jestjs/jest/pull/16005))
9+
- `[@jest/fake-timers]` Accept `Temporal.Duration` in `jest.advanceTimersByTime()` and `jest.advanceTimersByTimeAsync()` ([#16128](https://github.com/jestjs/jest/pull/16128))
10+
- `[@jest/fake-timers]` Accept `Temporal.Instant` and `Temporal.ZonedDateTime` in `jest.setSystemTime()` and `useFakeTimers({now})` ([#16128](https://github.com/jestjs/jest/pull/16128))
911
- `[jest-mock]` Add `clearMocksOnScope(scope)` on `ModuleMocker` for clearing every mock function exposed on a scope object ([#16088](https://github.com/jestjs/jest/pull/16088))
1012
- `[jest-resolve]` Add `canResolveSync()` on `Resolver` so callers can detect when a user-configured resolver only exports an `async` hook ([#16064](https://github.com/jestjs/jest/pull/16064))
1113
- `[jest-runtime]` Use synchronous `evaluate()` for ES modules without top-level `await` on Node versions that support it (v24.9+), and prefer the synchronous transform path when a sync transformer is configured ([#16062](https://github.com/jestjs/jest/pull/16062))
@@ -322,4 +324,4 @@
322324

323325
## Older Changelog Entries
324326

325-
For newer CHANGELOG entries see [`CHANGELOG_PRE_v30.md`](CHANGELOG_PRE_v30.md).
327+
For older CHANGELOG entries see [`CHANGELOG_PRE_v30.md`](CHANGELOG_PRE_v30.md).

docs/JestObjectAPI.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -942,8 +942,12 @@ type FakeTimersConfig = {
942942
* The default is `false`.
943943
*/
944944
legacyFakeTimers?: boolean;
945-
/** Sets current system time to be used by fake timers, in milliseconds. The default is `Date.now()`. */
946-
now?: number | Date;
945+
/**
946+
* Sets current system time to be used by fake timers. Accepts a millisecond
947+
* timestamp, a `Date`, a `Temporal.Instant`, or a `Temporal.ZonedDateTime`.
948+
* The default is `Date.now()`.
949+
*/
950+
now?: number | Date | Temporal.Instant | Temporal.ZonedDateTime;
947951
/**
948952
* The maximum number of recursive timers that will be run when calling `jest.runAllTimers()`.
949953
* The default is `100_000` timers.
@@ -1050,6 +1054,8 @@ Executes only the macro task queue (i.e. all tasks queued by `setTimeout()` or `
10501054

10511055
When this API is called, all timers are advanced by `msToRun` milliseconds. All pending "macro-tasks" that have been queued via `setTimeout()` or `setInterval()`, and would be executed within this time frame will be executed. Additionally, if those macro-tasks schedule new macro-tasks that would be executed within the same time frame, those will be executed until there are no more macro-tasks remaining in the queue, that should be run within `msToRun` milliseconds.
10521056

1057+
`msToRun` also accepts a `Temporal.Duration`. Calendar units (`years`, `months`, `weeks`) are not supported and will throw — use time-based units (`days`, `hours`, `minutes`, `seconds`, `milliseconds`) instead.
1058+
10531059
### `jest.advanceTimersByTimeAsync(msToRun)`
10541060

10551061
Asynchronous equivalent of `jest.advanceTimersByTime(msToRun)`. It allows any scheduled promise callbacks to execute _before_ running the timers.
@@ -1116,10 +1122,12 @@ Returns the number of fake timers still left to run.
11161122

11171123
Returns the time in ms of the current clock. This is equivalent to `Date.now()` if real timers are in use, or if `Date` is mocked. In other cases (such as legacy timers) it may be useful for implementing custom mocks of `Date.now()`, `performance.now()`, etc.
11181124

1119-
### `jest.setSystemTime(now?: number | Date)`
1125+
### `jest.setSystemTime(now?: number | Date | Temporal.Instant | Temporal.ZonedDateTime)`
11201126

11211127
Set the current system time used by fake timers. Simulates a user changing the system clock while your program is running. It affects the current time but it does not in itself cause e.g. timers to fire; they will fire exactly as they would have done without the call to `jest.setSystemTime()`.
11221128

1129+
Note that `Temporal` itself is **not** faked when using fake timers — see [sinonjs/fake-timers#335](https://github.com/sinonjs/fake-timers/issues/335) for the upstream tracking issue.
1130+
11231131
:::info
11241132

11251133
This function is not available when using legacy fake timers implementation.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {onNodeVersions} from '@jest/test-utils';
9+
import runJest from '../runJest';
10+
11+
onNodeVersions('>=26', () => {
12+
test('useFakeTimers({now}) and setSystemTime accept Temporal instances', () => {
13+
const result = runJest('fake-timers-temporal');
14+
expect(result.exitCode).toBe(0);
15+
});
16+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
const DATE = new Date('2026-01-01T00:00:00Z');
9+
const EPOCH_MS = DATE.getTime();
10+
const ISO = DATE.toISOString();
11+
12+
describe('Temporal support in fake timers', () => {
13+
afterEach(() => {
14+
jest.useRealTimers();
15+
});
16+
17+
test('useFakeTimers({now}) accepts Temporal.Instant', () => {
18+
jest.useFakeTimers({now: Temporal.Instant.from(ISO)});
19+
expect(Date.now()).toBe(EPOCH_MS);
20+
});
21+
22+
test('useFakeTimers({now}) accepts Temporal.ZonedDateTime', () => {
23+
const zdt = Temporal.Instant.from(ISO).toZonedDateTimeISO('UTC');
24+
jest.useFakeTimers({now: zdt});
25+
expect(Date.now()).toBe(EPOCH_MS);
26+
});
27+
28+
test('setSystemTime accepts Temporal.Instant', () => {
29+
jest.useFakeTimers();
30+
jest.setSystemTime(Temporal.Instant.from(ISO));
31+
expect(Date.now()).toBe(EPOCH_MS);
32+
});
33+
34+
test('setSystemTime accepts Temporal.ZonedDateTime', () => {
35+
jest.useFakeTimers();
36+
const zdt = Temporal.Instant.from(ISO).toZonedDateTimeISO('UTC');
37+
jest.setSystemTime(zdt);
38+
expect(Date.now()).toBe(EPOCH_MS);
39+
});
40+
41+
test('advanceTimersByTime accepts Temporal.Duration', () => {
42+
jest.useFakeTimers({now: EPOCH_MS});
43+
jest.advanceTimersByTime(Temporal.Duration.from({hours: 1}));
44+
expect(Date.now()).toBe(EPOCH_MS + 3_600_000);
45+
});
46+
47+
test('advanceTimersByTimeAsync accepts Temporal.Duration', async () => {
48+
jest.useFakeTimers({now: EPOCH_MS});
49+
await jest.advanceTimersByTimeAsync(Temporal.Duration.from({minutes: 30}));
50+
expect(Date.now()).toBe(EPOCH_MS + 1_800_000);
51+
});
52+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "fake-timers-temporal"
3+
}

eslint.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,12 @@ const config = defineConfig(
681681
},
682682
},
683683
},
684+
{
685+
files: ['e2e/fake-timers-temporal/__tests__/*'],
686+
languageOptions: {
687+
globals: {Temporal: 'readonly'},
688+
},
689+
},
684690
{
685691
files: [
686692
'e2e/**',

packages/jest-environment/src/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,18 @@ export interface Jest {
5959
* that have been queued via `setTimeout()` or `setInterval()`, and would be
6060
* executed within this time frame will be executed.
6161
*/
62-
advanceTimersByTime(msToRun: number): void;
62+
advanceTimersByTime(
63+
msToRun: number | {total(options: {unit: string}): number},
64+
): void;
6365
/**
6466
* Advances all timers by `msToRun` milliseconds, firing callbacks if necessary.
6567
*
6668
* @remarks
6769
* Not available when using legacy fake timers implementation.
6870
*/
69-
advanceTimersByTimeAsync(msToRun: number): Promise<void>;
71+
advanceTimersByTimeAsync(
72+
msToRun: number | {total(options: {unit: string}): number},
73+
): Promise<void>;
7074
/**
7175
* Advances all timers by the needed milliseconds to execute callbacks currently scheduled with `requestAnimationFrame`.
7276
* `advanceTimersToNextFrame()` is a helpful way to execute code that is scheduled using `requestAnimationFrame`.
@@ -375,7 +379,7 @@ export interface Jest {
375379
* @remarks
376380
* Not available when using legacy fake timers implementation.
377381
*/
378-
setSystemTime(now?: number | Date): void;
382+
setSystemTime(now?: number | Date | {epochMilliseconds: number}): void;
379383
/**
380384
* Set the default timeout interval for tests and before/after hooks in
381385
* milliseconds.

packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,4 +1636,25 @@ describe('FakeTimers', () => {
16361636
expect(now).toBeLessThanOrEqual(after);
16371637
});
16381638
});
1639+
1640+
describe('Temporal', () => {
1641+
let timers: FakeTimers;
1642+
1643+
beforeEach(() => {
1644+
const global = {process} as unknown as typeof globalThis;
1645+
timers = new FakeTimers({config, global, moduleMocker, timerConfig});
1646+
timers.useFakeTimers();
1647+
});
1648+
1649+
afterEach(() => {
1650+
timers.useRealTimers();
1651+
});
1652+
1653+
it('advanceTimersByTime accepts an object with total()', () => {
1654+
timers.advanceTimersByTime({
1655+
total: ({unit}) => (unit === 'millisecond' ? 5000 : 5),
1656+
});
1657+
expect(timers.now()).toBe(5000);
1658+
});
1659+
});
16391660
});

packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,4 +1520,70 @@ describe('FakeTimers', () => {
15201520
expect(now).toBeLessThanOrEqual(after);
15211521
});
15221522
});
1523+
1524+
describe('Temporal', () => {
1525+
const epoch_ms = new Date('2026-01-01T00:00:00Z').getTime();
1526+
let timers: FakeTimers;
1527+
let fakedGlobal: typeof globalThis;
1528+
1529+
beforeEach(() => {
1530+
fakedGlobal = {
1531+
Date,
1532+
clearInterval,
1533+
clearTimeout,
1534+
process,
1535+
setInterval,
1536+
setTimeout,
1537+
} as unknown as typeof globalThis;
1538+
timers = new FakeTimers({
1539+
config: makeProjectConfig(),
1540+
global: fakedGlobal,
1541+
});
1542+
});
1543+
1544+
afterEach(() => {
1545+
timers.useRealTimers();
1546+
});
1547+
1548+
it('setSystemTime accepts an object with epochMilliseconds', () => {
1549+
timers.useFakeTimers();
1550+
timers.setSystemTime({epochMilliseconds: epoch_ms});
1551+
expect(fakedGlobal.Date.now()).toBe(epoch_ms);
1552+
});
1553+
1554+
it('useFakeTimers({now}) accepts an object with epochMilliseconds', () => {
1555+
timers.useFakeTimers({now: {epochMilliseconds: epoch_ms}});
1556+
expect(fakedGlobal.Date.now()).toBe(epoch_ms);
1557+
});
1558+
1559+
it('advanceTimersByTime accepts an object with total()', () => {
1560+
timers.useFakeTimers({now: 0});
1561+
timers.advanceTimersByTime({
1562+
total: ({unit}) => (unit === 'millisecond' ? 5000 : 5),
1563+
});
1564+
expect(fakedGlobal.Date.now()).toBe(5000);
1565+
});
1566+
1567+
it('advanceTimersByTimeAsync accepts an object with total()', async () => {
1568+
const asyncGlobal = {
1569+
Date,
1570+
Promise,
1571+
clearInterval,
1572+
clearTimeout,
1573+
process,
1574+
setInterval,
1575+
setTimeout,
1576+
} as unknown as typeof globalThis;
1577+
const asyncTimers = new FakeTimers({
1578+
config: makeProjectConfig(),
1579+
global: asyncGlobal,
1580+
});
1581+
asyncTimers.useFakeTimers({now: 0});
1582+
await asyncTimers.advanceTimersByTimeAsync({
1583+
total: ({unit}) => (unit === 'millisecond' ? 5000 : 5),
1584+
});
1585+
expect(asyncGlobal.Date.now()).toBe(5000);
1586+
asyncTimers.useRealTimers();
1587+
});
1588+
});
15231589
});

packages/jest-fake-timers/src/legacyFakeTimers.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
UnknownFunction,
1717
} from 'jest-mock';
1818
import {setGlobal} from 'jest-util';
19+
import {type TemporalDurationLike, toDurationMs} from './temporalUtils';
1920

2021
type Callback = (...args: Array<unknown>) => void;
2122

@@ -274,8 +275,9 @@ export default class FakeTimers<TimerRef = unknown> {
274275
}
275276
}
276277

277-
advanceTimersByTime(msToRun: number): void {
278+
advanceTimersByTime(msToRun: number | TemporalDurationLike): void {
278279
this._checkFakeTimers();
280+
let msRemaining = toDurationMs(msToRun);
279281
// Only run a generous number of timers and then bail.
280282
// This is just to help avoid recursive loops
281283
let i;
@@ -288,18 +290,18 @@ export default class FakeTimers<TimerRef = unknown> {
288290
}
289291
const [timerHandle, nextTimerExpiry] = timerHandleAndExpiry;
290292

291-
if (this._now + msToRun < nextTimerExpiry) {
293+
if (this._now + msRemaining < nextTimerExpiry) {
292294
// There are no timers between now and the target we're running to
293295
break;
294296
} else {
295-
msToRun -= nextTimerExpiry - this._now;
297+
msRemaining -= nextTimerExpiry - this._now;
296298
this._now = nextTimerExpiry;
297299
this._runTimerHandle(timerHandle);
298300
}
299301
}
300302

301303
// Advance the clock by whatever time we still have left to run
302-
this._now += msToRun;
304+
this._now += msRemaining;
303305

304306
if (i === this._maxLoops) {
305307
throw new Error(

0 commit comments

Comments
 (0)