--- name: playwright-e2e description: > Write and run Playwright E2E tests for MetoYou (Angular chat + WebRTC voice/video app). Handles multi-browser-context WebRTC voice testing, fake media devices, signaling validation, audio channel verification, and UI state assertions. Use when: "E2E test", "Playwright", "end-to-end", "browser test", "test voice", "test call", "test WebRTC", "test chat", "test login", "test video", "test screen share", "integration test", "multi-client test". --- # Playwright E2E Testing — MetoYou ## Step 1 — Check Project Setup Before writing any test, verify the Playwright infrastructure exists: ``` e2e/ # Test root (lives at repo root) ├── playwright.config.ts # Config ├── fixtures/ # Custom fixtures (multi-client, auth, etc.) ├── pages/ # Page Object Models ├── tests/ # Test specs │ ├── auth/ │ ├── chat/ │ ├── voice/ │ └── settings/ └── helpers/ # Shared utilities (WebRTC introspection, etc.) ``` If missing, scaffold it. See [reference/project-setup.md](./reference/project-setup.md). ## Step 2 — Identify Test Category | Request | Category | Key Patterns | | ------------------------------- | ---------------- | ---------------------------------------------- | | Login, register, invite | **Auth** | Single browser context, form interaction | | Send message, rooms, chat UI | **Chat** | May need 2 clients for real-time sync | | Voice call, mute, deafen, audio | **Voice/WebRTC** | Multi-client, fake media, WebRTC introspection | | Camera, video tiles | **Video** | Multi-client, fake video, stream validation | | Screen share | **Screen Share** | Multi-client, display media mocking | | Settings, themes | **Settings** | Single client, preference persistence | For **Voice/WebRTC** and **Multi-client** tests, read [reference/multi-client-webrtc.md](./reference/multi-client-webrtc.md) immediately. ## Step 3 — Core Conventions ### Config Essentials ```typescript // e2e/playwright.config.ts import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', timeout: 60_000, // WebRTC needs longer timeouts expect: { timeout: 10_000 }, retries: process.env.CI ? 2 : 0, workers: 1, // Sequential — shared server state reporter: [['html'], ['list']], use: { baseURL: 'http://localhost:4200', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'on-first-retry', permissions: ['microphone', 'camera'] }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'], launchOptions: { args: [ '--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream' // Feed a specific audio file as fake mic input: // '--use-file-for-fake-audio-capture=/path/to/audio.wav', ] } } } ], webServer: [ { command: 'cd server && npm run dev', port: 3001, reuseExistingServer: !process.env.CI, timeout: 30_000 }, { command: 'cd toju-app && npx ng serve', port: 4200, reuseExistingServer: !process.env.CI, timeout: 60_000 } ] }); ``` ### Selector Strategy Use in this order — stop at the first that works: 1. `getByRole('button', { name: 'Mute' })` — accessible, resilient 2. `getByLabel('Email')` — form fields 3. `getByPlaceholder('Enter email')` — when label missing 4. `getByText('Welcome')` — visible text 5. `getByTestId('voice-controls')` — last resort, needs `data-testid` 6. `locator('app-voice-controls')` — Angular component selectors (acceptable in this project) Angular component selectors (`app-*`) are stable in this project and acceptable as locators when semantic selectors are not feasible. ### Assertions — Always Web-First ```typescript // ✅ Auto-retries until timeout await expect(page.getByRole('heading')).toBeVisible(); await expect(page.getByRole('alert')).toHaveText('Saved'); await expect(page).toHaveURL(/\/room\//); // ❌ No auto-retry — races with DOM const text = await page.textContent('.msg'); expect(text).toBe('Saved'); ``` ### Anti-Patterns | ❌ Don't | ✅ Do | Why | | ------------------------------ | --------------------------------------------------- | ------------------------- | | `page.waitForTimeout(3000)` | `await expect(locator).toBeVisible()` | Hard waits are flaky | | `expect(await el.isVisible())` | `await expect(el).toBeVisible()` | No auto-retry | | `page.$('.btn')` | `page.getByRole('button')` | Fragile selector | | `page.click('.submit')` | `page.getByRole('button', {name:'Submit'}).click()` | Not accessible | | Shared state between tests | `test.beforeEach` for setup | Tests must be independent | | `try/catch` around assertions | Let Playwright handle retries | Swallows real failures | ### Test Structure ```typescript import { test, expect } from '../fixtures/base'; test.describe('Feature Name', () => { test('should do specific thing', async ({ page }) => { await test.step('Navigate to page', async () => { await page.goto('/login'); }); await test.step('Fill form', async () => { await page.getByLabel('Email').fill('test@example.com'); }); await test.step('Verify result', async () => { await expect(page).toHaveURL(/\/search/); }); }); }); ``` Use `test.step()` for readability in complex flows. ## Step 4 — Page Object Models Use POM for any test file with more than 2 tests. Match the app's route structure: ```typescript // e2e/pages/login.page.ts import { type Page, type Locator } from '@playwright/test'; export class LoginPage { readonly emailInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; constructor(private page: Page) { this.emailInput = page.getByLabel('Email'); this.passwordInput = page.getByLabel('Password'); this.submitButton = page.getByRole('button', { name: /sign in|log in/i }); } async goto() { await this.page.goto('/login'); } async login(email: string, password: string) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.submitButton.click(); } } ``` **Key pages to model (match `app.routes.ts`):** | Route | Page Object | Component | | ------------------- | ------------------ | ----------------------- | | `/login` | `LoginPage` | `LoginComponent` | | `/register` | `RegisterPage` | `RegisterComponent` | | `/search` | `ServerSearchPage` | `ServerSearchComponent` | | `/room/:roomId` | `ChatRoomPage` | `ChatRoomComponent` | | `/settings` | `SettingsPage` | `SettingsComponent` | | `/invite/:inviteId` | `InvitePage` | `InviteComponent` | ## Step 5 — MetoYou App Architecture Context The agent writing tests MUST understand these domain boundaries: ### Voice/WebRTC Stack | Layer | What It Does | Test Relevance | | ----------------------- | ----------------------------------------------------- | -------------------------------- | | `VoiceConnectionFacade` | High-level voice API (connect/disconnect/mute/deafen) | State signals to assert against | | `VoiceSessionFacade` | Session lifecycle, workspace layout | UI mode changes | | `VoiceActivityService` | Speaking detection (RMS threshold 0.015) | `isSpeaking()` signal validation | | `VoicePlaybackService` | Per-peer GainNode (0–200% volume) | Volume level assertions | | `PeerConnectionManager` | RTCPeerConnection lifecycle | Connection state introspection | | `MediaManager` | getUserMedia, mute, gain chain | Track state validation | | `SignalingManager` | WebSocket per signal URL | Connection establishment | ### Voice UI Components | Component | Selector | Contains | | ----------------------------------- | --------------------------------- | ------------------------------------------- | | `VoiceWorkspaceComponent` | `app-voice-workspace` | Stream tiles, layout | | `VoiceControlsComponent` | `app-voice-controls` | Mute, camera, screen share, hang-up buttons | | `FloatingVoiceControlsComponent` | `app-floating-voice-controls` | Floating variant of controls | | `VoiceWorkspaceStreamTileComponent` | `app-voice-workspace-stream-tile` | Per-peer audio/video tile | ### Voice UI Icons (Lucide) | Icon | Meaning | | ------------------------------------ | -------------------- | | `lucideMic` / `lucideMicOff` | Mute toggle | | `lucideVideo` / `lucideVideoOff` | Camera toggle | | `lucideMonitor` / `lucideMonitorOff` | Screen share toggle | | `lucidePhoneOff` | Hang up / disconnect | | `lucideHeadphones` | Deafen state | | `lucideVolume2` / `lucideVolumeX` | Volume indicator | ### Server & Signaling - **Signaling server**: Port `3001` (HTTP by default, HTTPS if `SSL=true`) - **Angular dev server**: Port `4200` - **WebSocket signaling**: Upgrades on same port as server - **Protocol**: `identify` → `server_users` → SDP offer/answer → ICE candidates - **PING/PONG**: Every 30s, 45s timeout ## Step 6 — Validation Workflow After generating any test: ``` 1. npx playwright install --with-deps chromium # First time only 2. npx playwright test --project=chromium # Run tests 3. npx playwright test --ui # Interactive debug 4. npx playwright show-report # HTML report ``` If the test involves WebRTC, always verify: - Fake media flags are set in config - Timeouts are sufficient (60s+ for connection establishment) - `workers: 1` if tests share server state - Browser permissions granted for microphone/camera ## Quick Reference — Commands ```bash npx playwright test # Run all npx playwright test --ui # Interactive UI npx playwright test --debug # Step-through debugger npx playwright test tests/voice/ # Voice tests only npx playwright test --project=chromium # Single browser npx playwright test -g "voice connects" # By test name npx playwright show-report # HTML report npx playwright codegen http://localhost:4200 # Record test ``` ## Reference Files | File | When to Read | | ---------------------------------------------------------------------- | ------------------------------------------------------------------ | | [reference/multi-client-webrtc.md](./reference/multi-client-webrtc.md) | Voice/video/WebRTC tests, multi-browser contexts, audio validation | | [reference/project-setup.md](./reference/project-setup.md) | First-time scaffold, dependency installation, config creation |