Tests e2e — couverture
Ce fichier doit rester synchronisé avec
e2e/specs/. À mettre à jour à chaque spec/test ajouté ou retiré — c'est l'inventaire de ce que la suite garantit réellement (et de ce qu'elle ne garantit pas).
Vue d'ensemble — 18 specs
L'inventaire assertion par assertion de chaque test vit dans
e2e-summary.md.
| Spec | Écran / parcours | Tests | Contrats backend vérifiés |
|---|---|---|---|
smoke.spec.ts | Cold start | 1 | GET /travels, GET /api/travels/seaside/categories — et surtout : le teardown fail-loud prouve que le monde couvre tout le trafic de démarrage |
home.spec.ts | Home / | 6 | GET /travels?seaside=false (+ page=2 via infinite scroll) ; connecté : GET /api/customers (cache-busté ?_=) + GET /api/bookings, refetch des deux à chaque activation Home ; card « Mes voyages » balnéaire = durée calculée depuis l'occurrence |
search.spec.ts | Overlay recherche Home | 8 | GET /travels avec search=, seaside=false, category= |
seaside.spec.ts | Listing /seasides | 4 | GET /travels avec seaside=true, seasideCategoryName=Baln MAJORQUE, search= |
travel-details.spec.ts | /voyage/:slug + sous-pages | 7 | GET /api/travels/:slug (dont 404 slug inconnu) |
auth-login.spec.ts | /login, /logout, session | 8 | POST /api/authentification (body), fetch customer du login allégé (lighter-response=true + ignoreTravels=true sur GET /api/customers), login réussi malgré un GET /api/bookings 500 (H6 — bookings non-fatals), non-appel de /refresh et de GET /api/customers sur 401, non-appel sur email malformé (Regle bloque, novalidate) |
auth-signup.spec.ts | /create-account | 4 | GET /api/customers/email-exists ; POST /api/customers (body complet, birthdate en yyyy-MM-dd) + auto-login |
session-expired.spec.ts | Modal de connexion global (session morte) | 7 | POST /api/authentification/refresh (échec 401 ET 400 — la vraie réponse Horizon token-mort — → logout complet customer inclus + modal ; échec 500 transitoire → ni logout ni modal), re-login POST /api/authentification depuis le modal sans quitter la page (succès et mauvais identifiants) ; demi-état désync (customer persisté sans auth) → auto-guérison : logout complet + modal |
logout-race.spec.ts | Course logout / fetch customer en vol (AUTH_BUG.md H1) | 1 | GET /api/customers gated résolu après « Oui, déconnecter » → la réponse est jetée (epoch de session) : localStorage.customer reste vide, Home reste anonyme. (Committé rouge le 2026-08-18 comme preuve du bug H1, vert depuis le fix epoch — cf. business-rules.md → « Epoch de session ») |
analytics-consent.spec.ts | Modal de consentement analytics au démarrage | 4 | Persistance localStorage analyticsConsent (accepted/refused, re-prompt si dismiss) ; zéro requête PostHog garanti par le teardown fail-loud de chaque test de la suite (token vidé en e2e) |
booking-wizard-catalog.spec.ts | Wizard voyage catalogue | 11 | POST /api/mobile/booking/initialize payload complet (connecté ET invité, dont acompte + bon cadeau : paymentDepositOnly, gifts[].id ; suppléments globaux booking.globalSupplements ; points consumeLoyaltyPoints) ; GET /api/gifts/:code ; PUT /api/customers (body scalaires, dates date-only, pas de Club 1970) ; GET .../payment-callback |
booking-wizard-oneday.spec.ts | Wizard course d'un jour | 5 | POST /api/mobile/booking/initialize payload complet (invité ET connecté) ; GET /api/gifts/:code (bon partiel remainingValue, plafonné) ; GET /api/loyalty/winning-points (affichage mobileWinningPoints) |
booking-wizard-seaside.spec.ts | Wizard balnéaire (chambres + pension, et vol-sec) | 5 | POST /api/mobile/booking/initialize payload complet (connecté ET invité, mealPlan par passager ; vol-sec : p.price bébé = 0 ; bus de nuit : checkIn/checkOut décalés) |
special-offers.spec.ts | Offres spéciales (teaser + calcul) | 4 | GET /api/travels/:slug (specialOffers occurrence + minPrice.*WithClubSpecialOffers) ; booking.discounts dans POST .../initialize (offre publique déduite invité, offre membres exclue invité, ×2 couple membre) |
payment-callback.spec.ts | /booking/confirmation | 5 | GET .../payment-callback?bookingId= |
booking-details.spec.ts | /booking/details/:bookingId | 5 | GET /api/bookings (recovery cold deep-link) ; GET /api/bookings/:id/confirmation-invoice (cache-buster ?_=, non-appel quand pending) |
loyalty-points.spec.ts | /loyalty-points | 4 | données loyalty du payload GET /api/customers (solde, pending, historique, expiration) ; fetch du mount en plein payload (pas de lighter-response — verrouille l'opt-out, les transactions brutes alimentant l'alerte d'expiration) ; pas de refetch à l'expansion de l'historique |
personal-info.spec.ts | /profile/edit | 2 | PUT /api/customers (scalaires only, birthdate date-only) |
Détail par domaine (mapping sur workflows.md)
Workflow 1 — Recherche → détail → réservation → paiement
Listing & recherche (home, search, seaside)
- Feed catalogue rendu depuis le mock (2 cards non-balnéaires), card « Prochains départs ».
- Split catalogue/balnéaires : Home émet
?seaside=false, Seaside?seaside=true— vérifié en query. - Overlay recherche = entrée d'historique (
?overlay=search) : ouverture, fermeture au back sans quitter Home (ADR 0009). - Recherche texte : contrat
search=<terme>, marqueur?search=1, en-tête « n Résultats » ; back depuis les résultats = retour catalogue complet ; empty state + lien « Consultez le catalogue complet ». - Annulation de l'overlay = snapshot restauré (le BackButton de l'overlay rejette les éditions non soumises) ; onglet « Découvrir » = reset au catalogue complet depuis une recherche active.
- Filtre par intérêt (
category=courses-1-jour) et par destination balnéaire (seasideCategoryName=Baln MAJORQUE, formatBaln {NOM}). - Champ texte Seaside (plus d'overlay côté balnéaire — cf. ADR 0012) : recherche au submit ; card seaside sans durée → pas de chip « n jours ».
- Refetch connecté à chaque activation Home (
GET /api/customerscache-busté +GET /api/bookings) — les points/réservations crédités pendant l'absence remontent. - Card « Mes voyages » (PendingTripCard) : pour un booking balnéaire (
travel.durationabsent), la durée affichée est calculée depuis l'occurrence (start/end), pas le champ backend — sinon « jour » sans nombre. - Feed long : pagination (page 2 via infinite scroll) + promo cards interleavées tous les 4 résultats (carte loyalty ×2 sur 14 voyages).
Détail voyage (travel-details)
- Rendu par slug : titre, description, durée, highlights, prestations incluses, barre « Dès {prix} » + CTA « Réserver » ; suffixe « p.p. en ch. double » visible en multi-day, caché pour les one-day.
- Sous-pages routes (ADR 0012) : departures (arrêts Sion/Lausanne), itinerary (jour par jour), comments — adressables par URL directe, back → détail.
- Commentaires : seuls les approuvés s'affichent.
- Slug inconnu : 404 backend → pas de contenu rendu (pas de crash).
Wizard de réservation (booking-wizard-catalog, booking-wizard-oneday, booking-wizard-seaside) — la matrice complète est couverte : invité ET connecté × one-day / multi-day / Seaside (6 happy paths complets jusqu'au paiement), plus le vol-sec Seaside (connecté) :
- Catalogue multi-day, connecté (2 adultes + 1 enfant) : sélection date (2 occurrences) → chambre familiale → 3 formulaires passagers (civilité, nom, naissance, lieu de départ, assurance) → plan de sièges (sièges occupés
seat-3/seat-4affichés pris, sélection de 3 sièges) → paiement (totaux UI 3'600 CHF, préremplissage facturation/contact depuis le compte, date de naissance du résumé en heure localedd.MM.yyyy— verrouille l'off-by-one UTC de la branche rooms, ne détecte la régression que grâce autimezoneId: 'Europe/Zurich'global deplaywright.config.ts). - Catalogue multi-day, invité (2 adultes, chambre double) : formulaires passagers vierges (pas de préremplissage), facturation + contact saisis à la main, totaux 2'980 CHF,
customerdu payload = les données saisies (c'est lui qui crée le compte Horizon invité), confirmation guest (« confirmation par e-mail », pas de CTA réservation). - Course d'un jour, invité (2 adultes) : étape date skippée (occurrence unique), tarifs par tranche (89/69/49), champs assurance/pension absents (voyage sans chambre), facturation + contact saisis à la main, totaux 178 CHF.
- Course d'un jour, connecté (1 adulte) : passager 1 prérempli depuis le compte (prénom/nom/naissance), facturation (
#citySion) et contact (#contact-email) préremplis, totaux 89 CHF, confirmation connectée (« Voir ma réservation »). - Seaside, connecté (2 adultes, chambre double) : les deux étapes date skippées (occurrence unique) — le wizard ouvre sur
accommodations; seule la chambre pricée s'affiche (la familiale sans prix d'occurrence est masquée) ; pension obligatoire (#mealPlan) — full board (+140) pour le passager 1, demi-pension incluse (0) pour le 2 ; assurance réelle (45) sur le passager 1 ; plan de sièges servi parcatalog.vehicles. Totaux 2'765 CHF ; contrat :p.price= part chambre + pension (1'430 / 1'290, l'assurance exclue dep.price),passengers[].mealPlan=[2, 1],roomTypeCategoriesfabrique le séjour suracc-hotel-majorque(Seaside sansdays). - Seaside, invité (2 adultes) : formulaires vierges, pension incluse + « Aucune assurance », totaux 2'580 CHF, payload guest, confirmation guest.
- Vol-sec Seaside, connecté (1 adulte + 1 bébé) : voyage
makeVolSecSeasideTravel()(SEASIDE sansaccommodation, poussé dansworld.travelspar le spec) → le wizard prend le chemin room-less (passenger-count/passenger-names) ; tranches vol-sec affichées (« Enfant de 2 ans révolus à 12 ans », « Bébé de moins de 2 ans ») avec bébé gratuit (0 CHF au sélecteur) ; étape passagers multi-type (fenêtre par groupe,passenger-type-group), pas de pension ni d'assurance, pas de téléphone pour le bébé ; totaux 890 CHF (le bébé n'ajoute rien) ; contrat :p.price= [890 adulte, 0 bébé],mealPlanabsent,amountToPay890. - Bus de nuit Seaside (connecté, chambre double) : voyage muté
firstNightInTheBus/lastNightInTheBus→ contrat : le séjour envoyé (roomTypeCategories[0].days[0]) atterrit sur les dates hôtel —checkIn= start +1 jour,checkOut= end −1 jour (jours calendaires vialocalDay), shape sans fuseau. Horizon persiste ces dates verbatim et la rooming list hôtel (Occupancy) filtre dessus — cf.business-rules.md→ « CheckIn/CheckOut Seaside : les nuits en bus décalent le séjour hôtel ». - Acompte + bon cadeau (catalogue, connecté, chambre double) : voyage avec
depositPercentage: 35(muté dans le spec — le monde n'offre pas d'acompte par défaut), radio « Payer un acompte » (sans %), code cadeau validé (GET /api/gifts/:code→ bon 200 CHF du monde, carte auto-repliée sur « Bon appliqué : 200.00 CHF ») ; résumé : lignes « Acompte » et « Solde à régler ultérieurement » (payment-balance1'937 CHF), total à payer 843 CHF (= 2'980 × 35 % − 200) ; contrat :paymentDepositOnly: true,amountToPay/paymentAmount= 843,general.amount= total complet 2'780,gifts: [{ id: 'gift-noel' }]. - Contrat
initializevérifié dans les sept happy paths (+ le test acompte) :occurrenceId,status(PendingWebOrMobile),seller: 'Mobile', nombre et prix par passager,amountToPay,customer,returnUrlvers/booking/confirmation. - Contrat
initialize— dates wire locales et sans fuseau (ADR 0016), vérifié dans les sept happy paths sous letimezoneId: 'Europe/Zurich'épinglé : birthdates saisies au formulaire = valeur exacteyyyy-MM-ddT00:00:00(unDate.toJSONUTC enverrait le jour précédent), birthdates préremplies du compte = shape sans fuseau (UNZONED_DATETIME) + jour calendaire de la fixture (localDay, fixtures midi-local),bookingDateetcheckIn/checkOut(catalog :checkIn; seaside : les deux) = shape sans fuseau. - Suppléments globaux (catalogue, connecté) : ligne « dont {description} » dans le résumé, montant flat×pax dans le total,
booking.globalSupplementsdans le payload (Horizon ne l'applique que si envoyé),p.priceinchangé. - Points de fidélité au checkout (catalogue, connecté) : « Ajouter » applique tout le solde (−12 CHF sur 120 pts), payload
consumeLoyaltyPoints: true+amountToPayréduit, puis solde optimiste à 0 sur la carte Home après la confirmation (masque le délai du worker Horizon). - Bon partiellement utilisé (one-day, invité) : la déduction =
remainingValue(pasvalue), plafonnée au prix du voyage, avec le message « Il vous restera n CHF sur ce bon cadeau. » (UI seulement, pas de submit à 0). PUT /api/customers(Valider de la carte Contact, connecté) : body scalaires uniquement (trim anti-hang iOS),birthdatedate-only, pas declubMembershipStart/Endpour un client sans Club.- Tranches room-less one-day : 4 tiers affichés (« Bébé de moins de 3 ans » gratuit 0 CHF, « Enfant de 3 ans révolus à 12 ans », « Adolescent de 12 ans révolus à 18 ans »).
- Lieu de départ : liste plate de villes triée alpha avec places restantes (
Sion (40 places)) ; voyage sans stops → select absent et non exigé (required-si-proposé). - Validation guidante : tap sur le « Suivant » pseudo-disabled avec formulaires vides → « Champ requis » surligné.
- Téléphone adulte : prérempli depuis
customer.mobile(connecté) ; absent des tiers non-adultes (vol-sec bébé, bébé catalogue). - Points gagnés affichés =
mobileWinningPointsdu calculateur backend (« Vous gagnez 18 points » pour 89 CHF — mockceil×2). - Filtre chambres (accordéon « Filtrer par nombre de personnes », >5 dispositions) : actif seulement déplié, capacité ≥ groupe, replié = tout réaffiché.
- Assurances avec option non tarifée (catalogue, invité) : régression sorrente-capri — room type portant une option sans prix sur l'occurrence + assurances
priceStart ≥ 1→ le select assurance s'affiche quand même (le filtre price la chambre/option réservée, pasrooms[0]/options[0]). - Child tier sans tranche junior (seaside) : disposition « 1 enfant » (jamais « bébé »), label date de naissance élargi « (0-15 ans) » + bornes min/max posées.
- Pension/assurance vierges au premier rendu (
''— choix conscient forcé). - Plan de sièges multi-day sourcé de l'occurrence : le happy path catalogue tourne avec
catalog.vehicles = []— toute régression vers le catalog casserait l'étape sièges. - Saferpay : popup
window.openintercepté (stub), retour simulé par deep-link. - Mécanique wizard : miroir étape ↔ URL (
:stepName), back = exactement une étape en arrière (sélection conservée), deep-link froid vers une étape avancée → ré-init à l'étape 0 (bookingConstructornon persisté), ✕ = historique tronqué à[Home, travel-details](un back post-sortie retombe toujours sur Home — ADR 0014). - Variantes :
bookingStateFULL → CTA « Liste d'attente » + arrivée sur/waiting-list;noVehiclePlan→ étape sièges skippée. - Liste d'attente — arrêt groupé + contrat de soumission (les trois types) : le dropdown « Lieu de départ souhaité » groupe les arrêts par ligne (
<optgroup label="Valais">) et les étiquette"locality, place (hour)"(Sion, Gare routière (06:30)/Lausanne, Gare CFF (07:45)) — parité site (CustomerForm.availableStops). Vérifié sur catalogue (invité, brancheelse), Seaside (connecté, brancheisSeaside) et one-day (connecté, picker « Ligne désirée » → arrêts filtrés). Sur Seaside et one-day, la soumission asserte le contratPOST /api/mail/add-waiting-list:requestedStop= le libellé complet (Sion, Gare routière (06:30)),requestedLine= le nom du trajet (Valais). - Badges de date liste d'attente / sur demande (catalogue, deux occurrences non réservables) :
bookingStateFULL → badge « Liste d'attente » (bg-secondary-80), TOO_LATE → « Sur demande » (bg-primary-50) — parité site (Occurrences.vue), verrouille l'appariement libellé/état contre l'ancienne inversion.
Offres spéciales (special-offers) — le rabais est une ligne booking.discounts agnostique du type de voyage (même chemin dans computeBookingPricing pour one-day / multi-day / Seaside), donc couvert sur le seul flow catalogue ; la logique de résolution (fenêtre, couple ×2, isPerPerson, éligibilité) est verrouillée en unitaire (utils/__tests__/special-offers.test.ts) :
- Teaser (invité) : carte catalogue (brut
1'490barré + prix club1'390+ badge « Rabais Club Buchard »), en-tête détail, et chaque ligne de date (prix remisé, sans badge) — depuisminPrice.*WithClubSpecialOffers. - Offre publique, invité : ligne « Offre spéciale: … » −100 dans le résumé, total 2'880,
booking.discounts = [{ type:0, value:100, showOnConfirmation:false, isCroisitour:false, description }],amountToPay2'880. - Offre membres, invité : aucune ligne, prix plein 2'980,
booking.discounts = []. - Offre membres, membre Couple (
hasActiveClubMemberShip+clubMembershipTypeCouple seedés) : ligne « Offre spéciale club Buchard: … » −200 (×2), total 2'780,booking.discounts[0].value = 200.
Callback paiement (payment-callback)
- Succès connecté : copy « espace client », CTA « Voir ma réservation » (deep-link
/booking/details/:id) + « Retour à l'accueil ». - Succès invité : copy « confirmation par e-mail », CTA unique (pas de « Voir ma réservation » — route auth-gated).
- Échecs : transaction
CANCELEDet erreur 500 de l'endpoint → écran d'échec. - Page terminale (
router.replace) : un back ne re-rentre pas dans la confirmation (ADR 0009).
Détail réservation (booking-details)
- Cold deep-link (route
requiresAuth, invité → redirect/login) : le miss du store déclencheGET /api/bookings+ retry (récupération post-paiement) ; rendu titre/durée/voyageurs/passagers. - Facture : bouton « Facture » actif (CONFIRMED) → GET invoice cache-busté
?_=; bookingPENDING_WEB_OR_MOBILE→ « Facture bientôt disponible »pseudo-disabled, aucune requête au tap. - Ré-activation
<keep-alive>: un booking rafraîchi par le refetch de Home (passager renommé) s'affiche au retour sur la page.
Loyauté (loyalty-points, + checkout dans booking-wizard-catalog)
- Carte de solde (points + équivalent CHF), pastille « en attente » dépliable, alerte « n points expirent le {date} ».
- Barre de palier modulo 1000 (
50 pts/ « Plus que 950 points… ») ; solde rond → message « Cumulez des points… ». - Historique : 3 max, « Voir tout l'historique » / « Réduire » inline sans refetch, libellés FR de l'enum.
- Invité : page programme (étapes, simulateur ×2 app / ×1 web, FAQ) sans blocs de solde.
- Consommation au checkout + solde optimiste : voir la section wizard ci-dessus.
Profil (personal-info)
/profile/edit: email de connexion + numéro de client readonly.- « Enregistrer » →
PUT /api/customers:cityédité transmis,birthdatedate-only, body 100 % scalaire (trim anti-hang iOS). ⚠️ Anomalie relevée (non assertée) : sur ce chemin,PersonalInfoFormseede les dates Club absentes ànew Date(0)→ le PUT porteclubMembershipStart/End = 1970-01-01(le chemin carte Contact du paiement, lui, les omet — c'est là que le comportement correct est verrouillé).
Workflow 2 — Création de compte (auth-signup)
- Vérif email : 200 = « Cette addresse est déjà utilisée! », 404 = silencieux.
- Validation Regle : soumission vide → « Champ requis ».
- Signup complet : contrat
POST /api/customers(dontbirthdateenyyyy-MM-dd, civilité, pays) puis auto-login + atterrissage Home.
Auth & session (auth-login, session-expired, logout-race)
- Login : validation, succès (contrat body + fetch customer/bookings), 401 → reste sur
/loginsans fetch profil avec erreur inline « La connexion a échoué… » (le toast natif est invisible derrière le backdrop du modal — l'inline couvre les deux contextes) ; unGET /bookings500 n'annule plus le login (bookings non-fatals, AUTH_BUG.md H6 — session persistée + atterrissage Home vérifiés). - Formulaire (
LoginForm, partagé page + modal) : email malformé → message Regle français (lenovalidatedu form neutralise la validation native du WebView, qui avalait le submit sans aucun message sur iOS) ; bouton désactivé pendant la requête (handler mock retardé). - Session seedée : survit à un reload, sans déclencher
/refresh(JWTexpfutur). - Logout : confirmation, retour Home, localStorage
authvidé. - Session expirée → modal de connexion sur place (ADR
0018) : JWT seedé expiré + refresh 401 ou 400 (la vraie réponse Horizon pour un token mort — AUTH_BUG.md H3) → modal global visible (?overlay=system-modaldans l'URL), logout complet vérifié (tokens vidés et storecustomerpersisté nettoyé — plus de zombie H2) ; re-login depuis le modal (contratPOST /api/authentification) le ferme sans quitter la page ; back navigateur = dismiss sans navigation. (Couvre aussi, de fait, le hostSystemModalet le servicecomposables/modal.ts— le modal message-only n'a pas de spec dédié, aucun call-site produit à ce jour.) - Sessions mortes vs blips transitoires (garde de
sessionExpired, cf.business-rules.md) : storecustomerseedé sans cléauth(demi-état zombie désync) → auto-guérison : logout complet + modal + store customer nettoyé (comportement inversé en août 2026 — avant le fix H2, ce demi-état ne déclenchait volontairement rien et restait bloqué) ; JWT expiré + refresh 500 (échec transitoire) → pas de modal et tokens toujours présents en localStorage (session préservée). - Course logout (AUTH_BUG.md H1,
logout-race) : le refetch customer de Home (onActivated) est tenu en vol par un handler gated, l'utilisateur navigue Home → Profil → Compte → Déconnexion → « Oui, déconnecter » (taps SPA — unpage.gototuerait la requête), la gate est relâchée après le logout → la réponse est jetée par l'epoch de session :localStorage.customerne contient paseMail, Home reste anonyme. (Committé rouge comme preuve du bug le 2026-08-18, vert depuis le fix — il verrouille désormais la non-régression de l'epoch.)
Démarrage app (smoke)
- Boot du shell (mode
e2evérifié via la dev-banner), trafic de cold start intégralement couvert par le monde.
Consentement analytics (analytics-consent, ADR 0019)
- Le spec opte out du seed de consentement (
test.use({ analyticsConsent: 'none' })— les autres specs démarrent'refused'via la fixture auto, donc ne voient jamais le modal). - Lancement vierge → modal « Améliorer l'application » au-dessus de Home, backé par
?overlay=system-modal. - « Accepter » / « Refuser » → modal fermé sur place, décision persistée (
localStorage.analyticsConsent), un reload ne re-demande pas. - Back navigateur = dismiss → reste indécis, un reload re-présente le modal.
- « PostHog ne collecte rien » n'est pas un test dédié : c'est le teardown fail-loud du
mockBackendde chaque test de la suite (toute requêteposthog.coméchouerait le test), plus le token vidé de.env.e2e.
Autres — confidentialité (others)
- Collapsible « Confidentialité » fermé par défaut : le texte et les radios sont clippés (
max-height: 0+overflow: hidden) — asserté ennot.toBeInViewport()(un contenu clippé garde une bounding box,toBeHidden()ne marche pas — recette à retenir pour tout contenu sousCollapsible). - Déplié : texte de consentement visible, radio pré-cochée selon le statut seedé (« Refuser » avec le seed par défaut, « Accepter » via
test.use({ analyticsConsent: 'accepted' })). - Cocher « Accepter » → persisté (
localStorage.analyticsConsent = { status: 'accepted' }), conservé après reload ; cocher « Refuser » depuis un état accepté → persistérefused.
Non couvert à ce jour (backlog assumé)
| Trou | Détail |
|---|---|
| Mot de passe oublié / reset | Endpoints forgot/reset enregistrés dans le monde mais aucun spec ne drive l'UI |
| Refresh token (chemin positif) | Le non-déclenchement (JWT valide, auth-login) et l'échec (session morte → modal, session-expired) sont vérifiés ; le flow « token expiré → refresh réussi → rejeu » n'est pas testé |
Échec d'initialize | Les branches d'erreur du wizard au paiement (initialize non-200) ne sont pas injectées — seuls les échecs du callback le sont |
Étape departure-location | Aucun voyage du monde n'est dynamique (stops variant par occurrence) — l'étape lieu de départ du wizard n'est jamais rendue |
| Mes avis | GET /api/comments/mine répond [], jamais asserté ; création/édition d'avis non couvertes |
| Code cadeau invalide | La branche « Le code saisi est invalide. » (gift 404) n'est pas exercée (bon valide et bon partiel le sont) |
| Filtre dates du calendrier | L'overlay DepartureCalendar n'est jamais driveé ; le wire n'émet qu'un param dates opaque — la règle « départ seulement » (1.2.0) n'est pas observable au contrat |
| Ouverture réelle de la facture / liens externes | FileViewer et Browser.open (DeleteAccount → formulaire de contact, redirection Saferpay native) sont des bridges Capacitor sans équivalent web observable — seuls les contrats réseau sont assertés |
One-way pricing (d8b0aa1) | Le chemin de prix TripType.ONE_WAY existe mais aucune UI ne le déclenche — rien à driver tant qu'un sélecteur aller simple n'existe pas |
| PUT profil : dates Club epoch | Anomalie app relevée par personal-info.spec.ts (voir section Profil) — à corriger côté PersonalInfoForm avant de pouvoir l'asserter |
Ces trous sont listés pour rester honnêtes sur la garantie réelle de la suite — les combler est le backlog naturel des prochains specs. En ajouter un = déplacer la ligne d'ici vers les sections couvertes ci-dessus.
Voir aussi
e2e-summary.md— l'inventaire assertion par assertion de chaque test.e2e-overview.md— comment lire un spec (UI vs contrat).e2e-writing-tests.md— comment combler un trou (checklist + recettes)..claude/docs/domain/workflows.md— les parcours produit auxquels cette couverture se rapporte.

