Skip to content
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;
/**
* 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', () => {
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(
(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