Skip to content

Tests e2e — le monde par défaut & les factories

Ce fichier doit rester synchronisé avec e2e/support/world.ts et e2e/fixtures/. À mettre à jour quand une entité du dataset, un endpoint enregistré ou une factory change.

Le « monde » est le dataset happy-path que chaque test reçoit via la fixture world (objet frais par test, defaultWorld()). registerDefaultWorld(backend, world) enregistre ensuite un handler pour chaque endpoint que l'app peut toucher pendant les parcours cœur — c'est la conséquence directe du fail-loud : un endpoint non enregistré fait échouer tout test qui le touche.

Les entités du monde (World, e2e/support/world.ts)

ts
interface World {
    travels: Travel[]              // les 3 voyages ci-dessous
    catalog: Catalog               // hôtel + chambres + assurances + véhicule
    customer: E2eCustomer          // Claire Dupont
    countries: Country[]           // CH / FR / IT / ES / DE
    seasideCategories: TravelCategory[]  // Majorque, Costa Brava, Igea
    roomTypeAvailabilities: RoomTypeAvailability[]
    occupiedSeats: Seat[]          // seat-3, seat-4 (matchent les sièges du pont)
    bookings: Booking[]            // [] par défaut
}

Les trois voyages — chacun existe pour exercer un chemin distinct du produit

VoyageFactoryTypeCe qu'il exerce
« Croisière sur le Rhin » (travel-rhin, slug croisiere-sur-le-rhin)makeTravel()CATALOG, 7 jours, days.length > 0Le chemin wizard avec hébergement : chambres (prices[].roomId ↔ catalog), 2 occurrences (occ-rhin-1 J+30, occ-rhin-2 J+60), lignes/arrêts (Sion, Lausanne), plan de sièges, chambre double 2'980 / familiale 3'600
« Marché de Noël de Colmar » (travel-colmar, slug marche-de-noel-de-colmar)makeOneDayTravel()ONE_DAY, 1 jour, days: []Le chemin sans chambre : transport tarifé par tranche d'âge (adulte 89 / junior 69 / enfant 49), occurrence unique occ-colmar-1 J+21 (skip de l'étape date), pas d'assurance ni pension
« Séjour balnéaire à Majorque » (travel-majorque, slug sejour-balneaire-a-majorque)makeSeasideTravel()SEASIDE, 14 joursLe listing balnéaire (?seaside=true) ET le wizard Seaside : accommodation singulier (acc-hotel-majorque, « Hôtel Playa Azul ») → chemin chambres + pension obligatoire (requiresMealPlan) ; occurrence occ-majorque-1 J+45 avec seule la chambre double pricée (2'580 — la disposition familiale reste masquée, filtre getPriceForRoom > 0) ; occurrence unique → les deux étapes date sont skippées, le wizard ouvre sur accommodations

Le customer (makeCustomer())

Claire Dupont (customer-claire, claire.dupont@example.ch), Sion, née il y a 40 ans (yearsAgo(40)), contact d'urgence Marc Dupont, loyaltyPoints: 120. Type local E2eCustomer = Customer augmenté des champs loyalty du wire (absents du package) : loyaltyPoints, pendingLoyaltyPoints?, loyaltyTransactions?, loyaltyTransactionsPretty? (types miroir de src/types/extensions.ts, dates en ISO strings). Seul loyaltyPoints a un défaut — les specs loyalty posent le reste par mutation de world.customer avant seedAuth(). C'est ce customer que seedAuth() persiste — le wizard préremplit le passager 1 depuis lui.

seedAuth est idempotent par clé : l'init script tourne à chaque navigation mais ne seede une clé (auth, customer) que si elle est absente du localStorage. L'état que l'app persiste elle-même en cours de test (ex. l'override optimiste pendingLoyalty posé au submit du paiement) survit donc à un page.goto — comme sur un vrai device.

Consentement analytics (seedAnalyticsConsent, fixture auto)

Chaque test démarre avec une décision de consentement analytics déjà persistée (localStorage.analyticsConsent, défaut 'refused') — sinon le modal opt-in (ADR 0019) s'ouvrirait par-dessus l'écran sous test. Piloté par la fixture-option analyticsConsent ('accepted' | 'refused' | 'none') : test.use({ analyticsConsent: 'none' }) désactive le seed pour tester le prompt lui-même (analytics-consent.spec.ts). Même idempotence par clé que seedAuth. PostHog n'est de toute façon jamais initialisé en e2e (VITE_POSTHOG_PROJECT_TOKEN vidé dans .env.e2e) — une requête posthog.com ferait échouer le teardown fail-loud.

Sièges occupés

occupiedSeats = seat-3 et seat-4, dont les ids matchent les sièges du pont (makeVehicle()) — c'est ce qui rend les sièges « pris » visibles dans TBSeatMap.

La cohérence par ids partagés (e2e/fixtures/ids.ts)

Les fixtures se référencent entre fichiers par id. IDS est la source unique — ne jamais mettre un id en dur dans une factory ou un spec :

occurrence.prices[].roomId ───► catalog rooms (room-double / room-family)   ← pricing wizard
occupiedSeats[].id         ───► deck.seats[].id (seat-1 … seat-12)          ← plan de sièges
roomTypeAvailability       ───► roomType (rt-double)                        ← dispo chambres
occurrence.drives[].stops  ───► stop-sion / stop-lausanne                   ← lieu de départ
catalog.insurances         ───► ins-annulation (45 CHF) / ins-none (0 CHF)  ← select assurance

Casser un de ces liens ne fait pas d'erreur de type — ça fait un wizard qui n'affiche pas de prix, des sièges tous libres, etc.

Endpoints enregistrés par registerDefaultWorld

Origines : relatif = n'importe quelle origine factice ; sinon anchoré. Tous les JSON sortent en content-type: application/json + CORS permissif (exigence CapacitorHttp web).

EndpointRéponse par défautParticularités
GET https://search.e2e.test/travels et GET /api/travelspaginate(hits)Handler partagé listTravels : honore ?seaside (split catalogue/balnéaire par TravelType), ?search (texte libre sur name/subtitle), ?page/?size
GET /api/travels/seaside/categoriesworld.seasideCategories
GET /api/travels/random3 premiers travels
GET /api/travels/:slugle travel, sinon 404 {message:'not found'}
GET /api/travels/:id/catalogworld.catalogLe même catalog pour tous les voyages
GET /api/occurrences/:idl'occurrence (avec travelSummary), sinon {}Quirk du vrai backend conservé (objet vide = pas trouvé)
GET /api/occurrences/upcomingoccurrences du Rhin + travelSummaryEnregistré après /:id pour gagner (plus spécifique)
GET /api/occurrences/room-type-availability/:occIdworld.roomTypeAvailabilities
GET /api/occurrences/vehicle-occupancy/:occId/:bookingId?world.occupiedSeatsSegment :bookingId? optionnel (édition de booking)
GET /api/countriesworld.countries
POST /api/authentification et /refresh{email, token: fakeJwt(...), refreshToken}Login accepte n'importe quel mot de passe par défaut
POST /api/authentification/forgot / /reset{}
GET /api/customers/email-exists404Sémantique app : 200 = email pris, sinon libre. Override en {status: 200} pour tester « déjà utilisé »
POST /api/customers / PUT /api/customersecho du body fusionné sur world.customer
GET /api/customersworld.customerHonore les params d'allègement comme Horizon : lighter-response=trueloyaltyTransactions: null, ignoreTravels=truetravels: []
GET /api/loyalty/winning-points{winningPoints: ceil(amount × 0.1), mobileWinningPoints: ×2}Miroir du calculateur Horizon ; appelé (débounce 300 ms) par l'étape Paiement du wizard
GET /api/bookingsworld.bookings ([])
POST /api/bookingsecho du body + general.id = 'booking-new-1'
GET /api/bookings/:id/confirmation-invoiceTINY_PDF (raw)
GET /api/comments/mine[]
POST /api/mail/add-waiting-list{}
GET /api/gifts/:codemakeGiftCodeResponse({ code }) (bon 200 CHF intact)Tout code valide par défaut — override en 404/{status} pour un code invalide, remainingValue pour un bon entamé
POST https://website.e2e.test/api/mobile/booking/initializemakePaymentInitializeResponse() (redirectUrl → stub Saferpay)
GET https://website.e2e.test/api/mobile/booking/payment-callbackmakePaymentCallbackResponse() (CAPTURED, succès)
GET https://saferpay.e2e.test/paySAFERPAY_STUB_HTML (raw)Page stub avec data-testid="saferpay-stub", rendue dans le popup
GET horizon.e2e.test/images/* (regex)PLACEHOLDER_PNG (raw)PNG valide 320×420 — le plan de sièges dimensionne sa grille sur la taille naturelle de l'image ; un stub 1×1 rend les sièges incliquables
GET horizon.e2e.test/Uploads/* (regex)TINY_PDF (raw)

Catalogue des factories (e2e/fixtures/)

Pattern commun : makeX(overrides: Partial<T> = {}) — défauts spreadés puis ...overrides, le tout en satisfies Partial<T> as T (les défauts restent honnêtes vis-à-vis du package de contrat, le cast couvre les champs backend-only jamais lus). Seuls les champs que l'app mobile lit sont peuplés.

FichierExportsÀ savoir
travel.tsSWITZERLAND, makeStop, makeDrive, makeLine, makeGlobalSupplement, makeOccurrence, makeTravel, makeOneDayTravel, makeSeasideTravel, makeVolSecSeasideTravelmakeOneDayTravel/makeSeasideTravel sont des makeTravel({...}) — n'importe quel override reste possible. makeVolSecSeasideTravel (« Vol sec Majorque », slug vol-sec-majorque) = Seaside sans accommodation (chemin room-less du wizard, transport par tranche, bébé gratuit) — hors monde par défaut : à pusher dans world.travels par le spec qui en a besoin. makeGlobalSupplement : supplément d'occurrence (défaut « Supplément carburant », 5 CHF flat/pers, isFlatRatePerPerson augmenté localement comme dans l'app) — à poser dans occurrence.globalSupplements par le spec. makeOccurrence : les seat maps multi-day lisent occurrence.vehicles (jamais GET /occurrences/:id — véhicules vides, bug backend connu, cf. occurrence-capacity.md) ; Seaside et single-day lisent catalog.vehicles à la place. makeSeasideTravel porte l'accommodation singulière (le champ qui décide seul du chemin chambres + pension du wizard)
catalog.tsmakeRoom, makeRoomType, makeAccommodation, makeInsurance, makeVehicle, makeCatalogmakeRoomType porte la disposition familiale exacte 2A+1C (room-family) qui drive le happy path wizard. makeVehicle : 1 pont, 12 sièges en 3 rangées de 4, sièges break = séparateurs de rangée. makeCatalog : 2 assurances dont « Aucune assurance » value 0 (ins-none)
customer.tsE2eCustomer, LoyaltyTransaction, LoyaltyTransactionPretty, makeCustomer, COUNTRIESTypes loyalty redéclarés depuis src/types/extensions.ts (dates ISO strings, transactionType = ordinal numérique) — à garder en sync
availability.tsmakeRoomTypeAvailabilityQuota par room type
booking.tsmakeBookingPassenger, makeBookingCôté lecture (MyTravels, pending trip Home) — le wizard, lui, crée son booking en mémoire
payment.tsPaymentInitializeResponse, PaymentCallbackResponse, TransactionStatus, makePaymentInitializeResponse, makePaymentCallbackResponse⚠️ Types redéclarés depuis src/types/payment.ts (le projet e2e n'a pas l'alias @/) — à garder en sync manuellement
gifts.tsGiftCodeResponse, makeGiftCodeResponse⚠️ Contrat redéclaré depuis src/types/extensions.ts (GiftCodeResponse) — à garder en sync. Défauts : id: IDS.gift, 200 CHF, remainingValue: null (bon intact), validUntil à +365 j
pagination.tspaginate<T>(records, overrides?)L'enveloppe {totalRecords, totalFilteredRecords, page, records} que lit stores/travels.ts
dates.tsdaysFromNow(n), yearsAgo(n), UNZONED_DATETIME, localDay(d)Vrais Date à midi local, toujours relatifs à la date d'exécution. Midi = le jour calendaire survit au round-trip UTC ; vrais Date = le mock JSON.stringify sérialise en ISO exactement comme le wire. UNZONED_DATETIME (regex du contrat wire sortant yyyy-MM-ddTHH:mm:ss, ADR 0016) et localDay (jour calendaire local d'une fixture) servent aux assertions de payload initialize
ids.tsIDSVoir « cohérence par ids » ci-dessus
enums.tsTravelType, TravelBookingState, OccurrenceStatus, MealPlan, BookingStatusSeul fichier autorisé à deep-importer le dist horizon-types (dist/enum/*.js) — le dist/index.js du package n'est pas chargeable par Node (cf. DETTE_TECHNIQUE.md). Les import type restent libres partout

Voir aussi

  • e2e-overview.md — le harnais (fixtures, interception, règles).
  • e2e-writing-tests.md — comment muter ce monde / overrider un endpoint dans un spec.
  • ADR 0015 — pourquoi des factories manuelles (et pas createDefault du package).

Contributors

No contributors

Changelog

No recent changes