Skip to content

Tests e2e — vue d'ensemble & lecture de la suite

Ce fichier doit rester synchronisé avec le code de e2e/ et playwright.config.ts. À mettre à jour à chaque changement structurel du harnais. Le « pourquoi » des choix d'architecture vit dans l'ADR 0015 ; 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.open Saferpay) sont couverts aussi.
  • Catch-all : localhost passe (c'est le serveur Vite) ; fonts.googleapis.com/fonts.gstatic.com sont avortés silencieusement (cosmétique, pas du trafic API) ; tout le reste est enregistré dans unmatched puis avorté.
  • Le port 4173 est volontairement distinct du 5173 du dev permanent (qui charge .env = prod). Ne jamais pointer la suite sur 5173.

Les fixtures (e2e/support/fixtures.ts)

FixtureRôleCycle de vie
worldDataset 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é.
mockBackendLe 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.
seedAuthawait 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).
seedAnalyticsConsentauto: 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 :

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

  1. 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.
  2. Contrat — ce que l'app a envoyé au backend : mockBackend.captured('POST /api/bookings') (liste des requêtes matchant MÉTHODE /path, query ignorée) et await mockBackend.waitForCaptured(…) (poll, l'UI émet en asynchrone). Chaque CapturedRequest expose postDataJSON, query (URLSearchParams), path, url. C'est le canal pour vérifier les payloads (ex. birthdate en yyyy-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 de world, override d'endpoint, seedAuth). Après le premier chargement, le cache LRU in-page retient les GET publics → un override tardif exige page.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). Les import type sont libres partout.
  • Émulation mobile uniquement : projets mobile-chrome (Pixel 5 ≈ WebView Android) et mobile-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

CommandeEffet
npm run test:e2eToute la suite, les deux devices
npm run test:e2e -- --project=mobile-chromeUn seul device (boucle rapide)
npm run test:e2e -- --uiMode interactif Playwright
npm run test:e2e -- --repeat-each=2Anti-flake avant de pousser
npm run test:e2e -- -g "callback"Filtrer par titre
npm run dev:e2eServeur Vite mode e2e sur :4173 (Playwright le lance/réutilise tout seul)
npm run type-check:e2etsc 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 de unit-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.
  • CI set → Playwright build (build:e2e) + preview:e2e au 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 (#app non vide + dev-banner mode e2e) est le canari : s'il tombe, l'app ne boote pas du tout.
  • Fail-fast : maxFailures: 10 en CI (playwright.config.ts) + timeout: 30m sur 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: true sur 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.

Contributors

No contributors

Changelog

No recent changes