Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jest-runtime): expose @sinonjs/fake-timers async APIs #13981

Merged
merged 14 commits into from Mar 6, 2023
Merged
48 changes: 48 additions & 0 deletions docs/JestObjectAPI.md
Expand Up @@ -983,6 +983,54 @@ This function is not available when using legacy fake timers implementation.

:::

### `jest.nextAsync()`

Advances the clock to the the moment of the first scheduled timer.

Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.

:::info

This function is not available when using legacy fake timers implementation.

:::

### `jest.runAllAsync()`

Runs all pending timers until there are none remaining.

Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.

:::info

This function is not available when using legacy fake timers implementation.

:::

### `jest.runToLastAsync()`

Takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as necessary.

Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.

:::info

This function is not available when using legacy fake timers implementation.

:::

### `jest.tickAsync()`

Advance the clock, firing callbacks if necessary.

Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.

:::info

This function is not available when using legacy fake timers implementation.

:::

## Misc

### `jest.getSeed()`
Expand Down
46 changes: 46 additions & 0 deletions packages/jest-environment/src/index.ts
Expand Up @@ -186,6 +186,17 @@ export interface Jest {
* `isolateModulesAsync`.
*/
isolateModulesAsync(fn: () => Promise<void>): Promise<void>;
/**
* Advances the clock to the the moment of the first scheduled timer, firing it.
*
* Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.
*
* @returns
* Fake milliseconds since the unix epoch.
* @remarks
* Not available when using legacy fake timers implementation.
*/
nextAsync(): Promise<number>;
/**
* Mocks a module with an auto-mocked version when it is being required.
*/
Expand Down Expand Up @@ -281,6 +292,18 @@ export interface Jest {
numRetries: number,
options?: {logErrorsBeforeRetry?: boolean},
): Jest;
/**
* Runs all pending timers until there are none remaining.
*
* Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.
*
* @returns Fake milliseconds since the unix epoch.
* @remarks
* If new timers are added while it is executing they will be run as well.
* @remarks
* Not available when using legacy fake timers implementation.
*/
runAllAsync: () => Promise<number>;
/**
* Exhausts tasks queued by `setImmediate()`.
*
Expand All @@ -305,6 +328,17 @@ export interface Jest {
* macro-tasks, those new tasks will not be executed by this call.
*/
runOnlyPendingTimers(): void;
/**
* Takes note of the last scheduled timer when it is run, and advances the clock to
* that time firing callbacks as necessary.
*
* Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.
SimenB marked this conversation as resolved.
Show resolved Hide resolved
* @returns
* Fake milliseconds since the unix epoch.
* @remarks
* Not available when using legacy fake timers implementation.
*/
runToLastAsync: () => Promise<number>;
/**
* Explicitly supplies the mock object that the module system should return
* for the specified module.
Expand Down Expand Up @@ -344,6 +378,18 @@ export interface Jest {
* behavior from most other test libraries.
*/
spyOn: ModuleMocker['spyOn'];
/**
* Advance the clock, firing callbacks if necessary.
*
* Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.
*
* @param time How many ticks to advance by.
* @returns
* Fake milliseconds since the unix epoch.
* @remarks
* Not available when using legacy fake timers implementation.
*/
tickAsync(time: string | number): Promise<number>;
/**
* Indicates that the module system should never return a mocked version of
* the specified module from `require()` (e.g. that it should always return the
Expand Down
107 changes: 107 additions & 0 deletions packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts
Expand Up @@ -960,6 +960,113 @@ describe('FakeTimers', () => {
});
});

describe('nextAsync', () => {
it('should advance the clock at the moment of the first scheduled timer', async () => {
const global = {
Date,
Promise,
clearTimeout,
process,
setTimeout,
} as unknown as typeof globalThis;
const timers = new FakeTimers({config: makeProjectConfig(), global});
timers.useFakeTimers();
timers.setSystemTime(0);

const spy = jest.fn();
global.setTimeout(async () => {
await Promise.resolve();
global.setTimeout(spy, 100);
}, 100);

await timers.nextAsync();
expect(timers.now()).toBe(100);

await timers.nextAsync();
expect(timers.now()).toBe(200);
expect(spy).toHaveBeenCalled();
});
});

describe('runAllAsync', () => {
it('should advance the clock to the last scheduled timer', async () => {
const global = {
Date,
Promise,
clearTimeout,
process,
setTimeout,
} as unknown as typeof globalThis;
const timers = new FakeTimers({config: makeProjectConfig(), global});
timers.useFakeTimers();
timers.setSystemTime(0);

const spy = jest.fn();
const spy2 = jest.fn();
global.setTimeout(async () => {
await Promise.resolve();
global.setTimeout(spy, 100);
global.setTimeout(spy2, 200);
}, 100);

await timers.runAllAsync();
expect(timers.now()).toBe(300);
expect(spy).toHaveBeenCalled();
expect(spy2).toHaveBeenCalled();
});
});

describe('runToLastAsync', () => {
it('should advance the clock to the last scheduled timer', async () => {
const global = {
Date,
Promise,
clearTimeout,
process,
setTimeout,
} as unknown as typeof globalThis;
const timers = new FakeTimers({config: makeProjectConfig(), global});
timers.useFakeTimers();
timers.setSystemTime(0);

const spy = jest.fn();
const spy2 = jest.fn();
global.setTimeout(spy, 50);
global.setTimeout(spy2, 50);
global.setTimeout(async () => {
await Promise.resolve();
}, 100);

await timers.runToLastAsync();
expect(timers.now()).toBe(100);
expect(spy).toHaveBeenCalled();
expect(spy2).toHaveBeenCalled();
});
});

describe('tickAsync', () => {
it('should advance the clock', async () => {
const global = {
Date,
Promise,
clearTimeout,
process,
setTimeout,
} as unknown as typeof globalThis;
const timers = new FakeTimers({config: makeProjectConfig(), global});
timers.useFakeTimers();

const spy = jest.fn();
global.setTimeout(async () => {
await Promise.resolve();
global.setTimeout(spy, 100);
}, 100);

await timers.tickAsync(200);
expect(spy).toHaveBeenCalled();
});
});

describe('now', () => {
let timers: FakeTimers;
let fakedGlobal: typeof globalThis;
Expand Down
28 changes: 28 additions & 0 deletions packages/jest-fake-timers/src/modernFakeTimers.ts
Expand Up @@ -137,6 +137,34 @@ export default class FakeTimers {
return 0;
}

nextAsync(): Promise<number> {
SimenB marked this conversation as resolved.
Show resolved Hide resolved
if (this._checkFakeTimers()) {
return this._clock.nextAsync();
}
return Promise.resolve(0);
}

runAllAsync(): Promise<number> {
SimenB marked this conversation as resolved.
Show resolved Hide resolved
if (this._checkFakeTimers()) {
return this._clock.runAllAsync();
}
return Promise.resolve(0);
}

runToLastAsync(): Promise<number> {
if (this._checkFakeTimers()) {
return this._clock.runToLastAsync();
SimenB marked this conversation as resolved.
Show resolved Hide resolved
}
return Promise.resolve(0);
}

tickAsync(time: string | number): Promise<number> {
SimenB marked this conversation as resolved.
Show resolved Hide resolved
if (this._checkFakeTimers()) {
return this._clock.tickAsync(time);
}
return Promise.resolve(0);
}

private _checkFakeTimers() {
if (!this._fakingTime) {
this._global.console.warn(
Expand Down
44 changes: 44 additions & 0 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -2274,6 +2274,17 @@ export default class Runtime {
isolateModulesAsync: this.isolateModulesAsync,
mock,
mocked,
nextAsync: (): Promise<number> => {
const fakeTimers = _getFakeTimers();

if (fakeTimers === this._environment.fakeTimersModern) {
return fakeTimers.nextAsync();
} else {
throw new TypeError(
'`jest.nextAsync()` is not available when using legacy fake timers.',
);
}
},
now: () => _getFakeTimers().now(),
replaceProperty,
requireActual: moduleName => this.requireActual(from, moduleName),
Expand All @@ -2282,6 +2293,17 @@ export default class Runtime {
resetModules,
restoreAllMocks,
retryTimes,
runAllAsync: (): Promise<number> => {
const fakeTimers = _getFakeTimers();

if (fakeTimers === this._environment.fakeTimersModern) {
return fakeTimers.runAllAsync();
} else {
throw new TypeError(
'`jest.runAllAsync()` is not available when using legacy fake timers.',
);
}
},
runAllImmediates: () => {
const fakeTimers = _getFakeTimers();

Expand All @@ -2296,6 +2318,17 @@ export default class Runtime {
runAllTicks: () => _getFakeTimers().runAllTicks(),
runAllTimers: () => _getFakeTimers().runAllTimers(),
runOnlyPendingTimers: () => _getFakeTimers().runOnlyPendingTimers(),
runToLastAsync: (): Promise<number> => {
const fakeTimers = _getFakeTimers();

if (fakeTimers === this._environment.fakeTimersModern) {
return fakeTimers.runToLastAsync();
} else {
throw new TypeError(
'`jest.runToLastAsync()` is not available when using legacy fake timers.',
);
}
},
setMock: (moduleName: string, mock: unknown) =>
setMockFactory(moduleName, () => mock),
setSystemTime: (now?: number | Date) => {
Expand All @@ -2311,6 +2344,17 @@ export default class Runtime {
},
setTimeout,
spyOn,
tickAsync: (time: number | string): Promise<number> => {
const fakeTimers = _getFakeTimers();

if (fakeTimers === this._environment.fakeTimersModern) {
return fakeTimers.tickAsync(time);
} else {
throw new TypeError(
'`jest.tickAsync()` is not available when using legacy fake timers.',
);
}
},
unmock,
unstable_mockModule: mockModule,
useFakeTimers,
Expand Down
12 changes: 12 additions & 0 deletions packages/jest-types/__typetests__/jest.test.ts
Expand Up @@ -574,6 +574,18 @@ expectError(jest.useFakeTimers('modern'));
expectType<typeof jest>(jest.useRealTimers());
expectError(jest.useRealTimers(true));

expectType<Promise<number>>(jest.tickAsync(250));
expectError(jest.tickAsync());

expectType<Promise<number>>(jest.nextAsync());
expectError(jest.nextAsync('jest'));

expectType<Promise<number>>(jest.runAllAsync());
expectError(jest.runAllAsync('jest'));

expectType<Promise<number>>(jest.runToLastAsync());
expectError(jest.runToLastAsync('jest'));

// Misc

expectType<typeof jest>(jest.retryTimes(3));
Expand Down