diff --git a/CHANGELOG.md b/CHANGELOG.md index e92a2f5d7cc0..2cdb00e79fc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - `[jest-validate]` Allow `maxWorkers` as part of the `jest.config.js` ([#8565](https://github.com/facebook/jest/pull/8565)) - `[jest-runtime]` Allow passing configuration objects to transformers ([#7288](https://github.com/facebook/jest/pull/7288)) - `[@jest/core, @jest/test-sequencer]` Support async sort in custom `testSequencer` ([#8642](https://github.com/facebook/jest/pull/8642)) +- `[jest-runtime, @jest/fake-timers]` Add `jest.advanceTimersToNextTimer` ([#8713](https://github.com/facebook/jest/pull/8713)) - `[@jest-transform]` Extract transforming require logic within `jest-core` into `@jest-transform` ([#8756](https://github.com/facebook/jest/pull/8756)) ### Fixes diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index 33011e837fd5..6919fcb905f6 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -623,6 +623,12 @@ Executes only the macro-tasks that are currently pending (i.e., only the tasks t 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.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.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 3e23757e4ee8..3801ca1cf2cb 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -62,6 +62,11 @@ export interface Jest { * @deprecated Use `expect.extend` instead */ addMatchers(matchers: Record): 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; /** * Disables automatic mocking in the module loader. */ diff --git a/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts index 483d2a7ca5a5..d011eee66d2d 100644 --- a/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts @@ -624,7 +624,6 @@ describe('FakeTimers', () => { timers.advanceTimersByTime(100); }); - it('throws before allowing infinite recursion', () => { const global = ({process} as unknown) as NodeJS.Global; const timers = new FakeTimers({ @@ -651,6 +650,128 @@ describe('FakeTimers', () => { }); }); + describe('advanceTimersToNextTimer', () => { + it('runs timers in order', () => { + const global = ({process} as unknown) as NodeJS.Global; + const timers = new FakeTimers({ + config, + global, + moduleMocker, + timerConfig, + }); + timers.useFakeTimers(); + + const runOrder: Array = []; + const mock1 = jest.fn(() => runOrder.push('mock1')); + const mock2 = jest.fn(() => runOrder.push('mock2')); + const mock3 = jest.fn(() => runOrder.push('mock3')); + const mock4 = jest.fn(() => runOrder.push('mock4')); + + global.setTimeout(mock1, 100); + global.setTimeout(mock2, 0); + global.setTimeout(mock3, 0); + global.setInterval(() => { + mock4(); + }, 200); + + timers.advanceTimersToNextTimer(); + // Move forward to t=0 + expect(runOrder).toEqual(['mock2', 'mock3']); + + timers.advanceTimersToNextTimer(); + // Move forward to t=100 + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']); + + timers.advanceTimersToNextTimer(); + // Move forward to t=200 + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4']); + + timers.advanceTimersToNextTimer(); + // Move forward to t=400 + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4', 'mock4']); + }); + + it('run correct amount of steps', () => { + const global = ({process} as unknown) as NodeJS.Global; + const timers = new FakeTimers({ + config, + global, + moduleMocker, + timerConfig, + }); + timers.useFakeTimers(); + + const runOrder: Array = []; + const mock1 = jest.fn(() => runOrder.push('mock1')); + const mock2 = jest.fn(() => runOrder.push('mock2')); + const mock3 = jest.fn(() => runOrder.push('mock3')); + const mock4 = jest.fn(() => runOrder.push('mock4')); + + global.setTimeout(mock1, 100); + global.setTimeout(mock2, 0); + global.setTimeout(mock3, 0); + global.setInterval(() => { + mock4(); + }, 200); + + // Move forward to t=100 + timers.advanceTimersToNextTimer(2); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']); + + // Move forward to t=600 + timers.advanceTimersToNextTimer(3); + expect(runOrder).toEqual([ + 'mock2', + 'mock3', + 'mock1', + 'mock4', + 'mock4', + 'mock4', + ]); + }); + + it('setTimeout inside setTimeout', () => { + const global = ({process} as unknown) as NodeJS.Global; + const timers = new FakeTimers({ + config, + global, + moduleMocker, + timerConfig, + }); + timers.useFakeTimers(); + + const runOrder: Array = []; + const mock1 = jest.fn(() => runOrder.push('mock1')); + const mock2 = jest.fn(() => runOrder.push('mock2')); + const mock3 = jest.fn(() => runOrder.push('mock3')); + const mock4 = jest.fn(() => runOrder.push('mock4')); + + global.setTimeout(mock1, 0); + global.setTimeout(() => { + mock2(); + global.setTimeout(mock3, 50); + }, 25); + global.setTimeout(mock4, 100); + + // Move forward to t=75 + timers.advanceTimersToNextTimer(3); + expect(runOrder).toEqual(['mock1', 'mock2', 'mock3']); + }); + + it('does nothing when no timers have been scheduled', () => { + const global = ({process} as unknown) as NodeJS.Global; + const timers = new FakeTimers({ + config, + global, + moduleMocker, + timerConfig, + }); + timers.useFakeTimers(); + + timers.advanceTimersToNextTimer(); + }); + }); + describe('reset', () => { it('resets all pending setTimeouts', () => { const global = ({process} as unknown) as NodeJS.Global; diff --git a/packages/jest-fake-timers/src/jestFakeTimers.ts b/packages/jest-fake-timers/src/jestFakeTimers.ts index 83876f13f02b..aea098eadf3d 100644 --- a/packages/jest-fake-timers/src/jestFakeTimers.ts +++ b/packages/jest-fake-timers/src/jestFakeTimers.ts @@ -237,6 +237,23 @@ export default class FakeTimers { .forEach(([timerHandle]) => this._runTimerHandle(timerHandle)); } + advanceTimersToNextTimer(steps = 1) { + if (steps < 1) { + return; + } + const nextExpiry = Array.from(this._timers.values()).reduce( + (minExpiry: number | null, timer: Timer): number => { + if (minExpiry === null || timer.expiry < minExpiry) return timer.expiry; + return minExpiry; + }, + null, + ); + if (nextExpiry !== null) { + this.advanceTimersByTime(nextExpiry - this._now); + this.advanceTimersToNextTimer(steps - 1); + } + } + advanceTimersByTime(msToRun: number) { this._checkFakeTimers(); // Only run a generous number of timers and then bail. diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index e3cba4c951bd..f107a5ab077d 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -1037,6 +1037,8 @@ class Runtime { this._environment.global.jasmine.addMatchers(matchers), advanceTimersByTime: (msToRun: number) => _getFakeTimers().advanceTimersByTime(msToRun), + advanceTimersToNextTimer: (steps?: number) => + _getFakeTimers().advanceTimersToNextTimer(steps), autoMockOff: disableAutomock, autoMockOn: enableAutomock, clearAllMocks,