Visual audit captures for all major pages (desktop, tablet, mobile). Add run-audit.sh and generate_page_fix_prompts.sh helper scripts. Add prompt templates directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
980 lines
41 KiB
Bash
980 lines
41 KiB
Bash
#!/usr/bin/env python3
|
|
"""
|
|
VEZA — Générateur de prompts Claude Code pour audit exhaustif de toutes les pages.
|
|
Génère un prompt par route, prêt à être copié-collé dans Claude Code avec Playwright MCP.
|
|
|
|
Usage:
|
|
python veza-prompt-generator.py # Génère tous les prompts dans ./prompts/
|
|
python veza-prompt-generator.py --route /settings # Génère un seul prompt
|
|
python veza-prompt-generator.py --list # Liste toutes les routes
|
|
python veza-prompt-generator.py --batch 1 # Génère le batch 1 (routes groupées)
|
|
python veza-prompt-generator.py --combined # Un seul fichier avec tous les prompts
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# Configuration
|
|
# ─────────────────────────────────────────────
|
|
|
|
OUTPUT_DIR = "./prompts"
|
|
DOMAIN = "veza.fr"
|
|
|
|
TEST_ACCOUNTS = [
|
|
{"role": "admin", "email": "admin@veza.music", "password": "Admin123!"},
|
|
{"role": "creator", "email": "artist@veza.music", "password": "Artist123!"},
|
|
{"role": "user", "email": "user@veza.music", "password": "User123!"},
|
|
{"role": "moderator", "email": "mod@veza.music", "password": "Mod123!"},
|
|
{"role": "user_new", "email": "new@veza.music", "password": "New123!"},
|
|
]
|
|
|
|
E2E_TEST_DIR = "veza-e2e/"
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# Route definitions
|
|
# ─────────────────────────────────────────────
|
|
|
|
@dataclass
|
|
class Route:
|
|
number: int
|
|
path: str
|
|
name: str
|
|
category: str
|
|
auth_required: bool = True
|
|
role: str = "user"
|
|
params: str = ""
|
|
test_with_accounts: list = field(default_factory=list)
|
|
specific_checks: list = field(default_factory=list)
|
|
|
|
|
|
ROUTES = [
|
|
# ── Pages publiques ──
|
|
Route(1, "/login", "Connexion", "public", auth_required=False,
|
|
specific_checks=[
|
|
"Formulaire email + password fonctionnel",
|
|
"Validation des champs (email invalide, champs vides)",
|
|
"Messages d'erreur (mauvais credentials)",
|
|
"Lien 'Mot de passe oublié' fonctionnel",
|
|
"Lien 'Inscription' fonctionnel",
|
|
"Redirection après login réussi",
|
|
"Protection contre brute-force (rate limiting visible)",
|
|
"Autocomplete attributes sur les inputs",
|
|
"Accessibilité: labels, focus order, aria",
|
|
]),
|
|
Route(2, "/register", "Inscription", "public", auth_required=False,
|
|
specific_checks=[
|
|
"Tous les champs du formulaire (username, email, password, confirm)",
|
|
"Validation temps réel (email format, password strength, username dispo)",
|
|
"Messages d'erreur clairs pour chaque règle de validation",
|
|
"Soumission et retour serveur (201, 409 duplicate, 422 validation)",
|
|
"Lien vers /login fonctionnel",
|
|
"CGU / checkbox conditions d'utilisation si présent",
|
|
"Accessibilité: labels, focus, aria, autocomplete",
|
|
]),
|
|
Route(3, "/forgot-password", "Récupération mot de passe", "public", auth_required=False,
|
|
specific_checks=[
|
|
"Formulaire email fonctionnel",
|
|
"Message de confirmation après soumission",
|
|
"Gestion email inexistant (pas de leak d'info)",
|
|
"Lien retour vers /login",
|
|
"Rate limiting sur les soumissions",
|
|
]),
|
|
Route(4, "/verify-email", "Vérification email", "public", auth_required=False,
|
|
specific_checks=[
|
|
"Comportement avec token valide vs invalide vs expiré",
|
|
"Message de succès / erreur approprié",
|
|
"Redirection après vérification",
|
|
"Gestion du cas sans token dans l'URL",
|
|
]),
|
|
Route(5, "/reset-password", "Réinitialisation mot de passe", "public", auth_required=False,
|
|
specific_checks=[
|
|
"Formulaire nouveau password + confirmation",
|
|
"Validation password strength côté client",
|
|
"Comportement avec token valide vs invalide vs expiré",
|
|
"Message de succès + redirection vers /login",
|
|
"Autocomplete attributes",
|
|
]),
|
|
Route(6, "/launch", "Landing page pré-lancement", "public", auth_required=False,
|
|
specific_checks=[
|
|
"Rendu visuel complet (hero, sections, CTA)",
|
|
"Tous les liens/CTA fonctionnels",
|
|
"Responsive (mobile/tablet/desktop)",
|
|
"Performance de chargement",
|
|
"SEO: title, meta description, OG tags",
|
|
]),
|
|
Route(7, "/design-system", "Démo du design system", "public", auth_required=False,
|
|
specific_checks=[
|
|
"Tous les composants se rendent sans erreur",
|
|
"Pas d'erreurs console",
|
|
"Interactivité des composants démo (boutons, toggles, modals)",
|
|
]),
|
|
Route(8, "/u/:username", "Profil public utilisateur", "public", auth_required=False,
|
|
params="username",
|
|
specific_checks=[
|
|
"Affichage avec username existant (ex: tester avec chaque compte)",
|
|
"Page 404 avec username inexistant",
|
|
"Infos publiques affichées (avatar, bio, tracks publiques)",
|
|
"Pas de fuite d'infos privées (email, settings)",
|
|
"Boutons follow/unfollow si connecté",
|
|
"Liste des tracks/playlists publiques",
|
|
"Liens fonctionnels vers les tracks",
|
|
]),
|
|
Route(9, "/playlists/shared/:token", "Playlist partagée", "public", auth_required=False,
|
|
params="token",
|
|
specific_checks=[
|
|
"Affichage avec token valide",
|
|
"Gestion token invalide / expiré",
|
|
"Lecture des tracks depuis le partage",
|
|
"Infos playlist (titre, description, nombre de tracks)",
|
|
]),
|
|
|
|
# ── Navigation principale ──
|
|
Route(10, "/dashboard", "Tableau de bord", "main_nav",
|
|
test_with_accounts=["user", "creator", "admin"],
|
|
specific_checks=[
|
|
"Widgets / cartes de stats se chargent",
|
|
"Données cohérentes avec le rôle (creator voit ses stats artiste)",
|
|
"Liens rapides fonctionnels",
|
|
"Activité récente / feed",
|
|
"Pas d'erreurs réseau dans la console",
|
|
"Responsive layout",
|
|
]),
|
|
Route(11, "/discover", "Découverte musicale", "main_nav",
|
|
specific_checks=[
|
|
"Sections de découverte (trending, new releases, genres)",
|
|
"Cartes de tracks/albums cliquables",
|
|
"Lecture depuis la page discover",
|
|
"Filtres / catégories fonctionnels",
|
|
"Infinite scroll ou pagination",
|
|
"État vide si aucun contenu",
|
|
]),
|
|
Route(12, "/feed", "Fil d'actualité", "main_nav",
|
|
specific_checks=[
|
|
"Posts / activités des artistes suivis",
|
|
"Infinite scroll ou pagination",
|
|
"Interactions (like, comment, share)",
|
|
"État vide (aucun artiste suivi)",
|
|
"Chargement sans erreurs réseau",
|
|
]),
|
|
Route(13, "/library", "Bibliothèque musicale", "main_nav",
|
|
test_with_accounts=["user", "creator"],
|
|
specific_checks=[
|
|
"Liste des tracks / albums / playlists de l'utilisateur",
|
|
"Tri et filtres fonctionnels",
|
|
"Recherche dans la bibliothèque",
|
|
"Actions sur les items (play, add to playlist, delete)",
|
|
"État vide pour un nouveau compte",
|
|
"Pagination / infinite scroll",
|
|
]),
|
|
Route(14, "/queue", "File de lecture", "main_nav",
|
|
specific_checks=[
|
|
"Affichage de la queue actuelle",
|
|
"Drag & drop pour réordonner",
|
|
"Suppression d'items de la queue",
|
|
"Bouton clear queue",
|
|
"État vide",
|
|
"Persistance après navigation",
|
|
]),
|
|
Route(15, "/search", "Recherche globale", "main_nav",
|
|
specific_checks=[
|
|
"Barre de recherche fonctionnelle",
|
|
"Résultats par catégorie (tracks, artists, playlists, albums)",
|
|
"Recherche en temps réel / debounce",
|
|
"Résultats cliquables et navigation correcte",
|
|
"État 'aucun résultat'",
|
|
"Historique de recherche si présent",
|
|
]),
|
|
|
|
# ── Playlists & Tracks ──
|
|
Route(16, "/playlists", "Liste des playlists", "playlists",
|
|
specific_checks=[
|
|
"Liste des playlists de l'utilisateur",
|
|
"Bouton créer une playlist",
|
|
"Cartes playlist cliquables",
|
|
"Infos affichées (titre, nombre de tracks, durée, cover)",
|
|
"Actions (edit, delete, share)",
|
|
"État vide",
|
|
]),
|
|
Route(17, "/playlists/favoris", "Playlist favoris", "playlists",
|
|
specific_checks=[
|
|
"Liste des tracks favorites",
|
|
"Bouton unfavorite fonctionnel",
|
|
"Lecture depuis les favoris",
|
|
"Tri (date ajoutée, titre, artiste)",
|
|
"État vide",
|
|
]),
|
|
Route(18, "/playlists/:id", "Détail playlist", "playlists",
|
|
params="id",
|
|
specific_checks=[
|
|
"Header playlist (titre, description, cover, auteur)",
|
|
"Liste des tracks avec numérotation",
|
|
"Boutons play / shuffle",
|
|
"Actions par track (play, add to queue, remove)",
|
|
"Boutons share / edit / delete (si owner)",
|
|
"Playlist inexistante → 404",
|
|
"Playlist privée d'un autre user → 403 ou 404",
|
|
]),
|
|
Route(19, "/playlists/:id/edit", "Édition playlist", "playlists",
|
|
params="id",
|
|
specific_checks=[
|
|
"Formulaire pré-rempli (titre, description)",
|
|
"Upload / changement de cover",
|
|
"Réordonnement des tracks (drag & drop)",
|
|
"Ajout / suppression de tracks",
|
|
"Sauvegarde fonctionnelle",
|
|
"Validation (titre requis)",
|
|
"Accès interdit si pas owner → redirect ou 403",
|
|
]),
|
|
Route(20, "/tracks/:id", "Détail track", "playlists",
|
|
params="id",
|
|
specific_checks=[
|
|
"Infos track (titre, artiste, album, durée, cover)",
|
|
"Bouton play fonctionnel",
|
|
"Boutons like / add to playlist / share",
|
|
"Waveform / visualisation audio si présent",
|
|
"Commentaires si présent",
|
|
"Tracks similaires / recommandations",
|
|
"Track inexistante → 404",
|
|
"Métadonnées (genre, BPM, key si affichés)",
|
|
]),
|
|
|
|
# ── Social & Communication ──
|
|
Route(21, "/social", "Communauté", "social",
|
|
specific_checks=[
|
|
"Liste des utilisateurs / artistes",
|
|
"Recherche / filtres",
|
|
"Boutons follow / unfollow",
|
|
"Profils cliquables",
|
|
"Compteurs followers/following",
|
|
"Sections (trending artists, new members, etc.)",
|
|
]),
|
|
Route(22, "/chat", "Messagerie", "social",
|
|
test_with_accounts=["user", "creator"],
|
|
specific_checks=[
|
|
"Liste des conversations",
|
|
"Envoi de message",
|
|
"Réception en temps réel (WebSocket)",
|
|
"Création de nouvelle conversation",
|
|
"Recherche dans les conversations",
|
|
"État vide (aucune conversation)",
|
|
"Indicateurs read/unread",
|
|
"Envoi de médias si supporté",
|
|
]),
|
|
Route(23, "/chat/join/:token", "Rejoindre un chat", "social",
|
|
params="token",
|
|
specific_checks=[
|
|
"Token valide → rejoint le chat + redirect",
|
|
"Token invalide → message d'erreur",
|
|
"Token expiré → message d'erreur",
|
|
"Déjà membre → redirect vers le chat existant",
|
|
]),
|
|
Route(24, "/live", "Streams en direct", "social",
|
|
specific_checks=[
|
|
"Liste des streams en cours",
|
|
"Cartes stream (titre, artiste, viewers, thumbnail)",
|
|
"Clic → page de visionnage",
|
|
"État vide (aucun stream)",
|
|
"Compteurs viewers",
|
|
]),
|
|
Route(25, "/live/go-live", "Lancer un stream", "social",
|
|
role="creator",
|
|
test_with_accounts=["creator", "admin"],
|
|
specific_checks=[
|
|
"Formulaire de configuration stream (titre, description, catégorie)",
|
|
"Prévisualisation caméra/micro",
|
|
"Bouton Go Live",
|
|
"Vérification permissions (role creator/admin requis)",
|
|
"Utilisateur standard → redirect ou message d'erreur",
|
|
"Configuration RTMP/HLS si affichée",
|
|
]),
|
|
Route(26, "/listen-together/:sessionId", "Écoute collaborative", "social",
|
|
params="sessionId",
|
|
specific_checks=[
|
|
"Session valide → rejoint l'écoute",
|
|
"Session invalide → erreur",
|
|
"Synchronisation audio entre participants",
|
|
"Liste des participants",
|
|
"Chat intégré si présent",
|
|
"Contrôles de lecture (host only vs all)",
|
|
]),
|
|
|
|
# ── Marketplace & Commerce ──
|
|
Route(27, "/marketplace", "Place de marché", "marketplace",
|
|
specific_checks=[
|
|
"Grille / liste de produits",
|
|
"Filtres (catégorie, prix, type)",
|
|
"Recherche produits",
|
|
"Cartes produits (image, titre, prix, vendeur)",
|
|
"Pagination / infinite scroll",
|
|
"Tri (prix, date, popularité)",
|
|
]),
|
|
Route(28, "/marketplace/products/:id", "Détail produit", "marketplace",
|
|
params="id",
|
|
specific_checks=[
|
|
"Infos produit (images, titre, description, prix)",
|
|
"Bouton acheter / ajouter au panier",
|
|
"Avis / ratings si présent",
|
|
"Infos vendeur",
|
|
"Produit inexistant → 404",
|
|
"Produits similaires",
|
|
]),
|
|
Route(29, "/wishlist", "Liste de souhaits", "marketplace",
|
|
specific_checks=[
|
|
"Liste des produits wishlistés",
|
|
"Bouton retirer de la wishlist",
|
|
"Bouton acheter directement",
|
|
"État vide",
|
|
"Infos produit à jour (prix, disponibilité)",
|
|
]),
|
|
Route(30, "/purchases", "Historique d'achats", "marketplace",
|
|
specific_checks=[
|
|
"Liste des achats passés",
|
|
"Détails par achat (date, montant, produit, statut)",
|
|
"Téléchargement si digital",
|
|
"État vide",
|
|
"Pagination si beaucoup d'achats",
|
|
]),
|
|
Route(31, "/checkout/complete", "Confirmation de commande", "marketplace",
|
|
specific_checks=[
|
|
"Message de confirmation",
|
|
"Récapitulatif de la commande",
|
|
"Liens vers les achats / téléchargements",
|
|
"Comportement si accès direct sans commande → redirect",
|
|
]),
|
|
Route(32, "/sell", "Dashboard vendeur", "marketplace",
|
|
role="creator",
|
|
test_with_accounts=["creator", "admin"],
|
|
specific_checks=[
|
|
"Stats de vente (revenus, nombre de ventes)",
|
|
"Liste des produits en vente",
|
|
"Bouton ajouter un produit",
|
|
"Gestion des produits (edit, delete, toggle visibility)",
|
|
"Accès interdit pour les non-creators",
|
|
]),
|
|
|
|
# ── Créateur & Analytics ──
|
|
Route(33, "/analytics", "Tableau de bord analytics", "creator",
|
|
role="creator",
|
|
test_with_accounts=["creator", "admin"],
|
|
specific_checks=[
|
|
"Graphiques de stats (plays, listeners, revenue)",
|
|
"Période sélectionnable (7j, 30j, 90j, 1an)",
|
|
"Top tracks / Top playlists",
|
|
"Données démographiques si présent",
|
|
"Export de données si présent",
|
|
"État vide pour un nouveau creator",
|
|
"Accès interdit pour user standard",
|
|
]),
|
|
Route(34, "/cloud", "Stockage cloud", "creator",
|
|
role="creator",
|
|
test_with_accounts=["creator"],
|
|
specific_checks=[
|
|
"Explorateur de fichiers",
|
|
"Upload de fichiers audio",
|
|
"Progress bar upload",
|
|
"Organisation en dossiers",
|
|
"Actions (rename, delete, move, download)",
|
|
"Quota de stockage affiché",
|
|
"Types de fichiers acceptés / rejetés",
|
|
]),
|
|
Route(35, "/gear", "Inventaire équipement", "creator",
|
|
role="creator",
|
|
specific_checks=[
|
|
"Liste de l'équipement",
|
|
"Ajout / édition / suppression d'items",
|
|
"Catégories (instruments, software, hardware)",
|
|
"Photos d'équipement si supporté",
|
|
"État vide",
|
|
]),
|
|
Route(36, "/distribution", "Distribution externe", "creator",
|
|
role="creator",
|
|
specific_checks=[
|
|
"Liste des plateformes de distribution",
|
|
"Statut de distribution par track/album",
|
|
"Bouton distribuer",
|
|
"Configuration des métadonnées requises",
|
|
"Historique des distributions",
|
|
]),
|
|
|
|
# ── Compte & Paramètres ──
|
|
Route(37, "/profile", "Profil (redirect)", "account",
|
|
specific_checks=[
|
|
"Redirection vers /u/:username du user connecté",
|
|
"Redirection correcte pour chaque rôle",
|
|
]),
|
|
Route(38, "/settings", "Paramètres du compte", "account",
|
|
test_with_accounts=["user", "creator", "admin"],
|
|
specific_checks=[
|
|
"Onglet Account: changement de password fonctionnel",
|
|
"Onglet Account: setup/disable 2FA (Authenticator + SMS)",
|
|
"Onglet Account: statut 2FA correct",
|
|
"Onglet Account: Delete Account (validation 'DELETE', password requis)",
|
|
"Onglet Préférences: thème (radio buttons mutuellement exclusifs)",
|
|
"Onglet Préférences: langue (changement effectif)",
|
|
"Onglet Préférences: timezone",
|
|
"Onglet Notifications: tous les toggles fonctionnels",
|
|
"Onglet Confidentialité: tous les toggles fonctionnels",
|
|
"Onglet Playback: audio quality, crossfade, etc.",
|
|
"Bouton Save Config fonctionnel (pas d'erreur validation)",
|
|
"Accessibilité: labels sur tous les checkboxes/toggles",
|
|
"Pas de mélange i18n (tout FR ou tout EN)",
|
|
"Pas de fuite d'infos (email dans search bar, VAPID key)",
|
|
]),
|
|
Route(39, "/settings/sessions", "Sessions actives", "account",
|
|
specific_checks=[
|
|
"Liste des sessions actives (device, IP, date)",
|
|
"Session courante identifiée",
|
|
"Bouton révoquer une session",
|
|
"Bouton révoquer toutes les autres sessions",
|
|
"Confirmation avant révocation",
|
|
]),
|
|
Route(40, "/notifications", "Centre de notifications", "account",
|
|
specific_checks=[
|
|
"Liste des notifications",
|
|
"Marquage lu/non-lu",
|
|
"Bouton marquer tout comme lu",
|
|
"Filtres par type de notification",
|
|
"Clic sur notification → navigation correcte",
|
|
"État vide",
|
|
"Pagination / infinite scroll",
|
|
]),
|
|
Route(41, "/subscription", "Abonnements & plans", "account",
|
|
specific_checks=[
|
|
"Affichage des plans disponibles",
|
|
"Plan actuel mis en évidence",
|
|
"Boutons upgrade / downgrade",
|
|
"Comparaison des features par plan",
|
|
"Historique de facturation si présent",
|
|
"Gestion du moyen de paiement",
|
|
]),
|
|
|
|
# ── Apprentissage & Support ──
|
|
Route(42, "/education", "Formation & ressources", "learning",
|
|
specific_checks=[
|
|
"Liste des cours / ressources",
|
|
"Catégories / filtres",
|
|
"Progression si présent",
|
|
"Contenu vidéo / texte se charge",
|
|
"Liens fonctionnels",
|
|
]),
|
|
Route(43, "/support", "Support & aide", "learning",
|
|
specific_checks=[
|
|
"FAQ / base de connaissances",
|
|
"Formulaire de contact / ticket",
|
|
"Chat support si présent",
|
|
"Recherche dans l'aide",
|
|
"Liens vers documentation",
|
|
]),
|
|
|
|
# ── Administration ──
|
|
Route(44, "/admin", "Dashboard admin", "admin", role="admin",
|
|
test_with_accounts=["admin"],
|
|
specific_checks=[
|
|
"Stats globales plateforme (users, tracks, revenue)",
|
|
"Graphiques / KPIs",
|
|
"Accès interdit pour non-admin → redirect ou 403",
|
|
"Liens rapides vers sous-sections admin",
|
|
]),
|
|
Route(45, "/admin/moderation", "Modération contenu", "admin", role="admin",
|
|
test_with_accounts=["admin", "moderator"],
|
|
specific_checks=[
|
|
"Queue de modération (signalements)",
|
|
"Actions (approve, reject, ban)",
|
|
"Filtres par type de contenu / statut",
|
|
"Détail du signalement",
|
|
"Historique des actions de modération",
|
|
]),
|
|
Route(46, "/admin/platform", "Administration plateforme", "admin", role="admin",
|
|
test_with_accounts=["admin"],
|
|
specific_checks=[
|
|
"Configuration plateforme",
|
|
"Gestion des features flags",
|
|
"Stats système (storage, bandwidth)",
|
|
"Logs si présent",
|
|
]),
|
|
Route(47, "/admin/transfers", "Gestion transferts/paiements", "admin", role="admin",
|
|
test_with_accounts=["admin"],
|
|
specific_checks=[
|
|
"Liste des transferts / paiements",
|
|
"Statuts (pending, completed, failed)",
|
|
"Détail par transfert",
|
|
"Actions (approve, reject, retry)",
|
|
"Filtres et recherche",
|
|
]),
|
|
Route(48, "/admin/roles", "Gestion des rôles", "admin", role="admin",
|
|
test_with_accounts=["admin"],
|
|
specific_checks=[
|
|
"Liste des rôles existants",
|
|
"Permissions par rôle",
|
|
"Assignation de rôles aux utilisateurs",
|
|
"Création / modification de rôles",
|
|
"Protection contre la suppression du rôle admin",
|
|
]),
|
|
|
|
# ── Développeur ──
|
|
Route(49, "/developer", "Dashboard développeur", "developer",
|
|
specific_checks=[
|
|
"Clés API (création, révocation, copie)",
|
|
"Documentation API intégrée ou liens",
|
|
"Stats d'utilisation API",
|
|
"Webhooks configurés",
|
|
"Rate limits affichés",
|
|
]),
|
|
Route(50, "/webhooks", "Gestion webhooks", "developer",
|
|
specific_checks=[
|
|
"Liste des webhooks configurés",
|
|
"Ajout d'un webhook (URL, events, secret)",
|
|
"Test de webhook",
|
|
"Logs de delivery (succès/échec)",
|
|
"Actions (edit, delete, toggle active)",
|
|
]),
|
|
|
|
# ── Redirections ──
|
|
Route(51, "/", "Redirect → /launch", "redirect", auth_required=False,
|
|
specific_checks=[
|
|
"Redirection automatique vers /launch",
|
|
"Status code 301/302 approprié",
|
|
"Pas de flash de contenu avant redirect",
|
|
]),
|
|
Route(52, "/tracks", "Redirect → /library", "redirect",
|
|
specific_checks=[
|
|
"Redirection automatique vers /library",
|
|
"Fonctionne quand connecté",
|
|
]),
|
|
Route(53, "/community", "Redirect → /social", "redirect",
|
|
specific_checks=[
|
|
"Redirection automatique vers /social",
|
|
]),
|
|
Route(54, "/favorites", "Redirect → /playlists/favoris", "redirect",
|
|
specific_checks=[
|
|
"Redirection automatique vers /playlists/favoris",
|
|
]),
|
|
Route(55, "/home", "Redirect → /dashboard", "redirect",
|
|
specific_checks=[
|
|
"Redirection automatique vers /dashboard",
|
|
]),
|
|
|
|
# ── Pages d'erreur ──
|
|
Route(56, "/404", "Page non trouvée", "error", auth_required=False,
|
|
specific_checks=[
|
|
"Affichage du message 404",
|
|
"Lien retour vers l'accueil",
|
|
"Design cohérent avec le reste du site",
|
|
"Pas d'erreurs console",
|
|
]),
|
|
Route(57, "/500", "Erreur serveur", "error", auth_required=False,
|
|
specific_checks=[
|
|
"Affichage du message d'erreur serveur",
|
|
"Lien retour / bouton réessayer",
|
|
"Pas de stack trace exposée",
|
|
]),
|
|
Route(58, "/random-nonexistent-path", "Catch-all → /404", "error", auth_required=False,
|
|
specific_checks=[
|
|
"Redirection vers la page 404",
|
|
"URL arbitraire correctement interceptée",
|
|
]),
|
|
]
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# Prompt template
|
|
# ─────────────────────────────────────────────
|
|
|
|
def build_test_accounts_block() -> str:
|
|
lines = []
|
|
lines.append("Comptes de test :")
|
|
lines.append("")
|
|
lines.append("┌───────────┬───────────────────┬──────────────┐")
|
|
lines.append("│ Rôle │ Email │ Mot de passe │")
|
|
lines.append("├───────────┼───────────────────┼──────────────┤")
|
|
for i, acc in enumerate(TEST_ACCOUNTS):
|
|
role = acc["role"].ljust(9)
|
|
email = acc["email"].ljust(17)
|
|
pwd = acc["password"].ljust(12)
|
|
lines.append(f"│ {role} │ {email} │ {pwd} │")
|
|
if i < len(TEST_ACCOUNTS) - 1:
|
|
lines.append("├───────────┼───────────────────┼──────────────┤")
|
|
lines.append("└───────────┴───────────────────┴──────────────┘")
|
|
lines.append(f"Domaine : {DOMAIN}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def build_prompt(route: Route) -> str:
|
|
"""Génère le prompt Claude Code pour une route donnée."""
|
|
|
|
# Déterminer les comptes à utiliser
|
|
if route.test_with_accounts:
|
|
accounts_to_use = route.test_with_accounts
|
|
elif route.role == "admin":
|
|
accounts_to_use = ["admin"]
|
|
elif route.role == "creator":
|
|
accounts_to_use = ["creator"]
|
|
elif not route.auth_required:
|
|
accounts_to_use = ["(pas de connexion)", "user"]
|
|
else:
|
|
accounts_to_use = ["user", "creator"]
|
|
|
|
accounts_str = ", ".join(accounts_to_use)
|
|
|
|
# Checks spécifiques
|
|
checks_block = ""
|
|
if route.specific_checks:
|
|
checks_lines = [f" - {c}" for c in route.specific_checks]
|
|
checks_block = "\n".join(checks_lines)
|
|
|
|
# Paramètres dynamiques
|
|
params_note = ""
|
|
if route.params:
|
|
params_note = f"""
|
|
Note sur les paramètres dynamiques :
|
|
Cette route utilise le paramètre `{route.params}`.
|
|
- Teste avec une valeur VALIDE (existante en base)
|
|
- Teste avec une valeur INVALIDE (inexistante) → comportement d'erreur attendu
|
|
- Teste avec une valeur malformée (injection, caractères spéciaux)
|
|
"""
|
|
|
|
prompt = f"""ok maintenant il faut dresser une liste exhaustive de toutes les erreurs qui existent dans la page "{route.name}" ({route.path}).
|
|
Il faut tester chaque fonctionnalité une par une avec le MCP Playwright pour simuler un utilisateur réel.
|
|
|
|
{build_test_accounts_block()}
|
|
|
|
Comptes à utiliser pour cette page : {accounts_str}
|
|
{params_note}
|
|
─── INSTRUCTIONS ───
|
|
|
|
1. AUDIT EXHAUSTIF avec Playwright MCP :
|
|
|
|
Pour la page {route.path} ("{route.name}"), teste systématiquement :
|
|
|
|
a) CHARGEMENT & RENDU
|
|
- La page se charge sans erreur (pas de crash, pas de blank screen)
|
|
- Pas d'erreurs dans la console navigateur (JS errors, failed network requests)
|
|
- Tous les éléments visuels attendus sont présents et visibles
|
|
- Le layout est correct (pas d'overflow, pas d'éléments qui se chevauchent)
|
|
|
|
b) FONCTIONNALITÉS SPÉCIFIQUES À CETTE PAGE
|
|
{checks_block}
|
|
|
|
c) RÉSEAU & API
|
|
- Tous les appels API réussissent (pas de 4xx/5xx inattendus)
|
|
- Les données affichées correspondent aux réponses API
|
|
- Gestion correcte du loading state
|
|
- Gestion correcte des erreurs réseau
|
|
|
|
d) SÉCURITÉ
|
|
- Pas de fuite d'informations sensibles (tokens, emails dans l'URL, clés API)
|
|
- Les données d'un autre utilisateur ne sont pas accessibles
|
|
- Les actions protégées nécessitent bien l'authentification
|
|
- CSRF / XSS : vérifier les inputs utilisateur
|
|
{" - Vérifier que les rôles non-autorisés reçoivent bien un 403 ou redirect" if route.role != "user" else ""}
|
|
|
|
e) ACCESSIBILITÉ (a11y)
|
|
- Tous les éléments interactifs ont un accessible name descriptif (pas juste "Checkbox" ou "Button")
|
|
- Les labels sont associés aux inputs
|
|
- Le focus order est logique (Tab navigation)
|
|
- Les aria-labels/aria-describedby sont présents et pertinents
|
|
- Contraste suffisant sur les textes
|
|
|
|
f) INTERNATIONALISATION (i18n)
|
|
- Pas de mélange de langues (tout FR ou tout EN, pas les deux)
|
|
- Les traductions sont complètes (pas de clés i18n brutes affichées)
|
|
|
|
g) RESPONSIVE
|
|
- Tester en viewport mobile (375px), tablet (768px), desktop (1280px)
|
|
- Pas d'éléments qui débordent
|
|
- Navigation mobile fonctionnelle
|
|
|
|
2. FORMAT DU RAPPORT :
|
|
|
|
Produis un rapport structuré exactement comme suit :
|
|
|
|
```
|
|
Rapport exhaustif des erreurs — Page {route.name} ({route.path})
|
|
Testé avec le(s) compte(s) : {accounts_str}
|
|
Date : [date du test]
|
|
|
|
───
|
|
ERREURS CRITIQUES (bloquantes)
|
|
|
|
BUG #1 — [Titre court]
|
|
- Sévérité: CRITIQUE
|
|
- Section: [section de la page]
|
|
- Repro: [étapes de reproduction]
|
|
- Erreur: [description technique]
|
|
- Source: [fichier:ligne si identifiable]
|
|
- Impact: [impact utilisateur]
|
|
|
|
───
|
|
ERREURS HAUTES
|
|
|
|
...
|
|
|
|
───
|
|
ERREURS MOYENNES
|
|
|
|
...
|
|
|
|
───
|
|
ERREURS FAIBLES
|
|
|
|
...
|
|
|
|
───
|
|
RÉSUMÉ
|
|
|
|
┌────────────────────────────┬─────────┬──────────────┐
|
|
│ Catégorie │ Nombre │ Sévérité max │
|
|
├────────────────────────────┼─────────┼──────────────┤
|
|
│ ... │ ... │ ... │
|
|
└────────────────────────────┴─────────┴──────────────┘
|
|
```
|
|
|
|
3. CORRECTION DES BUGS :
|
|
|
|
Après avoir dressé la liste, corrige TOUS les problèmes trouvés pour que la page soit
|
|
entièrement fonctionnelle. Pour chaque fix :
|
|
- Identifie le fichier source exact
|
|
- Applique le correctif
|
|
- Vérifie avec Playwright que le bug est résolu
|
|
|
|
4. TRANSFORMATION EN TESTS E2E :
|
|
|
|
Transforme toute la suite de vérification que tu viens de faire pour la page {route.path} en suite de
|
|
tests Playwright que tu peux ajouter directement à ceux existants dans {E2E_TEST_DIR}.
|
|
|
|
Spécifications des tests :
|
|
- Fichier : {E2E_TEST_DIR}tests/{_route_to_filename(route.path)}.spec.ts
|
|
- Framework : Playwright Test (@playwright/test)
|
|
- Pattern : Page Object Model si la page est complexe
|
|
- Chaque bug trouvé ET corrigé = au moins 1 test de non-régression
|
|
- Chaque fonctionnalité testée manuellement = 1 test automatisé
|
|
- Tests organisés par describe() : "Chargement", "Fonctionnalités", "Sécurité", "a11y", "i18n"
|
|
- Les tests doivent être indépendants (setup/teardown propre)
|
|
- Utiliser les test accounts définis ci-dessus pour l'auth dans les fixtures
|
|
|
|
Structure attendue :
|
|
```typescript
|
|
import {{ test, expect }} from '@playwright/test';
|
|
|
|
test.describe('{route.name} ({route.path})', () => {{
|
|
test.describe('Chargement & Rendu', () => {{
|
|
test('la page se charge sans erreur', async ({{ page }}) => {{
|
|
// ...
|
|
}});
|
|
}});
|
|
|
|
test.describe('Fonctionnalités', () => {{
|
|
// Un test par fonctionnalité
|
|
}});
|
|
|
|
test.describe('Sécurité', () => {{
|
|
// Tests de sécurité
|
|
}});
|
|
|
|
test.describe('Accessibilité', () => {{
|
|
// Tests a11y
|
|
}});
|
|
|
|
test.describe('Régression', () => {{
|
|
// Un test par bug corrigé
|
|
}});
|
|
}});
|
|
```
|
|
|
|
Assure-toi que les nouveaux tests s'intègrent avec la config Playwright existante
|
|
et ne dupliquent pas des tests déjà présents.
|
|
"""
|
|
return prompt.strip()
|
|
|
|
|
|
def _route_to_filename(path: str) -> str:
|
|
"""Convertit un path comme /admin/moderation en admin-moderation."""
|
|
clean = path.strip("/").replace("/", "-").replace(":", "")
|
|
if not clean:
|
|
clean = "root-redirect"
|
|
return clean
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# Output functions
|
|
# ─────────────────────────────────────────────
|
|
|
|
def write_single_prompt(route: Route, output_dir: str) -> str:
|
|
"""Écrit un prompt dans un fichier et retourne le chemin."""
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
filename = f"{route.number:02d}-{_route_to_filename(route.path)}.md"
|
|
filepath = os.path.join(output_dir, filename)
|
|
prompt = build_prompt(route)
|
|
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
f.write(prompt)
|
|
|
|
return filepath
|
|
|
|
|
|
def write_combined(routes: list[Route], output_dir: str) -> str:
|
|
"""Écrit tous les prompts dans un seul fichier."""
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
filepath = os.path.join(output_dir, "ALL-PROMPTS.md")
|
|
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
f.write(f"# VEZA — Prompts d'audit exhaustif pour les {len(routes)} routes\n")
|
|
f.write(f"# Généré le {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
f.write(f"# Usage : copier-coller chaque section dans Claude Code une par une.\n\n")
|
|
|
|
for route in routes:
|
|
f.write(f"\n{'='*80}\n")
|
|
f.write(f"# ROUTE {route.number:02d}/{len(routes)} — {route.path} ({route.name})\n")
|
|
f.write(f"{'='*80}\n\n")
|
|
f.write(build_prompt(route))
|
|
f.write("\n\n")
|
|
|
|
return filepath
|
|
|
|
|
|
def list_routes(routes: list[Route]):
|
|
"""Affiche toutes les routes."""
|
|
categories = {}
|
|
for r in routes:
|
|
categories.setdefault(r.category, []).append(r)
|
|
|
|
cat_names = {
|
|
"public": "Pages publiques",
|
|
"main_nav": "Navigation principale",
|
|
"playlists": "Playlists & Tracks",
|
|
"social": "Social & Communication",
|
|
"marketplace": "Marketplace & Commerce",
|
|
"creator": "Créateur & Analytics",
|
|
"account": "Compte & Paramètres",
|
|
"learning": "Apprentissage & Support",
|
|
"admin": "Administration",
|
|
"developer": "Développeur",
|
|
"redirect": "Redirections",
|
|
"error": "Pages d'erreur",
|
|
}
|
|
|
|
for cat, cat_routes in categories.items():
|
|
print(f"\n ── {cat_names.get(cat, cat)} ──")
|
|
for r in cat_routes:
|
|
auth = "🔓" if not r.auth_required else f"🔒 {r.role}"
|
|
print(f" {r.number:2d}. {r.path:<30s} {r.name:<35s} {auth}")
|
|
|
|
print(f"\n Total : {len(routes)} routes")
|
|
|
|
|
|
def get_batch(routes: list[Route], batch_num: int, batch_size: int = 5) -> list[Route]:
|
|
"""Retourne un sous-ensemble de routes par batch."""
|
|
start = (batch_num - 1) * batch_size
|
|
end = start + batch_size
|
|
return routes[start:end]
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# CLI
|
|
# ─────────────────────────────────────────────
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="VEZA — Générateur de prompts Claude Code pour audit exhaustif",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Exemples :
|
|
python veza-prompt-generator.py # Tous les prompts → ./prompts/
|
|
python veza-prompt-generator.py --route /settings # Un seul prompt
|
|
python veza-prompt-generator.py --route /admin # Un seul prompt
|
|
python veza-prompt-generator.py --list # Liste les routes
|
|
python veza-prompt-generator.py --batch 3 # Routes 11-15
|
|
python veza-prompt-generator.py --batch 3 --size 10 # Routes 21-30
|
|
python veza-prompt-generator.py --combined # Tout dans un fichier
|
|
python veza-prompt-generator.py --category admin # Toutes les routes admin
|
|
python veza-prompt-generator.py --out ./my-prompts # Dossier custom
|
|
python veza-prompt-generator.py --print /settings # Affiche dans le terminal
|
|
""",
|
|
)
|
|
parser.add_argument("--route", type=str, help="Génère le prompt pour une route spécifique (ex: /settings)")
|
|
parser.add_argument("--list", action="store_true", help="Liste toutes les routes")
|
|
parser.add_argument("--batch", type=int, help="Numéro de batch (1-indexed)")
|
|
parser.add_argument("--size", type=int, default=5, help="Taille d'un batch (défaut: 5)")
|
|
parser.add_argument("--combined", action="store_true", help="Génère un seul fichier avec tous les prompts")
|
|
parser.add_argument("--category", type=str, help="Filtre par catégorie (public, admin, social, ...)")
|
|
parser.add_argument("--out", type=str, default=OUTPUT_DIR, help=f"Dossier de sortie (défaut: {OUTPUT_DIR})")
|
|
parser.add_argument("--print", dest="print_route", type=str, help="Affiche le prompt dans le terminal au lieu de l'écrire")
|
|
parser.add_argument("--critical-only", action="store_true", help="Uniquement les routes avec auth/rôle spécifique")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# ── List ──
|
|
if args.list:
|
|
list_routes(ROUTES)
|
|
return
|
|
|
|
# ── Print single ──
|
|
if args.print_route:
|
|
route = next((r for r in ROUTES if r.path == args.print_route), None)
|
|
if not route:
|
|
print(f"❌ Route '{args.print_route}' non trouvée.", file=sys.stderr)
|
|
print(" Utilise --list pour voir toutes les routes.", file=sys.stderr)
|
|
sys.exit(1)
|
|
print(build_prompt(route))
|
|
return
|
|
|
|
# ── Single route ──
|
|
if args.route:
|
|
route = next((r for r in ROUTES if r.path == args.route), None)
|
|
if not route:
|
|
print(f"❌ Route '{args.route}' non trouvée.", file=sys.stderr)
|
|
print(" Utilise --list pour voir toutes les routes.", file=sys.stderr)
|
|
sys.exit(1)
|
|
filepath = write_single_prompt(route, args.out)
|
|
print(f"✅ Prompt généré : {filepath}")
|
|
return
|
|
|
|
# ── Filter by category ──
|
|
routes = ROUTES
|
|
if args.category:
|
|
routes = [r for r in ROUTES if r.category == args.category]
|
|
if not routes:
|
|
print(f"❌ Catégorie '{args.category}' non trouvée.", file=sys.stderr)
|
|
cats = sorted(set(r.category for r in ROUTES))
|
|
print(f" Catégories disponibles : {', '.join(cats)}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if args.critical_only:
|
|
routes = [r for r in routes if r.role != "user" or not r.auth_required]
|
|
|
|
# ── Batch ──
|
|
if args.batch:
|
|
routes = get_batch(routes, args.batch, args.size)
|
|
if not routes:
|
|
total_batches = (len(ROUTES) + args.size - 1) // args.size
|
|
print(f"❌ Batch {args.batch} vide. Batches disponibles : 1-{total_batches}", file=sys.stderr)
|
|
sys.exit(1)
|
|
print(f"📦 Batch {args.batch} ({len(routes)} routes)")
|
|
|
|
# ── Combined ──
|
|
if args.combined:
|
|
filepath = write_combined(routes, args.out)
|
|
print(f"✅ Fichier combiné généré : {filepath} ({len(routes)} prompts)")
|
|
return
|
|
|
|
# ── All individual files ──
|
|
os.makedirs(args.out, exist_ok=True)
|
|
for route in routes:
|
|
filepath = write_single_prompt(route, args.out)
|
|
print(f" ✅ {route.number:2d}. {route.path:<30s} → {filepath}")
|
|
|
|
print(f"\n🎉 {len(routes)} prompts générés dans {args.out}/")
|
|
print(f" Copie chaque prompt dans Claude Code un par un.")
|
|
total_batches = (len(routes) + args.size - 1) // args.size
|
|
print(f" Ou utilise --batch N (1-{total_batches}) pour y aller par groupes de {args.size}.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|