diff --git a/docs/api/index.md b/docs/api/index.md index 6c76e70721fe..a5f3af069f55 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -2553,6 +2553,19 @@ Vitest provides utility functions to help you out through it's **vi** helper. Yo vi.advanceTimersByTime(150) ``` +### vi.advanceTimersByTimeAsync + +- **Type:** `(ms: number) => Promise` + + Works just like `runAllTimersAsync`, but will end after passed milliseconds. This will include asynchronously set timers. For example this will log `1, 2, 3` and will not throw: + + ```ts + let i = 0 + setInterval(() => Promise.resolve().then(() => console.log(++i)), 50) + + await vi.advanceTimersByTimeAsync(150) + ``` + ### vi.advanceTimersToNextTimer - **Type:** `() => Vitest` @@ -2568,6 +2581,21 @@ Vitest provides utility functions to help you out through it's **vi** helper. Yo .advanceTimersToNextTimer() // log 3 ``` +### vi.advanceTimersToNextTimerAsync + +- **Type:** `() => Promise` + + Will call next available timer even if it was set asynchronously. Useful to make assertions between each timer call. You can chain call it to manage timers by yourself. + + ```ts + let i = 0 + setInterval(() => Promise.resolve().then(() => console.log(++i)), 50) + + vi.advanceTimersToNextTimerAsync() // log 1 + .advanceTimersToNextTimerAsync() // log 2 + .advanceTimersToNextTimerAsync() // log 3 + ``` + ### vi.getTimerCount - **Type:** `() => number` @@ -2984,6 +3012,21 @@ IntersectionObserver === undefined vi.runAllTimers() ``` +### vi.runAllTimersAsync + +- **Type:** `() => Promise` + + This method will asynchronously invoke every initiated timer until the timers queue is empty. It means that every timer called during `runAllTimersAsync` will be fired even asynchronous timers. If you have an infinite interval, + it will throw after 10 000 tries. For example this will log `result`: + + ```ts + setTimeout(async () => { + console.log(await Promise.resolve('result')) + }, 100) + + await vi.runAllTimersAsync() + ``` + ### vi.runOnlyPendingTimers - **Type:** `() => Vitest` @@ -2997,6 +3040,28 @@ IntersectionObserver === undefined vi.runOnlyPendingTimers() ``` +### vi.runOnlyPendingTimersAsync + +- **Type:** `() => Promise` + + This method will asynchronously call every timer that was initiated after `vi.useFakeTimers()` call, even asynchronous ones. It will not fire any timer that was initiated during its call. For example this will log `2, 3, 3, 1`: + + ```ts + setTimeout(() => { + console.log(1) + }, 100) + setTimeout(() => { + Promise.resolve().then(() => { + console.log(2) + setInterval(() => { + console.log(3) + }, 40) + }) + }, 10) + + await vi.runOnlyPendingTimersAsync() + ``` + ### vi.setSystemTime - **Type**: `(date: string | number | Date) => void` diff --git a/packages/vitest/src/integrations/mock/timers.ts b/packages/vitest/src/integrations/mock/timers.ts index 58881a93286e..2731204dc2e8 100644 --- a/packages/vitest/src/integrations/mock/timers.ts +++ b/packages/vitest/src/integrations/mock/timers.ts @@ -52,11 +52,21 @@ export class FakeTimers { this._clock.runAll() } + async runAllTimersAsync(): Promise { + if (this._checkFakeTimers()) + await this._clock.runAllAsync() + } + runOnlyPendingTimers(): void { if (this._checkFakeTimers()) this._clock.runToLast() } + async runOnlyPendingTimersAsync(): Promise { + if (this._checkFakeTimers()) + await this._clock.runToLastAsync() + } + advanceTimersToNextTimer(steps = 1): void { if (this._checkFakeTimers()) { for (let i = steps; i > 0; i--) { @@ -70,11 +80,29 @@ export class FakeTimers { } } + async advanceTimersToNextTimerAsync(steps = 1): Promise { + if (this._checkFakeTimers()) { + for (let i = steps; i > 0; i--) { + await this._clock.nextAsync() + // Fire all timers at this point: https://github.com/sinonjs/fake-timers/issues/250 + this._clock.tick(0) + + if (this._clock.countTimers() === 0) + break + } + } + } + advanceTimersByTime(msToRun: number): void { if (this._checkFakeTimers()) this._clock.tick(msToRun) } + async advanceTimersByTimeAsync(msToRun: number): Promise { + if (this._checkFakeTimers()) + await this._clock.tickAsync(msToRun) + } + runAllTicks(): void { if (this._checkFakeTimers()) { // @ts-expect-error method not exposed diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 474befedd8c8..398fa4c47142 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -61,11 +61,21 @@ class VitestUtils { return this } + public async runOnlyPendingTimersAsync() { + await this._timers.runOnlyPendingTimersAsync() + return this + } + public runAllTimers() { this._timers.runAllTimers() return this } + public async runAllTimersAsync() { + await this._timers.runAllTimersAsync() + return this + } + public runAllTicks() { this._timers.runAllTicks() return this @@ -76,11 +86,21 @@ class VitestUtils { return this } + public async advanceTimersByTimeAsync(ms: number) { + await this._timers.advanceTimersByTimeAsync(ms) + return this + } + public advanceTimersToNextTimer() { this._timers.advanceTimersToNextTimer() return this } + public async advanceTimersToNextTimerAsync() { + await this._timers.advanceTimersToNextTimerAsync() + return this + } + public getTimerCount() { return this._timers.getTimerCount() } diff --git a/test/core/test/timers.test.ts b/test/core/test/timers.test.ts index 1ff9e1e005a9..85d821ddffc9 100644 --- a/test/core/test/timers.test.ts +++ b/test/core/test/timers.test.ts @@ -336,6 +336,136 @@ describe('FakeTimers', () => { }) }) + describe('runAllTimersAsync', () => { + it('runs all timers in order', async () => { + const global = { Date: FakeDate, clearTimeout, process, setTimeout, Promise } + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + const runOrder = [] + const mock1 = vi.fn(() => runOrder.push('mock1')) + const mock2 = vi.fn(() => runOrder.push('mock2')) + const mock3 = vi.fn(() => runOrder.push('mock3')) + const mock4 = vi.fn(() => runOrder.push('mock4')) + const mock5 = vi.fn(() => runOrder.push('mock5')) + const mock6 = vi.fn(() => runOrder.push('mock6')) + + global.setTimeout(mock1, 100) + global.setTimeout(mock2, NaN) + global.setTimeout(mock3, 0) + const intervalHandler = global.setInterval(() => { + mock4() + global.clearInterval(intervalHandler) + }, 200) + global.setTimeout(mock5, Infinity) + global.setTimeout(mock6, -Infinity) + + await timers.runAllTimersAsync() + expect(runOrder).toEqual([ + 'mock2', + 'mock3', + 'mock5', + 'mock6', + 'mock1', + 'mock4', + ]) + }) + + it('warns when trying to advance timers while real timers are used', async () => { + const timers = new FakeTimers({ + config: { + rootDir: __dirname, + }, + global, + }) + await expect(timers.runAllTimersAsync()).rejects.toThrow(/Timers are not mocked/) + }) + + it('only runs a setTimeout callback once (ever)', async () => { + const global = { Date: FakeDate, clearTimeout, process, setTimeout, Promise } + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + const fn = vi.fn() + global.setTimeout(fn, 0) + expect(fn).toHaveBeenCalledTimes(0) + + await timers.runAllTimersAsync() + expect(fn).toHaveBeenCalledTimes(1) + + await timers.runAllTimersAsync() + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('runs callbacks with arguments after the interval', async () => { + const global = { Date: FakeDate, clearTimeout, process, setTimeout, Promise } + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + const fn = vi.fn() + global.setTimeout(fn, 0, 'mockArg1', 'mockArg2') + + await timers.runAllTimersAsync() + expect(fn).toHaveBeenCalledTimes(1) + expect(fn).toHaveBeenCalledWith('mockArg1', 'mockArg2') + }) + + it('throws before allowing infinite recursion', async () => { + const global = { Date: FakeDate, clearTimeout, process, setTimeout, Promise } + const timers = new FakeTimers({ global, config: { loopLimit: 20 } }) + timers.useFakeTimers() + + global.setTimeout(function infinitelyRecursingCallback() { + global.setTimeout(infinitelyRecursingCallback, 0) + }, 0) + + await expect( + timers.runAllTimersAsync(), + ).rejects.toThrow( + 'Aborting after running 20 timers, assuming an infinite loop!', + ) + }) + + it('also clears ticks', async () => { + const global = { Date: FakeDate, clearTimeout, process, setTimeout, Promise } + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + const fn = vi.fn() + global.setTimeout(() => { + process.nextTick(fn) + }, 0) + expect(fn).toHaveBeenCalledTimes(0) + + await timers.runAllTimersAsync() + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('all callbacks are called when setTimeout calls asynchronous method', async () => { + const global = { Date: FakeDate, clearTimeout, process, setTimeout, Promise } + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + const runOrder = [] + const mock2 = vi.fn(async () => { + runOrder.push('mock2') + return global.Promise.resolve(true) + }) + const mock1 = vi.fn(async () => { + await mock2() + runOrder.push('mock1') + }) + + global.setTimeout(mock1, 100) + await timers.runAllTimersAsync() + + expect(runOrder).toEqual([ + 'mock2', + 'mock1', + ]) + }) + }) + describe('advanceTimersByTime', () => { it('runs timers in order', () => { const global = { Date: FakeDate, clearTimeout, process, setTimeout } @@ -385,6 +515,55 @@ describe('FakeTimers', () => { }) }) + describe('advanceTimersByTimeAsync', () => { + it('runs timers in order', async () => { + const global = { Date: FakeDate, clearTimeout, clearInterval, process, setTimeout, setInterval, Promise } + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + const runOrder = [] + const mock1 = vi.fn(() => runOrder.push('mock1')) + const mock2 = vi.fn(() => runOrder.push('mock2')) + const mock3 = vi.fn(() => runOrder.push('mock3')) + const mock4 = vi.fn(() => runOrder.push('mock4')) + + global.setTimeout(mock1, 100) + global.setTimeout(mock2, 0) + global.setTimeout(mock3, 0) + global.setInterval(() => { + mock4() + }, 200) + + // Move forward to t=50 + await timers.advanceTimersByTimeAsync(50) + expect(runOrder).toEqual(['mock2', 'mock3']) + + // Move forward to t=60 + await timers.advanceTimersByTimeAsync(10) + expect(runOrder).toEqual(['mock2', 'mock3']) + + // Move forward to t=100 + await timers.advanceTimersByTimeAsync(40) + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']) + + // Move forward to t=200 + await timers.advanceTimersByTimeAsync(100) + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4']) + + // Move forward to t=400 + await timers.advanceTimersByTimeAsync(200) + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4', 'mock4']) + }) + + it('does nothing when no timers have been scheduled', async () => { + const global = { Date: FakeDate, clearTimeout, process, setTimeout, Promise } + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + await timers.advanceTimersByTimeAsync(100) + }) + }) + describe('advanceTimersToNextTimer', () => { it('runs timers in order', () => { const global = { Date: FakeDate, clearTimeout, process, setTimeout } @@ -487,6 +666,108 @@ describe('FakeTimers', () => { }) }) + describe('advanceTimersToNextTimerAsync', () => { + it('runs timers in order', async () => { + const global = { Date: FakeDate, clearTimeout, process, setTimeout, Promise } + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + const runOrder: Array = [] + const mock1 = vi.fn(() => runOrder.push('mock1')) + const mock2 = vi.fn(() => runOrder.push('mock2')) + const mock3 = vi.fn(() => runOrder.push('mock3')) + const mock4 = vi.fn(() => runOrder.push('mock4')) + + global.setTimeout(mock1, 100) + global.setTimeout(mock2, 0) + global.setTimeout(mock3, 0) + global.setInterval(() => { + mock4() + }, 200) + + await timers.advanceTimersToNextTimer() + // Move forward to t=0 + expect(runOrder).toEqual(['mock2', 'mock3']) + + await timers.advanceTimersToNextTimer() + // Move forward to t=100 + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']) + + await timers.advanceTimersToNextTimer() + // Move forward to t=200 + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4']) + + await timers.advanceTimersToNextTimer() + // Move forward to t=400 + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4', 'mock4']) + }) + + it('run correct amount of steps', async () => { + const global = { Date: FakeDate, clearTimeout, process, setTimeout, Promise } + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + const runOrder: Array = [] + const mock1 = vi.fn(() => runOrder.push('mock1')) + const mock2 = vi.fn(() => runOrder.push('mock2')) + const mock3 = vi.fn(() => runOrder.push('mock3')) + const mock4 = vi.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 + await timers.advanceTimersToNextTimer(2) + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']) + + // Move forward to t=600 + await timers.advanceTimersToNextTimer(3) + expect(runOrder).toEqual([ + 'mock2', + 'mock3', + 'mock1', + 'mock4', + 'mock4', + 'mock4', + ]) + }) + + it('setTimeout inside setTimeout', async () => { + const global = { Date: FakeDate, clearTimeout, process, setTimeout, Promise } + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + const runOrder: Array = [] + const mock1 = vi.fn(() => runOrder.push('mock1')) + const mock2 = vi.fn(() => runOrder.push('mock2')) + const mock3 = vi.fn(() => runOrder.push('mock3')) + const mock4 = vi.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 + await timers.advanceTimersToNextTimer(3) + expect(runOrder).toEqual(['mock1', 'mock2', 'mock3']) + }) + + it('does nothing when no timers have been scheduled', async () => { + const global = { Date: FakeDate, clearTimeout, process, setTimeout, Promise } + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + await timers.advanceTimersToNextTimer() + }) + }) + describe('reset', () => { it('resets all pending setTimeouts', () => { const global = { Date: FakeDate, clearTimeout, process, setTimeout } @@ -639,6 +920,113 @@ describe('FakeTimers', () => { }) }) + describe('runOnlyPendingTimersAsync', () => { + it('runs all existing timers', async () => { + const global = { + Date: FakeDate, + clearTimeout, + process, + setTimeout, + Promise, + } + + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + const spies = [vi.fn(), vi.fn()] + global.setTimeout(spies[0], 10) + global.setTimeout(spies[1], 50) + + await timers.runOnlyPendingTimersAsync() + + expect(spies[0]).toBeCalled() + expect(spies[1]).toBeCalled() + }) + + it('runs all timers in order', async () => { + const global = { + Date: FakeDate, + clearTimeout, + process, + setImmediate, + setTimeout, + Promise, + } + + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + const runOrder = [] + + global.setTimeout(function cb() { + runOrder.push('mock1') + global.setTimeout(cb, 100) + }, 100) + + global.setTimeout(function cb() { + runOrder.push('mock2') + global.setTimeout(cb, 50) + }, 0) + + global.setInterval(() => { + runOrder.push('mock3') + }, 200) + + global.setImmediate(() => { + runOrder.push('mock4') + }) + + global.setImmediate(function cb() { + runOrder.push('mock5') + global.setTimeout(cb, 400) + }) + + await timers.runOnlyPendingTimersAsync() + const firsRunOrder = [ + 'mock4', + 'mock5', + 'mock2', + 'mock2', + 'mock1', + 'mock2', + 'mock2', + 'mock3', + 'mock1', + 'mock2', + ] + + expect(runOrder).toEqual(firsRunOrder) + + await timers.runOnlyPendingTimersAsync() + expect(runOrder).toEqual([ + ...firsRunOrder, + 'mock2', + 'mock1', + 'mock2', + 'mock2', + 'mock3', + 'mock5', + 'mock1', + 'mock2', + ]) + }) + + it('does not run timers that were cleared in another timer', async () => { + const global = { Date: FakeDate, clearTimeout, process, setTimeout, Promise } + const timers = new FakeTimers({ global }) + timers.useFakeTimers() + + const fn = vi.fn() + const timer = global.setTimeout(fn, 10) + global.setTimeout(() => { + global.clearTimeout(timer) + }, 0) + + await timers.runOnlyPendingTimersAsync() + expect(fn).not.toBeCalled() + }) + }) + describe('useRealTimers', () => { it('resets native timer APIs', () => { const nativeSetTimeout = vi.fn()