-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(vitest): support
vi.waitFor
method (#4113)
Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>
- Loading branch information
1 parent
a7e0993
commit d79cb44
Showing
5 changed files
with
282 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { getSafeTimers } from '@vitest/utils' | ||
import { vi } from './vi' | ||
|
||
// The waitFor function was inspired by https://github.com/testing-library/web-testing-library/pull/2 | ||
|
||
export type WaitForCallback<T> = () => T | Promise<T> | ||
|
||
export interface WaitForOptions { | ||
/** | ||
* @description Time in ms between each check callback | ||
* @default 50ms | ||
*/ | ||
interval?: number | ||
/** | ||
* @description Time in ms after which the throw a timeout error | ||
* @default 1000ms | ||
*/ | ||
timeout?: number | ||
} | ||
|
||
function copyStackTrace(target: Error, source: Error) { | ||
if (source.stack !== undefined) | ||
target.stack = source.stack.replace(source.message, target.message) | ||
return target | ||
} | ||
|
||
export function waitFor<T>(callback: WaitForCallback<T>, options: number | WaitForOptions = {}) { | ||
const { setTimeout, setInterval, clearTimeout, clearInterval } = getSafeTimers() | ||
const { interval = 50, timeout = 1000 } = typeof options === 'number' ? { timeout: options } : options | ||
const STACK_TRACE_ERROR = new Error('STACK_TRACE_ERROR') | ||
|
||
return new Promise<T>((resolve, reject) => { | ||
let lastError: unknown | ||
let promiseStatus: 'idle' | 'pending' | 'resolved' | 'rejected' = 'idle' | ||
let timeoutId: ReturnType<typeof setTimeout> | ||
let intervalId: ReturnType<typeof setInterval> | ||
|
||
const onResolve = (result: T) => { | ||
if (timeoutId) | ||
clearTimeout(timeoutId) | ||
if (intervalId) | ||
clearInterval(intervalId) | ||
|
||
resolve(result) | ||
} | ||
|
||
const handleTimeout = () => { | ||
let error = lastError | ||
if (!error) | ||
error = copyStackTrace(new Error('Timed out in waitFor!'), STACK_TRACE_ERROR) | ||
|
||
reject(error) | ||
} | ||
|
||
const checkCallback = () => { | ||
if (vi.isFakeTimers()) | ||
vi.advanceTimersByTime(interval) | ||
|
||
if (promiseStatus === 'pending') | ||
return | ||
try { | ||
const result = callback() | ||
if ( | ||
result !== null | ||
&& typeof result === 'object' | ||
&& typeof (result as any).then === 'function' | ||
) { | ||
const thenable = result as PromiseLike<T> | ||
promiseStatus = 'pending' | ||
thenable.then( | ||
(resolvedValue) => { | ||
promiseStatus = 'resolved' | ||
onResolve(resolvedValue) | ||
}, | ||
(rejectedValue) => { | ||
promiseStatus = 'rejected' | ||
lastError = rejectedValue | ||
}, | ||
) | ||
} | ||
else { | ||
onResolve(result as T) | ||
return true | ||
} | ||
} | ||
catch (error) { | ||
lastError = error | ||
} | ||
} | ||
|
||
if (checkCallback() === true) | ||
return | ||
|
||
timeoutId = setTimeout(handleTimeout, timeout) | ||
intervalId = setInterval(checkCallback, interval) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { describe, expect, test, vi } from 'vitest' | ||
|
||
describe('waitFor', () => { | ||
describe('options', () => { | ||
test('timeout', async () => { | ||
expect(async () => { | ||
await vi.waitFor(() => { | ||
return new Promise((resolve) => { | ||
setTimeout(() => { | ||
resolve(true) | ||
}, 100) | ||
}) | ||
}, 50) | ||
}).rejects.toThrow('Timed out in waitFor!') | ||
}) | ||
|
||
test('interval', async () => { | ||
const callback = vi.fn(() => { | ||
throw new Error('interval error') | ||
}) | ||
|
||
await expect( | ||
vi.waitFor(callback, { | ||
timeout: 60, | ||
interval: 30, | ||
}), | ||
).rejects.toThrowErrorMatchingInlineSnapshot('"interval error"') | ||
|
||
expect(callback).toHaveBeenCalledTimes(2) | ||
}) | ||
}) | ||
|
||
test('basic', async () => { | ||
let throwError = false | ||
await vi.waitFor(() => { | ||
if (!throwError) { | ||
throwError = true | ||
throw new Error('basic error') | ||
} | ||
}) | ||
expect(throwError).toBe(true) | ||
}) | ||
|
||
test('async function', async () => { | ||
let finished = false | ||
setTimeout(() => { | ||
finished = true | ||
}, 50) | ||
await vi.waitFor(async () => { | ||
if (finished) | ||
return Promise.resolve(true) | ||
else | ||
return Promise.reject(new Error('async function error')) | ||
}) | ||
}) | ||
|
||
test('stacktrace correctly', async () => { | ||
const check = () => { | ||
const _a = 1 | ||
// @ts-expect-error test | ||
_a += 1 | ||
} | ||
try { | ||
await vi.waitFor(check, 100) | ||
} | ||
catch (error) { | ||
expect((error as Error).message).toMatchInlineSnapshot('"Assignment to constant variable."') | ||
expect.soft((error as Error).stack).toMatch(/at check/) | ||
} | ||
}) | ||
|
||
test('stacktrace point to waitFor', async () => { | ||
const check = async () => { | ||
return new Promise((resolve) => { | ||
setTimeout(resolve, 60) | ||
}) | ||
} | ||
try { | ||
await vi.waitFor(check, 50) | ||
} | ||
catch (error) { | ||
expect(error).toMatchInlineSnapshot('[Error: Timed out in waitFor!]') | ||
expect((error as Error).stack?.split('\n')[1]).toMatch(/waitFor\s*\(.*\)?/) | ||
} | ||
}) | ||
|
||
test('fakeTimer works', async () => { | ||
vi.useFakeTimers() | ||
|
||
setTimeout(() => { | ||
vi.advanceTimersByTime(200) | ||
}, 50) | ||
|
||
await vi.waitFor(() => { | ||
return new Promise<void>((resolve) => { | ||
setTimeout(() => { | ||
resolve() | ||
}, 150) | ||
}) | ||
}, 200) | ||
|
||
vi.useRealTimers() | ||
}) | ||
}) |