Skip to content

Commit 4e552d4

Browse files
committed
feat(timers)!: expose setTickMode from fake-timers
This change exposes the `setTickMode` function from the underlying `fake-timers` library. This is to align with the new feature introduced in `fake-timers`. The new `setTickMode` method allows developers to configure whether time should auto advance and what the delta should be after the clock has been created. Prior to this, it could only be done at creation time with `shouldAdvanceTime: true`. This also adds a new mode for automatically advancing time that moves more quickly than the existing shouldAdvanceTime, which uses real time. Testing with mock clocks can often turn into a real struggle when dealing with situations where some work in the test is truly async and other work is captured by the mock clock. When using mock clocks, testers are always forced to write tests with intimate knowledge of when the mock clock needs to be ticked. It is ideal for test code to be written in a way that is independent of whether a mock clock is installed. `shouldAdvanceTime` is essentially `setInterval(() => clock.tick(ms), ms)` while this feature is `const loop = () => setTimeout(() => clock.nextAsync().then(() => loop()), 0);` There are two key differences: 1. `shouldAdvanceTime` uses `clock.tick(ms)` so it synchronously runs all timers inside the "ms" of the clock queue. This doesn't allow the microtask queue to empty between the macrotask timers in the clock. 2. `shouldAdvanceTime` uses real time to advance the same amount of real time in the mock clock. `setTickMode({mode: "nextAsync"})` advances time as quickly possible and as far as necessary. See: sinonjs/fake-timers@108efae
1 parent 6922259 commit 4e552d4

File tree

4 files changed

+155
-1
lines changed

4 files changed

+155
-1
lines changed

docs/api/vi.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,46 @@ The implementation is based internally on [`@sinonjs/fake-timers`](https://githu
10341034
But you can enable it by specifying the option in `toFake` argument: `vi.useFakeTimers({ toFake: ['nextTick', 'queueMicrotask'] })`.
10351035
:::
10361036

1037+
### vi.setTimerTickMode
1038+
1039+
- **Type:** `(mode: 'manual' | 'nextTimerAsync') => Vitest | (mode: 'interval', interval?: number) => Vitest`
1040+
1041+
Controls how fake timers are advanced.
1042+
1043+
- `manual`: The default behavior. Timers will only advance when you call one of `vi.advanceTimers...()` methods.
1044+
- `nextTimerAsync`: Timers will be advanced automatically to the next available timer after each macrotask.
1045+
- `interval`: Timers are advanced automatically by a specified interval.
1046+
1047+
When `mode` is `'interval'`, you can also provide an `interval` in milliseconds.
1048+
1049+
**Example:**
1050+
1051+
```ts
1052+
import { vi } from 'vitest'
1053+
1054+
vi.useFakeTimers()
1055+
1056+
// Manual mode (default)
1057+
vi.setTimerTickMode({ mode: 'manual' })
1058+
1059+
let i = 0
1060+
setInterval(() => console.log(++i), 50)
1061+
1062+
vi.advanceTimersByTime(150) // logs 1, 2, 3
1063+
1064+
// nextTimerAsync mode
1065+
vi.setTimerTickMode({ mode: 'nextTimerAsync' })
1066+
1067+
// Timers will advance automatically after each macrotask
1068+
await new Promise(resolve => setTimeout(resolve, 150)) // logs 4, 5, 6
1069+
1070+
// interval mode (default when 'fakeTimers.shouldAdvanceTime' is `true`)
1071+
vi.setTimerTickMode({ mode: 'interval', interval: 50 })
1072+
1073+
// Timers will advance automatically every 50ms
1074+
await new Promise(resolve => setTimeout(resolve, 150)) // logs 7, 8, 9
1075+
```
1076+
10371077
### vi.isFakeTimers {#vi-isfaketimers}
10381078

10391079
```ts

packages/vitest/src/integrations/mock/timers.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,23 @@ export class FakeTimers {
212212
return 0
213213
}
214214

215+
setTimerTickMode(mode: 'manual' | 'nextTimerAsync' | 'interval', interval?: number): void {
216+
if (this._checkFakeTimers()) {
217+
if (mode === 'manual') {
218+
this._clock.setTickMode({ mode: 'manual' })
219+
}
220+
else if (mode === 'nextTimerAsync') {
221+
this._clock.setTickMode({ mode: 'nextAsync' })
222+
}
223+
else if (mode === 'interval') {
224+
this._clock.setTickMode({ mode: 'interval', delta: interval })
225+
}
226+
else {
227+
throw new Error(`Invalid tick mode: ${mode}`)
228+
}
229+
}
230+
}
231+
215232
configure(config: FakeTimerInstallOpts): void {
216233
this._userConfig = config
217234
}

packages/vitest/src/integrations/vi.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ export interface VitestUtils {
9797
*/
9898
clearAllTimers: () => VitestUtils
9999

100+
/**
101+
* Controls how fake timers are advanced.
102+
* @param mode The mode to use for advancing timers.
103+
* - `manual`: The default behavior. Timers will only advance when you call one of `vi.advanceTimers...()` methods.
104+
* - `nextTimerAsync`: Timers will be advanced automatically to the next available timer after each macrotask.
105+
* - `interval`: Timers are advanced automatically by a specified interval.
106+
* @param interval The interval in milliseconds to use when `mode` is `'interval'`.
107+
*/
108+
setTimerTickMode: ((mode: 'manual' | 'nextTimerAsync') => VitestUtils) & ((mode: 'interval', interval?: number) => VitestUtils)
109+
100110
/**
101111
* Creates a spy on a method or getter/setter of an object similar to [`vi.fn()`](https://vitest.dev/api/vi#vi-fn). It returns a [mock function](https://vitest.dev/api/mock).
102112
* @example
@@ -553,6 +563,11 @@ function createVitest(): VitestUtils {
553563
return utils
554564
},
555565

566+
setTimerTickMode(mode: 'manual' | 'nextTimerAsync' | 'interval', interval?: number) {
567+
timers().setTimerTickMode(mode, interval)
568+
return utils
569+
},
570+
556571
// mocks
557572

558573
spyOn,

test/core/test/fixtures/timers.suite.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* LICENSE file in the root directory of https://github.com/facebook/jest.
1111
*/
1212

13-
import { afterEach, describe, expect, it, vi } from 'vitest'
13+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1414
import { FakeTimers } from '../../../../packages/vitest/src/integrations/mock/timers'
1515

1616
class FakeDate extends Date {}
@@ -1503,4 +1503,86 @@ describe('FakeTimers', () => {
15031503
timers.useRealTimers()
15041504
})
15051505
})
1506+
1507+
describe('setTimerTickMode', () => {
1508+
const realTimeout = setTimeout;
1509+
let timers: FakeTimers;
1510+
1511+
beforeEach(() => {
1512+
timers = new FakeTimers({ global })
1513+
timers.useFakeTimers();
1514+
})
1515+
afterEach(() => {
1516+
timers.useRealTimers();
1517+
})
1518+
1519+
it('can be set to manual', async () => {
1520+
const spy = vi.fn()
1521+
setTimeout(spy, 10)
1522+
1523+
timers.setTimerTickMode('manual')
1524+
await new Promise(resolve => realTimeout(resolve, 20))
1525+
1526+
expect(spy).not.toHaveBeenCalled()
1527+
1528+
timers.advanceTimersByTime(100)
1529+
expect(spy).toHaveBeenCalledOnce()
1530+
})
1531+
1532+
it('can be set to nextTimerAsync', async () => {
1533+
const spy = vi.fn()
1534+
setTimeout(spy, 10_000_000)
1535+
1536+
timers.setTimerTickMode('nextTimerAsync')
1537+
await new Promise(resolve => setTimeout(resolve, 20_000_000))
1538+
1539+
expect(spy).toHaveBeenCalledOnce()
1540+
})
1541+
1542+
it('can be set to interval', async () => {
1543+
const spy = vi.fn()
1544+
setTimeout(spy, 10)
1545+
1546+
timers.setTimerTickMode('interval', 5)
1547+
await new Promise(resolve => setTimeout(resolve, 15))
1548+
1549+
expect(spy).toHaveBeenCalledOnce()
1550+
})
1551+
1552+
it('can switch from nextTimerAsync to manual', async () => {
1553+
const spy = vi.fn()
1554+
setTimeout(spy, 10)
1555+
1556+
timers.setTimerTickMode('nextTimerAsync')
1557+
1558+
// Let one macrotask run, but the timer is not due yet.
1559+
await new Promise(resolve => setTimeout(resolve, 5))
1560+
expect(spy).not.toHaveBeenCalled()
1561+
1562+
timers.setTimerTickMode('manual')
1563+
1564+
await new Promise(resolve => realTimeout(resolve, 10))
1565+
1566+
expect(spy).not.toHaveBeenCalled()
1567+
1568+
timers.advanceTimersByTime(100)
1569+
expect(spy).toHaveBeenCalledOnce()
1570+
})
1571+
1572+
it('nextTimerAsync advances timers scheduled inside other timers', async () => {
1573+
const nestedSpy = vi.fn()
1574+
const spy = vi.fn(() => {
1575+
setTimeout(nestedSpy, 50)
1576+
})
1577+
setTimeout(spy, 100)
1578+
1579+
timers.setTimerTickMode('nextTimerAsync')
1580+
1581+
// Wait long enough for both timers to have a chance to run
1582+
await new Promise(resolve => setTimeout(resolve, 300))
1583+
1584+
expect(spy).toHaveBeenCalledOnce()
1585+
expect(nestedSpy).toHaveBeenCalledOnce()
1586+
})
1587+
})
15061588
})

0 commit comments

Comments
 (0)