This document outlines our approach to end-to-end testing, emphasizing simplicity and reliability over comprehensive coverage. This philosophy emerged from solving Playwright test flakiness in CI environments.
- Start with the absolute minimum - single test, single assertion
- Add complexity incrementally - one check at a time
- Remove until it works - when debugging, strip everything down first
E2E tests should validate:
- ✅ Homepage loads successfully
- ✅ Core navigation works
- ✅ Essential user journeys function
E2E tests should NOT validate:
- ❌ Specific text content (too fragile)
- ❌ Complex user interactions (unit tests handle this)
- ❌ Edge cases or error scenarios
- ❌ Visual styling or layout details
- Better to have 2 tests that always pass than 20 tests that are flaky
- CI failures should indicate real problems, not test instability
- Simple assertions are more stable than complex ones
// ✅ Good - Minimal and focused
test('homepage loads', async ({ page }) => {
await page.goto('/');
await expect(page.locator('body')).toBeVisible();
await expect(page.locator('input')).toBeVisible();
});
// ❌ Bad - Too complex and fragile
test('homepage loads with all content', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
await page.waitForLoadState('domcontentloaded');
await expect(page.getByText('Analyze GitHub Repository Contributors')).toBeVisible();
await expect(page.locator('input[placeholder*="Search repositories"]')).toBeVisible();
await expect(page.locator('h1, h2, h3')).toBeVisible();
});// ✅ Good - Simple configuration
export default defineConfig({
workers: 1, // Avoid resource conflicts
timeout: 30000, // Use defaults
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});
// ❌ Bad - Over-engineered configuration
export default defineConfig({
workers: process.env.CI ? 8 : undefined,
timeout: process.env.CI ? 120000 : 30000,
webServer: process.env.CI ? {
command: 'npm run build && npm run preview',
url: 'http://localhost:4173',
// ... complex CI-specific logic
} : {
// ... different local logic
},
});When e2e tests fail in CI:
- Remove all but one test
- Remove all but one assertion
- Use default timeouts
- Single worker only
- Once one test passes, add one more
- Add one assertion at a time
- If something breaks, you know exactly what caused it
- Vite dev server flakiness - Use simple server setup
- Resource conflicts - Always use
workers: 1 - Timing issues - Avoid custom timeouts and wait strategies
- Content loading - Check for
bodyexistence, not specific text
A developer with the exact same issue found that:
- Tests worked locally but timed out in GitHub Actions
- Problem was multiple workers and complex server setup
- Solution: "Remove things until it works, then add back 1 by 1"
- Vite dev server can be unreliable in CI environments
- Preview builds are more stable but add complexity
- Simple dev server setup works fine for minimal tests
- Use CLI instead of GitHub Action
- Install browsers with
--with-deps - Keep test scenarios focused and simple
Our minimal test suite validates:
test.describe('Critical User Flows', () => {
test('homepage loads', async ({ page }) => {
await page.goto('/');
await expect(page.locator('body')).toBeVisible();
await expect(page.locator('input')).toBeVisible();
});
test('repository page loads', async ({ page }) => {
await page.goto('/facebook/react');
await expect(page.locator('body')).toBeVisible();
});
});This covers:
- Homepage accessibility - Site loads, search input present
- Routing functionality - Can navigate to repository pages
- Reliable in CI - Tests consistently pass
- Fast execution - Minimal overhead
- Clear purpose - Catches real breakages
- Easy debugging - Simple assertions, obvious failures
- Low maintenance - Rarely needs updates
- Limited coverage - Doesn't test complex interactions
- Surface-level - Won't catch subtle bugs
- Requires discipline - Team must resist adding complexity
Before adding a new e2e test, ask:
- Is this a critical user flow? (Can users complete their main task?)
- Would this break the entire application? (Not just a feature)
- Can this be tested more reliably at the unit level?
If yes to 1 & 2, and no to 3, then add a minimal e2e test.
A successful e2e test suite:
- ✅ Passes consistently in CI (>99% reliability)
- ✅ Runs quickly (<2 minutes total)
- ✅ Catches real deployment issues
- ✅ Rarely produces false positives
- ✅ Requires minimal maintenance
E2E tests are deployment smoke tests, not comprehensive feature validation. Keep them minimal, focused, and reliable. Use unit and integration tests for detailed functionality validation.
"Perfect is the enemy of good" - A working minimal test suite is infinitely better than a comprehensive but flaky one.