Tests e2e — inventaire détaillé (spec → test → assertions)
Liste exhaustive de chaque test de la suite et de chaque vérification qu'il fait, flow par flow. À mettre à jour en même temps que
e2e-coverage.mdà chaque test ajouté/retiré. Conventions de lecture : UI = ce que l'utilisateur voit (sélecteur → attendu) ; contrat = ce que l'app a envoyé au backend mocké (mockBackend.captured/waitForCaptured). Sauf mention « connecté » (seedAuth), les tests tournent en invité.
15 specs documentés ici (par device ; la suite tourne sur mobile-chrome et mobile-safari). ⚠️ Inventaire en retard sur la suite réelle : session-expired.spec.ts, special-offers.spec.ts et logout-race.spec.ts (repro AUTH_BUG.md H1, vert depuis le fix epoch de session) n'ont pas encore de section (voir e2e-coverage.md pour leur résumé).
smoke.spec.ts — démarrage app (1 test)
home route boots and renders the app shell from the mocked backend
- UI :
#appnon vide ;.dev-banner=e2e(preuve que la build tourne en mode Vitee2e, origines backend factices) ; barre de recherche (.gradient-search-wrapper) visible. - Contrat :
GET /api/travels/seaside/categoriesetGET /travelscapturés — et, implicitement, le teardown fail-loud prouve que tout le trafic de cold start a un handler.
analytics-consent.spec.ts — modal de consentement analytics (4 tests)
test.use({ analyticsConsent: 'none' })— désactive le seed auto'refused'de la fixture pour exercer le chemin indécis (ADR0019). « Zéro requête PostHog » est implicite dans chaque test de la suite (teardown fail-loud + token vidé en e2e).
a fresh launch shows the consent modal over the home page
- UI : Home rendue dessous (
travel-cardvisible) ;system-modalvisible avec le headingAméliorer l'applicationet les boutonsAccepter/Refuser; URL porteoverlay=system-modal(vraie entrée d'historique).
« Accepter » persists the decision and never asks again
- UI : tap
Accepter→ modal fermé (toBeHidden), URL revenue sur/. - État :
localStorage.analyticsConsent={"status":"accepted"};page.reload()→ pas de modal.
« Refuser » persists the refusal and never asks again
- Miroir du précédent avec
{"status":"refused"}.
dismissing via the native back keeps the question for the next launch
- UI :
goBack()→ modal fermé sans quitter/. - État : décision toujours indécise →
page.reload()re-présente le modal.
home.spec.ts — Home / (6 tests)
renders the catalogue feed from the mocked API
- UI : 2 cards
travel-card(rhin + colmar, pardata-travel-slug) ; la card rhin contient nom +7 jours; cardupcoming-departuresavecProchains départset unetiny-travel-cardrhin ; pas d'en-têteRésultatssans recherche active. - Contrat :
GET /travelsavecseaside=false.
a travel card navigates to the travel details and back returns to home
- UI : tap card → URL
/voyage/croisiere-sur-le-rhin,travel-title= nom ;goBack()→ URL/, card à nouveau visible.
the seaside promo card navigates to the seaside listing
- UI : le lien promo
a[href="/seasides"]navigue vers/seasideset la card majorque s'affiche.
logged-in home greets the customer with their pending trip — connecté, world.bookings = [makeBooking()]
- Contrat :
GET /api/customersetGET /api/bookingsau chargement. - UI : heading
Mes voyages+ nom du voyage réservé. - Refetch à chaque activation : nav card → détail → back → les compteurs
captured('GET /api/bookings')etcaptured('GET /api/customers')augmentent (points/réservations crédités côté serveur remontent sans redémarrage). - Contrat : le dernier
GET /api/customersporte le cache-buster?_=.
the pending-trip card derives a seaside duration from the occurrence — connecté, booking balnéaire
- Monde muté avant
goto: la card « Mes voyages » (PendingTripCard) pointe sur un booking dont le voyage balnéaire adurationmis àundefined(réel : les Seaside n'ont pas deTravelDay[]). - UI : la card affiche la durée calculée depuis l'occurrence (start/end), pas le champ backend absent — durée attendue dérivée des mêmes instants de la fixture (robuste au DST / à la date de run).
promo cards interleave into a long catalogue feed — monde muté : 14 voyages catalogue
- UI : 1ʳᵉ page = 10 cards ; scroll sur la dernière → infinite scroll charge la page 2 → 14 cards.
- UI : la carte loyalty (
Obtenez des points de fidélité) apparaît 2× (slot fixe + slot interleavé tous les 4 après le 8ᵉ voyage) ; promo seasideLa mer pour pas chervisible.
search.spec.ts — overlay recherche Home (8 tests)
tapping the search bar opens the ?overlay=search panel
- UI : tap
search-bar→ URL contient?overlay=search,search-inputvisible.
browser back closes the overlay without leaving home
- UI :
goBack()→ param overlay disparu, input caché, toujours 2 cards (ADR 0009 : overlay = entrée d'historique).
a text search hits the search endpoint and renders matching results
- Contrat : un
GET /travelsportesearch=rhinetseaside=false. - UI : URL
?search=1(marqueur), heading1 Résultats, 1 card (rhin).
back from search results clears the filter and restores the full catalogue
- UI :
goBack()depuis les résultats → marqueur disparu, 2 cards, en-têteRésultatscaché.
cancelling the overlay restores the previous filter snapshot
- UI : après une recherche
rhin, rouvrir l'overlay, taper un autre terme, sortir par le BackButton de l'overlay (= annulation) → les résultats1 Résultats/card rhin sont intacts ; rouvrir → l'input est revenu àrhin(restauration du snapshot).
the Découvrir tab resets an active search to the full catalogue
- UI : après une recherche (1 card), tap sur l'onglet Home de la bottom-nav (
#bottomBar a[href="/"]) → 2 cards, en-têteRésultatscaché.
a search with no matches shows the empty-state message
- UI : heading
0 Résultats+ copyaucun voyage ne correspond…; le lienConsultez le catalogue completrecharge les 2 cards.
an interest category narrows the listing query
- Contrat : tap bouton
Courses 1 jour→ unGET /travelsportecategory=courses-1-jour.
seaside.spec.ts — listing /seasides (4 tests)
lists only seaside travels, requested with seaside=true — monde muté : majorque duration: 0
- Contrat : un
GET /travelsporteseaside=true. - UI : 1 seule card (majorque) ; la card d'un voyage sans durée n'affiche pas de chip
jours(les vrais balnéaires arrivent sans durée — rotation 2/3 semaines).
a destination category fires the Baln-prefixed filter
- Contrat : tap
seaside-categoryMajorque →GET /travelsavecseasideCategoryName=Baln MAJORQUE(formatBaln {NOM}).
the plain text field searches the seaside listing on submit
- Contrat :
seaside-search-input+ Enter →GET /travelsavecsearch=majorqueetseaside=true. - UI : heading
1 Résultats.
a seaside card opens its travel details
- UI : tap card → URL
/voyage/sejour-balneaire-a-majorque,travel-titlecorrect.
travel-details.spec.ts — /voyage/:slug + sous-pages (7 tests)
renders the travel from GET /travels/:slug
- UI :
travel-title, description,7 jours; highlights (Les vignobles de la Moselle), headingPrestations incluses+ item ; barre du bas :Dès+ suffixep.p. en ch. doublevisible (multi-day) ;travel-book-cta=Réserver,href=/reservation/croisiere-sur-le-rhin.
a one-day trip prices without the double-room qualifier
- UI : sur colmar,
Dès+89 CHFvisibles,p.p. en ch. doublecaché (pas de chambre sur une course d'un jour).
the departures sub-page lists occurrence dates and is URL-addressable
- UI : lien départs → URL
/departures, arrêtsSion/Lausannevisibles ; back → détail ; chargement URL directe OK (sous-page = route, ADR 0012).
the itinerary sub-page opens when the travel has day-by-day content — monde muté (1 jour d'itinéraire)
- UI : lien itinéraire → URL
/itinerary, contenu du jour visible.
the comments sub-page shows approved reviews — monde muté (1 avis approuvé + 1 non approuvé)
- UI : l'avis approuvé s'affiche ; le non-approuvé jamais (filtre backend).
« Réserver » enters the booking wizard on the departure-date step
- UI : CTA → URL
/reservation/:slug/departure-date, 2wizard-date-option.
an unknown slug renders no travel content
- Contrat :
GET /api/travels/voyage-inexistant(404 mock). - UI : ni titre ni CTA (pas de crash).
auth-login.spec.ts — login / session / logout (5 tests)
submitting the empty form shows the French validation errors
- UI : submit vide →
Champ requis, URL reste/login.
a successful login stores the session and lands on home
- Contrat :
POST /api/authentificationbody exact{ emailOrCustomerNumber, password }; puisGET /api/customers+GET /api/bookings. - UI : atterrissage
/, cards visibles.
a rejected login (401) stays on the login page, logged out — override 401
- UI : reste sur
/login; contrat : aucunGET /api/customers(pas de fetch profil sur échec).
a seeded session survives a reload without logging in again — connecté
- Contrat : après
reload(),GET /api/customersrejoué ; aucunPOST /api/authentification/refresh(JWTexpfutur).
logout clears the persisted session and returns home — connecté
- UI : heading
Déconnexion, boutonOui, déconnecter→ URL/. - État : localStorage
auth→email/tokennuls.
auth-signup.spec.ts — création de compte (4 tests)
an already-used email is rejected by the availability check — override 200
- UI : blur du champ email →
Cette addresse est déjà utilisée!.
a free email passes the availability check silently
- Contrat :
GET /api/customers/email-existscapturé ; UI : pas de message (404 = libre).
submitting the empty form surfaces the required-field errors
- UI :
Champ requisvisible, URL reste/create-account.
a complete signup POSTs the customer and logs the new account in
- Contrat :
POST /api/customers—eMail,birthdateenyyyy-MM-dd(jamais ISO datetime),civility='Madam',name/firstName,country='CH'; puis auto-login (POST /api/authentification) +GET /api/customers. - UI : atterrissage
/.
booking-wizard-catalog.spec.ts — wizard voyage catalogue multi-day (11 tests)
full happy path: 2 adults + 1 child through payment — connecté ; monde muté : catalog.vehicles = []
- UI : CTA →
departure-date; 2 dates, la 1ʳᵉ afficheDès 1'490 CHF; étapeaccommodations: disposition familiale2 adultes+1 bébé, incrément ;accommodations-passengers: 3 formulaires ; le bébé n'a pas de champ téléphone ;seat-selection:seat-3/seat-4à l'étattaken, sélection de 3 sièges libres — le plan vient deoccurrence.vehicles(le catalog est vidé : toute régression vers le catalog casserait l'étape) ; paiement :payment-submitdésactivé tant que les cartes (Choix de paiement / Méthode / Contact) ne sont pas validées, puis activé ; totauxpayment-full-price=payment-total=3'600.00 CHF; résumé liste les passagers avec la date de naissance en heure locale12.04.1986(une conversion UTC afficherait le 11 — détectable grâce autimezoneId: 'Europe/Zurich'épinglé dansplaywright.config.ts). - Contrat
PUT /api/customers(Valider de la carte Contact, connecté) : body scalaires uniquement (aucune valeur objet/array — le trim anti-hang iOS),birthdateenyyyy-MM-dd,clubMembershipStart/Endabsents (client sans Club — pas de 1970-01-01),fullName='Claire Dupont'. - Contrat
POST /api/mobile/booking/initialize:occurrenceId='occ-rhin-1',status=999(PendingWebOrMobile),seller='Mobile',numberOfPassengers=3, 3 passagers àp.price=1200(3600/3),amountToPay=3600,roomTypeCategoriesnon vide,customer.eMaildu compte,returnUrl→/booking/confirmation; dates wire locales sans fuseau (ADR 0016) : les 3 birthdates saisies = exactement1986-04-12T00:00:00/1984-09-03T00:00:00/2024-02-01T00:00:00,bookingDateetroomTypeCategories[0].days[0].checkInau shapeUNZONED_DATETIME. - Saferpay : popup sur
https://saferpay.e2e.test/pay(saferpay-stubvisible) ; deep-link retour →confirmation-title='Paiement confirmé', CTAVoir ma réservation,GET …/payment-callbackcapturé.
guest books the double room and creates the Horizon account at payment — invité
- UI : formulaires passagers vierges (
#firstnamevide) ; carte Contact remplie à la main ; totaux2'980.00 CHF. - Contrat
initialize:p.price=1490×2,amountToPay=2980,customer.eMail/city= les données saisies (c'est ce payload qui crée le client Horizon invité) ; birthdates saisies exactes1977-02-14T00:00:00/1979-08-22T00:00:00(locales sans fuseau). - UI : confirmation guest (
confirmation par e-mail, pas de CTA secondaire).
deposit + gift code: acompte and balance lines, flag and gift id in the payload — connecté ; monde muté : depositPercentage: 35
- UI : radio
Payer un acompte(sans % affiché) ; codeNOEL200validé → carte auto-repliée surBon appliqué200.00; résumé : ligneAcompte(exact),Solde à régler ultérieurement,payment-balance='1'937.00 CHF',payment-total='843.00 CHF'(2'980×35% − 200). - Contrat
initialize:paymentDepositOnly=true,amountToPay=paymentAmount=843,general.amount=2780(total complet sur le booking),gifts=[{id:'gift-noel'}].
global supplements enter the totals, the summary and the payload — connecté ; monde muté : supplément flat 5 CHF/pers sur l'occurrence
- UI :
payment-full-price='2'990.00 CHF'(2'980 + 5×2) ; lignedont Supplément carburant=10.00 CHF;payment-total='2'990.00 CHF'. - Contrat
initialize:booking.globalSupplements=[{globalSupplementId:'gs-carburant', description, amount:10}](Horizon ne l'applique que si le client l'envoie) ;p.price=1490inchangé (supplément au niveau booking) ;amountToPay=2990.
loyalty points reduce the total and the balance is optimistically debited — connecté (120 pts)
- UI : section
Ajouter vos points de fidélité:Vous avez 120 points, boutonAjouter→Vous économisez,payment-total='2'968.00 CHF'(−12 CHF). - Contrat
initialize:general.consumeLoyaltyPoints=true(tout-ou-rien),amountToPay=2968. - UI post-confirmation : retour Home → la carte loyalty affiche
Obtenez des points de fidélitéet plus120 points de fidélité(solde optimiste 120−120=0 en attendant le débit du worker Horizon).
the room-count filter narrows dispositions only while expanded — monde muté : 6 dispositions pricées
- UI : accordéon
Filtrer par nombre de personnesvisible (>5 dispositions) ; replié → 6room-option; ouvert + Adulte(s) monté à 3 → 2 dispositions (triple, quad — capacité ≥ groupe) ; replié à nouveau → 6 (filtre actif seulement accordéon ouvert).
insurance dropdown shows when the room type has an unpriced option — invité ; monde muté : option supérieure sans prix d'occurrence, assurances priceStart: 1
- Régression sorrente-capri : le room type porte une option non tarifée sur l'occurrence ; la double option-less (2'980 / 2 = 1'490/pers) est réservée → le
#insuranceIddu formulaire passager est visible et proposeAssurance annulation+Aucune assurance(le filtre price la chambre/option réservée — l'ancien raccourcirooms[0]/options[0]donnait prix 0 → tout exclu → select masqué).
the wizard ✕ truncates history to [Home, travel-details]
- UI : entrée Home → détail → wizard → étape 2 ; tap ✕ (bouton
Close) → URL/voyage/:slug; un seul back → Home (historique tronqué à[Home, travel-details], ADR 0014). (Le test réalignehistory.state.positionà 0 après le 1ᵉʳ goto : la page Playwright boote surabout:blank, décalant le compteur d'une entrée vs un vrai cold start.)
each wizard step mirrors to the URL and back returns exactly one step
- UI : avancer d'une étape → URL
/accommodations;goBack()→/departure-date, la date sélectionnée a toujours la classeactive(état conservé).
a deep link to a later step re-enters the wizard at the first step
- UI : goto
/paymentà froid → redirigé/departure-date(bookingConstructornon persisté).
a lone non-bookable occurrence short-circuits to the waiting list — monde muté : bookingState FULL
- UI : CTA détail =
Liste d'attente; tap → URL/waiting-list,wizard-waiting-list-submitvisible.
booking-wizard-oneday.spec.ts — wizard course d'un jour (5 tests)
guest books a day trip for two adults through payment — invité
- UI : CTA →
passenger-countdirectement (occurrence unique = étape date skippée) ; tier adulte89 CHF,69/49visibles ; les 4 tranches client :Adolescent de 12 ans révolus à 18 ans,Enfant de 3 ans révolus à 12 ans, tier bébé =Bébé de moins de 3 anset0 CHF(toujours gratuit) ;wizard-continuedésactivé à 0 passager. - UI
passenger-names: pas de champ#insuranceIdni#mealPlan(parcours sans chambre) ; tap sur le « Suivant » pseudo-disabled avec formulaires vides →Champ requissurligné (validation guidante) ; options du#loadingStopId= liste plate de villes avec places restantes : exactement[Sélectionner, Lausanne (40 places), Sion (40 places)](quota 50 − occupancy 10). - UI : 2 sièges sélectionnés ; paiement : carte
Choix de paiementabsente (pas d'acompte one-day) ; contact saisi à la main ; totaux178.00 CHF. - Contrat
initialize:occ-colmar-1, 2 passagersp.price=89(prix par tranche),amountToPay=178,customer= données invité ; birthdates saisies exactes1972-11-19T00:00:00/1975-03-05T00:00:00,bookingDateau shapeUNZONED_DATETIME. - UI : confirmation guest (mail, un seul CTA).
logged-in customer books with prefilled details through payment — connecté
- UI : passager 1 prérempli (prénom/nom/date) + téléphone prérempli depuis
customer.mobile; paiement prérempli (#contact-city='Sion',#contact-emaildu compte) ;PUT /api/customersau Valider de Contact ; totaux89.00 CHF;Vous gagnez 18 points de fidélité=mobileWinningPointsdu calculateur backend (mock :ceil(8.9)×2— pas de formule front). - Contrat
initialize: 1 passagerprice=89,amountToPay=89,customer.eMaildu compte ; birthdate préremplie du compte = shapeUNZONED_DATETIME+ jour calendaire deworld.customer.birthdate(localDay— fixture midi-local, pas deT00:00:00). - UI : confirmation connectée (
Voir ma réservation).
an occurrence without a vehicle plan skips seat selection — monde muté : noVehiclePlan: true
- UI : après les infos passager, « Suivant » atterrit directement sur
payment(étape sièges skippée).
a one-day travel without stops never asks for a departure place — monde muté : occurrence sans drives
- UI :
#loadingStopIdabsent du formulaire ; le formulaire se valide sans lieu de départ (required-si-proposé) et « Suivant » mène àseat-selection.
a gift larger than the trip price is capped and shows the leftover — override gift {value: 500, remainingValue: 300}
- UI : après
NOEL200:Bon appliqué : 178.00 CHF(=remainingValueplafonné au prix du voyage, ni 500 ni 300),Il vous restera 122.00 CHF sur ce bon cadeau.(300−178),payment-total='0 CHF'. (UI seulement — pas de submit à 0.)
booking-wizard-seaside.spec.ts — wizard balnéaire + vol-sec (4 tests)
logged-in customer books the double room with a full-board upcharge — connecté
- UI : CTA →
accommodationsdirectement (les deux étapes date skippées, occurrence unique) ; seule la chambre double pricée s'affiche (familiale sans prix d'occurrence cachée) ; passager 1 prérempli ;#mealPlanvisible et obligatoire —#mealPlanet#insuranceIddémarrent à''(choix conscient forcé) ; passager 1 : full board (2, +140) + assurance réelle (ins-annulation, 45) ; passager 2 : demi-pension incluse (1),ins-none; plan de sièges servi parcatalog.vehicles(seat-3taken) ; totaux2'765.00 CHF(2'580+140+45) ;PUT /api/customersau Valider Contact. - Contrat
initialize:occ-majorque-1,passengers[0].price=1430(chambre+pension),[1].price=1290,mealPlan=[2,1]par passager, assurance exclue dep.price,amountToPay=2765,roomTypeCategoriesfabriqué suracc-hotel-majorque; dates wire : p1 prérempli = jour deworld.customer.birthdate, p2 saisi =1984-09-03T00:00:00exact,bookingDate+checkIndu séjour fabriqué au shapeUNZONED_DATETIME. - UI : confirmation connectée.
guest books the double room through payment — invité
- UI : formulaires vierges ; pension incluse +
ins-none; contact à la main ; totaux2'580.00 CHF. - Contrat
initialize:p.price=1290×2,amountToPay=2580,customer.eMailinvité ; birthdates saisies exactes1988-01-20T00:00:00/1990-06-15T00:00:00. - UI : confirmation guest.
an accommodation without a junior band relabels the child tier « Enfant » — monde muté : accommodation child 6-16 / junior 0-0 / adult 16+, familiale pricée
- UI : la disposition familiale affiche
2 adultes+1 enfant(jamaisbébé— seule tranche sous-adulte) ; après réservation de la familiale, le 3ᵉ formulaire porte le labelDate de naissance (0-15 ans)(tranche élargie [0, childMax)) avec attrsmin/maxposés sur l'input date.
logged-in customer books one adult and a free baby through payment — vol-sec (SEASIDE sans accommodation), connecté, monde muté : makeVolSecSeasideTravel() poussé
- UI : CTA →
passenger-count(chemin room-less) ; tranches vol-sec : adulte890 CHF,Enfant de 2 ans révolus à 12 ans490 CHF,Bébé de moins de 2 ans0 CHF; étape passagers multi-type (fenêtre par groupepassenger-type-group) : l'adulte est prérempli du compte,#mealPlan/#insuranceIdabsents ; le bébé n'a pas de champ téléphone ; paiement :Bébés de moins de 2 anslisté, totaux890.00 CHF(le bébé n'ajoute rien). - Contrat
initialize:occ-vol-sec-1, adulteprice=890, bébéprice=0etmealPlanabsent,amountToPay=890; dates wire : bébé saisi =${babyBirthday}T00:00:00exact (const partagée fill/assert), adulte prérempli = shapeUNZONED_DATETIME+ jour deworld.customer.birthdate.
payment-callback.spec.ts — /booking/confirmation (5 tests)
success while logged in offers the booking deep link — connecté
- Contrat :
GET …/payment-callbackavecbookingId=booking-new-1. - UI :
Paiement confirmé, sous-titreespace client,Voir ma réservation+Retour à l'accueil; le CTA primaire deep-link/booking/details/booking-new-1.
success as a guest promises the e-mail confirmation instead
- UI : sous-titre
confirmation par e-mail, CTA uniqueRetour à l'accueil, pas de secondaire (route détail auth-gated).
a CANCELED transaction renders the failure copy — override callback success:false/CANCELED
- UI :
Échec du paiement,Veuillez réessayer,Retour à l'accueil.
a callback endpoint error still renders the failure screen — override 500
- UI :
Échec du paiement.
leaving the confirmation replaces history — back cannot re-enter it — connecté
- UI : quitter via le CTA puis
goBack()→ l'URL ne re-matche jamais/booking/confirmation(page terminale enrouter.replace, ADR 0009).
booking-details.spec.ts — /booking/details/:bookingId (5 tests)
a cold deep-link recovers the booking via GET /api/bookings — connecté, world.bookings=[makeBooking()]
- Contrat : le miss du store au cold start déclenche
GET /api/bookings(retry du lookup — plus d'écran d'erreur post-paiement). - UI : heading = nom du voyage,
7 jours,1 voyageur,Claire Dupont; boutonFacture(exact — booking CONFIRMED).
the invoice download carries a cache-buster
- Contrat : tap
Facture→GET /api/bookings/booking-1/confirmation-invoiceavec?_=non nul (sinon le cache HTTP natif resservirait un PDF factice pré-worker).
a pending booking greys the invoice button without firing the request — booking PENDING_WEB_OR_MOBILE
- UI : bouton
Facture bientôt disponible, classepseudo-disabled(tappable, pasdisablednatif). - Contrat : tap → aucune requête invoice capturée.
returning to the page picks up a booking refreshed by Home
- UI : passager renommé côté serveur (override
GET /api/bookings) pendant un aller Home (le back flottant est un lien/) → retour via la PendingTripCard → la page kept-alive re-capture le booking :Clara Dupontaffiché.
a guest deep-link is redirected to the login page
- UI : goto sans session → URL
/login(requiresAuth).
loyalty-points.spec.ts — /loyalty-points (4 tests)
the balance card shows points, CHF value, pending chip and expiry alert — connecté ; customer muté : 1050 pts, 60 en attente, un lot de 200 expirant
- UI : heading
Votre solde de points,1'050,Équivalent à … 105 CHF; chip+60 points en attente→ tap → copycrédités automatiquement au retour; bande expiration200 points+expirent le; barre de palier modulo 1000 :50 pts(exact) etPlus que … 950 points.
a round balance shows the palier call-to-action instead of a countdown — connecté, 1000 pts
- UI : la carte affiche le total réel
1'000; la barre (1000 % 1000 = 0) afficheCumulez des points pour atteindre votre prochain palier de 100 CHF !.
the history caps at 3 movements and expands inline — connecté, 5 mouvements
- UI : 3
.loyalty-history-itemmax, libellés FR de l'enum (Achat de voyage,Utilisation de pointsvisibles ;Création de comptecaché) ;Voir tout l'historique→ 5 items sans refetch (compteurGET /api/customersinchangé) ;Réduire→ 3.
a guest sees the program explanation without the balance blocks — invité
- UI :
Comment ça fonctionnevisible ; heading solde etHistorique des mouvementscachés ; simulateurCalculez vos rabais: défauts 5'000 CHF × app =1'000points ; basculeSite web / agence→500(le ×2 mobile).
personal-info.spec.ts — /profile/edit (2 tests)
the login email and client number are readonly — connecté
- UI :
#account-email= email du compte + attrreadonly;#account-navno=C-10042+readonly.
« Enregistrer » PUTs a scalar-only, date-only profile — connecté
- UI :
#cityprérempliSion, édité enMartigny,Enregistrer. - Contrat
PUT /api/customers:city='Martigny';birthdateenyyyy-MM-ddstrict ; aucune valeur objet/array dans le body (trim anti-hang iOS). Trou connu, non asserté : sur ce chemin, le formulaire seede les dates Club absentes avec epoch (new Date(0)dansPersonalInfoForm) → le PUT porteclubMembershipStart/End = 1970-01-01. Le comportement correct (champs omis) est verrouillé sur la carte Contact du paiement (booking-wizard-catalog.spec.ts).
Voir aussi
e2e-coverage.md— vue d'ensemble par domaine + les trous connus (le backlog).e2e-overview.md— comment lire un spec (UI vs contrat), le harnais.e2e-world.md— le dataset par défaut et les factories que ces tests mutent.

