Aller au contenu

Tests E2E — Playwright

Documentation des tests end-to-end avec Playwright pour Primatch.


Configuration

# Playwright tourne sur l'hôte (pas dans Docker)
make test-e2e           # Tous les tests E2E (headless)
make test-e2e-ui        # Interface graphique Playwright
make test-e2e-report    # Rapport HTML du dernier run

Les tests sont dans frontend/e2e/ et configurés dans playwright.config.ts.

  • Navigateur : Chromium uniquement
  • Locale : fr-FR
  • Base URL : http://localhost:3010
  • Reporter : HTML + JSON
  • Timeout : 30 secondes par test

Structure actuelle

frontend/e2e/
├── auth.spec.ts          # Authentification (login OTP, inscription, déconnexion)
├── games.spec.ts         # Liste des parties + assistant de création
├── game-detail.spec.ts   # Détail d'une partie + positions + composition d'équipe
├── home.spec.ts          # Page d'accueil
└── profile.spec.ts       # Profil utilisateur

Patterns d'implémentation

Authentification mockée

Tous les tests qui nécessitent un utilisateur connecté utilisent un helper authenticateUser(page) qui :

  1. Mock les endpoints /auth/request-otp, /auth/verify-otp et /auth/me
  2. Navigue vers /login, saisit l'email et soumet
  3. Saisit le code OTP de vérification
  4. Attend l'arrivée sur /player/matches
async function authenticateUser(page: Page) {
    // Mock API routes
    await page.route(`${API_URL}/v1/auth/request-otp`, (route) =>
        route.fulfill({ status: 200, body: JSON.stringify({ message: 'OTP sent' }) })
    )
    // ... verify-otp, /auth/me ...

    await page.goto('/login')
    await page.getByLabel('Email').fill('jean@example.com')
    await page.getByRole('button', { name: 'Se connecter' }).click()
    // Saisie OTP et navigation automatique
}

Mock API centralisé

Pour les pages qui font plusieurs appels API (liste + détail + score), on utilise un intercepteur unique avec pattern matching :

async function mockGameApis(page: Page, game: typeof mockGameOpen) {
    await page.route(`${API_URL}/v1/games**`, (route) => {
        const url = route.request().url()
        if (url.match(/\/games\/\d+$/))  // GET /games/:id → détail
        if (url.includes('/score'))       // GET /games/:id/score
        // sinon → liste paginée
    })
}

Important

Après l'authentification mockée, il faut naviguer via des clics SPA (pas page.goto()) pour préserver l'état d'authentification en mémoire. Le pattern recommandé :

async function navigateToGameDetail(page: Page, clubName: string) {
    await page.getByText(clubName).first().click()
    await expect(page.getByText('Le Terrain')).toBeVisible()
}

Couverture E2E — Parties

game-detail.spec.ts — 22 tests

Section Tests Vérifications
Court View & Info 6 Header, badges (type/env/statut), équipes, joueurs positionnés, slots libres, grille infos
Join with Position 8 Bouton rejoindre, ouverture modale, slots occupés, bouton confirmer désactivé/activé, payload API, annulation, clic direct terrain
Change Position 4 Bouton changer, pas de bouton rejoindre, position actuelle marquée, payload API mise à jour
Draw Teams 2 Bouton tirage visible, appel API
Full Game 1 Pas de bouton rejoindre, tous les joueurs affichés
Navigation 1 Retour à la liste

games.spec.ts — Tests liste et création

Couvre la liste des parties (filtres, pagination, cartes) et l'assistant de création multi-étapes.


Bonnes pratiques Playwright

  • ✅ Utiliser getByRole, getByLabel, getByText (résistants aux changements CSS)
  • ✅ Un test = un cas d'utilisation documenté dans la section Fonctionnel
  • ✅ Naviguer via clics SPA après authentification (pas de page.goto())
  • ✅ Un seul page.route() avec pattern matching pour mocker plusieurs endpoints
  • ❌ Ne pas utiliser de sélecteurs CSS fragiles (div > span.class-name)
  • ❌ Ne pas faire de page.waitForTimeout() sauf pour attendre un appel API asynchrone
  • ❌ Ne pas utiliser page.goto() pour naviguer après l'authentification