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

feat(jest-fake-timers): Add feature to enable automatically advancing… #15300

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
27 changes: 23 additions & 4 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -674,11 +674,30 @@ type FakeableAPI =

type ModernFakeTimersConfig = {
/**
* If set to `true` all timers will be advanced automatically by 20 milliseconds
* every 20 milliseconds. A custom time delta may be provided by passing a number.
* The default is `false`.
* There are 3 different types of modes for advancing timers:
*
* - 'manual': Timers do not advance without explicit, manual calls to the tick
* APIs (`jest.advanceTimersToNextTimer`, `jest.runAllTimers`, etc). This mode is equivalent to `false`.
* - 'nextAsync': Jest will continuously await 'jest.advanceTimersToNextTimerAsync' until the mode changes.
* With this mode, jest will advance the clock to the next timer in the queue after a macrotask.
* As a result, tests can be written in a way that is independent from whether fake timers are installed.
* Tests can always be written to wait for timers to resolve, even when using fake timers.
* - 'interval': In this mode, all timers will be advanced automatically
* by the number of milliseconds provided in the delta. If the delta is
* not specified, 20 will be used by default. This mode is equivalent to `true` or providing a number for the delta.
*
* The 'nextAsync' mode differs from `interval` in two key ways:
* 1. The microtask queue is allowed to empty between each timer execution,
* as would be the case without fake timers installed.
* 1. It advances as quickly and as far as necessary. If the next timer in
* the queue is at 1000ms, it will advance 1000ms immediately whereas interval,
* without manually advancing time in the test, would take `1000 / advanceTimersMs`
* real time to reach and execute the timer.
*
* @defaultValue
* The default mode is `'manual'` (equivalent to `false`).
*/
advanceTimers?: boolean | number;
advanceTimers?: boolean | number | AdvanceTimersConfig;
/**
* List of names of APIs that should not be faked. The default is `[]`, meaning
* all APIs are faked.
Expand Down
37 changes: 33 additions & 4 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -882,11 +882,30 @@ type FakeableAPI =

type FakeTimersConfig = {
/**
* If set to `true` all timers will be advanced automatically by 20 milliseconds
* every 20 milliseconds. A custom time delta may be provided by passing a number.
* The default is `false`.
* There are 3 different types of modes for advancing timers:
*
* - 'manual': Timers do not advance without explicit, manual calls to the tick
* APIs (`jest.advanceTimersToNextTimer`, `jest.runAllTimers`, etc). This mode is equivalent to `false`.
* - 'nextAsync': Jest will continuously await 'jest.advanceTimersToNextTimerAsync' until the mode changes.
* With this mode, jest will advance the clock to the next timer in the queue after a macrotask.
* As a result, tests can be written in a way that is independent from whether fake timers are installed.
* Tests can always be written to wait for timers to resolve, even when using fake timers.
* - 'interval': In this mode, all timers will be advanced automatically
* by the number of milliseconds provided in the delta. If the delta is
* not specified, 20 will be used by default. This mode is equivalent to `true` or providing a number for the delta.
*
* The 'nextAsync' mode differs from `interval` in two key ways:
* 1. The microtask queue is allowed to empty between each timer execution,
* as would be the case without fake timers installed.
* 1. It advances as quickly and as far as necessary. If the next timer in
* the queue is at 1000ms, it will advance 1000ms immediately whereas interval,
* without manually advancing time in the test, would take `1000 / advanceTimersMs`
* real time to reach and execute the timer.
*
* @defaultValue
* The default mode is `'manual'` (equivalent to `false`).
*/
advanceTimers?: boolean | number;
advanceTimers?: boolean | number | AdvanceTimersConfig;
/**
* List of names of APIs that should not be faked. The default is `[]`, meaning
* all APIs are faked.
Expand Down Expand Up @@ -1067,10 +1086,20 @@ This means, if any timers have been scheduled (but have not yet executed), they

Returns the number of fake timers still left to run.

### `jest.setAdvanceTimers(config)`

Used to update the configured `AdvanceTimersConfig` after the fake timers have been installed. See `jest.useFakeTimers` for more information.

### `jest.now()`

Returns the time in ms of the current clock. This is equivalent to `Date.now()` if real timers are in use, or if `Date` is mocked. In other cases (such as legacy timers) it may be useful for implementing custom mocks of `Date.now()`, `performance.now()`, etc.

:::info

This function is not available when using legacy fake timers implementation.

:::

### `jest.setSystemTime(now?: number | Date)`

Set the current system time used by fake timers. Simulates a user changing the system clock while your program is running. It affects the current time but it does not in itself cause e.g. timers to fire; they will fire exactly as they would have done without the call to `jest.setSystemTime()`.
Expand Down
19 changes: 19 additions & 0 deletions packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,23 @@ export interface Jest {
* performance, time and timer APIs.
*/
useRealTimers(): Jest;
/**
* Updates the mode of advancing timers when using fake timers.
*
* @param config The configuration to use for advancing timers
*
* When mode is 'nextAsync', configures whether timers advance automatically. With automatically advancing
* timers enabled, tests can be written in a way that is independent from whether
* fake timers are installed. Tests can always be written to wait for timers to
* resolve, even when using fake timers.
*
* When mode is 'manual' (the default), timers will not advance automatically. Instead,
* timers must be advanced using APIs such as `advanceTimersToNextTimer`, `advanceTimersByTime`, etc.
*
* @remarks
* Not available when using legacy fake timers implementation.
* In addition, the mode can only be changed from 'nextAsync' to 'manual' or vice versa.
* It cannot currently be used with 'interval' from `AdvanceTimersConfig`.
*/
setAdvanceTimers(config: {mode: 'manual' | 'nextAsync'}): void;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`FakeTimers advanceTimers nextAsync warns when trying to set tick mode when already using interval 1`] = `"\`setTickMode\` cannot be used when fake timers are configured to advance at an interval."`;

exports[`FakeTimers runAllTimers warns when trying to advance timers while real timers are used 1`] = `"A function to advance timers was called but the timers APIs are not replaced with fake timers. Call \`jest.useFakeTimers()\` in this test file or enable fake timers for all tests by setting 'fakeTimers': {'enableGlobally': true} in Jest configuration file."`;
154 changes: 154 additions & 0 deletions packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,159 @@
});
});

describe('advanceTimers', () => {
let global: typeof globalThis;
let timers: FakeTimers;
beforeEach(() => {
global = {
Date,
Promise,
clearInterval,
clearTimeout,
console,
process,
setInterval,
setTimeout,
} as unknown as typeof globalThis;

timers = new FakeTimers({config: makeProjectConfig(), global});
});

afterEach(() => {
timers.clearAllTimers();
timers.dispose();
});

describe('nextAsync', () => {
beforeEach(() => {
timers.useFakeTimers({advanceTimers: {mode: 'nextAsync'}});
});

it('can always wait for a timer to execute', async () => {
const p = new Promise(resolve => {
global.setTimeout(resolve, 100);
});
await expect(p).resolves.toBeUndefined();
});

it('can mix promises inside timers', async () => {
const p = new Promise(resolve =>
global.setTimeout(async () => {
await Promise.resolve();
global.setTimeout(resolve, 100);
}, 100),
);
await expect(p).resolves.toBeUndefined();
});

it('automatically advances all timers', async () => {
const p1 = new Promise(resolve => global.setTimeout(resolve, 50));
const p2 = new Promise(resolve => global.setTimeout(resolve, 50));
const p3 = new Promise(resolve => global.setTimeout(resolve, 100));
await expect(Promise.all([p1, p2, p3])).resolves.toEqual([
undefined,
undefined,
undefined,
]);
});

it('can turn off and on auto advancing of time', async () => {
let p2Resolved = false;
const p1 = new Promise(resolve => global.setTimeout(resolve, 1));
const p2 = new Promise<void>(resolve =>
global.setTimeout(() => {
p2Resolved = true;
resolve();
}, 2),
);
const p3 = new Promise(resolve => global.setTimeout(resolve, 3));

await expect(p1).resolves.toBeUndefined();

timers.setTickMode('manual');
// wait real, unpatched time to ensure p2 doesn't resolve on its own
await new Promise(resolve => setTimeout(resolve, 5));
expect(p2Resolved).toBe(false);

// simply updating the tick mode should not result in time immediately advancing
timers.setTickMode('nextAsync');
expect(p2Resolved).toBe(false);

// wait real, unpatched time and observe p2 and p3 resolve on their own
await new Promise(resolve => setTimeout(resolve, 5));
await expect(p2).resolves.toBeUndefined();
await expect(p3).resolves.toBeUndefined();
expect(p2Resolved).toBe(true);
});

it('warns when trying to set tick mode when already using interval', () => {
const consoleWarnSpy = jest
.spyOn(console, 'warn')
.mockImplementation(() => {
// nothing
});
timers.useFakeTimers({advanceTimers: {mode: 'interval'}});
timers.setTickMode('nextAsync');
expect(
consoleWarnSpy.mock.calls[0][0].split('\nStack Trace')[0],
).toMatchSnapshot();
consoleWarnSpy.mockRestore();
});

describe('works with manual calls to async tick functions', () => {
let timerLog: number[];

Check failure on line 1354 in packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Array type using 'number[]' is forbidden. Use 'Array<number>' instead
let allTimersDone: Promise<void>;
beforeEach(() => {
timerLog = [];
allTimersDone = new Promise<void>(resolve => {
global.setTimeout(() => timerLog.push(1), 1);
global.setTimeout(() => timerLog.push(2), 2);
global.setTimeout(() => timerLog.push(3), 3);
global.setTimeout(() => {
timerLog.push(4);
global.setTimeout(() => {
timerLog.push(5);
resolve();
}, 1);
}, 5);
});
});

afterEach(async () => {
await allTimersDone;
expect(timerLog).toEqual([1, 2, 3, 4, 5]);
});

it('runAllTimersAsync', async () => {
await timers.runAllTimersAsync();
expect(timerLog).toEqual([1, 2, 3, 4, 5]);
});

it('runOnlyPendingTimersAsync', async () => {
await timers.runOnlyPendingTimersAsync();
// 5 should not resolve because it wasn't queued when we called "only pending timers"
expect(timerLog).toEqual([1, 2, 3, 4]);
});

it('advanceTimersToNextTimerAsync', async () => {
await timers.advanceTimersToNextTimerAsync();
expect(timerLog).toEqual([1]);
await timers.advanceTimersToNextTimerAsync();
expect(timerLog).toEqual([1, 2]);
await timers.advanceTimersToNextTimerAsync();
expect(timerLog).toEqual([1, 2, 3]);
});

it('advanceTimersByTimeAsync', async () => {
await timers.advanceTimersByTimeAsync(2);
expect(timerLog).toEqual([1, 2]);
await timers.advanceTimersByTimeAsync(1);
expect(timerLog).toEqual([1, 2, 3]);
});
});
});
});

describe('runAllTimersAsync', () => {
it('should advance the clock to the last scheduled timer', async () => {
const global = {
Expand All @@ -1260,6 +1413,7 @@
process,
setTimeout,
} as unknown as typeof globalThis;

const timers = new FakeTimers({config: makeProjectConfig(), global});
timers.useFakeTimers();
timers.setSystemTime(0);
Expand Down
1 change: 1 addition & 0 deletions packages/jest-fake-timers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

export {default as LegacyFakeTimers} from './legacyFakeTimers';
export {default as ModernFakeTimers} from './modernFakeTimers';
export type {TimerTickMode} from './modernFakeTimers';
Loading
Loading