From ca9791df1ab44b1108a09cf54ce5beeffa0d9bed Mon Sep 17 00:00:00 2001 From: eran shabi Date: Thu, 18 Jul 2019 12:50:02 +0300 Subject: [PATCH 1/5] add jest.advanceTimersToNextTimer method --- docs/JestObjectAPI.md | 6 ++ packages/jest-environment/src/index.ts | 5 + .../src/__tests__/jestFakeTimers.test.ts | 94 +++++++++++++++++++ .../jest-fake-timers/src/jestFakeTimers.ts | 14 +++ packages/jest-runtime/src/index.ts | 2 + 5 files changed, 121 insertions(+) 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..b940df208b6e 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -65,6 +65,11 @@ export interface Jest { /** * Disables automatic mocking in the module loader. */ + advanceTimersToNextTimer(steps?: number): 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. + */ autoMockOff(): Jest; /** * Enables 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..59a6e5b76e46 100644 --- a/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts @@ -651,6 +651,100 @@ 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=200 + 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('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..56d49d88f3df 100644 --- a/packages/jest-fake-timers/src/jestFakeTimers.ts +++ b/packages/jest-fake-timers/src/jestFakeTimers.ts @@ -237,6 +237,20 @@ export default class FakeTimers { .forEach(([timerHandle]) => this._runTimerHandle(timerHandle)); } + advanceTimersToNextTimer(steps: number = 1): void { + 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 && steps > 0) { + 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..11065bf85dab 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: () => + _getFakeTimers().advanceTimersToNextTimer(), autoMockOff: disableAutomock, autoMockOn: enableAutomock, clearAllMocks, From d814b45c5438ed87717440deec210ebc33dd0c41 Mon Sep 17 00:00:00 2001 From: eran shabi Date: Thu, 18 Jul 2019 14:22:24 +0300 Subject: [PATCH 2/5] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d13a1c27a04..7c41f1cc55ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,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)) ### Fixes From 040cdab59ff7c32d58d0f805b152a38e9849a220 Mon Sep 17 00:00:00 2001 From: eran shabi Date: Thu, 18 Jul 2019 17:06:11 +0300 Subject: [PATCH 3/5] update according to review --- packages/jest-environment/src/index.ts | 6 ++-- .../src/__tests__/jestFakeTimers.test.ts | 28 +++++++++++++++++++ .../jest-fake-timers/src/jestFakeTimers.ts | 7 +++-- packages/jest-runtime/src/index.ts | 4 +-- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index b940df208b6e..3801ca1cf2cb 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -63,12 +63,12 @@ export interface Jest { */ addMatchers(matchers: Record): void; /** - * Disables automatic mocking in the module loader. + * 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 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. + * Disables automatic mocking in the module loader. */ autoMockOff(): Jest; /** diff --git a/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts index 59a6e5b76e46..a700c662c177 100644 --- a/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts @@ -625,6 +625,34 @@ describe('FakeTimers', () => { timers.advanceTimersByTime(100); }); + 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('throws before allowing infinite recursion', () => { const global = ({process} as unknown) as NodeJS.Global; const timers = new FakeTimers({ diff --git a/packages/jest-fake-timers/src/jestFakeTimers.ts b/packages/jest-fake-timers/src/jestFakeTimers.ts index 56d49d88f3df..aea098eadf3d 100644 --- a/packages/jest-fake-timers/src/jestFakeTimers.ts +++ b/packages/jest-fake-timers/src/jestFakeTimers.ts @@ -237,7 +237,10 @@ export default class FakeTimers { .forEach(([timerHandle]) => this._runTimerHandle(timerHandle)); } - advanceTimersToNextTimer(steps: number = 1): void { + 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; @@ -245,7 +248,7 @@ export default class FakeTimers { }, null, ); - if (nextExpiry !== null && steps > 0) { + if (nextExpiry !== null) { this.advanceTimersByTime(nextExpiry - this._now); this.advanceTimersToNextTimer(steps - 1); } diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 11065bf85dab..f107a5ab077d 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -1037,8 +1037,8 @@ class Runtime { this._environment.global.jasmine.addMatchers(matchers), advanceTimersByTime: (msToRun: number) => _getFakeTimers().advanceTimersByTime(msToRun), - advanceTimersToNextTimer: () => - _getFakeTimers().advanceTimersToNextTimer(), + advanceTimersToNextTimer: (steps?: number) => + _getFakeTimers().advanceTimersToNextTimer(steps), autoMockOff: disableAutomock, autoMockOn: enableAutomock, clearAllMocks, From 0a34a3cba4cc0e3e7378f0e301eef77a35bb3f71 Mon Sep 17 00:00:00 2001 From: eran shabi Date: Thu, 18 Jul 2019 17:34:10 +0300 Subject: [PATCH 4/5] update according to review --- .../src/__tests__/jestFakeTimers.test.ts | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts index a700c662c177..bd03431fad39 100644 --- a/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts @@ -624,35 +624,6 @@ describe('FakeTimers', () => { timers.advanceTimersByTime(100); }); - - 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('throws before allowing infinite recursion', () => { const global = ({process} as unknown) as NodeJS.Global; const timers = new FakeTimers({ @@ -759,6 +730,34 @@ describe('FakeTimers', () => { ]); }); + 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({ From 9274b0b705e081381b98969216f15ff016d164db Mon Sep 17 00:00:00 2001 From: eran shabi Date: Sun, 21 Jul 2019 17:51:01 +0300 Subject: [PATCH 5/5] fix comment --- packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts index bd03431fad39..d011eee66d2d 100644 --- a/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts @@ -714,7 +714,7 @@ describe('FakeTimers', () => { mock4(); }, 200); - // Move forward to t=200 + // Move forward to t=100 timers.advanceTimersToNextTimer(2); expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']);