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 @@ -11,6 +11,7 @@
- `[jest-message-util]` Add support for [AggregateError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError) ([#13946](https://github.com/facebook/jest/pull/13946) & [#13947](https://github.com/facebook/jest/pull/13947))
- `[jest-message-util]` Add support for [Error causes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) in `test` and `it` ([#13935](https://github.com/facebook/jest/pull/13935) & [#13966](https://github.com/facebook/jest/pull/13966))
- `[jest-reporters]` Add `summaryThreshold` option to summary reporter to allow overriding the internal threshold that is used to print the summary of all failed tests when the number of test suites surpasses it ([#13895](https://github.com/facebook/jest/pull/13895))
- `[jest-runtime]` Expose `@sinonjs/fake-timers` async APIs functions `advanceTimersByTimeAsync(msToRun)` (`tickAsync(msToRun)`), `advanceTimersToNextTimerAsync(steps)` (`nextAsync`), `runAllTimersAsync` (`runAllAsync`), and `runOnlyPendingTimersAsync` (`runToLastAsync`) ([#13981](https://github.com/facebook/jest/pull/13981))
- `[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))
Expand Down
40 changes: 40 additions & 0 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,16 @@ When this API is called, all pending macro-tasks and micro-tasks will be execute

This is often useful for synchronously executing setTimeouts during a test in order to synchronously assert about some behavior that would only happen after the `setTimeout()` or `setInterval()` callbacks executed. See the [Timer mocks](TimerMocks.md) doc for more information.

### `jest.runAllTimersAsync()`

Asynchronous equivalent of `jest.runAllTimers()`. It allows any scheduled promise callbacks to execute _before_ running the timers.

:::info

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

:::

### `jest.runAllImmediates()`

Exhausts all tasks queued by `setImmediate()`.
Expand All @@ -937,18 +947,48 @@ 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.

### `jest.advanceTimersByTimeAsync(msToRun)`

Asynchronous equivalent of `jest.advanceTimersByTime(msToRun)`. It allows any scheduled promise callbacks to execute _before_ running the timers.

:::info

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

:::

### `jest.runOnlyPendingTimers()`

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.

This is useful for scenarios such as one where the module being tested schedules a `setTimeout()` whose callback schedules another `setTimeout()` recursively (meaning the scheduling never stops). In these scenarios, it's useful to be able to run forward in time by a single step at a time.

### `jest.runOnlyPendingTimersAsync()`

Asynchronous equivalent of `jest.runOnlyPendingTimers()`. It allows any scheduled promise callbacks to execute _before_ running the timers.

:::info

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

:::

### `jest.advanceTimersToNextTimer(steps)`

Advances all timers by the needed milliseconds so that only the next timeouts/intervals will run.

Optionally, you can provide `steps`, so it will run `steps` amount of next timeouts/intervals.

### `jest.advanceTimersToNextTimerAsync(steps)`

Asynchronous equivalent of `jest.advanceTimersToNextTimer(steps)`. It allows any scheduled promise callbacks to execute _before_ running the timers.

:::info

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

:::

### `jest.clearAllTimers()`

Removes any pending timers from the timer system.
Expand Down
36 changes: 36 additions & 0 deletions packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,28 @@ 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.
*
* @remarks
* Not available when using legacy fake timers implementation.
*/
advanceTimersByTimeAsync(msToRun: number): Promise<void>;
/**
* Advances all timers by the needed milliseconds so that only the next
* timeouts/intervals will run. Optionally, you can provide steps, so it will
* run steps amount of next timeouts/intervals.
*/
advanceTimersToNextTimer(steps?: number): void;
/**
* Advances the clock to the the moment of the first scheduled timer, firing it.
* Optionally, you can provide steps, so it will run steps amount of
* next timeouts/intervals.
*
* @remarks
* Not available when using legacy fake timers implementation.
*/
advanceTimersToNextTimerAsync(steps?: number): Promise<void>;
/**
* Disables automatic mocking in the module loader.
*/
Expand Down Expand Up @@ -298,13 +314,33 @@ export interface Jest {
* and `setInterval()`).
*/
runAllTimers(): void;
/**
* Exhausts the macro-task queue (i.e., all tasks queued by `setTimeout()`
* and `setInterval()`).
*
* @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<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.
*/
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.
*
* @remarks
* Not available when using legacy fake timers implementation.
*/
runOnlyPendingTimersAsync(): Promise<void>;
/**
* Explicitly supplies the mock object that the module system should return
* for the specified module.
Expand Down
131 changes: 131 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,137 @@ 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();
});

it('should advance the clock at the moment of the n-th 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(2);

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
32 changes: 32 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,24 @@ export default class FakeTimers {
}
}

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--) {
Expand All @@ -72,12 +84,32 @@ export default class FakeTimers {
}
}

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): void {
if (this._checkFakeTimers()) {
this._clock.tick(msToRun);
}
}

async advanceTimersByTimeAsync(msToRun: number): Promise<void> {
if (this._checkFakeTimers()) {
await this._clock.tickAsync(msToRun);
}
}

runAllTicks(): void {
if (this._checkFakeTimers()) {
// @ts-expect-error - doesn't exist?
Expand Down
Loading