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
Expand Up @@ -15,6 +15,7 @@
- `[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-worker]` Support passing a URL as path to worker ([#13982](https://github.com/facebook/jest/pull/13982))
- `[jest-runtime]` Expose `@sinonjs/fake-timers` async APIs functions `advanceTimersByTimeAsync(msToRun)` (`tickAsync(msToRun)`), `advanceTimersToNextTimerAsync` (`nextAsync`), `runAllTimersAsync` (`runAllAsync`), and `runOnlyPendingTimersAsync` (`runToLastAsync`) ([#13981](https://github.com/facebook/jest/pull/13981))

### Fixes

Expand Down
48 changes: 48 additions & 0 deletions docs/JestObjectAPI.md
Expand Up @@ -921,6 +921,18 @@ 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()`

Runs all pending timers until there are none remaining.
SimenB marked this conversation as resolved.
Show resolved Hide resolved

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

:::info

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

:::

### `jest.runAllImmediates()`

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

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.

:::

### `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()`

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

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

Removes any pending timers from the timer system.
Expand Down
42 changes: 42 additions & 0 deletions packages/jest-environment/src/index.ts
Expand Up @@ -60,12 +60,30 @@ 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.
*
* @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.
*
* Also breaks the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers.
*
* @remarks
* Not available when using legacy fake timers implementation.
*/
advanceTimersToNextTimerAsync(): Promise<void>;
/**
* Disables automatic mocking in the module loader.
*/
Expand Down Expand Up @@ -298,13 +316,37 @@ 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.
*
* @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.
*
* 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
*
* @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
107 changes: 107 additions & 0 deletions packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts
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
24 changes: 24 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,24 @@ export default class FakeTimers {
}
}

async advanceTimersToNextTimerAsync(): Promise<void> {
SimenB marked this conversation as resolved.
Show resolved Hide resolved
if (this._checkFakeTimers()) {
await this._clock.nextAsync();
}
}

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
56 changes: 56 additions & 0 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -2231,8 +2231,36 @@ export default class Runtime {
const jestObject: Jest = {
advanceTimersByTime: (msToRun: number) =>
_getFakeTimers().advanceTimersByTime(msToRun),
advanceTimersByTimeAsync: async (msToRun: number): Promise<void> => {
const fakeTimers = _getFakeTimers();

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

if (
fakeTimers === this._environment.fakeTimersModern &&
typeof fakeTimers.advanceTimersToNextTimerAsync === 'function'
) {
await 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 +2323,35 @@ export default class Runtime {
},
runAllTicks: () => _getFakeTimers().runAllTicks(),
runAllTimers: () => _getFakeTimers().runAllTimers(),
runAllTimersAsync: async (): Promise<void> => {
const fakeTimers = _getFakeTimers();

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

if (
fakeTimers === this._environment.fakeTimersModern &&
typeof fakeTimers.runOnlyPendingTimersAsync === 'function'
) {
await 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