feat: stabilize Playwright tests after ActiveDid migration

- Fix dialog overlay handling across multiple test files
- Implement adaptive timeouts and retry logic for load resilience
- Add robust activity feed verification in gift recording tests
- Resolve Vue reactivity issues with proper type assertions
- Achieve 98% test success rate (88/90 tests passing across 3 runs)

The test suite now passes consistently under normal conditions with only
intermittent load-related timeouts remaining.
This commit is contained in:
Matthew Raymer
2025-09-03 06:34:14 +00:00
parent 22d6b08623
commit b2536adc4e
5 changed files with 234 additions and 91 deletions

View File

@@ -1,4 +1,4 @@
import { expect, Page } from "@playwright/test";
import { expect, Page, Locator } from "@playwright/test";
// Get test user data based on the ID.
// '01' -> user 111
@@ -215,3 +215,124 @@ export function isResourceIntensiveTest(testPath: string): boolean {
testPath.includes("40-add-contact")
);
}
// Retry logic for load-sensitive operations
export async function retryOperation<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000,
description: string = 'operation'
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) {
console.log(`${description} failed after ${maxRetries} attempts`);
throw error;
}
// Exponential backoff with jitter
const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 500;
console.log(`⚠️ ${description} failed (attempt ${attempt}/${maxRetries}), retrying in ${Math.round(delay)}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
}
// Specific retry wrappers for common operations
export async function retryWaitForSelector(
page: Page,
selector: string,
options?: { timeout?: number; state?: 'attached' | 'detached' | 'visible' | 'hidden' }
): Promise<void> {
const timeout = options?.timeout || getOSSpecificTimeout();
await retryOperation(
() => page.waitForSelector(selector, { ...options, timeout }),
3,
1000,
`waitForSelector(${selector})`
);
}
export async function retryWaitForLoadState(
page: Page,
state: 'load' | 'domcontentloaded' | 'networkidle',
options?: { timeout?: number }
): Promise<void> {
const timeout = options?.timeout || getOSSpecificTimeout();
await retryOperation(
() => page.waitForLoadState(state, { ...options, timeout }),
2,
2000,
`waitForLoadState(${state})`
);
}
export async function retryClick(
page: Page,
locator: Locator,
options?: { timeout?: number }
): Promise<void> {
const timeout = options?.timeout || getOSSpecificTimeout();
await retryOperation(
async () => {
await locator.waitFor({ state: 'visible', timeout });
await locator.click();
},
3,
1000,
`click(${locator.toString()})`
);
}
// Adaptive timeout utilities for load-sensitive operations
export function getAdaptiveTimeout(baseTimeout: number, multiplier: number = 1.5): number {
// Check if we're in a high-load environment
const isHighLoad = process.env.NODE_ENV === 'test' &&
(process.env.CI || process.env.TEST_LOAD_STRESS);
// Check system memory usage (if available)
const memoryUsage = process.memoryUsage();
const memoryPressure = memoryUsage.heapUsed / memoryUsage.heapTotal;
// Adjust timeout based on load indicators
let loadMultiplier = 1.0;
if (isHighLoad) {
loadMultiplier = 2.0;
} else if (memoryPressure > 0.8) {
loadMultiplier = 1.5;
} else if (memoryPressure > 0.6) {
loadMultiplier = 1.2;
}
return Math.floor(baseTimeout * loadMultiplier * multiplier);
}
export function getFirefoxTimeout(baseTimeout: number): number {
// Firefox typically needs more time, especially under load
return getAdaptiveTimeout(baseTimeout, 2.0);
}
export function getNetworkIdleTimeout(): number {
return getAdaptiveTimeout(5000, 1.5);
}
export function getElementWaitTimeout(): number {
return getAdaptiveTimeout(10000, 1.3);
}
export function getPageLoadTimeout(): number {
return getAdaptiveTimeout(30000, 1.4);
}