Tests e2e — vue d'ensemble & lecture de la suite
Ce fichier doit rester synchronisé avec le code de
e2e/etplaywright.config.ts. À mettre à jour à chaque changement structurel du harnais. Le « pourquoi » des choix d'architecture vit dans l'ADR0015; ce fichier décrit le « comment ça marche » et « comment lire un spec ».
Idée centrale : l'app tourne pour de vrai (vraie build Vite, vrai router, vrais stores), le backend n'existe pas. Playwright sert l'app en mode Vite e2e, où toutes les origines backend sont des hôtes factices *.e2e.test non résolvables en DNS — la prod est injoignable par construction. Un MockBackend intercepte tout le trafic réseau et répond depuis une table de handlers ; toute requête sans handler fait échouer le test en la listant.
Architecture
spec (e2e/specs/*.spec.ts)
│ importe { test, expect } depuis e2e/support/fixtures (JAMAIS @playwright/test)
│
├─ fixture `world` dataset happy-path par test (e2e/support/world.ts)
├─ fixture `mockBackend` AUTO — installée sur chaque test
│ setup: registerDefaultWorld(backend, world) + backend.install(context)
│ teardown: backend.assertNoUnmatched() ← échec loud si mock gap
├─ fixture `seedAuth` opt-in — session connectée seedée avant page.goto
└─ fixture `seedAnalyticsConsent` AUTO — consentement analytics 'refused' seedé
│ (option `analyticsConsent`, 'none' = pas de seed)
▼
BrowserContext ── context.route() ──► MockBackend (e2e/support/mock-backend.ts)
│ ├─ https://horizon.e2e.test (API + images + PDF)
│ ├─ https://search.e2e.test (endpoint recherche)
│ ├─ https://website.e2e.test (API paiement)
│ └─ https://saferpay.e2e.test (stub passerelle, popup)
▼
app Vue sur http://localhost:4173 (vite --mode e2e → .env.e2e)- Interception au niveau du
BrowserContext(pas de la page) : les popups (window.openSaferpay) sont couverts aussi. - Catch-all :
localhostpasse (c'est le serveur Vite) ;fonts.googleapis.com/fonts.gstatic.comsont avortés silencieusement (cosmétique, pas du trafic API) ; tout le reste est enregistré dansunmatchedpuis avorté. - Le port
4173est volontairement distinct du5173du dev permanent (qui charge.env= prod). Ne jamais pointer la suite sur5173.
Les fixtures (e2e/support/fixtures.ts)
| Fixture | Rôle | Cycle de vie |
|---|---|---|
world | Dataset happy-path frais par test (defaultWorld()) — voir e2e-world.md. Se lit pour les assertions, se mute avant page.goto pour customiser. | Un objet neuf par test, jamais partagé. |
mockBackend | Le mock réseau. auto: true : installé sur chaque test, même ceux qui ne le nomment pas — aucun test ne peut tourner « en direct ». | Setup : enregistre le monde + installe les routes. Teardown : assertNoUnmatched() — une seule requête orpheline = test rouge, avec la liste des trous. |
seedAuth | await seedAuth() avant page.goto → session connectée. Seede localStorage : clé auth (JSON brut, JWT fakeJwt avec exp futur — sinon refresh avant chaque appel protégé) et clé customer (vrai superjson pour que les Date revivent — le wizard préremplit le passager 1 depuis ce customer). | Sans appel = test invité (guest). |
seedAnalyticsConsent | auto: true : seede une décision de consentement analytics (localStorage.analyticsConsent, ADR 0019) pour que le modal opt-in ne recouvre pas l'écran sous test. Piloté par la fixture-option analyticsConsent ('accepted' | 'refused' | 'none', défaut 'refused') — test.use({ analyticsConsent: 'none' }) désactive le seed (spec du prompt). | Idempotent par clé, comme seedAuth. |
Anatomie d'un spec (comment lire)
Structure systématique — un fichier par écran, verbeux et granulaire :
import { test, expect } from '../support/fixtures'
test.describe('Payment callback', () => { // describe = écran
test('success while logged in offers the booking deep link', // titre = comportement (anglais)
async ({ page, seedAuth, mockBackend }) => {
await seedAuth() // arrange : AVANT goto
await page.goto('/booking/confirmation?bookingId=…')
await test.step('the callback endpoint was queried…', async () => { // step = action/assertion utilisateur
const cb = await mockBackend.waitForCaptured('GET /api/mobile/booking/payment-callback')
expect(cb.query.get('bookingId')).toBe('booking-new-1') // ② assertion de CONTRAT
})
await test.step('success copy with both CTAs', async () => {
await expect(page.getByTestId('confirmation-title')).toHaveText('Paiement confirmé') // ① assertion UI
})
})
})Deux familles d'assertions s'entrelacent :
- UI — ce que l'utilisateur voit :
getByRole/texte français d'abord (la copy fait partie du contrat produit),getByTestId(kebab-case) quand le sélecteur se couplerait à la structure DOM Bootstrap. - Contrat — ce que l'app a envoyé au backend :
mockBackend.captured('POST /api/bookings')(liste des requêtes matchantMÉTHODE /path, query ignorée) etawait mockBackend.waitForCaptured(…)(poll, l'UI émet en asynchrone). ChaqueCapturedRequestexposepostDataJSON,query(URLSearchParams),path,url. C'est le canal pour vérifier les payloads (ex.birthdateenyyyy-MM-dd, prix par passager,?seaside=true).
Pas de page objects : les specs interagissent directement, la duplication assumée garde chaque test lisible seul.
Les règles du harnais (à connaître avant de toucher un spec)
- Fail loud : requête sans handler = test rouge au teardown. Corollaire : tout nouvel endpoint consommé par l'app doit être enregistré dans
world.ts, sinon chaque test qui le touche échoue en listant la requête orpheline. C'est voulu — le mock ne peut pas diverger silencieusement du code. - Les enregistrements tardifs gagnent : un spec override un handler du monde en ré-enregistrant simplement (
mockBackend.onGet(...)) — le dispatch parcourt la table du dernier au premier. - Tout se configure AVANT
page.goto(mutation deworld, override d'endpoint,seedAuth). Après le premier chargement, le cache LRU in-page retient les GET publics → un override tardif exigepage.reload(). - Dates toujours relatives (
daysFromNow,yearsAgo) — une date en dur pourrit face aux filtres « départs à venir ». - Enums horizon-types : valeurs runtime uniquement via
e2e/fixtures/enums.ts(deep-imports.js— le dist du package n'est pas chargeable par Node, cf.DETTE_TECHNIQUE.md). Lesimport typesont libres partout. - Émulation mobile uniquement : projets
mobile-chrome(Pixel 5 ≈ WebView Android) etmobile-safari(iPhone 12 ≈ WKWebView iOS). Pas de desktop — le produit est une app mobile. - Fuseau horaire épinglé :
timezoneId: 'Europe/Zurich'global (playwright.config.ts) — la CI tourne en UTC, où les bugs local-vs-UTC (ex.toISOString()sur une Date locale-minuit → date de naissance décalée d'un jour) sont invisibles. Le fuseau des vrais devices suisses rend la suite déterministe et ces régressions détectables.
Commandes
| Commande | Effet |
|---|---|
npm run test:e2e | Toute la suite, les deux devices |
npm run test:e2e -- --project=mobile-chrome | Un seul device (boucle rapide) |
npm run test:e2e -- --ui | Mode interactif Playwright |
npm run test:e2e -- --repeat-each=2 | Anti-flake avant de pousser |
npm run test:e2e -- -g "callback" | Filtrer par titre |
npm run dev:e2e | Serveur Vite mode e2e sur :4173 (Playwright le lance/réutilise tout seul) |
npm run type-check:e2e | tsc sur e2e/ (specs + support — non couverts par type-check) |
Config (playwright.config.ts) : timeout test 30 s, expect 5 s, actionTimeout 15 s (un locator qui ne matche rien échoue en nommant le sélecteur fautif, au lieu de consommer silencieusement les 30 s du test × retries), trace: retain-on-failure, screenshot: only-on-failure. En local le serveur existant est réutilisé (reuseExistingServer).
CI (GitLab)
Job e2e-test (stage test, .gitlab-ci.yml) :
- Déclenché uniquement sur les pipelines de merge request (
$CI_PIPELINE_SOURCE == "merge_request_event") — pas sur les pushes de branche, tags, web ni schedule : la suite est lente, on ne la paie qu'au moment où le code est relu. (Diffère deunit-test, qui garde un catch-all non-bloquant sur les autres déclencheurs.) - Image
mcr.microsoft.com/playwright:v<X.Y.Z>-nobleépinglée à la version exacte de@playwright/test(package.json sans caret) — toujours bumper les deux ensemble. CIset → Playwright build (build:e2e) +preview:e2eau lieu du dev server ; retries ×2, workers = 1.- La CI exerce donc l'app buildée, pas le dev server — c'est le seul endroit où le bundle de prod est testé. Un comportement dev-vs-build (ex. le deadlock top-level await / chunks web Capacitor, cf.
architecture/overview.md→ « Pas de top-level await ») ne se voit que là. Le smoke test (#appnon vide + dev-banner modee2e) est le canari : s'il tombe, l'app ne boote pas du tout. - Fail-fast :
maxFailures: 10en CI (playwright.config.ts) +timeout: 30msur le job — un échec systémique (app qui ne boote pas = 90 tests × 3 tentatives × 30 s) se voit en minutes au lieu de tuer le job à l'heure. - Réflexe « tout échoue en CI mais c'est vert en local » : reproduire le chemin CI en local avec
CI=1 npx playwright test --project=mobile-chrome e2e/specs/smoke.spec.ts(force build+preview), puis lire la console navigateur dans la trace. Les artefacts du job (traces + screenshots) sont l'équivalent côté CI. allow_failure: truesur MR pour l'instant (le temps que la suite prouve sa stabilité) — à retirer ensuite.- Artefacts : JUnit (
playwright-junit.xml) → widget tests de la MR ;playwright-report/+test-results/(traces) conservés 1 semaine.
Le dossier e2e/ en un coup d'œil
e2e/
├── specs/ un fichier par écran — voir e2e-coverage.md
├── support/
│ ├── fixtures.ts les 3 fixtures ci-dessus — le point d'entrée de tout spec
│ ├── mock-backend.ts la classe MockBackend (routing, capture, fail-loud)
│ ├── world.ts dataset par défaut + enregistrement des endpoints
│ ├── jwt.ts fakeJwt (l'app ne lit que le `exp` via atob)
│ └── binary.ts PNG valide taille réelle, TINY_PDF, page stub Saferpay
├── fixtures/ factories typées make*(overrides) — voir e2e-world.md
└── tsconfig.json moduleResolution bundler (aligné sur le transpileur Playwright)Voir aussi
- ADR
0015(.claude/docs/architecture/adr/) — le pourquoi : mock réseau vs MSW/staging, origines factices, factories manuelles, mobile-only. e2e-world.md— le dataset par défaut, endpoint par endpoint, et le catalogue des factories.e2e-writing-tests.md— recettes pour ajouter un test (overrides, erreurs, Saferpay, contrats).e2e-coverage.md— ce qui est couvert, spec par spec, et les trous connus.patterns.md→ « Écrire un test e2e » — la version condensée de la recette.

