Skip to content

Commit

Permalink
feat: add jest.advanceTimersToNextTimer method (#8713)
Browse files Browse the repository at this point in the history
  • Loading branch information
eranshabi authored and SimenB committed Jul 28, 2019
1 parent dcc1918 commit a5a1a59
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/JestObjectAPI.md
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions packages/jest-environment/src/index.ts
Expand Up @@ -62,6 +62,11 @@ export interface Jest {
* @deprecated Use `expect.extend` instead
*/
addMatchers(matchers: Record<string, any>): 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.
*/
Expand Down
123 changes: 122 additions & 1 deletion packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts
Expand Up @@ -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({
Expand All @@ -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<string> = [];
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<string> = [];
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<string> = [];
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;
Expand Down
17 changes: 17 additions & 0 deletions packages/jest-fake-timers/src/jestFakeTimers.ts
Expand Up @@ -237,6 +237,23 @@ export default class FakeTimers<TimerRef> {
.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.
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -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,
Expand Down

0 comments on commit a5a1a59

Please sign in to comment.