mirror of
https://github.com/hanzoai/jest.git
synced 2026-09-24 16:59:57 +00:00
feat(fake-timers): support mocking Temporal.Now time (#16131)
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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'});
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
@@ -30,7 +30,8 @@ export type FakeableAPI =
|
||||
| 'setInterval'
|
||||
| 'clearInterval'
|
||||
| 'setTimeout'
|
||||
| 'clearTimeout';
|
||||
| 'clearTimeout'
|
||||
| 'Temporal';
|
||||
|
||||
export type GlobalFakeTimersConfig = {
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user