Files
gym-manage/gym-manage-web/e2e/utils/RetryHelper.ts
T
张翔 d2cef85187 docs: add test report and database reset scripts
- Add comprehensive test report (TEST_REPORT.md)
- Add database reset scripts for testing
- Update .gitignore to exclude temporary files
- Add frontend e2e test utilities and configuration
2026-04-23 16:36:12 +08:00

289 lines
6.7 KiB
TypeScript

export class RetryHelper {
static async retry<T>(
fn: () => Promise<T>,
options: {
maxAttempts?: number;
delay?: number;
backoff?: boolean;
onRetry?: (attempt: number, error: Error) => void;
} = {}
): Promise<T> {
const {
maxAttempts = 3,
delay = 1000,
backoff = true,
onRetry
} = options;
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt === maxAttempts) {
throw lastError;
}
if (onRetry) {
onRetry(attempt, lastError);
}
const currentDelay = backoff ? delay * attempt : delay;
await this.sleep(currentDelay);
}
}
throw lastError!;
}
static async retryWithCondition<T>(
fn: () => Promise<T>,
condition: (result: T) => boolean,
options: {
maxAttempts?: number;
delay?: number;
timeout?: number;
onRetry?: (attempt: number, lastResult: T) => void;
} = {}
): Promise<T> {
const {
maxAttempts = 10,
delay = 500,
timeout = 10000,
onRetry
} = options;
const startTime = Date.now();
let lastResult: T | undefined;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
lastResult = await fn();
if (condition(lastResult)) {
return lastResult;
}
if (Date.now() - startTime > timeout) {
throw new Error(`Timeout after ${timeout}ms waiting for condition to be met`);
}
if (onRetry && lastResult !== undefined) {
onRetry(attempt, lastResult);
}
await this.sleep(delay);
} catch (error) {
if (Date.now() - startTime > timeout) {
throw new Error(`Timeout after ${timeout}ms: ${error}`);
}
await this.sleep(delay);
}
}
throw new Error(`Condition not met after ${maxAttempts} attempts`);
}
static async retryElementAction<T>(
fn: () => Promise<T>,
options: {
maxAttempts?: number;
delay?: number;
ignoreErrors?: string[];
} = {}
): Promise<T> {
const {
maxAttempts = 3,
delay = 1000,
ignoreErrors = ['Timeout', 'Element not found', 'Element not visible']
} = options;
return this.retry(fn, {
maxAttempts,
delay,
backoff: true,
onRetry: (attempt, error) => {
const shouldIgnore = ignoreErrors.some(ignoredError =>
error.message.includes(ignoredError)
);
if (shouldIgnore) {
console.log(`Attempt ${attempt} failed with ignorable error: ${error.message}`);
}
}
});
}
static async retryNetworkRequest<T>(
fn: () => Promise<T>,
options: {
maxAttempts?: number;
delay?: number;
retryableStatuses?: number[];
} = {}
): Promise<T> {
const {
maxAttempts = 3,
delay = 2000,
retryableStatuses = [408, 429, 500, 502, 503, 504]
} = options;
return this.retry(fn, {
maxAttempts,
delay,
backoff: true,
onRetry: (attempt, error) => {
console.log(`Network request attempt ${attempt} failed: ${error.message}`);
}
});
}
static async retryClick(
clickFn: () => Promise<void>,
options: {
maxAttempts?: number;
delay?: number;
} = {}
): Promise<void> {
const { maxAttempts = 3, delay = 500 } = options;
return this.retry(clickFn, {
maxAttempts,
delay,
backoff: false,
onRetry: (attempt, error) => {
console.log(`Click attempt ${attempt} failed: ${error.message}`);
}
});
}
static async retryFill(
fillFn: () => Promise<void>,
options: {
maxAttempts?: number;
delay?: number;
} = {}
): Promise<void> {
const { maxAttempts = 3, delay = 500 } = options;
return this.retry(fillFn, {
maxAttempts,
delay,
backoff: false,
onRetry: (attempt, error) => {
console.log(`Fill attempt ${attempt} failed: ${error.message}`);
}
});
}
static async retryNavigation(
navigateFn: () => Promise<void>,
options: {
maxAttempts?: number;
delay?: number;
} = {}
): Promise<void> {
const { maxAttempts = 3, delay = 1000 } = options;
return this.retry(navigateFn, {
maxAttempts,
delay,
backoff: true,
onRetry: (attempt, error) => {
console.log(`Navigation attempt ${attempt} failed: ${error.message}`);
}
});
}
static async retryAssertion<T>(
assertionFn: () => Promise<T>,
options: {
maxAttempts?: number;
delay?: number;
} = {}
): Promise<T> {
const { maxAttempts = 5, delay = 500 } = options;
return this.retry(assertionFn, {
maxAttempts,
delay,
backoff: false,
onRetry: (attempt, error) => {
console.log(`Assertion attempt ${attempt} failed: ${error.message}`);
}
});
}
private static sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
static createRetryPolicy<T>(
fn: () => Promise<T>,
policy: {
maxAttempts: number;
initialDelay: number;
maxDelay?: number;
backoffMultiplier?: number;
retryCondition?: (error: Error) => boolean;
}
): () => Promise<T> {
const {
maxAttempts,
initialDelay,
maxDelay = 30000,
backoffMultiplier = 2,
retryCondition
} = policy;
return async () => {
let currentDelay = initialDelay;
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (retryCondition && !retryCondition(lastError)) {
throw lastError;
}
if (attempt === maxAttempts) {
throw lastError;
}
console.log(`Attempt ${attempt}/${maxAttempts} failed: ${lastError.message}`);
await this.sleep(currentDelay);
currentDelay = Math.min(currentDelay * backoffMultiplier, maxDelay);
}
}
throw lastError!;
};
}
static async retryWithTimeout<T>(
fn: () => Promise<T>,
timeout: number,
options: {
maxAttempts?: number;
delay?: number;
} = {}
): Promise<T> {
const { maxAttempts = 3, delay = 1000 } = options;
return Promise.race([
this.retry(fn, { maxAttempts, delay }),
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error(`Operation timed out after ${timeout}ms`)), timeout)
)
]);
}
}