Agent skill
e2e-test-optimizer
Optimize Playwright E2E tests by removing anti-patterns, implementing smart waits, enabling test sharding, and improving reliability.
Install this agent skill to your Project
npx add-skill https://github.com/d-oit/do-novelist-ai/tree/main/.opencode/skill/e2e-test-optimizer
SKILL.md
E2E Test Optimizer
Specialized skill for optimizing Playwright E2E tests to eliminate flakiness, improve speed, and ensure reliable CI/CD execution.
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
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
Core Methodology
Systematic E2E optimization focusing on three pillars: Reliability, Speed, and Maintainability.
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
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
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
Anti-patterns to Eliminate
1. waitForTimeout Anti-pattern
Problem: Fixed time delays are flaky and slow down tests
// ❌ BAD - Flaky, slow
await page.waitForTimeout(2000);
await expect(element).toBeVisible();
// ✅ GOOD - Reliable, fast
await expect(element).toBeVisible({ timeout: 5000 });
2. Brittle Selectors Anti-pattern
Problem: CSS/XPath selectors break on UI changes
// ❌ 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();
3. Missing Network Waits Anti-pattern
Problem: Race conditions between user action and network requests
// ❌ 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();
4. Hardcoded Timeout Anti-pattern
Problem: One-size-fits-all timeout doesn't work for all tests
// ❌ 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 });
Best Practices
✅ Do
- Use
expect().toBeVisible()for automatic waiting - Use
data-testidattributes for element selection - 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)
❌ Don't
- Use
waitForTimeout()- always wait for state - 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
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
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
Smart Waiting Strategies
waitForLoadState
Wait for different load states based on context:
// For navigation
await page.waitForLoadState('load');
// For AJAX requests
await page.waitForLoadState('networkidle');
// For dynamic content
await page.waitForLoadState('domcontentloaded');
expect() Built-in Waiting
Use Playwright's auto-waiting assertions:
// 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();
Locators with Filters
Combine locators with smart filters:
// 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();
Test Sharding Strategy
GitHub Actions Matrix
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=2
Dynamic Sharding
# Calculate 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
CI/CD Integration
Environment Variables
# Required for Playwright in CI
export CI=true
export PLAYWRIGHT_BROWSERS_PATH=~/.cache/ms-playwright
export NODE_ENV=test
export NODE_OPTIONS=--max-old-space-size=4096
Optimized Test Command
pnpm exec playwright test \
--project=chromium \
--reporter=list,html,json \
--retries=2 \
--timeout=30000 \
--workers=2 \
--max-failures=5
Caching Strategy
- 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:
// ❌ DON'T - Wait arbitrary time
await page.waitForTimeout(500);
// ✅ DO - Wait for animation to complete
await page.waitForLoadState('domcontentloaded');
Lazy Loading
Lazy-loaded components need special handling:
// ❌ 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();
Multiple Loading States
Handle skeleton loaders gracefully:
// Wait for loading state to complete
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:
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`);
}
});
});
Failure Analysis
Categorize failures for targeted fixes:
test.afterEach(async () => {
if (test.info().status !== 'passed') {
// Take screenshot on failure
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
Create fast, reliable, and maintainable E2E tests.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
novel-development
Work on novel-specific features including plot engines, character development, world-building, timeline management, and GOAP-based story generation. Use when implementing narrative systems, character arcs, or story planning tools.
tech-stack-specialist
Manage framework usage, dependencies, build configuration, and environment setup. Use when adding new dependencies, updating packages, configuring build tools, or setting up development environment.
performance-engineer
Optimize application performance including build times, runtime speed, bundle size and resource usage. Use when addressing performance issues, implementing caching strategies, or optimizing rendering.
domain-expert
Apply domain-driven design principles for business logic, entities, events and aggregate boundaries. Use when modeling domain concepts, implementing business rules, or defining clear separation between domain and infrastructure layers.
qa-engineer
Define comprehensive testing strategies, write tests with proper naming conventions, organize tests by type, and implement mocking strategies. Use when creating tests, refactoring test suites, or improving test coverage.
writing-assistant
Work on writing assistance features including real-time style analysis, grammar checking, writing goals tracking, inline suggestions, and writing analytics. Use when implementing linguistic analysis, productivity tracking, or writing quality tools.
Didn't find tool you were looking for?