From fce24a03b8a6c6fe02a8728cfaea71778a2edcdf Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 5 May 2026 17:47:56 +0200 Subject: [PATCH 1/7] feat(fake-timers): basic support for `Temporal` --- CHANGELOG.md | 1 + docs/JestObjectAPI.md | 14 +++- e2e/__tests__/fakeTimersTemporal.test.ts | 16 +++++ .../__tests__/temporal.test.js | 40 +++++++++++ e2e/fake-timers-temporal/package.json | 3 + eslint.config.mjs | 6 ++ packages/jest-environment/src/index.ts | 10 ++- .../src/__tests__/modernFakeTimers.test.ts | 66 +++++++++++++++++++ .../jest-fake-timers/src/modernFakeTimers.ts | 36 +++++++--- packages/jest-types/src/Config.ts | 10 ++- 10 files changed, 183 insertions(+), 19 deletions(-) create mode 100644 e2e/__tests__/fakeTimersTemporal.test.ts create mode 100644 e2e/fake-timers-temporal/__tests__/temporal.test.js create mode 100644 e2e/fake-timers-temporal/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 6342a1c2011d..c83a34988045 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - `[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)) - `[jest-config, jest-runner, jest-worker]` Add `workerGracefulExitTimeout` config option to control how long workers are given to exit before being force-killed ([#XXXX](https://github.com/jestjs/jest/pull/XXXX)) - `[jest-config]` Add support for `jest.config.mts` as a valid configuration file ([#16005](https://github.com/jestjs/jest/pull/16005)) +- `[@jest/fake-timers, jest-environment, jest-types]` Accept `Temporal.Instant` and `Temporal.ZonedDateTime` in `jest.setSystemTime()` and `useFakeTimers({now})` - `[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)) diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index 6c976c64e0da..73002d3abd9a 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -942,8 +942,12 @@ type FakeTimersConfig = { * The default is `false`. */ legacyFakeTimers?: boolean; - /** Sets current system time to be used by fake timers, in milliseconds. The default is `Date.now()`. */ - now?: number | Date; + /** + * Sets current system time to be used by fake timers. Accepts a millisecond + * timestamp, a `Date`, a `Temporal.Instant`, or a `Temporal.ZonedDateTime`. + * The default is `Date.now()`. + */ + now?: number | Date | Temporal.Instant | Temporal.ZonedDateTime; /** * The maximum number of recursive timers that will be run when calling `jest.runAllTimers()`. * The default is `100_000` timers. @@ -1050,6 +1054,8 @@ Executes only the macro task queue (i.e. all tasks queued by `setTimeout()` or ` 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. +`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. + ### `jest.advanceTimersByTimeAsync(msToRun)` 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. 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. -### `jest.setSystemTime(now?: number | Date)` +### `jest.setSystemTime(now?: number | Date | Temporal.Instant | Temporal.ZonedDateTime)` 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. + :::info This function is not available when using legacy fake timers implementation. diff --git a/e2e/__tests__/fakeTimersTemporal.test.ts b/e2e/__tests__/fakeTimersTemporal.test.ts new file mode 100644 index 000000000000..465598667915 --- /dev/null +++ b/e2e/__tests__/fakeTimersTemporal.test.ts @@ -0,0 +1,16 @@ +/** + * 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. + */ + +import {onNodeVersions} from '@jest/test-utils'; +import runJest from '../runJest'; + +onNodeVersions('>=26', () => { + test('useFakeTimers({now}) and setSystemTime accept Temporal instances', () => { + const result = runJest('fake-timers-temporal'); + expect(result.exitCode).toBe(0); + }); +}); diff --git a/e2e/fake-timers-temporal/__tests__/temporal.test.js b/e2e/fake-timers-temporal/__tests__/temporal.test.js new file mode 100644 index 000000000000..bcc25a6e4f5c --- /dev/null +++ b/e2e/fake-timers-temporal/__tests__/temporal.test.js @@ -0,0 +1,40 @@ +/** + * 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. + */ + +const DATE = new Date('2026-01-01T00:00:00Z'); +const EPOCH_MS = DATE.getTime(); +const ISO = DATE.toISOString(); + +describe('Temporal support in fake timers', () => { + afterEach(() => { + jest.useRealTimers(); + }); + + test('useFakeTimers({now}) accepts Temporal.Instant', () => { + jest.useFakeTimers({now: Temporal.Instant.from(ISO)}); + expect(Date.now()).toBe(EPOCH_MS); + }); + + test('useFakeTimers({now}) accepts Temporal.ZonedDateTime', () => { + const zdt = Temporal.Instant.from(ISO).toZonedDateTimeISO('UTC'); + jest.useFakeTimers({now: zdt}); + expect(Date.now()).toBe(EPOCH_MS); + }); + + test('setSystemTime accepts Temporal.Instant', () => { + jest.useFakeTimers(); + jest.setSystemTime(Temporal.Instant.from(ISO)); + expect(Date.now()).toBe(EPOCH_MS); + }); + + test('setSystemTime accepts Temporal.ZonedDateTime', () => { + jest.useFakeTimers(); + const zdt = Temporal.Instant.from(ISO).toZonedDateTimeISO('UTC'); + jest.setSystemTime(zdt); + expect(Date.now()).toBe(EPOCH_MS); + }); +}); diff --git a/e2e/fake-timers-temporal/package.json b/e2e/fake-timers-temporal/package.json new file mode 100644 index 000000000000..4c71df47098a --- /dev/null +++ b/e2e/fake-timers-temporal/package.json @@ -0,0 +1,3 @@ +{ + "name": "fake-timers-temporal" +} diff --git a/eslint.config.mjs b/eslint.config.mjs index d419c2325e7d..aaa99f8f791e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -681,6 +681,12 @@ const config = defineConfig( }, }, }, + { + files: ['e2e/fake-timers-temporal/__tests__/*'], + languageOptions: { + globals: {Temporal: 'readonly'}, + }, + }, { files: [ 'e2e/**', diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index e6d47e5aac69..f01ec68a354d 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -59,14 +59,18 @@ 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): void; + advanceTimersByTime( + msToRun: number | {total(options: {unit: string}): number}, + ): void; /** * Advances all timers by `msToRun` milliseconds, firing callbacks if necessary. * * @remarks * Not available when using legacy fake timers implementation. */ - advanceTimersByTimeAsync(msToRun: number): Promise; + advanceTimersByTimeAsync( + msToRun: number | {total(options: {unit: string}): number}, + ): Promise; /** * Advances all timers by the needed milliseconds to execute callbacks currently scheduled with `requestAnimationFrame`. * `advanceTimersToNextFrame()` is a helpful way to execute code that is scheduled using `requestAnimationFrame`. @@ -375,7 +379,7 @@ export interface Jest { * @remarks * Not available when using legacy fake timers implementation. */ - setSystemTime(now?: number | Date): void; + setSystemTime(now?: number | Date | {epochMilliseconds: number}): void; /** * Set the default timeout interval for tests and before/after hooks in * milliseconds. diff --git a/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts index a9dfbf91bf09..1937a51722cf 100644 --- a/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts @@ -1520,4 +1520,70 @@ describe('FakeTimers', () => { expect(now).toBeLessThanOrEqual(after); }); }); + + describe('Temporal', () => { + const epoch_ms = new Date('2026-01-01T00:00:00Z').getTime(); + let timers: FakeTimers; + let fakedGlobal: typeof globalThis; + + beforeEach(() => { + fakedGlobal = { + Date, + clearInterval, + clearTimeout, + process, + setInterval, + setTimeout, + } as unknown as typeof globalThis; + timers = new FakeTimers({ + config: makeProjectConfig(), + global: fakedGlobal, + }); + }); + + afterEach(() => { + timers.useRealTimers(); + }); + + it('setSystemTime accepts an object with epochMilliseconds', () => { + timers.useFakeTimers(); + timers.setSystemTime({epochMilliseconds: epoch_ms}); + expect(fakedGlobal.Date.now()).toBe(epoch_ms); + }); + + it('useFakeTimers({now}) accepts an object with epochMilliseconds', () => { + timers.useFakeTimers({now: {epochMilliseconds: epoch_ms}}); + expect(fakedGlobal.Date.now()).toBe(epoch_ms); + }); + + it('advanceTimersByTime accepts an object with total()', () => { + timers.useFakeTimers({now: 0}); + timers.advanceTimersByTime({ + total: ({unit}) => (unit === 'millisecond' ? 5000 : 5), + }); + expect(fakedGlobal.Date.now()).toBe(5000); + }); + + it('advanceTimersByTimeAsync accepts an object with total()', async () => { + const asyncGlobal = { + Date, + Promise, + clearInterval, + clearTimeout, + process, + setInterval, + setTimeout, + } as unknown as typeof globalThis; + const asyncTimers = new FakeTimers({ + config: makeProjectConfig(), + global: asyncGlobal, + }); + asyncTimers.useFakeTimers({now: 0}); + await asyncTimers.advanceTimersByTimeAsync({ + total: ({unit}) => (unit === 'millisecond' ? 5000 : 5), + }); + expect(asyncGlobal.Date.now()).toBe(5000); + asyncTimers.useRealTimers(); + }); + }); }); diff --git a/packages/jest-fake-timers/src/modernFakeTimers.ts b/packages/jest-fake-timers/src/modernFakeTimers.ts index d8ee32caad28..9e3aa1bc66d9 100644 --- a/packages/jest-fake-timers/src/modernFakeTimers.ts +++ b/packages/jest-fake-timers/src/modernFakeTimers.ts @@ -15,6 +15,23 @@ import { import type {Config} from '@jest/types'; import {formatStackTrace} from 'jest-message-util'; +type TemporalLike = {epochMilliseconds: number}; +type TemporalDurationLike = {total(options: {unit: string}): number}; + +function toEpochMs( + value: number | Date | TemporalLike | undefined, +): number | undefined { + if (value == null) return undefined; + if (typeof value === 'number') return value; + if (value instanceof Date) return value.getTime(); + return value.epochMilliseconds; +} + +function toDurationMs(value: number | TemporalDurationLike): number { + if (typeof value === 'number') return value; + return value.total({unit: 'millisecond'}); +} + export default class FakeTimers { private _clock!: InstalledClock; private readonly _config: Config.ProjectConfig; @@ -98,15 +115,17 @@ export default class FakeTimers { } } - advanceTimersByTime(msToRun: number): void { + advanceTimersByTime(msToRun: number | TemporalDurationLike): void { if (this._checkFakeTimers()) { - this._clock.tick(msToRun); + this._clock.tick(toDurationMs(msToRun)); } } - async advanceTimersByTimeAsync(msToRun: number): Promise { + async advanceTimersByTimeAsync( + msToRun: number | TemporalDurationLike, + ): Promise { if (this._checkFakeTimers()) { - await this._clock.tickAsync(msToRun); + await this._clock.tickAsync(toDurationMs(msToRun)); } } @@ -149,9 +168,9 @@ export default class FakeTimers { } } - setSystemTime(now?: number | Date): void { + setSystemTime(now?: number | Date | TemporalLike): void { if (this._checkFakeTimers()) { - this._clock.setSystemTime(now instanceof Date ? now.getTime() : now); + this._clock.setSystemTime(toEpochMs(now)); } } @@ -226,10 +245,7 @@ export default class FakeTimers { return { advanceTimeDelta, loopLimit: fakeTimersConfig.timerLimit || 100_000, - now: - fakeTimersConfig.now instanceof Date - ? fakeTimersConfig.now.getTime() - : (fakeTimersConfig.now ?? Date.now()), + now: toEpochMs(fakeTimersConfig.now) ?? Date.now(), shouldAdvanceTime: Boolean(fakeTimersConfig.advanceTimers), shouldClearNativeTimers: true, toFake: [...toFake], diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index 931141a912f9..ebc5367deec3 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -61,12 +61,13 @@ export type FakeTimersConfig = { */ doNotFake?: Array; /** - * Sets current system time to be used by fake timers, in milliseconds. + * Sets current system time to be used by fake timers. Accepts a millisecond + * timestamp, a `Date`, a `Temporal.Instant`, or a `Temporal.ZonedDateTime`. * * @defaultValue * The default is `Date.now()`. */ - now?: number | Date; + now?: number | Date | {epochMilliseconds: number}; /** * The maximum number of recursive timers that will be run when calling * `jest.runAllTimers()`. @@ -99,7 +100,10 @@ export type LegacyFakeTimersConfig = { type FakeTimers = GlobalFakeTimersConfig & ( | (FakeTimersConfig & { - now?: Exclude; + now?: Exclude< + FakeTimersConfig['now'], + Date | {epochMilliseconds: number} + >; }) | LegacyFakeTimersConfig ); From ba049699786c39e68313dc0d0b7a9f5acb6cc601 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 5 May 2026 17:58:24 +0200 Subject: [PATCH 2/7] changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c83a34988045..13e1fe2a9839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ - `[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)) - `[jest-config, jest-runner, jest-worker]` Add `workerGracefulExitTimeout` config option to control how long workers are given to exit before being force-killed ([#XXXX](https://github.com/jestjs/jest/pull/XXXX)) - `[jest-config]` Add support for `jest.config.mts` as a valid configuration file ([#16005](https://github.com/jestjs/jest/pull/16005)) -- `[@jest/fake-timers, jest-environment, jest-types]` Accept `Temporal.Instant` and `Temporal.ZonedDateTime` in `jest.setSystemTime()` and `useFakeTimers({now})` +- `[@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-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)) @@ -322,4 +323,4 @@ ## Older Changelog Entries -For newer CHANGELOG entries see [`CHANGELOG_PRE_v30.md`](CHANGELOG_PRE_v30.md). +For older CHANGELOG entries see [`CHANGELOG_PRE_v30.md`](CHANGELOG_PRE_v30.md). From ae6126a49f651c4d56d69c120fcd51c42ed116bb Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 5 May 2026 18:26:29 +0200 Subject: [PATCH 3/7] address review --- .../jest-fake-timers/src/legacyFakeTimers.ts | 10 +++++--- .../jest-fake-timers/src/modernFakeTimers.ts | 25 ++++++------------- .../jest-fake-timers/src/temporalUtils.ts | 23 +++++++++++++++++ 3 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 packages/jest-fake-timers/src/temporalUtils.ts diff --git a/packages/jest-fake-timers/src/legacyFakeTimers.ts b/packages/jest-fake-timers/src/legacyFakeTimers.ts index 87fd301efb2d..72cb533b772b 100644 --- a/packages/jest-fake-timers/src/legacyFakeTimers.ts +++ b/packages/jest-fake-timers/src/legacyFakeTimers.ts @@ -16,6 +16,7 @@ import type { UnknownFunction, } from 'jest-mock'; import {setGlobal} from 'jest-util'; +import {type TemporalDurationLike, toDurationMs} from './temporalUtils'; type Callback = (...args: Array) => void; @@ -274,8 +275,9 @@ export default class FakeTimers { } } - advanceTimersByTime(msToRun: number): void { + advanceTimersByTime(msToRun: number | TemporalDurationLike): void { this._checkFakeTimers(); + let msRemaining = toDurationMs(msToRun); // Only run a generous number of timers and then bail. // This is just to help avoid recursive loops let i; @@ -288,18 +290,18 @@ export default class FakeTimers { } const [timerHandle, nextTimerExpiry] = timerHandleAndExpiry; - if (this._now + msToRun < nextTimerExpiry) { + if (this._now + msRemaining < nextTimerExpiry) { // There are no timers between now and the target we're running to break; } else { - msToRun -= nextTimerExpiry - this._now; + msRemaining -= nextTimerExpiry - this._now; this._now = nextTimerExpiry; this._runTimerHandle(timerHandle); } } // Advance the clock by whatever time we still have left to run - this._now += msToRun; + this._now += msRemaining; if (i === this._maxLoops) { throw new Error( diff --git a/packages/jest-fake-timers/src/modernFakeTimers.ts b/packages/jest-fake-timers/src/modernFakeTimers.ts index 9e3aa1bc66d9..da0c316b5e5a 100644 --- a/packages/jest-fake-timers/src/modernFakeTimers.ts +++ b/packages/jest-fake-timers/src/modernFakeTimers.ts @@ -14,23 +14,12 @@ import { } from '@sinonjs/fake-timers'; import type {Config} from '@jest/types'; import {formatStackTrace} from 'jest-message-util'; - -type TemporalLike = {epochMilliseconds: number}; -type TemporalDurationLike = {total(options: {unit: string}): number}; - -function toEpochMs( - value: number | Date | TemporalLike | undefined, -): number | undefined { - if (value == null) return undefined; - if (typeof value === 'number') return value; - if (value instanceof Date) return value.getTime(); - return value.epochMilliseconds; -} - -function toDurationMs(value: number | TemporalDurationLike): number { - if (typeof value === 'number') return value; - return value.total({unit: 'millisecond'}); -} +import { + type TemporalDurationLike, + type TemporalInstantLike, + toDurationMs, + toEpochMs, +} from './temporalUtils'; export default class FakeTimers { private _clock!: InstalledClock; @@ -168,7 +157,7 @@ export default class FakeTimers { } } - setSystemTime(now?: number | Date | TemporalLike): void { + setSystemTime(now?: number | Date | TemporalInstantLike): void { if (this._checkFakeTimers()) { this._clock.setSystemTime(toEpochMs(now)); } diff --git a/packages/jest-fake-timers/src/temporalUtils.ts b/packages/jest-fake-timers/src/temporalUtils.ts new file mode 100644 index 000000000000..39ce0cd47624 --- /dev/null +++ b/packages/jest-fake-timers/src/temporalUtils.ts @@ -0,0 +1,23 @@ +/** + * 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 TemporalInstantLike = {epochMilliseconds: number}; +export type TemporalDurationLike = {total(options: {unit: string}): number}; + +export function toEpochMs( + value: number | Date | TemporalInstantLike | undefined, +): number | undefined { + if (value == null) return undefined; + if (typeof value === 'number') return value; + if (value instanceof Date) return value.getTime(); + return value.epochMilliseconds; +} + +export function toDurationMs(value: number | TemporalDurationLike): number { + if (typeof value === 'number') return value; + return value.total({unit: 'millisecond'}); +} From bc221e7ba4a9e3620e69bf3240cfc30dd846bafb Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 5 May 2026 18:36:05 +0200 Subject: [PATCH 4/7] fix ci --- .../src/__tests__/legacyFakeTimers.test.ts | 21 +++++++++++++++++++ .../jest-fake-timers/src/temporalUtils.ts | 8 +++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts index fe4b59475ef3..b9e34ef25d22 100644 --- a/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts @@ -1636,4 +1636,25 @@ describe('FakeTimers', () => { expect(now).toBeLessThanOrEqual(after); }); }); + + describe('Temporal', () => { + let timers: FakeTimers; + + beforeEach(() => { + const global = {process} as unknown as typeof globalThis; + timers = new FakeTimers({config, global, moduleMocker, timerConfig}); + timers.useFakeTimers(); + }); + + afterEach(() => { + timers.useRealTimers(); + }); + + it('advanceTimersByTime accepts an object with total()', () => { + timers.advanceTimersByTime({ + total: ({unit}) => (unit === 'millisecond' ? 5000 : 5), + }); + expect(timers.now()).toBe(5000); + }); + }); }); diff --git a/packages/jest-fake-timers/src/temporalUtils.ts b/packages/jest-fake-timers/src/temporalUtils.ts index 39ce0cd47624..9a7425ca4708 100644 --- a/packages/jest-fake-timers/src/temporalUtils.ts +++ b/packages/jest-fake-timers/src/temporalUtils.ts @@ -13,8 +13,12 @@ export function toEpochMs( ): number | undefined { if (value == null) return undefined; if (typeof value === 'number') return value; - if (value instanceof Date) return value.getTime(); - return value.epochMilliseconds; + // 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 TemporalInstantLike).epochMilliseconds; } export function toDurationMs(value: number | TemporalDurationLike): number { From fb4b089c2501075eea4722ff197ba1422d82a5e7 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 5 May 2026 19:45:52 +0200 Subject: [PATCH 5/7] type tests --- .../jest-types/__typetests__/jest.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index 099eecdc4fe9..0189ab3e109b 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -536,9 +536,17 @@ expect>().type.toBeAssignableFrom( // Mock Timers expect(jest.advanceTimersByTime(6000)).type.toBe(); +// expect(jest.advanceTimersByTime(Temporal.Duration.from(''))).type.toBe(); +expect(jest.advanceTimersByTime({total: () => 6000})).type.toBe(); expect(jest.advanceTimersByTime()).type.toRaiseError(); expect(jest.advanceTimersByTimeAsync(6000)).type.toBe>(); +// expect(jest.advanceTimersByTimeAsync(Temporal.Duration.from(''))).type.toBe< + Promise +>(); +expect(jest.advanceTimersByTimeAsync({total: () => 6000})).type.toBe< + Promise +>(); expect(jest.advanceTimersByTimeAsync()).type.toRaiseError(); expect(jest.advanceTimersToNextTimer()).type.toBe(); @@ -587,7 +595,17 @@ expect(jest.setSystemTime()).type.toBe(); expect(jest.setSystemTime(1_483_228_800_000)).type.toBe(); expect(jest.setSystemTime(Date.now())).type.toBe(); expect(jest.setSystemTime(new Date(1995, 11, 17))).type.toBe(); +// Temporal-shaped input (duck-typed as {epochMilliseconds: number}) +// TODO: add these two once we are on TypeScript v6 +// expect(jest.setSystemTime(Temporal.Now.instant())).type.toBe(); +// expect(jest.setSystemTime(Temporal.Now.zonedDateTimeISO())).type.toBe(); +expect( + jest.setSystemTime({epochMilliseconds: 1_483_228_800_000}), +).type.toBe(); expect(jest.setSystemTime('1995-12-17T03:24:00')).type.toRaiseError(); +expect( + jest.setSystemTime({epochMilliseconds: 'not a number'}), +).type.toRaiseError(); expect(jest.useFakeTimers()).type.toBe(); @@ -639,6 +657,12 @@ expect(jest.useFakeTimers({now: Date.now()})).type.toBe(); expect(jest.useFakeTimers({now: new Date(1995, 11, 17)})).type.toBe< typeof jest >(); +expect( + jest.useFakeTimers({now: {epochMilliseconds: 1_483_228_800_000}}), +).type.toBe(); +expect( + jest.useFakeTimers({now: {epochMilliseconds: 'not a number'}}), +).type.toRaiseError(); expect(jest.useFakeTimers({now: '1995-12-17T03:24:00'})).type.toRaiseError(); expect(jest.useFakeTimers({timerLimit: 1000})).type.toBe(); From 06d3c2666163cddc1866f854bd41fb1b1446a991 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 5 May 2026 20:00:20 +0200 Subject: [PATCH 6/7] oops --- packages/jest-types/__typetests__/jest.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index 0189ab3e109b..1046540e844b 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -542,8 +542,8 @@ expect(jest.advanceTimersByTime()).type.toRaiseError(); expect(jest.advanceTimersByTimeAsync(6000)).type.toBe>(); // expect(jest.advanceTimersByTimeAsync(Temporal.Duration.from(''))).type.toBe< - Promise ->(); +// Promise +// >(); expect(jest.advanceTimersByTimeAsync({total: () => 6000})).type.toBe< Promise >(); From 021cd99485a0ec2afb5935fdb6c4d62848370d4f Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 5 May 2026 20:00:30 +0200 Subject: [PATCH 7/7] review comments --- e2e/fake-timers-temporal/__tests__/temporal.test.js | 12 ++++++++++++ packages/jest-fake-timers/src/modernFakeTimers.ts | 4 ++-- packages/jest-fake-timers/src/temporalUtils.ts | 6 +++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/e2e/fake-timers-temporal/__tests__/temporal.test.js b/e2e/fake-timers-temporal/__tests__/temporal.test.js index bcc25a6e4f5c..6e0de8cd7510 100644 --- a/e2e/fake-timers-temporal/__tests__/temporal.test.js +++ b/e2e/fake-timers-temporal/__tests__/temporal.test.js @@ -37,4 +37,16 @@ describe('Temporal support in fake timers', () => { jest.setSystemTime(zdt); expect(Date.now()).toBe(EPOCH_MS); }); + + test('advanceTimersByTime accepts Temporal.Duration', () => { + jest.useFakeTimers({now: EPOCH_MS}); + jest.advanceTimersByTime(Temporal.Duration.from({hours: 1})); + expect(Date.now()).toBe(EPOCH_MS + 3_600_000); + }); + + test('advanceTimersByTimeAsync accepts Temporal.Duration', async () => { + jest.useFakeTimers({now: EPOCH_MS}); + await jest.advanceTimersByTimeAsync(Temporal.Duration.from({minutes: 30})); + expect(Date.now()).toBe(EPOCH_MS + 1_800_000); + }); }); diff --git a/packages/jest-fake-timers/src/modernFakeTimers.ts b/packages/jest-fake-timers/src/modernFakeTimers.ts index da0c316b5e5a..71743d56084e 100644 --- a/packages/jest-fake-timers/src/modernFakeTimers.ts +++ b/packages/jest-fake-timers/src/modernFakeTimers.ts @@ -16,7 +16,7 @@ import type {Config} from '@jest/types'; import {formatStackTrace} from 'jest-message-util'; import { type TemporalDurationLike, - type TemporalInstantLike, + type TemporalEpochLike, toDurationMs, toEpochMs, } from './temporalUtils'; @@ -157,7 +157,7 @@ export default class FakeTimers { } } - setSystemTime(now?: number | Date | TemporalInstantLike): void { + setSystemTime(now?: number | Date | TemporalEpochLike): void { if (this._checkFakeTimers()) { this._clock.setSystemTime(toEpochMs(now)); } diff --git a/packages/jest-fake-timers/src/temporalUtils.ts b/packages/jest-fake-timers/src/temporalUtils.ts index 9a7425ca4708..c6bfa3feb97c 100644 --- a/packages/jest-fake-timers/src/temporalUtils.ts +++ b/packages/jest-fake-timers/src/temporalUtils.ts @@ -5,11 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -export type TemporalInstantLike = {epochMilliseconds: number}; +export type TemporalEpochLike = {epochMilliseconds: number}; export type TemporalDurationLike = {total(options: {unit: string}): number}; export function toEpochMs( - value: number | Date | TemporalInstantLike | undefined, + value: number | Date | TemporalEpochLike | undefined, ): number | undefined { if (value == null) return undefined; if (typeof value === 'number') return value; @@ -18,7 +18,7 @@ export function toEpochMs( // across module boundaries in a webpack bundle). if (typeof (value as Date).getTime === 'function') return (value as Date).getTime(); - return (value as TemporalInstantLike).epochMilliseconds; + return (value as TemporalEpochLike).epochMilliseconds; } export function toDurationMs(value: number | TemporalDurationLike): number {