diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index f7dba6ec7..3245517f5 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -96,9 +96,13 @@ export default defineConfig({ link: '/guide/features' }, { - text: 'Mocking', + text: 'Mocking Functions', link: '/guide/mocking' }, + { + text: 'Mocking Timers', + link: '/guide/mocking-timers' + }, /* TODO { text: 'Using Plugins', diff --git a/docs/guide/features.md b/docs/guide/features.md index 031815cb1..8d79dec3a 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -160,21 +160,23 @@ You can also use `.skip`, `.only`, and `.todo` with concurrent suites and tests. ## Mocking -[Tinyspy](https://github.com/Aslemammad/tinyspy) built-in for mocking with `jest` compatible APIs on global `vi` object. +[Tinyspy](https://github.com/Aslemammad/tinyspy) built-in for mocking with `jest` compatible APIs on `vi` object. ```ts +import { vi } from 'vitest' + const fn = vi.fn() fn('hello', 1) -expect(vi.isMockFunction()).toBe(true) +expect(vi.isMockFunction(fn)).toBe(true) expect(fn.mock.calls[0]).toEqual(['hello', 1]) fn.mockImplementation((arg) => arg) fn('world', 2) -expect(fn.mock.returns[0]).toBe('world') +expect(fn.mock.returns[1]).toBe('world') ``` Vitest supports both [happy-dom](https://github.com/capricorn86/happy-dom) or [jsdom](https://github.com/jsdom/jsdom) for mocking DOM and browser APIs. They don't come with Vitest, you might need to install them: diff --git a/docs/guide/mocking-timers.md b/docs/guide/mocking-timers.md new file mode 100644 index 000000000..75e06cb0b --- /dev/null +++ b/docs/guide/mocking-timers.md @@ -0,0 +1,76 @@ +# Mocking Timers + +To make your tests faster, you can mock calls to `setTimeout` and `setInterval`. All methods to manipulate timers are located on `vi` object that you can import from `vitest` package or access globally, if you have [`global`](/config/#global) config enabled. + +## useFakeTimers + +**Type:** `() => Vitest` + +To enable mocking timers, you need to call this method. It will wrap all further calls to timers, until [`vi.useRealTimers()`](#userealtimers) is called. + +## useRealTimers + +**Type:** `() => Vitest` + +When timers are run out, you may call this method to return mocked timers to its original implementations. All timers that were run before will not be restored. + +## runOnlyPendingTimers + +**Type:** `() => Vitest` + +This method will call every timer that was initiated after `vi.useFakeTimers()` call. It will not fire any timer that was initiated during its call. For example this will only log `1`: + +```ts +let i = 0 +setInterval(() => console.log(++i), 50) + +vi.runOnlyPendingTimers() +``` + +## runAllTimers + +**Type:** `() => Vitest` + +This method will invoke every initiated timer until the timers queue is empty. It means that every timer called during `runAllTimers` will be fired. If you have an infinite interval, +it will throw after 10 000 tries. For example this will log `1, 2, 3`: + +```ts +let i = 0 +setTimeout(() => console.log(++i)) +let interval = setInterval(() => { + console.log(++i) + if (i === 2) { + clearInterval(interval) + } +}, 50) + +vi.runAllTimers() +``` + +## advanceTimersByTime + +**Type:** `(ms: number) => Vitest` + +Works just like `runAllTimers`, but will end after passed milliseconds. For example this will log `1, 2, 3` and will not throw: + +```ts +let i = 0 +setInterval(() => console.log(++i), 50) + +vi.advanceTimersByTime(150) +``` + +## advanceTimersToNextTimer + +**Type:** `() => Vitest` + +Will call next available timer. Useful to make assertions between each timer call. You can chain call it to manage timers by yourself. + +```ts +let i = 0 +setInterval(() => console.log(++i), 50) + +vi.advanceTimersToNextTimer() // log 1 + .advanceTimersToNextTimer() // log 2 + .advanceTimersToNextTimer() // log 3 +``` diff --git a/packages/vitest/src/integrations/timers.ts b/packages/vitest/src/integrations/timers.ts index eb3291b0a..9e58de234 100644 --- a/packages/vitest/src/integrations/timers.ts +++ b/packages/vitest/src/integrations/timers.ts @@ -144,20 +144,9 @@ export class FakeTimers { } public advanceTimersToNextTimer(): void | Promise { - throw new Error('advanceTimersToNextTimer is not implemented') - } - - public runAllTicks(): void | Promise { - throw new Error('runAllTicks is not implemented') - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public setSystemTime(now?: number | Date): void { - throw new Error('setSystemTime is not implemented') - } + this.assertMocked() - public getRealSystemTime(): number { - return Date.now() + this.callQueueItem(0) } public getTimerCount(): number { @@ -181,14 +170,42 @@ export class FakeTimers { this._setTimeout?.mockRestore() } + private callQueueItem(index: number) { + const task = this._tasksQueue[index] + + if (!task) return + + const { call, type } = task + + this._scopeId = call.id + this._isNested = true + + this._nestedTime[call.id] ??= 0 + this._nestedTime[call.id] += call.ms + + if (type === 'timeout') { + this.removeTask(index) + } + else if (type === 'interval') { + call.nestedMs += call.ms + const nestedMs = call.nestedMs + const closestTask = this._tasksQueue.findIndex(({ type, call }) => type === 'interval' && call.nestedMs < nestedMs) + + if (closestTask !== -1 && closestTask !== index) + this.ensureQueueOrder() + } + + call.cb() + + this._queueCount++ + } + private runQueue() { let index = 0 while (this._tasksQueue[index]) { assertMaxLoop(this._queueCount) - const task = this._tasksQueue[index] - - const { call, nested, type } = task + const { call, nested } = this._tasksQueue[index] if (this._advancedTime && call.nestedMs > this._advancedTime) break @@ -198,27 +215,7 @@ export class FakeTimers { continue } - this._scopeId = call.id - this._isNested = true - - this._nestedTime[call.id] ??= 0 - this._nestedTime[call.id] += call.ms - - if (type === 'timeout') { - this.removeTask(index) - } - else if (type === 'interval') { - call.nestedMs += call.ms - const nestedMs = call.nestedMs - const closestTask = this._tasksQueue.findIndex(({ type, call }) => type === 'interval' && call.nestedMs < nestedMs) - - if (closestTask !== -1 && closestTask !== index) - this.ensureQueueOrder() - } - - call.cb() - - this._queueCount++ + this.callQueueItem(index) } } diff --git a/packages/vitest/src/integrations/utils.ts b/packages/vitest/src/integrations/utils.ts index 4b5c0a80d..d97a1cf7e 100644 --- a/packages/vitest/src/integrations/utils.ts +++ b/packages/vitest/src/integrations/utils.ts @@ -35,18 +35,6 @@ class VitestUtils { return this._timers.advanceTimersToNextTimer() } - public runAllTicks() { - return this._timers.runAllTicks() - } - - public setSystemTime(time?: number | Date) { - return this._timers.setSystemTime(time) - } - - public getRealSystemTime() { - return this._timers.getRealSystemTime() - } - public getTimerCount() { return this._timers.getTimerCount() } diff --git a/test/core/test/timers.test.ts b/test/core/test/timers.test.ts index 9116ac4a9..27be50114 100644 --- a/test/core/test/timers.test.ts +++ b/test/core/test/timers.test.ts @@ -223,3 +223,28 @@ test('async timer', async() => { expect(res).toEqual(['item1', 'item2']) }) + +test('advance timer', async() => { + const a1 = vi.fn() + const a2 = vi.fn() + + vi.useFakeTimers() + + setTimeout(a1) + setInterval(a2) + + vi.advanceTimersToNextTimer() + + expect(a1).toHaveBeenCalled() + expect(a2).not.toHaveBeenCalled() + + vi.advanceTimersToNextTimer() + + expect(a2).toHaveBeenCalled() + + vi.advanceTimersToNextTimer() + + expect(a2).toHaveBeenCalledTimes(2) + + vi.useRealTimers() +})