feat(bdd): réorganiser features en catégories api/ui/e2e et créer ADR-024
Résolution des incohérences #10, #11, et #12 de l'analyse d'architecture. ## Phase 1 : Réorganisation Features BDD (Point #10 - RÉSOLU) - Créer structure features/{api,ui,e2e} - Déplacer 83 features en 3 catégories via git mv (historique préservé) - features/api/ : 53 features (tests API backend) - features/ui/ : 22 features (tests UI mobile) - features/e2e/ : 8 features (tests end-to-end) Domaines déplacés : - API : authentication, recommendation, rgpd-compliance, content-creation, moderation, monetisation, premium, radio-live, publicites - UI : audio-guides, navigation, interest-gauges, mode-offline, partage, profil, recherche - E2E : abonnements, error-handling ## Phase 2 : Mise à jour Documentation ### ADR-007 - Tests BDD - Ajouter section "Convention de Catégorisation des Features" - Documenter règles api/ui/e2e avec exemples concrets - Spécifier step definitions (backend Go, mobile Dart) ### ADR-024 - Stratégie CI/CD Monorepo (NOUVEAU) - Créer ADR dédié pour stratégie CI/CD avec path filters - Architecture workflows séparés (backend.yml, mobile.yml, shared.yml) - Configuration path filters détaillée avec exemples YAML - Matrice de déclenchement et optimisations (~70% gain temps CI) - Plan d'implémentation (~2h, reporté jusqu'au développement) ### ADR-016 - Organisation Monorepo - Simplifier en retirant section CI/CD détaillée - Ajouter référence vers ADR-024 pour stratégie CI/CD ### INCONSISTENCIES-ANALYSIS.md - Point #10 (Tests BDD synchronisés) : ✅ RÉSOLU - Catégorisation features implémentée - ADR-007 mis à jour avec convention complète - Point #11 (70/30 Split paiements) : ✅ ANNULÉ (faux problème) - ADR-009 et Règle 18 parfaitement cohérents - Documentation exhaustive existante (formule, SQL, comparaisons) - Point #12 (Monorepo path filters) : ⏸️ DOCUMENTÉ - Architecture CI/CD complète dans ADR-024 - Implémentation reportée (projet en phase documentation) - Métriques mises à jour : - MODERATE : 6/9 traités (4 résolus + 1 annulé + 1 documenté) - ADR à jour : 100% (19/19 avec ADR-024) ## Phase 3 : Validation - Structure features validée (api/ui/e2e, aucun répertoire restant) - Historique Git préservé (git mv, renommages détectés) - 83 features total (API: 53, UI: 22, E2E: 8) Closes: Point #10 (résolu), Point #11 (annulé), Point #12 (documenté) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
333
features/ui/audio-guides/creation-audio-guide.feature
Normal file
333
features/ui/audio-guides/creation-audio-guide.feature
Normal file
@@ -0,0 +1,333 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: Création d'audio-guide multi-séquences
|
||||
En tant que créateur de contenu
|
||||
Je veux créer des audio-guides avec plusieurs séquences géolocalisées
|
||||
Afin d'offrir des expériences guidées adaptées aux différents modes de déplacement
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'application RoadWave est démarrée
|
||||
Et que le créateur "guide@example.com" est connecté
|
||||
Et que son compte est vérifié
|
||||
|
||||
# 16.1 - Types d'audio-guides et modes de déplacement
|
||||
|
||||
Plan du Scénario: Détection automatique du mode selon la vitesse
|
||||
Étant donné que l'utilisateur se déplace à <vitesse> km/h
|
||||
Quand la vitesse est calculée sur 30 secondes
|
||||
Alors le mode <mode> est suggéré automatiquement
|
||||
|
||||
Exemples:
|
||||
| vitesse | mode |
|
||||
| 3 | Piéton |
|
||||
| 15 | Vélo |
|
||||
| 35 | Voiture |
|
||||
| 50 | Voiture |
|
||||
|
||||
Scénario: Suggestion de mode au démarrage avec confirmation
|
||||
Étant donné qu'un audio-guide "Safari du Paugre" est disponible
|
||||
Et que l'utilisateur se déplace à 35 km/h
|
||||
Quand l'audio-guide démarre
|
||||
Alors une popup s'affiche:
|
||||
"""
|
||||
Détection : 🚗 Voiture. Est-ce correct ?
|
||||
[Oui] [Changer]
|
||||
"""
|
||||
|
||||
Scénario: Changement manuel du mode détecté
|
||||
Étant donné que le mode "Voiture" est suggéré automatiquement
|
||||
Quand l'utilisateur clique sur "Changer"
|
||||
Alors les 4 modes sont proposés:
|
||||
| mode | emoji |
|
||||
| Piéton | 🚶 |
|
||||
| Voiture | 🚗 |
|
||||
| Vélo | 🚴 |
|
||||
| Transport | 🚌 |
|
||||
|
||||
Plan du Scénario: Caractéristiques par mode de déplacement
|
||||
Étant donné un audio-guide configuré en mode <mode>
|
||||
Alors les paramètres suivants sont appliqués:
|
||||
| paramètre | valeur |
|
||||
| Vitesse détection | <vitesse_detection> |
|
||||
| Déclenchement | <declenchement> |
|
||||
|
||||
Exemples:
|
||||
| mode | vitesse_detection | declenchement |
|
||||
| Piéton | <5 km/h | Manuel (bouton Suivant) |
|
||||
| Voiture | >10 km/h | Auto GPS + Manuel |
|
||||
| Vélo | 5-25 km/h | Auto GPS + Manuel |
|
||||
| Transport | Variable | Auto GPS + Manuel |
|
||||
|
||||
# 16.1.2 - Création d'un audio-guide (côté créateur)
|
||||
|
||||
Scénario: Accès au formulaire de création d'audio-guide
|
||||
Étant donné que le créateur est sur son dashboard
|
||||
Quand il clique sur "Créer un audio-guide"
|
||||
Alors le formulaire de création s'affiche
|
||||
Et le wizard guidé en 4 étapes est visible:
|
||||
| étape | description |
|
||||
| 1 | Infos générales |
|
||||
| 2 | Ajout séquences |
|
||||
| 3 | Preview carte |
|
||||
| 4 | Validation modération |
|
||||
|
||||
Scénario: Étape 1 - Informations générales obligatoires
|
||||
Étant donné que le créateur est sur l'étape 1 du wizard
|
||||
Quand il complète le formulaire
|
||||
Alors les champs suivants sont obligatoires:
|
||||
| champ | contrainte |
|
||||
| Titre | 5-100 caractères |
|
||||
| Description | 10-500 caractères |
|
||||
| Mode de déplacement | Choix parmi 4 |
|
||||
| Tags | 1-3 tags |
|
||||
| Classification âge | Tout public/13+/16+/18+ |
|
||||
|
||||
Scénario: Sélection du mode de déplacement
|
||||
Étant donné que le créateur crée un audio-guide
|
||||
Quand il sélectionne le mode "🚗 Voiture (GPS auto + manuel)"
|
||||
Alors le champ "Vitesse recommandée" s'affiche
|
||||
Et la plage suggérée est "30-50 km/h"
|
||||
|
||||
Scénario: Validation du titre
|
||||
Étant donné que le créateur entre un titre
|
||||
Quand le titre contient moins de 5 caractères
|
||||
Alors un message d'erreur "Minimum 5 caractères" s'affiche
|
||||
Et le bouton "Suivant" est désactivé
|
||||
|
||||
Scénario: Validation de la description
|
||||
Étant donné que le créateur entre une description
|
||||
Quand la description contient 520 caractères
|
||||
Alors un message d'erreur "Maximum 500 caractères" s'affiche
|
||||
Et les 20 caractères en trop sont surlignés en rouge
|
||||
|
||||
Scénario: Étape 2 - Ajout de la première séquence
|
||||
Étant donné que le créateur est sur l'étape 2 "Ajout séquences"
|
||||
Quand il clique sur "Ajouter séquence"
|
||||
Alors le formulaire de séquence s'affiche avec:
|
||||
| champ | requis | note |
|
||||
| Titre séquence | ✅ | 5-80 caractères |
|
||||
| Audio | ✅ | Upload MP3/AAC, max 200 MB |
|
||||
| Point GPS | ✅* | *Sauf mode piéton |
|
||||
| Rayon déclenchement | ✅* | *Sauf mode piéton, 10-200m |
|
||||
|
||||
Scénario: Ajout du point GPS pour une séquence
|
||||
Étant donné que le créateur ajoute une séquence en mode "Voiture"
|
||||
Quand il clique sur "📍 Ajouter point GPS"
|
||||
Alors une carte s'affiche
|
||||
Et il peut:
|
||||
| action |
|
||||
| Cliquer sur la carte |
|
||||
| Entrer coordonnées manuelles |
|
||||
| Utiliser sa position actuelle |
|
||||
|
||||
Scénario: Configuration du rayon de déclenchement avec preview
|
||||
Étant donné qu'un point GPS est défini à (43.1234, 2.5678)
|
||||
Quand le créateur ajuste le curseur de rayon
|
||||
Alors le rayon varie de 10m à 200m
|
||||
Et un cercle visuel est affiché sur la carte
|
||||
Et la valeur actuelle s'affiche "30m"
|
||||
|
||||
Plan du Scénario: Rayon par défaut selon le mode
|
||||
Étant donné un audio-guide en mode <mode>
|
||||
Quand le créateur ajoute un point GPS
|
||||
Alors le rayon par défaut est <rayon_defaut>
|
||||
|
||||
Exemples:
|
||||
| mode | rayon_defaut |
|
||||
| Voiture | 30m |
|
||||
| Vélo | 50m |
|
||||
| Transport | 100m |
|
||||
|
||||
Scénario: Suggestion intelligente du rayon
|
||||
Étant donné un audio-guide en mode "Voiture" avec vitesse recommandée 30 km/h
|
||||
Quand le créateur ajoute un point GPS
|
||||
Alors une suggestion s'affiche: "Recommandé : 30m pour voiture à 30 km/h"
|
||||
|
||||
Scénario: Upload audio pour une séquence
|
||||
Étant donné que le créateur crée une séquence "Introduction"
|
||||
Quand il upload un fichier audio de 5 MB
|
||||
Alors le fichier est vérifié:
|
||||
| vérification | règle |
|
||||
| Format | MP3, AAC, M4A |
|
||||
| Taille max | 200 MB |
|
||||
| Durée max | 15 minutes |
|
||||
|
||||
Scénario: Ordre des séquences modifiable
|
||||
Étant donné un audio-guide avec 5 séquences:
|
||||
| ordre | titre |
|
||||
| 1 | Introduction |
|
||||
| 2 | Les lions |
|
||||
| 3 | Les girafes |
|
||||
| 4 | Les éléphants |
|
||||
| 5 | Conclusion |
|
||||
Quand le créateur glisse "Les éléphants" en position 2
|
||||
Alors l'ordre devient:
|
||||
| ordre | titre |
|
||||
| 1 | Introduction |
|
||||
| 2 | Les éléphants |
|
||||
| 3 | Les lions |
|
||||
| 4 | Les girafes |
|
||||
| 5 | Conclusion |
|
||||
|
||||
Scénario: Nombre minimum de séquences requis
|
||||
Étant donné un audio-guide avec seulement 1 séquence
|
||||
Quand le créateur tente de passer à l'étape suivante
|
||||
Alors un message d'erreur s'affiche: "Minimum 2 séquences requis"
|
||||
Et le bouton "Suivant" est désactivé
|
||||
|
||||
Scénario: Nombre maximum de séquences
|
||||
Étant donné un audio-guide avec 50 séquences
|
||||
Quand le créateur tente d'ajouter une 51ème séquence
|
||||
Alors un message d'erreur s'affiche: "Maximum 50 séquences par audio-guide"
|
||||
Et le bouton "+ Ajouter séquence" est désactivé
|
||||
|
||||
Scénario: Étape 3 - Preview carte avec tracé et points
|
||||
Étant donné un audio-guide avec 5 séquences géolocalisées
|
||||
Quand le créateur accède à l'étape 3 "Preview carte"
|
||||
Alors une carte Leaflet s'affiche
|
||||
Et les éléments suivants sont visibles:
|
||||
| élément | description |
|
||||
| Markers numérotés | 1, 2, 3, 4, 5 sur chaque point |
|
||||
| Tracé entre points | Ligne pointillée connectant les points |
|
||||
| Cercles de déclenchement | Rayon visuel autour de chaque point |
|
||||
|
||||
Scénario: Statistiques du parcours
|
||||
Étant donné un audio-guide avec les séquences suivantes:
|
||||
| séquence | durée | distance_au_suivant |
|
||||
| 1 | 2:15 | 150m |
|
||||
| 2 | 3:42 | 200m |
|
||||
| 3 | 4:10 | 320m |
|
||||
Quand les statistiques sont calculées
|
||||
Alors le résumé suivant est affiché:
|
||||
| métrique | valeur |
|
||||
| Séquences | 3 complètes |
|
||||
| Durée totale | 10:07 |
|
||||
| Distance totale | 670m |
|
||||
|
||||
Scénario: Modification d'une séquence depuis la carte
|
||||
Étant donné que la preview carte est affichée
|
||||
Quand le créateur clique sur le marker "2"
|
||||
Alors une popup s'affiche avec:
|
||||
| information |
|
||||
| Titre: "Les lions" |
|
||||
| Durée: 3:42 |
|
||||
| Rayon: 30m |
|
||||
| [✏️ Modifier] |
|
||||
| [🗑️ Supprimer] |
|
||||
|
||||
Scénario: Zone de diffusion géographique
|
||||
Étant donné un audio-guide avec des points dans Paris
|
||||
Quand le créateur définit la zone de diffusion
|
||||
Alors il peut choisir:
|
||||
| type | exemple |
|
||||
| Polygon | Tracé manuel sur carte |
|
||||
| Ville | Paris (API Nominatim) |
|
||||
| Département | 75 - Paris |
|
||||
| Région | Île-de-France |
|
||||
|
||||
Scénario: Étape 4 - Publication et validation modération
|
||||
Étant donné un créateur qui publie ses 3 premiers audio-guides
|
||||
Quand il clique sur "✅ Publier audio-guide"
|
||||
Alors un message s'affiche:
|
||||
"""
|
||||
Votre audio-guide est en cours de validation.
|
||||
Notre équipe le vérifiera sous 24-48h.
|
||||
Vous recevrez une notification dès validation.
|
||||
"""
|
||||
|
||||
Scénario: Publication directe pour créateurs expérimentés
|
||||
Étant donné un créateur ayant publié 5 audio-guides validés
|
||||
Et aucun strike actif
|
||||
Quand il publie un nouvel audio-guide
|
||||
Alors l'audio-guide est publié immédiatement
|
||||
Et il devient visible pour les utilisateurs
|
||||
Et aucune validation manuelle n'est requise
|
||||
|
||||
Scénario: Mode piéton sans points GPS obligatoires
|
||||
Étant donné un audio-guide en mode "🚶 Piéton"
|
||||
Quand le créateur ajoute une séquence
|
||||
Alors le champ "Point GPS" est optionnel
|
||||
Et le champ "Rayon déclenchement" est masqué
|
||||
Et un message info s'affiche: "Mode manuel : les séquences se déclenchent au clic utilisateur"
|
||||
|
||||
Scénario: Sauvegarde brouillon automatique
|
||||
Étant donné que le créateur édite un audio-guide depuis 5 minutes
|
||||
Quand il ajoute une nouvelle séquence
|
||||
Alors l'audio-guide est sauvegardé en brouillon automatiquement
|
||||
Et un toast "Brouillon sauvegardé" s'affiche brièvement
|
||||
|
||||
Scénario: Reprise d'un brouillon
|
||||
Étant donné un audio-guide en brouillon "Safari du Paugre"
|
||||
Et qu'il contient 3 séquences complètes
|
||||
Quand le créateur retourne sur son dashboard
|
||||
Alors le brouillon est visible avec le statut "📝 Brouillon"
|
||||
Et un bouton "Continuer" est disponible
|
||||
Et la progression "3/5 séquences" est affichée
|
||||
|
||||
Scénario: Suppression d'un brouillon
|
||||
Étant donné un audio-guide en brouillon
|
||||
Quand le créateur clique sur "🗑️ Supprimer"
|
||||
Alors une confirmation s'affiche:
|
||||
"""
|
||||
Supprimer ce brouillon ?
|
||||
Toutes les séquences seront perdues.
|
||||
[Annuler] [Supprimer définitivement]
|
||||
"""
|
||||
|
||||
Scénario: Modification d'un audio-guide publié
|
||||
Étant donné un audio-guide publié "Safari du Paugre"
|
||||
Quand le créateur clique sur "✏️ Modifier"
|
||||
Alors il peut modifier:
|
||||
| élément modifiable | élément non modifiable |
|
||||
| Titre | Mode de déplacement |
|
||||
| Description | Points GPS |
|
||||
| Tags | Rayons déclenchement |
|
||||
| Séquences (ordre) | |
|
||||
Et un avertissement s'affiche: "Les modifications structurelles nécessitent une nouvelle publication"
|
||||
|
||||
Scénario: Duplication d'un audio-guide existant
|
||||
Étant donné un audio-guide publié "Visite Paris"
|
||||
Quand le créateur clique sur "📋 Dupliquer"
|
||||
Alors une copie est créée avec le titre "Visite Paris (copie)"
|
||||
Et toutes les séquences sont copiées
|
||||
Et le statut est "📝 Brouillon"
|
||||
Et le créateur peut modifier avant publication
|
||||
|
||||
# Cas d'erreur
|
||||
|
||||
Scénario: Upload audio échoue (format non supporté)
|
||||
Étant donné que le créateur upload un fichier "audio.wav"
|
||||
Quand le format est vérifié
|
||||
Alors un message d'erreur s'affiche: "Format non supporté. Utilisez MP3, AAC ou M4A"
|
||||
Et le fichier est rejeté
|
||||
|
||||
Scénario: Upload audio échoue (taille trop grande)
|
||||
Étant donné que le créateur upload un fichier de 250 MB
|
||||
Quand la taille est vérifiée
|
||||
Alors un message d'erreur s'affiche: "Fichier trop volumineux. Maximum 200 MB"
|
||||
Et le fichier est rejeté
|
||||
|
||||
Scénario: Points GPS trop éloignés (alerte cohérence)
|
||||
Étant donné un audio-guide en mode "Piéton"
|
||||
Et une séquence au Louvre (Paris)
|
||||
Quand le créateur ajoute une séquence à Lyon
|
||||
Alors un avertissement s'affiche:
|
||||
"""
|
||||
⚠️ Attention : distance importante entre points (465 km)
|
||||
Vérifiez que le mode "Piéton" est approprié.
|
||||
[Modifier le mode] [Continuer]
|
||||
"""
|
||||
|
||||
Scénario: Pas de connexion lors de la sauvegarde
|
||||
Étant donné que le créateur édite un audio-guide
|
||||
Et que la connexion réseau est perdue
|
||||
Quand il tente de sauvegarder
|
||||
Alors le brouillon est sauvegardé localement
|
||||
Et un message s'affiche: "Sauvegarde locale. Sera synchronisée à la reconnexion"
|
||||
Et une icône "☁️ Hors ligne" s'affiche
|
||||
|
||||
Scénario: Reprise après perte de connexion
|
||||
Étant donné un brouillon sauvegardé localement
|
||||
Quand la connexion réseau est rétablie
|
||||
Alors le brouillon est synchronisé automatiquement
|
||||
Et un toast "✅ Audio-guide synchronisé" s'affiche
|
||||
349
features/ui/audio-guides/integration-fonctionnalites.feature
Normal file
349
features/ui/audio-guides/integration-fonctionnalites.feature
Normal file
@@ -0,0 +1,349 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: Intégration audio-guides avec autres fonctionnalités
|
||||
En tant qu'utilisateur
|
||||
Je veux utiliser les audio-guides avec toutes les fonctionnalités de l'app
|
||||
Afin d'avoir une expérience complète et cohérente
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'application RoadWave est démarrée
|
||||
Et que l'utilisateur "jean@example.com" est connecté
|
||||
|
||||
# 16.14 - Téléchargement offline
|
||||
|
||||
Scénario: Téléchargement complet d'un audio-guide
|
||||
Étant donné un audio-guide "Visite du Louvre" avec 12 séquences
|
||||
Quand l'utilisateur clique sur "⬇️ Télécharger pour écouter hors ligne"
|
||||
Alors toutes les 12 séquences sont téléchargées
|
||||
Et les métadonnées (titres, descriptions, GPS) sont sauvegardées
|
||||
Et les images (cover, miniatures) sont mises en cache
|
||||
|
||||
Scénario: Affichage de la progression du téléchargement
|
||||
Étant donné qu'un téléchargement d'audio-guide est en cours
|
||||
Quand l'utilisateur consulte l'état
|
||||
Alors la progression s'affiche:
|
||||
"""
|
||||
⬇️ Téléchargement en cours...
|
||||
Séquence 7/12 • 245 MB / 380 MB
|
||||
──────●──────── 64%
|
||||
"""
|
||||
|
||||
Scénario: Téléchargement uniquement en WiFi (par défaut)
|
||||
Étant donné que l'option "Télécharger uniquement en WiFi" est activée
|
||||
Quand l'utilisateur lance un téléchargement sur réseau mobile
|
||||
Alors un avertissement s'affiche:
|
||||
"""
|
||||
⚠️ Téléchargement nécessite WiFi
|
||||
Cet audio-guide pèse 380 MB.
|
||||
[Attendre WiFi] [Télécharger quand même]
|
||||
"""
|
||||
|
||||
Scénario: Gestion de l'espace de stockage
|
||||
Étant donné que l'appareil a 500 MB d'espace libre
|
||||
Et qu'un audio-guide pèse 380 MB
|
||||
Quand l'utilisateur lance le téléchargement
|
||||
Alors un avertissement s'affiche:
|
||||
"""
|
||||
⚠️ Espace de stockage limité
|
||||
Après téléchargement : 120 MB restants
|
||||
[Continuer] [Gérer stockage] [Annuler]
|
||||
"""
|
||||
|
||||
Scénario: Liste des audio-guides téléchargés
|
||||
Étant donné que l'utilisateur a téléchargé 3 audio-guides
|
||||
Quand il accède à "Bibliothèque > Téléchargés"
|
||||
Alors il voit:
|
||||
| audio_guide | taille | date_telechargement |
|
||||
| Visite du Louvre | 380 MB | 2026-01-20 |
|
||||
| Safari du Paugre | 245 MB | 2026-01-18 |
|
||||
| Circuit Loire à Vélo | 520 MB | 2026-01-15 |
|
||||
|
||||
Scénario: Lecture hors connexion complète
|
||||
Étant donné qu'un audio-guide est téléchargé
|
||||
Et que l'utilisateur active le mode avion
|
||||
Quand il lance l'audio-guide
|
||||
Alors toutes les séquences sont lisibles
|
||||
Et les métadonnées sont accessibles
|
||||
Et les images s'affichent normalement
|
||||
Et la progression est sauvegardée localement
|
||||
|
||||
Scénario: GPS fonctionne en mode avion (mode voiture)
|
||||
Étant donné qu'un audio-guide voiture est téléchargé
|
||||
Et que le mode avion est activé (avec GPS actif)
|
||||
Quand l'utilisateur se déplace
|
||||
Alors les déclenchements GPS fonctionnent normalement
|
||||
Et la distance/ETA sont calculés
|
||||
Parce que le GPS ne nécessite pas de connexion internet
|
||||
|
||||
Scénario: Suppression d'audio-guide téléchargé
|
||||
Étant donné qu'un audio-guide téléchargé pèse 380 MB
|
||||
Quand l'utilisateur clique sur "🗑️ Supprimer téléchargement"
|
||||
Alors une confirmation s'affiche
|
||||
Et si confirmé, les 380 MB sont libérés
|
||||
Et l'audio-guide reste accessible en streaming
|
||||
|
||||
Scénario: Mise à jour automatique si nouvelle version
|
||||
Étant donné qu'un audio-guide téléchargé a été mis à jour par le créateur
|
||||
Quand l'utilisateur se connecte en WiFi
|
||||
Alors une notification s'affiche:
|
||||
"""
|
||||
🔄 Mise à jour disponible
|
||||
"Visite du Louvre" - Nouvelle version
|
||||
[Mettre à jour] [Plus tard]
|
||||
"""
|
||||
|
||||
# 16.15 - Playlists et collections
|
||||
|
||||
Scénario: Ajout d'audio-guide à une playlist
|
||||
Étant donné que l'utilisateur consulte un audio-guide
|
||||
Quand il clique sur "➕ Ajouter à une playlist"
|
||||
Alors ses playlists s'affichent:
|
||||
| playlist |
|
||||
| 🗺️ Voyages en France |
|
||||
| 🏛️ Musées parisiens |
|
||||
| + Créer nouvelle playlist |
|
||||
|
||||
Scénario: Comportement audio-guide dans une playlist
|
||||
Étant donné une playlist contenant 2 audio-guides et 1 podcast
|
||||
Quand la lecture atteint un audio-guide
|
||||
Alors l'audio-guide démarre à la séquence 1 (ou progression sauvegardée)
|
||||
Et les séquences se jouent normalement
|
||||
Quand l'audio-guide se termine (dernière séquence)
|
||||
Alors le contenu suivant de la playlist démarre
|
||||
|
||||
Scénario: Audio-guide marqué comme "Favori"
|
||||
Étant donné qu'un utilisateur aime un audio-guide
|
||||
Quand il clique sur "⭐ Ajouter aux favoris"
|
||||
Alors l'audio-guide est ajouté à la section "Favoris"
|
||||
Et il est facilement accessible depuis le menu principal
|
||||
|
||||
Scénario: Collections thématiques d'audio-guides
|
||||
Étant donné que RoadWave propose des collections éditoriales
|
||||
Quand l'utilisateur accède à "Collections"
|
||||
Alors il voit des collections comme:
|
||||
| collection | nombre_audio_guides |
|
||||
| 🏛️ Musées de France | 12 |
|
||||
| 🦁 Parcs animaliers | 8 |
|
||||
| 🚴 Circuits vélo | 15 |
|
||||
| 🚗 Routes touristiques | 10 |
|
||||
|
||||
# 16.16 - Partage d'audio-guide
|
||||
|
||||
Scénario: Bouton partager sur page audio-guide
|
||||
Étant donné qu'un utilisateur consulte un audio-guide
|
||||
Quand il clique sur "⬆️ Partager"
|
||||
Alors le menu de partage natif s'ouvre
|
||||
Et le lien généré est "https://roadwave.fr/share/ag/louvre_123"
|
||||
|
||||
Scénario: Page web de partage pour audio-guide
|
||||
Étant donné qu'un lien d'audio-guide partagé est ouvert sur le web
|
||||
Quand la page se charge
|
||||
Alors elle affiche:
|
||||
| élément | exemple |
|
||||
| Cover image 16:9 | Photo du Louvre |
|
||||
| Titre | "Visite du Louvre" |
|
||||
| Créateur | "@art_guide ✓" |
|
||||
| Badge type | "🎧 Audio-guide • 12 séquences" |
|
||||
| Durée totale | "45 minutes" |
|
||||
| Mode | "🚶 Piéton" |
|
||||
| Description | Texte complet |
|
||||
| Preview séquence 1 | Player HTML5 (séquence intro) |
|
||||
| Carte avec points GPS | Leaflet avec 12 markers |
|
||||
| CTA téléchargement | Boutons App Store / Google Play |
|
||||
|
||||
Scénario: Deep link vers audio-guide spécifique
|
||||
Étant donné que l'app est installée
|
||||
Et qu'un lien "https://roadwave.fr/share/ag/louvre_123" est cliqué
|
||||
Quand le système détecte l'app
|
||||
Alors l'app s'ouvre directement sur l'audio-guide
|
||||
Et l'utilisateur peut démarrer immédiatement
|
||||
|
||||
Scénario: Partage avec séquence spécifique
|
||||
Étant donné qu'un utilisateur est sur la séquence 5 "La Joconde"
|
||||
Quand il partage l'audio-guide
|
||||
Alors le lien généré est "https://roadwave.fr/share/ag/louvre_123?seq=5"
|
||||
Et le destinataire est dirigé vers la séquence 5 directement
|
||||
|
||||
# 16.17 - Notations et commentaires
|
||||
|
||||
Scénario: Note globale de l'audio-guide
|
||||
Étant donné qu'un utilisateur termine un audio-guide
|
||||
Quand la dernière séquence se termine
|
||||
Alors une popup de notation s'affiche:
|
||||
"""
|
||||
Comment avez-vous trouvé cet audio-guide ?
|
||||
⭐⭐⭐⭐⭐
|
||||
[Ajouter un commentaire (optionnel)]
|
||||
"""
|
||||
|
||||
Scénario: Note moyenne affichée sur la page
|
||||
Étant donné qu'un audio-guide a reçu 150 notes
|
||||
Et que la moyenne est 4.3/5
|
||||
Quand la page est affichée
|
||||
Alors la note "⭐ 4.3 (150 avis)" est visible
|
||||
|
||||
Scénario: Commentaires triés par pertinence
|
||||
Étant donné qu'un audio-guide a 50 commentaires
|
||||
Quand l'utilisateur consulte les avis
|
||||
Alors les commentaires sont triés par défaut selon:
|
||||
| critère | poids |
|
||||
| Note élevée | 30% |
|
||||
| Récent | 30% |
|
||||
| Likes reçus | 40% |
|
||||
|
||||
Scénario: Réponse du créateur aux commentaires
|
||||
Étant donné qu'un utilisateur laisse un commentaire négatif
|
||||
Quand le créateur consulte son dashboard
|
||||
Alors il peut répondre au commentaire
|
||||
Et sa réponse apparaît en dessous avec badge "Créateur"
|
||||
|
||||
# 16.18 - Recommandations intelligentes
|
||||
|
||||
Scénario: Audio-guides similaires recommandés
|
||||
Étant donné qu'un utilisateur termine "Visite du Louvre"
|
||||
Quand il consulte les recommandations
|
||||
Alors l'algorithme suggère des audio-guides basés sur:
|
||||
| critère | exemple |
|
||||
| Tags similaires | #Art #Histoire #Musée |
|
||||
| Créateur identique | Autres audio-guides de @art_guide |
|
||||
| Localisation proche | Autres musées parisiens |
|
||||
| Mode de déplacement | Autres audio-guides piéton |
|
||||
|
||||
Scénario: Suggestion géographique contextuelle
|
||||
Étant donné qu'un utilisateur est à Paris (GPS détecté)
|
||||
Quand il ouvre l'onglet "Audio-guides"
|
||||
Alors les audio-guides parisiens sont mis en avant
|
||||
Et un filtre "🗺️ Autour de moi" est pré-appliqué
|
||||
|
||||
Scénario: Badge "Populaire dans votre région"
|
||||
Étant donné qu'un audio-guide a >100 écoutes dans la région Île-de-France
|
||||
Et que l'utilisateur est en Île-de-France
|
||||
Quand l'audio-guide est affiché
|
||||
Alors un badge "🔥 Populaire près de chez vous" est visible
|
||||
|
||||
# 16.19 - Optimisations techniques
|
||||
|
||||
Scénario: Préchargement de la séquence suivante
|
||||
Étant donné que la séquence 3 est en cours à 2:30/3:42
|
||||
Quand il reste 60 secondes de lecture
|
||||
Alors la séquence 4 est préchargée en arrière-plan
|
||||
Et la transition est instantanée (0 latence)
|
||||
|
||||
Scénario: Buffer adaptatif selon connexion
|
||||
Étant donné qu'un utilisateur est sur réseau 4G
|
||||
Quand la séquence démarre
|
||||
Alors 30 secondes d'audio sont bufferisées initialement
|
||||
Et le buffering continue en arrière-plan
|
||||
|
||||
Plan du Scénario: Buffer selon qualité réseau
|
||||
Étant donné qu'un utilisateur est sur réseau <reseau>
|
||||
Quand une séquence démarre
|
||||
Alors <buffer_secondes> secondes sont bufferisées
|
||||
|
||||
Exemples:
|
||||
| reseau | buffer_secondes |
|
||||
| WiFi | 60 |
|
||||
| 5G | 45 |
|
||||
| 4G | 30 |
|
||||
| 3G | 20 |
|
||||
|
||||
Scénario: Compression audio adaptative
|
||||
Étant donné qu'un utilisateur est sur connexion lente (3G)
|
||||
Quand une séquence est streamée
|
||||
Alors le CDN sert la version 64 kbps (au lieu de 128 kbps)
|
||||
Et la qualité reste acceptable pour la voix
|
||||
|
||||
Scénario: Cache intelligent des séquences jouées
|
||||
Étant donné qu'un utilisateur a écouté les séquences 1-5
|
||||
Quand il clique sur "Précédent" pour réécouter la séquence 4
|
||||
Alors la séquence 4 est chargée depuis le cache local
|
||||
Et le chargement est instantané (pas de stream)
|
||||
|
||||
Scénario: Nettoyage automatique du cache
|
||||
Étant donné que le cache audio occupe 500 MB
|
||||
Et que la limite configurée est 300 MB
|
||||
Quand le nettoyage automatique s'exécute
|
||||
Alors les séquences les plus anciennes (non téléchargées) sont supprimées
|
||||
Et le cache revient à 280 MB
|
||||
|
||||
# 16.20 - Analytics et tracking
|
||||
|
||||
Scénario: Tracking des événements clés
|
||||
Étant donné qu'un utilisateur écoute un audio-guide
|
||||
Quand il interagit avec l'application
|
||||
Alors les événements suivants sont trackés:
|
||||
| événement | données |
|
||||
| audio_guide_started | audio_guide_id, mode, user_id |
|
||||
| sequence_completed | sequence_id, completion_rate, duration |
|
||||
| audio_guide_completed | audio_guide_id, total_time, sequences_count|
|
||||
| point_gps_triggered | point_id, distance, auto_or_manual |
|
||||
| point_gps_missed | point_id, distance, action_taken |
|
||||
| paywall_displayed | audio_guide_id, sequence_number |
|
||||
| premium_conversion | source: audio_guide_paywall |
|
||||
|
||||
Scénario: Heatmap des abandons par séquence
|
||||
Étant donné qu'un audio-guide a été écouté 1000 fois
|
||||
Quand le créateur consulte la heatmap
|
||||
Alors il voit pour chaque séquence:
|
||||
| sequence | starts | completions | abandon_rate |
|
||||
| 1 | 1000 | 950 | 5% |
|
||||
| 2 | 950 | 920 | 3% |
|
||||
| 3 | 920 | 850 | 8% |
|
||||
| ... | ... | ... | ... |
|
||||
| 12 | 650 | 580 | 11% |
|
||||
|
||||
Scénario: Attribution GPS auto vs manuel
|
||||
Étant donné un audio-guide voiture avec 8 points GPS
|
||||
Quand les statistiques sont calculées
|
||||
Alors le créateur voit:
|
||||
| mode_declenchement | nombre |
|
||||
| GPS automatique | 542 |
|
||||
| Manuel | 123 |
|
||||
| Point manqué | 89 |
|
||||
|
||||
# Cas d'erreur et edge cases
|
||||
|
||||
Scénario: Audio-guide avec une seule séquence (edge case)
|
||||
Étant donné un audio-guide avec seulement 1 séquence
|
||||
Quand il est publié
|
||||
Alors un avertissement s'affiche:
|
||||
"""
|
||||
⚠️ Un audio-guide doit contenir au minimum 2 séquences
|
||||
Ajoutez au moins 1 séquence supplémentaire avant publication
|
||||
"""
|
||||
|
||||
Scénario: Séquence manquante ou corrompue
|
||||
Étant donné qu'une séquence 5 a un fichier audio corrompu
|
||||
Quand l'utilisateur tente de la lire
|
||||
Alors un message d'erreur s'affiche
|
||||
Et un bouton "⏭️ Passer à la suivante" est disponible
|
||||
Et le créateur reçoit une notification de l'erreur
|
||||
|
||||
Scénario: GPS désactivé puis réactivé en cours de route
|
||||
Étant donné un audio-guide voiture en cours
|
||||
Et que l'utilisateur désactive le GPS
|
||||
Quand il le réactive 10 minutes plus tard
|
||||
Alors le déclenchement automatique reprend
|
||||
Et les points GPS manqués entre-temps ne déclenchent pas de popup
|
||||
|
||||
Scénario: Modification d'audio-guide avec utilisateurs en cours
|
||||
Étant donné qu'un audio-guide a 50 utilisateurs en cours d'écoute
|
||||
Quand le créateur modifie une séquence
|
||||
Alors les utilisateurs actuels conservent l'ancienne version
|
||||
Et les nouveaux utilisateurs obtiennent la nouvelle version
|
||||
Et un message informe les utilisateurs lors de la prochaine ouverture
|
||||
|
||||
Scénario: Suppression d'audio-guide par le créateur
|
||||
Étant donné qu'un audio-guide a 20 utilisateurs avec progression
|
||||
Quand le créateur supprime l'audio-guide
|
||||
Alors une confirmation stricte est demandée
|
||||
Et si confirmé, les progressions utilisateurs sont archivées (30 jours)
|
||||
Et l'audio-guide devient inaccessible
|
||||
|
||||
Scénario: Signalement d'audio-guide pour contenu inapproprié
|
||||
Étant donné qu'un utilisateur signale un audio-guide
|
||||
Quand le signalement est modéré
|
||||
Et jugé valide
|
||||
Alors l'audio-guide est dépublié temporairement
|
||||
Et le créateur reçoit une notification d'explication
|
||||
Et il peut corriger puis republier
|
||||
243
features/ui/audio-guides/mode-pieton.feature
Normal file
243
features/ui/audio-guides/mode-pieton.feature
Normal file
@@ -0,0 +1,243 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: Audio-guide mode piéton (navigation manuelle)
|
||||
En tant qu'utilisateur à pied
|
||||
Je veux naviguer manuellement entre les séquences d'un audio-guide
|
||||
Afin de contrôler mon rythme de visite
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'application RoadWave est démarrée
|
||||
Et que l'utilisateur "jean@example.com" est connecté (gratuit)
|
||||
Et qu'un audio-guide piéton "Visite du Louvre" est disponible avec 12 séquences
|
||||
|
||||
# 16.2.1 - Passage entre séquences
|
||||
|
||||
Scénario: Fin de séquence normale avec pause automatique
|
||||
Étant donné que la séquence 1 "Introduction" est en cours de lecture
|
||||
Quand la séquence se termine à 2:15
|
||||
Alors le player se met en pause automatiquement
|
||||
Et le message suivant s'affiche: "Séquence 1 terminée. Appuyez sur Suivant quand vous êtes prêt."
|
||||
Et la barre de progression indique "1/12 complétée"
|
||||
|
||||
Scénario: Passage manuel à la séquence suivante
|
||||
Étant donné que la séquence 1 est terminée et le player en pause
|
||||
Quand l'utilisateur appuie sur le bouton [▶|] "Suivant"
|
||||
Alors la séquence 2 "Pyramide du Louvre" démarre immédiatement
|
||||
Et aucune latence n'est observée
|
||||
|
||||
Scénario: Séquence avec publicité (1/5 séquences)
|
||||
Étant donné que la séquence 5 se termine
|
||||
Et que c'est la 5ème séquence (1 pub toutes les 5)
|
||||
Quand la séquence se termine
|
||||
Alors la publicité s'enchaîne automatiquement (sans attente bouton)
|
||||
Et la publicité se lit normalement
|
||||
Et elle est skippable après 5 secondes
|
||||
|
||||
Scénario: Fin de publicité avec pause automatique
|
||||
Étant donné qu'une publicité est en cours de lecture
|
||||
Quand la publicité se termine
|
||||
Alors le player se met en pause automatiquement
|
||||
Et le message suivant s'affiche: "Séquence 6 prête. Appuyez sur Suivant."
|
||||
Et l'utilisateur doit cliquer sur [▶|] pour continuer
|
||||
|
||||
Scénario: Flux complet séquence → pub → séquence
|
||||
Étant donné que la séquence 5 démarre
|
||||
Quand la séquence 5 se termine
|
||||
Alors la publicité démarre automatiquement
|
||||
Quand la publicité se termine
|
||||
Alors le player se met en pause
|
||||
Quand l'utilisateur clique sur [▶|]
|
||||
Alors la séquence 6 démarre
|
||||
|
||||
Plan du Scénario: Fréquence de publicité configurable
|
||||
Étant donné que l'utilisateur gratuit écoute un audio-guide
|
||||
Et que la fréquence pub est configurée à <frequence>
|
||||
Quand il termine la séquence <numero_sequence>
|
||||
Alors une publicité est insérée : <pub_inseree>
|
||||
|
||||
Exemples:
|
||||
| frequence | numero_sequence | pub_inseree |
|
||||
| 1/5 | 5 | Oui |
|
||||
| 1/5 | 10 | Oui |
|
||||
| 1/5 | 4 | Non |
|
||||
| 1/3 | 3 | Oui |
|
||||
| 1/3 | 6 | Oui |
|
||||
|
||||
Scénario: Utilisateur Premium sans publicités
|
||||
Étant donné que l'utilisateur "premium@example.com" est abonné Premium
|
||||
Et qu'il écoute un audio-guide piéton
|
||||
Quand il termine la séquence 5
|
||||
Alors aucune publicité n'est insérée
|
||||
Et le player se met en pause immédiatement
|
||||
Et le message "Séquence 6 prête. Appuyez sur Suivant." s'affiche
|
||||
|
||||
# 16.2.2 - Navigation et contrôles
|
||||
|
||||
Scénario: Boutons de contrôle disponibles en mode piéton
|
||||
Étant donné qu'un audio-guide piéton est en lecture
|
||||
Quand l'utilisateur consulte les contrôles
|
||||
Alors les boutons suivants sont visibles:
|
||||
| bouton | fonction |
|
||||
| [▶\|] Suivant | Passe à la séquence suivante |
|
||||
| [\|◀] Précédent | Retour à la séquence précédente |
|
||||
| [⏸️] Pause | Pause temporaire |
|
||||
| [▶️] Play | Reprend la lecture |
|
||||
| [📋] Liste | Affiche toutes les séquences |
|
||||
|
||||
Scénario: Passage à la séquence suivante pendant la lecture
|
||||
Étant donné que la séquence 3 "La Joconde" est en cours à 1:42/3:42
|
||||
Quand l'utilisateur clique sur [▶|] "Suivant"
|
||||
Alors la séquence 4 "Vénus de Milo" démarre immédiatement
|
||||
Et la séquence 3 n'est pas marquée comme écoutée (car <80%)
|
||||
|
||||
Scénario: Retour à la séquence précédente (saut direct)
|
||||
Étant donné que la séquence 5 est en cours de lecture
|
||||
Quand l'utilisateur clique sur [|◀] "Précédent"
|
||||
Alors la séquence 4 démarre depuis le début (0:00)
|
||||
Et il n'y a pas de logique "replay si >10s" (contrairement au contenu classique)
|
||||
|
||||
Scénario: Pause et reprise pendant une séquence
|
||||
Étant donné que la séquence 2 est en cours à 1:15/1:48
|
||||
Quand l'utilisateur clique sur [⏸️] "Pause"
|
||||
Alors la lecture se met en pause
|
||||
Et la position 1:15 est conservée
|
||||
Quand l'utilisateur clique sur [▶️] "Play"
|
||||
Alors la lecture reprend exactement à 1:15
|
||||
|
||||
Scénario: Interface liste des séquences
|
||||
Étant donné qu'un audio-guide de 12 séquences est en cours
|
||||
Quand l'utilisateur clique sur [📋] "Liste séquences"
|
||||
Alors une liste complète s'affiche avec:
|
||||
| élément | exemple |
|
||||
| Numéro et titre | "3. La Joconde" |
|
||||
| Durée | (3:42) |
|
||||
| État | ✅ Écouté / ▶️ En cours / ⭕ À écouter |
|
||||
| Date écoute (si écouté) | "Écouté le 15/01/2026" |
|
||||
|
||||
Scénario: Séquence en cours dans la liste
|
||||
Étant donné que la séquence 3 est en cours à 1:22/3:42
|
||||
Quand la liste des séquences est affichée
|
||||
Alors la séquence 3 affiche:
|
||||
"""
|
||||
▶️ 3. La Joconde (3:42) - EN COURS
|
||||
──●──────────── 1:22/3:42
|
||||
"""
|
||||
|
||||
Scénario: Navigation libre vers séquence non encore écoutée
|
||||
Étant donné que l'utilisateur est sur la séquence 3
|
||||
Et que les séquences 4 à 12 n'ont pas été écoutées
|
||||
Quand l'utilisateur clique sur "8. Les Appartements de Napoléon"
|
||||
Alors la séquence 8 démarre immédiatement depuis 0:00
|
||||
Et les séquences 4 à 7 restent marquées ⭕ "À écouter"
|
||||
|
||||
Scénario: Retour à une séquence déjà écoutée
|
||||
Étant donné que la séquence 2 "Pyramide du Louvre" a été écoutée à 100%
|
||||
Et qu'elle est marquée ✅ "Écouté"
|
||||
Quand l'utilisateur clique dessus dans la liste
|
||||
Alors la séquence 2 démarre depuis 0:00
|
||||
Et le statut ✅ est conservé
|
||||
|
||||
Scénario: Checkmarks sur séquences écoutées >80%
|
||||
Étant donné que l'utilisateur écoute la séquence 2 de durée 1:48
|
||||
Quand il écoute jusqu'à 1:30 (83% de complétion)
|
||||
Et qu'il passe à la séquence suivante
|
||||
Alors la séquence 2 est marquée ✅ "Écouté"
|
||||
Et la date d'écoute est enregistrée
|
||||
|
||||
Scénario: Pas de checkmark si séquence écoutée <80%
|
||||
Étant donné que l'utilisateur écoute la séquence 3 de durée 3:42
|
||||
Quand il écoute jusqu'à 1:30 (40% de complétion)
|
||||
Et qu'il passe à la séquence suivante
|
||||
Alors la séquence 3 reste marquée ⭕ "À écouter"
|
||||
|
||||
Scénario: Bouton "Tout afficher" si plus de 6 séquences
|
||||
Étant donné un audio-guide avec 12 séquences
|
||||
Quand la liste est affichée
|
||||
Alors seules les 6 premières séquences sont visibles initialement
|
||||
Et un bouton "Tout afficher ▼" est présent
|
||||
Quand l'utilisateur clique sur "Tout afficher ▼"
|
||||
Alors les 6 séquences restantes sont affichées
|
||||
|
||||
Scénario: Saut vers séquence spécifique depuis la barre de progression
|
||||
Étant donné qu'un audio-guide est en cours
|
||||
Quand l'utilisateur clique sur "3/12" dans la barre de progression
|
||||
Alors la liste des séquences s'ouvre
|
||||
Et la séquence en cours (3) est mise en surbrillance
|
||||
|
||||
# Sauvegarde progression
|
||||
|
||||
Scénario: Position exacte sauvegardée automatiquement
|
||||
Étant donné que la séquence 5 est en cours à 2:34/4:10
|
||||
Quand l'utilisateur quitte l'application
|
||||
Alors la position 2:34 dans la séquence 5 est sauvegardée
|
||||
Et la sauvegarde est effectuée localement (SQLite)
|
||||
Et la sauvegarde est synchronisée sur le cloud (PostgreSQL)
|
||||
|
||||
Scénario: Reprise après fermeture de l'application
|
||||
Étant donné que l'utilisateur a quitté l'app à la séquence 5 position 2:34
|
||||
Quand il rouvre l'audio-guide
|
||||
Alors une popup de reprise s'affiche
|
||||
Quand il clique sur "▶️ Reprendre"
|
||||
Alors la lecture reprend à la séquence 5 position 2:34 exacte
|
||||
|
||||
# Cas d'usage réels
|
||||
|
||||
Scénario: Visiteur qui connaît déjà certaines œuvres
|
||||
Étant donné qu'un visiteur du Louvre démarre l'audio-guide
|
||||
Et qu'il connaît déjà "La Joconde" (séquence 3)
|
||||
Quand il arrive à la séquence 3
|
||||
Et qu'il clique sur [▶|] "Suivant" après 10 secondes
|
||||
Alors la séquence 4 démarre immédiatement
|
||||
Et la séquence 3 n'est pas marquée comme écoutée
|
||||
|
||||
Scénario: Visiteur qui veut voir une œuvre éloignée
|
||||
Étant donné qu'un visiteur est à la séquence 2
|
||||
Et qu'il aperçoit "La Victoire de Samothrace" (séquence 8) physiquement
|
||||
Quand il ouvre la liste et clique sur la séquence 8
|
||||
Alors la séquence 8 démarre immédiatement
|
||||
Et il peut écouter la description même si les séquences 3-7 ne sont pas écoutées
|
||||
|
||||
Scénario: Visiteur qui prend une pause café
|
||||
Étant donné qu'un visiteur écoute la séquence 6
|
||||
Quand il clique sur [⏸️] "Pause"
|
||||
Et qu'il ferme l'application pendant 30 minutes
|
||||
Quand il rouvre l'application
|
||||
Alors la séquence 6 reprend à la position exacte où il s'était arrêté
|
||||
|
||||
Scénario: Visiteur qui revient le lendemain
|
||||
Étant donné qu'un visiteur a écouté les séquences 1-5 hier
|
||||
Et qu'il revient au musée aujourd'hui
|
||||
Quand il ouvre l'audio-guide
|
||||
Alors une popup propose "▶️ Reprendre" (séquence 6)
|
||||
Et les séquences 1-5 sont marquées ✅ "Écouté"
|
||||
|
||||
# Cas d'erreur
|
||||
|
||||
Scénario: Séquence audio corrompue ou indisponible
|
||||
Étant donné que la séquence 7 a un fichier audio corrompu
|
||||
Quand l'utilisateur tente de la lire
|
||||
Alors un message d'erreur s'affiche:
|
||||
"""
|
||||
⚠️ Cette séquence est temporairement indisponible.
|
||||
[⏭️ Passer à la suivante] [🔄 Réessayer]
|
||||
"""
|
||||
|
||||
Scénario: Perte de connexion pendant le chargement
|
||||
Étant donné que l'utilisateur lance la séquence 4
|
||||
Et que la connexion réseau est perdue
|
||||
Quand le chargement échoue
|
||||
Alors un message s'affiche: "Connexion perdue. Vérifiez votre réseau."
|
||||
Et un bouton "🔄 Réessayer" est disponible
|
||||
|
||||
Scénario: Batterie faible en cours de visite
|
||||
Étant donné que la batterie de l'appareil est à 5%
|
||||
Quand l'utilisateur écoute une séquence
|
||||
Alors une notification système s'affiche: "Batterie faible. Progression sauvegardée."
|
||||
Et la position est sauvegardée localement toutes les 10 secondes
|
||||
|
||||
Scénario: Mode piéton sans points GPS (pas d'alerte localisation)
|
||||
Étant donné un audio-guide en mode piéton
|
||||
Et que le GPS est désactivé
|
||||
Quand l'utilisateur démarre l'audio-guide
|
||||
Alors aucune alerte GPS ne s'affiche
|
||||
Et l'audio-guide fonctionne normalement (navigation 100% manuelle)
|
||||
400
features/ui/audio-guides/mode-voiture.feature
Normal file
400
features/ui/audio-guides/mode-voiture.feature
Normal file
@@ -0,0 +1,400 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: Audio-guide mode voiture (GPS automatique)
|
||||
En tant qu'utilisateur en voiture
|
||||
Je veux que les séquences se déclenchent automatiquement selon ma position GPS
|
||||
Afin de profiter d'une expérience guidée hands-free
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'application RoadWave est démarrée
|
||||
Et que l'utilisateur "jean@example.com" est connecté (gratuit)
|
||||
Et qu'un audio-guide voiture "Safari du Paugre" est disponible avec 8 séquences
|
||||
Et que le GPS est activé
|
||||
|
||||
# 16.3.1 - Déclenchement et contrôles
|
||||
|
||||
Scénario: Distinction audio-guides vs contenus géolocalisés simples
|
||||
Étant donné que l'utilisateur est en mode voiture
|
||||
Quand il écoute un contenu géolocalisé simple (1 séquence unique)
|
||||
Alors une notification avec compteur 7→1 est affichée 7s avant le point
|
||||
Et il doit valider avec "Suivant" + décompte 5s
|
||||
Et ce contenu compte 1/6 dans le quota horaire
|
||||
Quand il démarre un audio-guide multi-séquences
|
||||
Alors les séquences se déclenchent au point GPS exact (rayon 30m)
|
||||
Et aucun compteur 7s n'est affiché (juste notification "Ding" + toast 2s)
|
||||
Et l'audio-guide entier compte 1/6 dans le quota
|
||||
|
||||
Scénario: Démarrage automatique au premier point GPS
|
||||
Étant donné que l'utilisateur démarre l'audio-guide "Safari du Paugre"
|
||||
Et que le point de départ est à (43.1234, 2.5678) avec rayon 30m
|
||||
Quand l'utilisateur entre dans le rayon de 30m
|
||||
Alors la séquence 1 "Introduction - Point d'accueil" démarre automatiquement
|
||||
Et une notification sonore "Ding" est jouée (non intrusif)
|
||||
Et un toast s'affiche brièvement pendant 2s: "Introduction - Point d'accueil"
|
||||
Et aucun compteur 7→1 n'est affiché (contrairement aux contenus géolocalisés simples)
|
||||
|
||||
Scénario: Déclenchement automatique séquence suivante
|
||||
Étant donné que la séquence 1 est terminée
|
||||
Et que l'utilisateur se déplace vers le point GPS 2 (43.1245, 2.5690)
|
||||
Quand l'utilisateur entre dans le rayon de 30m du point 2
|
||||
Alors la séquence 2 "Enclos des lions" démarre automatiquement
|
||||
Et une notification "Ding" + toast "Enclos des lions" s'affiche
|
||||
|
||||
Scénario: Navigation manuelle conservée (bouton Suivant actif)
|
||||
Étant donné que la séquence 1 est en cours
|
||||
Et que l'utilisateur est encore loin du point GPS 2 (distance 500m)
|
||||
Quand l'utilisateur clique sur [▶|] "Suivant"
|
||||
Alors la séquence 2 démarre immédiatement
|
||||
Et aucune vérification GPS n'est effectuée
|
||||
|
||||
Scénario: Navigation manuelle conservée (bouton Précédent actif)
|
||||
Étant donné que la séquence 3 est en cours
|
||||
Quand l'utilisateur clique sur [|◀] "Précédent"
|
||||
Alors la séquence 2 démarre depuis le début
|
||||
Et aucune vérification GPS n'est effectuée
|
||||
|
||||
Scénario: Tous les boutons de contrôle restent actifs
|
||||
Étant donné qu'un audio-guide voiture est en cours
|
||||
Quand l'utilisateur consulte les contrôles
|
||||
Alors les boutons suivants sont actifs:
|
||||
| bouton | état | comportement |
|
||||
| [▶\|] Suivant | ✅ | Passe séquence suivante immédiate |
|
||||
| [\|◀] Précédent | ✅ | Retour séquence précédente |
|
||||
| [⏸️] Pause | ✅ | Pause temporaire |
|
||||
| [📋] Liste | ✅ | Saut direct possible |
|
||||
|
||||
Scénario: Use case - Embouteillage (séquence finie, point GPS loin)
|
||||
Étant donné que la séquence 3 "Enclos des girafes" est terminée
|
||||
Et que le point GPS 4 est à 2 km de distance (embouteillage)
|
||||
Quand l'utilisateur clique manuellement sur [▶|] "Suivant"
|
||||
Alors la séquence 4 démarre immédiatement
|
||||
Et l'utilisateur peut continuer l'expérience sans attendre d'atteindre le point GPS
|
||||
|
||||
Scénario: Use case - Route fermée (point GPS inaccessible)
|
||||
Étant donné que le point GPS 5 est sur une route fermée
|
||||
Et que l'utilisateur ne peut pas s'en approcher
|
||||
Quand l'utilisateur clique sur [▶|] "Suivant"
|
||||
Alors la séquence 5 démarre quand même
|
||||
Et l'audio-guide continue normalement
|
||||
|
||||
Scénario: Use case - Passager manipule l'application
|
||||
Étant donné que l'utilisateur est passager (non conducteur)
|
||||
Et que la vitesse du véhicule est 45 km/h
|
||||
Quand le passager clique sur [▶|] "Suivant"
|
||||
Alors la séquence suivante démarre
|
||||
Et un avertissement s'affiche pendant 3 secondes
|
||||
|
||||
Scénario: Avertissement sécurité si vitesse >10 km/h
|
||||
Étant donné que la vitesse actuelle est 35 km/h
|
||||
Quand l'utilisateur clique sur un bouton (Suivant ou Précédent)
|
||||
Alors l'action est exécutée immédiatement (pas de blocage)
|
||||
Et un toast s'affiche pendant 3 secondes:
|
||||
"""
|
||||
⚠️ Manipulation en conduite détectée.
|
||||
Pour votre sécurité, demandez à un passager.
|
||||
"""
|
||||
|
||||
Plan du Scénario: Avertissement selon la vitesse
|
||||
Étant donné que la vitesse actuelle est <vitesse> km/h
|
||||
Quand l'utilisateur clique sur un bouton de navigation
|
||||
Alors l'avertissement est affiché : <avertissement>
|
||||
|
||||
Exemples:
|
||||
| vitesse | avertissement |
|
||||
| 5 | Non |
|
||||
| 10 | Non |
|
||||
| 11 | Oui |
|
||||
| 35 | Oui |
|
||||
| 90 | Oui |
|
||||
|
||||
# 16.3.2 - Affichage distance et guidage
|
||||
|
||||
Scénario: Affichage entre deux séquences avec progress bar
|
||||
Étant donné que la séquence 2 "Les lions" vient de se terminer
|
||||
Et que le prochain point GPS 3 "Enclos des girafes" est à 500m
|
||||
Quand l'interface bascule en mode "attente prochain point"
|
||||
Alors l'écran affiche:
|
||||
| élément | description |
|
||||
| Statut séquence | "✅ Séquence 2/8 terminée" |
|
||||
| Nom séquence | "Les lions" |
|
||||
| Progress bar | Barre dynamique remplie selon distance (0%) |
|
||||
| Distance prochain point| "500 mètres" |
|
||||
| ETA | "≈ 1 minute 30" |
|
||||
| Direction | ↗️ |
|
||||
| Vitesse actuelle | "28 km/h" |
|
||||
| Bouton "Rejouer séq." | Permet de réécouter la séquence qui vient de finir |
|
||||
|
||||
Scénario: Progress bar dynamique vers le prochain point
|
||||
Étant donné que la distance initiale vers le prochain point était 500m
|
||||
Et que la séquence précédente est terminée
|
||||
Quand l'utilisateur se rapproche du prochain point
|
||||
Et que la distance actuelle est 175m
|
||||
Alors la progress bar affiche "65%" remplie
|
||||
Et le calcul est: 100 - (175 / 500 * 100) = 65%
|
||||
Et la barre se met à jour chaque seconde
|
||||
|
||||
Scénario: Bouton "Rejouer séq." pour réécouter
|
||||
Étant donné que la séquence 3 vient de se terminer
|
||||
Et que l'interface "attente prochain point" est affichée
|
||||
Quand l'utilisateur clique sur [▶️ Rejouer séq.]
|
||||
Alors la séquence 3 redémarre depuis 0:00
|
||||
Et l'utilisateur peut la réécouter (utile si distraction)
|
||||
|
||||
Scénario: Interface en conduite avec distance et ETA
|
||||
Étant donné que la séquence 2 est en cours
|
||||
Et que le prochain point GPS 3 "Enclos des girafes" est à 320m
|
||||
Et que la vitesse actuelle est 28 km/h
|
||||
Quand l'interface est affichée
|
||||
Alors les informations suivantes sont visibles:
|
||||
| information | valeur |
|
||||
| Nom prochain point | "Enclos des girafes" |
|
||||
| Distance | "320 mètres" |
|
||||
| ETA | "≈ 40 secondes" |
|
||||
| Direction | ↗️ (flèche direction) |
|
||||
| Vitesse actuelle | "28 km/h" |
|
||||
| Vitesse recommandée | "20-30 km/h" |
|
||||
|
||||
Scénario: Mise à jour de la distance en temps réel
|
||||
Étant donné que la distance au prochain point est 500m
|
||||
Quand 10 secondes s'écoulent et que l'utilisateur se rapproche
|
||||
Alors la distance est mise à jour chaque seconde
|
||||
Et la nouvelle distance "450m" s'affiche
|
||||
|
||||
Scénario: Mise à jour de l'ETA en temps réel
|
||||
Étant donné que l'ETA est "≈ 2 minutes"
|
||||
Et que la vitesse est constante à 30 km/h
|
||||
Quand l'utilisateur se rapproche du point
|
||||
Alors l'ETA est recalculé chaque seconde
|
||||
Et il diminue progressivement: "≈ 1 minute 50", "≈ 1 minute 40", etc.
|
||||
|
||||
Plan du Scénario: Format d'affichage de la distance
|
||||
Étant donné que la distance au prochain point est <distance_metres>
|
||||
Quand l'interface est mise à jour
|
||||
Alors la distance affichée est "<affichage>"
|
||||
|
||||
Exemples:
|
||||
| distance_metres | affichage |
|
||||
| 50 | 50 m |
|
||||
| 320 | 320 m |
|
||||
| 980 | 980 m |
|
||||
| 1200 | 1.2 km |
|
||||
| 5400 | 5.4 km |
|
||||
|
||||
Plan du Scénario: Format d'affichage de l'ETA
|
||||
Étant donné que l'ETA calculé est <secondes> secondes
|
||||
Quand l'interface est mise à jour
|
||||
Alors l'ETA affiché est "<affichage>"
|
||||
|
||||
Exemples:
|
||||
| secondes | affichage |
|
||||
| 30 | ≈ 30 secondes |
|
||||
| 75 | ≈ 1 minute |
|
||||
| 150 | ≈ 2 minutes |
|
||||
| 400 | ≈ 6 minutes |
|
||||
|
||||
Scénario: Calcul de la direction (flèche 8 directions)
|
||||
Étant donné que la position actuelle est (43.1234, 2.5678)
|
||||
Et que le prochain point est au nord-est (angle 45°)
|
||||
Quand la direction est calculée
|
||||
Alors la flèche "↗" est affichée
|
||||
|
||||
Plan du Scénario: Flèches de direction selon l'angle
|
||||
Étant donné que l'angle vers le prochain point est <angle>°
|
||||
Quand la direction est calculée
|
||||
Alors la flèche "<fleche>" est affichée
|
||||
|
||||
Exemples:
|
||||
| angle | fleche |
|
||||
| 0 | ↑ |
|
||||
| 45 | ↗ |
|
||||
| 90 | → |
|
||||
| 135 | ↘ |
|
||||
| 180 | ↓ |
|
||||
| 225 | ↙ |
|
||||
| 270 | ← |
|
||||
| 315 | ↖ |
|
||||
|
||||
Scénario: Mise à jour de la direction toutes les 5 secondes
|
||||
Étant donné que la direction actuelle est ↑ (nord)
|
||||
Et que l'utilisateur tourne vers l'est
|
||||
Quand 5 secondes s'écoulent
|
||||
Alors la direction est recalculée
|
||||
Et la nouvelle flèche ↗ (nord-est) s'affiche
|
||||
|
||||
Scénario: Message "En attente de déplacement" si vitesse <5 km/h
|
||||
Étant donné que la vitesse actuelle est 2 km/h (arrêté)
|
||||
Quand l'ETA est calculé
|
||||
Alors le message "En attente de déplacement" s'affiche
|
||||
Et l'ETA n'est pas calculé (car vitesse insuffisante)
|
||||
|
||||
Scénario: Simplicité de l'interface (pas de carte miniature)
|
||||
Étant donné qu'un audio-guide voiture est en cours
|
||||
Quand l'interface est affichée
|
||||
Alors aucune carte miniature n'est présente
|
||||
Et seuls les éléments essentiels sont affichés:
|
||||
| élément |
|
||||
| Distance |
|
||||
| ETA |
|
||||
| Direction (flèche) |
|
||||
| Vitesse |
|
||||
| Contrôles audio |
|
||||
|
||||
# 16.3.3 - Rayon de déclenchement et tolérance
|
||||
|
||||
Scénario: Rayon de déclenchement par défaut en mode voiture
|
||||
Étant donné un audio-guide voiture
|
||||
Quand un point GPS est défini
|
||||
Alors le rayon de déclenchement est 30 mètres par défaut
|
||||
Et le rayon de tolérance "point manqué" est 100 mètres
|
||||
|
||||
Scénario: Déclenchement dans le rayon (30m)
|
||||
Étant donné que le point GPS 3 est défini avec rayon 30m
|
||||
Quand l'utilisateur entre à 25m du point
|
||||
Alors la séquence 3 se déclenche automatiquement
|
||||
|
||||
Scénario: Pas de déclenchement hors rayon
|
||||
Étant donné que le point GPS 3 a un rayon de 30m
|
||||
Quand l'utilisateur passe à 45m du point
|
||||
Alors la séquence 3 ne se déclenche pas automatiquement
|
||||
|
||||
Scénario: Point manqué dans rayon de tolérance (100m)
|
||||
Étant donné que l'utilisateur passe à 60m du point GPS 4 (hors rayon 30m)
|
||||
Et que 60m < 100m (rayon tolérance)
|
||||
Quand le point est détecté comme manqué
|
||||
Alors un toast s'affiche: "⚠️ Point manqué : Enclos des éléphants"
|
||||
Et une popup s'affiche pendant 5 secondes avec 3 options
|
||||
|
||||
Scénario: Popup "Point manqué" avec 3 actions
|
||||
Étant donné qu'un point GPS a été manqué (distance 60m)
|
||||
Quand la popup s'affiche
|
||||
Alors les options suivantes sont disponibles:
|
||||
| bouton | icône | comportement |
|
||||
| Écouter quand même | 🔊 | Lance séquence immédiatement (même hors zone) |
|
||||
| Passer au suivant | ⏭️ | Skip séquence, continue vers prochain point |
|
||||
| Faire demi-tour | 🔙 | Ouvre GPS externe (Google Maps/Waze) vers point |
|
||||
|
||||
Scénario: Action "Écouter quand même"
|
||||
Étant donné qu'un point GPS est manqué
|
||||
Quand l'utilisateur clique sur "🔊 Écouter quand même"
|
||||
Alors la séquence correspondante démarre immédiatement
|
||||
Et l'utilisateur peut continuer sa route
|
||||
|
||||
Scénario: Action "Passer au suivant"
|
||||
Étant donné qu'un point GPS 5 est manqué
|
||||
Quand l'utilisateur clique sur "⏭️ Passer au suivant"
|
||||
Alors la séquence 5 est ignorée (non écoutée)
|
||||
Et l'application attend le point GPS 6
|
||||
Et la distance vers le point 6 s'affiche
|
||||
|
||||
Scénario: Action "Faire demi-tour"
|
||||
Étant donné qu'un point GPS est manqué à (43.1250, 2.5700)
|
||||
Quand l'utilisateur clique sur "🔙 Faire demi-tour"
|
||||
Alors l'application détecte l'app GPS installée (Google Maps ou Waze)
|
||||
Et ouvre la navigation GPS externe vers (43.1250, 2.5700)
|
||||
|
||||
Scénario: Point manqué au-delà du rayon de tolérance (>100m)
|
||||
Étant donné que l'utilisateur passe à 150m du point GPS 6
|
||||
Quand la distance est détectée
|
||||
Alors aucune popup ne s'affiche (point trop loin)
|
||||
Et l'utilisateur peut naviguer manuellement avec [▶|]
|
||||
|
||||
Plan du Scénario: Gestion selon la distance au point
|
||||
Étant donné un point GPS avec rayon 30m et tolérance 100m
|
||||
Quand l'utilisateur passe à <distance> du point
|
||||
Alors le comportement est <comportement>
|
||||
|
||||
Exemples:
|
||||
| distance | comportement |
|
||||
| 20m | Déclenchement automatique séquence |
|
||||
| 40m | Rien (hors rayon, pas encore tolérance) |
|
||||
| 60m | Popup "Point manqué" avec 3 options |
|
||||
| 110m | Rien (trop loin, hors tolérance) |
|
||||
|
||||
Scénario: Configuration rayon personnalisé par le créateur
|
||||
Étant donné qu'un créateur définit un rayon de 50m (au lieu de 30m)
|
||||
Quand un utilisateur entre à 45m du point
|
||||
Alors la séquence se déclenche automatiquement
|
||||
Et le rayon personnalisé est respecté
|
||||
|
||||
Scénario: Rayon minimum et maximum configurables
|
||||
Étant donné qu'un créateur configure un rayon
|
||||
Quand il ajuste le curseur
|
||||
Alors les valeurs disponibles sont de 10m à 200m
|
||||
Et le rayon par défaut suggéré est 30m pour la voiture
|
||||
|
||||
# Cas d'usage réels
|
||||
|
||||
Scénario: Safari-parc avec déclenchement automatique fluide
|
||||
Étant donné qu'un utilisateur roule dans un safari à 20 km/h
|
||||
Quand il passe devant "Enclos des lions" (point GPS 2)
|
||||
Alors la séquence 2 démarre automatiquement sans intervention
|
||||
Et il peut se concentrer sur la conduite et l'observation
|
||||
|
||||
Scénario: Détour imprévu (travaux sur la route)
|
||||
Étant donné qu'un utilisateur prend un détour à cause de travaux
|
||||
Et que le point GPS 4 devient inaccessible
|
||||
Quand il est loin du point (>100m)
|
||||
Et qu'il clique manuellement sur [▶|]
|
||||
Alors la séquence 4 démarre quand même
|
||||
Et l'expérience continue sans blocage
|
||||
|
||||
Scénario: Passager qui navigue librement
|
||||
Étant donné qu'un passager utilise l'application
|
||||
Et que le conducteur roule à 50 km/h
|
||||
Quand le passager clique sur "Précédent" pour réécouter
|
||||
Alors l'action est exécutée immédiatement
|
||||
Et un warning apparaît brièvement (sensibilisation)
|
||||
|
||||
Scénario: Embouteillage prolongé
|
||||
Étant donné que la séquence 3 est terminée depuis 10 minutes
|
||||
Et que l'utilisateur est bloqué dans un embouteillage
|
||||
Et que le point GPS 4 est encore à 1.5 km
|
||||
Quand l'utilisateur clique sur [▶|]
|
||||
Alors la séquence 4 démarre immédiatement
|
||||
Et l'utilisateur peut passer le temps en écoutant
|
||||
|
||||
# Cas d'erreur
|
||||
|
||||
Scénario: GPS désactivé en mode voiture
|
||||
Étant donné qu'un audio-guide voiture est démarré
|
||||
Et que le GPS est désactivé
|
||||
Quand l'application détecte l'absence de GPS
|
||||
Alors une alerte s'affiche:
|
||||
"""
|
||||
⚠️ GPS requis pour le mode Voiture
|
||||
Activez la localisation pour profiter du déclenchement automatique.
|
||||
[Activer GPS] [Passer en mode Manuel]
|
||||
"""
|
||||
|
||||
Scénario: Action "Passer en mode Manuel"
|
||||
Étant donné que le GPS est désactivé
|
||||
Quand l'utilisateur clique sur "Passer en mode Manuel"
|
||||
Alors l'audio-guide bascule en navigation 100% manuelle
|
||||
Et les boutons [▶|] et [|◀] permettent de naviguer
|
||||
Et aucun déclenchement GPS n'est tenté
|
||||
|
||||
Scénario: Précision GPS insuffisante
|
||||
Étant donné que le signal GPS a une précision de ±150m
|
||||
Quand l'utilisateur approche d'un point GPS avec rayon 30m
|
||||
Alors un avertissement s'affiche:
|
||||
"""
|
||||
⚠️ Signal GPS imprécis (±150m)
|
||||
Le déclenchement automatique peut être perturbé.
|
||||
Utilisez les boutons manuels si nécessaire.
|
||||
"""
|
||||
|
||||
Scénario: Perte signal GPS en cours de route
|
||||
Étant donné qu'un audio-guide voiture est en cours
|
||||
Quand le signal GPS est perdu (tunnel, parking souterrain)
|
||||
Alors un toast s'affiche: "Signal GPS perdu. Navigation manuelle active."
|
||||
Et les boutons de navigation restent actifs
|
||||
Quand le signal GPS revient
|
||||
Alors un toast s'affiche: "Signal GPS rétabli"
|
||||
Et le déclenchement automatique est réactivé
|
||||
|
||||
Scénario: Dépassement de la vitesse recommandée
|
||||
Étant donné qu'un audio-guide recommande 20-30 km/h
|
||||
Et que l'utilisateur roule à 65 km/h
|
||||
Quand la vitesse est détectée
|
||||
Alors l'affichage vitesse est en orange: "⚠️ 65 km/h"
|
||||
Et un message info s'affiche: "Vitesse élevée. Risque de manquer des points."
|
||||
274
features/ui/audio-guides/modes-velo-transport.feature
Normal file
274
features/ui/audio-guides/modes-velo-transport.feature
Normal file
@@ -0,0 +1,274 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: Audio-guides modes vélo et transport
|
||||
En tant qu'utilisateur à vélo ou en transport en commun
|
||||
Je veux profiter d'un guidage GPS adapté à mon mode de déplacement
|
||||
Afin d'avoir une expérience optimisée avec tolérances appropriées
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'application RoadWave est démarrée
|
||||
Et que l'utilisateur "jean@example.com" est connecté
|
||||
Et que le GPS est activé
|
||||
|
||||
# 16.4 - Modes Vélo et Transport
|
||||
|
||||
Plan du Scénario: Paramètres par mode de déplacement
|
||||
Étant donné un audio-guide configuré en mode <mode>
|
||||
Alors les paramètres suivants sont appliqués:
|
||||
| paramètre | valeur |
|
||||
| Rayon déclenchement | <rayon_declenchement> |
|
||||
| Rayon tolérance "point manqué" | <rayon_tolerance> |
|
||||
| Vitesse recommandée | <vitesse_recommandee> |
|
||||
| Seuil warning sécurité | <seuil_warning> |
|
||||
|
||||
Exemples:
|
||||
| mode | rayon_declenchement | rayon_tolerance | vitesse_recommandee | seuil_warning |
|
||||
| Voiture | 30m | 100m | 20-50 km/h | >10 km/h |
|
||||
| Vélo | 50m | 75m | 10-25 km/h | >5 km/h |
|
||||
| Transport | 100m | 150m | Variable | Désactivé |
|
||||
|
||||
# Mode Vélo
|
||||
|
||||
Scénario: Déclenchement automatique avec rayon 50m (mode vélo)
|
||||
Étant donné un audio-guide vélo "Circuit des châteaux de la Loire"
|
||||
Et que le point GPS 3 a un rayon de 50m
|
||||
Quand l'utilisateur à vélo entre à 45m du point
|
||||
Alors la séquence 3 "Château de Chambord" se déclenche automatiquement
|
||||
|
||||
Scénario: Rayon plus large justifié pour le vélo
|
||||
Étant donné qu'un cycliste roule sur piste cyclable
|
||||
Et que sa vitesse varie entre 8 et 22 km/h (arrêts fréquents)
|
||||
Et que le tracé est moins prévisible qu'en voiture
|
||||
Quand un point GPS avec rayon 50m est défini
|
||||
Alors le rayon plus large compense la variabilité de trajectoire
|
||||
|
||||
Scénario: Warning sécurité dès 5 km/h en vélo
|
||||
Étant donné un audio-guide vélo en cours
|
||||
Et que la vitesse actuelle est 12 km/h
|
||||
Quand l'utilisateur clique sur [▶|] "Suivant"
|
||||
Alors l'action est exécutée
|
||||
Et un warning s'affiche: "⚠️ Manipulation en déplacement détecté. Pour votre sécurité, arrêtez-vous."
|
||||
|
||||
Plan du Scénario: Warning vélo selon la vitesse
|
||||
Étant donné que la vitesse actuelle à vélo est <vitesse> km/h
|
||||
Quand l'utilisateur clique sur un bouton de navigation
|
||||
Alors le warning est affiché : <warning>
|
||||
|
||||
Exemples:
|
||||
| vitesse | warning |
|
||||
| 0 | Non |
|
||||
| 4 | Non |
|
||||
| 6 | Oui |
|
||||
| 15 | Oui |
|
||||
| 25 | Oui |
|
||||
|
||||
Scénario: Tolérance GPS moins stricte en vélo
|
||||
Étant donné qu'un cycliste passe à 65m du point GPS 4
|
||||
Et que le rayon de déclenchement est 50m
|
||||
Et que le rayon de tolérance est 75m
|
||||
Quand la distance est détectée
|
||||
Alors la popup "Point manqué" s'affiche avec 3 options
|
||||
Et le système tolère l'écart (trajectoire vélo moins prévisible)
|
||||
|
||||
Scénario: Affichage adapté au vélo
|
||||
Étant donné un audio-guide vélo en cours
|
||||
Quand l'interface est affichée
|
||||
Alors les informations suivantes sont visibles:
|
||||
| information | valeur |
|
||||
| Icône mode | 🚴 |
|
||||
| Distance prochain point| "450 m" |
|
||||
| ETA | "≈ 2 minutes" |
|
||||
| Direction | ↗️ |
|
||||
| Vitesse actuelle | "18 km/h" |
|
||||
| Vitesse recommandée | "10-25 km/h" |
|
||||
|
||||
Scénario: Cas d'usage - Piste cyclable avec arrêts fréquents
|
||||
Étant donné qu'un cycliste suit un circuit nature
|
||||
Et qu'il s'arrête régulièrement (feux, photos, fatigue)
|
||||
Quand il s'arrête à 40m d'un point GPS (rayon 50m)
|
||||
Alors la séquence se déclenche automatiquement
|
||||
Et le rayon large permet le déclenchement malgré l'arrêt
|
||||
|
||||
Scénario: Cas d'usage - Circulation mixte piétons/vélos
|
||||
Étant donné qu'un cycliste roule sur voie partagée
|
||||
Et qu'il doit ralentir fréquemment pour éviter les piétons
|
||||
Quand sa vitesse varie entre 5 et 20 km/h
|
||||
Alors le système s'adapte avec le rayon 50m
|
||||
Et le déclenchement reste fiable
|
||||
|
||||
# Mode Transport
|
||||
|
||||
Scénario: Déclenchement automatique avec rayon 100m (mode transport)
|
||||
Étant donné un audio-guide transport "Ligne touristique Paris"
|
||||
Et que le point GPS "Tour Eiffel" a un rayon de 100m
|
||||
Quand le bus touristique entre à 85m du point
|
||||
Alors la séquence "Tour Eiffel" se déclenche automatiquement
|
||||
|
||||
Scénario: Rayon très large justifié pour le transport
|
||||
Étant donné qu'un bus touristique suit une ligne fixe
|
||||
Et qu'il effectue des arrêts fréquents (stations)
|
||||
Et que l'utilisateur n'a aucun contrôle sur la trajectoire
|
||||
Quand un point GPS avec rayon 100m est défini
|
||||
Alors le rayon large compense les arrêts et la ligne fixe
|
||||
|
||||
Scénario: Pas de warning sécurité en mode transport
|
||||
Étant donné un audio-guide transport en cours
|
||||
Et que le bus roule à 50 km/h
|
||||
Quand l'utilisateur clique sur [▶|] "Suivant"
|
||||
Alors l'action est exécutée immédiatement
|
||||
Et aucun warning n'est affiché
|
||||
Parce que l'utilisateur est passager (pas conducteur)
|
||||
|
||||
Scénario: Vitesse recommandée "Selon ligne"
|
||||
Étant donné un audio-guide transport
|
||||
Quand l'interface est affichée
|
||||
Alors la vitesse recommandée indique "Selon ligne"
|
||||
Et aucune valeur fixe n'est affichée (car ligne de transport varie)
|
||||
|
||||
Scénario: Tolérance horaire pour retards
|
||||
Étant donné qu'un bus touristique est en retard de 3 minutes
|
||||
Et qu'il arrive au point GPS "Musée du Louvre" avec retard
|
||||
Quand il entre dans le rayon de 100m
|
||||
Alors la séquence se déclenche normalement
|
||||
Et le système tolère le retard (pas de pénalité temporelle)
|
||||
|
||||
Scénario: Tolérance spatiale très large (150m)
|
||||
Étant donné qu'un bus passe à 120m du point GPS "Arc de Triomphe"
|
||||
Et que le rayon de déclenchement est 100m
|
||||
Et que le rayon de tolérance est 150m
|
||||
Quand la distance est détectée
|
||||
Alors la popup "Point manqué" s'affiche avec 3 options
|
||||
|
||||
Scénario: Affichage adapté au transport
|
||||
Étant donné un audio-guide transport en cours
|
||||
Quand l'interface est affichée
|
||||
Alors les informations suivantes sont visibles:
|
||||
| information | valeur |
|
||||
| Icône mode | 🚌 |
|
||||
| Distance prochain point| "1.2 km" |
|
||||
| ETA | "≈ 3 minutes" |
|
||||
| Direction | → |
|
||||
| Vitesse actuelle | "35 km/h" |
|
||||
| Vitesse recommandée | "Selon ligne" |
|
||||
|
||||
Scénario: Cas d'usage - Bus touristique hop-on hop-off
|
||||
Étant donné un bus touristique "Paris Open Tour"
|
||||
Et qu'il suit un circuit fixe avec 15 arrêts
|
||||
Quand il approche de chaque arrêt
|
||||
Alors la séquence correspondante se déclenche automatiquement
|
||||
Et l'utilisateur n'a rien à faire (expérience passive)
|
||||
|
||||
Scénario: Cas d'usage - Train panoramique
|
||||
Étant donné un train touristique "Ligne des Alpes"
|
||||
Et qu'il roule à vitesse variable (20-80 km/h)
|
||||
Quand il passe près de points d'intérêt
|
||||
Alors les séquences se déclenchent avec rayon 100m
|
||||
Et le système compense la vitesse élevée
|
||||
|
||||
# Comportements identiques à la voiture
|
||||
|
||||
Scénario: Navigation manuelle conservée (vélo et transport)
|
||||
Étant donné un audio-guide en mode <mode>
|
||||
Quand l'utilisateur clique sur [▶|] ou [|◀]
|
||||
Alors les boutons manuels fonctionnent normalement
|
||||
Et aucune vérification GPS n'est effectuée
|
||||
|
||||
Exemples:
|
||||
| mode |
|
||||
| Vélo |
|
||||
| Transport |
|
||||
|
||||
Scénario: Affichage distance + ETA + direction (tous modes)
|
||||
Étant donné un audio-guide en mode <mode>
|
||||
Quand l'interface est affichée
|
||||
Alors les informations distance, ETA et direction sont affichées
|
||||
Et le format est identique au mode voiture
|
||||
|
||||
Exemples:
|
||||
| mode |
|
||||
| Vélo |
|
||||
| Transport |
|
||||
|
||||
Scénario: Gestion "Point manqué" identique
|
||||
Étant donné un audio-guide en mode <mode>
|
||||
Quand un point GPS est manqué (dans rayon tolérance)
|
||||
Alors la popup avec 3 options s'affiche:
|
||||
| option |
|
||||
| 🔊 Écouter quand même |
|
||||
| ⏭️ Passer au suivant |
|
||||
| 🔙 Faire demi-tour |
|
||||
|
||||
Exemples:
|
||||
| mode |
|
||||
| Vélo |
|
||||
| Transport |
|
||||
|
||||
# Publicités (identique tous modes)
|
||||
|
||||
Plan du Scénario: Insertion publicité dans tous les modes
|
||||
Étant donné un utilisateur gratuit écoute un audio-guide en mode <mode>
|
||||
Quand la séquence 5 se termine (1 pub / 5 séquences)
|
||||
Alors la publicité s'enchaîne automatiquement
|
||||
Et elle est skippable après 5 secondes
|
||||
|
||||
Exemples:
|
||||
| mode |
|
||||
| Voiture |
|
||||
| Vélo |
|
||||
| Transport |
|
||||
| Piéton |
|
||||
|
||||
# Cas d'erreur
|
||||
|
||||
Scénario: GPS imprécis en forêt (vélo)
|
||||
Étant donné un cycliste dans une forêt dense
|
||||
Et que la précision GPS est ±80m
|
||||
Quand il approche d'un point GPS avec rayon 50m
|
||||
Alors un avertissement s'affiche:
|
||||
"""
|
||||
⚠️ Signal GPS imprécis (±80m)
|
||||
Le déclenchement peut être perturbé.
|
||||
Utilisez les boutons manuels si nécessaire.
|
||||
"""
|
||||
|
||||
Scénario: Bus dévié de son itinéraire (transport)
|
||||
Étant donné un bus touristique avec déviation
|
||||
Et que plusieurs points GPS deviennent inaccessibles
|
||||
Quand l'utilisateur est informé
|
||||
Alors un message s'affiche:
|
||||
"""
|
||||
⚠️ Itinéraire modifié
|
||||
Certains points ne seront pas atteints.
|
||||
Utilisez la navigation manuelle.
|
||||
"""
|
||||
|
||||
Scénario: Changement de mode en cours de route
|
||||
Étant donné un audio-guide démarré en mode "Vélo"
|
||||
Quand l'utilisateur décide de continuer à pied
|
||||
Et qu'il ouvre les paramètres
|
||||
Alors il peut changer le mode vers "Piéton"
|
||||
Et les rayons sont reconfigurés automatiquement
|
||||
Et une confirmation s'affiche:
|
||||
"""
|
||||
Mode changé : 🚶 Piéton
|
||||
Navigation manuelle activée.
|
||||
"""
|
||||
|
||||
Scénario: Détection automatique incohérente
|
||||
Étant donné qu'un utilisateur marche rapidement (7 km/h)
|
||||
Et que le système détecte "Vélo" par erreur
|
||||
Quand la suggestion s'affiche
|
||||
Alors l'utilisateur peut cliquer sur "Changer"
|
||||
Et sélectionner manuellement "Piéton"
|
||||
|
||||
Scénario: Batterie en mode vélo longue distance
|
||||
Étant donné un circuit vélo de 50 km avec 20 séquences
|
||||
Et que l'utilisateur roule pendant 3 heures
|
||||
Quand la batterie atteint 15%
|
||||
Alors une notification suggère:
|
||||
"""
|
||||
🔋 Batterie à 15%
|
||||
Recommandé : activer mode économie d'énergie
|
||||
(Désactive affichage continu distance)
|
||||
[Activer] [Ignorer]
|
||||
"""
|
||||
307
features/ui/audio-guides/premium-monetisation.feature
Normal file
307
features/ui/audio-guides/premium-monetisation.feature
Normal file
@@ -0,0 +1,307 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: Audio-guides Premium et monétisation
|
||||
En tant que créateur
|
||||
Je veux pouvoir proposer des audio-guides Premium
|
||||
Afin de monétiser mon contenu de qualité
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'application RoadWave est démarrée
|
||||
Et que le créateur "guide@example.com" est connecté et vérifié
|
||||
|
||||
# 16.9 - Audio-guides Premium
|
||||
|
||||
Scénario: Création d'un audio-guide Premium
|
||||
Étant donné que le créateur crée un audio-guide "Visite VIP Versailles"
|
||||
Quand il accède aux paramètres de monétisation (étape 4)
|
||||
Alors il peut choisir:
|
||||
| option | description |
|
||||
| Gratuit | Accessible à tous (avec pubs) |
|
||||
| Premium | Réservé abonnés Premium |
|
||||
|
||||
Scénario: Badge Premium visible sur l'audio-guide
|
||||
Étant donné un audio-guide configuré en Premium
|
||||
Quand il est affiché dans les résultats de recherche
|
||||
Alors un badge "👑 Premium" est visible
|
||||
Et la cover image a un cadre doré subtil
|
||||
|
||||
Scénario: Preview 3 premières séquences pour utilisateurs gratuits
|
||||
Étant donné un audio-guide Premium "Visite VIP Versailles" avec 15 séquences
|
||||
Et qu'un utilisateur gratuit ouvre l'audio-guide
|
||||
Quand il consulte la liste des séquences
|
||||
Alors les séquences affichent:
|
||||
| séquence | état |
|
||||
| 1 | ✅ Accessible (preview) |
|
||||
| 2 | ✅ Accessible (preview) |
|
||||
| 3 | ✅ Accessible (preview) |
|
||||
| 4 | 🔒 Réservé Premium |
|
||||
| 5-15 | 🔒 Réservé Premium |
|
||||
|
||||
Scénario: Écoute des 3 premières séquences sans blocage
|
||||
Étant donné un utilisateur gratuit
|
||||
Et un audio-guide Premium avec preview
|
||||
Quand il écoute les séquences 1, 2 et 3
|
||||
Alors aucune publicité n'est insérée (preview = teasing)
|
||||
Et l'écoute est fluide
|
||||
|
||||
Scénario: Paywall après la 3ème séquence
|
||||
Étant donné qu'un utilisateur gratuit termine la séquence 3
|
||||
Quand la séquence se termine
|
||||
Alors un overlay paywall s'affiche immédiatement:
|
||||
"""
|
||||
👑 Contenu réservé Premium
|
||||
|
||||
Continuez cette expérience exclusive
|
||||
et accédez à 12 séquences supplémentaires
|
||||
|
||||
✓ Sans publicité
|
||||
✓ Accès illimité à tous les contenus Premium
|
||||
✓ Téléchargement offline
|
||||
✓ Audio haute qualité
|
||||
|
||||
[Passer Premium - 4.99€/mois]
|
||||
[Découvrir d'autres audio-guides gratuits]
|
||||
"""
|
||||
|
||||
Scénario: Bouton "Passer Premium" vers tunnel d'abonnement
|
||||
Étant donné que l'overlay paywall Premium est affiché
|
||||
Quand l'utilisateur clique sur "Passer Premium"
|
||||
Alors il est redirigé vers la page d'abonnement Mangopay
|
||||
Et l'audio-guide actuel est marqué en "pending" (reprise après souscription)
|
||||
|
||||
Scénario: Reprise automatique après souscription Premium
|
||||
Étant donné qu'un utilisateur s'est abonné Premium depuis un paywall audio-guide
|
||||
Quand l'abonnement est activé
|
||||
Alors il est redirigé vers l'audio-guide automatiquement
|
||||
Et la séquence 4 démarre immédiatement
|
||||
Et un toast de bienvenue s'affiche: "✨ Bienvenue Premium ! Profitez de votre audio-guide"
|
||||
|
||||
Scénario: Utilisateur Premium - Accès complet immédiat
|
||||
Étant donné qu'un utilisateur Premium ouvre un audio-guide Premium
|
||||
Quand il consulte la liste des séquences
|
||||
Alors toutes les 15 séquences sont accessibles
|
||||
Et aucun paywall ne s'affiche
|
||||
Et aucune publicité n'est insérée
|
||||
|
||||
Scénario: Pas de preview si l'audio-guide a <3 séquences
|
||||
Étant donné un audio-guide Premium avec seulement 2 séquences
|
||||
Quand un utilisateur gratuit tente de l'ouvrir
|
||||
Alors un paywall s'affiche immédiatement (avant lecture)
|
||||
Et aucune preview n'est disponible
|
||||
|
||||
# 16.10 - Revenus créateur
|
||||
|
||||
Scénario: Rémunération créateur pour audio-guide Premium
|
||||
Étant donné un créateur avec un audio-guide Premium
|
||||
Et que 50 utilisateurs Premium ont écouté l'audio-guide ce mois
|
||||
Quand la répartition des revenus est calculée
|
||||
Alors le créateur reçoit 70% des revenus proportionnels
|
||||
Et la formule est: (Écoutes créateur / Total écoutes Premium) × 70% pool Premium
|
||||
|
||||
Scénario: Dashboard revenus par audio-guide
|
||||
Étant donné qu'un créateur a 3 audio-guides Premium publiés
|
||||
Quand il consulte son dashboard revenus
|
||||
Alors il voit pour chaque audio-guide:
|
||||
| audio_guide | ecoutes_mois | revenus_estime |
|
||||
| Visite VIP Versailles | 142 | 45.20 € |
|
||||
| Secrets du Louvre | 89 | 28.50 € |
|
||||
| Châteaux de la Loire | 203 | 64.80 € |
|
||||
|
||||
Scénario: Comparaison gratuit vs Premium
|
||||
Étant donné qu'un créateur a publié 2 audio-guides:
|
||||
| titre | type | ecoutes_mois | revenus |
|
||||
| Tour de Paris | Gratuit | 1200 | 12.50 € |
|
||||
| Visite VIP Versailles| Premium | 142 | 45.20 € |
|
||||
Quand il consulte son dashboard
|
||||
Alors il peut comparer les performances
|
||||
Et constater que Premium génère plus de revenus par écoute
|
||||
|
||||
Scénario: Seuil minimum de paiement (20€)
|
||||
Étant donné qu'un créateur a généré 18€ de revenus ce mois
|
||||
Quand le paiement mensuel est traité
|
||||
Alors le montant est reporté au mois suivant
|
||||
Et un message s'affiche: "Seuil minimum non atteint (20€). Montant reporté."
|
||||
|
||||
Scénario: Paiement automatique mensuel
|
||||
Étant donné qu'un créateur a généré 138.50€ de revenus en janvier
|
||||
Quand le 5 février arrive
|
||||
Alors le paiement est initié automatiquement via Mangopay
|
||||
Et le créateur reçoit une notification: "Paiement de 138.50€ en cours"
|
||||
Et les fonds arrivent sous 2-3 jours ouvrés
|
||||
|
||||
# 16.11 - Publicités dans audio-guides gratuits
|
||||
|
||||
Scénario: Insertion publicité toutes les 5 séquences (gratuit)
|
||||
Étant donné un audio-guide gratuit avec 12 séquences
|
||||
Et un utilisateur gratuit
|
||||
Quand il termine la séquence 5
|
||||
Alors une publicité démarre automatiquement
|
||||
Quand il termine la séquence 10
|
||||
Alors une deuxième publicité démarre
|
||||
|
||||
Scénario: Publicité après séquence en mode piéton (avec pause)
|
||||
Étant donné un audio-guide piéton gratuit
|
||||
Quand la séquence 5 se termine
|
||||
Alors la publicité démarre automatiquement (pas d'attente bouton)
|
||||
Et la pub est skippable après 5 secondes
|
||||
Quand la publicité se termine
|
||||
Alors le player se met en pause
|
||||
Et l'utilisateur doit cliquer sur [▶|] pour continuer
|
||||
|
||||
Scénario: Publicité en mode voiture/vélo/transport (automatique)
|
||||
Étant donné un audio-guide voiture gratuit
|
||||
Quand la séquence 5 se termine
|
||||
Alors la publicité démarre automatiquement
|
||||
Quand la publicité se termine
|
||||
Alors la séquence 6 démarre automatiquement (pas de pause)
|
||||
Parce que l'utilisateur est en conduite (mode hands-free)
|
||||
|
||||
Scénario: Publicités géolocalisées dans audio-guides
|
||||
Étant donné un audio-guide dans la région "Île-de-France"
|
||||
Quand une publicité doit être insérée
|
||||
Alors l'API publicitaire filtre par:
|
||||
| critère | valeur |
|
||||
| Géolocalisation | Île-de-France |
|
||||
| Catégorie | Tourisme, Culture |
|
||||
| Langue | Français |
|
||||
|
||||
Scénario: Comptabilisation des impressions pub pour créateur
|
||||
Étant donné qu'un audio-guide gratuit génère 200 écoutes complètes
|
||||
Et que chaque écoute complète = 2 publicités (séq. 5 et 10)
|
||||
Quand les revenus pub sont calculés
|
||||
Alors 400 impressions sont comptabilisées
|
||||
Et le créateur reçoit 0.80€ (400 × 0.002€)
|
||||
|
||||
# 16.12 - Stratégies de conversion
|
||||
|
||||
Scénario: CTA Premium après audio-guide gratuit complété
|
||||
Étant donné qu'un utilisateur gratuit complète un audio-guide gratuit
|
||||
Quand il termine la dernière séquence
|
||||
Alors un overlay s'affiche:
|
||||
"""
|
||||
🎉 Audio-guide complété !
|
||||
|
||||
Vous avez aimé cette expérience ?
|
||||
Découvrez nos audio-guides Premium pour aller plus loin
|
||||
|
||||
[Découvrir Premium] [Fermer]
|
||||
"""
|
||||
|
||||
Scénario: Recommandations d'audio-guides Premium après gratuit
|
||||
Étant donné qu'un utilisateur termine un audio-guide gratuit "Tour de Paris"
|
||||
Quand l'overlay de fin s'affiche
|
||||
Alors 3 audio-guides Premium similaires sont suggérés:
|
||||
| titre | type | créateur |
|
||||
| Secrets de Montmartre | Premium | @paris_stories |
|
||||
| Visite VIP Musée d'Orsay | Premium | @art_guide |
|
||||
| Paris hors des sentiers | Premium | @explore_paris |
|
||||
|
||||
Scénario: Badge "Premium recommandé" sur audio-guides populaires
|
||||
Étant donné un audio-guide Premium avec >500 écoutes et note >4.5/5
|
||||
Quand il est affiché dans les résultats de recherche
|
||||
Alors un badge "⭐ Premium recommandé" est visible
|
||||
Et il est mis en avant dans les résultats
|
||||
|
||||
Scénario: Conversion tracking pour attribution créateur
|
||||
Étant donné qu'un utilisateur découvre Premium via un audio-guide créateur
|
||||
Quand il s'abonne
|
||||
Alors la conversion est trackée:
|
||||
| donnée | valeur |
|
||||
| source_conversion | audio_guide_paywall |
|
||||
| audio_guide_id | visite_vip_versailles_123 |
|
||||
| creator_id | guide_versailles_456 |
|
||||
Et le créateur bénéficie d'un bonus de conversion
|
||||
|
||||
# 16.13 - Offres spéciales
|
||||
|
||||
Scénario: Essai gratuit 7 jours Premium via audio-guide
|
||||
Étant donné qu'un utilisateur gratuit atteint le paywall d'un audio-guide Premium
|
||||
Et qu'il n'a jamais essayé Premium
|
||||
Quand l'overlay s'affiche
|
||||
Alors une offre d'essai est proposée:
|
||||
"""
|
||||
👑 Essayez Premium gratuitement pendant 7 jours
|
||||
|
||||
✓ Accès complet à cet audio-guide
|
||||
✓ Tous les contenus Premium débloqués
|
||||
✓ Sans engagement, annulable à tout moment
|
||||
|
||||
[Démarrer l'essai gratuit] [Plus tard]
|
||||
"""
|
||||
|
||||
Scénario: Activation immédiate après essai gratuit
|
||||
Étant donné qu'un utilisateur démarre un essai gratuit 7 jours
|
||||
Quand l'essai est activé
|
||||
Alors l'audio-guide Premium démarre immédiatement
|
||||
Et toutes les séquences sont débloquées
|
||||
Et aucune publicité n'est insérée
|
||||
|
||||
Scénario: Rappel 2 jours avant fin d'essai
|
||||
Étant donné qu'un utilisateur a démarré un essai gratuit le 15/01
|
||||
Quand le 20/01 arrive (J-2)
|
||||
Alors une notification est envoyée:
|
||||
"""
|
||||
⏰ Votre essai Premium se termine dans 2 jours
|
||||
|
||||
Continuez à profiter de tous les audio-guides Premium
|
||||
pour seulement 4.99€/mois
|
||||
|
||||
[Rester Premium] [Gérer abonnement]
|
||||
"""
|
||||
|
||||
# Cas d'usage
|
||||
|
||||
Scénario: Créateur mix gratuit + Premium
|
||||
Étant donné qu'un créateur a publié 5 audio-guides:
|
||||
| titre | type |
|
||||
| Découverte de Paris | Gratuit |
|
||||
| Visite VIP Louvre | Premium |
|
||||
| Balade Montmartre | Gratuit |
|
||||
| Secrets Versailles | Premium |
|
||||
| Visite express Orsay | Gratuit |
|
||||
Quand un utilisateur découvre son profil
|
||||
Alors les audio-guides gratuits servent de teasing
|
||||
Et les audio-guides Premium sont mis en avant avec badge
|
||||
|
||||
Scénario: Utilisateur hésite à s'abonner
|
||||
Étant donné qu'un utilisateur atteint le paywall d'un audio-guide Premium
|
||||
Et qu'il clique sur "Découvrir d'autres audio-guides gratuits"
|
||||
Quand il revient 2 jours plus tard sur le même audio-guide
|
||||
Alors le paywall s'affiche à nouveau
|
||||
Et une réduction temporaire est proposée: "Offre spéciale : -20% premier mois"
|
||||
|
||||
# Cas d'erreur
|
||||
|
||||
Scénario: Échec du paiement Premium via paywall
|
||||
Étant donné qu'un utilisateur tente de s'abonner Premium
|
||||
Quand le paiement Mangopay échoue
|
||||
Alors un message d'erreur s'affiche:
|
||||
"""
|
||||
❌ Paiement refusé
|
||||
Vérifiez vos informations bancaires ou contactez votre banque.
|
||||
[Réessayer] [Annuler]
|
||||
"""
|
||||
|
||||
Scénario: Abonnement Premium expiré pendant écoute
|
||||
Étant donné qu'un utilisateur Premium écoute un audio-guide Premium
|
||||
Et que son abonnement expire pendant l'écoute (séquence 8/15)
|
||||
Quand l'expiration est détectée
|
||||
Alors l'écoute continue jusqu'à la fin de la séquence en cours
|
||||
Et un overlay s'affiche ensuite:
|
||||
"""
|
||||
⚠️ Votre abonnement Premium a expiré
|
||||
|
||||
Renouvelez pour continuer à profiter des contenus exclusifs
|
||||
[Renouveler - 4.99€/mois] [Plus tard]
|
||||
"""
|
||||
|
||||
Scénario: Créateur change audio-guide de gratuit à Premium
|
||||
Étant donné qu'un audio-guide gratuit a 50 utilisateurs avec progression
|
||||
Quand le créateur le passe en Premium
|
||||
Alors les utilisateurs ayant déjà commencé gardent l'accès complet
|
||||
Et seuls les nouveaux utilisateurs sont soumis au paywall
|
||||
Et un message de transparence s'affiche:
|
||||
"""
|
||||
ℹ️ Cet audio-guide est maintenant Premium
|
||||
Vous conservez votre accès car vous l'aviez démarré avant le changement.
|
||||
"""
|
||||
310
features/ui/audio-guides/progression-sauvegarde.feature
Normal file
310
features/ui/audio-guides/progression-sauvegarde.feature
Normal file
@@ -0,0 +1,310 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: Sauvegarde et reprise de progression audio-guide
|
||||
En tant qu'utilisateur
|
||||
Je veux que ma progression soit sauvegardée automatiquement
|
||||
Afin de pouvoir reprendre mon audio-guide là où je me suis arrêté
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'application RoadWave est démarrée
|
||||
Et que l'utilisateur "jean@example.com" est connecté
|
||||
|
||||
# 16.5 - Sauvegarde de progression
|
||||
|
||||
Scénario: Sauvegarde automatique toutes les 10 secondes
|
||||
Étant donné qu'un audio-guide "Visite du Louvre" est en cours
|
||||
Et que la séquence 3 est à la position 1:24
|
||||
Quand 10 secondes s'écoulent
|
||||
Alors la progression est sauvegardée automatiquement:
|
||||
| donnée | valeur |
|
||||
| audio_guide_id | louvre_123 |
|
||||
| sequence_actuelle | 3 |
|
||||
| position_audio | 1:24 |
|
||||
| timestamp | 2026-01-22 14:35:42 |
|
||||
| sequences_ecoutees | [1, 2] |
|
||||
|
||||
Scénario: Sauvegarde locale (SQLite) pour rapidité
|
||||
Étant donné qu'une sauvegarde est déclenchée
|
||||
Quand la progression est enregistrée
|
||||
Alors les données sont écrites en SQLite local
|
||||
Et l'écriture prend moins de 50ms
|
||||
Et l'application reste fluide
|
||||
|
||||
Scénario: Synchronisation cloud en arrière-plan
|
||||
Étant donné qu'une sauvegarde locale est effectuée
|
||||
Quand 30 secondes s'écoulent
|
||||
Alors la progression est synchronisée vers PostgreSQL cloud
|
||||
Et la synchronisation s'effectue en arrière-plan
|
||||
Et elle n'impacte pas les performances
|
||||
|
||||
Scénario: Sauvegarde immédiate lors de la fermeture
|
||||
Étant donné qu'un audio-guide est en cours à la séquence 4 position 2:15
|
||||
Quand l'utilisateur ferme l'application
|
||||
Alors la progression est sauvegardée immédiatement (local + cloud)
|
||||
Et les données sont écrites avant la fermeture complète
|
||||
|
||||
Scénario: Sauvegarde des séquences complétées
|
||||
Étant donné qu'un audio-guide de 12 séquences est en cours
|
||||
Et que les séquences 1, 2, 4, 5 ont été écoutées à >80%
|
||||
Quand la progression est sauvegardée
|
||||
Alors les séquences complétées sont enregistrées:
|
||||
"""json
|
||||
{
|
||||
"audio_guide_id": "louvre_123",
|
||||
"completed_sequences": [1, 2, 4, 5],
|
||||
"current_sequence": 6,
|
||||
"current_position": "0:42",
|
||||
"last_played_at": "2026-01-22T14:35:42Z"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Historique des écoutes pour statistiques
|
||||
Étant donné qu'un utilisateur a écouté 3 séquences d'un audio-guide
|
||||
Quand les données sont sauvegardées
|
||||
Alors l'historique d'écoute inclut:
|
||||
| sequence_id | started_at | completed_at | completion_rate |
|
||||
| 1 | 2026-01-22 14:10:00 | 2026-01-22 14:12:15 | 100% |
|
||||
| 2 | 2026-01-22 14:12:20 | 2026-01-22 14:14:08 | 100% |
|
||||
| 3 | 2026-01-22 14:14:15 | 2026-01-22 14:17:45 | 92% |
|
||||
|
||||
# 16.6 - Reprise de progression
|
||||
|
||||
Scénario: Popup de reprise au redémarrage
|
||||
Étant donné que l'utilisateur a quitté l'app à la séquence 6 position 2:34
|
||||
Quand il rouvre l'audio-guide "Visite du Louvre"
|
||||
Alors une popup s'affiche:
|
||||
"""
|
||||
Continuer où vous vous étiez arrêté ?
|
||||
|
||||
📍 Séquence 6/12 : "Vénus de Milo"
|
||||
⏱️ Position : 2:34 / 4:10
|
||||
|
||||
[▶️ Reprendre] [🔄 Recommencer]
|
||||
"""
|
||||
|
||||
Scénario: Action "Reprendre" - Position exacte restaurée
|
||||
Étant donné qu'une popup de reprise est affichée
|
||||
Quand l'utilisateur clique sur "▶️ Reprendre"
|
||||
Alors la séquence 6 "Vénus de Milo" se charge
|
||||
Et la position exacte 2:34 est restaurée
|
||||
Et la lecture démarre automatiquement après 1 seconde
|
||||
|
||||
Scénario: Action "Recommencer" - Réinitialisation complète
|
||||
Étant donné qu'une popup de reprise est affichée
|
||||
Quand l'utilisateur clique sur "🔄 Recommencer"
|
||||
Alors l'audio-guide redémarre depuis la séquence 1 position 0:00
|
||||
Et toutes les séquences sont marquées ⭕ "À écouter"
|
||||
Et l'historique d'écoute est réinitialisé pour cette session
|
||||
|
||||
Scénario: Reprise après 7 jours d'inactivité
|
||||
Étant donné qu'un utilisateur a arrêté un audio-guide le 15/01/2026
|
||||
Et qu'il le rouvre le 22/01/2026 (7 jours plus tard)
|
||||
Quand l'audio-guide se charge
|
||||
Alors la popup de reprise s'affiche normalement
|
||||
Et toutes les données de progression sont conservées
|
||||
|
||||
Scénario: Reprise sur un autre appareil (synchronisation cloud)
|
||||
Étant donné qu'un utilisateur écoute un audio-guide sur iPhone
|
||||
Et qu'il quitte à la séquence 4 position 1:20
|
||||
Quand il ouvre le même audio-guide sur iPad
|
||||
Alors la popup de reprise s'affiche avec la progression iPhone
|
||||
Et il peut reprendre exactement où il s'était arrêté
|
||||
|
||||
Scénario: Conflit de synchronisation (dernier appareil gagne)
|
||||
Étant donné qu'un utilisateur écoute sur iPhone à la séquence 3
|
||||
Et simultanément sur iPad à la séquence 7
|
||||
Quand les deux appareils synchronisent
|
||||
Alors la progression la plus récente (timestamp) est conservée
|
||||
Et l'appareil avec ancienne progression affiche une notification:
|
||||
"""
|
||||
ℹ️ Progression mise à jour
|
||||
Une écoute plus récente a été détectée.
|
||||
Séquence 7 restaurée.
|
||||
"""
|
||||
|
||||
Scénario: Mode hors-ligne - Sauvegarde locale uniquement
|
||||
Étant donné qu'un utilisateur écoute un audio-guide hors connexion
|
||||
Et qu'il atteint la séquence 5
|
||||
Quand la progression est sauvegardée
|
||||
Alors les données sont écrites localement (SQLite)
|
||||
Et une icône "☁️ Non synchronisé" s'affiche discrètement
|
||||
|
||||
Scénario: Synchronisation automatique à la reconnexion
|
||||
Étant donné que l'utilisateur a écouté hors ligne jusqu'à la séquence 8
|
||||
Et que 5 progressions locales ne sont pas synchronisées
|
||||
Quand la connexion réseau est rétablie
|
||||
Alors les 5 progressions sont synchronisées automatiquement
|
||||
Et un toast s'affiche brièvement: "✅ Progression synchronisée"
|
||||
|
||||
Scénario: Suppression de la progression (recommencer proprement)
|
||||
Étant donné qu'un utilisateur est à la séquence 10/12
|
||||
Quand il ouvre les paramètres de l'audio-guide
|
||||
Et qu'il clique sur "🔄 Réinitialiser progression"
|
||||
Alors une confirmation s'affiche:
|
||||
"""
|
||||
Réinitialiser cet audio-guide ?
|
||||
Toutes les séquences seront marquées comme non écoutées.
|
||||
[Annuler] [Réinitialiser]
|
||||
"""
|
||||
Et si confirmé, la progression est effacée
|
||||
|
||||
# 16.7 - Statistiques d'écoute
|
||||
|
||||
Scénario: Taux de complétion global de l'audio-guide
|
||||
Étant donné un audio-guide de 12 séquences
|
||||
Et que l'utilisateur a écouté complètement 8 séquences
|
||||
Et partiellement 1 séquence (45%)
|
||||
Quand les statistiques sont calculées
|
||||
Alors le taux de complétion affiché est "67%" (8/12)
|
||||
|
||||
Scénario: Badge "Audio-guide complété" à 100%
|
||||
Étant donné un audio-guide de 12 séquences
|
||||
Quand l'utilisateur écoute la 12ème séquence à 100%
|
||||
Alors un badge "✅ Audio-guide complété" s'affiche
|
||||
Et une notification de félicitations est envoyée
|
||||
Et le statut "Complété le 22/01/2026" est visible dans l'historique
|
||||
|
||||
Scénario: Temps total passé sur l'audio-guide
|
||||
Étant donné qu'un utilisateur a écouté un audio-guide sur 2 sessions:
|
||||
| session | durée |
|
||||
| 1 | 25 min |
|
||||
| 2 | 18 min |
|
||||
Quand les statistiques sont calculées
|
||||
Alors le temps total est "43 minutes"
|
||||
Et il est affiché dans l'historique personnel
|
||||
|
||||
Scénario: Liste des audio-guides "En cours" dans le profil
|
||||
Étant donné qu'un utilisateur a 3 audio-guides en cours:
|
||||
| audio_guide | progression |
|
||||
| Visite du Louvre | 6/12 |
|
||||
| Safari du Paugre | 3/8 |
|
||||
| Circuit Loire à Vélo | 12/15 |
|
||||
Quand il consulte son profil "Audio-guides"
|
||||
Alors la section "📍 En cours" affiche les 3 audio-guides
|
||||
Et chaque élément montre la progression sous forme de barre
|
||||
|
||||
Scénario: Liste des audio-guides "Complétés" dans le profil
|
||||
Étant donné qu'un utilisateur a complété 2 audio-guides:
|
||||
| audio_guide | date_completion |
|
||||
| Tour de Paris | 2026-01-15 |
|
||||
| Découverte de Lyon | 2026-01-20 |
|
||||
Quand il consulte son profil "Audio-guides"
|
||||
Alors la section "✅ Complétés" affiche les 2 audio-guides
|
||||
Et la date de complétion est visible
|
||||
|
||||
Scénario: Badge "Complétiste" pour 10 audio-guides complétés
|
||||
Étant donné qu'un utilisateur complète son 10ème audio-guide
|
||||
Quand la complétion est enregistrée
|
||||
Alors un badge "🏆 Complétiste" est débloqué
|
||||
Et il apparaît sur son profil
|
||||
Et une notification est envoyée:
|
||||
"""
|
||||
🎉 Badge débloqué : Complétiste
|
||||
Vous avez complété 10 audio-guides !
|
||||
"""
|
||||
|
||||
Plan du Scénario: Niveaux de badges selon nombre d'audio-guides complétés
|
||||
Étant donné qu'un utilisateur complète <nombre> audio-guides
|
||||
Quand le badge est attribué
|
||||
Alors il reçoit le badge "<badge>"
|
||||
|
||||
Exemples:
|
||||
| nombre | badge |
|
||||
| 1 | 🎧 Premier audio-guide |
|
||||
| 5 | 🗺️ Explorateur |
|
||||
| 10 | 🏆 Complétiste |
|
||||
| 25 | 🌟 Expert |
|
||||
| 50 | 💎 Maître audio-guideur |
|
||||
|
||||
# 16.8 - Métriques créateur
|
||||
|
||||
Scénario: Dashboard créateur - Statistiques par audio-guide
|
||||
Étant donné qu'un créateur a publié l'audio-guide "Visite du Louvre"
|
||||
Quand il consulte son dashboard
|
||||
Alors les métriques suivantes sont affichées:
|
||||
| métrique | valeur |
|
||||
| Écoutes totales | 1542 |
|
||||
| Écoutes complètes (>80%) | 892 |
|
||||
| Taux de complétion moyen | 58% |
|
||||
| Temps d'écoute total | 423h |
|
||||
| Séquence la plus écoutée | Séq. 3 |
|
||||
| Séquence la moins écoutée | Séq. 11 |
|
||||
|
||||
Scénario: Graphique de complétion par séquence
|
||||
Étant donné un audio-guide de 12 séquences
|
||||
Quand le créateur consulte les statistiques détaillées
|
||||
Alors un graphique en barres affiche:
|
||||
| séquence | taux_completion |
|
||||
| 1 | 100% |
|
||||
| 2 | 95% |
|
||||
| 3 | 89% |
|
||||
| ... | ... |
|
||||
| 12 | 58% |
|
||||
|
||||
Scénario: Détection des points d'abandon
|
||||
Étant donné qu'un audio-guide a un taux de complétion de 58%
|
||||
Et que 35% des utilisateurs abandonnent à la séquence 7
|
||||
Quand le créateur consulte les insights
|
||||
Alors un avertissement s'affiche:
|
||||
"""
|
||||
⚠️ Point d'abandon détecté
|
||||
35% des utilisateurs abandonnent à la séquence 7 "Aile Richelieu"
|
||||
Durée : 8 min
|
||||
Suggestion : Réduire la durée ou rendre plus captivant
|
||||
"""
|
||||
|
||||
Scénario: Heatmap géographique des écoutes
|
||||
Étant donné un audio-guide géolocalisé
|
||||
Quand le créateur consulte la heatmap
|
||||
Alors une carte affiche:
|
||||
| élément | description |
|
||||
| Densité d'écoutes | Zones rouge/orange/jaune selon écoutes |
|
||||
| Points GPS | Marqueurs sur chaque point |
|
||||
| Statistiques par point | Nombre d'écoutes par zone |
|
||||
|
||||
Scénario: Temps moyen par séquence
|
||||
Étant donné qu'un créateur analyse son audio-guide
|
||||
Quand il consulte les statistiques temporelles
|
||||
Alors il voit pour chaque séquence:
|
||||
| séquence | durée_audio | temps_ecoute_moyen | ecart |
|
||||
| 1 | 2:15 | 2:10 | -5s |
|
||||
| 2 | 1:48 | 1:30 | -18s |
|
||||
| 3 | 3:42 | 3:40 | -2s |
|
||||
|
||||
Scénario: Notification créateur pour milestone
|
||||
Étant donné qu'un audio-guide atteint 1000 écoutes
|
||||
Quand le seuil est franchi
|
||||
Alors une notification est envoyée au créateur:
|
||||
"""
|
||||
🎉 Félicitations !
|
||||
Votre audio-guide "Visite du Louvre" a atteint 1000 écoutes !
|
||||
Taux de complétion : 58%
|
||||
"""
|
||||
|
||||
# Cas d'erreur et limites
|
||||
|
||||
Scénario: Corruption de données de sauvegarde
|
||||
Étant donné qu'une sauvegarde locale (SQLite) est corrompue
|
||||
Quand l'application tente de charger la progression
|
||||
Alors une récupération depuis le cloud est tentée
|
||||
Et si réussie, les données cloud sont restaurées
|
||||
Et la base locale est reconstruite
|
||||
|
||||
Scénario: Échec de synchronisation cloud
|
||||
Étant donné que l'API cloud est indisponible
|
||||
Quand une tentative de synchronisation est effectuée
|
||||
Alors l'application continue avec sauvegarde locale uniquement
|
||||
Et un retry automatique est programmé dans 5 minutes
|
||||
Et l'icône "☁️ Non synchronisé" reste affichée
|
||||
|
||||
Scénario: Suppression accidentelle de progression (récupération)
|
||||
Étant donné qu'un utilisateur réinitialise un audio-guide par erreur
|
||||
Quand il contacte le support dans les 7 jours
|
||||
Alors l'équipe peut restaurer la progression depuis les backups
|
||||
Et les données sont récupérables (backup quotidien conservé 30 jours)
|
||||
|
||||
Scénario: Nettoyage automatique des vieilles progressions
|
||||
Étant donné qu'une progression n'a pas été mise à jour depuis 6 mois
|
||||
Quand le nettoyage automatique s'exécute
|
||||
Alors la progression est archivée (mais pas supprimée)
|
||||
Et l'utilisateur peut la restaurer via l'historique
|
||||
142
features/ui/interest-gauges/degradation-temporelle.feature
Normal file
142
features/ui/interest-gauges/degradation-temporelle.feature
Normal file
@@ -0,0 +1,142 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Pas de dégradation temporelle des jauges
|
||||
En tant que système de recommandation
|
||||
Je veux que les jauges n'évoluent que par les actions utilisateur
|
||||
Afin d'avoir un comportement prévisible et fiable
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et qu'un utilisateur est connecté
|
||||
|
||||
Scénario: Aucune dégradation automatique avec le temps
|
||||
Étant donné que ma jauge "Économie" est à 80%
|
||||
Et que je n'écoute aucun contenu pendant 30 jours
|
||||
Quand je me reconnecte après 30 jours
|
||||
Alors ma jauge "Économie" est toujours à 80%
|
||||
Et aucune dégradation temporelle n'a été appliquée
|
||||
|
||||
Scénario: Jauges conservées après 6 mois d'inactivité
|
||||
Étant donné que mes jauges sont:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 75% |
|
||||
| Voyage | 60% |
|
||||
| Musique | 45% |
|
||||
Et que je pars en vacances pendant 6 mois sans utiliser l'app
|
||||
Quand je me reconnecte après 6 mois
|
||||
Alors mes jauges sont exactement les mêmes:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 75% |
|
||||
| Voyage | 60% |
|
||||
| Musique | 45% |
|
||||
|
||||
Scénario: Évolution naturelle par les actions
|
||||
Étant donné que j'aimais "Économie" il y a 1 an (jauge 80%)
|
||||
Et que depuis, je skip tous les contenus "Économie"
|
||||
Et que j'ai skippé 50 contenus "Économie" en 1 an
|
||||
Alors ma jauge "Économie" descend naturellement via les skips
|
||||
Et atteint environ 55% (80% - 50 × 0.5% = 55%)
|
||||
Et la dégradation vient des actions, pas du temps
|
||||
|
||||
Scénario: Pas de cron job de dégradation
|
||||
Étant donné que le système vérifie les jauges quotidiennement
|
||||
Quand un utilisateur n'a pas d'activité depuis 90 jours
|
||||
Alors aucun job de dégradation n'est exécuté
|
||||
Et les jauges restent inchangées
|
||||
Et aucune ressource CPU n'est consommée pour la dégradation
|
||||
|
||||
Scénario: Comportement prévisible après absence
|
||||
Étant donné que ma jauge "Sport" était à 70%
|
||||
Et que je n'utilise pas l'app pendant 1 an
|
||||
Quand je reviens et demande des recommandations
|
||||
Alors mes recommandations reflètent toujours mes goûts d'avant
|
||||
Et je reçois du contenu "Sport" prioritaire
|
||||
Et le comportement est cohérent et prévisible
|
||||
|
||||
Scénario: Réinitialiser manuellement mes centres d'intérêt
|
||||
Étant donné que je veux repartir de zéro
|
||||
Quand je vais dans les paramètres
|
||||
Et que je clique sur "Réinitialiser mes centres d'intérêt"
|
||||
Et que je confirme l'action
|
||||
Alors toutes mes jauges reviennent à 50%
|
||||
Et je vois le message "Vos centres d'intérêt ont été réinitialisés"
|
||||
|
||||
Scénario: Confirmation avant réinitialisation
|
||||
Étant donné que je suis dans les paramètres
|
||||
Quand je clique sur "Réinitialiser mes centres d'intérêt"
|
||||
Alors je vois un message de confirmation:
|
||||
| titre | Êtes-vous sûr ? |
|
||||
| message | Cette action remettra toutes vos jauges à 50% |
|
||||
| actions | Confirmer / Annuler |
|
||||
|
||||
Scénario: Annuler la réinitialisation
|
||||
Étant donné que j'ai cliqué sur "Réinitialiser mes centres d'intérêt"
|
||||
Et que la confirmation est affichée
|
||||
Quand je clique sur "Annuler"
|
||||
Alors mes jauges ne sont pas modifiées
|
||||
Et je reviens aux paramètres
|
||||
|
||||
Scénario: Raison de réinitialisation - changement de vie
|
||||
Étant donné que j'utilisais RoadWave pour mes trajets professionnels
|
||||
Et que mes jauges reflétaient "Économie" (85%) et "Technologie" (75%)
|
||||
Et que je change de vie et deviens musicien
|
||||
Quand je réinitialise mes centres d'intérêt
|
||||
Alors je peux repartir avec toutes les jauges à 50%
|
||||
Et découvrir du contenu "Musique" et "Culture" sans biais
|
||||
|
||||
Scénario: Pas de suggestion automatique de réinitialisation
|
||||
Étant donné que je n'ai pas utilisé l'app depuis 1 an
|
||||
Quand je me reconnecte
|
||||
Alors aucune suggestion de réinitialisation n'est affichée
|
||||
Et mes jauges sont conservées telles quelles
|
||||
Et je garde le contrôle total
|
||||
|
||||
Scénario: Historique conservé après réinitialisation
|
||||
Étant donné que j'ai écouté 500 contenus
|
||||
Quand je réinitialise mes centres d'intérêt
|
||||
Alors mes jauges reviennent à 50%
|
||||
Mais mon historique d'écoute est conservé
|
||||
Et je peux toujours consulter mes anciens contenus écoutés
|
||||
|
||||
Scénario: Évolution future basée sur nouvelles actions
|
||||
Étant donné que j'ai réinitialisé mes jauges à 50%
|
||||
Quand j'écoute 5 contenus "Voyage" à >80%
|
||||
Alors ma jauge "Voyage" monte à 60% (50% + 5 × 2%)
|
||||
Et l'algorithme recommence à apprendre mes nouvelles préférences
|
||||
|
||||
Scénario: Respect de l'historique utilisateur
|
||||
Étant donné qu'un utilisateur aime "Cryptomonnaie" depuis 2 ans
|
||||
Et que sa jauge est à 90%
|
||||
Quand 2 ans s'écoulent sans dégradation temporelle
|
||||
Alors sa jauge reste à 90%
|
||||
Car l'historique de ses goûts est respecté
|
||||
Et le système ne fait pas d'"oubli" artificiel
|
||||
|
||||
Scénario: Coût infrastructure zéro
|
||||
Étant donné qu'aucune dégradation temporelle n'existe
|
||||
Quand le système calcule les jauges
|
||||
Alors aucun calcul de date n'est nécessaire
|
||||
Et aucun batch nocturne ne tourne
|
||||
Et aucun bug de fuseau horaire ne peut survenir
|
||||
Et le coût CPU est minimal
|
||||
|
||||
Scénario: UX prévisible - jauge = actions
|
||||
Étant donné qu'un utilisateur consulte sa jauge "Sport" à 65%
|
||||
Quand il se demande pourquoi elle est à 65%
|
||||
Alors il peut retracer ses actions:
|
||||
| action | impact |
|
||||
| 10 likes automatiques | +10% |
|
||||
| 3 abonnements Sport | +15% |
|
||||
| 5 skips de contenu non-Sport| 0% |
|
||||
Et il comprend que c'est le reflet exact de ses actions
|
||||
Et il n'y a pas de mystère ou automatisme caché
|
||||
|
||||
Scénario: Statistiques affichées sans date
|
||||
Étant donné que je consulte mes centres d'intérêt
|
||||
Quand je vois mes jauges
|
||||
Alors je vois:
|
||||
| information | affiché |
|
||||
| Niveau actuel | ✅ 75% |
|
||||
| Évolution depuis début | ✅ +25% |
|
||||
| Dernière mise à jour | ❌ |
|
||||
Et aucune date n'est affichée car non pertinente
|
||||
Et seules les actions comptent
|
||||
193
features/ui/interest-gauges/evolution-jauges.feature
Normal file
193
features/ui/interest-gauges/evolution-jauges.feature
Normal file
@@ -0,0 +1,193 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Évolution des jauges d'intérêt
|
||||
En tant que système de recommandation
|
||||
Je veux faire évoluer les jauges d'intérêt selon les actions utilisateur
|
||||
Afin d'affiner les recommandations personnalisées
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et qu'un utilisateur est connecté
|
||||
|
||||
Scénario: Like automatique renforcé après écoute ≥80%
|
||||
Étant donné qu'un contenu de 5 minutes est tagué "Automobile"
|
||||
Et que ma jauge "Automobile" est à 45%
|
||||
Quand j'écoute le contenu pendant 4 minutes 30 secondes (90%)
|
||||
Alors je reçois un like automatique renforcé
|
||||
Et ma jauge "Automobile" augmente de 2%
|
||||
Et ma jauge "Automobile" est maintenant à 47%
|
||||
|
||||
Scénario: Like automatique renforcé exactement à 80%
|
||||
Étant donné qu'un contenu de 10 minutes est tagué "Voyage"
|
||||
Et que ma jauge "Voyage" est à 60%
|
||||
Quand j'écoute le contenu pendant exactement 8 minutes (80%)
|
||||
Alors je reçois un like automatique renforcé
|
||||
Et ma jauge "Voyage" augmente de 2%
|
||||
Et ma jauge "Voyage" est maintenant à 62%
|
||||
|
||||
Scénario: Like automatique standard après écoute 30-79%
|
||||
Étant donné qu'un contenu de 5 minutes est tagué "Automobile"
|
||||
Et que ma jauge "Automobile" est à 45%
|
||||
Quand j'écoute le contenu pendant 2 minutes 30 secondes (50%)
|
||||
Alors je reçois un like automatique standard
|
||||
Et ma jauge "Automobile" augmente de 1%
|
||||
Et ma jauge "Automobile" est maintenant à 46%
|
||||
|
||||
Scénario: Like automatique standard à 30% exactement
|
||||
Étant donné qu'un contenu de 10 minutes est tagué "Musique"
|
||||
Et que ma jauge "Musique" est à 40%
|
||||
Quand j'écoute le contenu pendant exactement 3 minutes (30%)
|
||||
Alors je reçois un like automatique standard
|
||||
Et ma jauge "Musique" augmente de 1%
|
||||
|
||||
Scénario: Like automatique standard à 79%
|
||||
Étant donné qu'un contenu de 10 minutes est tagué "Sport"
|
||||
Et que ma jauge "Sport" est à 55%
|
||||
Quand j'écoute le contenu pendant 7 minutes 54 secondes (79%)
|
||||
Alors je reçois un like automatique standard
|
||||
Et ma jauge "Sport" augmente de 1%
|
||||
Et ma jauge "Sport" est maintenant à 56%
|
||||
|
||||
Scénario: Like explicite (manuel) +2%
|
||||
Étant donné qu'un contenu est tagué "Économie"
|
||||
Et que ma jauge "Économie" est à 70%
|
||||
Quand j'écoute le contenu partiellement
|
||||
Et que je clique manuellement sur le bouton "Like"
|
||||
Alors ma jauge "Économie" augmente de 2%
|
||||
Et ma jauge "Économie" est maintenant à 72%
|
||||
|
||||
Scénario: Like manuel cumulable avec like automatique
|
||||
Étant donné qu'un contenu de 5 minutes est tagué "Automobile"
|
||||
Et que ma jauge "Automobile" est à 45%
|
||||
Quand j'écoute le contenu pendant 2 minutes 30 secondes (50%)
|
||||
Alors je reçois un like automatique standard (+1%)
|
||||
Quand je clique ensuite sur le bouton "Like"
|
||||
Alors ma jauge augmente encore de 2% (like manuel)
|
||||
Et ma jauge "Automobile" a augmenté de 3% au total
|
||||
Et ma jauge "Automobile" est maintenant à 48%
|
||||
|
||||
Scénario: Abonnement créateur impacte tous ses tags
|
||||
Étant donné qu'un créateur publie des contenus tagués "Automobile" et "Technologie"
|
||||
Et que mes jauges sont:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 50% |
|
||||
| Technologie | 45% |
|
||||
Quand je m'abonne à ce créateur
|
||||
Alors ma jauge "Automobile" augmente de 5%
|
||||
Et ma jauge "Technologie" augmente de 5%
|
||||
Et mes nouvelles jauges sont:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 55% |
|
||||
| Technologie | 50% |
|
||||
|
||||
Scénario: Skip rapide (<10s) diminue la jauge
|
||||
Étant donné qu'un contenu est tagué "Économie"
|
||||
Et que ma jauge "Économie" est à 45%
|
||||
Quand je skip le contenu après 5 secondes
|
||||
Alors ma jauge "Économie" diminue de 0.5%
|
||||
Et ma jauge "Économie" est maintenant à 44.5%
|
||||
|
||||
Scénario: Skip à exactement 10s ne diminue pas la jauge
|
||||
Étant donné qu'un contenu est tagué "Politique"
|
||||
Et que ma jauge "Politique" est à 50%
|
||||
Quand je skip le contenu après exactement 10 secondes
|
||||
Alors ma jauge "Politique" ne change pas
|
||||
Et reste à 50%
|
||||
|
||||
Scénario: Skip tardif (≥30%) est neutre
|
||||
Étant donné qu'un contenu de 10 minutes est tagué "Musique"
|
||||
Et que ma jauge "Musique" est à 60%
|
||||
Quand j'écoute pendant 3 minutes (30%)
|
||||
Et que je skip ensuite
|
||||
Alors ma jauge "Musique" ne diminue pas (signal neutre)
|
||||
Et ma jauge reste à 60% (plus le +1% de like auto si applicable)
|
||||
|
||||
Scénario: Contenu avec plusieurs tags impacte toutes les jauges
|
||||
Étant donné qu'un contenu est tagué "Automobile" et "Voyage"
|
||||
Et que mes jauges sont:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 45% |
|
||||
| Voyage | 60% |
|
||||
Quand j'écoute le contenu à 90%
|
||||
Alors les deux jauges augmentent de 2%
|
||||
Et mes nouvelles jauges sont:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 47% |
|
||||
| Voyage | 62% |
|
||||
|
||||
Scénario: Contenu avec 3 tags impacte les 3 jauges
|
||||
Étant donné qu'un contenu est tagué "Sport", "Santé" et "Technologie"
|
||||
Et que mes jauges sont à 50% pour chaque catégorie
|
||||
Quand je skip rapidement après 5 secondes
|
||||
Alors les 3 jauges diminuent de 0.5%
|
||||
Et toutes passent à 49.5%
|
||||
|
||||
Scénario: Jauges bornées - ne peut pas dépasser 100%
|
||||
Étant donné que ma jauge "Cryptomonnaie" est à 99%
|
||||
Et qu'un contenu tagué "Cryptomonnaie" est disponible
|
||||
Quand j'écoute le contenu à 95% (like auto renforcé +2%)
|
||||
Alors ma jauge "Cryptomonnaie" passe à 100% (maximum)
|
||||
Et ne dépasse pas 100%
|
||||
|
||||
Scénario: Jauges bornées - ne peut pas descendre sous 0%
|
||||
Étant donné que ma jauge "Politique" est à 0.3%
|
||||
Et qu'un contenu tagué "Politique" est disponible
|
||||
Quand je skip rapidement après 3 secondes (-0.5%)
|
||||
Alors ma jauge "Politique" passe à 0% (minimum)
|
||||
Et ne devient pas négative
|
||||
|
||||
Scénario: Calcul immédiat à chaque action
|
||||
Étant donné que ma jauge "Voyage" est à 50%
|
||||
Quand j'écoute un contenu "Voyage" à 85%
|
||||
Alors la jauge est mise à jour immédiatement (pas de batch)
|
||||
Et passe à 52%
|
||||
Quand je demande mes recommandations dans la seconde suivante
|
||||
Alors l'algorithme utilise déjà la valeur 52%
|
||||
|
||||
Scénario: Like manuel après écoute <30% (pas de like auto)
|
||||
Étant donné qu'un contenu de 10 minutes est tagué "Culture"
|
||||
Et que ma jauge "Culture" est à 60%
|
||||
Quand j'écoute pendant 2 minutes (20%)
|
||||
Alors je ne reçois pas de like automatique
|
||||
Quand je clique sur le bouton "Like"
|
||||
Alors ma jauge "Culture" augmente de 2% uniquement
|
||||
Et ma jauge "Culture" est maintenant à 62%
|
||||
|
||||
Scénario: Unlike retire le like manuel
|
||||
Étant donné que j'ai liké manuellement un contenu "Sport"
|
||||
Et que ma jauge "Sport" est passée de 55% à 57% (+2%)
|
||||
Quand je clique sur "Unlike"
|
||||
Alors ma jauge "Sport" diminue de 2%
|
||||
Et ma jauge "Sport" revient à 55%
|
||||
|
||||
Scénario: Unlike ne peut pas retirer un like automatique
|
||||
Étant donné que j'ai écouté un contenu "Musique" à 90%
|
||||
Et que j'ai reçu un like automatique renforcé (+2%)
|
||||
Et que ma jauge "Musique" est à 52%
|
||||
Quand j'essaie de faire "Unlike"
|
||||
Alors l'action n'est pas disponible
|
||||
Et ma jauge reste à 52%
|
||||
Car les likes automatiques ne peuvent pas être retirés
|
||||
|
||||
Scénario: Tags définis par créateur à la publication
|
||||
Étant donné que je suis un créateur
|
||||
Quand je publie un contenu
|
||||
Alors je dois sélectionner 1 à 3 tags
|
||||
Et ces tags sont fixés après publication
|
||||
Et impacteront les jauges de tous les auditeurs
|
||||
|
||||
Plan du Scénario: Calculs avec différentes durées d'écoute
|
||||
Étant donné qu'un contenu de 10 minutes est tagué "Voyage"
|
||||
Et que ma jauge "Voyage" est à 50%
|
||||
Quand j'écoute pendant <duree> (<pourcentage>)
|
||||
Alors ma jauge évolue de <impact>
|
||||
Et ma nouvelle jauge est à <nouveau_niveau>
|
||||
|
||||
Exemples:
|
||||
| duree | pourcentage | impact | nouveau_niveau |
|
||||
| 1 min | 10% | 0% | 50% |
|
||||
| 3 min | 30% | +1% | 51% |
|
||||
| 5 min | 50% | +1% | 51% |
|
||||
| 7.9 min | 79% | +1% | 51% |
|
||||
| 8 min | 80% | +2% | 52% |
|
||||
| 9.5 min | 95% | +2% | 52% |
|
||||
| 5 sec | <1% | -0.5% | 49.5% |
|
||||
147
features/ui/interest-gauges/jauge-initiale.feature
Normal file
147
features/ui/interest-gauges/jauge-initiale.feature
Normal file
@@ -0,0 +1,147 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Jauge initiale et cold start
|
||||
En tant que nouvel utilisateur
|
||||
Je veux que mes jauges d'intérêt démarrent de manière neutre
|
||||
Afin de découvrir du contenu sans biais initial
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
|
||||
Scénario: Inscription - toutes les jauges à 50%
|
||||
Quand je m'inscris sur RoadWave
|
||||
Alors toutes mes jauges d'intérêt sont initialisées à 50%
|
||||
Et je ne dois pas remplir de questionnaire
|
||||
Et l'inscription est ultra-rapide
|
||||
|
||||
Scénario: Liste des catégories disponibles
|
||||
Étant donné que je suis un nouvel utilisateur
|
||||
Quand je consulte mes centres d'intérêt
|
||||
Alors je vois les catégories suivantes à 50%:
|
||||
| catégorie |
|
||||
| Automobile |
|
||||
| Voyage |
|
||||
| Famille |
|
||||
| Amour |
|
||||
| Musique |
|
||||
| Économie |
|
||||
| Cryptomonnaie |
|
||||
| Politique |
|
||||
| Culture générale |
|
||||
| Sport |
|
||||
| Technologie |
|
||||
| Santé |
|
||||
|
||||
Scénario: Cold start - premier contenu écouté
|
||||
Étant donné que je viens de m'inscrire
|
||||
Et que toutes mes jauges sont à 50%
|
||||
Quand j'écoute mon premier podcast "Automobile" à 90%
|
||||
Alors ma jauge "Automobile" monte à 52% (+2%)
|
||||
Et toutes les autres jauges restent à 50%
|
||||
|
||||
Scénario: Cold start - premier skip
|
||||
Étant donné que je viens de m'inscrire
|
||||
Et que toutes mes jauges sont à 50%
|
||||
Quand je skip rapidement un contenu "Économie"
|
||||
Alors ma jauge "Économie" descend à 49.5% (-0.5%)
|
||||
Et toutes les autres jauges restent à 50%
|
||||
|
||||
Scénario: Après 10 écoutes, profil commence à se dessiner
|
||||
Étant donné que je suis un nouvel utilisateur
|
||||
Et que j'ai écouté:
|
||||
| contenu | tags | completion |
|
||||
| Contenu 1 | Automobile | 90% |
|
||||
| Contenu 2 | Automobile, Sport | 85% |
|
||||
| Contenu 3 | Voyage | 75% |
|
||||
| Contenu 4 | Économie | skip 5s |
|
||||
| Contenu 5 | Automobile | 95% |
|
||||
| Contenu 6 | Sport | 80% |
|
||||
| Contenu 7 | Politique | skip 8s |
|
||||
| Contenu 8 | Voyage | 88% |
|
||||
| Contenu 9 | Automobile | 92% |
|
||||
| Contenu 10 | Technologie | 40% |
|
||||
Alors mes jauges reflètent mes préférences:
|
||||
| catégorie | tendance |
|
||||
| Automobile | Forte hausse (>55%) |
|
||||
| Voyage | Hausse modérée (~53%) |
|
||||
| Sport | Hausse modérée (~53%) |
|
||||
| Économie | Baisse légère (~49.5%) |
|
||||
| Politique | Baisse légère (~49.5%) |
|
||||
| Technologie | Neutre (~51%) |
|
||||
|
||||
Scénario: Pas de questionnaire onboarding par défaut
|
||||
Quand je termine l'inscription
|
||||
Alors aucun questionnaire de centres d'intérêt n'est affiché
|
||||
Et je peux commencer à écouter immédiatement
|
||||
Et l'algorithme apprend naturellement
|
||||
|
||||
Scénario: Algorithme avec jauges à 50% - chances égales
|
||||
Étant donné que toutes mes jauges sont à 50%
|
||||
Quand l'algorithme calcule les recommandations
|
||||
Alors tous les types de contenus ont une chance égale
|
||||
Et aucun biais initial n'est appliqué
|
||||
Et la géolocalisation prime sur les intérêts
|
||||
|
||||
Scénario: Questionnaire optionnel après 3 écoutes (post-MVP)
|
||||
Étant donné que j'ai écouté 3 contenus
|
||||
Quand je termine ma 3ème écoute
|
||||
Alors je vois une notification in-app optionnelle:
|
||||
| titre | Améliorez vos recommandations |
|
||||
| message | Sélectionnez vos centres d'intérêt |
|
||||
| actions | Configurer maintenant / Plus tard |
|
||||
|
||||
Scénario: Remplir le questionnaire optionnel (post-MVP)
|
||||
Étant donné que le questionnaire optionnel est affiché
|
||||
Quand je sélectionne les centres d'intérêt suivants:
|
||||
| catégorie |
|
||||
| Automobile |
|
||||
| Voyage |
|
||||
| Sport |
|
||||
Alors les jauges sélectionnées passent à 70%
|
||||
Et les jauges non sélectionnées passent à 30%
|
||||
Et je vois le message "Vos préférences ont été enregistrées"
|
||||
|
||||
Scénario: Skipper le questionnaire optionnel (post-MVP)
|
||||
Étant donné que le questionnaire optionnel est affiché
|
||||
Quand je clique sur "Plus tard"
|
||||
Alors toutes mes jauges conservent 50%
|
||||
Et l'algorithme continue d'apprendre naturellement
|
||||
Et je ne suis plus sollicité
|
||||
|
||||
Scénario: Comportement déterministe et testable
|
||||
Étant donné deux nouveaux utilisateurs A et B
|
||||
Quand les deux s'inscrivent au même moment
|
||||
Alors leurs jauges sont identiques (toutes à 50%)
|
||||
Et leurs recommandations initiales sont identiques (basées sur géo uniquement)
|
||||
|
||||
Scénario: Équité entre créateurs au cold start
|
||||
Étant donné qu'un nouvel utilisateur s'inscrit
|
||||
Et qu'il existe 1000 contenus de catégories variées dans sa zone
|
||||
Quand l'algorithme génère les premières recommandations
|
||||
Alors tous les contenus ont une pondération intérêts identique (50%)
|
||||
Et seuls la géolocalisation et l'engagement différencient les contenus
|
||||
Et aucun créateur n'a d'avantage initial
|
||||
|
||||
Scénario: Catégories extensibles
|
||||
Étant donné que RoadWave ajoute une nouvelle catégorie "Gastronomie"
|
||||
Quand je consulte mes centres d'intérêt
|
||||
Alors je vois la nouvelle catégorie "Gastronomie" à 50%
|
||||
Et je peux commencer à l'explorer normalement
|
||||
|
||||
Scénario: Voir l'évolution de mes jauges
|
||||
Étant donné que je suis un utilisateur avec historique
|
||||
Quand je consulte mes centres d'intérêt dans les paramètres
|
||||
Alors je vois mes jauges actuelles:
|
||||
| catégorie | niveau | evolution |
|
||||
| Automobile | 67% | +17% |
|
||||
| Voyage | 82% | +32% |
|
||||
| Économie | 34% | -16% |
|
||||
| Sport | 50% | 0% |
|
||||
Et je comprends mes préférences actuelles
|
||||
|
||||
Scénario: Friction zéro à l'inscription
|
||||
Étant donné que je veux m'inscrire rapidement
|
||||
Quand je remplis les 4 champs obligatoires
|
||||
Et que je clique sur "S'inscrire"
|
||||
Alors mon compte est créé immédiatement
|
||||
Et je peux commencer à écouter dans les 30 secondes
|
||||
Et aucune configuration supplémentaire n'est requise
|
||||
425
features/ui/mode-offline/synchronisation-actions.feature
Normal file
425
features/ui/mode-offline/synchronisation-actions.feature
Normal file
@@ -0,0 +1,425 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Synchronisation actions offline
|
||||
En tant qu'utilisateur
|
||||
Je veux que mes actions offline soient synchronisées quand je me reconnecte
|
||||
Afin de ne perdre aucune interaction même sans connexion
|
||||
|
||||
Contexte:
|
||||
Étant donné que j'utilise l'application RoadWave
|
||||
|
||||
# ===== ACTIONS STOCKÉES LOCALEMENT =====
|
||||
|
||||
Scénario: Like d'un contenu en mode offline
|
||||
Étant donné que je n'ai aucune connexion Internet
|
||||
Quand je like un contenu téléchargé
|
||||
Alors l'action est enregistrée localement dans SQLite:
|
||||
```sql
|
||||
INSERT INTO pending_actions (type, content_id, created_at)
|
||||
VALUES ('like', 'abc123', '2025-06-15 14:30:00');
|
||||
```
|
||||
Et l'UI affiche immédiatement le like (optimistic update)
|
||||
|
||||
Scénario: Unlike d'un contenu en mode offline
|
||||
Étant donné que je n'ai aucune connexion Internet
|
||||
Et que j'avais liké un contenu
|
||||
Quand je retire mon like
|
||||
Alors l'action est enregistrée localement:
|
||||
```sql
|
||||
INSERT INTO pending_actions (type, content_id, created_at)
|
||||
VALUES ('unlike', 'abc123', '2025-06-15 14:35:00');
|
||||
```
|
||||
Et l'UI retire immédiatement le like
|
||||
|
||||
Scénario: Abonnement à un créateur en mode offline
|
||||
Étant donné que je n'ai aucune connexion Internet
|
||||
Quand je m'abonne à un créateur
|
||||
Alors l'action est enregistrée localement:
|
||||
```sql
|
||||
INSERT INTO pending_actions (type, creator_id, created_at)
|
||||
VALUES ('subscribe', 'creator456', '2025-06-15 14:40:00');
|
||||
```
|
||||
Et l'UI affiche immédiatement "Abonné ✓"
|
||||
|
||||
Scénario: Désabonnement d'un créateur en mode offline
|
||||
Étant donné que je n'ai aucune connexion Internet
|
||||
Et que j'étais abonné à un créateur
|
||||
Quand je me désabonne
|
||||
Alors l'action est enregistrée localement:
|
||||
```sql
|
||||
INSERT INTO pending_actions (type, creator_id, created_at)
|
||||
VALUES ('unsubscribe', 'creator456', '2025-06-15 14:45:00');
|
||||
```
|
||||
Et l'UI affiche "S'abonner"
|
||||
|
||||
Scénario: Signalement d'un contenu en mode offline
|
||||
Étant donné que je n'ai aucune connexion Internet
|
||||
Quand je signale un contenu pour "Contenu inapproprié"
|
||||
Alors l'action est enregistrée localement:
|
||||
```sql
|
||||
INSERT INTO pending_actions (type, content_id, reason, created_at)
|
||||
VALUES ('report', 'abc123', 'Contenu inapproprié', '2025-06-15 14:50:00');
|
||||
```
|
||||
Et je vois "Signalement enregistré. Sera envoyé à la reconnexion."
|
||||
|
||||
Scénario: Progression audio-guide en mode offline
|
||||
Étant donné que je n'ai aucune connexion Internet
|
||||
Et que j'écoute un audio-guide multi-séquences
|
||||
Quand je termine la séquence 3/10
|
||||
Alors la progression est enregistrée localement:
|
||||
```sql
|
||||
INSERT INTO pending_actions (type, guide_id, sequence_id, created_at)
|
||||
VALUES ('guide_progress', 'guide789', 'seq003', '2025-06-15 15:00:00');
|
||||
```
|
||||
Et ma progression est sauvegardée
|
||||
|
||||
Scénario: Multiple actions offline stockées en queue
|
||||
Étant donné que je n'ai aucune connexion Internet pendant 2 jours
|
||||
Quand j'effectue plusieurs actions:
|
||||
| action | cible |
|
||||
| like | contenu A |
|
||||
| like | contenu B |
|
||||
| subscribe | créateur X |
|
||||
| unlike | contenu C |
|
||||
| report | contenu D |
|
||||
Alors les 5 actions sont stockées dans pending_actions
|
||||
Et elles seront synchronisées dans l'ordre à la reconnexion
|
||||
|
||||
# ===== SYNCHRONISATION AUTOMATIQUE =====
|
||||
|
||||
Scénario: Détection reconnexion Internet
|
||||
Étant donné que j'étais en mode offline
|
||||
Quand l'app détecte une reconnexion Internet
|
||||
Alors le processus de synchronisation démarre automatiquement
|
||||
Et je vois une notification "Synchronisation en cours..."
|
||||
|
||||
Scénario: Récupération queue locale pendant sync
|
||||
Étant donné que la synchronisation démarre
|
||||
Quand l'app récupère les actions en attente
|
||||
Alors une requête SQL est exécutée:
|
||||
```sql
|
||||
SELECT * FROM pending_actions ORDER BY created_at ASC;
|
||||
```
|
||||
Et toutes les actions sont récupérées dans l'ordre chronologique
|
||||
|
||||
Scénario: Envoi batch API des actions
|
||||
Étant donné que 15 actions sont en attente
|
||||
Quand le batch est envoyé au backend
|
||||
Alors une requête POST /sync/actions est faite:
|
||||
```json
|
||||
{
|
||||
"actions": [
|
||||
{"type": "like", "content_id": "abc123", "timestamp": "2025-06-15T14:30:00Z"},
|
||||
{"type": "subscribe", "creator_id": "creator456", "timestamp": "2025-06-15T14:40:00Z"},
|
||||
{"type": "unlike", "content_id": "def789", "timestamp": "2025-06-15T14:50:00Z"},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
Et toutes les actions sont groupées en une seule requête
|
||||
|
||||
Scénario: Backend traite chaque action
|
||||
Étant donné que le backend reçoit le batch d'actions
|
||||
Quand il traite chaque action
|
||||
Alors pour chaque action:
|
||||
| étape | détail |
|
||||
| Validation | Vérifier user_id, content_id valides |
|
||||
| Vérification existence | Contenu/créateur existe toujours ? |
|
||||
| Application action | INSERT/UPDATE/DELETE en base |
|
||||
| Mise à jour compteurs | Likes, abonnés, etc. |
|
||||
| Impact sur algorithme | Mise à jour jauges si nécessaire |
|
||||
|
||||
Scénario: Confirmation réception et suppression queue locale
|
||||
Étant donné que le backend a traité toutes les actions avec succès
|
||||
Quand la confirmation est reçue par l'app
|
||||
Alors les actions sont supprimées de la queue locale:
|
||||
```sql
|
||||
DELETE FROM pending_actions WHERE id IN (1, 2, 3, ..., 15);
|
||||
```
|
||||
Et la table pending_actions est vidée
|
||||
|
||||
Scénario: Toast confirmation synchronisation
|
||||
Étant donné que 15 actions ont été synchronisées
|
||||
Quand la synchronisation se termine
|
||||
Alors je vois un toast:
|
||||
"""
|
||||
✅ Synchronisation réussie
|
||||
|
||||
3 likes, 1 abonnement et 1 signalement synchronisés.
|
||||
"""
|
||||
|
||||
Scénario: Synchronisation silencieuse si peu d'actions
|
||||
Étant donné que j'ai seulement 2 actions en attente
|
||||
Quand la synchronisation se termine
|
||||
Alors aucun toast n'est affiché (sync silencieuse)
|
||||
Et l'expérience reste fluide
|
||||
Mais je peux voir le détail dans l'historique des syncs
|
||||
|
||||
# ===== GESTION ERREURS SYNC =====
|
||||
|
||||
Scénario: Échec synchronisation - Retry automatique
|
||||
Étant donné que la synchronisation échoue (erreur réseau)
|
||||
Quand l'échec est détecté
|
||||
Alors un retry automatique est programmé dans 30 secondes
|
||||
Et les actions restent dans pending_actions
|
||||
|
||||
Scénario: 3 tentatives échouées - Notification utilisateur
|
||||
Étant donné que 3 tentatives de synchronisation ont échoué
|
||||
Quand la 3ème tentative échoue
|
||||
Alors je reçois une notification:
|
||||
"""
|
||||
⚠️ Impossible de synchroniser vos actions
|
||||
|
||||
15 actions en attente de synchronisation.
|
||||
Vérifiez votre connexion et réessayez.
|
||||
|
||||
[Réessayer maintenant] [Plus tard]
|
||||
```
|
||||
|
||||
Scénario: Actions conservées jusqu'à sync réussie
|
||||
Étant donné que la synchronisation échoue plusieurs fois
|
||||
Quand les tentatives continuent d'échouer
|
||||
Alors les actions restent dans pending_actions
|
||||
Et aucune action n'est perdue
|
||||
Et elles seront envoyées dès que la connexion sera stable
|
||||
|
||||
Scénario: Rétention max 7 jours - Purge automatique
|
||||
Étant donné qu'une action est en attente depuis 7 jours
|
||||
Quand le système détecte cette ancienneté
|
||||
Alors l'action est automatiquement supprimée de la queue
|
||||
Et je vois "1 action trop ancienne supprimée (>7 jours)"
|
||||
Et cela évite une queue infinie
|
||||
|
||||
Scénario: Justification rétention 7 jours
|
||||
Étant donné qu'un utilisateur ne se connecte jamais pendant 2 semaines
|
||||
Quand ses actions ont >7 jours
|
||||
Alors elles sont purgées automatiquement
|
||||
Car après 7 jours, l'action perd sa pertinence
|
||||
Et évite une queue qui grandit indéfiniment
|
||||
|
||||
Scénario: Retry manuel après échec
|
||||
Étant donné que la synchronisation a échoué
|
||||
Quand je clique sur "Réessayer maintenant"
|
||||
Alors une nouvelle tentative de synchronisation est lancée immédiatement
|
||||
Et si elle réussit, les actions sont synchronisées
|
||||
|
||||
# ===== CONFLITS CONTENUS SUPPRIMÉS =====
|
||||
|
||||
Scénario: Backend retourne contenus supprimés
|
||||
Étant donné que j'ai liké un contenu offline
|
||||
Mais que le contenu a été supprimé entre temps
|
||||
Quand le backend traite la synchronisation
|
||||
Alors il retourne:
|
||||
```json
|
||||
{
|
||||
"status": "partial_success",
|
||||
"deleted_content_ids": [123, 456],
|
||||
"failed_actions": [
|
||||
{"type": "like", "content_id": "123", "reason": "content_deleted"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Scénario: App supprime fichiers locaux contenus supprimés
|
||||
Étant donné que le backend retourne deleted_content_ids: [123, 456]
|
||||
Quand l'app traite la réponse
|
||||
Alors elle supprime les fichiers locaux des contenus 123 et 456
|
||||
Et libère l'espace disque
|
||||
Et les actions associées sont retirées de la queue
|
||||
|
||||
Scénario: Contenu supprimé en cours d'écoute
|
||||
Étant donné que j'écoute le contenu 123 en offline
|
||||
Et que la sync détecte que le contenu a été supprimé
|
||||
Quand la lecture actuelle se termine
|
||||
Alors l'app attend 2 secondes
|
||||
Et passe automatiquement au contenu suivant
|
||||
Et le fichier du contenu 123 est supprimé en arrière-plan
|
||||
|
||||
Scénario: Toast notification contenu retiré
|
||||
Étant donné que 2 contenus téléchargés ont été supprimés
|
||||
Quand la synchronisation se termine
|
||||
Alors je vois un toast:
|
||||
"""
|
||||
🗑️ 2 contenus téléchargés ont été retirés
|
||||
|
||||
Raison: Violation des règles de la plateforme
|
||||
"""
|
||||
|
||||
Scénario: Contenu modéré après téléchargement
|
||||
Étant donné que j'ai téléchargé un contenu qui est ensuite modéré
|
||||
Quand la synchronisation détecte la modération
|
||||
Alors le contenu est immédiatement supprimé du device
|
||||
Et je ne peux plus l'écouter
|
||||
Et cela garantit la conformité même offline
|
||||
|
||||
# ===== JUSTIFICATIONS =====
|
||||
|
||||
Scénario: Justification pas de conflit possible
|
||||
Étant donné que les actions offline sont unilatérales (likes, abonnements)
|
||||
Quand elles sont synchronisées
|
||||
Alors il n'y a pas de conflit de version possible
|
||||
Car l'utilisateur ajoute/retire simplement des préférences
|
||||
Et pas de merge complexe nécessaire
|
||||
|
||||
Scénario: Justification UX fluide offline
|
||||
Étant donné que toutes les actions fonctionnent offline
|
||||
Quand l'utilisateur interagit sans connexion
|
||||
Alors l'expérience est identique au mode online
|
||||
Et l'utilisateur n'est pas bloqué
|
||||
Et peut utiliser l'app normalement
|
||||
|
||||
Scénario: Justification batch = Économie requêtes
|
||||
Étant donné que 15 actions sont en attente
|
||||
Quand elles sont synchronisées en batch
|
||||
Alors 1 seule requête HTTP est envoyée (vs 15 si individuelles)
|
||||
Et cela économise la bande passante et la batterie
|
||||
Et réduit la charge serveur
|
||||
|
||||
Scénario: Justification conformité modération offline
|
||||
Étant donné qu'un contenu illégal est modéré pendant qu'un user est offline
|
||||
Quand le user se reconnecte
|
||||
Alors le contenu est immédiatement supprimé de son device
|
||||
Et cela garantit que les contenus illégaux disparaissent même offline
|
||||
|
||||
# ===== STATISTIQUES ET MONITORING =====
|
||||
|
||||
Scénario: Historique synchronisations
|
||||
Étant donné que j'accède à "Paramètres > Synchronisation"
|
||||
Quand je consulte l'historique
|
||||
Alors je vois:
|
||||
| date | actions sync | statut |
|
||||
| 15/06/2025 14:30:00 | 15 | Réussi ✅ |
|
||||
| 14/06/2025 09:15:00 | 7 | Réussi ✅ |
|
||||
| 13/06/2025 18:45:00 | 3 | Échec ❌ |
|
||||
|
||||
Scénario: Détail d'une synchronisation
|
||||
Étant donné que je clique sur une ligne de l'historique
|
||||
Quand le détail s'affiche
|
||||
Alors je vois:
|
||||
```
|
||||
Synchronisation du 15/06/2025 14:30:00
|
||||
|
||||
Actions synchronisées:
|
||||
• 3 likes
|
||||
• 1 abonnement
|
||||
• 1 signalement
|
||||
• 10 progressions audio-guides
|
||||
|
||||
Durée: 1.2s
|
||||
Statut: Réussi ✅
|
||||
```
|
||||
|
||||
Scénario: Compteur actions en attente visible
|
||||
Étant donné que j'ai 12 actions en attente de synchronisation
|
||||
Quand j'accède à l'onglet Profil
|
||||
Alors je vois un badge "12" sur l'icône de synchronisation
|
||||
Et je sais qu'il y a des actions en attente
|
||||
|
||||
Scénario: Synchronisation manuelle forcée
|
||||
Étant donné que je veux forcer une synchronisation immédiate
|
||||
Quand je vais dans "Paramètres > Synchronisation"
|
||||
Et que je clique sur "Synchroniser maintenant"
|
||||
Alors la synchronisation démarre immédiatement
|
||||
Et toutes les actions en attente sont envoyées
|
||||
|
||||
Scénario: Statistiques utilisateur - Syncs effectuées
|
||||
Étant donné que j'accède à mes statistiques
|
||||
Quand je consulte la section Synchronisation
|
||||
Alors je vois:
|
||||
| métrique | valeur |
|
||||
| Synchronisations depuis début | 87 |
|
||||
| Actions synchronisées total | 1,234 |
|
||||
| Taux de succès | 94% |
|
||||
| Dernière sync | Il y a 2h|
|
||||
|
||||
Scénario: Statistiques admin - Volume synchronisations
|
||||
Étant donné qu'un admin consulte les métriques de synchronisation
|
||||
Quand il accède au dashboard
|
||||
Alors il voit:
|
||||
| métrique | valeur |
|
||||
| Synchronisations/jour | 45,678 |
|
||||
| Actions synchronisées/jour | 234,567 |
|
||||
| Taux succès sync | 96.5% |
|
||||
| Temps moyen traitement batch | 0.8s |
|
||||
| Actions en attente (global) | 12,345 |
|
||||
|
||||
Scénario: Alerte admin si taux échec sync >10%
|
||||
Étant donné que le taux d'échec sync dépasse 10%
|
||||
Quand le système détecte cette anomalie
|
||||
Alors une alerte est envoyée:
|
||||
"""
|
||||
⚠️ Taux échec synchronisation anormal: 12.3%
|
||||
|
||||
Échecs aujourd'hui: 5,621 / 45,678 syncs
|
||||
Causes principales:
|
||||
- Timeout serveur: 3,245
|
||||
- Erreur réseau client: 1,876
|
||||
- Données invalides: 500
|
||||
|
||||
Action recommandée: Vérifier charge serveur + logs erreurs
|
||||
"""
|
||||
|
||||
# ===== TESTS PERFORMANCE =====
|
||||
|
||||
Scénario: Synchronisation rapide <2s
|
||||
Étant donné que j'ai 20 actions en attente
|
||||
Quand la synchronisation démarre
|
||||
Alors le traitement prend <2 secondes
|
||||
Et je ne remarque aucun ralentissement de l'app
|
||||
|
||||
Scénario: Synchronisation de gros batch (100 actions)
|
||||
Étant donné que je n'ai pas synchronisé pendant 1 semaine
|
||||
Et que j'ai 100 actions en attente
|
||||
Quand la synchronisation démarre
|
||||
Alors le batch de 100 actions est traité en <5 secondes
|
||||
Et toutes les actions sont synchronisées avec succès
|
||||
|
||||
Scénario: Gestion charge serveur - 10 000 syncs simultanées
|
||||
Étant donné que 10 000 utilisateurs se reconnectent simultanément
|
||||
Quand chacun envoie un batch de 20 actions
|
||||
Alors le serveur traite 200 000 actions
|
||||
Et grâce au traitement asynchrone (queue Redis), le temps de réponse reste <3s
|
||||
Et aucun timeout n'est constaté
|
||||
|
||||
Scénario: Stockage SQLite optimisé
|
||||
Étant donné que la table pending_actions stocke des centaines d'actions
|
||||
Quand des requêtes sont exécutées
|
||||
Alors la table est indexée sur created_at
|
||||
Et les requêtes SELECT et DELETE sont instantanées (<10ms)
|
||||
Et l'expérience utilisateur reste fluide
|
||||
|
||||
Scénario: Nettoyage automatique table pending_actions
|
||||
Étant donné que la table pending_actions grossit avec le temps
|
||||
Quand les actions sont synchronisées et supprimées
|
||||
Alors la table est automatiquement optimisée (VACUUM sur SQLite)
|
||||
Et l'espace disque est libéré
|
||||
Et les performances restent optimales
|
||||
|
||||
# ===== EDGE CASES =====
|
||||
|
||||
Scénario: Action dupliquée - Idempotence
|
||||
Étant donné que j'ai liké un contenu offline
|
||||
Et que la sync échoue et retry
|
||||
Quand le backend reçoit 2 fois le même like
|
||||
Alors il applique l'idempotence (1 seul like enregistré)
|
||||
Et le compteur de likes n'est pas faussé
|
||||
|
||||
Scénario: Séquence like/unlike offline
|
||||
Étant donné que j'ai liké puis unliké un contenu offline
|
||||
Quand les 2 actions sont synchronisées
|
||||
Alors le backend applique les 2 actions dans l'ordre
|
||||
Et le résultat final est "pas de like" (état correct)
|
||||
|
||||
Scénario: Abonnement puis désabonnement offline
|
||||
Étant donné que je me suis abonné puis désabonné d'un créateur offline
|
||||
Quand les 2 actions sont synchronisées
|
||||
Alors le backend applique les 2 actions dans l'ordre
|
||||
Et le résultat final est "pas abonné"
|
||||
Et les jauges évoluent correctement (+5% puis -5% = 0% net)
|
||||
|
||||
Scénario: Créateur supprimé pendant offline
|
||||
Étant donné que je me suis abonné à un créateur offline
|
||||
Mais que le créateur a supprimé son compte entre temps
|
||||
Quand la sync traite l'abonnement
|
||||
Alors le backend retourne "creator_deleted"
|
||||
Et l'action est ignorée silencieusement
|
||||
Et aucune erreur n'est affichée à l'utilisateur
|
||||
409
features/ui/mode-offline/telechargement.feature
Normal file
409
features/ui/mode-offline/telechargement.feature
Normal file
@@ -0,0 +1,409 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Téléchargement de contenus offline
|
||||
En tant qu'utilisateur
|
||||
Je veux télécharger des contenus pour les écouter sans connexion
|
||||
Afin de profiter de RoadWave même dans les zones sans réseau
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis connecté à l'application RoadWave
|
||||
|
||||
# ===== SÉLECTION ZONE GÉOGRAPHIQUE =====
|
||||
|
||||
Scénario: Option "Autour de moi" - Rayon 50 km
|
||||
Étant donné que je suis à Paris (position GPS détectée)
|
||||
Quand je sélectionne "Télécharger > Autour de moi"
|
||||
Alors l'app recherche tous les contenus géolocalisés dans un rayon de 50 km
|
||||
Et je vois une liste de contenus de Paris et banlieue proche
|
||||
Et l'estimation affiche "~150 contenus disponibles"
|
||||
|
||||
Scénario: Option "Ma ville" - Limite administrative détectée
|
||||
Étant donné que je suis à Lyon (position GPS détectée)
|
||||
Quand je sélectionne "Télécharger > Ma ville"
|
||||
Alors l'app détecte automatiquement "Lyon" comme ville
|
||||
Et recherche tous les contenus géolocalisés "Lyon"
|
||||
Et je vois uniquement les contenus de la ville de Lyon (pas banlieue)
|
||||
|
||||
Scénario: Option "Mon département" - Sélection dans liste
|
||||
Étant donné que je veux télécharger des contenus pour un département
|
||||
Quand je sélectionne "Télécharger > Mon département"
|
||||
Alors je vois une liste de tous les départements français:
|
||||
| département |
|
||||
| 01 - Ain |
|
||||
| 02 - Aisne |
|
||||
| 75 - Paris |
|
||||
| 69 - Rhône |
|
||||
| ... |
|
||||
Et je peux choisir un département
|
||||
|
||||
Scénario: Sélection département et téléchargement contenus
|
||||
Étant donné que je sélectionne "75 - Paris" dans la liste des départements
|
||||
Quand la sélection est confirmée
|
||||
Alors l'app recherche tous les contenus géolocalisés "Paris"
|
||||
Et je vois "~234 contenus disponibles pour Paris"
|
||||
|
||||
Scénario: Option "Ma région" - Sélection dans liste
|
||||
Étant donné que je veux télécharger des contenus pour une région
|
||||
Quand je sélectionne "Télécharger > Ma région"
|
||||
Alors je vois une liste de toutes les régions françaises:
|
||||
| région |
|
||||
| Auvergne-Rhône-Alpes |
|
||||
| Bretagne |
|
||||
| Île-de-France |
|
||||
| Nouvelle-Aquitaine |
|
||||
| Occitanie |
|
||||
| ... |
|
||||
Et je peux choisir une région
|
||||
|
||||
Scénario: Sélection région et téléchargement contenus
|
||||
Étant donné que je sélectionne "Bretagne" dans la liste des régions
|
||||
Quand la sélection est confirmée
|
||||
Alors l'app recherche tous les contenus géolocalisés des départements bretons:
|
||||
| département |
|
||||
| Côtes-d'Armor (22) |
|
||||
| Finistère (29) |
|
||||
| Ille-et-Vilaine (35) |
|
||||
| Morbihan (56) |
|
||||
Et je vois "~487 contenus disponibles pour Bretagne"
|
||||
|
||||
Scénario: Recherche manuelle ville
|
||||
Étant donné que je veux télécharger des contenus pour une ville spécifique
|
||||
Quand je tape "Marseille" dans la barre de recherche
|
||||
Alors l'app propose des suggestions:
|
||||
| suggestion |
|
||||
| Marseille (13) |
|
||||
| Marseille-en-Beauvaisis |
|
||||
Et je peux sélectionner "Marseille (13)"
|
||||
|
||||
Scénario: Recherche manuelle avec autocomplétion
|
||||
Étant donné que je tape "Ly" dans la barre de recherche
|
||||
Quand l'autocomplétion s'active
|
||||
Alors je vois des suggestions:
|
||||
| suggestion |
|
||||
| Lyon (69) |
|
||||
| Lys-lez-Lannoy |
|
||||
Et je peux affiner ma recherche
|
||||
|
||||
# ===== LIMITES TÉLÉCHARGEMENT =====
|
||||
|
||||
Scénario: Utilisateur gratuit - Limite 50 contenus max
|
||||
Étant donné que je suis un utilisateur gratuit
|
||||
Et que j'ai déjà téléchargé 45 contenus
|
||||
Quand j'accède à la page Téléchargements
|
||||
Alors je vois "45 / 50 contenus téléchargés"
|
||||
Et je peux télécharger 5 contenus supplémentaires maximum
|
||||
|
||||
Scénario: Utilisateur gratuit - Tentative dépasser limite 50
|
||||
Étant donné que je suis gratuit et j'ai déjà 50 contenus téléchargés
|
||||
Quand j'essaie de télécharger un 51ème contenu
|
||||
Alors le téléchargement est refusé
|
||||
Et je vois le message:
|
||||
"""
|
||||
📥 Limite atteinte (50 contenus)
|
||||
|
||||
Vous avez atteint la limite de téléchargements gratuits.
|
||||
|
||||
Options:
|
||||
• Supprimez des contenus existants pour en télécharger de nouveaux
|
||||
• Passez Premium pour des téléchargements illimités
|
||||
|
||||
[Gérer mes téléchargements] [Découvrir Premium]
|
||||
"""
|
||||
|
||||
Scénario: Utilisateur Premium - Téléchargements illimités
|
||||
Étant donné que je suis un utilisateur Premium
|
||||
Et que j'ai déjà téléchargé 245 contenus
|
||||
Quand j'accède à la page Téléchargements
|
||||
Alors je vois "245 contenus (3.2 GB)"
|
||||
Et aucune limite n'est affichée
|
||||
Et je peux télécharger autant de contenus que je veux
|
||||
|
||||
Scénario: Limite Premium = Espace disque disponible
|
||||
Étant donné que je suis Premium
|
||||
Et que mon device a 500 MB d'espace disque disponible
|
||||
Quand j'essaie de télécharger 100 contenus (2 GB)
|
||||
Alors le téléchargement échoue après ~50 contenus (500 MB)
|
||||
Et je vois "Espace disque insuffisant. Libérez de l'espace pour continuer."
|
||||
|
||||
Scénario: Calcul temps écoute disponible gratuit
|
||||
Étant donné que je suis gratuit avec 50 contenus téléchargés
|
||||
Et que la durée moyenne d'un contenu est 5 minutes
|
||||
Quand je calcule le temps d'écoute disponible
|
||||
Alors 50 contenus × 5 min = 250 minutes = 4h10 d'écoute
|
||||
Et cela suffit pour un trajet quotidien ou road trip court
|
||||
|
||||
Scénario: Calcul temps écoute disponible Premium illimité
|
||||
Étant donné que je suis Premium avec 300 contenus téléchargés
|
||||
Et que la durée moyenne est 5 minutes
|
||||
Quand je calcule le temps d'écoute disponible
|
||||
Alors 300 contenus × 5 min = 1500 minutes = 25h d'écoute
|
||||
Et cela suffit pour un road trip de plusieurs jours
|
||||
|
||||
# ===== CONNEXION WIFI / MOBILE =====
|
||||
|
||||
Scénario: Téléchargement par défaut en WiFi uniquement
|
||||
Étant donné que je suis connecté en WiFi
|
||||
Quand je clique sur "Télécharger 20 contenus"
|
||||
Alors le téléchargement démarre immédiatement
|
||||
Et aucune popup de confirmation n'apparaît
|
||||
|
||||
Scénario: Tentative téléchargement en données mobiles - Popup confirmation
|
||||
Étant donné que je suis connecté en 4G (pas de WiFi)
|
||||
Quand je clique sur "Télécharger 20 contenus"
|
||||
Alors une popup apparaît:
|
||||
"""
|
||||
📡 Vous n'êtes pas connecté en WiFi
|
||||
|
||||
Télécharger via données mobiles consommera environ 72 MB.
|
||||
|
||||
[Attendre WiFi] [Continuer quand même]
|
||||
"""
|
||||
|
||||
Scénario: Calcul estimation consommation data mobile
|
||||
Étant donné que je veux télécharger 20 contenus
|
||||
Et que la durée moyenne est 5 minutes
|
||||
Et que la qualité Standard est 48 kbps Opus
|
||||
Quand l'estimation est calculée
|
||||
Alors consommation = 20 contenus × 5 min × 48 kbps / 8 = 72 MB
|
||||
Et ce montant est affiché dans la popup
|
||||
|
||||
Scénario: Confirmation téléchargement en données mobiles
|
||||
Étant donné que je vois la popup de confirmation données mobiles
|
||||
Quand je clique sur "Continuer quand même"
|
||||
Alors le téléchargement démarre immédiatement via 4G
|
||||
Et la consommation data est comptabilisée sur mon forfait mobile
|
||||
|
||||
Scénario: Refus téléchargement données mobiles - Attendre WiFi
|
||||
Étant donné que je vois la popup de confirmation données mobiles
|
||||
Quand je clique sur "Attendre WiFi"
|
||||
Alors les téléchargements sont mis en file d'attente
|
||||
Et ils démarreront automatiquement quand le WiFi sera détecté
|
||||
|
||||
Scénario: Détection automatique WiFi et reprise téléchargements
|
||||
Étant donné que j'ai mis 20 contenus en file d'attente (attente WiFi)
|
||||
Quand l'app détecte une connexion WiFi
|
||||
Alors les téléchargements démarrent automatiquement
|
||||
Et je reçois une notification "Téléchargements en cours via WiFi"
|
||||
|
||||
# ===== QUALITÉ AUDIO =====
|
||||
|
||||
Scénario: Qualité Standard (48 kbps) par défaut
|
||||
Étant donné que je configure mes téléchargements
|
||||
Quand j'accède aux paramètres de qualité
|
||||
Alors la qualité "Standard (48 kbps - ~20 MB/h)" est sélectionnée par défaut
|
||||
Et elle est disponible pour tous (gratuit + Premium)
|
||||
|
||||
Scénario: Qualité Basse (24 kbps) disponible pour tous
|
||||
Étant donné que j'ai peu d'espace disque disponible
|
||||
Quand je sélectionne qualité "Basse (24 kbps - ~10 MB/h)"
|
||||
Alors mes prochains téléchargements seront en 24 kbps
|
||||
Et l'espace utilisé sera divisé par 2 par rapport à Standard
|
||||
Et cette option est disponible pour gratuit + Premium
|
||||
|
||||
Scénario: Qualité Haute (64 kbps) réservée Premium
|
||||
Étant donné que je suis un utilisateur gratuit
|
||||
Quand je consulte les options de qualité
|
||||
Alors l'option "Haute (64 kbps - ~30 MB/h)" est grisée
|
||||
Et je vois "👑 Premium uniquement"
|
||||
Et je ne peux pas la sélectionner
|
||||
|
||||
Scénario: Utilisateur Premium peut choisir qualité Haute
|
||||
Étant donné que je suis un utilisateur Premium
|
||||
Quand je consulte les options de qualité
|
||||
Alors l'option "Haute (64 kbps - ~30 MB/h)" est disponible
|
||||
Et je peux la sélectionner pour mes téléchargements
|
||||
Et la qualité audio sera excellente (meilleure restitution voix et ambiances)
|
||||
|
||||
Scénario: Comparaison taille fichiers selon qualité
|
||||
Étant donné que je veux télécharger 50 contenus de 5 min chacun
|
||||
Quand je compare les qualités
|
||||
Alors les tailles totales sont:
|
||||
| qualité | bitrate | taille totale |
|
||||
| Basse | 24 kbps | ~250 MB |
|
||||
| Standard | 48 kbps | ~500 MB |
|
||||
| Haute | 64 kbps | ~650 MB |
|
||||
|
||||
Scénario: Justification Standard = Bon compromis
|
||||
Étant donné que le contenu RoadWave est principalement de la voix
|
||||
Quand la qualité Standard (48 kbps Opus) est utilisée
|
||||
Alors la qualité est très correcte pour la voix
|
||||
Et équivalente à la radio FM
|
||||
Et le compromis qualité/taille est optimal
|
||||
|
||||
Scénario: Justification Haute réservée Premium = Incitation upgrade
|
||||
Étant donné qu'un utilisateur gratuit veut la meilleure qualité
|
||||
Quand il voit que Haute est réservée Premium
|
||||
Alors cela l'incite à passer Premium pour 4.99€/mois
|
||||
Et c'est un avantage tangible supplémentaire de Premium
|
||||
|
||||
Scénario: Changement qualité après téléchargements existants
|
||||
Étant donné que j'ai déjà téléchargé 30 contenus en qualité Standard
|
||||
Quand je change la qualité vers Haute (si Premium)
|
||||
Alors les 30 contenus existants restent en Standard
|
||||
Et seuls les nouveaux téléchargements seront en Haute
|
||||
Et je peux manuellement re-télécharger les 30 contenus pour les avoir en Haute
|
||||
|
||||
# ===== PROCESSUS DE TÉLÉCHARGEMENT =====
|
||||
|
||||
Scénario: Téléchargement individuel d'un contenu
|
||||
Étant donné que je consulte la page d'un contenu
|
||||
Quand je clique sur l'icône de téléchargement 📥
|
||||
Alors le téléchargement démarre
|
||||
Et une barre de progression apparaît
|
||||
Et l'icône devient ✅ quand terminé
|
||||
|
||||
Scénario: Téléchargement batch de contenus sélectionnés
|
||||
Étant donné que je consulte une liste de contenus pour "Paris"
|
||||
Quand je sélectionne 15 contenus manuellement
|
||||
Et que je clique sur "Télécharger la sélection"
|
||||
Alors les 15 contenus sont téléchargés en parallèle (max 3 simultanés)
|
||||
Et une notification affiche "15 contenus téléchargés"
|
||||
|
||||
Scénario: Téléchargement automatique recommandations zone
|
||||
Étant donné que je sélectionne "Autour de moi" (Paris)
|
||||
Quand je clique sur "Télécharger les 50 meilleurs contenus"
|
||||
Alors l'algorithme sélectionne automatiquement les 50 contenus les mieux notés/récents
|
||||
Et les télécharge tous
|
||||
Et je n'ai pas besoin de choisir manuellement
|
||||
|
||||
Scénario: Barre de progression téléchargement global
|
||||
Étant donné que je télécharge 20 contenus
|
||||
Quand les téléchargements sont en cours
|
||||
Alors je vois une barre de progression globale:
|
||||
"""
|
||||
📥 Téléchargement en cours...
|
||||
7 / 20 contenus (35%)
|
||||
~45 MB restants
|
||||
Temps estimé: 2 min
|
||||
"""
|
||||
|
||||
Scénario: Téléchargements en tâche de fond
|
||||
Étant donné que je lance le téléchargement de 30 contenus
|
||||
Quand je ferme l'app ou passe à une autre activité
|
||||
Alors les téléchargements continuent en arrière-plan
|
||||
Et je reçois une notification quand tous sont terminés
|
||||
|
||||
Scénario: Pause et reprise téléchargements
|
||||
Étant donné que je télécharge 20 contenus
|
||||
Quand je clique sur "Pause"
|
||||
Alors les téléchargements en cours se terminent
|
||||
Et les téléchargements en attente sont mis en pause
|
||||
Et je peux cliquer sur "Reprendre" plus tard
|
||||
|
||||
Scénario: Annulation téléchargements
|
||||
Étant donné que je télécharge 20 contenus
|
||||
Quand je clique sur "Annuler"
|
||||
Alors tous les téléchargements sont arrêtés
|
||||
Et les fichiers partiels sont supprimés
|
||||
Et l'espace disque est libéré
|
||||
|
||||
Scénario: Gestion erreurs téléchargement
|
||||
Étant donné que je télécharge un contenu
|
||||
Mais que la connexion Internet coupe au milieu
|
||||
Quand la connexion revient
|
||||
Alors le téléchargement reprend automatiquement où il s'était arrêté
|
||||
Et aucune perte de progression n'a lieu
|
||||
|
||||
Scénario: Retry automatique après échec
|
||||
Étant donné qu'un téléchargement échoue 3 fois consécutives
|
||||
Quand l'échec est détecté
|
||||
Alors le contenu est marqué "Échec"
|
||||
Et je vois une notification "3 contenus n'ont pas pu être téléchargés"
|
||||
Et je peux retry manuellement en cliquant sur "Réessayer"
|
||||
|
||||
# ===== GESTION CONTENUS TÉLÉCHARGÉS =====
|
||||
|
||||
Scénario: Liste contenus téléchargés
|
||||
Étant donné que j'ai téléchargé 45 contenus
|
||||
Quand j'accède à "Téléchargements"
|
||||
Alors je vois la liste complète de mes 45 contenus
|
||||
Et pour chaque contenu: titre, créateur, durée, taille, date téléchargement
|
||||
|
||||
Scénario: Tri contenus téléchargés
|
||||
Étant donné que je consulte ma liste de téléchargements
|
||||
Quand je clique sur "Trier par"
|
||||
Alors je peux trier par:
|
||||
| critère | ordre |
|
||||
| Date téléchargement | Plus récent / Plus ancien|
|
||||
| Titre | A-Z / Z-A |
|
||||
| Créateur | A-Z / Z-A |
|
||||
| Durée | Plus long / Plus court |
|
||||
| Taille | Plus gros / Plus petit |
|
||||
|
||||
Scénario: Recherche dans contenus téléchargés
|
||||
Étant donné que j'ai 200 contenus téléchargés
|
||||
Quand je tape "Tesla" dans la barre de recherche
|
||||
Alors seuls les contenus contenant "Tesla" s'affichent
|
||||
Et je peux rapidement trouver un contenu spécifique
|
||||
|
||||
Scénario: Suppression individuelle contenu téléchargé
|
||||
Étant donné que je veux supprimer un contenu téléchargé
|
||||
Quand je swipe left (iOS) ou long press (Android) sur le contenu
|
||||
Et que je clique sur "Supprimer"
|
||||
Alors le fichier est supprimé du device
|
||||
Et l'espace disque est libéré
|
||||
Et le compteur est décrémenté (ex: 45/50 → 44/50)
|
||||
|
||||
Scénario: Suppression batch contenus téléchargés
|
||||
Étant donné que je veux supprimer plusieurs contenus
|
||||
Quand je sélectionne 10 contenus
|
||||
Et que je clique sur "Supprimer la sélection"
|
||||
Alors les 10 fichiers sont supprimés
|
||||
Et ~100 MB d'espace disque sont libérés
|
||||
Et une notification confirme "10 contenus supprimés"
|
||||
|
||||
Scénario: Suppression tous les contenus téléchargés
|
||||
Étant donné que j'ai 45 contenus téléchargés
|
||||
Quand je clique sur "Supprimer tout"
|
||||
Et que je confirme l'action
|
||||
Alors tous les 45 contenus sont supprimés
|
||||
Et l'espace disque total est libéré (~450 MB)
|
||||
Et le compteur repasse à 0/50
|
||||
|
||||
Scénario: Espace disque utilisé visible
|
||||
Étant donné que j'ai téléchargé 45 contenus
|
||||
Quand j'accède à la page Téléchargements
|
||||
Alors je vois l'espace disque utilisé:
|
||||
"""
|
||||
📥 Téléchargements
|
||||
45 / 50 contenus
|
||||
Espace utilisé: 478 MB
|
||||
"""
|
||||
|
||||
Scénario: Statistiques téléchargements
|
||||
Étant donné que j'accède à mes statistiques
|
||||
Quand je consulte la section Téléchargements
|
||||
Alors je vois:
|
||||
| métrique | valeur |
|
||||
| Contenus actuellement téléchargés | 45 |
|
||||
| Espace disque utilisé | 478 MB |
|
||||
| Contenus téléchargés depuis début | 287 |
|
||||
| Total data téléchargée | 3.2 GB |
|
||||
| Téléchargements via WiFi | 92% |
|
||||
| Téléchargements via mobile | 8% |
|
||||
|
||||
# ===== LECTURE OFFLINE =====
|
||||
|
||||
Scénario: Lecture contenu téléchargé sans connexion
|
||||
Étant donné que je n'ai aucune connexion Internet (mode avion)
|
||||
Et que j'ai des contenus téléchargés
|
||||
Quand je lance un contenu téléchargé
|
||||
Alors la lecture démarre normalement depuis le fichier local
|
||||
Et aucune erreur de connexion n'apparaît
|
||||
|
||||
Scénario: Badge "Téléchargé" sur contenus offline
|
||||
Étant donné que j'ai téléchargé certains contenus
|
||||
Quand je consulte une liste de contenus
|
||||
Alors les contenus téléchargés ont un badge ✅ "Offline"
|
||||
Et je sais immédiatement lesquels sont disponibles sans connexion
|
||||
|
||||
Scénario: Filtre "Téléchargés uniquement"
|
||||
Étant donné que je veux voir uniquement mes contenus offline
|
||||
Quand j'active le filtre "Téléchargés uniquement"
|
||||
Alors seuls les contenus téléchargés s'affichent
|
||||
Et je peux facilement naviguer dans mon catalogue offline
|
||||
|
||||
Scénario: Playlist offline automatique
|
||||
Étant donné que j'ai téléchargé 45 contenus
|
||||
Quand j'accède à "Téléchargements"
|
||||
Alors je peux lancer une playlist aléatoire de mes 45 contenus
|
||||
Et profiter d'une écoute continue offline
|
||||
335
features/ui/mode-offline/validite-renouvellement.feature
Normal file
335
features/ui/mode-offline/validite-renouvellement.feature
Normal file
@@ -0,0 +1,335 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Validité et renouvellement contenus offline
|
||||
En tant qu'utilisateur
|
||||
Je veux que mes contenus téléchargés restent valides un certain temps
|
||||
Afin de garantir la légalité et la fraîcheur du contenu
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis connecté à l'application RoadWave
|
||||
Et que j'ai des contenus téléchargés
|
||||
|
||||
# ===== DURÉE DE VALIDITÉ =====
|
||||
|
||||
Scénario: Validité de 30 jours après téléchargement
|
||||
Étant donné que je télécharge un contenu le 1er juin 2025
|
||||
Quand le téléchargement est terminé
|
||||
Alors le contenu est valide jusqu'au 1er juillet 2025 (30 jours)
|
||||
Et la date d'expiration est stockée en local
|
||||
|
||||
Scénario: Affichage date expiration sur contenu téléchargé
|
||||
Étant donné que j'ai téléchargé un contenu il y a 20 jours
|
||||
Quand je consulte les détails du contenu
|
||||
Alors je vois "Expire dans 10 jours"
|
||||
Et je sais combien de temps il reste avant expiration
|
||||
|
||||
Scénario: Standard industrie aligné (Spotify, YouTube, Deezer)
|
||||
Étant donné que Spotify, YouTube Music et Deezer utilisent 30 jours
|
||||
Quand RoadWave fixe également 30 jours
|
||||
Alors c'est le standard accepté par les utilisateurs
|
||||
Et il n'y a pas de confusion avec les autres plateformes
|
||||
|
||||
Scénario: Justification 30 jours - Force reconnexion régulière
|
||||
Étant donné qu'un utilisateur ne se connecte jamais
|
||||
Quand ses contenus expirent après 30 jours
|
||||
Alors il est obligé de se reconnecter pour les renouveler
|
||||
Et le système peut vérifier:
|
||||
| vérification |
|
||||
| Abonnement Premium toujours actif|
|
||||
| Contenus non modérés/supprimés |
|
||||
| Métadonnées à jour |
|
||||
|
||||
Scénario: Justification 30 jours - Évite stockage obsolète
|
||||
Étant donné qu'un contenu a été modéré après téléchargement
|
||||
Quand le contenu expire après 30 jours maximum
|
||||
Alors le contenu illégal est automatiquement supprimé
|
||||
Et ne reste pas indéfiniment sur le device
|
||||
|
||||
# ===== RENOUVELLEMENT AUTOMATIQUE =====
|
||||
|
||||
Scénario: Détection WiFi et contenus >25 jours
|
||||
Étant donné que j'ai des contenus téléchargés il y a 26 jours
|
||||
Quand l'app détecte une connexion WiFi
|
||||
Alors une requête GET /offline/contents/refresh est envoyée
|
||||
Et le backend vérifie chaque contenu
|
||||
|
||||
Scénario: Vérification abonnement Premium toujours actif
|
||||
Étant donné qu'un contenu téléchargé en Premium est à renouveler
|
||||
Quand le backend vérifie le statut
|
||||
Et que l'abonnement Premium est toujours actif
|
||||
Alors la validité est renouvelée à 30 jours supplémentaires
|
||||
|
||||
Scénario: Abonnement Premium expiré - Contenu non renouvelé
|
||||
Étant donné qu'un contenu Premium téléchargé est à renouveler
|
||||
Quand le backend vérifie le statut
|
||||
Et que l'abonnement Premium a expiré
|
||||
Alors le contenu n'est pas renouvelé
|
||||
Et il sera supprimé à l'expiration (J-0)
|
||||
Et l'utilisateur voit "Contenu Premium expiré (abonnement inactif)"
|
||||
|
||||
Scénario: Vérification contenu pas modéré/supprimé
|
||||
Étant donné qu'un contenu téléchargé est à renouveler
|
||||
Quand le backend vérifie le statut
|
||||
Et que le contenu a été modéré ou supprimé entre temps
|
||||
Alors le contenu n'est pas renouvelé
|
||||
Et sera supprimé immédiatement du device
|
||||
Et l'utilisateur voit "1 contenu retiré (violation règles)"
|
||||
|
||||
Scénario: Mise à jour métadonnées lors du renouvellement
|
||||
Étant donné qu'un contenu téléchargé est renouvelé
|
||||
Quand le backend traite le renouvellement
|
||||
Alors les métadonnées sont mises à jour:
|
||||
| métadonnée | mise à jour si changée |
|
||||
| Titre | ✅ |
|
||||
| Nom créateur | ✅ |
|
||||
| Description | ✅ |
|
||||
| Tags | ✅ |
|
||||
| Statut Premium | ✅ |
|
||||
Et l'utilisateur voit les infos à jour
|
||||
|
||||
Scénario: Pas de re-téléchargement audio si fichier OK
|
||||
Étant donné qu'un contenu est renouvelé
|
||||
Quand le fichier audio local est intact
|
||||
Alors seules les métadonnées sont mises à jour
|
||||
Et le fichier audio n'est pas re-téléchargé
|
||||
Et cela économise la bande passante
|
||||
|
||||
Scénario: Re-téléchargement audio si fichier corrompu
|
||||
Étant donné qu'un contenu est renouvelé
|
||||
Quand le fichier audio local est corrompu (checksum invalide)
|
||||
Alors le fichier audio est re-téléchargé entièrement
|
||||
Et le nouveau fichier remplace le corrompu
|
||||
|
||||
Scénario: Renouvellement silencieux si WiFi régulier
|
||||
Étant donné que je me connecte en WiFi tous les jours
|
||||
Quand mes contenus atteignent 25-30 jours
|
||||
Alors ils sont automatiquement renouvelés en arrière-plan
|
||||
Et je ne vois aucune notification (processus transparent)
|
||||
Et mes contenus restent valides indéfiniment
|
||||
|
||||
Scénario: Renouvellement batch de plusieurs contenus
|
||||
Étant donné que j'ai 30 contenus à renouveler
|
||||
Quand le renouvellement automatique se déclenche
|
||||
Alors une requête batch est envoyée:
|
||||
```json
|
||||
POST /offline/contents/refresh
|
||||
{
|
||||
"content_ids": ["abc123", "def456", "ghi789", ...]
|
||||
}
|
||||
```
|
||||
Et le backend traite les 30 contenus en une seule requête
|
||||
Et cela économise les requêtes HTTP
|
||||
|
||||
Scénario: Temps de traitement renouvellement
|
||||
Étant donné que 30 contenus sont à renouveler
|
||||
Quand la requête batch est traitée
|
||||
Alors le backend répond en <2 secondes
|
||||
Et les métadonnées sont mises à jour localement
|
||||
Et l'utilisateur ne remarque aucun ralentissement
|
||||
|
||||
# ===== NOTIFICATIONS EXPIRATION =====
|
||||
|
||||
Scénario: Notification J-3 avant expiration
|
||||
Étant donné que j'ai 15 contenus qui expirent dans 3 jours
|
||||
Quand le système vérifie les expirations
|
||||
Alors je reçois une notification:
|
||||
"""
|
||||
⚠️ 15 contenus expirent dans 3 jours
|
||||
|
||||
Connectez-vous en WiFi pour les renouveler automatiquement.
|
||||
"""
|
||||
Et je peux agir avant l'expiration
|
||||
|
||||
Scénario: Pas de notification si connexion WiFi régulière
|
||||
Étant donné que je me connecte en WiFi tous les jours
|
||||
Et que mes contenus sont automatiquement renouvelés
|
||||
Quand le système vérifie les expirations
|
||||
Alors aucune notification J-3 n'est envoyée
|
||||
Car les contenus sont déjà renouvelés silencieusement
|
||||
|
||||
Scénario: Notification uniquement si contenus non renouvelés
|
||||
Étant donné que j'ai 20 contenus dont 15 renouvelés et 5 non renouvelés
|
||||
Quand le J-3 arrive pour les 5 non renouvelés
|
||||
Alors je reçois "5 contenus expirent dans 3 jours"
|
||||
Et seuls les contenus à risque sont mentionnés
|
||||
|
||||
Scénario: Action utilisateur après notification J-3
|
||||
Étant donné que je reçois la notification J-3
|
||||
Quand je clique sur la notification
|
||||
Alors l'app s'ouvre sur la page Téléchargements
|
||||
Et je vois les contenus qui vont expirer en rouge
|
||||
Et je peux me connecter en WiFi pour les renouveler
|
||||
|
||||
Scénario: Suppression automatique J-0 (expiration)
|
||||
Étant donné qu'un contenu n'a pas été renouvelé
|
||||
Quand le jour d'expiration arrive (J-0)
|
||||
Alors le fichier est automatiquement supprimé du device
|
||||
Et l'espace disque est libéré
|
||||
Et le compteur est décrémenté (ex: 45/50 → 44/50)
|
||||
|
||||
Scénario: Toast après suppression automatique J-0
|
||||
Étant donné que 15 contenus viennent d'expirer
|
||||
Quand l'utilisateur ouvre l'app
|
||||
Alors il voit un toast:
|
||||
"""
|
||||
🗑️ 15 contenus expirés ont été supprimés
|
||||
|
||||
Reconnectez-vous en WiFi régulièrement pour éviter les expirations.
|
||||
"""
|
||||
|
||||
Scénario: Liste contenus supprimés après expiration
|
||||
Étant donné que 15 contenus ont expiré
|
||||
Quand je consulte l'historique des suppressions
|
||||
Alors je vois la liste des 15 contenus supprimés:
|
||||
| titre | créateur | date expiration |
|
||||
| Mon épisode préféré | JeanDupont | 15 juin 2025 |
|
||||
| Road trip Bretagne | MarieLambert| 15 juin 2025 |
|
||||
| ... | ... | ... |
|
||||
Et je peux les re-télécharger si je veux
|
||||
|
||||
Scénario: Re-téléchargement après expiration
|
||||
Étant donné qu'un contenu a expiré et été supprimé
|
||||
Quand je retrouve ce contenu dans l'app
|
||||
Alors le badge ✅ "Offline" n'est plus affiché
|
||||
Et je peux le re-télécharger normalement
|
||||
Et la validité repart à 30 jours
|
||||
|
||||
# ===== CAS PARTICULIERS =====
|
||||
|
||||
Scénario: Utilisateur ne se connecte jamais pendant 30 jours
|
||||
Étant donné que je télécharge 50 contenus le 1er juin
|
||||
Mais que je ne me connecte jamais en WiFi pendant 30 jours
|
||||
Quand le 1er juillet arrive
|
||||
Alors tous les 50 contenus expirent
|
||||
Et sont automatiquement supprimés
|
||||
Et je n'ai plus aucun contenu offline
|
||||
|
||||
Scénario: Utilisateur en zone blanche 30+ jours
|
||||
Étant donné que je télécharge 50 contenus avant de partir en zone sans réseau
|
||||
Et que je reste 45 jours sans connexion
|
||||
Quand les contenus expirent après 30 jours
|
||||
Alors ils sont supprimés même si je ne peux pas me connecter
|
||||
Et je perds l'accès à mes contenus offline
|
||||
|
||||
Scénario: Recommandation téléchargement avant zone blanche longue
|
||||
Étant donné que je prépare un road trip de 60 jours
|
||||
Quand je consulte la FAQ
|
||||
Alors je vois la recommandation:
|
||||
"""
|
||||
⚠️ Road trips >30 jours
|
||||
|
||||
Les contenus téléchargés expirent après 30 jours.
|
||||
Pour les longs voyages sans connexion:
|
||||
• Téléchargez de nouveaux contenus tous les 25 jours si possible
|
||||
• Ou planifiez une reconnexion WiFi tous les 25 jours
|
||||
"""
|
||||
|
||||
Scénario: Changement statut Premium en gratuit pendant validité
|
||||
Étant donné que je suis Premium et j'ai téléchargé 200 contenus
|
||||
Quand mon abonnement Premium expire
|
||||
Et que je repasse en gratuit
|
||||
Alors au prochain renouvellement, seulement 50 contenus sont conservés
|
||||
Et les 150 autres sont supprimés (limite gratuit)
|
||||
Et je vois "Limite gratuit (50 contenus) appliquée. 150 contenus supprimés."
|
||||
|
||||
Scénario: Sélection automatique 50 meilleurs contenus si passage gratuit
|
||||
Étant donné que je repasse en gratuit avec 200 contenus téléchargés
|
||||
Quand le système applique la limite de 50
|
||||
Alors les 50 contenus les plus récemment écoutés sont conservés
|
||||
Et les 150 autres sont supprimés
|
||||
Et cela maximise les chances de garder les contenus que j'aime
|
||||
|
||||
Scénario: Contenus Premium exclusifs supprimés si abonnement expire
|
||||
Étant donné que j'ai téléchargé 20 contenus Premium exclusifs
|
||||
Quand mon abonnement Premium expire
|
||||
Alors les 20 contenus Premium sont immédiatement supprimés
|
||||
Car ils ne sont accessibles qu'aux abonnés Premium actifs
|
||||
Et je vois "20 contenus Premium supprimés (abonnement expiré)"
|
||||
|
||||
# ===== STATISTIQUES ET MONITORING =====
|
||||
|
||||
Scénario: Affichage temps restant avant expiration
|
||||
Étant donné que j'ai 45 contenus téléchargés
|
||||
Quand je consulte la page Téléchargements
|
||||
Alors je vois pour chaque contenu:
|
||||
| contenu | temps restant |
|
||||
| Mon épisode (récent)| Expire dans 28 jours |
|
||||
| Road trip (ancien) | Expire dans 3 jours |
|
||||
Et je sais lesquels sont prioritaires pour renouvellement
|
||||
|
||||
Scénario: Tri par date expiration
|
||||
Étant donné que j'ai 45 contenus avec différentes dates d'expiration
|
||||
Quand je trie par "Expiration"
|
||||
Alors les contenus qui expirent le plus tôt apparaissent en premier
|
||||
Et je peux voir rapidement lesquels nécessitent une reconnexion urgente
|
||||
|
||||
Scénario: Badge rouge si expiration <3 jours
|
||||
Étant donné qu'un contenu expire dans 2 jours
|
||||
Quand je consulte la liste des téléchargements
|
||||
Alors le contenu a un badge rouge "⚠️ Expire bientôt"
|
||||
Et il est visuellement mis en avant
|
||||
|
||||
Scénario: Statistiques utilisateur - Taux de renouvellement
|
||||
Étant donné que j'accède à mes statistiques
|
||||
Quand je consulte la section Téléchargements
|
||||
Alors je vois:
|
||||
| métrique | valeur |
|
||||
| Contenus actuels | 45 |
|
||||
| Contenus expirés depuis début | 87 |
|
||||
| Contenus renouvelés (auto) | 234 |
|
||||
| Taux renouvellement automatique | 73% |
|
||||
|
||||
Scénario: Statistiques admin - Taux expiration global
|
||||
Étant donné qu'un admin consulte les métriques offline
|
||||
Quand il accède au dashboard
|
||||
Alors il voit:
|
||||
| métrique | valeur |
|
||||
| Contenus téléchargés actifs | 1,234,567 |
|
||||
| Expirations ce mois | 45,678 |
|
||||
| Taux expiration | 3.7% |
|
||||
| Renouvellements automatiques/mois | 234,567 |
|
||||
|
||||
Scénario: Alerte admin si taux expiration >10%
|
||||
Étant donné que le taux d'expiration mensuel dépasse 10%
|
||||
Quand le système détecte cette anomalie
|
||||
Alors une alerte est envoyée:
|
||||
"""
|
||||
⚠️ Taux d'expiration anormal: 12.3%
|
||||
|
||||
Nombre expirations ce mois: 152,345
|
||||
Causes possibles:
|
||||
- Utilisateurs ne se connectent plus en WiFi
|
||||
- Problème renouvellement automatique ?
|
||||
- Churn utilisateurs augmenté ?
|
||||
|
||||
Action recommandée: Enquête technique + email rappel utilisateurs
|
||||
"""
|
||||
|
||||
Scénario: Email rappel si pas de connexion WiFi depuis 20 jours
|
||||
Étant donné que je n'ai pas connecté l'app en WiFi depuis 20 jours
|
||||
Et que j'ai 45 contenus téléchargés
|
||||
Quand le système détecte cette inactivité WiFi
|
||||
Alors je reçois un email:
|
||||
"""
|
||||
📡 Connectez-vous en WiFi pour conserver vos téléchargements
|
||||
|
||||
Vous n'avez pas connecté RoadWave en WiFi depuis 20 jours.
|
||||
Vos 45 contenus téléchargés expireront dans 10 jours si non renouvelés.
|
||||
|
||||
Connectez-vous en WiFi avant le 30 juin pour les renouveler automatiquement.
|
||||
"""
|
||||
|
||||
Scénario: Performance renouvellement avec 10 000 utilisateurs simultanés
|
||||
Étant donné que 10 000 utilisateurs se connectent en WiFi simultanément
|
||||
Quand chacun demande le renouvellement de 50 contenus
|
||||
Alors le serveur traite 500 000 vérifications
|
||||
Et grâce au cache Redis et index PostgreSQL, le temps de réponse reste <3s
|
||||
Et les serveurs gèrent la charge sans problème
|
||||
|
||||
Scénario: Logs audit renouvellements
|
||||
Étant donné qu'un contenu est renouvelé
|
||||
Quand l'opération se termine
|
||||
Alors un log est enregistré:
|
||||
| timestamp | user_id | content_id | action | résultat |
|
||||
| 2025-06-15 14:30:00 | abc123 | xyz789 | renew | success (+30d) |
|
||||
| 2025-06-15 14:30:01 | abc123 | def456 | renew | failed (deleted)|
|
||||
Et ces logs aident à débugger les problèmes
|
||||
195
features/ui/navigation/actions-mode-pieton.feature
Normal file
195
features/ui/navigation/actions-mode-pieton.feature
Normal file
@@ -0,0 +1,195 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Actions complémentaires - Mode piéton
|
||||
En tant qu'auditeur en mode piéton
|
||||
Je veux accéder à des actions avancées depuis l'application mobile
|
||||
Afin de liker explicitement, m'abonner ou signaler du contenu
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et qu'un utilisateur est connecté
|
||||
Et qu'il est en mode piéton (vitesse < 5 km/h)
|
||||
|
||||
Scénario: Like explicite avec bouton cœur
|
||||
Étant donné que j'écoute un contenu tagué "Automobile"
|
||||
Et que ma jauge "Automobile" est à 60%
|
||||
Quand je clique sur le bouton cœur "Like"
|
||||
Alors ma jauge "Automobile" augmente de 2%
|
||||
Et une animation de cœur rouge s'affiche
|
||||
Et une vibration courte est déclenchée
|
||||
Et ma jauge "Automobile" est maintenant à 62%
|
||||
|
||||
Scénario: Like explicite cumulable avec like automatique
|
||||
Étant donné que j'ai écouté un contenu "Voyage" à 85%
|
||||
Et que j'ai reçu un like automatique renforcé (+2%)
|
||||
Et que ma jauge "Voyage" est à 52%
|
||||
Quand je clique sur le bouton cœur "Like"
|
||||
Alors ma jauge "Voyage" augmente encore de 2%
|
||||
Et ma jauge "Voyage" passe à 54%
|
||||
Et les deux likes sont cumulés
|
||||
|
||||
Scénario: Unlike retire le like manuel uniquement
|
||||
Étant donné que j'ai liké manuellement un contenu "Sport"
|
||||
Et que ma jauge "Sport" est à 57%
|
||||
Quand je clique à nouveau sur le bouton cœur (toggle)
|
||||
Alors le cœur redevient vide (unlike)
|
||||
Et ma jauge "Sport" diminue de 2%
|
||||
Et ma jauge "Sport" revient à 55%
|
||||
|
||||
Scénario: Unlike ne retire pas le like automatique
|
||||
Étant donné que j'ai écouté un contenu "Musique" à 90%
|
||||
Et que j'ai reçu un like automatique renforcé (+2%)
|
||||
Et que ma jauge "Musique" est à 52%
|
||||
Et que je n'ai PAS liké manuellement
|
||||
Quand je consulte l'interface
|
||||
Alors le bouton "Unlike" n'est pas disponible
|
||||
Et le cœur reste grisé (aucun like manuel)
|
||||
Et ma jauge reste à 52%
|
||||
|
||||
Scénario: Abonnement à un créateur
|
||||
Étant donné qu'un créateur publie des contenus tagués "Automobile" et "Technologie"
|
||||
Et que mes jauges sont:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 50% |
|
||||
| Technologie | 45% |
|
||||
Quand je clique sur "S'abonner" sur le profil du créateur
|
||||
Alors ma jauge "Automobile" augmente de 5%
|
||||
Et ma jauge "Technologie" augmente de 5%
|
||||
Et une animation d'étoile dorée s'affiche
|
||||
Et un badge "Abonné ✓" apparaît sur le profil
|
||||
Et mes nouvelles jauges sont:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 55% |
|
||||
| Technologie | 50% |
|
||||
|
||||
Scénario: Désabonnement d'un créateur
|
||||
Étant donné que je suis abonné à un créateur
|
||||
Et que mes jauges "Automobile" et "Technologie" sont à 55% et 50%
|
||||
Quand je clique sur "Se désabonner"
|
||||
Alors ma jauge "Automobile" diminue de 5%
|
||||
Et ma jauge "Technologie" diminue de 5%
|
||||
Et le badge "Abonné ✓" disparaît
|
||||
Et mes nouvelles jauges sont:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 50% |
|
||||
| Technologie | 45% |
|
||||
|
||||
Scénario: Signalement d'un contenu inapproprié
|
||||
Étant donné que j'écoute un contenu
|
||||
Quand je clique sur le menu contextuel "⋮"
|
||||
Et que je sélectionne "Signaler"
|
||||
Alors un formulaire de signalement s'ouvre
|
||||
Et je dois sélectionner une catégorie:
|
||||
| Catégorie |
|
||||
| Haine et violence |
|
||||
| Contenu sexuel |
|
||||
| Illégalité |
|
||||
| Droits d'auteur |
|
||||
| Spam |
|
||||
| Désinformation (fake news) |
|
||||
| Autre |
|
||||
Et je peux ajouter un commentaire optionnel
|
||||
Et le signalement est envoyé au flux de modération
|
||||
|
||||
Scénario: Feedback visuel pour like explicite
|
||||
Étant donné que je clique sur le bouton cœur
|
||||
Quand le like est enregistré
|
||||
Alors une animation de cœur rouge se lance (0.5s)
|
||||
Et le cœur reste rouge plein
|
||||
Et une vibration haptique courte est déclenchée (iOS: .light, Android: 50ms)
|
||||
Et un badge "♥ Ajouté à vos favoris" s'affiche 2 secondes
|
||||
|
||||
Scénario: Feedback visuel pour abonnement
|
||||
Étant donné que je clique sur "S'abonner"
|
||||
Quand l'abonnement est enregistré
|
||||
Alors une animation d'étoile dorée se lance (0.8s)
|
||||
Et le bouton devient "Abonné ✓" avec badge doré
|
||||
Et une notification "Abonné à [Créateur]" s'affiche
|
||||
Et les contenus du créateur seront boostés +30% dans l'algo
|
||||
|
||||
Scénario: Menu contextuel avec toutes les options
|
||||
Étant donné que j'utilise l'app en mode piéton
|
||||
Quand je clique sur le menu "⋮" (3 points verticaux)
|
||||
Alors les options disponibles sont:
|
||||
| Option |
|
||||
| Like (cœur) |
|
||||
| S'abonner au créateur |
|
||||
| Signaler |
|
||||
| Partager |
|
||||
| Voir le profil du créateur |
|
||||
| Télécharger (mode offline) |
|
||||
Et toutes les options sont cliquables
|
||||
|
||||
Scénario: Persistance des likes manuels en base de données
|
||||
Étant donné que je like manuellement 5 contenus
|
||||
Quand je ferme l'application
|
||||
Et que je me reconnecte plus tard
|
||||
Alors tous mes likes manuels sont toujours présents
|
||||
Et les cœurs rouges sont affichés sur les contenus likés
|
||||
Et mes jauges reflètent toujours l'impact (+2% × 5 likes)
|
||||
|
||||
Scénario: Liste "Mes contenus likés" accessible dans profil
|
||||
Étant donné que j'ai liké manuellement 10 contenus
|
||||
Quand j'accède à mon profil utilisateur
|
||||
Alors je vois une section "❤️ Mes favoris"
|
||||
Et la liste affiche les 10 contenus likés
|
||||
Et je peux cliquer pour réécouter
|
||||
Et je peux retirer un like (unlike) depuis cette liste
|
||||
|
||||
Scénario: Liste "Mes abonnements" accessible dans profil
|
||||
Étant donné que je suis abonné à 5 créateurs
|
||||
Quand j'accède à mon profil utilisateur
|
||||
Alors je vois une section "⭐ Mes abonnements"
|
||||
Et la liste affiche les 5 créateurs avec leurs avatars
|
||||
Et je peux accéder au profil de chaque créateur
|
||||
Et je peux me désabonner depuis cette liste
|
||||
|
||||
Scénario: Impact abonnement sur tous les tags du créateur
|
||||
Étant donné qu'un créateur a publié des contenus avec ces tags:
|
||||
| Contenu | Tags |
|
||||
| C1 | Automobile, Voyage |
|
||||
| C2 | Automobile, Technologie |
|
||||
| C3 | Voyage, Famille |
|
||||
Et que mes jauges sont toutes à 50%
|
||||
Quand je m'abonne à ce créateur
|
||||
Alors les jauges impactées sont:
|
||||
| Tag | Impact |
|
||||
| Automobile | +5% |
|
||||
| Voyage | +5% |
|
||||
| Technologie | +5% |
|
||||
| Famille | +5% |
|
||||
Et toutes les autres jauges restent à 50%
|
||||
|
||||
Scénario: Limite d'abonnements (200 maximum)
|
||||
Étant donné que je suis abonné à 200 créateurs
|
||||
Quand j'essaie de m'abonner à un 201ème créateur
|
||||
Alors un message "Limite de 200 abonnements atteinte" s'affiche
|
||||
Et je dois me désabonner d'un créateur existant pour en ajouter un nouveau
|
||||
|
||||
Scénario: Confirmation avant désabonnement
|
||||
Étant donné que je suis abonné à un créateur
|
||||
Quand je clique sur "Se désabonner"
|
||||
Alors une popup de confirmation s'affiche:
|
||||
"""
|
||||
Se désabonner de @CreateurNom ?
|
||||
|
||||
Vous ne recevrez plus de notifications pour ses contenus.
|
||||
Vos jauges diminueront de 5%.
|
||||
"""
|
||||
Et je dois confirmer pour valider
|
||||
Et je peux annuler pour conserver l'abonnement
|
||||
|
||||
Plan du Scénario: Cumul like automatique + like manuel
|
||||
Étant donné qu'un contenu est tagué "Sport"
|
||||
Et que ma jauge "Sport" est à 50%
|
||||
Quand j'écoute à <pourcentage>% (like auto <auto>)
|
||||
Et que je like manuellement (+2%)
|
||||
Alors l'impact total est <total>
|
||||
Et ma nouvelle jauge est <nouveau_niveau>
|
||||
|
||||
Exemples:
|
||||
| pourcentage | auto | total | nouveau_niveau |
|
||||
| 10 | 0 | +2% | 52% |
|
||||
| 30 | +1% | +3% | 53% |
|
||||
| 50 | +1% | +3% | 53% |
|
||||
| 80 | +2% | +4% | 54% |
|
||||
| 95 | +2% | +4% | 54% |
|
||||
177
features/ui/navigation/commande-precedent.feature
Normal file
177
features/ui/navigation/commande-precedent.feature
Normal file
@@ -0,0 +1,177 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Commande "Précédent"
|
||||
En tant qu'auditeur
|
||||
Je veux que le bouton "Précédent" ait un comportement intelligent
|
||||
Afin de rejouer le contenu actuel ou revenir au précédent selon la progression
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et qu'un utilisateur est connecté
|
||||
|
||||
Scénario: Précédent après <10s revient au contenu précédent
|
||||
Étant donné que j'ai écouté le contenu "A" pendant 2 minutes
|
||||
Et que j'écoute maintenant le contenu "B" depuis 5 secondes
|
||||
Quand j'appuie sur "Précédent"
|
||||
Alors la lecture revient au contenu "A"
|
||||
Et la position de lecture est à 2 minutes (position exacte sauvegardée)
|
||||
Et le contenu "B" reste en historique
|
||||
|
||||
Scénario: Précédent après ≥10s rejoue le contenu actuel
|
||||
Étant donné que j'écoute le contenu "C" depuis 15 secondes
|
||||
Quand j'appuie sur "Précédent"
|
||||
Alors le contenu "C" rejoue depuis le début (position 0:00)
|
||||
Et la lecture ne revient pas au contenu précédent
|
||||
Et la progress bar revient à 0%
|
||||
|
||||
Scénario: Précédent exactement à 10s rejoue le contenu actuel
|
||||
Étant donné que j'écoute le contenu "D" depuis exactement 10 secondes
|
||||
Quand j'appuie sur "Précédent"
|
||||
Alors le contenu "D" rejoue depuis le début
|
||||
Et la lecture ne revient pas au contenu précédent
|
||||
|
||||
Scénario: Précédent sur le premier contenu de session
|
||||
Étant donné que je viens de démarrer l'application
|
||||
Et que j'écoute le contenu "Premier" depuis 3 secondes
|
||||
Quand j'appuie sur "Précédent"
|
||||
Alors le contenu "Premier" rejoue depuis le début
|
||||
Et aucun contenu précédent n'existe
|
||||
|
||||
Scénario: Historique de navigation limité à 10 contenus
|
||||
Étant donné que j'ai écouté 10 contenus [C1, C2, ..., C10]
|
||||
Et que l'historique Redis contient 10 entrées
|
||||
Quand je passe au contenu C11
|
||||
Alors le contenu C1 est supprimé de l'historique (FIFO)
|
||||
Et l'historique contient [C2, C3, ..., C10, C11]
|
||||
Et la taille reste à 10 contenus maximum
|
||||
|
||||
Scénario: Position exacte sauvegardée dans l'historique
|
||||
Étant donné que j'écoute le contenu "A" (durée 5 minutes)
|
||||
Quand j'atteins 2 minutes 30 secondes
|
||||
Et que j'appuie sur "Suivant"
|
||||
Alors l'historique enregistre:
|
||||
| content_id | position_seconds | listened_at |
|
||||
| A | 150 | 2026-01-21T10:30:00 |
|
||||
Quand je reviens au contenu "A" via "Précédent"
|
||||
Alors la lecture reprend exactement à 2 minutes 30 secondes
|
||||
|
||||
Scénario: Navigation arrière sur plusieurs contenus
|
||||
Étant donné que j'ai écouté dans l'ordre: A (2min), B (30s), C (3min)
|
||||
Et que j'écoute maintenant D depuis 1 seconde
|
||||
Quand j'appuie sur "Précédent" (1ère fois)
|
||||
Alors je reviens au contenu C à la position 3 minutes
|
||||
Quand j'appuie sur "Précédent" (<10s sur C)
|
||||
Alors je reviens au contenu B à la position 30 secondes
|
||||
Quand j'appuie sur "Précédent" (<10s sur B)
|
||||
Alors je reviens au contenu A à la position 2 minutes
|
||||
|
||||
Scénario: Précédent après milieu du contenu rejoue depuis début
|
||||
Étant donné que j'écoute un contenu de 5 minutes
|
||||
Quand j'atteins 2 minutes 30 secondes (milieu)
|
||||
Et que j'appuie sur "Précédent"
|
||||
Alors le contenu actuel rejoue depuis 0:00
|
||||
Et je ne reviens pas au contenu précédent
|
||||
|
||||
Scénario: Enchaînement Suivant puis Précédent rapide
|
||||
Étant donné que j'écoute le contenu "A" depuis 1 minute
|
||||
Quand j'appuie sur "Suivant"
|
||||
Alors le contenu "B" démarre
|
||||
Quand j'appuie immédiatement sur "Précédent" (2s après)
|
||||
Alors je reviens au contenu "A" à la position 1 minute
|
||||
Et le contenu "B" reste dans l'historique
|
||||
|
||||
Scénario: Transition fluide avec animation 0.3s
|
||||
Étant donné que j'appuie sur "Précédent"
|
||||
Quand le changement de contenu se produit
|
||||
Alors la transition audio utilise un fade out/in de 0.3 secondes
|
||||
Et la progress bar revient avec une animation fluide
|
||||
Et l'interface ne montre aucun message de confirmation
|
||||
|
||||
Scénario: Historique survit au changement de réseau
|
||||
Étant donné que j'ai un historique de 5 contenus en cache Redis
|
||||
Quand je perds la connexion réseau temporairement
|
||||
Et que je reviens en ligne
|
||||
Alors l'historique de navigation est toujours disponible
|
||||
Et je peux toujours utiliser "Précédent"
|
||||
|
||||
Scénario: Historique stocké en Redis avec structure complète
|
||||
Étant donné que j'ai écouté 3 contenus
|
||||
Quand je consulte le cache Redis
|
||||
Alors la structure est:
|
||||
"""
|
||||
user:{user_id}:history = [
|
||||
{content_id: "C3", position_seconds: 45, listened_at: "2026-01-21T10:33:00Z"},
|
||||
{content_id: "C2", position_seconds: 120, listened_at: "2026-01-21T10:30:00Z"},
|
||||
{content_id: "C1", position_seconds: 180, listened_at: "2026-01-21T10:27:00Z"}
|
||||
]
|
||||
"""
|
||||
Et l'ordre est du plus récent au plus ancien
|
||||
|
||||
Scénario: Précédent sur contenu en cours au début (<10s) du premier
|
||||
Étant donné que je démarre une session avec le contenu "Initial"
|
||||
Et que j'écoute depuis 3 secondes
|
||||
Quand j'appuie sur "Précédent"
|
||||
Alors le contenu "Initial" rejoue depuis le début
|
||||
Et aucune erreur n'est générée
|
||||
Et l'historique reste vide
|
||||
|
||||
Scénario: Compteur de temps respecte les seuils exacts
|
||||
Étant donné que j'écoute un contenu
|
||||
Quand le temps écoulé est de 9.9 secondes
|
||||
Et que j'appuie sur "Précédent"
|
||||
Alors je reviens au contenu précédent
|
||||
Quand le temps écoulé est de 10.0 secondes
|
||||
Et que j'appuie sur "Précédent"
|
||||
Alors le contenu actuel rejoue depuis le début
|
||||
|
||||
Scénario: Progress bar visuelle reflète le retour exact
|
||||
Étant donné que j'ai écouté le contenu "A" jusqu'à 75% (3min45 sur 5min)
|
||||
Et que je suis passé au contenu "B"
|
||||
Quand je reviens au contenu "A" via "Précédent"
|
||||
Alors la progress bar affiche 75%
|
||||
Et l'indicateur de temps affiche "3:45 / 5:00"
|
||||
Et la lecture reprend exactement à cet endroit
|
||||
|
||||
Scénario: Métadonnées d'historique incluent timestamp précis
|
||||
Étant donné que j'écoute un contenu "X" pendant 45 secondes à 10:30:15
|
||||
Quand je passe au contenu suivant
|
||||
Alors l'historique enregistre:
|
||||
| content_id | position_seconds | listened_at |
|
||||
| X | 45 | 2026-01-21T10:30:15Z |
|
||||
Et le timestamp précis permet l'analyse d'usage
|
||||
|
||||
Scénario: Suppression FIFO respecte l'ordre chronologique
|
||||
Étant donné un historique de [C1@10:00, C2@10:02, ..., C10@10:20]
|
||||
Quand j'ajoute C11 à 10:22
|
||||
Alors C1 (le plus ancien) est supprimé
|
||||
Et l'historique contient [C2@10:02, ..., C11@10:22]
|
||||
Et la taille reste exactement 10 entrées
|
||||
|
||||
Plan du Scénario: Comportement selon temps écouté
|
||||
Étant donné que j'écoute un contenu depuis <temps> secondes
|
||||
Quand j'appuie sur "Précédent"
|
||||
Alors l'action est <comportement>
|
||||
|
||||
Exemples:
|
||||
| temps | comportement |
|
||||
| 1 | revenir au contenu précédent |
|
||||
| 5 | revenir au contenu précédent |
|
||||
| 9 | revenir au contenu précédent |
|
||||
| 10 | rejouer le contenu actuel depuis 0:00 |
|
||||
| 11 | rejouer le contenu actuel depuis 0:00 |
|
||||
| 30 | rejouer le contenu actuel depuis 0:00 |
|
||||
| 180 | rejouer le contenu actuel depuis 0:00 |
|
||||
|
||||
Plan du Scénario: Positions de reprise exactes
|
||||
Étant donné que j'écoute un contenu de 10 minutes
|
||||
Quand j'atteins <position> et passe au suivant
|
||||
Et que je reviens via "Précédent"
|
||||
Alors la lecture reprend exactement à <position>
|
||||
|
||||
Exemples:
|
||||
| position |
|
||||
| 0:15 |
|
||||
| 1:30 |
|
||||
| 3:45 |
|
||||
| 5:00 |
|
||||
| 7:23 |
|
||||
| 9:50 |
|
||||
238
features/ui/navigation/commandes-vocales.feature
Normal file
238
features/ui/navigation/commandes-vocales.feature
Normal file
@@ -0,0 +1,238 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Commandes vocales CarPlay et Android Auto
|
||||
En tant que conducteur avec CarPlay ou Android Auto
|
||||
Je veux utiliser des commandes vocales pour interagir avec l'application
|
||||
Afin de garder les mains sur le volant et les yeux sur la route
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et qu'un utilisateur est connecté
|
||||
Et que CarPlay ou Android Auto est activé
|
||||
|
||||
Scénario: Disponibilité des commandes vocales uniquement avec CarPlay/Android Auto
|
||||
Étant donné que je conduis avec CarPlay activé
|
||||
Quand je dis "Hey Siri"
|
||||
Alors Siri est disponible pour les commandes RoadWave
|
||||
Étant donné que je conduis avec Android Auto activé
|
||||
Quand je dis "OK Google"
|
||||
Alors Google Assistant est disponible pour les commandes RoadWave
|
||||
|
||||
Scénario: Parc automobile compatible avec vocal (30-40% en 2026)
|
||||
Étant donné que nous sommes en 2026
|
||||
Quand je consulte les statistiques du parc automobile EU
|
||||
Alors environ 30-40% des véhicules ont CarPlay ou Android Auto
|
||||
Et ces utilisateurs peuvent utiliser les commandes vocales
|
||||
Et les 60-70% restants utilisent les commandes au volant uniquement
|
||||
|
||||
Scénario: Commande vocale "Like ce podcast" avec Siri
|
||||
Étant donné que j'écoute un contenu tagué "Automobile"
|
||||
Et que ma jauge "Automobile" est à 60%
|
||||
Quand je dis "Hey Siri, like ce podcast"
|
||||
Alors un like explicite (+2%) est enregistré
|
||||
Et ma jauge "Automobile" passe à 62%
|
||||
Et Siri confirme vocalement "J'ai ajouté ce contenu à vos favoris"
|
||||
Et aucune interaction écran n'est requise
|
||||
|
||||
Scénario: Commande vocale "Like ce contenu" avec Google Assistant
|
||||
Étant donné que j'écoute un contenu tagué "Voyage"
|
||||
Quand je dis "OK Google, like ce contenu"
|
||||
Alors un like explicite est enregistré (+2%)
|
||||
Et Google Assistant confirme "J'ai liké ce contenu pour vous"
|
||||
Et la commande fonctionne sans toucher l'écran
|
||||
|
||||
Scénario: Commande vocale "Abonne-moi à ce créateur"
|
||||
Étant donné que j'écoute un contenu d'un créateur tagué "Automobile" et "Technologie"
|
||||
Et que mes jauges sont à 50% et 45%
|
||||
Quand je dis "Hey Siri, abonne-moi à ce créateur"
|
||||
Alors l'abonnement est enregistré
|
||||
Et mes jauges augmentent de 5% chacune (55% et 50%)
|
||||
Et Siri confirme "Vous êtes maintenant abonné à [Nom du créateur]"
|
||||
|
||||
Scénario: Commande vocale "Passe au contenu suivant"
|
||||
Étant donné que j'écoute un contenu "A"
|
||||
Quand je dis "Hey Siri, passe au contenu suivant"
|
||||
Alors le contenu "B" démarre immédiatement
|
||||
Et la commande a le même effet que le bouton physique "Suivant"
|
||||
|
||||
Scénario: Commande vocale "Signale ce contenu"
|
||||
Étant donné que j'écoute un contenu inapproprié
|
||||
Quand je dis "OK Google, signale ce contenu"
|
||||
Alors Google Assistant demande "Quelle catégorie ?"
|
||||
Et je réponds vocalement "Spam"
|
||||
Alors le signalement est enregistré avec la catégorie "Spam"
|
||||
Et Google Assistant confirme "J'ai signalé ce contenu"
|
||||
|
||||
Scénario: Commande vocale avec catégorie de signalement
|
||||
Étant donné que j'écoute un contenu
|
||||
Quand je dis "Hey Siri, signale ce contenu pour haine"
|
||||
Alors le signalement est enregistré avec la catégorie "Haine et violence"
|
||||
Et Siri confirme "J'ai signalé ce contenu pour haine et violence"
|
||||
Et le flux de modération reçoit le signalement
|
||||
|
||||
Scénario: Liste des catégories de signalement vocales supportées
|
||||
Étant donné que je dis "signale ce contenu pour [catégorie]"
|
||||
Quand la catégorie est:
|
||||
| Mot-clé vocal | Catégorie mappée |
|
||||
| "haine" | Haine et violence |
|
||||
| "sexuel" | Contenu sexuel |
|
||||
| "illégalité" | Illégalité |
|
||||
| "droits d'auteur" | Droits d'auteur |
|
||||
| "spam" | Spam |
|
||||
| "fake news" | Désinformation |
|
||||
| "autre" | Autre |
|
||||
Alors le signalement est enregistré avec la bonne catégorie
|
||||
|
||||
Scénario: Commande vocale non reconnue - fallback
|
||||
Étant donné que je dis "Hey Siri, super ce podcast"
|
||||
Quand Siri ne reconnaît pas l'intent RoadWave
|
||||
Alors Siri répond "Je ne comprends pas cette commande RoadWave"
|
||||
Et elle suggère "Dites 'like ce podcast' ou 'passe au suivant'"
|
||||
|
||||
Scénario: Commandes vocales disponibles en conduite uniquement
|
||||
Étant donné que je roule à 50 km/h
|
||||
Quand j'utilise les commandes vocales
|
||||
Alors toutes les commandes sont disponibles:
|
||||
| Commande | Action |
|
||||
| "Like ce podcast" | Like explicite +2% |
|
||||
| "Abonne-moi à ce créateur" | Abonnement +5% |
|
||||
| "Passe au suivant" | Contenu suivant |
|
||||
| "Reviens au précédent" | Contenu précédent (règle 10s) |
|
||||
| "Pause" | Pause lecture |
|
||||
| "Reprends la lecture" | Play |
|
||||
| "Signale ce contenu" | Signalement |
|
||||
|
||||
Scénario: Intent iOS personnalisé pour RoadWave
|
||||
Étant donné que l'app iOS implémente les Intents
|
||||
Quand je configure les Shortcuts iOS
|
||||
Alors les intents suivants sont disponibles:
|
||||
| Intent Name | Action |
|
||||
| LikeCurrentContentIntent | Like explicite |
|
||||
| SubscribeToCreatorIntent | Abonnement |
|
||||
| ReportContentIntent | Signalement |
|
||||
| SkipToNextContentIntent | Suivant |
|
||||
Et Siri les reconnaît automatiquement
|
||||
|
||||
Scénario: Intent Android personnalisé pour RoadWave
|
||||
Étant donné que l'app Android implémente les Voice Actions
|
||||
Quand je configure les actions Google Assistant
|
||||
Alors les actions suivantes sont disponibles:
|
||||
| Action Name | Action |
|
||||
| com.roadwave.LIKE_CONTENT | Like explicite |
|
||||
| com.roadwave.SUBSCRIBE_CREATOR | Abonnement |
|
||||
| com.roadwave.REPORT_CONTENT | Signalement |
|
||||
| com.roadwave.SKIP_NEXT | Suivant |
|
||||
Et Google Assistant les reconnaît
|
||||
|
||||
Scénario: Confirmation vocale après action réussie
|
||||
Étant donné que je dis "Hey Siri, like ce podcast"
|
||||
Quand l'action est enregistrée avec succès
|
||||
Alors Siri répond immédiatement avec confirmation:
|
||||
"""
|
||||
J'ai ajouté ce contenu à vos favoris
|
||||
"""
|
||||
Et la réponse est naturelle et concise
|
||||
Et elle ne distrait pas de la conduite
|
||||
|
||||
Scénario: Gestion d'erreur vocale si action échoue
|
||||
Étant donné que je dis "Hey Siri, abonne-moi à ce créateur"
|
||||
Et que j'ai atteint la limite de 200 abonnements
|
||||
Quand Siri essaie d'enregistrer l'abonnement
|
||||
Alors l'action échoue
|
||||
Et Siri répond "Impossible de s'abonner, limite de 200 abonnements atteinte"
|
||||
Et elle suggère "Désabonnez-vous d'un créateur pour continuer"
|
||||
|
||||
Scénario: Commandes vocales multilingues (français)
|
||||
Étant donné que mon Siri est configuré en français
|
||||
Quand je dis "Hey Siri, j'aime ce podcast"
|
||||
Alors la commande est reconnue (variante de "like ce podcast")
|
||||
Quand je dis "Hey Siri, mets une étoile"
|
||||
Alors la commande est reconnue (variante de "like")
|
||||
|
||||
Scénario: Implémentation post-MVP (Sprint 5)
|
||||
Étant donné que les commandes vocales sont une feature Sprint 5
|
||||
Quand le MVP est lancé
|
||||
Alors seules les commandes au volant physiques sont disponibles
|
||||
Quand le Sprint 5 est déployé
|
||||
Alors les intents iOS/Android sont activés
|
||||
Et les commandes vocales deviennent disponibles
|
||||
|
||||
Scénario: Priorisation commandes vocales vs boutons physiques
|
||||
Étant donné que je conduis avec CarPlay
|
||||
Et que j'ai accès aux boutons physiques ET aux commandes vocales
|
||||
Quand je veux liker un contenu
|
||||
Alors je peux soit:
|
||||
- Attendre l'arrêt et cliquer le bouton cœur (recommandé)
|
||||
- Dire "Hey Siri, like ce podcast" (en conduite)
|
||||
- Laisser le like automatique se faire (écoute ≥80%)
|
||||
Et les 3 méthodes sont valides
|
||||
|
||||
Scénario: Statistiques d'usage des commandes vocales
|
||||
Étant donné que 100 utilisateurs avec CarPlay utilisent RoadWave
|
||||
Quand je consulte les analytics
|
||||
Alors je peux voir:
|
||||
| Métrique | Exemple valeur |
|
||||
| Taux d'utilisation commandes vocal | 15% |
|
||||
| Commande la plus utilisée | "Like" |
|
||||
| Taux de reconnaissance réussie | 92% |
|
||||
| Taux d'échec / incompréhension | 8% |
|
||||
|
||||
Scénario: Feedback haptique désactivé pour commandes vocales
|
||||
Étant donné que je like un contenu via commande vocale
|
||||
Quand l'action est enregistrée
|
||||
Alors aucune vibration haptique n'est déclenchée
|
||||
Et seule la confirmation vocale est donnée
|
||||
Car je n'ai pas le téléphone en main
|
||||
|
||||
Scénario: Badge visuel mis à jour après commande vocale
|
||||
Étant donné que je dis "Hey Siri, like ce podcast"
|
||||
Quand l'action est enregistrée
|
||||
Alors le badge "♥ Ajouté à vos favoris" s'affiche sur l'écran CarPlay
|
||||
Et le cœur devient rouge plein dans l'interface
|
||||
Et la mise à jour est visible même sans toucher l'écran
|
||||
|
||||
Scénario: Commandes vocales avec contenu sans créateur
|
||||
Étant donné que j'écoute un contenu anonyme (créateur supprimé)
|
||||
Quand je dis "Hey Siri, abonne-moi à ce créateur"
|
||||
Alors Siri répond "Ce créateur n'est plus disponible"
|
||||
Et aucun abonnement n'est enregistré
|
||||
|
||||
Scénario: Limitation temporelle des commandes vocales
|
||||
Étant donné que je dis "Hey Siri, like ce podcast"
|
||||
Et que le contenu change 1 seconde après
|
||||
Quand Siri traite la commande 2 secondes plus tard
|
||||
Alors la commande s'applique au contenu qui était en lecture au moment de la commande
|
||||
Et non au contenu actuel (système de timestamp)
|
||||
|
||||
Plan du Scénario: Commandes vocales avec différents assistants
|
||||
Étant donné que j'utilise <assistant>
|
||||
Quand je dis <commande>
|
||||
Alors l'action <action> est exécutée
|
||||
Et la confirmation est <confirmation>
|
||||
|
||||
Exemples:
|
||||
| assistant | commande | action | confirmation |
|
||||
| Siri | "Like ce podcast" | Like +2% | "Ajouté à vos favoris" |
|
||||
| Google Assistant | "Like ce contenu" | Like +2% | "J'ai liké ce contenu" |
|
||||
| Siri | "Abonne-moi à ce créateur" | Abonnement +5% | "Vous êtes abonné" |
|
||||
| Google Assistant | "Abonne-moi à ce créateur" | Abonnement +5% | "Abonnement enregistré" |
|
||||
| Siri | "Signale ce contenu" | Signalement | "J'ai signalé ce contenu" |
|
||||
| Google Assistant | "Signale ce contenu" | Signalement | "Contenu signalé" |
|
||||
|
||||
Plan du Scénario: Mapping catégories signalement vocal
|
||||
Étant donné que je dis "signale ce contenu pour <mot_cle>"
|
||||
Quand <mot_cle> est reconnu
|
||||
Alors la catégorie mappée est <categorie>
|
||||
|
||||
Exemples:
|
||||
| mot_cle | categorie |
|
||||
| haine | Haine et violence |
|
||||
| violence | Haine et violence |
|
||||
| sexuel | Contenu sexuel |
|
||||
| porno | Contenu sexuel |
|
||||
| illégal | Illégalité |
|
||||
| terrorisme | Illégalité |
|
||||
| copyright | Droits d'auteur |
|
||||
| droits auteur | Droits d'auteur |
|
||||
| spam | Spam |
|
||||
| fake news | Désinformation |
|
||||
| fausse info | Désinformation |
|
||||
205
features/ui/navigation/commandes-volant.feature
Normal file
205
features/ui/navigation/commandes-volant.feature
Normal file
@@ -0,0 +1,205 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Commandes au volant et interactions simplifiées
|
||||
En tant que conducteur en sécurité
|
||||
Je veux utiliser uniquement les commandes simplifiées au volant
|
||||
Afin de naviguer sans distraction et en toute sécurité
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et qu'un utilisateur est connecté
|
||||
Et que l'application est connectée via CarPlay ou Android Auto
|
||||
|
||||
Scénario: Trois commandes disponibles au volant uniquement
|
||||
Étant donné que je conduis à 50 km/h
|
||||
Quand je consulte les commandes physiques disponibles
|
||||
Alors seules 3 actions sont disponibles:
|
||||
| Commande | Action |
|
||||
| Suivant | Passer au contenu suivant |
|
||||
| Précédent | Revenir au précédent (règle 10s) |
|
||||
| Play/Pause | Pause/reprise avec fade 0.3s |
|
||||
Et aucune commande complexe n'est proposée
|
||||
|
||||
Scénario: Commande "Suivant" au volant
|
||||
Étant donné que j'écoute un contenu "A"
|
||||
Quand j'appuie sur le bouton physique "Suivant" au volant
|
||||
Alors le contenu "B" démarre immédiatement
|
||||
Et aucune action supplémentaire n'est requise
|
||||
Et l'interface ne demande aucune confirmation
|
||||
|
||||
Scénario: Commande "Précédent" au volant respecte règle 10s
|
||||
Étant donné que j'écoute un contenu depuis 5 secondes
|
||||
Quand j'appuie sur "Précédent" au volant
|
||||
Alors je reviens au contenu précédent (règle <10s)
|
||||
Étant donné que j'écoute un contenu depuis 15 secondes
|
||||
Quand j'appuie sur "Précédent" au volant
|
||||
Alors le contenu actuel rejoue depuis le début (règle ≥10s)
|
||||
|
||||
Scénario: Commande "Play/Pause" avec fade audio
|
||||
Étant donné qu'un contenu est en lecture
|
||||
Quand j'appuie sur "Pause" au volant
|
||||
Alors la lecture se met en pause avec un fade out de 0.3 secondes
|
||||
Et la position de lecture est sauvegardée
|
||||
Quand j'appuie sur "Play" au volant
|
||||
Alors la lecture reprend avec un fade in de 0.3 secondes
|
||||
Et la reprise se fait à la position exacte
|
||||
|
||||
Scénario: Aucune commande complexe supportée
|
||||
Étant donné que je conduis
|
||||
Quand j'essaie un appui long sur "Suivant"
|
||||
Alors l'action n'est pas détectée (non supporté iOS/Android)
|
||||
Quand j'essaie un double-appui sur "Pause"
|
||||
Alors l'action n'est pas détectée
|
||||
Et seules les actions simples (clic simple) fonctionnent
|
||||
|
||||
Scénario: Compatibilité 100% tous véhicules
|
||||
Étant donné que je conduis une voiture avec commandes basiques
|
||||
Et que mon véhicule a seulement Suivant/Précédent/Pause
|
||||
Quand j'utilise RoadWave
|
||||
Alors toutes les fonctions essentielles sont accessibles
|
||||
Et je n'ai pas besoin de boutons supplémentaires
|
||||
|
||||
Scénario: Feedback visuel discret après action
|
||||
Étant donné que j'appuie sur "Suivant"
|
||||
Quand le contenu change
|
||||
Alors l'interface CarPlay/Android Auto affiche le nouveau titre
|
||||
Et aucune popup ne bloque la vue
|
||||
Et le changement est fluide et immédiat
|
||||
|
||||
Scénario: Like automatique renforcé après écoute ≥80%
|
||||
Étant donné que j'écoute un contenu de 5 minutes tagué "Automobile"
|
||||
Quand j'écoute pendant 4 minutes 30 secondes (90%)
|
||||
Alors un like automatique renforcé (+2 points) est enregistré
|
||||
Et un badge discret "♥ Ajouté à vos favoris" s'affiche 2 secondes
|
||||
Et aucune action manuelle n'est requise
|
||||
|
||||
Scénario: Like automatique standard après écoute 30-79%
|
||||
Étant donné que j'écoute un contenu de 5 minutes tagué "Voyage"
|
||||
Quand j'écoute pendant 2 minutes (40%)
|
||||
Et que j'appuie sur "Suivant"
|
||||
Alors un like automatique standard (+1 point) est enregistré
|
||||
Et un badge discret s'affiche brièvement
|
||||
Et je peux continuer à conduire sans interruption
|
||||
|
||||
Scénario: Signal négatif après skip rapide <10s
|
||||
Étant donné que j'écoute un contenu tagué "Politique"
|
||||
Quand j'appuie sur "Suivant" après seulement 5 secondes
|
||||
Alors un signal négatif (-0.5 point) est enregistré
|
||||
Et la jauge "Politique" diminue légèrement
|
||||
Et aucun message n'est affiché (transparence)
|
||||
|
||||
Scénario: Pas de like si écoute <30%
|
||||
Étant donné que j'écoute un contenu de 10 minutes
|
||||
Quand j'écoute pendant 2 minutes (20%)
|
||||
Et que j'appuie sur "Suivant"
|
||||
Alors aucun like n'est enregistré
|
||||
Et les jauges ne changent pas
|
||||
Et le système considère l'écoute comme neutre
|
||||
|
||||
Scénario: Badge de feedback visuel disparaît après 2 secondes
|
||||
Étant donné que je reçois un like automatique
|
||||
Quand le badge "♥ Ajouté à vos favoris" apparaît
|
||||
Alors il reste visible 2 secondes en bas de l'écran
|
||||
Et il disparaît automatiquement sans action
|
||||
Et il ne bloque pas la vue de la route
|
||||
|
||||
Scénario: Tracking du temps d'écoute précis côté client
|
||||
Étant donné que je démarre la lecture d'un contenu
|
||||
Quand le player audio iOS/Android enregistre le temps
|
||||
Alors le startTime est enregistré à la milliseconde
|
||||
Quand j'arrête la lecture (Suivant, Pause, ou fin)
|
||||
Alors la durée exacte écoutée est calculée
|
||||
Et le pourcentage (durée / durée_totale * 100) est envoyé à l'API
|
||||
|
||||
Scénario: API reçoit les événements d'écoute pour calcul
|
||||
Étant donné que j'écoute un contenu de 5 minutes à 80%
|
||||
Quand l'événement est envoyé à l'API
|
||||
Alors le backend reçoit:
|
||||
"""
|
||||
{
|
||||
"content_id": "abc123",
|
||||
"duration": 240,
|
||||
"content_total": 300,
|
||||
"percentage": 80.0,
|
||||
"action": "completed"
|
||||
}
|
||||
"""
|
||||
Et le backend calcule le like automatique (+2 points)
|
||||
Et les jauges sont mises à jour immédiatement (Redis + PostgreSQL)
|
||||
|
||||
Scénario: Actions différentes selon arrêt du contenu
|
||||
Étant donné que j'écoute un contenu
|
||||
Quand j'appuie sur "Suivant"
|
||||
Alors l'action envoyée est "skipped"
|
||||
Quand le contenu se termine naturellement
|
||||
Alors l'action envoyée est "completed"
|
||||
Quand j'appuie sur "Pause"
|
||||
Alors l'action envoyée est "paused"
|
||||
Et le backend traite chaque action différemment
|
||||
|
||||
Scénario: Calcul immédiat côté backend sans délai
|
||||
Étant donné que l'API reçoit un événement d'écoute
|
||||
Quand le backend traite l'événement
|
||||
Alors les jauges sont mises à jour immédiatement (< 100ms)
|
||||
Et les nouvelles recommandations utilisent les valeurs actualisées
|
||||
Et il n'y a aucun batch différé
|
||||
|
||||
Scénario: Compatibilité iOS avec AVPlayer
|
||||
Étant donné que l'app iOS utilise AVPlayer
|
||||
Quand les commandes physiques sont interceptées
|
||||
Alors les événements MPRemoteCommandCenter sont capturés:
|
||||
| Commande | Événement iOS |
|
||||
| Suivant | nextTrackCommand |
|
||||
| Précédent | previousTrackCommand |
|
||||
| Play/Pause | playCommand / pauseCommand |
|
||||
Et le tracking du temps utilise CMTime
|
||||
|
||||
Scénario: Compatibilité Android avec MediaSession
|
||||
Étant donné que l'app Android utilise MediaPlayer
|
||||
Quand les commandes physiques sont interceptées
|
||||
Alors les événements MediaSession sont capturés:
|
||||
| Commande | Action Android |
|
||||
| Suivant | ACTION_SKIP_TO_NEXT |
|
||||
| Précédent | ACTION_SKIP_TO_PREVIOUS |
|
||||
| Play/Pause | ACTION_PLAY / ACTION_PAUSE |
|
||||
Et le tracking du temps utilise SystemClock.elapsedRealtime()
|
||||
|
||||
Scénario: Sécurité maximale - pas de distraction
|
||||
Étant donné que je conduis à 80 km/h
|
||||
Quand j'utilise RoadWave avec les commandes au volant
|
||||
Alors je n'ai jamais besoin de regarder mon téléphone
|
||||
Et je n'ai jamais besoin de toucher l'écran CarPlay/Android Auto
|
||||
Et toutes les actions sont accessibles via boutons physiques
|
||||
Et les likes sont enregistrés automatiquement
|
||||
|
||||
Plan du Scénario: Calcul du like automatique selon pourcentage
|
||||
Étant donné que j'écoute un contenu tagué "Sport"
|
||||
Quand j'écoute pendant <pourcentage>%
|
||||
Alors le like automatique est <type>
|
||||
Et l'impact sur la jauge est <points>
|
||||
|
||||
Exemples:
|
||||
| pourcentage | type | points |
|
||||
| 10 | aucun | 0 |
|
||||
| 25 | aucun | 0 |
|
||||
| 29 | aucun | 0 |
|
||||
| 30 | standard | +1 |
|
||||
| 50 | standard | +1 |
|
||||
| 79 | standard | +1 |
|
||||
| 80 | renforcé | +2 |
|
||||
| 90 | renforcé | +2 |
|
||||
| 100 | renforcé | +2 |
|
||||
|
||||
Plan du Scénario: Signal négatif uniquement si skip très rapide
|
||||
Étant donné que j'écoute un contenu
|
||||
Quand je skip après <secondes> secondes
|
||||
Alors le signal est <type>
|
||||
Et l'impact est <points>
|
||||
|
||||
Exemples:
|
||||
| secondes | type | points |
|
||||
| 3 | négatif | -0.5 |
|
||||
| 5 | négatif | -0.5 |
|
||||
| 9 | négatif | -0.5 |
|
||||
| 10 | neutre | 0 |
|
||||
| 15 | neutre | 0 |
|
||||
| 30 | neutre | 0 |
|
||||
188
features/ui/navigation/file-attente-suivant.feature
Normal file
188
features/ui/navigation/file-attente-suivant.feature
Normal file
@@ -0,0 +1,188 @@
|
||||
# language: fr
|
||||
Fonctionnalité: File d'attente et commande "Suivant"
|
||||
En tant qu'auditeur en déplacement
|
||||
Je veux que l'application pré-calcule intelligemment les prochains contenus
|
||||
Afin d'avoir une navigation fluide sans latence
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et qu'un utilisateur est connecté
|
||||
Et que la géolocalisation est activée
|
||||
|
||||
Scénario: Pré-calcul initial de 5 contenus en cache
|
||||
Étant donné que je viens de démarrer l'application
|
||||
Et que je suis situé à Paris (48.8566, 2.3522)
|
||||
Et que je suis en mode voiture (vitesse ≥ 5 km/h)
|
||||
Quand l'application initialise la lecture
|
||||
Alors une file d'attente de 5 contenus est pré-calculée
|
||||
Et la file est stockée en cache Redis avec la clé "user:{user_id}:queue"
|
||||
Et les métadonnées incluent ma position, le timestamp de calcul et le mode
|
||||
Et le cache a un TTL de 15 minutes
|
||||
|
||||
Scénario: Commande "Suivant" sans latence
|
||||
Étant donné qu'une file d'attente de 5 contenus est en cache
|
||||
Et que j'écoute actuellement le contenu "A"
|
||||
Quand j'appuie sur le bouton "Suivant"
|
||||
Alors le contenu suivant démarre immédiatement (< 100ms)
|
||||
Et le contenu est retiré de la file d'attente
|
||||
Et il reste 4 contenus dans la file
|
||||
|
||||
Scénario: Recalcul automatique après déplacement >10km
|
||||
Étant donné que la file a été calculée à Paris (48.8566, 2.3522)
|
||||
Et que j'ai 5 contenus en cache
|
||||
Quand je me déplace à Versailles (48.8049, 2.1204) soit 12km
|
||||
Alors la file d'attente est invalidée automatiquement
|
||||
Et une nouvelle file de 5 contenus est recalculée
|
||||
Et elle est basée sur ma nouvelle position
|
||||
|
||||
Scénario: Recalcul automatique toutes les 10 minutes
|
||||
Étant donné qu'une file a été calculée il y a 10 minutes
|
||||
Et que ma position n'a pas changé
|
||||
Quand le timer de rafraîchissement expire
|
||||
Alors une nouvelle file de 5 contenus est recalculée
|
||||
Et les anciens contenus non écoutés sont remplacés
|
||||
Et les nouveaux contenus publiés depuis sont inclus
|
||||
|
||||
Scénario: Recalcul quand il reste moins de 3 contenus
|
||||
Étant donné qu'il reste 3 contenus dans ma file d'attente
|
||||
Quand j'appuie sur "Suivant"
|
||||
Alors il reste 2 contenus
|
||||
Et un recalcul asynchrone est déclenché en arrière-plan
|
||||
Et 3 nouveaux contenus sont ajoutés à la file
|
||||
Et la file contient maintenant 5 contenus
|
||||
|
||||
Scénario: Insertion prioritaire d'un contenu géolocalisé en mode voiture
|
||||
Étant donné que j'ai une file de 5 contenus pré-calculée
|
||||
Et que je suis en mode voiture
|
||||
Et que je me déplace à 50 km/h vers un point avec contenu géolocalisé
|
||||
Quand je suis à 98m du point (ETA = 7 secondes)
|
||||
Alors une notification est envoyée (icône + compteur 7→1 + son)
|
||||
Et je dois appuyer sur "Suivant" dans les 7 secondes pour valider
|
||||
Quand j'appuie sur "Suivant"
|
||||
Alors un décompte de 5 secondes démarre
|
||||
Et après 5 secondes, le contenu géolocalisé s'insère et démarre
|
||||
Et il remplace le contenu actuel dans la lecture
|
||||
|
||||
Scénario: Contenu géolocalisé ignoré est perdu (cooldown activé)
|
||||
Étant donné qu'une notification géolocalisée est affichée (compteur 7→1)
|
||||
Quand je ne clique pas sur "Suivant" pendant les 7 secondes
|
||||
Alors la notification disparaît
|
||||
Et le contenu géolocalisé est perdu (pas d'insertion dans la file)
|
||||
Et un cooldown de 10 minutes est activé
|
||||
Et aucune nouvelle notification géolocalisée ne sera envoyée pendant 10 minutes
|
||||
|
||||
Scénario: Validation d'une notification géolocalisée
|
||||
Étant donné qu'une notification géolocalisée est affichée (compteur à 5)
|
||||
Et que j'écoute un podcast
|
||||
Quand j'appuie sur "Suivant"
|
||||
Alors le compteur bascule à "5" (décompte final)
|
||||
Et le podcast actuel continue de jouer
|
||||
Et après 5 secondes, le contenu géolocalisé démarre
|
||||
Et le podcast est mis en pause et sauvegardé dans l'historique
|
||||
|
||||
Scénario: Invalidation immédiate après modification des préférences
|
||||
Étant donné que j'ai une file de 5 contenus en cache
|
||||
Et que ma vitesse GPS est de 5 km/h (piéton)
|
||||
Quand je modifie mes curseurs de préférences (géo/découverte/politique)
|
||||
Alors la file d'attente est invalidée immédiatement
|
||||
Et une nouvelle file est recalculée avec les nouvelles préférences
|
||||
Et les anciens contenus en cache sont supprimés
|
||||
|
||||
Scénario: Blocage modification préférences en conduite (>10 km/h)
|
||||
Étant donné que ma vitesse GPS est de 50 km/h (en voiture)
|
||||
Quand j'essaie d'accéder aux réglages de préférences
|
||||
Alors l'interface affiche "Paramètres verrouillés en conduite"
|
||||
Et je ne peux pas modifier les curseurs géo/découverte/politique
|
||||
Et un message "Arrêtez-vous pour modifier vos préférences" s'affiche
|
||||
|
||||
Scénario: Invalidation lors du démarrage d'un live suivi
|
||||
Étant donné que je suis abonné au créateur "RadioVoyage"
|
||||
Et que j'ai une file de 5 contenus en cache
|
||||
Et que je suis dans la zone géographique du créateur
|
||||
Quand le créateur "RadioVoyage" démarre une radio live
|
||||
Alors je reçois une notification push
|
||||
Et le contenu live s'insère en tête de la file d'attente
|
||||
Et la file d'attente est recalculée
|
||||
|
||||
Scénario: Métadonnées de cache Redis
|
||||
Étant donné qu'une file d'attente est calculée
|
||||
Quand elle est stockée dans Redis
|
||||
Alors la clé est "user:{user_id}:queue"
|
||||
Et les métadonnées incluent:
|
||||
| champ | valeur |
|
||||
| last_lat | 48.8566 |
|
||||
| last_lon | 2.3522 |
|
||||
| computed_at | 2026-01-21T10:30:00Z |
|
||||
| mode | voiture |
|
||||
Et le TTL est de 15 minutes (900 secondes)
|
||||
|
||||
Scénario: Contenu géolocalisé remplace le contenu actuel (pas d'insertion en file)
|
||||
Étant donné que j'écoute le contenu C2 de ma file [C1, C2, C3, C4, C5]
|
||||
Et qu'une notification géolocalisée "Tour Eiffel" est déclenchée
|
||||
Quand je valide la notification
|
||||
Et que le décompte de 5s se termine
|
||||
Alors le contenu "Tour Eiffel" remplace C2 et démarre
|
||||
Et C2 est sauvegardé dans l'historique de navigation
|
||||
Et la file reste [C3, C4, C5] (pas de contenu retiré)
|
||||
Et quand "Tour Eiffel" se termine, C3 démarre
|
||||
|
||||
Scénario: Invalidation après déplacement exactement 10km
|
||||
Étant donné que la file a été calculée à une position donnée
|
||||
Quand je me déplace d'exactement 10.0 km
|
||||
Alors la file d'attente n'est PAS invalidée (seuil strict >10km)
|
||||
Et les contenus en cache restent valides
|
||||
Quand je me déplace de 10.1 km supplémentaires (total 10.1km)
|
||||
Alors la file d'attente est invalidée
|
||||
Et une nouvelle file est calculée
|
||||
|
||||
Scénario: Rafraîchissement exactement après 10 minutes
|
||||
Étant donné qu'une file a été calculée à 10:00:00
|
||||
Quand l'heure actuelle est 10:10:00
|
||||
Alors le timer de rafraîchissement expire
|
||||
Et une nouvelle file de 5 contenus est recalculée
|
||||
Et le timestamp "computed_at" est mis à jour
|
||||
|
||||
Scénario: Recalcul asynchrone non-bloquant
|
||||
Étant donné qu'il reste 2 contenus dans la file
|
||||
Et que j'appuie sur "Suivant"
|
||||
Quand le recalcul asynchrone démarre
|
||||
Alors la lecture du contenu actuel n'est pas interrompue
|
||||
Et le recalcul se fait en arrière-plan
|
||||
Et les nouveaux contenus sont ajoutés dès disponibles (< 500ms)
|
||||
Et l'utilisateur ne perçoit aucune latence
|
||||
|
||||
Scénario: Notification basée sur ETA (pas distance fixe)
|
||||
Étant donné qu'un contenu géolocalisé existe à un point GPS
|
||||
Et que je roule à 130 km/h
|
||||
Quand je suis à 252m du point (ETA = 7 secondes)
|
||||
Alors une notification est envoyée
|
||||
Quand je suis à 300m du point (ETA = 8 secondes)
|
||||
Alors aucune notification n'est envoyée (ETA >7s)
|
||||
|
||||
Plan du Scénario: Différentes distances de déplacement et invalidation
|
||||
Étant donné qu'une file a été calculée à une position donnée
|
||||
Quand je me déplace de <distance> km
|
||||
Alors la file est <action>
|
||||
|
||||
Exemples:
|
||||
| distance | action |
|
||||
| 5 | conservée |
|
||||
| 9.9 | conservée |
|
||||
| 10.0 | conservée |
|
||||
| 10.1 | invalidée et recalculée |
|
||||
| 15 | invalidée et recalculée |
|
||||
| 50 | invalidée et recalculée |
|
||||
|
||||
Scénario: Quota de 6 contenus géolocalisés par heure
|
||||
Étant donné que j'ai validé 6 notifications géolocalisées dans la dernière heure
|
||||
Quand un 7ème contenu géolocalisé est détecté (ETA 7s)
|
||||
Alors aucune notification n'est envoyée
|
||||
Et le quota horaire est respecté
|
||||
|
||||
Scénario: Mode piéton - pas de notification avec compteur 7s
|
||||
Étant donné que je suis en mode piéton (vitesse <5 km/h)
|
||||
Et qu'un audio-guide géolocalisé existe à 150m
|
||||
Quand je passe dans le rayon de 200m
|
||||
Alors une notification push système est envoyée
|
||||
Et aucun compteur 7s n'est affiché
|
||||
Et je peux ouvrir l'app en tapant sur la notification
|
||||
255
features/ui/navigation/lecture-enchainement.feature
Normal file
255
features/ui/navigation/lecture-enchainement.feature
Normal file
@@ -0,0 +1,255 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Lecture en boucle et enchaînement automatique
|
||||
En tant qu'auditeur
|
||||
Je veux que les contenus s'enchaînent automatiquement avec un délai paramétrable
|
||||
Afin d'avoir une expérience fluide sans interruption
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et qu'un utilisateur est connecté
|
||||
|
||||
Scénario: Passage automatique après 2 secondes (mode standard)
|
||||
Étant donné que j'écoute un contenu "A" en mode standard
|
||||
Quand la lecture se termine naturellement
|
||||
Alors un timer de 2 secondes démarre
|
||||
Et un overlay s'affiche: "Contenu suivant dans 2s..."
|
||||
Et une barre de décompte visuelle s'affiche
|
||||
Quand le timer atteint 0
|
||||
Alors le contenu "B" démarre automatiquement
|
||||
Et l'overlay disparaît
|
||||
|
||||
Scénario: Passage automatique après 1 seconde (mode Kids)
|
||||
Étant donné que je suis en mode Kids
|
||||
Et que j'écoute un contenu pour enfants
|
||||
Quand la lecture se termine
|
||||
Alors un timer de 1 seconde démarre
|
||||
Et le message "Contenu suivant dans 1s..." s'affiche
|
||||
Quand le timer expire
|
||||
Alors le contenu suivant démarre automatiquement
|
||||
Car l'attention des enfants est plus courte
|
||||
|
||||
Scénario: Passage immédiat après une radio live (0 seconde)
|
||||
Étant donné que j'écoute une radio live
|
||||
Quand le créateur arrête la diffusion
|
||||
Alors le passage au contenu suivant est immédiat (0s de délai)
|
||||
Et aucun overlay de décompte n'est affiché
|
||||
Et la transition est fluide
|
||||
|
||||
Scénario: Annulation du passage automatique
|
||||
Étant donné qu'un contenu se termine
|
||||
Et que le timer de 2 secondes démarre
|
||||
Quand je clique sur "Rester sur ce contenu" pendant le décompte
|
||||
Alors le timer est annulé
|
||||
Et le contenu actuel reste en pause à la fin
|
||||
Et le contenu suivant n'est pas lancé
|
||||
|
||||
Scénario: Insertion de publicité pendant le délai de transition
|
||||
Étant donné que j'ai écouté 4 contenus sans publicité
|
||||
Et que le 5ème contenu se termine
|
||||
Quand le délai de 2 secondes démarre
|
||||
Alors une publicité s'insère dans la file d'attente
|
||||
Et le message devient "Publicité (15s)"
|
||||
Et la publicité démarre après les 2 secondes
|
||||
Et elle ne coupe jamais un contenu en cours
|
||||
|
||||
Scénario: Fréquence de publicité paramétrable admin
|
||||
Étant donné que la fréquence pub est configurée à "1/5 contenus"
|
||||
Quand j'écoute 10 contenus
|
||||
Alors 2 publicités sont insérées (après les contenus 5 et 10)
|
||||
Étant donné que l'admin change la fréquence à "1/3 contenus"
|
||||
Quand j'écoute 9 contenus
|
||||
Alors 3 publicités sont insérées (après les contenus 3, 6 et 9)
|
||||
|
||||
Scénario: Publicité skippable après 5 secondes par défaut
|
||||
Étant donné qu'une publicité de 30 secondes démarre
|
||||
Et que le délai minimal de visionnage est configuré à 5 secondes
|
||||
Quand j'écoute pendant 3 secondes
|
||||
Alors le bouton "Passer" n'est pas encore visible
|
||||
Quand j'atteins 5 secondes d'écoute
|
||||
Alors le bouton "Passer" apparaît
|
||||
Et je peux cliquer pour passer au contenu suivant
|
||||
|
||||
Scénario: Délai minimal de publicité paramétrable admin
|
||||
Étant donné qu'une publicité démarre
|
||||
Et que l'admin a configuré le délai à 10 secondes
|
||||
Quand j'écoute pendant 9 secondes
|
||||
Alors le bouton "Passer" n'est pas visible
|
||||
Quand j'atteins 10 secondes
|
||||
Alors le bouton "Passer" apparaît
|
||||
Et je peux skipper la publicité
|
||||
|
||||
Scénario: Like et abonnement autorisés sur une publicité
|
||||
Étant donné qu'une publicité est en lecture
|
||||
Quand je clique sur le bouton cœur (véhicule arrêté)
|
||||
Alors la publicité reçoit un like (+2% jauges tags pub)
|
||||
Quand je clique sur "S'abonner" au créateur de la pub
|
||||
Alors je suis abonné (+5% jauges tags créateur)
|
||||
Et le créateur de pub bénéficie de l'engagement
|
||||
|
||||
Scénario: Métriques d'engagement publicité trackées
|
||||
Étant donné qu'une publicité de 30s est diffusée à 100 auditeurs
|
||||
Quand 40 auditeurs écoutent entièrement (30s)
|
||||
Et que 50 auditeurs skippent après 10s
|
||||
Et que 10 auditeurs skippent avant 5s
|
||||
Alors les métriques sont:
|
||||
| Métrique | Valeur |
|
||||
| Taux d'écoute complète | 40% |
|
||||
| Taux de skip après seuil | 50% |
|
||||
| Taux de skip immédiat | 10% |
|
||||
| Durée moyenne d'écoute | 18s |
|
||||
|
||||
Scénario: Message "Aucun contenu disponible" si file vide
|
||||
Étant donné que la file d'attente est vide
|
||||
Et qu'aucun contenu n'est disponible dans ma zone
|
||||
Quand le contenu actuel se termine
|
||||
Alors un message s'affiche: "Aucun contenu disponible dans cette zone"
|
||||
Et une proposition apparaît: "Élargir la zone de recherche ?"
|
||||
Et un bouton "Élargir" est disponible
|
||||
Et la lecture se met en pause automatiquement
|
||||
|
||||
Scénario: Élargissement automatique de la zone de recherche
|
||||
Étant donné que le message "Aucun contenu disponible" s'affiche
|
||||
Quand je clique sur "Élargir la zone"
|
||||
Alors l'algorithme relance une recherche avec rayon +50km
|
||||
Et une notification "Recherche élargie à 50km" s'affiche
|
||||
Et la file d'attente est recalculée
|
||||
Et la lecture reprend automatiquement
|
||||
|
||||
Scénario: Refus d'élargissement laisse en pause
|
||||
Étant donné que le message "Aucun contenu disponible" s'affiche
|
||||
Quand je clique sur "Annuler"
|
||||
Alors la lecture reste en pause
|
||||
Et l'interface affiche "En attente de contenu"
|
||||
Et je peux manuellement naviguer ou chercher du contenu
|
||||
|
||||
Scénario: Retry avec backoff exponentiel en cas d'échec réseau
|
||||
Étant donné que le contenu suivant échoue au chargement
|
||||
Quand la première tentative échoue
|
||||
Alors le système retente après 1 seconde (backoff 1s)
|
||||
Quand la 2ème tentative échoue
|
||||
Alors le système retente après 2 secondes (backoff 2s)
|
||||
Quand la 3ème tentative échoue
|
||||
Alors le système retente après 4 secondes (backoff 4s)
|
||||
Et après 3 échecs totaux, le système bascule en mode offline
|
||||
|
||||
Scénario: Basculement mode offline après 3 échecs réseau
|
||||
Étant donné que j'ai eu 3 échecs de chargement consécutifs
|
||||
Quand le 3ème échec se produit
|
||||
Alors un message "Connexion instable, basculement mode offline" s'affiche
|
||||
Et la lecture continue avec les contenus téléchargés uniquement
|
||||
Et les contenus en ligne sont temporairement désactivés
|
||||
Quand la connexion revient
|
||||
Alors le mode en ligne est automatiquement rétabli
|
||||
|
||||
Scénario: Overlay de décompte avec barre visuelle
|
||||
Étant donné qu'un contenu se termine
|
||||
Quand le timer de 2 secondes démarre
|
||||
Alors un overlay semi-transparent s'affiche en bas de l'écran
|
||||
Et le texte "Contenu suivant dans 2s..." est visible
|
||||
Et une barre de progression décroît de 100% à 0% en 2 secondes
|
||||
Et la couleur de la barre passe de vert à orange
|
||||
Et l'overlay disparaît automatiquement après le décompte
|
||||
|
||||
Scénario: Bouton "Rester sur ce contenu" pendant décompte
|
||||
Étant donné que le décompte de 2 secondes est actif
|
||||
Quand l'overlay s'affiche
|
||||
Alors un bouton "Rester sur ce contenu" est visible
|
||||
Et il est cliquable pendant les 2 secondes
|
||||
Quand je clique dessus
|
||||
Alors le timer est annulé immédiatement
|
||||
Et l'overlay disparaît
|
||||
Et le contenu actuel reste affiché en pause
|
||||
|
||||
Scénario: Pas d'interruption d'un contenu en cours
|
||||
Étant donné que j'écoute un contenu de 10 minutes
|
||||
Et que je suis à 5 minutes de lecture
|
||||
Quand une publicité devrait s'insérer (fréquence 1/5)
|
||||
Alors la publicité n'interrompt jamais le contenu en cours
|
||||
Et elle attend la fin du contenu actuel
|
||||
Et elle s'insère pendant le délai de transition (2s)
|
||||
|
||||
Scénario: Publicités uniquement pour utilisateurs gratuits
|
||||
Étant donné que je suis un utilisateur gratuit
|
||||
Quand j'écoute 5 contenus
|
||||
Alors une publicité est insérée après le 5ème contenu
|
||||
Étant donné que je passe en compte Premium
|
||||
Quand j'écoute 100 contenus
|
||||
Alors aucune publicité n'est insérée
|
||||
Et l'enchaînement est direct (2s de transition seulement)
|
||||
|
||||
Scénario: Message clair pour l'utilisateur lors de la publicité
|
||||
Étant donné qu'une publicité va démarrer
|
||||
Quand le délai de transition démarre
|
||||
Alors le message affiché est: "Publicité (15s)"
|
||||
Et la durée totale de la pub est indiquée
|
||||
Et l'utilisateur sait qu'il s'agit d'une pub
|
||||
Et la transparence est maximale
|
||||
|
||||
Scénario: Transition fluide entre contenus sans coupure
|
||||
Étant donné qu'un contenu se termine
|
||||
Et que le suivant est pré-chargé en cache
|
||||
Quand le timer de 2s expire
|
||||
Alors la transition audio utilise un crossfade de 0.3s
|
||||
Et il n'y a aucun blanc ou coupure
|
||||
Et l'expérience est fluide
|
||||
|
||||
Scénario: Gestion des erreurs de chargement avec retry
|
||||
Étant donné que le contenu suivant échoue au chargement
|
||||
Quand la 1ère tentative échoue
|
||||
Alors une notification "Chargement..." s'affiche
|
||||
Et le système retente automatiquement
|
||||
Quand la 2ème tentative réussit
|
||||
Alors la lecture démarre normalement
|
||||
Et aucune action utilisateur n'est requise
|
||||
|
||||
Scénario: Mode offline après échecs multiples
|
||||
Étant donné que j'ai 50 contenus téléchargés en mode offline
|
||||
Et que j'ai eu 3 échecs réseau consécutifs
|
||||
Quand le mode offline s'active
|
||||
Alors seuls les contenus téléchargés sont disponibles
|
||||
Et un badge "Mode offline" s'affiche en haut de l'écran
|
||||
Et la lecture continue sans interruption
|
||||
|
||||
Scénario: Compteur de contenus avant prochaine publicité
|
||||
Étant donné que la fréquence pub est 1/5 contenus
|
||||
Et que j'ai écouté 3 contenus depuis la dernière pub
|
||||
Quand je consulte l'interface
|
||||
Alors un indicateur discret affiche "2 contenus avant pub"
|
||||
Et l'utilisateur sait quand attendre la prochaine publicité
|
||||
|
||||
Plan du Scénario: Délai de transition selon mode
|
||||
Étant donné que je suis en mode <mode>
|
||||
Quand un contenu se termine
|
||||
Alors le délai de transition est <delai> secondes
|
||||
Et le message affiché est <message>
|
||||
|
||||
Exemples:
|
||||
| mode | delai | message |
|
||||
| Standard | 2 | "Contenu suivant dans 2s..." |
|
||||
| Kids | 1 | "Contenu suivant dans 1s..." |
|
||||
| Live | 0 | (aucun message) |
|
||||
|
||||
Plan du Scénario: Fréquence d'insertion des publicités
|
||||
Étant donné que la fréquence pub est configurée à <frequence>
|
||||
Quand j'écoute <contenus> contenus
|
||||
Alors <pubs> publicités sont insérées
|
||||
|
||||
Exemples:
|
||||
| frequence | contenus | pubs |
|
||||
| 1/3 | 6 | 2 |
|
||||
| 1/3 | 9 | 3 |
|
||||
| 1/5 | 10 | 2 |
|
||||
| 1/5 | 15 | 3 |
|
||||
| 1/7 | 14 | 2 |
|
||||
| 1/7 | 21 | 3 |
|
||||
|
||||
Plan du Scénario: Backoff exponentiel retry
|
||||
Étant donné que le chargement échoue
|
||||
Quand je suis à la tentative <tentative>
|
||||
Alors le délai de retry est <delai> secondes
|
||||
|
||||
Exemples:
|
||||
| tentative | delai |
|
||||
| 1 | 1 |
|
||||
| 2 | 2 |
|
||||
| 3 | 4 |
|
||||
206
features/ui/partage/partage-contenu.feature
Normal file
206
features/ui/partage/partage-contenu.feature
Normal file
@@ -0,0 +1,206 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: Partage de contenu
|
||||
En tant qu'utilisateur de RoadWave
|
||||
Je veux pouvoir partager du contenu audio
|
||||
Afin de faire découvrir l'application à d'autres personnes
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'application RoadWave est démarrée
|
||||
Et que l'utilisateur "jean@example.com" est connecté
|
||||
|
||||
# 15.1.1 - Bouton "Partager"
|
||||
|
||||
Scénario: Bouton partager disponible dans le player en lecture
|
||||
Étant donné que le contenu "Balade à Paris" est en cours de lecture
|
||||
Quand l'utilisateur consulte les contrôles du player
|
||||
Alors le bouton "Partager" ⬆️ est visible
|
||||
|
||||
Scénario: Bouton partager disponible sur la page profil créateur
|
||||
Étant donné que l'utilisateur consulte le profil de "@paris_stories"
|
||||
Quand l'utilisateur consulte un contenu dans la liste
|
||||
Alors le bouton "Partager" est disponible pour chaque contenu
|
||||
|
||||
Scénario: Bouton partager dans la liste de recherche
|
||||
Étant donné que l'utilisateur effectue une recherche "voyage paris"
|
||||
Quand l'utilisateur ouvre le menu contextuel d'un résultat
|
||||
Alors l'option "Partager" est disponible
|
||||
|
||||
Scénario: Bouton partager dans l'historique personnel
|
||||
Étant donné que l'utilisateur consulte son historique d'écoute
|
||||
Quand l'utilisateur sélectionne un contenu de l'historique
|
||||
Alors le bouton "Partager" est accessible
|
||||
|
||||
Plan du Scénario: Menu de partage avec options multiples
|
||||
Étant donné que le contenu "<contenu>" est disponible
|
||||
Quand l'utilisateur clique sur le bouton "Partager"
|
||||
Alors le menu natif OS s'ouvre
|
||||
Et les options suivantes sont disponibles:
|
||||
| option |
|
||||
| Copier le lien |
|
||||
| WhatsApp |
|
||||
| Email |
|
||||
| SMS |
|
||||
| Plus... |
|
||||
|
||||
Exemples:
|
||||
| contenu |
|
||||
| Balade à Paris |
|
||||
| Secrets de Montmartre |
|
||||
|
||||
# 15.1.2 - Comportement du lien partagé
|
||||
|
||||
Scénario: Génération du lien de partage
|
||||
Étant donné un contenu avec l'ID "content_12345"
|
||||
Quand l'utilisateur copie le lien de partage
|
||||
Alors le lien généré est "https://roadwave.fr/share/c/content_12345"
|
||||
|
||||
Scénario: Ouverture du lien partagé avec l'application installée (Deep link)
|
||||
Étant donné que l'application RoadWave est installée sur l'appareil
|
||||
Et qu'un lien "https://roadwave.fr/share/c/content_12345" est partagé
|
||||
Quand l'utilisateur clique sur le lien
|
||||
Alors l'application RoadWave s'ouvre automatiquement
|
||||
Et le contenu "content_12345" commence à jouer
|
||||
|
||||
Scénario: Ouverture du lien partagé sans l'application installée (Web player)
|
||||
Étant donné que l'application RoadWave n'est pas installée
|
||||
Et qu'un lien "https://roadwave.fr/share/c/content_12345" est partagé
|
||||
Quand l'utilisateur clique sur le lien
|
||||
Alors une page web responsive s'affiche
|
||||
Et le web player HTML5 est visible
|
||||
Et les boutons de téléchargement App Store et Google Play sont affichés
|
||||
|
||||
Scénario: Contenu de la page web de partage
|
||||
Étant donné un contenu public avec les métadonnées suivantes:
|
||||
| champ | valeur |
|
||||
| titre | Balade à Paris |
|
||||
| créateur | @paris_stories |
|
||||
| durée | 12 min |
|
||||
| écoutes | 2300 |
|
||||
| localisation | Paris 5e |
|
||||
| type_geo | Ancré |
|
||||
| tags | Voyage, Histoire |
|
||||
Quand la page de partage est affichée
|
||||
Alors la page contient:
|
||||
| élément |
|
||||
| Cover image 16:9 |
|
||||
| Titre "Balade à Paris" |
|
||||
| "@paris_stories" |
|
||||
| "12 min · 🎧 2.3K" |
|
||||
| "📍 Paris 5e · Ancré" |
|
||||
| "🏷️ #Voyage #Histoire" |
|
||||
| Description |
|
||||
| Player HTML5 |
|
||||
| Bouton App Store |
|
||||
| Bouton Google Play |
|
||||
|
||||
Scénario: Métadonnées Open Graph pour partage social
|
||||
Étant donné un contenu "Balade à Paris" par "@paris_stories"
|
||||
Quand la page de partage est générée
|
||||
Alors les métadonnées Open Graph incluent:
|
||||
| propriété | valeur |
|
||||
| og:title | Balade à Paris - RoadWave |
|
||||
| og:description | Écoutez ce contenu par @paris_stories |
|
||||
| og:type | music.song |
|
||||
| og:site_name | RoadWave |
|
||||
| twitter:card | player |
|
||||
Et l'aperçu s'affiche correctement sur WhatsApp
|
||||
Et l'aperçu s'affiche correctement sur Facebook
|
||||
Et l'aperçu s'affiche correctement sur Twitter
|
||||
|
||||
Plan du Scénario: Deep linking par plateforme
|
||||
Étant donné que l'application RoadWave est installée sur <plateforme>
|
||||
Et qu'un lien de partage est ouvert
|
||||
Quand le système détecte l'application
|
||||
Alors l'application s'ouvre via <mécanisme>
|
||||
|
||||
Exemples:
|
||||
| plateforme | mécanisme |
|
||||
| iOS | Universal Links |
|
||||
| Android | App Links |
|
||||
|
||||
Scénario: Fallback URL scheme pour deep linking
|
||||
Étant donné que les App Links ne fonctionnent pas
|
||||
Quand le système tente d'ouvrir le contenu
|
||||
Alors l'URL scheme "roadwave://content/content_12345" est utilisé
|
||||
|
||||
# 15.1.3 - Contenus Premium partagés
|
||||
|
||||
Scénario: Badge Premium visible sur le lien partagé
|
||||
Étant donné un contenu Premium "Visite VIP Louvre"
|
||||
Quand l'utilisateur non-premium clique sur le lien partagé
|
||||
Alors la page web affiche le badge "👑 Contenu Premium"
|
||||
|
||||
Scénario: Preview 30 secondes d'un contenu Premium partagé
|
||||
Étant donné un contenu Premium "Visite VIP Louvre" de 15 minutes
|
||||
Et qu'un utilisateur non-premium ouvre le lien partagé
|
||||
Quand le player démarre automatiquement
|
||||
Alors l'audio joue pendant 30 secondes exactement
|
||||
Et un fade out de 2 secondes est appliqué
|
||||
Et un overlay "Contenu réservé Premium" s'affiche après 32 secondes
|
||||
|
||||
Scénario: Contenu de l'overlay paywall Premium
|
||||
Étant donné qu'un contenu Premium a atteint la limite de 30 secondes
|
||||
Quand l'overlay paywall s'affiche
|
||||
Alors le texte suivant est visible:
|
||||
"""
|
||||
👑 Contenu réservé Premium
|
||||
|
||||
Profitez de ce contenu complet
|
||||
et de milliers d'autres
|
||||
sans publicité
|
||||
|
||||
[Passer Premium - 4.99€/mois]
|
||||
[Télécharger l'app]
|
||||
"""
|
||||
|
||||
Scénario: Actions disponibles sur l'overlay Premium
|
||||
Étant donné que l'overlay paywall Premium est affiché
|
||||
Quand l'utilisateur consulte les options
|
||||
Alors les actions suivantes sont disponibles:
|
||||
| action | comportement |
|
||||
| Passer Premium | Redirection vers paiement Mangopay web |
|
||||
| Télécharger l'app | Redirection vers App Store/Google Play |
|
||||
| Rejouer les 30 premières sec | Relecture illimitée du preview |
|
||||
|
||||
Scénario: Relecture illimitée du preview Premium
|
||||
Étant donné un contenu Premium partagé
|
||||
Et que l'utilisateur a écouté les 30 premières secondes
|
||||
Quand l'utilisateur clique sur "Rejouer"
|
||||
Alors les 30 premières secondes sont rejouées
|
||||
Et cette action est possible de manière illimitée
|
||||
|
||||
Scénario: Tracking des partages Premium
|
||||
Étant donné un créateur "@guide_louvre" avec un contenu Premium
|
||||
Quand son contenu est partagé
|
||||
Alors les métriques suivantes sont enregistrées:
|
||||
| métrique | valeur |
|
||||
| Partages Premium | +1 |
|
||||
| Ouvertures lien | compteur |
|
||||
| Conversions Premium | si souscription |
|
||||
|
||||
Scénario: Rémunération créateur sur conversion Premium via partage
|
||||
Étant donné un contenu Premium partagé par "@guide_louvre"
|
||||
Quand un utilisateur s'abonne via le lien partagé
|
||||
Alors le créateur reçoit 70% des revenus de cet abonnement
|
||||
Et la conversion est trackée dans son dashboard
|
||||
|
||||
# Cas d'erreur
|
||||
|
||||
Scénario: Partage d'un contenu supprimé
|
||||
Étant donné qu'un lien de partage "https://roadwave.fr/share/c/deleted_content" est ouvert
|
||||
Et que le contenu n'existe plus
|
||||
Quand la page web se charge
|
||||
Alors un message "Ce contenu n'est plus disponible" s'affiche
|
||||
Et les boutons de téléchargement de l'app sont affichés
|
||||
|
||||
Scénario: Partage d'un contenu en attente de modération
|
||||
Étant donné un contenu en cours de validation modération
|
||||
Quand un lien de partage est ouvert
|
||||
Alors le message "Ce contenu est en cours de validation" s'affiche
|
||||
|
||||
Scénario: Génération du lien hors connexion
|
||||
Étant donné que l'utilisateur n'a pas de connexion réseau
|
||||
Quand l'utilisateur tente de partager un contenu
|
||||
Alors le lien est copié dans le presse-papiers
|
||||
Et un message "Lien copié (nécessite connexion pour ouvrir)" s'affiche
|
||||
293
features/ui/profil/profil-createur.feature
Normal file
293
features/ui/profil/profil-createur.feature
Normal file
@@ -0,0 +1,293 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: Profil créateur
|
||||
En tant qu'utilisateur de RoadWave
|
||||
Je veux consulter les profils des créateurs
|
||||
Afin de découvrir leur contenu et décider de m'abonner
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'application RoadWave est démarrée
|
||||
|
||||
# 15.2.1 - Structure de la page profil
|
||||
|
||||
Scénario: URL du profil créateur
|
||||
Étant donné un créateur avec le pseudo "paris_stories"
|
||||
Quand l'utilisateur accède au profil
|
||||
Alors l'URL est "https://roadwave.fr/@paris_stories"
|
||||
|
||||
Scénario: Informations principales du profil
|
||||
Étant donné un créateur "@paris_stories" avec les informations suivantes:
|
||||
| champ | valeur |
|
||||
| photo | avatar_120x120.jpg |
|
||||
| pseudo | paris_stories |
|
||||
| badge_vérifié | true |
|
||||
| bio | Histoires et anecdotes de Paris |
|
||||
| abonnés | 1200 |
|
||||
| contenus | 42 |
|
||||
| durée_totale | 18h |
|
||||
| écoutes_totales | 54000 |
|
||||
Quand le profil est affiché
|
||||
Alors les éléments suivants sont visibles:
|
||||
| élément | valeur affichée |
|
||||
| Photo profil | 120×120 px |
|
||||
| @pseudo | @paris_stories |
|
||||
| Badge vérifié | ✓ |
|
||||
| Bio | Histoires et... |
|
||||
| Nombre abonnés | 1.2K abonnés |
|
||||
| Nombre contenus | 42 contenus |
|
||||
| Durée totale | 18h de contenu créé |
|
||||
| Écoutes totales | 54K écoutes totales |
|
||||
|
||||
Plan du Scénario: Arrondi des statistiques publiques
|
||||
Étant donné un créateur avec <valeur_exacte> <métrique>
|
||||
Quand le profil est affiché
|
||||
Alors la valeur affichée est "<valeur_affichée>"
|
||||
|
||||
Exemples:
|
||||
| métrique | valeur_exacte | valeur_affichée |
|
||||
| abonnés | 342 | 342 |
|
||||
| abonnés | 1200 | 1.2K |
|
||||
| abonnés | 54000 | 54K |
|
||||
| abonnés | 1200000 | 1.2M |
|
||||
| écoutes | 842 | 842 |
|
||||
| écoutes | 5400 | 5.4K |
|
||||
| écoutes | 142000 | 142K |
|
||||
| écoutes | 2100000 | 2.1M |
|
||||
| durée (heures) | 18 | 18h |
|
||||
| durée (heures) | 142 | 142h |
|
||||
|
||||
Scénario: Bio avec markdown basique
|
||||
Étant donné un créateur avec la bio suivante en markdown:
|
||||
"""
|
||||
**Histoires de Paris** - Découvrez la capitale
|
||||
*Nouveau contenu chaque semaine*
|
||||
https://paris-stories.fr
|
||||
"""
|
||||
Quand le profil est affiché
|
||||
Alors le texte en gras "Histoires de Paris" est formaté
|
||||
Et le texte en italique "Nouveau contenu chaque semaine" est formaté
|
||||
Et le lien "https://paris-stories.fr" est cliquable
|
||||
|
||||
Scénario: Limitation de la bio à 300 caractères
|
||||
Étant donné un créateur qui entre une bio de 350 caractères
|
||||
Quand la bio est sauvegardée
|
||||
Alors seuls les 300 premiers caractères sont conservés
|
||||
Et un message "Maximum 300 caractères" s'affiche
|
||||
|
||||
Scénario: Boutons d'action principaux
|
||||
Étant donné que l'utilisateur consulte un profil créateur
|
||||
Quand la page est chargée
|
||||
Alors les boutons suivants sont visibles:
|
||||
| bouton | action |
|
||||
| S'abonner | Abonnement au créateur |
|
||||
| Partager profil | Menu de partage |
|
||||
| ••• | Menu contextuel |
|
||||
|
||||
Scénario: Menu contextuel du profil [•••]
|
||||
Étant donné que l'utilisateur clique sur le bouton [•••]
|
||||
Quand le menu s'ouvre
|
||||
Alors les options suivantes sont disponibles:
|
||||
| option | description |
|
||||
| Partager profil | Partager le lien du profil |
|
||||
| Signaler profil | Signaler spam ou usurpation d'identité |
|
||||
| Bloquer créateur | Masquer tous les contenus du créateur |
|
||||
|
||||
Scénario: Liste des contenus du créateur
|
||||
Étant donné un créateur avec 3 contenus publiés
|
||||
Quand le profil est affiché
|
||||
Alors chaque contenu affiche:
|
||||
| élément | exemple |
|
||||
| Cover image | Image 16:9 |
|
||||
| Titre | Balade à Paris |
|
||||
| Durée et écoutes | 12 min · 🎧 2.3K |
|
||||
| Localisation | 📍 Paris |
|
||||
| Bouton lecture | ▶️ |
|
||||
|
||||
Plan du Scénario: Options de tri des contenus
|
||||
Étant donné un créateur avec 10 contenus publiés
|
||||
Quand l'utilisateur sélectionne le tri "<option_tri>"
|
||||
Alors les contenus sont triés par <critère>
|
||||
|
||||
Exemples:
|
||||
| option_tri | critère |
|
||||
| Plus récents | Date publication DESC (défaut) |
|
||||
| Plus populaires | Écoutes × facteur temporel (90 jours) |
|
||||
| Plus anciens | Date publication ASC |
|
||||
|
||||
Scénario: Filtrage des contenus par tag
|
||||
Étant donné un créateur avec des contenus taggés "Voyage", "Histoire", "Gastronomie"
|
||||
Quand l'utilisateur filtre par tags "Voyage, Histoire"
|
||||
Alors seuls les contenus avec ces tags sont affichés
|
||||
Et le nombre de résultats est indiqué "12 contenus"
|
||||
|
||||
Scénario: Recherche locale dans le profil
|
||||
Étant donné que l'utilisateur consulte le profil de "@paris_stories"
|
||||
Et que le créateur a publié 50 contenus
|
||||
Quand l'utilisateur entre "Montmartre" dans la barre de recherche
|
||||
Alors la recherche s'effectue sur les titres et descriptions
|
||||
Et seuls les contenus correspondants sont affichés
|
||||
Et le placeholder indique "Rechercher dans les contenus de @paris_stories"
|
||||
|
||||
Scénario: Chargement paginé des contenus
|
||||
Étant donné un créateur avec 100 contenus publiés
|
||||
Quand le profil est affiché
|
||||
Alors 20 contenus sont chargés initialement
|
||||
Et un bouton "Charger plus" est visible en bas de page
|
||||
Quand l'utilisateur clique sur "Charger plus"
|
||||
Alors 20 contenus supplémentaires sont chargés
|
||||
|
||||
# 15.2.2 - Statistiques publiques
|
||||
|
||||
Scénario: Informations publiques visibles par tous
|
||||
Étant donné que l'utilisateur consulte un profil créateur
|
||||
Alors les informations suivantes sont publiques:
|
||||
| information | visible |
|
||||
| Photo et pseudo | ✅ |
|
||||
| Badge vérifié | ✅ |
|
||||
| Bio | ✅ |
|
||||
| Nombre abonnés | ✅ |
|
||||
| Nombre contenus | ✅ |
|
||||
| Durée totale créée | ✅ |
|
||||
| Écoutes totales | ✅ |
|
||||
|
||||
Scénario: Informations privées non visibles
|
||||
Étant donné que l'utilisateur consulte un profil créateur
|
||||
Alors les informations suivantes sont privées:
|
||||
| information | visible |
|
||||
| Liste des abonnés | ❌ |
|
||||
| Revenus | ❌ |
|
||||
| Localisation précise | ❌ |
|
||||
| Email | ❌ |
|
||||
|
||||
Scénario: Dashboard créateur avec métriques privées
|
||||
Étant donné que le créateur "@paris_stories" consulte son propre dashboard
|
||||
Quand la page statistiques est affichée
|
||||
Alors les métriques suivantes sont accessibles:
|
||||
| métrique | type |
|
||||
| Taux complétion moyen | 78% |
|
||||
| Évolution abonnés | Graphique |
|
||||
| Écoutes par contenu | Tableau |
|
||||
| Revenus | Dashboard |
|
||||
| Taux conversion Premium | Pourcentage |
|
||||
| Démographie (âge/zone) | Agrégée |
|
||||
|
||||
Scénario: Graphique d'évolution des abonnés
|
||||
Étant donné que le créateur consulte son dashboard
|
||||
Quand il sélectionne la période "30 jours"
|
||||
Alors un graphique d'évolution des abonnés est affiché
|
||||
Et les périodes disponibles sont:
|
||||
| période |
|
||||
| 30j |
|
||||
| 90j |
|
||||
| 1 an |
|
||||
|
||||
Scénario: Tableau détaillé des écoutes par contenu
|
||||
Étant donné un créateur avec 10 contenus publiés
|
||||
Quand il consulte le tableau des performances
|
||||
Alors chaque contenu affiche:
|
||||
| métrique | exemple |
|
||||
| Titre | Balade |
|
||||
| Écoutes totales | 2300 |
|
||||
| Écoutes complètes >80% | 1840 |
|
||||
| Taux complétion | 80% |
|
||||
| Likes | 420 |
|
||||
| Partages | 56 |
|
||||
|
||||
# 15.2.3 - Badge vérifié
|
||||
|
||||
Scénario: Affichage du badge vérifié
|
||||
Étant donné un créateur vérifié "@paris_stories"
|
||||
Quand son profil est affiché
|
||||
Alors le badge bleu "✓" est accolé au pseudo
|
||||
Et un tooltip "Compte vérifié" s'affiche au survol
|
||||
|
||||
Scénario: Badge vérifié visible partout
|
||||
Étant donné un créateur vérifié "@paris_stories"
|
||||
Alors le badge "✓" est affiché dans:
|
||||
| emplacement |
|
||||
| Page profil |
|
||||
| Player en lecture |
|
||||
| Résultats de recherche|
|
||||
| Notifications |
|
||||
|
||||
Plan du Scénario: Attribution automatique du badge selon critères
|
||||
Étant donné un créateur avec <critère>
|
||||
Quand les conditions sont validées
|
||||
Alors le badge vérifié est attribué <automatique>
|
||||
|
||||
Exemples:
|
||||
| critère | automatique |
|
||||
| KYC Mangopay validé | Oui |
|
||||
| ≥10K abonnés + compte >6 mois | Oui |
|
||||
| Célébrité / Média officiel | Manuel |
|
||||
|
||||
Scénario: Attribution automatique via KYC
|
||||
Étant donné un créateur qui complète son KYC Mangopay
|
||||
Quand les documents sont validés
|
||||
Alors le badge vérifié est attribué automatiquement
|
||||
Et une notification "Votre compte est maintenant vérifié ✓" est envoyée
|
||||
|
||||
Scénario: Attribution automatique à 10K abonnés
|
||||
Étant donné un créateur avec 9999 abonnés et un compte de 7 mois
|
||||
Quand il atteint 10000 abonnés
|
||||
Alors le badge vérifié est attribué automatiquement
|
||||
Et une notification de félicitations est envoyée
|
||||
|
||||
Scénario: Demande manuelle de vérification (célébrité)
|
||||
Étant donné un créateur reconnu publiquement
|
||||
Quand il soumet le formulaire de demande de vérification
|
||||
Alors une requête est créée pour l'équipe RoadWave
|
||||
Et l'équipe vérifie l'identité sous 48-72h
|
||||
Et le badge est attribué si validation réussie
|
||||
|
||||
Scénario: Retrait du badge en cas de suspension
|
||||
Étant donné un créateur vérifié avec le badge "✓"
|
||||
Quand sa monétisation est suspendue
|
||||
Alors le badge vérifié est retiré temporairement
|
||||
Et le badge est restauré après levée de la suspension
|
||||
|
||||
Scénario: Retrait définitif du badge pour strikes multiples
|
||||
Étant donné un créateur vérifié avec 3 strikes actifs
|
||||
Quand un 4ème strike est appliqué (ban)
|
||||
Alors le badge vérifié est retiré définitivement
|
||||
Et le compte est banni
|
||||
|
||||
Scénario: Retrait du badge pour usurpation d'identité
|
||||
Étant donné un créateur vérifié qui usurpe l'identité d'une célébrité
|
||||
Quand la fraude est détectée
|
||||
Alors le badge est retiré immédiatement
|
||||
Et le compte est banni
|
||||
Et une enquête est ouverte
|
||||
|
||||
# Cas d'erreur et limites
|
||||
|
||||
Scénario: Profil créateur supprimé
|
||||
Étant donné qu'un utilisateur tente d'accéder à "@deleted_user"
|
||||
Quand la page est chargée
|
||||
Alors un message "Ce profil n'existe pas ou a été supprimé" s'affiche
|
||||
|
||||
Scénario: Blocage d'un créateur
|
||||
Étant donné que l'utilisateur bloque le créateur "@spam_account"
|
||||
Quand l'utilisateur consulte son flux de recommandations
|
||||
Alors aucun contenu de "@spam_account" n'est affiché
|
||||
Et le créateur n'apparaît plus dans les recherches
|
||||
|
||||
Scénario: Déblocage d'un créateur
|
||||
Étant donné que l'utilisateur a bloqué "@paris_stories"
|
||||
Quand il accède à ses paramètres "Comptes bloqués"
|
||||
Et qu'il débloque "@paris_stories"
|
||||
Alors les contenus du créateur réapparaissent dans les recommandations
|
||||
|
||||
Scénario: Signalement d'un profil pour spam
|
||||
Étant donné que l'utilisateur signale le profil "@spam_account"
|
||||
Quand il sélectionne la raison "Spam"
|
||||
Alors le signalement est envoyé à la modération
|
||||
Et un message de confirmation s'affiche
|
||||
Et le profil reste visible jusqu'à décision de modération
|
||||
|
||||
Scénario: Signalement pour usurpation d'identité
|
||||
Étant donné que l'utilisateur signale le profil "@fake_celebrity"
|
||||
Quand il sélectionne "Usurpation d'identité"
|
||||
Et qu'il fournit une preuve
|
||||
Alors le signalement est priorisé (priorité HAUTE)
|
||||
Et l'équipe modération traite sous 24h
|
||||
472
features/ui/recherche/recherche.feature
Normal file
472
features/ui/recherche/recherche.feature
Normal 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
|
||||
Reference in New Issue
Block a user