-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(vitest): support vi.waitFor method
#4113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
5487b2c
feat(vitest): support for `waitFor` method
Dunqing ea5a767
feat: improve
Dunqing 1552a84
test: update
Dunqing 3a033e0
docs: update vi.waitFor
Dunqing fac1ab3
feat: add inspired
Dunqing 31e0ae2
docs: update
Dunqing 99bd05f
test: adjust number for timeout time
Dunqing 3b5fe20
test: fix time
Dunqing d54e466
test: fix windows only
Dunqing 65b200f
fix: times error
Dunqing a59af2d
Merge branch 'main' into feat/wait-for
Dunqing 95e6b59
feat: fake timer support
Dunqing 11b4182
docs: fix typo
Dunqing 79294f1
feat: improve
Dunqing bbce930
feat: add `vi.isFakeTimers` method
Dunqing b07c149
Update packages/vitest/src/integrations/vi.ts
Dunqing 48537bd
Update docs/api/vi.md
Dunqing dfeefe7
Update docs/api/vi.md
Dunqing File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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) | ||
|
sheremet-va marked this conversation as resolved.
Dunqing marked this conversation as resolved.
|
||
|
|
||
| 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 hidden or 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() | ||
| }) | ||
| }) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.