Initial commit

This commit is contained in:
jpgiannetti
2026-01-31 11:45:11 +01:00
commit f99fb3c614
166 changed files with 115155 additions and 0 deletions

View File

@@ -0,0 +1,472 @@
# language: fr
Fonctionnalité: Recherche de contenu
En tant qu'utilisateur de RoadWave
Je veux rechercher des contenus audio par mots-clés, localisation et filtres
Afin de trouver facilement le contenu qui m'intéresse
Contexte:
Étant donné que l'application RoadWave est démarrée
Et que l'utilisateur "jean@example.com" est connecté
# 15.3.1 - Recherche par mot-clé
Scénario: Recherche full-text basique
Étant donné que la base contient les contenus suivants:
| titre | description | créateur |
| Balade à Paris | Visite du quartier Latin | @paris_stories |
| Secrets de Montmartre | Histoire de la butte | @explore_paris |
| Voyage en Normandie | Découverte des plages | @voyages_fr |
Quand l'utilisateur recherche "paris"
Alors 2 résultats sont retournés
Et les résultats incluent "Balade à Paris"
Et les résultats incluent "Secrets de Montmartre"
Scénario: Recherche avec stemming français
Étant donné un contenu avec le titre "Voyage en Bretagne"
Quand l'utilisateur recherche "voyages"
Alors le contenu "Voyage en Bretagne" est trouvé
Et le stemming a transformé "voyages" en racine "voyag"
Plan du Scénario: Stemming français sur différentes formes
Étant donné un contenu avec le mot "<mot_original>"
Quand l'utilisateur recherche "<recherche>"
Alors le contenu est trouvé grâce au stemming français
Exemples:
| mot_original | recherche |
| voyage | voyages |
| voyager | voyage |
| balades | balade |
| historique | histoire |
Scénario: Recherche avec accents ignorés
Étant donné un contenu avec le titre "Découverte de l'Élysée"
Quand l'utilisateur recherche "decouverte elysee"
Alors le contenu est trouvé
Et les accents sont normalisés automatiquement
Scénario: Champs indexés avec pondération
Étant donné les contenus suivants:
| titre | description | créateur | tags |
| Voyage Paris | Balade sympa | @user1 | Tourisme |
| Balade Lyon | Voyage en ville | @paris_guide | Voyage |
Quand l'utilisateur recherche "paris"
Alors "Voyage Paris" est en première position
Parce que le titre a un poids × 3
Et "@paris_guide" apparaît en second
Parce que le créateur a un poids × 2
Scénario: Ranking par pertinence et popularité
Étant donné les contenus suivants:
| titre | écoutes | rang_texte |
| Balade Paris | 50000 | 0.8 |
| Paris la nuit | 1000 | 0.9 |
Quand l'utilisateur recherche "paris"
Alors le score final combine rang_texte × (1 + log(écoutes + 1))
Et "Balade Paris" est mieux classé grâce à sa popularité
Scénario: Autocomplete pendant la frappe
Étant donné que l'utilisateur commence à taper "par"
Quand 3 caractères sont saisis
Alors des suggestions apparaissent:
| suggestion |
| paris |
| parc naturel |
| parvis notre-dame |
Et le top 5 des suggestions est affiché
Scénario: Historique des 10 dernières recherches
Étant donné que l'utilisateur a effectué les recherches suivantes:
| recherche | date |
| voyage paris | 2026-01-20 |
| audio-guide louvre | 2026-01-19 |
| podcast automobile | 2026-01-18 |
Quand l'utilisateur ouvre la barre de recherche
Alors les 10 dernières recherches sont affichées
Et elles sont triées par date décroissante
Scénario: Correction automatique si aucun résultat
Étant donné que l'utilisateur recherche "ballade paris" (faute d'orthographe)
Et qu'aucun résultat n'est trouvé
Quand la page de résultats s'affiche
Alors une suggestion "Essayez plutôt : balade paris" est affichée
Scénario: Recherches populaires suggérées
Étant donné qu'aucun résultat n'est trouvé pour une recherche
Quand la page s'affiche
Alors des suggestions populaires sont affichées:
| suggestion |
| balade paris |
| audio-guide louvre |
| visite montmartre |
# 15.3.2 - Recherche géographique
Scénario: Saisie d'un lieu avec autocomplete
Étant donné que l'utilisateur ouvre le filtre "Lieu"
Quand il tape "Louv"
Alors Nominatim retourne des suggestions:
| suggestion | type |
| Musée du Louvre, Paris | monument |
| Louvres, Val-d'Oise | commune |
Scénario: Sélection d'un lieu et définition du rayon
Étant donné que l'utilisateur sélectionne "Paris, France"
Et que les coordonnées sont (48.8566, 2.3522)
Quand il définit un rayon de 50 km
Alors la recherche PostGIS utilise ST_DWithin avec 50000 mètres
Plan du Scénario: Recherche géographique avec différents rayons
Étant donné un contenu à 30 km de Paris
Quand l'utilisateur recherche autour de Paris avec un rayon de <rayon>
Alors le contenu est <résultat>
Exemples:
| rayon | résultat |
| 20 km | non trouvé |
| 50 km | trouvé |
| 100 km | trouvé |
Scénario: Utilisation de "Autour de moi" (GPS actuel)
Étant donné que l'utilisateur active le GPS
Et que sa position est (48.8566, 2.3522)
Quand il sélectionne "Autour de moi"
Alors la recherche utilise ses coordonnées GPS actuelles
Et un rayon par défaut de 10 km est appliqué
Scénario: Curseur de rayon avec limites
Étant donné que l'utilisateur ouvre le curseur de rayon
Quand il ajuste le curseur
Alors les valeurs disponibles vont de 5 km à 500 km
Et la valeur s'affiche en temps réel "50 km"
Scénario: Affichage de la distance dans les résultats
Étant donné une recherche géographique autour de Paris
Et un contenu à 2.3 km de distance
Quand les résultats sont affichés
Alors la distance "À 2.3 km" est indiquée pour chaque résultat
Plan du Scénario: Tri par proximité géographique
Étant donné des contenus à différentes distances de Paris:
| contenu | distance |
| Louvre Guide | 0.5 km |
| Tour Eiffel | 2.0 km |
| Versailles | 20 km |
Quand l'utilisateur trie par "Proximité"
Alors les résultats sont affichés dans l'ordre:
| position | contenu |
| 1 | Louvre Guide |
| 2 | Tour Eiffel |
| 3 | Versailles |
Scénario: Géocodage avec Nominatim (MVP)
Étant donné que l'application est en phase MVP
Quand une requête de géocodage est effectuée
Alors l'API publique Nominatim est utilisée
Et le rate limit de 1 req/s est respecté
Scénario: Géocodage avec fallback Mapbox
Étant donné que Nominatim ne retourne aucun résultat
Quand l'application tente un fallback
Alors l'API Mapbox Geocoding est utilisée
Et le coût de 0.50 / 1000 requêtes est appliqué
# 15.3.3 - Filtres avancés
Scénario: Ouverture du panneau de filtres
Étant donné que l'utilisateur est sur la page de recherche
Quand il clique sur "Filtres"
Alors un panneau latéral s'ouvre
Et 7 catégories de filtres sont affichées:
| catégorie |
| Type de contenu |
| Durée |
| Classification âge |
| Géo-pertinence |
| Tags |
| Date de publication |
| Abonnement |
Scénario: Filtre par type de contenu (multi-sélection)
Étant donné que l'utilisateur ouvre les filtres
Quand il sélectionne:
| type |
| Contenu court |
| Audio-guide |
Alors seuls ces types de contenus sont recherchés
Et les podcasts et radios live sont exclus
Plan du Scénario: Filtre par durée
Étant donné un contenu de <durée> minutes
Quand l'utilisateur filtre par "<tranche>"
Alors le contenu est <résultat>
Exemples:
| durée | tranche | résultat |
| 3 | <5 min | trouvé |
| 3 | 5-15 min | non trouvé |
| 10 | 5-15 min | trouvé |
| 20 | 15-30 min | trouvé |
| 45 | >30 min | trouvé |
Scénario: Filtre par classification âge
Étant donné des contenus avec différentes classifications:
| contenu | classification |
| Conte enfants | Tout public |
| Podcast news | 13+ |
| Débat politique | 16+ |
Quand l'utilisateur filtre "Tout public"
Alors seul "Conte enfants" est affiché
Scénario: Filtre par géo-pertinence
Étant donné des contenus avec différents types géo:
| contenu | type_geo |
| Guide Louvre | Ancré |
| Podcast Paris | Contextuel |
| News nationales | Neutre |
Quand l'utilisateur filtre "Ancré, Contextuel"
Alors "Guide Louvre" et "Podcast Paris" sont affichés
Et "News nationales" est exclu
Scénario: Filtre par tags (multi-sélection)
Étant donné des contenus taggés:
| contenu | tags |
| Voyage en Italie | Voyage, Gastronomie |
| Histoire de Rome | Voyage, Histoire |
| Économie italienne | Économie |
Quand l'utilisateur sélectionne les tags "Voyage, Histoire"
Alors "Histoire de Rome" est en priorité (2 tags correspondants)
Et "Voyage en Italie" est affiché (1 tag correspondant)
Et "Économie italienne" est exclu
Plan du Scénario: Filtre par date de publication
Étant donné un contenu publié il y a <délai>
Quand l'utilisateur filtre par "<période>"
Alors le contenu est <résultat>
Exemples:
| délai | période | résultat |
| 12 heures | Dernières 24h | trouvé |
| 3 jours | Cette semaine | trouvé |
| 15 jours | Ce mois | trouvé |
| 8 mois | Cette année | trouvé |
| 2 ans | Toutes dates | trouvé |
| 2 ans | Cette année | non trouvé |
Scénario: Filtre par type d'abonnement
Étant donné des contenus gratuits et Premium:
| contenu | type |
| Balade Paris | Gratuit |
| Visite VIP Louvre | Premium |
Quand l'utilisateur filtre "Premium uniquement 👑"
Alors seul "Visite VIP Louvre" est affiché
Scénario: Combinaison de filtres multiples (AND logic)
Étant donné que l'utilisateur applique les filtres:
| filtre | valeur |
| Type | Audio-guide |
| Durée | 5-15 min |
| Tags | Voyage |
| Classification | Tout public |
Quand la recherche est lancée
Alors seuls les contenus respectant TOUS les critères sont affichés
Scénario: Réinitialisation des filtres
Étant donné que l'utilisateur a appliqué 5 filtres différents
Quand il clique sur "Réinitialiser"
Alors tous les filtres sont désactivés
Et la recherche affiche tous les résultats
Scénario: Sauvegarde d'une recherche
Étant donné que l'utilisateur a appliqué plusieurs filtres
Quand il clique sur "💾 Sauvegarder cette recherche"
Et qu'il entre le nom "Podcasts voyage Paris"
Alors la recherche est sauvegardée
Et elle apparaît dans l'onglet "Recherches sauvegardées"
Scénario: Limite de 5 recherches sauvegardées
Étant donné que l'utilisateur a déjà 5 recherches sauvegardées
Quand il tente de sauvegarder une 6ème recherche
Alors un message d'erreur s'affiche
Et il doit supprimer une recherche existante avant d'en ajouter une nouvelle
Scénario: Notifications pour recherches sauvegardées
Étant donné une recherche sauvegardée "Podcasts voyage Paris"
Et que l'utilisateur a activé les notifications
Quand 3 nouveaux contenus correspondants sont publiés
Alors une notification "3 nouveaux contenus dans 'Podcasts voyage Paris'" est envoyée
Plan du Scénario: Options de tri des résultats
Étant donné une recherche avec plusieurs résultats
Quand l'utilisateur sélectionne le tri "<option>"
Alors les résultats sont triés selon <algorithme>
Exemples:
| option | algorithme |
| Pertinence | Score recherche × (1 + log(écoutes + 1)) |
| Popularité | Écoutes complètes derniers 30j DESC |
| Récent | Date publication DESC |
| Proximité | Distance GPS ASC (si recherche géo) |
| Durée | Durée audio ASC ou DESC |
# 15.3.4 - Page de résultats
Scénario: Structure d'un résultat de recherche
Étant donné un résultat de recherche
Quand la page est affichée
Alors chaque résultat contient:
| élément | exemple |
| Cover image | 120×68 px (16:9) |
| Titre | Balade à Paris (2 lignes max) |
| Créateur | @paris_stories |
| Durée | 12 min |
| Écoutes | 🎧 2.3K |
| Localisation | 📍 Paris 5e · Ancré |
| Tags | 🏷 #Voyage #Histoire |
| Badge Premium | 👑 (si applicable) |
| Distance | À 2.3 km (si recherche géo) |
| Bouton lecture | Écouter |
| Menu contextuel | |
Scénario: Lazy loading des images
Étant donné une page avec 20 résultats de recherche
Quand la page se charge
Alors seules les 5 premières images sont chargées
Et les images suivantes se chargent au scroll
Scénario: Troncature du titre sur 2 lignes maximum
Étant donné un contenu avec un titre de 120 caractères
Quand le résultat est affiché
Alors le titre est tronqué après 2 lignes
Et "..." est ajouté à la fin
Scénario: Lien cliquable vers le profil créateur
Étant donné un résultat de recherche pour "@paris_stories"
Quand l'utilisateur clique sur "@paris_stories"
Alors il est redirigé vers "https://roadwave.fr/@paris_stories"
Scénario: Menu contextuel d'un résultat [⋮]
Étant donné que l'utilisateur clique sur [] pour un résultat
Quand le menu s'ouvre
Alors les actions suivantes sont disponibles:
| action |
| Partager |
| Ajouter à une playlist |
| Télécharger (offline) |
| Signaler |
Scénario: Pagination avec 20 résultats par page
Étant donné une recherche retournant 100 résultats
Quand la page est affichée
Alors 20 résultats sont chargés initialement
Et un indicateur "1-20 sur 100 résultats" est visible
Scénario: Infinite scroll automatique
Étant donné que l'utilisateur scroll dans les résultats
Quand il atteint 80% de la page
Alors les 20 résultats suivants sont chargés automatiquement
Et un loader est affiché pendant le chargement
Scénario: Bouton fallback "Charger 20 suivants"
Étant donné que l'infinite scroll est désactivé (paramètres)
Quand l'utilisateur atteint la fin de la page
Alors un bouton "Charger 20 suivants" est affiché
Et les résultats se chargent au clic
# Vue carte
Scénario: Basculement entre vue liste et vue carte
Étant donné que l'utilisateur est sur la page de résultats
Quand il clique sur le toggle "Liste / Carte"
Alors la vue carte Leaflet s'affiche
Et les résultats sont affichés comme markers sur la carte
Scénario: Affichage de la carte Leaflet
Étant donné que la vue carte est activée
Quand la carte se charge
Alors la carte utilise les tuiles OpenStreetMap
Et le centre est la position de recherche (ou GPS utilisateur)
Et le zoom initial montre tous les résultats
Scénario: Markers cliquables sur la carte
Étant donné que 10 résultats sont affichés sur la carte
Quand l'utilisateur clique sur un marker
Alors une popup s'affiche avec:
| élément |
| Titre |
| Créateur |
| Durée |
| Distance |
| Bouton Écouter|
Scénario: Clustering des markers proches
Étant donné que 50 résultats sont très proches géographiquement
Quand la carte est affichée
Alors les markers proches sont groupés en clusters
Et le nombre de contenus est affiché sur le cluster
Et le cluster se décompose au zoom
Scénario: Synchronisation liste / carte
Étant donné que l'utilisateur est en vue carte
Quand il clique sur un marker et écoute le contenu
Et qu'il rebascule en vue liste
Alors le contenu écouté est marqué dans la liste
Et la position de scroll est maintenue
# Performances et index
Scénario: Index PostgreSQL full-text pour performances
Étant donné que la base contient 100K contenus
Quand une recherche full-text est effectuée
Alors l'index GIN sur to_tsvector est utilisé
Et la requête retourne en moins de 100ms
Scénario: Index PostGIS GIST pour recherche géo
Étant donné une recherche géographique avec rayon 50 km
Quand la requête PostGIS ST_DWithin est exécutée
Alors l'index GIST sur la colonne location est utilisé
Et la requête retourne en moins de 50ms
Scénario: Index composites pour filtres
Étant donné une recherche avec filtres multiples
Quand les filtres type, durée, âge, géo, date sont appliqués
Alors l'index composite idx_content_filters est utilisé
Et les performances restent optimales
Scénario: Index GIN pour recherche par tags
Étant donné une recherche filtrée par tags "Voyage, Histoire"
Quand la requête est exécutée
Alors l'index GIN sur la colonne tags est utilisé
Et la recherche est performante même avec 500K contenus
# Cas d'erreur
Scénario: Aucun résultat trouvé
Étant donné que l'utilisateur recherche "xyzabc123"
Quand aucun résultat n'est trouvé
Alors un message "Aucun résultat pour 'xyzabc123'" s'affiche
Et des suggestions de recherches populaires sont proposées
Scénario: Recherche vide
Étant donné que l'utilisateur clique sur "Rechercher" sans saisir de texte
Quand la recherche est lancée
Alors un message "Veuillez entrer au moins 2 caractères" s'affiche
Scénario: Erreur de géocodage Nominatim
Étant donné que l'API Nominatim est indisponible
Quand l'utilisateur tente une recherche géographique
Alors un message "Service de localisation temporairement indisponible" s'affiche
Et la recherche continue sans filtre géographique
Scénario: GPS désactivé pour "Autour de moi"
Étant donné que l'utilisateur a désactivé le GPS
Quand il sélectionne "Autour de moi"
Alors un message "Veuillez activer la localisation" s'affiche
Et un bouton "Activer" ouvre les paramètres système
Scénario: Timeout de recherche après 10 secondes
Étant donné qu'une recherche complexe est lancée
Quand la requête dépasse 10 secondes
Alors la recherche est annulée
Et un message "La recherche a pris trop de temps, veuillez réessayer" s'affiche