Files

250 lines
6.1 KiB
TypeScript

/**
* 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 {
type FakeTimers as FakeTimerWithContext,
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';
export default class FakeTimers {
private _clock!: InstalledClock;
private readonly _config: Config.ProjectConfig;
private _fakingTime: boolean;
private readonly _global: typeof globalThis;
private readonly _fakeTimers: FakeTimerWithContext;
constructor({
global,
config,
}: {
global: typeof globalThis;
config: Config.ProjectConfig;
}) {
this._global = global;
this._config = config;
this._fakingTime = false;
this._fakeTimers = withGlobal(global);
}
clearAllTimers(): void {
if (this._fakingTime) {
this._clock.reset();
}
}
dispose(): void {
this.useRealTimers();
}
runAllTimers(): void {
if (this._checkFakeTimers()) {
this._clock.runAll();
}
}
async runAllTimersAsync(): Promise<void> {
if (this._checkFakeTimers()) {
await this._clock.runAllAsync();
}
}
runOnlyPendingTimers(): void {
if (this._checkFakeTimers()) {
this._clock.runToLast();
}
}
async runOnlyPendingTimersAsync(): Promise<void> {
if (this._checkFakeTimers()) {
await this._clock.runToLastAsync();
}
}
advanceTimersToNextTimer(steps = 1): void {
if (this._checkFakeTimers()) {
for (let i = steps; i > 0; i--) {
this._clock.next();
// Fire all timers at this point: https://github.com/sinonjs/fake-timers/issues/250
this._clock.tick(0);
if (this._clock.countTimers() === 0) {
break;
}
}
}
}
async advanceTimersToNextTimerAsync(steps = 1): Promise<void> {
if (this._checkFakeTimers()) {
for (let i = steps; i > 0; i--) {
await this._clock.nextAsync();
// Fire all timers at this point: https://github.com/sinonjs/fake-timers/issues/250
await this._clock.tickAsync(0);
if (this._clock.countTimers() === 0) {
break;
}
}
}
}
advanceTimersByTime(msToRun: number | TemporalDuration): void {
if (this._checkFakeTimers()) {
// 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 | TemporalDuration,
): Promise<void> {
if (this._checkFakeTimers()) {
// 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'}),
);
}
}
advanceTimersToNextFrame(): void {
if (this._checkFakeTimers()) {
this._clock.runToFrame();
}
}
runAllTicks(): void {
if (this._checkFakeTimers()) {
this._clock.runMicrotasks();
}
}
useRealTimers(): void {
if (this._fakingTime) {
this._clock.uninstall();
this._fakingTime = false;
}
}
useFakeTimers(fakeTimersConfig?: Config.FakeTimersConfig): void {
if (this._fakingTime) {
this._clock.uninstall();
}
this._clock = this._fakeTimers.install(
this._toSinonFakeTimersConfig(fakeTimersConfig),
);
this._fakingTime = true;
}
reset(): void {
if (this._checkFakeTimers()) {
const {now} = this._clock;
this._clock.reset();
this._clock.setSystemTime(now);
}
}
setSystemTime(now?: number | Date | TemporalTimelike): void {
if (this._checkFakeTimers()) {
this._clock.setSystemTime(now);
}
}
setTimerTickMode(tickModeConfig: {
mode: 'interval' | 'manual' | 'nextAsync';
delta?: number;
}): void {
if (this._checkFakeTimers()) {
this._clock.setTickMode(tickModeConfig);
}
}
getRealSystemTime(): number {
return Date.now();
}
now(): number {
if (this._fakingTime) {
return this._clock.now;
}
return Date.now();
}
getTimerCount(): number {
if (this._checkFakeTimers()) {
return this._clock.countTimers();
}
return 0;
}
private _checkFakeTimers() {
if (!this._fakingTime) {
this._global.console.warn(
'A function to advance timers was called but the timers APIs are not replaced ' +
'with fake timers. Call `jest.useFakeTimers()` in this test file or enable ' +
"fake timers for all tests by setting 'fakeTimers': {'enableGlobally': true} " +
`in Jest configuration file.\nStack Trace:\n${formatStackTrace(
// eslint-disable-next-line unicorn/error-message
new Error().stack!,
this._config,
{noStackTrace: false},
)}`,
);
}
return this._fakingTime;
}
private _toSinonFakeTimersConfig(
fakeTimersConfig: Config.FakeTimersConfig = {},
): SinonFakeTimersConfig {
fakeTimersConfig = {
...this._config.fakeTimers,
...fakeTimersConfig,
} as Config.FakeTimersConfig;
const advanceTimeDelta =
typeof fakeTimersConfig.advanceTimers === 'number'
? fakeTimersConfig.advanceTimers
: undefined;
const toFake = new Set(
Object.keys(this._fakeTimers.timers) as Array<FakeableAPI>,
);
if (fakeTimersConfig.doNotFake)
for (const nameOfFakeableAPI of fakeTimersConfig.doNotFake) {
toFake.delete(nameOfFakeableAPI);
}
return {
advanceTimeDelta,
loopLimit: fakeTimersConfig.timerLimit || 100_000,
now: fakeTimersConfig.now ?? Date.now(),
shouldAdvanceTime: Boolean(fakeTimersConfig.advanceTimers),
shouldClearNativeTimers: true,
toFake: [...toFake],
};
}
}