Implement a Jest waitFor Utility
Jest's waitFor function is a crucial tool for testing asynchronous operations. It allows tests to wait for a specific condition to become true before proceeding, making tests more robust and less prone to flaky failures. Your challenge is to implement a simplified version of this utility.
Problem Description
You need to create a TypeScript function named customWaitFor that mimics the core functionality of Jest's waitFor. This function should accept a callback that returns a promise, and it should repeatedly execute this callback until it resolves successfully or a timeout occurs.
Key Requirements:
- The
customWaitForfunction should accept two arguments:callback: A function that returns aPromise<T>for some typeT.options(optional): An object with the following properties:timeout: The maximum time in milliseconds to wait for the callback to resolve. Defaults to 1000ms.interval: The time in milliseconds to wait between retrying the callback. Defaults to 50ms.
- The
customWaitForfunction should return aPromise<T>that resolves with the value returned by thecallbackwhen it successfully resolves. - If the
callbackthrows an error or rejects its promise,customWaitForshould retry thecallbackafter the specifiedinterval, up to thetimeout. - If the
timeoutis reached before thecallbackresolves successfully,customWaitForshould reject with an error indicating that the timeout occurred. - The
callbackshould be executed at least once, even if thetimeoutis less than theinterval.
Expected Behavior:
- If the
callbackresolves within thetimeout, the returned promise should resolve with the resolved value. - If the
callbackcontinuously rejects or throws errors, and thetimeoutis reached, the returned promise should reject with a timeout error.
Edge Cases:
- What happens if
timeoutis 0? - What happens if
intervalis 0? - What happens if the
callbackresolves immediately? - What happens if the
callbackthrows synchronously?
Examples
Example 1:
async function resolveAfter100ms() {
return new Promise(resolve => setTimeout(() => resolve('Success!'), 100));
}
// Inside a test:
await customWaitFor(() => resolveAfter100ms(), { timeout: 500, interval: 50 });
// Expected output: Promise resolves with 'Success!'
Explanation: The resolveAfter100ms function resolves after 100ms. customWaitFor will retry it every 50ms. Since 100ms is well within the 500ms timeout, it will resolve with 'Success!'.
Example 2:
let counter = 0;
async function resolveAfter3Attempts() {
counter++;
if (counter === 3) {
return new Promise(resolve => resolve('Resolved after 3 attempts'));
}
return new Promise((_, reject) => reject(new Error('Not ready yet')));
}
// Inside a test:
await customWaitFor(() => resolveAfter3Attempts(), { timeout: 500, interval: 50 });
// Expected output: Promise resolves with 'Resolved after 3 attempts'
Explanation: The resolveAfter3Attempts function will reject twice before resolving on the third attempt. With an interval of 50ms, the third attempt will occur around 100ms, well within the 500ms timeout.
Example 3:
async function neverResolve() {
return new Promise(() => {}); // This promise never resolves
}
// Inside a test:
await customWaitFor(() => neverResolve(), { timeout: 200, interval: 50 });
// Expected output: Promise rejects with an error like "customWaitFor timed out in 200ms."
Explanation: The neverResolve function will never resolve or reject. customWaitFor will keep retrying until the 200ms timeout is reached, at which point it will reject.
Constraints
timeout: Must be a non-negative integer.interval: Must be a non-negative integer.- The
callbackfunction is assumed to be a function that returns aPromise.
Notes
- Consider how you will manage timers effectively.
setTimeoutandclearTimeoutwill be your primary tools. - Think about how to handle both promise rejections and synchronous errors thrown by the callback.
- The default values for
timeoutandintervalshould be respected if not provided. - The error message for timeout should be informative.