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 :
- Mock les endpoints
/auth/request-otp,/auth/verify-otpet/auth/me - Navigue vers
/login, saisit l'email et soumet - Saisit le code OTP de vérification
- 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
})
}
Navigation SPA¶
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