---
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      |
