-
-
Notifications
You must be signed in to change notification settings - Fork 20
Description
Description
vitest provides built-in vi.waitFor() and vi.waitUntil() polling helpers that retry a callback until it succeeds or a timeout is reached. rstest currently has no equivalent — users migrating from vitest have to build their own polling helpers.
How vi.waitFor() works in vitest
vitest ships vi.waitFor() as part of VitestUtils:
function waitFor<T>(
callback: WaitForCallback<T>,
options?: number | WaitForOptions,
): Promise<T>
interface WaitForOptions {
timeout?: number // default: 1000
interval?: number // default: 50
}The function repeatedly calls callback at the specified interval. If the callback throws or returns a rejected promise, it retries. If it succeeds, the resolved value is returned. If the timeout is reached, the last error is thrown.
Key behaviors:
- Supports both sync and async callbacks
- Integrates with fake timers (
vi.useFakeTimers) — automatically advances time by the interval on each retry - The thrown error on timeout includes retry count and elapsed time for diagnostics
- Prevents concurrent callback executions (waits for a pending promise before retrying)
Usage examples
// E2E: poll until a server responds
await vi.waitFor(
async () => {
const res = await fetch('http://localhost:3000/health');
expect(res.ok).toBe(true);
},
{ timeout: 30_000, interval: 1_000 },
);
// Unit: wait for async state
const element = await vi.waitFor(
async () => {
const el = await findElement('#loaded');
expect(el).toBeTruthy();
return el;
},
{ timeout: 500, interval: 20 },
);vitest also ships vi.waitUntil() which expects the callback to return a truthy value (rather than not-throwing):
const server = await vi.waitUntil(
() => startServer().catch(() => null),
{ timeout: 5000, interval: 100 },
);Proposal
Add waitFor() and waitUntil() to the RstestUtilities interface with the same API as vitest:
export interface RstestUtilities {
// ... existing methods ...
/**
* Retry callback until it succeeds (doesn't throw) or timeout is reached.
* If timeout is reached, throws the last error from the callback.
*/
waitFor<T>(
callback: WaitForCallback<T>,
options?: number | WaitForOptions,
): Promise<T>
/**
* Retry callback until it returns a truthy value or timeout is reached.
* If timeout is reached, throws an error.
*/
waitUntil<T>(
callback: () => T | Promise<T>,
options?: number | WaitUntilOptions,
): Promise<T>
}
interface WaitForOptions {
timeout?: number // default: 1000
interval?: number // default: 50
}Key implementation considerations:
- Fake timer integration — if
rs.isFakeTimers()is true, advance timers byintervalon each retry instead of using realsetTimeout - Concurrency guard — don't start a new retry while a previous async callback is still pending
- Diagnostics — include retry count and elapsed time in the timeout error message
Result
// ✅ Built-in, zero setup
await rs.waitFor(async () => {
const res = await fetch(url);
expect(res.ok).toBe(true);
}, { timeout: 30_000, interval: 1_000 });This would make the vitest → rstest migration fully mechanical for projects that use vi.waitFor().