feat(fake-timers): support mocking Temporal.Now time (#16131)

This commit is contained in:
Simen Bekkhus
2026-05-07 10:44:00 +02:00
committed by GitHub
parent ff266eef2e
commit 63c55d4cf5
13 changed files with 66 additions and 58 deletions
+1
View File
@@ -9,6 +9,7 @@
- `[jest-config, jest-core, jest-reporters, jest-runner]` `verbose` and `silent` can now be set per-project; the project-level value overrides the global value for that project's tests ([#16133](https://github.com/jestjs/jest/pull/16133))
- `[@jest/fake-timers]` Accept `Temporal.Duration` in `jest.advanceTimersByTime()` and `jest.advanceTimersByTimeAsync()` ([#16128](https://github.com/jestjs/jest/pull/16128))
- `[@jest/fake-timers]` Accept `Temporal.Instant` and `Temporal.ZonedDateTime` in `jest.setSystemTime()` and `useFakeTimers({now})` ([#16128](https://github.com/jestjs/jest/pull/16128))
- `[@jest/fake-timers]` Support faking `Temporal.Now.*` ([#16131](https://github.com/jestjs/jest/pull/16131))
- `[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))
- `[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))
- `[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))
+3 -2
View File
@@ -923,7 +923,8 @@ type FakeableAPI =
| 'setInterval'
| 'clearInterval'
| 'setTimeout'
| 'clearTimeout';
| 'clearTimeout'
| 'Temporal';
type FakeTimersConfig = {
/**
@@ -1126,7 +1127,7 @@ Returns the time in ms of the current clock. This is equivalent to `Date.now()`
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()`.
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.
When `'Temporal'` is faked (see `useFakeTimers`), `Temporal.Now.*` methods (`instant()`, `zonedDateTimeISO()`, `plainDateTimeISO()`, etc.) are driven by the fake clock. `Temporal.Now.timeZoneId()` is not faked.
:::info
@@ -14,6 +14,24 @@ describe('Temporal support in fake timers', () => {
jest.useRealTimers();
});
test('fakes Temporal.Now.instant()', () => {
jest.useFakeTimers({now: EPOCH_MS});
expect(Temporal.Now.instant().epochMilliseconds).toBe(EPOCH_MS);
});
test('fakes Temporal.Now.zonedDateTimeISO()', () => {
jest.useFakeTimers({now: EPOCH_MS});
expect(Temporal.Now.zonedDateTimeISO('UTC').epochMilliseconds).toBe(
EPOCH_MS,
);
});
test('Temporal.Now follows setSystemTime', () => {
jest.useFakeTimers({now: 0});
jest.setSystemTime(EPOCH_MS);
expect(Temporal.Now.instant().epochMilliseconds).toBe(EPOCH_MS);
});
test('useFakeTimers({now}) accepts Temporal.Instant', () => {
jest.useFakeTimers({now: Temporal.Instant.from(ISO)});
expect(Date.now()).toBe(EPOCH_MS);
+9 -6
View File
@@ -6,7 +6,12 @@
*/
import type {Context} from 'node:vm';
import type {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers';
import type {
LegacyFakeTimers,
ModernFakeTimers,
TemporalDurationLike,
TemporalEpochLike,
} from '@jest/fake-timers';
import type {Circus, Config, Global} from '@jest/types';
import type {Mocked, ModuleMocker} from 'jest-mock';
@@ -59,9 +64,7 @@ export interface Jest {
* that have been queued via `setTimeout()` or `setInterval()`, and would be
* executed within this time frame will be executed.
*/
advanceTimersByTime(
msToRun: number | {total(options: {unit: string}): number},
): void;
advanceTimersByTime(msToRun: number | TemporalDurationLike): void;
/**
* Advances all timers by `msToRun` milliseconds, firing callbacks if necessary.
*
@@ -69,7 +72,7 @@ export interface Jest {
* Not available when using legacy fake timers implementation.
*/
advanceTimersByTimeAsync(
msToRun: number | {total(options: {unit: string}): number},
msToRun: number | TemporalDurationLike,
): Promise<void>;
/**
* Advances all timers by the needed milliseconds to execute callbacks currently scheduled with `requestAnimationFrame`.
@@ -379,7 +382,7 @@ export interface Jest {
* @remarks
* Not available when using legacy fake timers implementation.
*/
setSystemTime(now?: number | Date | {epochMilliseconds: number}): void;
setSystemTime(now?: number | Date | TemporalEpochLike): void;
/**
* Set the default timeout interval for tests and before/after hooks in
* milliseconds.
@@ -7,6 +7,7 @@
*/
import {makeProjectConfig} from '@jest/test-utils';
import type {TemporalDuration} from '@sinonjs/fake-timers';
import FakeTimers from '../modernFakeTimers';
describe('FakeTimers', () => {
@@ -1560,7 +1561,7 @@ describe('FakeTimers', () => {
timers.useFakeTimers({now: 0});
timers.advanceTimersByTime({
total: ({unit}) => (unit === 'millisecond' ? 5000 : 5),
});
} as TemporalDuration);
expect(fakedGlobal.Date.now()).toBe(5000);
});
@@ -1581,7 +1582,7 @@ describe('FakeTimers', () => {
asyncTimers.useFakeTimers({now: 0});
await asyncTimers.advanceTimersByTimeAsync({
total: ({unit}) => (unit === 'millisecond' ? 5000 : 5),
});
} as TemporalDuration);
expect(asyncGlobal.Date.now()).toBe(5000);
asyncTimers.useRealTimers();
});
@@ -128,7 +128,7 @@ describe('`@sinonjs/fake-timers` integration', () => {
expect(mockInstall).toHaveBeenCalledWith({
advanceTimeDelta: 40,
loopLimit: 2000,
now: new Date('1995-12-17').getTime(),
now: new Date('1995-12-17'),
shouldAdvanceTime: true,
shouldClearNativeTimers: true,
toFake: [
+4
View File
@@ -7,3 +7,7 @@
export {default as LegacyFakeTimers} from './legacyFakeTimers';
export {default as ModernFakeTimers} from './modernFakeTimers';
export type {
TemporalDuration as TemporalDurationLike,
TemporalTimelike as TemporalEpochLike,
} from '@sinonjs/fake-timers';
@@ -16,9 +16,9 @@ import type {
UnknownFunction,
} from 'jest-mock';
import {setGlobal} from 'jest-util';
import {type TemporalDurationLike, toDurationMs} from './temporalUtils';
type Callback = (...args: Array<unknown>) => void;
type TemporalDurationLike = {total(options: {unit: string}): number};
type TimerID = string;
@@ -277,7 +277,10 @@ export default class FakeTimers<TimerRef = unknown> {
advanceTimersByTime(msToRun: number | TemporalDurationLike): void {
this._checkFakeTimers();
let msRemaining = toDurationMs(msToRun);
let msRemaining =
typeof msToRun === 'number'
? msToRun
: msToRun.total({unit: 'millisecond'});
// Only run a generous number of timers and then bail.
// This is just to help avoid recursive loops
let i;
@@ -10,16 +10,12 @@ import {
type FakeMethod as FakeableAPI,
type Clock as InstalledClock,
type Config as SinonFakeTimersConfig,
type TemporalDuration,
type TemporalTimelike,
withGlobal,
} from '@sinonjs/fake-timers';
import type {Config} from '@jest/types';
import {formatStackTrace} from 'jest-message-util';
import {
type TemporalDurationLike,
type TemporalEpochLike,
toDurationMs,
toEpochMs,
} from './temporalUtils';
export default class FakeTimers {
private _clock!: InstalledClock;
@@ -104,17 +100,27 @@ export default class FakeTimers {
}
}
advanceTimersByTime(msToRun: number | TemporalDurationLike): void {
advanceTimersByTime(msToRun: number | TemporalDuration): void {
if (this._checkFakeTimers()) {
this._clock.tick(toDurationMs(msToRun));
// TODO: pass msToRun directly once https://github.com/sinonjs/fake-timers/pull/574 is published
this._clock.tick(
typeof msToRun === 'number'
? msToRun
: msToRun.total({unit: 'millisecond'}),
);
}
}
async advanceTimersByTimeAsync(
msToRun: number | TemporalDurationLike,
msToRun: number | TemporalDuration,
): Promise<void> {
if (this._checkFakeTimers()) {
await this._clock.tickAsync(toDurationMs(msToRun));
// TODO: pass msToRun directly once https://github.com/sinonjs/fake-timers/pull/574 is published
await this._clock.tickAsync(
typeof msToRun === 'number'
? msToRun
: msToRun.total({unit: 'millisecond'}),
);
}
}
@@ -157,9 +163,9 @@ export default class FakeTimers {
}
}
setSystemTime(now?: number | Date | TemporalEpochLike): void {
setSystemTime(now?: number | Date | TemporalTimelike): void {
if (this._checkFakeTimers()) {
this._clock.setSystemTime(toEpochMs(now));
this._clock.setSystemTime(now);
}
}
@@ -234,7 +240,7 @@ export default class FakeTimers {
return {
advanceTimeDelta,
loopLimit: fakeTimersConfig.timerLimit || 100_000,
now: toEpochMs(fakeTimersConfig.now) ?? Date.now(),
now: fakeTimersConfig.now ?? Date.now(),
shouldAdvanceTime: Boolean(fakeTimersConfig.advanceTimers),
shouldClearNativeTimers: true,
toFake: [...toFake],
@@ -1,27 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export type TemporalEpochLike = {epochMilliseconds: number};
export type TemporalDurationLike = {total(options: {unit: string}): number};
export function toEpochMs(
value: number | Date | TemporalEpochLike | undefined,
): number | undefined {
if (value == null) return undefined;
if (typeof value === 'number') return value;
// Use duck-typing rather than instanceof to handle cross-realm Date objects
// (e.g. Sinon's ClockDate, which extends Date but may fail instanceof checks
// across module boundaries in a webpack bundle).
if (typeof (value as Date).getTime === 'function')
return (value as Date).getTime();
return (value as TemporalEpochLike).epochMilliseconds;
}
export function toDurationMs(value: number | TemporalDurationLike): number {
if (typeof value === 'number') return value;
return value.total({unit: 'millisecond'});
}
+1
View File
@@ -138,6 +138,7 @@ const FakeableAPI = Type.Union([
Type.Literal('clearInterval'),
Type.Literal('setTimeout'),
Type.Literal('clearTimeout'),
Type.Literal('Temporal'),
]);
const FakeTimersConfig = Type.Partial(
@@ -537,16 +537,12 @@ expect<jest.SpiedSetter<typeof someObject.propertyC>>().type.toBeAssignableFrom(
expect(jest.advanceTimersByTime(6000)).type.toBe<void>();
// expect(jest.advanceTimersByTime(Temporal.Duration.from(''))).type.toBe<void>();
expect(jest.advanceTimersByTime({total: () => 6000})).type.toBe<void>();
expect(jest.advanceTimersByTime()).type.toRaiseError();
expect(jest.advanceTimersByTimeAsync(6000)).type.toBe<Promise<void>>();
// expect(jest.advanceTimersByTimeAsync(Temporal.Duration.from(''))).type.toBe<
// Promise<void>
// >();
expect(jest.advanceTimersByTimeAsync({total: () => 6000})).type.toBe<
Promise<void>
>();
expect(jest.advanceTimersByTimeAsync()).type.toRaiseError();
expect(jest.advanceTimersToNextTimer()).type.toBe<void>();
+2 -1
View File
@@ -30,7 +30,8 @@ export type FakeableAPI =
| 'setInterval'
| 'clearInterval'
| 'setTimeout'
| 'clearTimeout';
| 'clearTimeout'
| 'Temporal';
export type GlobalFakeTimersConfig = {
/**