Skip to content
Open
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
40 changes: 40 additions & 0 deletions docs/api/vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,46 @@ The implementation is based internally on [`@sinonjs/fake-timers`](https://githu
But you can enable it by specifying the option in `toFake` argument: `vi.useFakeTimers({ toFake: ['nextTick', 'queueMicrotask'] })`.
:::

### vi.setTimerTickMode

- **Type:** `(mode: 'manual' | 'nextTimerAsync') => Vitest | (mode: 'interval', interval?: number) => Vitest`

Controls how fake timers are advanced.

- `manual`: The default behavior. Timers will only advance when you call one of `vi.advanceTimers...()` methods.
- `nextTimerAsync`: Timers will be advanced automatically to the next available timer after each macrotask.
- `interval`: Timers are advanced automatically by a specified interval.

When `mode` is `'interval'`, you can also provide an `interval` in milliseconds.

**Example:**

```ts
import { vi } from 'vitest'

vi.useFakeTimers()

// Manual mode (default)
vi.setTimerTickMode({ mode: 'manual' })

let i = 0
setInterval(() => console.log(++i), 50)

vi.advanceTimersByTime(150) // logs 1, 2, 3

// nextTimerAsync mode
vi.setTimerTickMode({ mode: 'nextTimerAsync' })

// Timers will advance automatically after each macrotask
await new Promise(resolve => setTimeout(resolve, 150)) // logs 4, 5, 6

// interval mode (default when 'fakeTimers.shouldAdvanceTime' is `true`)
vi.setTimerTickMode({ mode: 'interval', interval: 50 })

// Timers will advance automatically every 50ms
await new Promise(resolve => setTimeout(resolve, 150)) // logs 7, 8, 9
```

### vi.isFakeTimers {#vi-isfaketimers}

```ts
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,8 @@
]
},
"patchedDependencies": {
"@sinonjs/fake-timers@14.0.0": "patches/@sinonjs__fake-timers@14.0.0.patch",
"@sinonjs/fake-timers@15.0.0": "patches/@sinonjs__fake-timers@15.0.0.patch",
"[email protected]": "patches/[email protected]",
"@types/[email protected]": "patches/@[email protected]",
"[email protected]": "patches/[email protected]"
},
"onlyBuiltDependencies": [
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@
"@edge-runtime/vm": "^5.0.0",
"@jridgewell/trace-mapping": "catalog:",
"@opentelemetry/api": "^1.9.0",
"@sinonjs/fake-timers": "14.0.0",
"@sinonjs/fake-timers": "15.0.0",
"@types/debug": "catalog:",
"@types/estree": "catalog:",
"@types/istanbul-lib-coverage": "catalog:",
Expand All @@ -212,7 +212,7 @@
"@types/node": "^24.10.1",
"@types/picomatch": "^4.0.2",
"@types/prompts": "^2.4.9",
"@types/sinonjs__fake-timers": "^8.1.5",
"@types/sinonjs__fake-timers": "^15.0.0",
"acorn-walk": "catalog:",
"birpc": "catalog:",
"cac": "catalog:",
Expand Down
18 changes: 17 additions & 1 deletion packages/vitest/src/integrations/mock/timers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ export class FakeTimers {

runAllTicks(): void {
if (this._checkFakeTimers()) {
// @ts-expect-error method not exposed
this._clock.runMicrotasks()
}
}
Expand Down Expand Up @@ -212,6 +211,23 @@ export class FakeTimers {
return 0
}

setTimerTickMode(mode: 'manual' | 'nextTimerAsync' | 'interval', interval?: number): void {
if (this._checkFakeTimers()) {
if (mode === 'manual') {
this._clock.setTickMode({ mode: 'manual' })
}
else if (mode === 'nextTimerAsync') {
this._clock.setTickMode({ mode: 'nextAsync' })
}
else if (mode === 'interval') {
this._clock.setTickMode({ mode: 'interval', delta: interval })
}
else {
throw new Error(`Invalid tick mode: ${mode}`)
}
}
}

configure(config: FakeTimerInstallOpts): void {
this._userConfig = config
}
Expand Down
15 changes: 15 additions & 0 deletions packages/vitest/src/integrations/vi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ export interface VitestUtils {
*/
clearAllTimers: () => VitestUtils

/**
* Controls how fake timers are advanced.
* @param mode The mode to use for advancing timers.
* - `manual`: The default behavior. Timers will only advance when you call one of `vi.advanceTimers...()` methods.
* - `nextTimerAsync`: Timers will be advanced automatically to the next available timer after each macrotask.
* - `interval`: Timers are advanced automatically by a specified interval.
* @param interval The interval in milliseconds to use when `mode` is `'interval'`.
*/
setTimerTickMode: ((mode: 'manual' | 'nextTimerAsync') => VitestUtils) & ((mode: 'interval', interval?: number) => VitestUtils)

/**
* 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).
* @example
Expand Down Expand Up @@ -553,6 +563,11 @@ function createVitest(): VitestUtils {
return utils
},

setTimerTickMode(mode: 'manual' | 'nextTimerAsync' | 'interval', interval?: number) {
timers().setTimerTickMode(mode, interval)
return utils
},

// mocks

spyOn,
Expand Down
4 changes: 2 additions & 2 deletions patches/@[email protected] → patches/@[email protected]
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
diff --git a/src/fake-timers-src.js b/src/fake-timers-src.js
index 11dab90bd4bafd8c3a232df20f82ec5bcf06e76d..1f633e6293bc4bff97ccf9a23214944c0f6f8395 100644
index a9bcfd1ca..942539085 100644
--- a/src/fake-timers-src.js
+++ b/src/fake-timers-src.js
@@ -2,14 +2,14 @@
Expand All @@ -20,7 +20,7 @@ index 11dab90bd4bafd8c3a232df20f82ec5bcf06e76d..1f633e6293bc4bff97ccf9a23214944c
} catch (e) {
// ignored
}
@@ -172,7 +172,7 @@ function withGlobal(_global) {
@@ -197,7 +197,7 @@ function withGlobal(_global) {
isPresent.hrtime && typeof _global.process.hrtime.bigint === "function";
isPresent.nextTick =
_global.process && typeof _global.process.nextTick === "function";
Expand Down
42 changes: 0 additions & 42 deletions patches/@[email protected]

This file was deleted.

32 changes: 17 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 83 additions & 1 deletion test/core/test/fixtures/timers.suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* LICENSE file in the root directory of https://github.com/facebook/jest.
*/

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

class FakeDate extends Date {}
Expand Down Expand Up @@ -1503,4 +1503,86 @@ describe('FakeTimers', () => {
timers.useRealTimers()
})
})

describe('setTimerTickMode', () => {
const realTimeout = setTimeout;
let timers: FakeTimers;

beforeEach(() => {
timers = new FakeTimers({ global })
timers.useFakeTimers();
})
afterEach(() => {
timers.useRealTimers();
})

it('can be set to manual', async () => {
const spy = vi.fn()
setTimeout(spy, 10)

timers.setTimerTickMode('manual')
await new Promise(resolve => realTimeout(resolve, 20))

expect(spy).not.toHaveBeenCalled()

timers.advanceTimersByTime(100)
expect(spy).toHaveBeenCalledOnce()
})

it('can be set to nextTimerAsync', async () => {
const spy = vi.fn()
setTimeout(spy, 10_000_000)

timers.setTimerTickMode('nextTimerAsync')
await new Promise(resolve => setTimeout(resolve, 20_000_000))

expect(spy).toHaveBeenCalledOnce()
})

it('can be set to interval', async () => {
const spy = vi.fn()
setTimeout(spy, 10)

timers.setTimerTickMode('interval', 5)
await new Promise(resolve => setTimeout(resolve, 15))

expect(spy).toHaveBeenCalledOnce()
})

it('can switch from nextTimerAsync to manual', async () => {
const spy = vi.fn()
setTimeout(spy, 10)

timers.setTimerTickMode('nextTimerAsync')

// Let one macrotask run, but the timer is not due yet.
await new Promise(resolve => setTimeout(resolve, 5))
expect(spy).not.toHaveBeenCalled()

timers.setTimerTickMode('manual')

await new Promise(resolve => realTimeout(resolve, 10))

expect(spy).not.toHaveBeenCalled()

timers.advanceTimersByTime(100)
expect(spy).toHaveBeenCalledOnce()
})

it('nextTimerAsync advances timers scheduled inside other timers', async () => {
const nestedSpy = vi.fn()
const spy = vi.fn(() => {
setTimeout(nestedSpy, 50)
})
setTimeout(spy, 100)

timers.setTimerTickMode('nextTimerAsync')

// Wait long enough for both timers to have a chance to run
await new Promise(resolve => setTimeout(resolve, 300))

expect(spy).toHaveBeenCalledOnce()
expect(nestedSpy).toHaveBeenCalledOnce()
})
})
})
Loading