From 60a81cd0f5d147aa79335d7d2676b94695c02c29 Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Tue, 25 Oct 2022 22:39:55 +0200 Subject: [PATCH 1/6] feat: add runAllTimersAsync from sinonjs --- .../vitest/src/integrations/mock/timers.ts | 5 + packages/vitest/src/integrations/vi.ts | 5 + test/core/test/timers.test.ts | 106 ++++++++++++++++++ 3 files changed, 116 insertions(+) diff --git a/packages/vitest/src/integrations/mock/timers.ts b/packages/vitest/src/integrations/mock/timers.ts index 58881a93286e..e8d88cbb9f97 100644 --- a/packages/vitest/src/integrations/mock/timers.ts +++ b/packages/vitest/src/integrations/mock/timers.ts @@ -52,6 +52,11 @@ export class FakeTimers { this._clock.runAll() } + async runAllTimersAsync(): Promise { + if (this._checkFakeTimers()) + await this._clock.runAllAsync() + } + runOnlyPendingTimers(): void { if (this._checkFakeTimers()) this._clock.runToLast() diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 474befedd8c8..a239a807dc9a 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -66,6 +66,11 @@ class VitestUtils { return this } + public async runAllTimersAsync() { + await this._timers.runAllTimersAsync() + return this + } + public runAllTicks() { this._timers.runAllTicks() return this diff --git a/test/core/test/timers.test.ts b/test/core/test/timers.test.ts index 1ff9e1e005a9..7a3d22084275 100644 --- a/test/core/test/timers.test.ts +++ b/test/core/test/timers.test.ts @@ -336,6 +336,112 @@ 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) + }) + }) + describe('advanceTimersByTime', () => { it('runs timers in order', () => { const global = { Date: FakeDate, clearTimeout, process, setTimeout } From 54d259b5f8a102ff8ca0b61b811152d574f4840a Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Tue, 25 Oct 2022 22:45:45 +0200 Subject: [PATCH 2/6] docs(api): add runAllAsyncTimer --- docs/api/index.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/api/index.md b/docs/api/index.md index 6c76e70721fe..5406ce572767 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -2984,6 +2984,25 @@ 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 `2, 1`: + + ```ts + setTimeout(async () => { + await setTimeout(() => { + console.log('2') + return Promise.resolve() + }) + console.log('1') + }, 100) + + await vi.runAllTimersAsync() + ``` + ### vi.runOnlyPendingTimers - **Type:** `() => Vitest` From a32b6b1ef2246c0941540fd1aace4fc7a7047b2d Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Wed, 26 Oct 2022 10:25:41 +0200 Subject: [PATCH 3/6] feat(timer): add asynchronous callback test --- test/core/test/timers.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/core/test/timers.test.ts b/test/core/test/timers.test.ts index 7a3d22084275..c3df8b8af98e 100644 --- a/test/core/test/timers.test.ts +++ b/test/core/test/timers.test.ts @@ -440,6 +440,30 @@ describe('FakeTimers', () => { 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', () => { From 29b78429d6866b53b883a0b7b9630bfd5ac92e8f Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Fri, 16 Dec 2022 17:35:20 +0100 Subject: [PATCH 4/6] feat(timer): add async method to advance timers --- .../vitest/src/integrations/mock/timers.ts | 23 ++ packages/vitest/src/integrations/vi.ts | 15 + test/core/test/timers.test.ts | 258 ++++++++++++++++++ 3 files changed, 296 insertions(+) diff --git a/packages/vitest/src/integrations/mock/timers.ts b/packages/vitest/src/integrations/mock/timers.ts index e8d88cbb9f97..2731204dc2e8 100644 --- a/packages/vitest/src/integrations/mock/timers.ts +++ b/packages/vitest/src/integrations/mock/timers.ts @@ -62,6 +62,11 @@ export class FakeTimers { 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--) { @@ -75,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 a239a807dc9a..398fa4c47142 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -61,6 +61,11 @@ class VitestUtils { return this } + public async runOnlyPendingTimersAsync() { + await this._timers.runOnlyPendingTimersAsync() + return this + } + public runAllTimers() { this._timers.runAllTimers() return this @@ -81,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 c3df8b8af98e..85d821ddffc9 100644 --- a/test/core/test/timers.test.ts +++ b/test/core/test/timers.test.ts @@ -515,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 } @@ -617,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 } @@ -769,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() From f8bb38070541144f41d92206a3729125a969319f Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Tue, 10 Jan 2023 13:54:48 +0100 Subject: [PATCH 5/6] docs(api): add advanceTimersByTimeAsync, advanceTimersToNextTimerAsync and runOnlyPendingTimersAsync --- docs/api/index.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/api/index.md b/docs/api/index.md index 5406ce572767..67cc2460e08c 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` @@ -3016,6 +3044,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` From 76fe9b3abbb6629038b9591ed173c29733d96d3d Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Fri, 13 Jan 2023 19:51:17 +0100 Subject: [PATCH 6/6] docs(api): better example for runAllTimersAsync --- docs/api/index.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/api/index.md b/docs/api/index.md index 67cc2460e08c..a5f3af069f55 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -3017,15 +3017,11 @@ IntersectionObserver === undefined - **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 `2, 1`: + it will throw after 10 000 tries. For example this will log `result`: ```ts setTimeout(async () => { - await setTimeout(() => { - console.log('2') - return Promise.resolve() - }) - console.log('1') + console.log(await Promise.resolve('result')) }, 100) await vi.runAllTimersAsync()