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

add jest.advanceTimersToNextTimer method #8713

Merged
merged 6 commits into from
Jul 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;
/**
eranshabi marked this conversation as resolved.
Show resolved Hide resolved
* Disables automatic mocking in the module loader.
*/
Expand Down
123 changes: 122 additions & 1 deletion packages/jest-fake-timers/src/__tests__/jestFakeTimers.test.ts
Original file line number Diff line number Diff line change
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', () => {
SimenB marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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(
SimenB marked this conversation as resolved.
Show resolved Hide resolved
(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
Original file line number Diff line number Diff line change
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