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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- `[jest-runtime, @jest/transform]` Allow V8 coverage provider to collect coverage from files which were not loaded explicitly ([#13974](https://github.com/facebook/jest/pull/13974))
- `[jest-snapshot]` Add support to `cts` and `mts` TypeScript files to inline snapshots ([#13975](https://github.com/facebook/jest/pull/13975))
- `[jest-worker]` Add `start` method to worker farms ([#13937](https://github.com/facebook/jest/pull/13937))
- `[jest-runtime]` Expose `@sinonjs/fake-timers` async APIs functions `advanceTimersToNextTimerAsync` (`nextAsync`), `runOnlyPendingTimersAsync` (`runToLastAsync`), `runAllTimersAsync` (`runAllAsync`), and `advanceTimersByTimeAsync(msToRun)` (`tickAsync(msToRun)`) ([#13981](https://github.com/facebook/jest/pull/13981))
SimenB marked this conversation as resolved.
Show resolved Hide resolved

### Fixes

Expand Down
48 changes: 48 additions & 0 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,54 @@ This function is not available when using legacy fake timers implementation.

:::

### `jest.advanceTimersToNextTimerAsync()`

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.runAllTimersAsync()`

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.runOnlyPendingTimersAsync()`

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.advanceTimersByTimeAsync(msToRun)`

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
49 changes: 49 additions & 0 deletions packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ export interface Jest {
* executed within this time frame will be executed.
*/
advanceTimersByTime(msToRun: number): void;
/**
* Advances all timers by `msToRun` milliseconds, firing callbacks if necessary.
*
* 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.
*/
advanceTimersByTimeAsync(msToRun: number): Promise<number>;
SimenB marked this conversation as resolved.
Show resolved Hide resolved
/**
* Advances all timers by the needed milliseconds so that only the next
* timeouts/intervals will run. Optionally, you can provide steps, so it will
Expand Down Expand Up @@ -186,6 +197,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.
*/
advanceTimersToNextTimerAsync(): Promise<number>;
SimenB marked this conversation as resolved.
Show resolved Hide resolved
/**
* Mocks a module with an auto-mocked version when it is being required.
*/
Expand Down Expand Up @@ -298,13 +320,40 @@ export interface Jest {
* and `setInterval()`).
*/
runAllTimers(): void;
/**
* Exhausts the macro-task queue (i.e., all tasks queued by `setTimeout()`
* and `setInterval()`).
*
* 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.
*/
runAllTimersAsync: () => Promise<number>;
/**
* Executes only the macro-tasks that are currently pending (i.e., only the
* tasks that have been queued by `setTimeout()` or `setInterval()` up to this
* point). If any of the currently pending macro-tasks schedule new
* macro-tasks, those new tasks will not be executed by this call.
*/
runOnlyPendingTimers(): void;
/**
* Executes only the macro-tasks that are currently pending (i.e., only the
* tasks that have been queued by `setTimeout()` or `setInterval()` up to this
* point). If any of the currently pending macro-tasks schedule new
* macro-tasks, those new tasks will not be executed by this call.
*
* 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.
*/
runOnlyPendingTimersAsync: () => Promise<number>;
/**
* Explicitly supplies the mock object that the module system should return
* for the specified module.
Expand Down
107 changes: 107 additions & 0 deletions packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,113 @@ describe('FakeTimers', () => {
});
});

describe('advanceTimersToNextTimerAsync', () => {
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.advanceTimersToNextTimerAsync();
expect(timers.now()).toBe(100);

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

describe('runAllTimersAsync', () => {
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.runAllTimersAsync();
expect(timers.now()).toBe(300);
expect(spy).toHaveBeenCalled();
expect(spy2).toHaveBeenCalled();
});
});

describe('runOnlyPendingTimersAsync', () => {
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.runOnlyPendingTimersAsync();
expect(timers.now()).toBe(100);
expect(spy).toHaveBeenCalled();
expect(spy2).toHaveBeenCalled();
});
});

describe('advanceTimersByTimeAsync', () => {
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.advanceTimersByTimeAsync(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
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,26 @@ export default class FakeTimers {
}
}

runAllTimersAsync(): Promise<number> {
if (this._checkFakeTimers()) {
return this._clock.runAllAsync();
}
return Promise.resolve(0);
}

runOnlyPendingTimers(): void {
if (this._checkFakeTimers()) {
this._clock.runToLast();
}
}

runOnlyPendingTimersAsync(): Promise<number> {
if (this._checkFakeTimers()) {
return this._clock.runToLastAsync();
}
return Promise.resolve(0);
}

advanceTimersToNextTimer(steps = 1): void {
if (this._checkFakeTimers()) {
for (let i = steps; i > 0; i--) {
Expand All @@ -72,12 +86,26 @@ export default class FakeTimers {
}
}

advanceTimersToNextTimerAsync(): Promise<number> {
if (this._checkFakeTimers()) {
return this._clock.nextAsync();
}
return Promise.resolve(0);
}

advanceTimersByTime(msToRun: number): void {
if (this._checkFakeTimers()) {
this._clock.tick(msToRun);
}
}

advanceTimersByTimeAsync(msToRun: number): Promise<number> {
if (this._checkFakeTimers()) {
return this._clock.tickAsync(msToRun);
}
return Promise.resolve(0);
}

runAllTicks(): void {
if (this._checkFakeTimers()) {
// @ts-expect-error - doesn't exist?
Expand Down
44 changes: 44 additions & 0 deletions packages/jest-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2231,8 +2231,30 @@ export default class Runtime {
const jestObject: Jest = {
advanceTimersByTime: (msToRun: number) =>
_getFakeTimers().advanceTimersByTime(msToRun),
advanceTimersByTimeAsync: (msToRun: number): Promise<number> => {
const fakeTimers = _getFakeTimers();

if (fakeTimers === this._environment.fakeTimersModern) {
return fakeTimers.advanceTimersByTimeAsync(msToRun);
SimenB marked this conversation as resolved.
Show resolved Hide resolved
} else {
throw new TypeError(
'`jest.advanceTimersByTimeAsync()` is not available when using legacy fake timers.',
);
}
},
advanceTimersToNextTimer: (steps?: number) =>
_getFakeTimers().advanceTimersToNextTimer(steps),
advanceTimersToNextTimerAsync: (): Promise<number> => {
const fakeTimers = _getFakeTimers();

if (fakeTimers === this._environment.fakeTimersModern) {
return fakeTimers.advanceTimersToNextTimerAsync();
} else {
throw new TypeError(
'`jest.advanceTimersToNextTimerAsync()` is not available when using legacy fake timers.',
);
}
},
autoMockOff: disableAutomock,
autoMockOn: enableAutomock,
clearAllMocks,
Expand Down Expand Up @@ -2295,7 +2317,29 @@ export default class Runtime {
},
runAllTicks: () => _getFakeTimers().runAllTicks(),
runAllTimers: () => _getFakeTimers().runAllTimers(),
runAllTimersAsync: (): Promise<number> => {
const fakeTimers = _getFakeTimers();

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

if (fakeTimers === this._environment.fakeTimersModern) {
return fakeTimers.runOnlyPendingTimersAsync();
} else {
throw new TypeError(
'`jest.runOnlyPendingTimersAsync()` is not available when using legacy fake timers.',
);
}
},
setMock: (moduleName: string, mock: unknown) =>
setMockFactory(moduleName, () => mock),
setSystemTime: (now?: number | Date) => {
Expand Down
12 changes: 12 additions & 0 deletions packages/jest-types/__typetests__/jest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,18 @@ expectError(jest.useFakeTimers('modern'));
expectType<typeof jest>(jest.useRealTimers());
expectError(jest.useRealTimers(true));

expectType<Promise<number>>(jest.advanceTimersByTimeAsync(250));
SimenB marked this conversation as resolved.
Show resolved Hide resolved
expectError(jest.advanceTimersByTimeAsync());

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

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

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

// Misc

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