Skip to content

Commit

Permalink
feat(jest-runtime): expose @sinonjs/fake-timers async APIs (#13981)
Browse files Browse the repository at this point in the history
  • Loading branch information
lpizzinidev committed Mar 6, 2023
1 parent d27e36f commit 43988ff
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
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
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
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
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
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

0 comments on commit 43988ff

Please sign in to comment.