e2e-test-optimizer
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseE2E Test Optimizer
E2E测试优化方案
Specialized skill for optimizing Playwright E2E tests to eliminate flakiness,
improve speed, and ensure reliable CI/CD execution.
这是一款专门用于优化Playwright E2E测试的方案,旨在消除测试不稳定问题、提升测试速度,并确保CI/CD执行的可靠性。
Quick Reference
快速参考
- Remove Anti-patterns - Eliminate waitForTimeout and fixed delays
- Smart Waits - Implement state-based waiting strategies
- Test Sharding - Configure parallel test execution
- Reliability Patterns - Reduce test flakiness
- 移除反模式 - 消除waitForTimeout和固定延迟
- 智能等待 - 实现基于状态的等待策略
- 测试分片 - 配置并行测试执行
- 可靠性模式 - 减少测试不稳定问题
When to Use
适用场景
Use this skill when:
- E2E tests are timing out in CI
- Tests fail intermittently (flaky tests)
- Test execution time exceeds CI limits
- Multiple retries needed for tests to pass
- Tests rely on fixed time delays (anti-patterns)
- Looking to speed up CI/CD pipeline
- Need to improve test maintainability
在以下场景中使用本方案:
- E2E测试在CI中出现超时
- 测试间歇性失败(不稳定测试)
- 测试执行时间超出CI限制
- 测试需要多次重试才能通过
- 测试依赖固定时长延迟(反模式)
- 希望加速CI/CD流水线
- 需要提升测试可维护性
Core Methodology
核心方法论
Systematic E2E optimization focusing on three pillars: Reliability,
Speed, and Maintainability.
系统化的E2E优化围绕三大核心:可靠性、速度和可维护性。
Key Principles
关键原则
- State-based waiting - Wait for conditions, not arbitrary time
- Smart selectors - Use stable, semantic element targeting
- Test isolation - Clean setup/teardown to prevent interference
- Parallel execution - Shard tests for faster CI runs
- Proper mocking - Mock external dependencies for reliability
- Network awareness - Wait for network completion
- Accessibility-first - Use semantic selectors for stability
- 基于状态的等待 - 等待特定条件而非固定时长
- 智能选择器 - 使用稳定、语义化的元素定位方式
- 测试隔离 - 清理测试前后的状态以避免相互干扰
- 并行执行 - 通过分片实现更快的CI运行速度
- 合理模拟 - 模拟外部依赖以提升可靠性
- 网络感知 - 等待网络请求完成
- 优先可访问性 - 使用语义化选择器提升稳定性
Optimization Hierarchy
优化优先级
Priority 1: Remove anti-patterns (waitForTimeout)
↓
Priority 2: Implement smart waits (toBeVisible, toBeAttached)
↓
Priority 3: Optimize selectors (data-testid, locators)
↓
Priority 4: Enable test sharding (parallel execution)
↓
Priority 5: Add performance monitoring优先级1:移除反模式(waitForTimeout)
↓
优先级2:实现智能等待(toBeVisible、toBeAttached)
↓
优先级3:优化选择器(data-testid、定位器)
↓
优先级4:启用测试分片(并行执行)
↓
优先级5:添加性能监控Integration Points
集成点
Works with these agents for comprehensive testing strategy:
- qa-engineer: Test writing strategies and conventions
- performance-engineer: Test performance benchmarking
- debugger: Diagnose and fix flaky tests
- mock-infrastructure-engineer: Optimize mock setup and caching
可与以下角色协作以构建全面的测试策略:
- qa-engineer:测试编写策略与规范
- performance-engineer:测试性能基准测试
- debugger:诊断并修复不稳定测试
- mock-infrastructure-engineer:优化模拟环境的设置与缓存
Anti-patterns to Eliminate
需要消除的反模式
1. waitForTimeout Anti-pattern
1. waitForTimeout反模式
Problem: Fixed time delays are flaky and slow down tests
typescript
// ❌ BAD - Flaky, slow
await page.waitForTimeout(2000);
await expect(element).toBeVisible();
// ✅ GOOD - Reliable, fast
await expect(element).toBeVisible({ timeout: 5000 });问题:固定时长延迟会导致测试不稳定且拖慢速度
typescript
// ❌ 不良实践 - 不稳定、缓慢
await page.waitForTimeout(2000);
await expect(element).toBeVisible();
// ✅ 良好实践 - 可靠、快速
await expect(element).toBeVisible({ timeout: 5000 });2. Brittle Selectors Anti-pattern
2. 脆弱选择器反模式
Problem: CSS/XPath selectors break on UI changes
typescript
// ❌ BAD - Brittle
await page.locator('div:nth-child(2) > button').click();
await page.locator('.btn.primary').click();
// ✅ GOOD - Stable
await page.getByTestId('submit-button').click();
await page.getByRole('button', { name: 'Submit' }).click();问题:CSS/XPath选择器会因UI变更而失效
typescript
// ❌ 不良实践 - 脆弱
await page.locator('div:nth-child(2) > button').click();
await page.locator('.btn.primary').click();
// ✅ 良好实践 - 稳定
await page.getByTestId('submit-button').click();
await page.getByRole('button', { name: 'Submit' }).click();3. Missing Network Waits Anti-pattern
3. 缺少网络等待反模式
Problem: Race conditions between user action and network requests
typescript
// ❌ BAD - Race condition
await page.click('button');
await expect(page.getByText('Loaded')).toBeVisible();
// ✅ GOOD - Waits for network
await page.click('button');
await page.waitForLoadState('networkidle');
await expect(page.getByText('Loaded')).toBeVisible();问题:用户操作与网络请求之间存在竞争条件
typescript
// ❌ 不良实践 - 竞争条件
await page.click('button');
await expect(page.getByText('Loaded')).toBeVisible();
// ✅ 良好实践 - 等待网络完成
await page.click('button');
await page.waitForLoadState('networkidle');
await expect(page.getByText('Loaded')).toBeVisible();4. Hardcoded Timeout Anti-pattern
4. 硬编码超时反模式
Problem: One-size-fits-all timeout doesn't work for all tests
typescript
// ❌ BAD - Too short for slow tests, too long for fast
test.setTimeout(10000); // Global timeout
// ✅ GOOD - Per-test or per-assertion timeout
await expect(element).toBeVisible({ timeout: 5000 });
await expect(slowElement).toBeVisible({ timeout: 30000 });问题:统一的超时设置无法适配所有测试
typescript
// ❌ 不良实践 - 对慢测试太短,对快测试太长
test.setTimeout(10000); // 全局超时
// ✅ 良好实践 - 为单个测试或断言设置超时
await expect(element).toBeVisible({ timeout: 5000 });
await expect(slowElement).toBeVisible({ timeout: 30000 });Best Practices
最佳实践
✅ Do
✅ 推荐做法
- Use for automatic waiting
expect().toBeVisible() - Use attributes for element selection
data-testid - Wait for network idle:
page.waitForLoadState('networkidle') - Use Playwright locators for efficient queries
- Clean state between tests
- Mock external dependencies (AI services, databases)
- Implement test-level retry logic
- Shard tests for parallel execution
- Test at realistic network speeds
- Use semantic selectors (roles, labels)
- 使用实现自动等待
expect().toBeVisible() - 使用属性进行元素选择
data-testid - 等待网络空闲:
page.waitForLoadState('networkidle') - 使用Playwright定位器实现高效查询
- 在测试之间清理状态
- 模拟外部依赖(AI服务、数据库)
- 实现测试级别的重试逻辑
- 分片测试以实现并行执行
- 在真实网络速度下进行测试
- 使用语义化选择器(角色、标签)
❌ Don't
❌ 不推荐做法
- Use - always wait for state
waitForTimeout() - Rely on brittle selectors (CSS, XPath)
- Skip tests without fixing the root cause
- Ignore flaky tests - investigate and fix
- Test implementation details
- Use global timeouts - use per-assertion timeouts
- Assume immediate UI updates - wait for changes
- 使用- 始终等待特定状态
waitForTimeout() - 依赖脆弱的选择器(CSS、XPath)
- 不修复根本原因就跳过测试
- 忽略不稳定测试 - 应调查并修复
- 测试实现细节
- 使用全局超时 - 应为单个断言设置超时
- 假设UI会立即更新 - 等待状态变化
Performance Targets
性能目标
Speed Metrics
速度指标
- Test timeout: 30 seconds per test (configurable)
- Assertion timeout: 5 seconds typical (adjust per test)
- Worker count: 2-4 workers for CI
- Shard factor: 2-4 shards based on test count
- Target execution time: < 2 minutes for smoke tests
- 测试超时:每个测试30秒(可配置)
- 断言超时:通常为5秒(可根据测试调整)
- 工作进程数:CI环境中使用2-4个工作进程
- 分片系数:根据测试数量设置2-4个分片
- 目标执行时间:冒烟测试<2分钟
Optimization Goals
优化目标
- Reduce flaky tests: < 1% failure rate
- Improve execution speed: 20-30% faster with sharding
- Reduce CI time: Minimize total pipeline duration
- Maintain readability: Keep tests easy to understand
- 减少不稳定测试:失败率<1%
- 提升执行速度:通过分片提升20-30%
- 缩短CI时间:最小化流水线总时长
- 保持可读性:确保测试易于理解
Smart Waiting Strategies
智能等待策略
waitForLoadState
waitForLoadState
Wait for different load states based on context:
typescript
// For navigation
await page.waitForLoadState('load');
// For AJAX requests
await page.waitForLoadState('networkidle');
// For dynamic content
await page.waitForLoadState('domcontentloaded');根据上下文等待不同的加载状态:
typescript
// 页面导航时
await page.waitForLoadState('load');
// AJAX请求时
await page.waitForLoadState('networkidle');
// 动态内容加载时
await page.waitForLoadState('domcontentloaded');expect() Built-in Waiting
expect()内置等待
Use Playwright's auto-waiting assertions:
typescript
// Wait for element to appear
await expect(page.getByTestId('element')).toBeVisible();
// Wait for element to disappear
await expect(page.getByTestId('loading')).toBeHidden();
// Wait for text content
await expect(page.getByText('Success')).toBeVisible();
// Wait for element to be attached
await expect(page.getByTestId('element')).toBeAttached();使用Playwright的自动等待断言:
typescript
// 等待元素出现
await expect(page.getByTestId('element')).toBeVisible();
// 等待元素消失
await expect(page.getByTestId('loading')).toBeHidden();
// 等待文本内容出现
await expect(page.getByText('Success')).toBeVisible();
// 等待元素附加到DOM
await expect(page.getByTestId('element')).toBeAttached();Locators with Filters
带筛选器的定位器
Combine locators with smart filters:
typescript
// Wait for specific element in list
await expect(
page.getByTestId('item').filter({ hasText: 'Target' })
).toBeVisible();
// Wait for enabled button
await expect(
page.getByRole('button', { name: 'Submit' })
.and(page.getByRole('button', { disabled: false })
).toBeVisible();结合定位器与智能筛选器:
typescript
// 等待列表中的特定元素
await expect(
page.getByTestId('item').filter({ hasText: 'Target' })
).toBeVisible();
// 等待可用按钮
await expect(
page.getByRole('button', { name: 'Submit' })
.and(page.getByRole('button', { disabled: false })
).toBeVisible();Test Sharding Strategy
测试分片策略
GitHub Actions Matrix
GitHub Actions矩阵配置
yaml
strategy:
matrix:
shard_index: [0, 1, 2, 3]
total_shards: [4]
steps:
- name: Run E2E tests
run: |
pnpm exec playwright test \
--project=chromium \
--shard=${{ matrix.shard_index }}/${{ matrix.total_shards }} \
--retries=2yaml
strategy:
matrix:
shard_index: [0, 1, 2, 3]
total_shards: [4]
steps:
- name: Run E2E tests
run: |
pnpm exec playwright test \
--project=chromium \
--shard=${{ matrix.shard_index }}/${{ matrix.total_shards }} \
--retries=2Dynamic Sharding
动态分片
bash
undefinedbash
undefinedCalculate shards based on test count
根据测试数量计算分片
SHARD_COUNT=4
TEST_COUNT=$(pnpm exec playwright test --list 2>/dev/null | grep -c '›')
SHARD_SIZE=$((TEST_COUNT / SHARD_COUNT + 1))
for i in $(seq 0 $((SHARD_COUNT - 1))); do
pnpm exec playwright test
--shard=$i/$SHARD_COUNT
--output=test-results/shard-$i done
--shard=$i/$SHARD_COUNT
--output=test-results/shard-$i done
undefinedSHARD_COUNT=4
TEST_COUNT=$(pnpm exec playwright test --list 2>/dev/null | grep -c '›')
SHARD_SIZE=$((TEST_COUNT / SHARD_COUNT + 1))
for i in $(seq 0 $((SHARD_COUNT - 1))); do
pnpm exec playwright test
--shard=$i/$SHARD_COUNT
--output=test-results/shard-$i done
--shard=$i/$SHARD_COUNT
--output=test-results/shard-$i done
undefinedCI/CD Integration
CI/CD集成
Environment Variables
环境变量
bash
undefinedbash
undefinedRequired for Playwright in CI
Playwright在CI中运行所需的环境变量
export CI=true
export PLAYWRIGHT_BROWSERS_PATH=~/.cache/ms-playwright
export NODE_ENV=test
export NODE_OPTIONS=--max-old-space-size=4096
undefinedexport CI=true
export PLAYWRIGHT_BROWSERS_PATH=~/.cache/ms-playwright
export NODE_ENV=test
export NODE_OPTIONS=--max-old-space-size=4096
undefinedOptimized Test Command
优化后的测试命令
bash
pnpm exec playwright test \
--project=chromium \
--reporter=list,html,json \
--retries=2 \
--timeout=30000 \
--workers=2 \
--max-failures=5bash
pnpm exec playwright test \
--project=chromium \
--reporter=list,html,json \
--retries=2 \
--timeout=30000 \
--workers=2 \
--max-failures=5Caching Strategy
缓存策略
yaml
- name: Cache Playwright browsers
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}yaml
- name: Cache Playwright browsers
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}Common Pitfalls
常见陷阱
Animation Delays
动画延迟
Animations can cause tests to be flaky:
typescript
// ❌ DON'T - Wait arbitrary time
await page.waitForTimeout(500);
// ✅ DO - Wait for animation to complete
await page.waitForLoadState('domcontentloaded');动画可能导致测试不稳定:
typescript
// ❌ 不推荐 - 等待固定时长
await page.waitForTimeout(500);
// ✅ 推荐 - 等待动画完成
await page.waitForLoadState('domcontentloaded');Lazy Loading
懒加载
Lazy-loaded components need special handling:
typescript
// ❌ DON'T - Element might not exist yet
await expect(page.getByTestId('lazy-content')).toBeVisible();
// ✅ DO - Scroll into view first
await page.getByTestId('lazy-container').scrollIntoViewIfNeeded();
await expect(page.getByTestId('lazy-content')).toBeVisible();懒加载组件需要特殊处理:
typescript
// ❌ 不推荐 - 元素可能尚未存在
await expect(page.getByTestId('lazy-content')).toBeVisible();
// ✅ 推荐 - 先滚动到视图中
await page.getByTestId('lazy-container').scrollIntoViewIfNeeded();
await expect(page.getByTestId('lazy-content')).toBeVisible();Multiple Loading States
多重加载状态
Handle skeleton loaders gracefully:
typescript
// Wait for loading state to complete
await expect(page.getByTestId('loading')).toBeVisible();
await expect(page.getByTestId('loading')).toBeHidden();
await expect(page.getByTestId('content')).toBeVisible();优雅处理骨架加载器:
typescript
// 等待加载状态完成
await expect(page.getByTestId('loading')).toBeVisible();
await expect(page.getByTestId('loading')).toBeHidden();
await expect(page.getByTestId('content')).toBeVisible();Monitoring and Metrics
监控与指标
Test Execution Time
测试执行时间
Track per-test execution to identify slow tests:
typescript
test.describe('Feature', () => {
test('slow test', async ({ page }) => {
const startTime = Date.now();
// ... test code ...
const duration = Date.now() - startTime;
if (duration > 5000) {
console.warn(`Test took ${duration}ms - consider optimization`);
}
});
});跟踪单个测试的执行时间以识别慢测试:
typescript
test.describe('Feature', () => {
test('slow test', async ({ page }) => {
const startTime = Date.now();
// ... 测试代码 ...
const duration = Date.now() - startTime;
if (duration > 5000) {
console.warn(`测试耗时${duration}ms - 建议优化`);
}
});
});Failure Analysis
失败分析
Categorize failures for targeted fixes:
typescript
test.afterEach(async () => {
if (test.info().status !== 'passed') {
// Take screenshot on failure
await page.screenshot({
path: `failures/${test.info().title}.png`,
fullPage: true,
});
}
});对失败进行分类以实现针对性修复:
typescript
test.afterEach(async () => {
if (test.info().status !== 'passed') {
// 失败时截图
await page.screenshot({
path: `failures/${test.info().title}.png`,
fullPage: true,
});
}
});Content Modules
内容模块
- Remove Anti-patterns - Detailed anti-pattern removal guide
- Smart Waits - Comprehensive waiting strategies
- Test Sharding - Parallel execution setup
- Reliability Patterns - Reduce test flakiness
- 移除反模式 - 详细的反模式移除指南
- 智能等待 - 全面的等待策略
- 测试分片 - 并行执行设置
- 可靠性模式 - 减少测试不稳定问题