Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +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]` Accept `Temporal.Duration` in `jest.advanceTimersByTime()` and `jest.advanceTimersByTimeAsync()` ([#16128](https://github.com/jestjs/jest/pull/16128))
Comment thread
SimenB marked this conversation as resolved.
Comment thread
SimenB marked this conversation as resolved.
- `[@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))
Expand Down Expand Up @@ -321,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).
14 changes: 11 additions & 3 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Comment thread
SimenB marked this conversation as resolved.
Expand Down Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions e2e/__tests__/fakeTimersTemporal.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Comment thread
SimenB marked this conversation as resolved.
});
52 changes: 52 additions & 0 deletions e2e/fake-timers-temporal/__tests__/temporal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* 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});
Comment on lines +17 to +24
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);
});

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);
});
});
3 changes: 3 additions & 0 deletions e2e/fake-timers-temporal/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "fake-timers-temporal"
}
6 changes: 6 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,12 @@ const config = defineConfig(
},
},
},
{
files: ['e2e/fake-timers-temporal/__tests__/*'],
languageOptions: {
globals: {Temporal: 'readonly'},
},
},
{
files: [
'e2e/**',
Expand Down
10 changes: 7 additions & 3 deletions packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
SimenB marked this conversation as resolved.
/**
* Advances all timers by `msToRun` milliseconds, firing callbacks if necessary.
*
* @remarks
* Not available when using legacy fake timers implementation.
*/
advanceTimersByTimeAsync(msToRun: number): Promise<void>;
advanceTimersByTimeAsync(
msToRun: number | {total(options: {unit: string}): number},
): Promise<void>;
Comment thread
SimenB marked this conversation as resolved.
Comment thread
SimenB marked this conversation as resolved.
/**
* 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`.
Expand Down Expand Up @@ -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.
Expand Down
21 changes: 21 additions & 0 deletions packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
66 changes: 66 additions & 0 deletions packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
10 changes: 6 additions & 4 deletions packages/jest-fake-timers/src/legacyFakeTimers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>) => void;

Expand Down Expand Up @@ -274,8 +275,9 @@ export default class FakeTimers<TimerRef = unknown> {
}
}

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;
Expand All @@ -288,18 +290,18 @@ export default class FakeTimers<TimerRef = unknown> {
}
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(
Expand Down
25 changes: 15 additions & 10 deletions packages/jest-fake-timers/src/modernFakeTimers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import {
} 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;
Expand Down Expand Up @@ -98,15 +104,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<void> {
async advanceTimersByTimeAsync(
msToRun: number | TemporalDurationLike,
): Promise<void> {
if (this._checkFakeTimers()) {
await this._clock.tickAsync(msToRun);
await this._clock.tickAsync(toDurationMs(msToRun));
}
}

Expand Down Expand Up @@ -149,9 +157,9 @@ export default class FakeTimers {
}
}

setSystemTime(now?: number | Date): void {
setSystemTime(now?: number | Date | TemporalEpochLike): void {
if (this._checkFakeTimers()) {
this._clock.setSystemTime(now instanceof Date ? now.getTime() : now);
this._clock.setSystemTime(toEpochMs(now));
}
}

Expand Down Expand Up @@ -226,10 +234,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],
Expand Down
27 changes: 27 additions & 0 deletions packages/jest-fake-timers/src/temporalUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* 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'});
}
Loading
Loading