# E2E Test Stability Guide ## Architecture ``` tests/e2e/ ├── playwright.config.ts # Main config (sharding, multi-browser) ├── global-setup.ts # Creates test users via API ├── global-teardown.ts # Cleanup ├── helpers.ts # Core helpers (login, navigate, assert) ├── helpers/ │ └── selectors.ts # Centralized selectors (SEL object) ├── fixtures/ │ ├── auth.fixture.ts # API-driven auth fixtures │ ├── factories.ts # Test data factories (user, playlist, etc.) │ └── file-helpers.ts # Mock MP3 file generators ├── *.spec.ts # Test specs └── audit/ # Audit-specific specs (a11y, visual, etc.) ``` ## Selectors **Always use `data-testid` for E2E selectors.** Import from `helpers/selectors.ts`: ```ts import { SEL } from './helpers/selectors'; // Good await page.getByTestId(SEL.toast.success); await page.getByTestId(SEL.dialog.confirm); // Bad — fragile, breaks on text changes await page.getByText('Create'); await page.locator('button.submit'); ``` Component `data-testid` are defined in `apps/web/src/components/ui/testids.ts` and mirrored in `SEL`. ## Authentication **Use API login, not UI login** for tests that don't test the login flow: ```ts import { test, expect } from '../fixtures/auth.fixture'; test('playlist CRUD', async ({ listenerPage }) => { // listenerPage is already authenticated via API await listenerPage.goto('/playlists'); }); ``` ## Data Factories **Create test data via API, not UI clicks:** ```ts import { createPlaylist, ensureTracksExist } from '../fixtures/factories'; test('add track to playlist', async ({ creatorPage }) => { const playlist = await createPlaylist(creatorPage, { name: 'Test Playlist' }); // ... }); ``` ## CI Configuration - **e2e-critical**: `@critical` tag only, Chromium, blocks PR (<3min) - **e2e-full**: All tests, 4-way sharded, all browsers, 10-15min ## Debugging Flaky Tests 1. Run locally with trace: `PLAYWRIGHT_TRACE=on npm run e2e:serial` 2. Check `tests/e2e/test-results/` for trace files 3. Open trace: `npx playwright show-trace ` 4. Check flaky report: `node scripts/flaky-detection.mjs` ## Common Pitfalls | Problem | Solution | |---------|----------| | Toast selector mismatch | Use `data-testid="toast-success"` not text content | | Dialog button collision | Use `data-testid="dialog-confirm"` not `getByText('Create')` | | Rate limit in tests | Env `DISABLE_RATE_LIMIT_FOR_TESTS=true` | | Stale selector after navigation | Wait for `main` element after `navigateTo()` | | Login flaky | Use `loginViaAPI()` instead of `loginViaUI()` |