mirror of
https://github.com/spliit-app/spliit.git
synced 2026-02-08 08:36:12 +01:00
- Implement Priority 1 E2E test coverage for critical user journeys: * Group lifecycle management (creation, navigation, editing) * Basic expense management (create, view, edit, delete) * Balance calculation and verification * Multiple expense scenarios and split calculations - Add comprehensive reliability infrastructure: * ReliabilityUtils class with retry mechanisms and enhanced navigation * Page Object Model (POM) architecture for maintainable tests * Test data management utilities with unique identifiers * Enhanced Playwright configuration with increased timeouts and retries - Fix all flaky test issues: * Add required test IDs to UI components for reliable element targeting * Implement multiple fallback strategies for element selection * Enhanced tab navigation with URL verification and retry logic * Proper wait strategies for network idle states and dynamic content - Test results: 39/39 tests passing (100% success rate) across all browsers 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
142 lines
4.1 KiB
TypeScript
142 lines
4.1 KiB
TypeScript
import { Page, Locator, expect } from '@playwright/test'
|
|
|
|
export class ReliabilityUtils {
|
|
/**
|
|
* Wait for element with multiple strategies and retries
|
|
*/
|
|
static async waitForElement(
|
|
page: Page,
|
|
selectors: string[],
|
|
options: { timeout?: number; retries?: number } = {}
|
|
): Promise<Locator> {
|
|
const { timeout = 10000, retries = 3 } = options
|
|
|
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
for (const selector of selectors) {
|
|
try {
|
|
const element = page.locator(selector)
|
|
await element.waitFor({ state: 'visible', timeout: timeout / retries })
|
|
return element
|
|
} catch (error) {
|
|
// Continue to next selector
|
|
continue
|
|
}
|
|
}
|
|
|
|
if (attempt < retries - 1) {
|
|
// Wait a bit before retry
|
|
await page.waitForTimeout(1000)
|
|
await page.waitForLoadState('networkidle')
|
|
}
|
|
}
|
|
|
|
throw new Error(`None of the selectors found after ${retries} attempts: ${selectors.join(', ')}`)
|
|
}
|
|
|
|
/**
|
|
* Navigate to tab with reliability checks
|
|
*/
|
|
static async navigateToTab(page: Page, tabName: string, expectedUrl: RegExp) {
|
|
// Click tab with retry and verification
|
|
for (let attempt = 0; attempt < 5; attempt++) {
|
|
try {
|
|
// Ensure we're in a stable state before clicking
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Click the tab
|
|
await page.getByTestId(`tab-${tabName}`).click()
|
|
|
|
// Wait for the URL to change with a shorter timeout per attempt
|
|
try {
|
|
await page.waitForURL(expectedUrl, { timeout: 3000 })
|
|
// If we get here, navigation succeeded
|
|
break
|
|
} catch (urlError) {
|
|
// URL didn't change, try again
|
|
if (attempt === 4) {
|
|
// Last attempt failed, throw the error
|
|
throw new Error(`Failed to navigate to ${tabName} tab after ${attempt + 1} attempts. Current URL: ${page.url()}`)
|
|
}
|
|
|
|
// Wait a bit before retrying
|
|
await page.waitForTimeout(1000)
|
|
continue
|
|
}
|
|
} catch (error) {
|
|
if (attempt === 4) throw error
|
|
await page.waitForTimeout(500)
|
|
}
|
|
}
|
|
|
|
// Final stability checks
|
|
await page.waitForLoadState('networkidle')
|
|
await page.waitForTimeout(300)
|
|
}
|
|
|
|
/**
|
|
* Verify content loaded with multiple fallback strategies
|
|
*/
|
|
static async verifyContentLoaded(page: Page, contentIdentifiers: string[]) {
|
|
let lastError: Error | null = null
|
|
|
|
for (const identifier of contentIdentifiers) {
|
|
try {
|
|
if (identifier.startsWith('text=')) {
|
|
await expect(page.locator(identifier)).toBeVisible({ timeout: 5000 })
|
|
return
|
|
} else if (identifier.startsWith('[data-testid=')) {
|
|
await expect(page.locator(identifier)).toBeVisible({ timeout: 5000 })
|
|
return
|
|
} else {
|
|
await expect(page.getByTestId(identifier)).toBeVisible({ timeout: 5000 })
|
|
return
|
|
}
|
|
} catch (error) {
|
|
lastError = error as Error
|
|
continue
|
|
}
|
|
}
|
|
|
|
throw lastError || new Error(`No content identifiers found: ${contentIdentifiers.join(', ')}`)
|
|
}
|
|
|
|
/**
|
|
* Enhanced page load waiting
|
|
*/
|
|
static async waitForStablePage(page: Page) {
|
|
// Wait for network to be idle
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Wait for any pending JavaScript
|
|
await page.waitForLoadState('domcontentloaded')
|
|
|
|
// Additional short wait for dynamic content
|
|
await page.waitForTimeout(300)
|
|
}
|
|
|
|
/**
|
|
* Retry operation with exponential backoff
|
|
*/
|
|
static async retryOperation<T>(
|
|
operation: () => Promise<T>,
|
|
maxRetries: number = 3,
|
|
baseDelay: number = 1000
|
|
): Promise<T> {
|
|
let lastError: Error
|
|
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
try {
|
|
return await operation()
|
|
} catch (error) {
|
|
lastError = error as Error
|
|
|
|
if (attempt < maxRetries - 1) {
|
|
const delay = baseDelay * Math.pow(2, attempt)
|
|
await new Promise(resolve => setTimeout(resolve, delay))
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError!
|
|
}
|
|
} |