diff --git a/CHANGELOG.md b/CHANGELOG.md index 360d7ca1cbe1..ae207ec6863d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index c012e9323d4a..0b59e124b438 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -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()`. @@ -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. diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index f503703519fe..6a99b3c7fe39 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -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; /** * 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; /** * Disables automatic mocking in the module loader. */ @@ -298,6 +314,16 @@ 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; /** * 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 @@ -305,6 +331,16 @@ export interface Jest { * 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; /** * Explicitly supplies the mock object that the module system should return * for the specified module. diff --git a/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts index 72afd53bfdb1..4ce6749bfab5 100644 --- a/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts @@ -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; diff --git a/packages/jest-fake-timers/src/modernFakeTimers.ts b/packages/jest-fake-timers/src/modernFakeTimers.ts index f1178707ec5b..c9db7ba331c0 100644 --- a/packages/jest-fake-timers/src/modernFakeTimers.ts +++ b/packages/jest-fake-timers/src/modernFakeTimers.ts @@ -52,12 +52,24 @@ export default class FakeTimers { } } + async runAllTimersAsync(): Promise { + if (this._checkFakeTimers()) { + await this._clock.runAllAsync(); + } + } + runOnlyPendingTimers(): void { if (this._checkFakeTimers()) { this._clock.runToLast(); } } + async runOnlyPendingTimersAsync(): Promise { + if (this._checkFakeTimers()) { + await this._clock.runToLastAsync(); + } + } + advanceTimersToNextTimer(steps = 1): void { if (this._checkFakeTimers()) { for (let i = steps; i > 0; i--) { @@ -72,12 +84,32 @@ export default class FakeTimers { } } + async advanceTimersToNextTimerAsync(steps = 1): Promise { + 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 { + if (this._checkFakeTimers()) { + await this._clock.tickAsync(msToRun); + } + } + runAllTicks(): void { if (this._checkFakeTimers()) { // @ts-expect-error - doesn't exist? diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 910004ca1769..5fa5186fd20b 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -2215,7 +2215,7 @@ export default class Runtime { ); }; - const setTimeout = (timeout: number) => { + const setTimeout: Jest['setTimeout'] = timeout => { this._environment.global[testTimeoutSymbol] = timeout; return jestObject; }; @@ -2229,24 +2229,56 @@ export default class Runtime { }; const jestObject: Jest = { - advanceTimersByTime: (msToRun: number) => + advanceTimersByTime: msToRun => _getFakeTimers().advanceTimersByTime(msToRun), - advanceTimersToNextTimer: (steps?: number) => + advanceTimersByTimeAsync: async msToRun => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers === this._environment.fakeTimersModern) { + // TODO: remove this check in Jest 30 + if (typeof fakeTimers.advanceTimersByTimeAsync !== 'function') { + throw new TypeError( + 'Your test environment does not support async fake timers - please ensure its Jest dependencies are updated to version 29.5 or later', + ); + } + await fakeTimers.advanceTimersByTimeAsync(msToRun); + } else { + throw new TypeError( + '`jest.advanceTimersByTimeAsync()` is not available when using legacy fake timers.', + ); + } + }, + advanceTimersToNextTimer: steps => _getFakeTimers().advanceTimersToNextTimer(steps), + advanceTimersToNextTimerAsync: async steps => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers === this._environment.fakeTimersModern) { + // TODO: remove this check in Jest 30 + if (typeof fakeTimers.advanceTimersToNextTimerAsync !== 'function') { + throw new TypeError( + 'Your test environment does not support async fake timers - please ensure its Jest dependencies are updated to version 29.5 or later', + ); + } + await fakeTimers.advanceTimersToNextTimerAsync(steps); + } else { + throw new TypeError( + '`jest.advanceTimersToNextTimerAsync()` is not available when using legacy fake timers.', + ); + } + }, autoMockOff: disableAutomock, autoMockOn: enableAutomock, clearAllMocks, clearAllTimers: () => _getFakeTimers().clearAllTimers(), - createMockFromModule: (moduleName: string) => - this._generateMock(from, moduleName), + createMockFromModule: moduleName => this._generateMock(from, moduleName), deepUnmock, disableAutomock, doMock: mock, dontMock: unmock, enableAutomock, fn, - genMockFromModule: (moduleName: string) => - this._generateMock(from, moduleName), + genMockFromModule: moduleName => this._generateMock(from, moduleName), getRealSystemTime: () => { const fakeTimers = _getFakeTimers(); @@ -2295,10 +2327,43 @@ export default class Runtime { }, runAllTicks: () => _getFakeTimers().runAllTicks(), runAllTimers: () => _getFakeTimers().runAllTimers(), + runAllTimersAsync: async () => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers === this._environment.fakeTimersModern) { + // TODO: remove this check in Jest 30 + if (typeof fakeTimers.runAllTimersAsync !== 'function') { + throw new TypeError( + 'Your test environment does not support async fake timers - please ensure its Jest dependencies are updated to version 29.5 or later', + ); + } + await fakeTimers.runAllTimersAsync(); + } else { + throw new TypeError( + '`jest.runAllTimersAsync()` is not available when using legacy fake timers.', + ); + } + }, runOnlyPendingTimers: () => _getFakeTimers().runOnlyPendingTimers(), - setMock: (moduleName: string, mock: unknown) => - setMockFactory(moduleName, () => mock), - setSystemTime: (now?: number | Date) => { + runOnlyPendingTimersAsync: async () => { + const fakeTimers = _getFakeTimers(); + + if (fakeTimers === this._environment.fakeTimersModern) { + // TODO: remove this check in Jest 30 + if (typeof fakeTimers.runOnlyPendingTimersAsync !== 'function') { + throw new TypeError( + 'Your test environment does not support async fake timers - please ensure its Jest dependencies are updated to version 29.5 or later', + ); + } + await fakeTimers.runOnlyPendingTimersAsync(); + } else { + throw new TypeError( + '`jest.runOnlyPendingTimersAsync()` is not available when using legacy fake timers.', + ); + } + }, + setMock: (moduleName, mock) => setMockFactory(moduleName, () => mock), + setSystemTime: now => { const fakeTimers = _getFakeTimers(); if (fakeTimers === this._environment.fakeTimersModern) { diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index 6a1042a45488..8415a88db114 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -488,10 +488,17 @@ expectAssignable>( expectType(jest.advanceTimersByTime(6000)); expectError(jest.advanceTimersByTime()); +expectType>(jest.advanceTimersByTimeAsync(6000)); +expectError(jest.advanceTimersByTimeAsync()); + expectType(jest.advanceTimersToNextTimer()); expectType(jest.advanceTimersToNextTimer(2)); expectError(jest.advanceTimersToNextTimer('2')); +expectType>(jest.advanceTimersToNextTimerAsync()); +expectType>(jest.advanceTimersToNextTimerAsync(2)); +expectError(jest.advanceTimersToNextTimerAsync('2')); + expectType(jest.clearAllTimers()); expectError(jest.clearAllTimers(false)); @@ -513,9 +520,15 @@ expectError(jest.runAllTicks(true)); expectType(jest.runAllTimers()); expectError(jest.runAllTimers(false)); +expectType>(jest.runAllTimersAsync()); +expectError(jest.runAllTimersAsync(false)); + expectType(jest.runOnlyPendingTimers()); expectError(jest.runOnlyPendingTimers(true)); +expectType>(jest.runOnlyPendingTimersAsync()); +expectError(jest.runOnlyPendingTimersAsync(true)); + expectType(jest.setSystemTime()); expectType(jest.setSystemTime(1483228800000)); expectType(jest.setSystemTime(Date.now()));