AsyncTestUtils.ts•9.89 kB
import { expect, vi } from 'vitest';
/**
* Utilities for testing asynchronous operations
*/
export class AsyncTestUtils {
/**
* Setup fake timers for testing
*/
static setupFakeTimers(): void {
vi.useFakeTimers();
}
/**
* Restore real timers after testing
*/
static restoreRealTimers(): void {
vi.useRealTimers();
}
/**
* Advance fake timers by a specific amount
*/
static async advanceTimers(ms: number): Promise<void> {
vi.advanceTimersByTime(ms);
await vi.runAllTimersAsync();
}
/**
* Run all pending timers
*/
static async runAllTimers(): Promise<void> {
await vi.runAllTimersAsync();
}
/**
* Run only the next timer
*/
static async runOnlyPendingTimers(): Promise<void> {
await vi.runOnlyPendingTimersAsync();
}
/**
* Wait for a promise to resolve with timeout
*/
static async waitForPromise<T>(promise: Promise<T>, timeout: number = 5000): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(`Promise timed out after ${timeout}ms`)), timeout);
}),
]);
}
/**
* Wait for a condition to be true
*/
static async waitForCondition(
condition: () => boolean | Promise<boolean>,
timeout: number = 5000,
interval: number = 100,
): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeout) {
const result = await condition();
if (result) {
return;
}
await AsyncTestUtils.sleep(interval);
}
throw new Error(`Condition not met within ${timeout}ms`);
}
/**
* Wait for a specific value to be returned by a function
*/
static async waitForValue<T>(
getValue: () => T | Promise<T>,
expectedValue: T,
timeout: number = 5000,
interval: number = 100,
): Promise<void> {
await AsyncTestUtils.waitForCondition(
async () => {
const value = await getValue();
return value === expectedValue;
},
timeout,
interval,
);
}
/**
* Wait for a mock to be called
*/
static async waitForMockCall(mock: any, timeout: number = 5000, interval: number = 100): Promise<void> {
await AsyncTestUtils.waitForCondition(() => mock.mock.calls.length > 0, timeout, interval);
}
/**
* Wait for a mock to be called a specific number of times
*/
static async waitForMockCallCount(
mock: any,
expectedCount: number,
timeout: number = 5000,
interval: number = 100,
): Promise<void> {
await AsyncTestUtils.waitForCondition(() => mock.mock.calls.length === expectedCount, timeout, interval);
}
/**
* Create a promise that resolves after a delay
*/
static sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Create a promise that rejects after a delay
*/
static rejectAfter(ms: number, error: Error = new Error('Timeout')): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => reject(error), ms);
});
}
/**
* Test that a function completes within a specific time
*/
static async expectCompletesWithin<T>(fn: () => Promise<T>, maxDuration: number): Promise<T> {
const start = Date.now();
const result = await fn();
const duration = Date.now() - start;
expect(duration).toBeLessThanOrEqual(maxDuration);
return result;
}
/**
* Test that a function takes at least a specific amount of time
*/
static async expectTakesAtLeast<T>(fn: () => Promise<T>, minDuration: number): Promise<T> {
const start = Date.now();
const result = await fn();
const duration = Date.now() - start;
expect(duration).toBeGreaterThanOrEqual(minDuration);
return result;
}
/**
* Test that a promise resolves
*/
static async expectResolves<T>(promise: Promise<T>): Promise<void> {
await expect(promise).resolves.toBeDefined();
}
/**
* Test that a promise rejects
*/
static async expectRejects(promise: Promise<any>): Promise<any> {
return expect(promise).rejects.toBeDefined();
}
/**
* Test that a promise rejects with a specific error
*/
static async expectRejectsWith(promise: Promise<any>, expectedError: string | RegExp | Error): Promise<any> {
if (typeof expectedError === 'string') {
return expect(promise).rejects.toThrow(expectedError);
} else if (expectedError instanceof RegExp) {
return expect(promise).rejects.toThrow(expectedError);
} else {
return expect(promise).rejects.toThrow(expectedError);
}
}
/**
* Create a controllable promise that can be resolved/rejected manually
*/
static createControllablePromise<T>(): {
promise: Promise<T>;
resolve: (value: T) => void;
reject: (reason?: any) => void;
isPending: () => boolean;
} {
let resolve: (value: T) => void;
let reject: (reason?: any) => void;
let isPending = true;
const promise = new Promise<T>((res, rej) => {
resolve = (value: T) => {
isPending = false;
res(value);
};
reject = (reason?: any) => {
isPending = false;
rej(reason);
};
});
return {
promise,
resolve: resolve!,
reject: reject!,
isPending: () => isPending,
};
}
/**
* Create a promise that resolves to a specific value after a delay
*/
static resolveAfter<T>(value: T, delay: number): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => resolve(value), delay);
});
}
/**
* Create a sequence of promises that resolve in order
*/
static createPromiseSequence<T>(values: T[], delay: number = 100): Promise<T>[] {
return values.map((value, index) => AsyncTestUtils.resolveAfter(value, delay * (index + 1)));
}
/**
* Test retry logic with exponential backoff
*/
static async testRetryLogic<T>(
fn: () => Promise<T>,
maxAttempts: number = 3,
baseDelay: number = 100,
backoffFactor: number = 2,
): Promise<{
result: T;
attempts: number;
totalTime: number;
}> {
const start = Date.now();
let attempts = 0;
let lastError: Error;
while (attempts < maxAttempts) {
attempts++;
try {
const result = await fn();
return {
result,
attempts,
totalTime: Date.now() - start,
};
} catch (error) {
lastError = error as Error;
if (attempts < maxAttempts) {
const delay = baseDelay * Math.pow(backoffFactor, attempts - 1);
await AsyncTestUtils.sleep(delay);
}
}
}
throw lastError!;
}
/**
* Test concurrent operations
*/
static async testConcurrency<T>(operations: (() => Promise<T>)[], maxConcurrency: number = 3): Promise<T[]> {
const results: T[] = [];
const executing: Promise<void>[] = [];
for (let i = 0; i < operations.length; i++) {
const operation = operations[i];
const promise = operation().then((result) => {
results[i] = result;
});
executing.push(promise);
if (executing.length >= maxConcurrency) {
await Promise.race(executing);
executing.splice(
executing.findIndex((p) => p === promise),
1,
);
}
}
await Promise.all(executing);
return results;
}
/**
* Test that operations are executed in a specific order
*/
static createOrderTracker(): {
track: (id: string) => void;
getOrder: () => string[];
expectOrder: (expectedOrder: string[]) => void;
} {
const order: string[] = [];
return {
track: (id: string) => {
order.push(id);
},
getOrder: () => [...order],
expectOrder: (expectedOrder: string[]) => {
expect(order).toEqual(expectedOrder);
},
};
}
/**
* Test debouncing behavior
*/
static async testDebounce<T>(
debouncedFn: (...args: any[]) => Promise<T>,
calls: { args: any[]; delay: number }[],
_expectedCallCount: number,
): Promise<void> {
const promises: Promise<T>[] = [];
for (const call of calls) {
await AsyncTestUtils.sleep(call.delay);
promises.push(debouncedFn(...call.args));
}
await Promise.all(promises);
// Additional assertions would need to be implemented based on the specific debounce implementation
// This is a framework for testing debounce behavior
}
/**
* Test throttling behavior
*/
static async testThrottle<T>(
throttledFn: (...args: any[]) => Promise<T>,
calls: { args: any[]; delay: number }[],
_expectedCallCount: number,
): Promise<void> {
const promises: Promise<T>[] = [];
for (const call of calls) {
await AsyncTestUtils.sleep(call.delay);
promises.push(throttledFn(...call.args));
}
await Promise.all(promises);
// Additional assertions would need to be implemented based on the specific throttle implementation
// This is a framework for testing throttle behavior
}
/**
* Measure the performance of an async operation
*/
static async measurePerformance<T>(
operation: () => Promise<T>,
iterations: number = 1,
): Promise<{
result: T;
averageTime: number;
minTime: number;
maxTime: number;
times: number[];
}> {
const times: number[] = [];
let result: T;
for (let i = 0; i < iterations; i++) {
const start = performance.now();
result = await operation();
const duration = performance.now() - start;
times.push(duration);
}
const averageTime = times.reduce((sum, time) => sum + time, 0) / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
return {
result: result!,
averageTime,
minTime,
maxTime,
times,
};
}
}