[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(vitest): support `vi.waitFor` method (#4113)

Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>

authored by

Dunqing
Vladimir
and committed by
GitHub
(Sep 14, 2023, 10:16 AM +0200) d79cb44b a7e09939

+282 -4
+69 -3
docs/api/vi.md
··· 254 254 ```ts 255 255 // increment.test.js 256 256 import { vi } from 'vitest' 257 - 257 + 258 258 // axios is a default export from `__mocks__/axios.js` 259 259 import axios from 'axios' 260 - 260 + 261 261 // increment is a named export from `src/__mocks__/increment.js` 262 262 import { increment } from '../increment.js' 263 263 ··· 371 371 372 372 ```ts 373 373 import { vi } from 'vitest' 374 - 374 + 375 375 import { data } from './data.js' // Will not get reevaluated beforeEach test 376 376 377 377 beforeEach(() => { ··· 706 706 707 707 The implementation is based internally on [`@sinonjs/fake-timers`](https://github.com/sinonjs/fake-timers). 708 708 709 + ## vi.isFakeTimers 710 + 711 + - **Type:** `() => boolean` 712 + - **Version:** Since Vitest 0.34.5 713 + 714 + Returns `true` if fake timers are enabled. 715 + 709 716 ## vi.useRealTimers 710 717 711 718 - **Type:** `() => Vitest` 712 719 713 720 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. 721 + 722 + ### vi.waitFor 723 + 724 + - **Type:** `function waitFor<T>(callback: WaitForCallback<T>, options?: number | WaitForOptions): Promise<T>` 725 + - **Version**: Since Vitest 0.34.5 726 + 727 + Wait for the callback to execute successfully. If the callback throws an error or returns a rejected promise it will continue to wait until it succeeds or times out. 728 + 729 + This is very useful when you need to wait for some asynchronous action to complete, for example, when you start a server and need to wait for it to start. 730 + 731 + ```ts 732 + import { test, vi } from 'vitest' 733 + 734 + test('Server started successfully', async () => { 735 + let server = false 736 + 737 + setTimeout(() => { 738 + server = true 739 + }, 100) 740 + 741 + function checkServerStart() { 742 + if (!server) 743 + throw new Error('Server not started') 744 + 745 + console.log('Server started') 746 + } 747 + 748 + const res = await vi.waitFor(checkServerStart, { 749 + timeout: 500, // default is 1000 750 + interval: 20, // default is 50 751 + }) 752 + expect(server).toBe(true) 753 + }) 754 + ``` 755 + 756 + It also works for asynchronous callbacks 757 + 758 + ```ts 759 + import { test, vi } from 'vitest' 760 + 761 + test('Server started successfully', async () => { 762 + async function startServer() { 763 + return new Promise((resolve) => { 764 + setTimeout(() => { 765 + server = true 766 + resolve('Server started') 767 + }, 100) 768 + }) 769 + } 770 + 771 + const server = await vi.waitFor(startServer, { 772 + timeout: 500, // default is 1000 773 + interval: 20, // default is 50 774 + }) 775 + expect(server).toBe('Server started') 776 + }) 777 + ``` 778 + 779 + If `vi.useFakeTimers` is used, `vi.waitFor` automatically calls `vi.advanceTimersByTime(interval)` in every check callback.
+104
test/core/test/wait.test.ts
··· 1 + import { describe, expect, test, vi } from 'vitest' 2 + 3 + describe('waitFor', () => { 4 + describe('options', () => { 5 + test('timeout', async () => { 6 + expect(async () => { 7 + await vi.waitFor(() => { 8 + return new Promise((resolve) => { 9 + setTimeout(() => { 10 + resolve(true) 11 + }, 100) 12 + }) 13 + }, 50) 14 + }).rejects.toThrow('Timed out in waitFor!') 15 + }) 16 + 17 + test('interval', async () => { 18 + const callback = vi.fn(() => { 19 + throw new Error('interval error') 20 + }) 21 + 22 + await expect( 23 + vi.waitFor(callback, { 24 + timeout: 60, 25 + interval: 30, 26 + }), 27 + ).rejects.toThrowErrorMatchingInlineSnapshot('"interval error"') 28 + 29 + expect(callback).toHaveBeenCalledTimes(2) 30 + }) 31 + }) 32 + 33 + test('basic', async () => { 34 + let throwError = false 35 + await vi.waitFor(() => { 36 + if (!throwError) { 37 + throwError = true 38 + throw new Error('basic error') 39 + } 40 + }) 41 + expect(throwError).toBe(true) 42 + }) 43 + 44 + test('async function', async () => { 45 + let finished = false 46 + setTimeout(() => { 47 + finished = true 48 + }, 50) 49 + await vi.waitFor(async () => { 50 + if (finished) 51 + return Promise.resolve(true) 52 + else 53 + return Promise.reject(new Error('async function error')) 54 + }) 55 + }) 56 + 57 + test('stacktrace correctly', async () => { 58 + const check = () => { 59 + const _a = 1 60 + // @ts-expect-error test 61 + _a += 1 62 + } 63 + try { 64 + await vi.waitFor(check, 100) 65 + } 66 + catch (error) { 67 + expect((error as Error).message).toMatchInlineSnapshot('"Assignment to constant variable."') 68 + expect.soft((error as Error).stack).toMatch(/at check/) 69 + } 70 + }) 71 + 72 + test('stacktrace point to waitFor', async () => { 73 + const check = async () => { 74 + return new Promise((resolve) => { 75 + setTimeout(resolve, 60) 76 + }) 77 + } 78 + try { 79 + await vi.waitFor(check, 50) 80 + } 81 + catch (error) { 82 + expect(error).toMatchInlineSnapshot('[Error: Timed out in waitFor!]') 83 + expect((error as Error).stack?.split('\n')[1]).toMatch(/waitFor\s*\(.*\)?/) 84 + } 85 + }) 86 + 87 + test('fakeTimer works', async () => { 88 + vi.useFakeTimers() 89 + 90 + setTimeout(() => { 91 + vi.advanceTimersByTime(200) 92 + }, 50) 93 + 94 + await vi.waitFor(() => { 95 + return new Promise<void>((resolve) => { 96 + setTimeout(() => { 97 + resolve() 98 + }, 150) 99 + }) 100 + }, 200) 101 + 102 + vi.useRealTimers() 103 + }) 104 + })
+8 -1
packages/vitest/src/integrations/vi.ts
··· 9 9 import { FakeTimers } from './mock/timers' 10 10 import type { EnhancedSpy, MaybeMocked, MaybeMockedDeep, MaybePartiallyMocked, MaybePartiallyMockedDeep } from './spy' 11 11 import { fn, isMockFunction, spies, spyOn } from './spy' 12 + import { waitFor } from './wait' 12 13 13 14 interface VitestUtils { 15 + isFakeTimers(): boolean 14 16 useFakeTimers(config?: FakeTimerInstallOpts): this 15 17 useRealTimers(): this 16 18 runOnlyPendingTimers(): this ··· 30 32 31 33 spyOn: typeof spyOn 32 34 fn: typeof fn 35 + waitFor: typeof waitFor 33 36 34 37 /** 35 38 * Run the factory before imports are evaluated. You can return a value from the factory ··· 213 216 return utils 214 217 }, 215 218 219 + isFakeTimers() { 220 + return _timers.isFakeTimers() 221 + }, 222 + 216 223 useRealTimers() { 217 224 _timers.useRealTimers() 218 225 _mockedDate = null ··· 292 299 293 300 spyOn, 294 301 fn, 295 - 302 + waitFor, 296 303 hoisted<T>(factory: () => T): T { 297 304 assertTypes(factory, '"vi.hoisted" factory', ['function']) 298 305 return factory()
+97
packages/vitest/src/integrations/wait.ts
··· 1 + import { getSafeTimers } from '@vitest/utils' 2 + import { vi } from './vi' 3 + 4 + // The waitFor function was inspired by https://github.com/testing-library/web-testing-library/pull/2 5 + 6 + export type WaitForCallback<T> = () => T | Promise<T> 7 + 8 + export interface WaitForOptions { 9 + /** 10 + * @description Time in ms between each check callback 11 + * @default 50ms 12 + */ 13 + interval?: number 14 + /** 15 + * @description Time in ms after which the throw a timeout error 16 + * @default 1000ms 17 + */ 18 + timeout?: number 19 + } 20 + 21 + function copyStackTrace(target: Error, source: Error) { 22 + if (source.stack !== undefined) 23 + target.stack = source.stack.replace(source.message, target.message) 24 + return target 25 + } 26 + 27 + export function waitFor<T>(callback: WaitForCallback<T>, options: number | WaitForOptions = {}) { 28 + const { setTimeout, setInterval, clearTimeout, clearInterval } = getSafeTimers() 29 + const { interval = 50, timeout = 1000 } = typeof options === 'number' ? { timeout: options } : options 30 + const STACK_TRACE_ERROR = new Error('STACK_TRACE_ERROR') 31 + 32 + return new Promise<T>((resolve, reject) => { 33 + let lastError: unknown 34 + let promiseStatus: 'idle' | 'pending' | 'resolved' | 'rejected' = 'idle' 35 + let timeoutId: ReturnType<typeof setTimeout> 36 + let intervalId: ReturnType<typeof setInterval> 37 + 38 + const onResolve = (result: T) => { 39 + if (timeoutId) 40 + clearTimeout(timeoutId) 41 + if (intervalId) 42 + clearInterval(intervalId) 43 + 44 + resolve(result) 45 + } 46 + 47 + const handleTimeout = () => { 48 + let error = lastError 49 + if (!error) 50 + error = copyStackTrace(new Error('Timed out in waitFor!'), STACK_TRACE_ERROR) 51 + 52 + reject(error) 53 + } 54 + 55 + const checkCallback = () => { 56 + if (vi.isFakeTimers()) 57 + vi.advanceTimersByTime(interval) 58 + 59 + if (promiseStatus === 'pending') 60 + return 61 + try { 62 + const result = callback() 63 + if ( 64 + result !== null 65 + && typeof result === 'object' 66 + && typeof (result as any).then === 'function' 67 + ) { 68 + const thenable = result as PromiseLike<T> 69 + promiseStatus = 'pending' 70 + thenable.then( 71 + (resolvedValue) => { 72 + promiseStatus = 'resolved' 73 + onResolve(resolvedValue) 74 + }, 75 + (rejectedValue) => { 76 + promiseStatus = 'rejected' 77 + lastError = rejectedValue 78 + }, 79 + ) 80 + } 81 + else { 82 + onResolve(result as T) 83 + return true 84 + } 85 + } 86 + catch (error) { 87 + lastError = error 88 + } 89 + } 90 + 91 + if (checkCallback() === true) 92 + return 93 + 94 + timeoutId = setTimeout(handleTimeout, timeout) 95 + intervalId = setInterval(checkCallback, interval) 96 + }) 97 + }
+4
packages/vitest/src/integrations/mock/timers.ts
··· 175 175 this._userConfig = config 176 176 } 177 177 178 + isFakeTimers() { 179 + return this._fakingTime 180 + } 181 + 178 182 private _checkFakeTimers() { 179 183 if (!this._fakingTime) { 180 184 throw new Error(