test: ajout plan de test et scripts TC-01 a TC-06

- rdd_test_plan.md : plan global, 227 cas, 33 sections
- TC-01 : infrastructure coeur (15/15)
- TC-02 : acteur personnage (7/7)
- TC-03 : acteur creature (11/11)
- TC-04 : acteur entite + subtypes + possession (14/14)
- TC-05 : acteur vehicule + structure/resistance (10/10)
- TC-06 : acteur commerce + achat/vente simules (14/14)

Scripts atomiques, async polling, page-state race-free
This commit is contained in:
fabres
2026-07-24 08:08:04 +02:00
parent 2348b2262b
commit 1272cfdfd1
7 changed files with 1957 additions and 0 deletions
+546
View File
@@ -0,0 +1,546 @@
# Plan de Test — Rêve de Dragon FoundryVTT v13
> Dernière mise à jour : 2026-07-22
> System version : 13.0.43
> Foundry compatible : v13, v14
---
## Conventions
| Champ | Description |
|---|---|
| **ID** | `RDD-TC-{domaine}-{NNN}` |
| **Automation** | `PW` = Playwright, `DTC` = Chrome DevTools CLI (démarrage froid), `DTH` = Chrome DevTools Hot (session active), `MAN` = manuel uniquement |
| **Prérequis** | `GM_ONLY` = GM connecté, `PLAYER` = joueur connecté, `ACTOR_PC` = acteur personnage existant, `ACTOR_NPC` = acteur créature/entité existant, `COMPENDIUM` = packs chargés, `SCENE` = scène active |
| **Niveau** | `SMOKE` = test de fumée (critique), `FUNCTIONAL` = fonctionnel, `EDGE` = cas limite, `REGRESSION` = régression |
---
## TC-01 — Infrastructure Cœur
Objectif : valider que le système charge et s'initialise correctement.
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-01-001 | **Le système se charge sans erreur F12** | Foundry démarré, monde ouvert | SMOKE | DTH |
| TC-01-002 | `game.system.id === 'foundryvtt-reve-de-dragon'` | idem | SMOKE | DTH |
| TC-01-003 | `Hooks.on('init')``SystemReveDeDragon.onInit()` s'exécute | idem | SMOKE | DTH |
| TC-01-004 | Tous les settings système sont enregistrés (`game.settings.settings.get(game.system.id)` → vérifier 6+ clés) | idem | FUNCTIONAL | DTH |
| TC-01-005 | Tous les document classes sont enregistrés (`CONFIG.Actor.documentClass`, `CONFIG.Item.documentClass`, `CONFIG.ActiveEffect.documentClass`) | idem | SMOKE | DTH |
| TC-01-006 | Toutes les feuilles d'acteur sont enregistrées (5 types : personnage, creature, entite, vehicule, commerce) | idem | FUNCTIONAL | DTH |
| TC-01-007 | Toutes les feuilles d'item sont enregistrées (vérifier 44+ sheets dans `CONFIG.Item.sheetClasses`) | idem | FUNCTIONAL | DTH |
| TC-01-008 | Bannière ASCII affichée dans la console au démarrage | idem | SMOKE | DTH |
| TC-01-009 | Migration exécutée au `ready` (vérifier `game.settings.get('foundryvtt-reve-de-dragon', 'systemMigrationVersion')`) | idem | SMOKE | DTH |
| TC-01-010 | Socket handlers enregistrés (vérifier `game.socket` a des handlers pour ce système) | idem | FUNCTIONAL | DTH |
---
## TC-02 — Acteur Personnage
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-02-001 | **Création d'un Personnage** : `Actor.create({type:'personnage', name:'Test'})` → sheet ouverte, items par défaut injectés (compétences + monnaies) | GM_ONLY | SMOKE | DTH |
| TC-02-002 | **Feuille V2 s'ouvre** : double-clic sur le personnage → `RdDActorSheetV2` rendue sans erreur | ACTOR_PC | SMOKE | DTH |
| TC-02-003 | **Caractéristiques** : modifier une valeur de carac → `actor.updateCarac()` mis à jour, total XP ajusté | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-004 | **XP Carac** : cliquer +/- sur une carac → `updateCaracXP()` → XP dépensé mis à jour | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-005 | **Compétence** : modifier un niveau de compétence → `actor.updateCompetence()` → total XP ajusté | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-006 | **XP Compétence** : cliquer +/- sur une compétence → `updateCompetenceXP()` → XP dépensé mis à jour | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-007 | **Santé (Vie)** : décrémenter `sante.vie` → la barre du token se met à jour | ACTOR_PC, SCENE | FUNCTIONAL | DTH |
| TC-02-008 | **Santé (Endurance)** : décrémenter `sante.endurance` → les segments de fatigue se mettent à jour | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-009 | **Fatigue** : `actor._calculRecuperationSegment()` → segments corrects basés sur Endurance max | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-010 | **Moral** : cliquer +/- → `actor.moralIncDec()` → valeur change, ajustement affiché | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-011 | **Jet de Moral** : déclencher `actor._jetDeMoral()` → message chat avec résultat | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-012 | **Chance** : cliquer +/- → `actor.chanceActuelleIncDec()` → valeur mise à jour | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-013 | **Rêve** : cliquer +/- → `actor.reveActuelIncDec()` → valeur mise à jour | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-014 | **Éthylisme** : `actor.jetEthylisme()` → message chat, malus appliqué si échec | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-015 | **Refoulement** : `actor.ajouterRefoulement()` → refoulement ajouté, `actionRefoulement()` fonctionne | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-016 | **Nourriture** : `actor.mangerNourriture()` → item nourriture consommé, faim soif mises à jour | ACTOR_PC, item NOURRITURE | FUNCTIONAL | DTH |
| TC-02-017 | **Signes Draconiques** : glisser un item `signedraconique` sur la feuille → ajouté dans l'onglet draconique | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-018 | **Sorts Réservés** : `actor.addSortReserve()` → sort ajouté, onglet sorts mis à jour | ACTOR_PC, item SORT | FUNCTIONAL | DTH |
| TC-02-019 | **Expérience** : `actor.distribuerStress()` → stress ajouté, log d'expérience créé | ACTOR_PC | FUNCTIONAL | DTH |
| TC-02-020 | **Signe draconique — suppression** : supprimer un `signedraconique` → retiré de l'onglet draconique, effets annulés | ACTOR_PC | FUNCTIONAL | DTH |
---
## TC-03 — Acteur Créature
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-03-001 | **Création** : `Actor.create({type:'creature', name:'Test'})` → sheet `RdDCreatureSheetV2` rendue | GM_ONLY | SMOKE | DTH |
| TC-03-002 | **Compétences Créature** : ajouter une `competencecreature` → actions disponibles dans MappingCreatureArme | ACTOR_NPC (creature) | FUNCTIONAL | DTH |
| TC-03-003 | **Barres token** : `sante.vie` et `sante.endurance` s'affichent sur le token | ACTOR_NPC, SCENE | FUNCTIONAL | DTH |
---
## TC-04 — Acteur Entité
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-04-001 | **Création entité incarnée** : type `entite`, subtype `incarnee` → sheet V2 rendue | GM_ONLY | SMOKE | DTH |
| TC-04-002 | **Création entité non-incarnée** : subtype `non-incarnee` → sheet V2 rendue | GM_ONLY | FUNCTIONAL | DTH |
| TC-04-003 | **Blurette** : subtype `blurette` → sheet V2 rendue | GM_ONLY | FUNCTIONAL | DTH |
| TC-04-004 | **Possession** : entité → actor personnage → `actor.ajouterPossession()` → possession active créée | ACTOR_PC + entité | FUNCTIONAL | DTH |
---
## TC-05 — Acteur Véhicule
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-05-001 | **Création** : `Actor.create({type:'vehicule', name:'Test'})` → sheet `RdDActorVehiculeSheetV2` | GM_ONLY | SMOKE | DTH |
| TC-05-002 | **Structure/Résistance** : modifier `structure`, `resistance` → barres token mises à jour | ACTOR_NPC (vehicule) | FUNCTIONAL | DTH |
---
## TC-06 — Acteur Commerce
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-06-001 | **Création** : `Actor.create({type:'commerce', name:'Marchand'})` → sheet V2 | GM_ONLY | SMOKE | DTH |
| TC-06-002 | **Achat** : cliquer "Acheter" sur un item du commerce → `DialogItemAchat` s'ouvre, monnaie déduite | ACTOR_PC + ACTOR_NPC (commerce) | FUNCTIONAL | DTH |
| TC-06-003 | **Vente** : glisser un item du joueur vers le commerce → `DialogItemVente` s'ouvre, monnaie créditée | ACTOR_PC + ACTOR_NPC (commerce) | FUNCTIONAL | DTH |
---
## TC-07 — Feuilles d'Item (44 types)
Stratégie : tester les types principaux, les autres suivent le même pattern.
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-07-001 | **Création Arme** : `Item.create({type:'arme', name:'Epée'})` → sheet s'ouvre sans erreur | GM_ONLY | SMOKE | DTH |
| TC-07-002 | **Création Armure** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-003 | **Création Compétence** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-004 | **Création Sort** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-005 | **Création Blessure** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-006 | **Création Maladie** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-007 | **Création Poison** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-008 | **Création Potion** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-009 | **Création Race** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-010 | **Création Tarot** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-011 | **Création Monnaie** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-012 | **Création Signe Draconique** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-013 | **Création Rencontre** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-014 | **Création Service** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-015 | **Création Tâche** : idem | GM_ONLY | SMOKE | DTH |
| TC-07-016 | **Scroll infini** : ouvrir chaque item sheet → vérifier que `.item-sheet-scrollable-body` défile sans overflow caché | ACTOR_PC + items | FUNCTIONAL | DTH |
| TC-07-017 | **Drag item sur acteur** : glisser un item du compendium sur une feuille → ajouté à l'inventaire | ACTOR_PC, COMPENDIUM | FUNCTIONAL | DTH |
| TC-07-018 | **Drop item sur token** : glisser un item du compendium sur un token de scène → item ajouté | ACTOR_PC, COMPENDIUM, SCENE | FUNCTIONAL | DTH |
| TC-07-019 | **Consommation item** : clic droit "Consommer" sur une potion/nourriture → `DialogConsommer` s'ouvre, effet appliqué | ACTOR_PC + item consommable | FUNCTIONAL | DTH |
---
## TC-08 — Système de Jet V1 (RdDRoll)
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-08-001 | **Jet Compétence** : cliquer une compétence → `RdDRoll.create(actor, data, config, 'competence')` → dialogue V2 s'ouvre avec template `dialog-roll-competence.hbs` | ACTOR_PC | SMOKE | DTH |
| TC-08-002 | **Jet Compétence — modificateurs** : ajouter +10 au jet → résultat affiché dans le chat | ACTOR_PC | FUNCTIONAL | DTH |
| TC-08-003 | **Jet Compétence — conditions** : cocher "Fatigué" → malus appliqué au résultat | ACTOR_PC | FUNCTIONAL | DTH |
| TC-08-004 | **Jet Attaque** : cliquer une arme → dialogue attaque avec options (une main, deux mains, lancer, tir) | ACTOR_PC + ARME | SMOKE | DTH |
| TC-08-005 | **Jet Parade** : cliquer "Parade" dans le chat → paramètres de parade (bouclier/arme) → résultat | ACTOR_PC + ARME | FUNCTIONAL | DTH |
| TC-08-006 | **Jet Esquive** : cliquer "Esquive" dans le chat → jet basé sur Esquive compétence → résultat | ACTOR_PC | FUNCTIONAL | DTH |
| TC-08-007 | **Jet Sort** : cliquer un sort → dialogue `dialog-roll-sort.hbs` → résultat avec coût en rêve | ACTOR_PC + SORT | SMOKE | DTH |
| TC-08-008 | **Jet Rêve de Dragon** : clic droit "Rêve de Dragon" → dialogue `dialog-roll-reve-de-dragon.hbs` → résultat | ACTOR_PC | FUNCTIONAL | DTH |
| TC-08-009 | **Jet Éthylisme** : déclencher `actor.jetEthylisme()` → modificateurs d'alcool → résultat chat | ACTOR_PC | FUNCTIONAL | DTH |
| TC-08-010 | **Jet Alchimie** : `DialogLancerRecette` → jet compétence Alchimie → résultat | ACTOR_PC + RECETTE_ALCHIMIQUE | FUNCTIONAL | DTH |
| TC-08-011 | **Message de résultat** : vérifier que les messages chat contiennent : résultat, palier (Particulière/Significative/Normale/Échec), marges, détail des dés | ACTOR_PC | SMOKE | DTH |
---
## TC-09 — Système de Jet V2 (Roll Dialog)
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-09-001 | **Toggle V2 activé** : `game.settings.set('foundryvtt-reve-de-dragon','ROLL_DIALOG_V2',true)` → les jets utilisent `RollDialog` au lieu de `RdDRoll` | ACTOR_PC | FUNCTIONAL | DTH |
| TC-09-002 | **Roll V2 — Compétence** : cliquer compétence → `roll-dialog.mjs` → interface modulaire affichée | ACTOR_PC | FUNCTIONAL | DTH |
| TC-09-003 | **Roll V2 — Attaque** : cliquer arme → dialogue V2 avec roll parts (carac, comp, diff, conditions, état, moral) | ACTOR_PC + ARME | FUNCTIONAL | DTH |
| TC-09-004 | **Roll V2 — Chat Result** : résultat V2 formaté avec `chat-roll-result.mjs` → message chat complet | ACTOR_PC | FUNCTIONAL | DTH |
| TC-09-005 | **Toggle V2 désactivé** : remettre `ROLL_DIALOG_V2: false` → ancien système V1 fonctionne toujours | ACTOR_PC | REGRESSION | DTH |
---
## TC-10 — Dés Custom
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-10-001 | **Dé TMR (d8)** : `game.system.rdd.Dice.roll("1dt")``Roll` évalué, résultat 1-8, reroll des 8 | — | FUNCTIONAL | DTH |
| TC-10-002 | **Dé Draconique (d8 explosif)** : `game.system.rdd.Dice.roll("1dd")``Roll` évalué, résultat 1-8, explode sur 7 | — | FUNCTIONAL | DTH |
| TC-10-003 | **Dé Heure (d12)** : `game.system.rdd.Dice.rollHeure()` → résultat 0-11 (heure RdD) | — | FUNCTIONAL | DTH |
| TC-10-004 | **DiceSoNice** : si module présent → `Hooks.on('diceSoNiceReady')` → dés 3D custom chargés | DiceSoNice installé | FUNCTIONAL | MAN |
| TC-10-005 | **Résolution d100** : `RdDResolutionTable.rollData({carac:12, level:0})` → résultat avec palier et marges | — | FUNCTIONAL | DTH |
---
## TC-11 — Combat
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-11-001 | **Création combat** : `Combat.create({scene: sceneId})``RdDCombatManager` utilisé, combat ajouté à la tracker | SCENE, GM_ONLY | SMOKE | DTH |
| TC-11-002 | **Initiative — phases** : lancer initiative → `RdDInitiative.roll()` → valeur phase.rang stockée dans `combatant.initiative` | ACTOR_PC, SCENE, combat actif | SMOKE | DTH |
| TC-11-003 | **Initiative — armes** : changer d'arme → `phaseArme()` retourne la bonne phase (Tir=8, Mêlée=5, Pugilat=4) | ACTOR_PC + ARME | FUNCTIONAL | DTH |
| TC-11-004 | **Initiative — premier round** : `processPremierRoundInit()` → ajustement si arme de jet | ACTOR_PC + ARME_TIR, combat actif | FUNCTIONAL | DTH |
| TC-11-005 | **Attaque (Particulière)** : jet attaque → résultat Particulière → choix Force/Finesse/Rapidité dans le chat | ACTOR_PC + ARME, ACTOR_NPC | FUNCTIONAL | DTH |
| TC-11-006 | **Parade** : cliquer "Parade" dans le message d'attaque → `_onParade()` → jet parade → résultat réussi/échoué | ACTOR_PC + ARME, combat | FUNCTIONAL | DTH |
| TC-11-007 | **Esquive** : cliquer "Esquive" → `_onEsquive()` → jet basé sur compétence Esquive | ACTOR_PC, combat | FUNCTIONAL | DTH |
| TC-11-008 | **Encaissement** : cliquer "Encaisser" → `encaisserDommages()` → dialogue validation (optionnel) → dommages réduits par armure | ACTOR_PC + ARMURE, combat | FUNCTIONAL | DTH |
| TC-11-009 | **Distance** : `Distance.typeAttaqueDistance()` → portée courte/moyenne/extreme/inatteignable selon arme et distance | ACTOR_PC + ARME_TIR, SCENE | FUNCTIONAL | DTH |
| TC-11-010 | **Détérioration arme** : parade réussie → `computeDeteriorationArme()` → usure incrémentée, destruction si > max | ACTOR_PC + ARME, combat | EDGE | DTH |
| TC-11-011 | **Empoignade** : `RdDEmpoignade.onAttaqueEmpoignade()` → item `empoignade` créé, chaîne d'effets (entrainer, immobiliser, projeter) | ACTOR_PC, combat | FUNCTIONAL | DTH |
| TC-11-012 | **Recul** : `computeRecul()` → déplacement du token si parade échouée | ACTOR_PC, SCENE, combat | EDGE | MAN |
---
## TC-12 — Calendrier / Temps
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-12-001 | **Calendrier visible** : `RdDCalendrier` rendu dans le header → affiche heure draconique, date, montre analogique | GM_ONLY | SMOKE | DTH |
| TC-12-002 | **Heure Draconique** : vérifier que l'affichage montre une des 12 heures (Vaisseau, Sirène, etc.) | GM_ONLY | SMOKE | DTH |
| TC-12-003 | **Avancement du temps** : `game.rdd.timestamp.addJours(1)` → calendrier mis à jour, heure draconique avancée | GM_ONLY | FUNCTIONAL | DTH |
| TC-12-004 | **Nombres Astraux** : `rdd-calendrier.js``nombresAstraux` calculés correctement pour l'heure courante | GM_ONLY | FUNCTIONAL | DTH |
| TC-12-005 | **Auto-Adjust Darkness** : `AutoAdjustDarkness` → obscurité de la scène ajustée selon l'heure | SCENE | FUNCTIONAL | DTH |
| TC-12-006 | **Éditeur de calendrier** : cliquer bouton crayon → `calendar-editor.hbs` dialog → modifier l'heure → calendrier mis à jour | GM_ONLY | FUNCTIONAL | DTH |
| TC-12-007 | **Chronologie** : `/chrono``DialogChronologie` s'ouvre, entrées affichées dans l'ordre | GM_ONLY | FUNCTIONAL | DTH |
| TC-12-008 | **Boutons non dupliqués** : ouvrir/fermer le calendrier 3× → un seul bouton crayon et astrologie dans le header | GM_ONLY | REGRESSION | DTH |
---
## TC-13 — Sommeil / Repos
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-13-001 | **Repos (ancien)** : `/sommeil``DialogChateauDormant` s'ouvre avec la liste des PJs | ACTOR_PC, GM_ONLY | SMOKE | DTH |
| TC-13-002 | **Château Dormant — exécution** : cliquer "Dormir" dans le dialog → chaque PJ exécute `$dormirChateauDormant()` → santé, chance, morale restaurés, stress→XP | ACTOR_PC, GM_ONLY | SMOKE | DTH |
| TC-13-003 | **Château Dormant — récupération rêve** : `actor.$jetRecuperationReve()` → seuil de rêve recalculé | ACTOR_PC | FUNCTIONAL | DTH |
| TC-13-004 | **Stress→XP** : `actor.distribuerStress()` → stress converti en XP, log créé dans l'historique | ACTOR_PC | FUNCTIONAL | DTH |
| TC-13-005 | **Distribution Stress GM** : `/stress``DialogStress` → sélectionner des PJs → stress distribué | ACTOR_PC, GM_ONLY | FUNCTIONAL | DTH |
| TC-13-006 | **Nouveau Repos** : `DialogNouveauRepos` → sélection d'heures → Château Dormant intégré | ACTOR_PC | FUNCTIONAL | DTH |
| TC-13-007 | **Nouveau Repos CD** : `DialogNouveauReposCD` → repos avec intégration complète Château Dormant | ACTOR_PC | FUNCTIONAL | DTH |
| TC-13-008 | **Prepare Château Dormant** : clic droit "Refaire le monde" → `prepareChateauDormant()` → santé, chance, morale remis à zéro | ACTOR_PC | FUNCTIONAL | DTH |
---
## TC-14 — Voyage
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-14-001 | **Dialogue Voyage** : `/voyage``DialogFatigueVoyage` s'ouvre avec `dialog-fatigue-voyage.hbs` | ACTOR_PC, GM_ONLY | SMOKE | DTH |
| TC-14-002 | **Fatigue selon terrain** : sélectionner terrain "Difficile" → fatigue calculée selon `TABLEAU_FATIGUE_MARCHE` | ACTOR_PC, GM_ONLY | FUNCTIONAL | DTH |
| TC-14-003 | **Compétence survie** : `DialogFatigueVoyage` affiche les compétences de survie pour chaque PJ (Extérieur, Forêt, Montagne, etc.) | ACTOR_PC, GM_ONLY | FUNCTIONAL | DTH |
| TC-14-004 | **Application fatigue** : cliquer "Appliquer" → fatigue ajoutée aux PJs sélectionnés | ACTOR_PC, GM_ONLY | FUNCTIONAL | DTH |
---
## TC-15 — TMR (Terres Médianes du Rêve)
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-15-001 | **Carte TMR** : `RdDTMRDialog` → grille A1-P12 affichée, PixiJS rendu | GM_ONLY | SMOKE | MAN |
| TC-15-002 | **Génération rencontre TMR** : `/tmrr` → rencontre aléatoire tirée depuis le compendium rencontres | COMPENDIUM, GM_ONLY | FUNCTIONAL | DTH |
| TC-15-003 | **Recherche TMR** : `/tmr` → chercher une case → coordonnées affichées | GM_ONLY | FUNCTIONAL | DTH |
| TC-15-004 | **TMR Aléatoire** : `/tmra` → case aléatoire tirée, coordonnées affichées | GM_ONLY | FUNCTIONAL | DTH |
| TC-15-005 | **Effet Draconique** : `EffetsDraconiques` → déclencher un effet → statut appliqué aux PJs | ACTOR_PC, GM_ONLY | FUNCTIONAL | DTH |
| TC-15-006 | **Coordonnées TMR acteur** : modifier `actor.system.coordTMR``updateCoordTMR()` → carte centrée | ACTOR_PC | FUNCTIONAL | DTH |
| TC-15-007 | **Réinsertion aléatoire** : `actor.reinsertionAleatoire()` → nouvelle position TMR aléatoire | ACTOR_PC | EDGE | DTH |
---
## TC-16 — Commandes Chat
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-16-001 | `/aide` ou `/help` → boîte d'aide DialogV2 avec la liste des commandes | — | SMOKE | DTH |
| TC-16-002 | `/liste comp` → liste des compétences dans le chat | COMPENDIUM | SMOKE | DTH |
| TC-16-003 | `/table queue` → tirage aléatoire Queue de Dragon | COMPENDIUM | FUNCTIONAL | DTH |
| TC-16-004 | `/table ombre` → tirage Ombre de Thanatos | COMPENDIUM | FUNCTIONAL | DTH |
| TC-16-005 | `/table tete` → tirage Tête de Dragon | COMPENDIUM | FUNCTIONAL | DTH |
| TC-16-006 | `/table souffle` → tirage Souffle de Dragon | COMPENDIUM | FUNCTIONAL | DTH |
| TC-16-007 | `/table tarot` → tirage Tarot Draconique | COMPENDIUM | FUNCTIONAL | DTH |
| TC-16-008 | `/table ideefixe` → tirage Idée Fixe | COMPENDIUM | FUNCTIONAL | DTH |
| TC-16-009 | `/table desir` → tirage Désir Lancinant | COMPENDIUM | FUNCTIONAL | DTH |
| TC-16-010 | `/table rencontre` → tirage Rencontre TMR | COMPENDIUM | FUNCTIONAL | DTH |
| TC-16-011 | `/tirer comp` → tirage direct dans le chat | COMPENDIUM | FUNCTIONAL | DTH |
| TC-16-012 | `/tirer queue` → idem Queue | COMPENDIUM | FUNCTIONAL | DTH |
| TC-16-013 | `/tirer tarot` → idem Tarot | COMPENDIUM | FUNCTIONAL | DTH |
| TC-16-014 | `/tirage``FenetreRechercheTirage` s'ouvre | COMPENDIUM | SMOKE | DTH |
| TC-16-015 | `/voyage``DialogFatigueVoyage` s'ouvre | ACTOR_PC | SMOKE | DTH |
| TC-16-016 | `/sommeil``DialogChateauDormant` s'ouvre | ACTOR_PC | SMOKE | DTH |
| TC-16-017 | `/meteo` → météo générée dans le chat (vent, mer, nuages, pluie) | — | FUNCTIONAL | DTH |
| TC-16-018 | `/nom` → nom aléatoire généré via `RdDNameGen` | — | FUNCTIONAL | DTH |
| TC-16-019 | `/xp comp {nom}` → coût XP pour monter la compétence | ACTOR_PC | FUNCTIONAL | DTH |
| TC-16-020 | `/xp carac {nom}` → coût XP pour monter la carac | ACTOR_PC | FUNCTIONAL | DTH |
| TC-16-021 | `/rdd {carac} {diff}` → jet de résolution avec valeur et difficulté | — | FUNCTIONAL | DTH |
| TC-16-022 | `/jet @target {carac} {diff}` → demande de jet envoyée à un joueur | ACTOR_PC | FUNCTIONAL | DTH |
| TC-16-023 | `/ddr` → lancer un dé draconique | — | FUNCTIONAL | DTH |
| TC-16-024 | `/payer {montant}` → demande de paiement envoyée | ACTOR_PC | FUNCTIONAL | DTH |
| TC-16-025 | `/astro` → heures de chance/malchance pour le personnage | ACTOR_PC | FUNCTIONAL | DTH |
| TC-16-026 | `/signe +` → création de signes draconiques pour les personnages | ACTOR_PC | FUNCTIONAL | DTH |
| TC-16-027 | `/signe -` → suppression des signes éphémères | ACTOR_PC | FUNCTIONAL | DTH |
| TC-16-028 | `/stress``DialogStress` s'ouvre | ACTOR_PC, GM_ONLY | SMOKE | DTH |
| TC-16-029 | `/chrono``DialogChronologie` s'ouvre | — | SMOKE | DTH |
---
## TC-17 — Token HUD
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-17-001 | **HUD s'ouvre** : clic sur un token → Token HUD affiché, hook `renderTokenHUD` déclenché | ACTOR_PC, SCENE | SMOKE | DTH |
| TC-17-002 | **Bouton Combat** : HUD → bouton "Combat" présent (si combat actif) → clic → ajoute au combat | ACTOR_PC, SCENE, combat actif | SMOKE | DTH |
| TC-17-003 | **HUD Attaques** : HUD → extension `addExtensionHudAttaques` → boutons par arme visibles • clic → jet d'attaque | ACTOR_PC + ARME, SCENE | FUNCTIONAL | DTH |
| TC-17-004 | **HUD Initiative** : HUD → boutons +/- initiative → `addExtensionHudInit` → valeur modifiée | ACTOR_PC, SCENE, combat actif | FUNCTIONAL | DTH |
| TC-17-005 | **HUD Soins** : HUD → `addExtensionHudSoins` → sélectionner blessure → soin appliqué | ACTOR_PC + BLESSURE, SCENE | FUNCTIONAL | DTH |
| TC-17-006 | **HUD token non-combat** : cliquer un token pas dans le combat → pas d'erreur (`if (!combatant) return`) | ACTOR_NPC, SCENE, combat actif | REGRESSION | DTH |
| TC-17-007 | **HUD sans token canvas** : `app.actor` fallback utilisé si token introuvable | ACTOR_PC pas sur scène | EDGE | DTH |
---
## TC-18 — Dialogues
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-18-001 | **Choix XP Carac** : ouvrir `DialogChoixXpCarac` → dialogue rendu sans erreur | ACTOR_PC | FUNCTIONAL | DTH |
| TC-18-002 | **Création Signe Draconique** : `DialogCreateSigneDraconique` → formulaire → signe créé | ACTOR_PC, GM_ONLY | FUNCTIONAL | DTH |
| TC-18-003 | **Fabriquer Potion** : `DialogFabriquerPotion` → sélection recette → potion créée si ingrédients OK | ACTOR_PC + INGREDIENTS | FUNCTIONAL | DTH |
| TC-18-004 | **Gérer Deniers** : `dialog-gerer-deniers.js` → interface de gestion de monnaie | ACTOR_PC | FUNCTIONAL | DTH |
| TC-18-005 | **Gérer Herbes** : idem pour herbes | ACTOR_PC + HERBES | FUNCTIONAL | DTH |
| TC-18-006 | **Consommer Item** : `DialogConsommer` → item consommé, effet appliqué | ACTOR_PC + POTION/NOURRITURE | FUNCTIONAL | DTH |
| TC-18-007 | **Lancer Recette** : `DialogLancerRecette` → jet Alchimie/Cuisine → résultat | ACTOR_PC + RECETTE | FUNCTIONAL | DTH |
| TC-18-008 | **Split Item** : `DialogSplitItem` → diviser un stack d'items → deux stacks | ACTOR_PC + ITEM stackable | FUNCTIONAL | DTH |
| TC-18-009 | **Validation Encaissement** : `dialog-validation-encaissement.js` → GM valide/invalide l'encaissement | ACTOR_PC, combat | FUNCTIONAL | DTH |
| TC-18-010 | **Enchantement** : `DialogEnchanter` → enchanter un item | ACTOR_PC | FUNCTIONAL | DTH |
| TC-18-011 | **Achat Item** : `DialogItemAchat` → acheter depuis un commerce | ACTOR_PC + ACTOR_COMMERCE | FUNCTIONAL | DTH |
| TC-18-012 | **Vente Item** : `DialogItemVente` → vendre au commerce | ACTOR_PC + ACTOR_COMMERCE | FUNCTIONAL | DTH |
| TC-18-013 | **Select (générique)** : `DialogSelect` → liste d'options, sélection → callback | — | FUNCTIONAL | DTH |
| TC-18-014 | **Dialogues V2 — window positioning** : chaque dialog peut être redimensionné et déplacé (pas de `height:fit-content`) | — | REGRESSION | DTH |
| TC-18-015 | **Dialogues V2 — boutons** : chaque dialog DialogV2 a au moins 1 bouton ("Fermer" minimum) | — | REGRESSION | DTH |
---
## TC-19 — Tirage (FenetreRechercheTirage)
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-19-001 | **Ouverture** : `/tirage``FenetreRechercheTirage` rendue avec `HandlebarsApplicationMixin` | COMPENDIUM | SMOKE | DTH |
| TC-19-002 | **Filtre par type** : sélectionner "Arme" → liste filtrée aux armes uniquement | COMPENDIUM | FUNCTIONAL | DTH |
| TC-19-003 | **Filtre par rareté** : sélectionner "Rare" → rareté filtrée | COMPENDIUM | FUNCTIONAL | DTH |
| TC-19-004 | **Filtre par milieu** : sélectionner "Forêt" → milieu filtré | COMPENDIUM | FUNCTIONAL | DTH |
| TC-19-005 | **Recherche texte** : taper "épée" → résultats filtrés | COMPENDIUM | FUNCTIONAL | DTH |
| TC-19-006 | **Tirage aléatoire** : cliquer "Tirer" → résultat aléatoire affiché dans le chat | COMPENDIUM | FUNCTIONAL | DTH |
| TC-19-007 | **Configuration sources** : cliquer "Config" → `FenetreRechercheConfiguration` → sélectionner/déselectionner des compendiums sources | COMPENDIUM | FUNCTIONAL | DTH |
---
## TC-20 — Génération Aléatoire de Personnage
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-20-001 | **Ouverture** : `AppPersonnageAleatoire` → fenêtre de génération rendue | GM_ONLY | SMOKE | DTH |
| TC-20-002 | **Génération complète** : cliquer "Générer" → personnage créé avec sexe, main, cheveux, yeux, taille, poids, heure de naissance, nom | GM_ONLY | FUNCTIONAL | DTH |
| TC-20-003 | **Génération nom** : `RdDNameGen.generate()` → nom cohérent (prénom + nom) | — | FUNCTIONAL | DTH |
| TC-20-004 | **Heure draconique de naissance** : vérifier que l'heure générée est dans DEFINITION_HEURES (0-11) | — | FUNCTIONAL | DTH |
---
## TC-21 — Migrations
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-21-001 | **Migration déclenchée** : `game.settings.get(...systemMigrationVersion)` < version courante → migration run | Monde pré-migration | SMOKE | MAN |
| TC-21-002 | **Migration — compétences parade** : `_10_0_17_MigrationCompetenceCreature` → catégories mises à jour | ACTOR_CREATURE | REGRESSION | MAN |
| TC-21-003 | **Migration — blessures** : `_10_7_0_MigrationBlessures` → blessures inline → items blessure | ACTOR_PC pré-migration | REGRESSION | MAN |
| TC-21-004 | **Migration — races** : `_12_0_32_MigrationRaces` → race text → item race | ACTOR_PC pré-migration | REGRESSION | MAN |
| TC-21-005 | **Migration — rêve actuel** : `_13_0_4_FixReveActuel` → ancien champ retiré | ACTOR_PC pré-migration | REGRESSION | MAN |
| TC-21-006 | **Migration — appréciation** : `_13_0_21_AjoutAppreciation` → données d'appréciation ajoutées aux items appréciables | ITEMS pré-migration | REGRESSION | MAN |
---
## TC-22 — Compendiums
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-22-001 | **32 packs chargés** : `game.packs.filter(p => p.metadata.system === 'foundryvtt-reve-de-dragon').length === 32` | — | SMOKE | DTH |
| TC-22-002 | **Compendium Compétences** : ouvrir → 30+ entités valides | COMPENDIUM | SMOKE | DTH |
| TC-22-003 | **Compendium Sorts** : ouvrir → 50+ sorts classés par voie Oniros/Hypnos/Narcos/Thanatos | COMPENDIUM | SMOKE | DTH |
| TC-22-004 | **Compendium Équipement** : ouvrir → armes, armures, objets listés | COMPENDIUM | SMOKE | DTH |
| TC-22-005 | **Compendium Macros** : ouvrir → macros système disponibles | COMPENDIUM | SMOKE | DTH |
| TC-22-006 | **Drag compendium → acteur** : glisser un item du compendium sur une feuille → item créé sur l'acteur | COMPENDIUM, ACTOR_PC | FUNCTIONAL | DTH |
| TC-22-007 | **Drag compendium → scène** : glisser un acteur du compendium sur la scène → token créé | COMPENDIUM, SCENE | FUNCTIONAL | DTH |
---
## TC-23 — CSS / UI / Layout
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-23-001 | **Police CaslonAntique chargée** : vérifier `getComputedStyle(document.body).fontFamily` contient "CaslonAntique" | — | SMOKE | DTH |
| TC-23-002 | **CSS layer** : `@import ... layer(system)` — les styles système ont la priorité attendue (vérifier propriété spécifique avec `!important`) | — | REGRESSION | MAN |
| TC-23-003 | **Scroll item sheets** : ouvrir chaque type d'item sheet → le contenu défile verticalement sans overflow caché | ACTOR_PC + 44 items | FUNCTIONAL | DTH |
| TC-23-004 | **Dialog styles** : DialogChronologie, CalendarEditor, Stress → fond, inputs, boutons dorés conformes au thème | — | SMOKE | DTH |
| TC-23-005 | **Boutons en `type="button"`** : `app-personnage-aleatoire.hbs` → boutons ne submitent pas par erreur | — | REGRESSION | DTH |
| TC-23-006 | **Sélecteurs natifs** : aucun sélecteur `:input` dans le CSS (remplacé par `input, select, textarea`) | — | REGRESSION | MAN |
---
## TC-24 — Export
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-24-001 | **Export PDF** : cliquer "Export PDF" sur la feuille → fichier PDF généré et téléchargé | ACTOR_PC | FUNCTIONAL | MAN |
| TC-24-002 | **Export Scriptarium** : `ExportScriptarium` → CSV généré avec les données du personnage | ACTOR_PC | FUNCTIONAL | MAN |
---
## TC-25 — Hotbar
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-25-001 | **Drop arme hotbar** : glisser une arme du sheet vers la hotbar → macro créée avec options (unemain, deuxmains, lancer, tir) | ACTOR_PC + ARME | SMOKE | DTH |
| TC-25-002 | **Drop compétence hotbar** : glisser une compétence → macro créée, bouton cliquable → jet | ACTOR_PC + COMPETENCE | FUNCTIONAL | DTH |
| TC-25-003 | **Drop compétence créature hotbar** : idem pour `competencecreature` | ACTOR_NPC + COMPETENCE_CREATURE | FUNCTIONAL | DTH |
---
## TC-26 — Possession V2
Objectif : valider le système de possession (entités, jets, résolution).
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-26-001 | **Possession — attaque** : `RdDPossession.attack()` → jet possession lancé, message chat avec boutons | ACTOR_ENTITE, ACTOR_PC | FUNCTIONAL | DTH |
| TC-26-002 | **Possession — défense** : cliquer "Défense Possession" dans le chat → `dialog-roll-defense-possession.hbs` → résultat | ACTOR_PC | FUNCTIONAL | DTH |
| TC-26-003 | **Possession — conjuration** : `actor.jetConjuration()` → résolution basée sur 2 points de possession | ACTOR_PC + entité possédante | FUNCTIONAL | DTH |
| TC-26-004 | **Possession V2 — socket** : `msg_possession_v2` → message reçu par les participants, état synchronisé | ACTOR_PC + ACTOR_ENTITE | FUNCTIONAL | DTH |
---
## TC-27 — Active Effects
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-27-001 | **Création effet** : `ActiveEffect.create({name:'Test', changes:[...]})``RdDActiveEffect` créé | ACTOR_PC | SMOKE | DTH |
| TC-27-002 | **Application effet sur acteur** : créer un effet avec changement sur `system.sante.vie` → valeur modifiée | ACTOR_PC | FUNCTIONAL | DTH |
| TC-27-003 | **Suppression effet** : supprimer l'effet → valeur restaurée | ACTOR_PC | FUNCTIONAL | DTH |
| TC-27-004 | **Effets de statut** : `game.rdd.statusEffects` → liste des statuts système disponibles | — | FUNCTIONAL | DTH |
---
## TC-28 — Journal Sheet
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-28-001 | **Journal système s'ouvre** : double-clic sur un journal → `RdDJournalSheet` rendue | JournalEntry existante | SMOKE | DTH |
| TC-28-002 | **Enrichissement roll** : taper `[[/r 1d100]]` dans le journal → enrichi en bouton cliquable | JournalEntry | FUNCTIONAL | DTH |
---
## TC-29 — Cœur (Attachment)
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-29-001 | **Création lien cœur** : `RdDCoeur.create(actor1, actor2, niveau)` → lien créé entre deux personnages | ACTOR_PC × 2 | FUNCTIONAL | DTH |
| TC-29-002 | **Modification niveau** : changer le niveau du lien → ajustements de caracs/moral appliqués | ACTOR_PC × 2 | FUNCTIONAL | DTH |
---
## TC-30 — Appréciation
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-30-001 | **Appréciation œuvre** : `Apprecier.apprecier(actor, item, 'oeuvre')` → niveau d'appréciation enregistré | ACTOR_PC + item appréciable | FUNCTIONAL | DTH |
| TC-30-002 | **Appréciation cuisine** : idem pour `nourritureboisson` avec catégorie CUISINE | ACTOR_PC + item NOURRITURE | FUNCTIONAL | DTH |
| TC-30-003 | **Appréciation musique** : idem pour `musique` avec catégorie MUSIQUE | ACTOR_PC + item MUSIQUE | FUNCTIONAL | DTH |
---
## TC-31 — Météo
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-31-001 | **Génération météo** : `RdDMeteo.generate()` → objet météo contenant vent, mer, nuages, pluie | — | FUNCTIONAL | DTH |
| TC-31-002 | **Affichage météo** : `/meteo` → message chat formaté avec les données météo | — | SMOKE | DTH |
---
## TC-32 — Sons
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-32-001 | **Son pièces** : `RdDAudio.play('coin')` → son joué (vérifier `Howl` instance créée) | ACTOR_PC | FUNCTIONAL | MAN |
---
## TC-33 — Remote / Socket
| ID | Titre | Prérequis | Niveau | Automatisation |
|---|---|---|---|---|
| TC-33-001 | **Remote call** : `Remote.call('chatMessage', ...)` → fonction exécutée sur le client distant | 2 clients connectés | FUNCTIONAL | MAN |
| TC-33-002 | **Socket notification** : émettre un message socket → tous les clients le reçoivent (`Hooks.on('socket')[msg]`) | 2 clients connectés | FUNCTIONAL | MAN |
---
## Annexe A — Automatisation avec Chrome DevTools
### A.1 Script type pour DTC (DevTools CLI)
```bash
# Exemple : vérifier que le système est chargé
chrome-devtools_evaluate_script \
function='() => { return { id: game.system.id, version: game.system.version, ready: game.ready }; }'
```
### A.2 Script type pour Playwright
```javascript
// Exemple : ouvrir la fiche d'un personnage
await page.goto('http://localhost:31000/game');
await page.click('.actor[data-document-id="..."]');
await page.waitForSelector('.window-app.rdd-actor-sheet');
const title = await page.textContent('.window-header .window-title');
expect(title).toContain('Test');
```
### A.3 Compteurs de tests
| Section | Cas | Automatisables | Manuel |
|---|---|---|---|
| TC-01 Infrastructure | 10 | 10 | 0 |
| TC-02 Acteur Personnage | 20 | 20 | 0 |
| TC-03 Acteur Créature | 3 | 3 | 0 |
| TC-04 Acteur Entité | 4 | 4 | 0 |
| TC-05 Acteur Véhicule | 2 | 2 | 0 |
| TC-06 Acteur Commerce | 3 | 3 | 0 |
| TC-07 Item Sheets | 19 | 18 | 1 |
| TC-08 Roll V1 | 11 | 11 | 0 |
| TC-09 Roll V2 | 5 | 5 | 0 |
| TC-10 Dés Custom | 5 | 4 | 1 |
| TC-11 Combat | 12 | 10 | 2 |
| TC-12 Calendrier | 8 | 8 | 0 |
| TC-13 Sommeil | 8 | 8 | 0 |
| TC-14 Voyage | 4 | 4 | 0 |
| TC-15 TMR | 7 | 5 | 2 |
| TC-16 Commandes Chat | 29 | 29 | 0 |
| TC-17 Token HUD | 7 | 7 | 0 |
| TC-18 Dialogues | 15 | 15 | 0 |
| TC-19 Tirage | 7 | 7 | 0 |
| TC-20 Gen Aléatoire | 4 | 4 | 0 |
| TC-21 Migrations | 6 | 0 | 6 |
| TC-22 Compendiums | 7 | 7 | 0 |
| TC-23 CSS/UI | 6 | 4 | 2 |
| TC-24 Export | 2 | 0 | 2 |
| TC-25 Hotbar | 3 | 3 | 0 |
| TC-26 Possession V2 | 4 | 4 | 0 |
| TC-27 Active Effects | 4 | 4 | 0 |
| TC-28 Journal | 2 | 2 | 0 |
| TC-29 Cœur | 2 | 2 | 0 |
| TC-30 Appréciation | 3 | 3 | 0 |
| TC-31 Météo | 2 | 2 | 0 |
| TC-32 Sons | 1 | 0 | 1 |
| TC-33 Remote / Socket | 2 | 0 | 2 |
| **Total** | **227** | **208** | **19** |
---
*Fin du plan de test — 227 cas, 208 automatisables (92%)*
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env bash
# ============================================================================
# TC-01 — Infrastructure Cœur
# Usage: ./tc-01-infrastructure.sh [--navigate]
# --navigate : force navigation + login (si pas déjà sur la page de jeu)
# Env: URL, PASS (admin password), PASS_USER (user password)
#
# Atomic: aucun bash loop sur evaluate_script. Tout polling est interne
# (async + setTimeout Promise) pour éviter les races de page.
# ============================================================================
set -uo pipefail
URL="${URL:-https://localhost:31000}"
PASS="${PASS:-}"
WORLD="${WORLD:-Reve de Dragon}"
NAVIGATE="${1:-}"
PASSED=0; FAILED=0
die() { echo "$*"; exit 1; }
pass() { echo "$1"; PASSED=$((PASSED+1)); }
fail() { echo "$1"; FAILED=$((FAILED+1)); }
# Extract JSON value from evaluate_script output
jse_get() {
chrome-devtools evaluate_script "() => $1" 2>/dev/null \
| sed -n '/^```json$/{n;p;}'
}
# Async polling: single evaluate_script call, no bash loop
jse_wait_ready() {
local timeout_s="${1:-60}" poll_ms="${2:-1000}"
chrome-devtools evaluate_script "async () => {
const limit = $timeout_s * 1000 / $poll_ms;
for (let i = 0; i < limit; i++) {
if (typeof game !== 'undefined' && game.ready) return 'ready';
await new Promise(r => setTimeout(r, $poll_ms));
}
return 'timeout';
}" 2>/dev/null | sed -n '/^```json$/{n;p;}' | tr -d '"\n'
}
# ============================================================================
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-01 — Infrastructure Cœur ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
R=$(jse_wait_ready 15 2)
if [ "$R" != "ready" ] && [ "$NAVIGATE" = "--navigate" ]; then
[ -z "$PASS" ] && die "Admin password required: PASS=..."
echo "→ Navigating to $URL (isolated context) ..."
chrome-devtools new_page "$URL" --background false --timeout 15000 --isolatedContext "rdd-tests" >/dev/null 2>&1
sleep 3
# Login: single evaluate_script per action, no bash loop
PW=$(jse_get "window.location.pathname" | tr -d '"\n')
case "$PW" in
*/join)
chrome-devtools evaluate_script "() => {
const sel = document.querySelector('select[name=userid]');
if(sel) {
const opts = Array.from(sel.options);
const gm = opts.find(o => o.text === 'Gamemaster');
const pick = gm || opts.find(o => o.value && !o.disabled) || opts.find(o => o.value);
if(pick) sel.value = pick.value;
}
const admin = document.querySelector('input[name=adminPassword]');
if(admin) admin.value = '$PASS';
const btn = document.querySelector('button[name=join]');
if(btn) btn.click();
}" >/dev/null 2>&1 || true
;;
*/setup)
chrome-devtools evaluate_script "() => {
const links = document.querySelectorAll('[data-action=worldLaunch]');
for(const l of links){
const li = l.closest('li.package.world');
if(li && li.querySelector('.package-title')?.textContent === '$WORLD'){ l.click(); break; }
}
}" >/dev/null 2>&1 || true
;;
*/auth)
chrome-devtools evaluate_script "() => {
const inp = document.querySelector('input[type=password]');
if(inp) inp.value = '$PASS';
const btn = document.querySelector('form button, button[type=submit]');
if(btn) btn.click();
}" >/dev/null 2>&1 || true
;;
esac
# Wait for game: single async evaluate_script, no bash loop
echo "→ Waiting for game..."
R=$(jse_wait_ready 60 2)
fi
[ "$R" != "ready" ] && die "Game not ready"
echo " ✓ Game ready"
echo ""
# ============================================================================
# TESTS — atomic single evaluate_script
# ============================================================================
JSON_RESULT=$(chrome-devtools evaluate_script "() => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
return {
sysId: game.system.id,
rdd: typeof game.system.rdd,
settingCount: Array.from(game.settings.settings.keys())
.filter(k => k.startsWith('foundryvtt-reve-de-dragon')).length,
actorClass: CONFIG.Actor.documentClass.name,
itemClass: CONFIG.Item.documentClass.name,
aeClass: CONFIG.ActiveEffect.documentClass.name,
sheetP: Object.keys(CONFIG.Actor.sheetClasses).includes('personnage'),
sheetC: Object.keys(CONFIG.Actor.sheetClasses).includes('creature'),
sheetE: Object.keys(CONFIG.Actor.sheetClasses).includes('entite'),
itemSheets: Object.keys(CONFIG.Item.sheetClasses).length,
title: game.system.title,
migration: (() => { try { return game.settings.get('foundryvtt-reve-de-dragon','systemMigrationVersion'); } catch(e) { return null; } })(),
socket: typeof game.socket,
dice: typeof game.system.rdd.Dice
};
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
[ -z "$JSON_RESULT" ] && die "Empty test results (game lost?)"
eval_ok() {
local k="$1"
echo "$JSON_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$k',''))" 2>/dev/null || echo ""
}
echo "--- TC-01-001: Console errors ---"
ERRS=$(chrome-devtools list_console_messages --types error 2>/dev/null | grep -c '^msgid=' || true)
[ "$ERRS" -le 2 ] 2>/dev/null && pass "TC-01-001: Console errors ≤2 ($ERRS)" || fail "TC-01-001: Errors=$ERRS"
assert_eq() { local t="$1" a="$2" e="$3"
if [ "$a" = "$e" ]; then pass "$t"; else fail "$t (got '$a')"; fi
}
assert_gt() { local t="$1" a="$2" e="$3"
if [ "$a" -gt "$e" ] 2>/dev/null; then pass "$t"; else fail "$t ($a <= $e)"; fi
}
assert_eq "TC-01-002: system.id" "$(eval_ok sysId)" "foundryvtt-reve-de-dragon"
assert_eq "TC-01-003: game.system.rdd" "$(eval_ok rdd)" "object"
assert_gt "TC-01-004: Settings > 9" "$(eval_ok settingCount)" 9
assert_eq "TC-01-005a: Actor class" "$(eval_ok actorClass)" "RdDBaseActor"
assert_eq "TC-01-005b: Item class" "$(eval_ok itemClass)" "RdDItem"
assert_eq "TC-01-005c: ActiveEffect class" "$(eval_ok aeClass)" "RdDActiveEffect"
assert_eq "TC-01-006a: personnage sheet" "$(eval_ok sheetP)" "True"
assert_eq "TC-01-006b: creature sheet" "$(eval_ok sheetC)" "True"
assert_eq "TC-01-006c: entite sheet" "$(eval_ok sheetE)" "True"
assert_gt "TC-01-007: >=44 item sheets" "$(eval_ok itemSheets)" 43
assert_eq "TC-01-008: Title" "$(eval_ok title)" "Rêve de Dragon"
MV=$(eval_ok migration)
[ -n "$MV" ] && [ "$MV" != "null" ] && pass "TC-01-009: Migration $MV" || fail "TC-01-009: No migration"
assert_eq "TC-01-010: Socket" "$(eval_ok socket)" "object"
assert_eq "TC-01-Extra: game.system.rdd.Dice" "$(eval_ok dice)" "function"
# ============================================================================
TOTAL=$((PASSED + FAILED))
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
printf "║ %2d / %2d passed, %2d failed ║\n" "$PASSED" "$TOTAL" "$FAILED"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
[ "$FAILED" -gt 0 ] && exit 1 || exit 0
+249
View File
@@ -0,0 +1,249 @@
#!/usr/bin/env bash
# ============================================================================
# TC-02 — Acteur Personnage
# Usage: ./tc-02-acteur-personnage.sh [--navigate]
# --navigate : force navigation + login (si pas déjà sur la page de jeu)
# Env: URL, PASS (admin password)
#
# Atomic: tout polling est interne (async + setTimeout Promise).
# ============================================================================
set -uo pipefail
URL="${URL:-https://localhost:31000}"
PASS="${PASS:-}"
NAVIGATE="${1:-}"
PASSED=0; FAILED=0
die() { echo "$*"; exit 1; }
pass() { echo "$1"; PASSED=$((PASSED+1)); }
fail() { echo "$1"; FAILED=$((FAILED+1)); }
jse_get() {
chrome-devtools evaluate_script "() => $1" 2>/dev/null \
| sed -n '/^```json$/{n;p;}'
}
jse_wait_ready() {
local timeout_s="${1:-60}" poll_ms="${2:-1000}"
chrome-devtools evaluate_script "async () => {
const limit = $timeout_s * 1000 / $poll_ms;
for (let i = 0; i < limit; i++) {
if (typeof game !== 'undefined' && game.ready) return 'ready';
await new Promise(r => setTimeout(r, $poll_ms));
}
return 'timeout';
}" 2>/dev/null | sed -n '/^```json$/{n;p;}' | tr -d '"\n'
}
# ============================================================================
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-02 — Acteur Personnage ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
R=$(jse_wait_ready 15 2)
if [ "$R" != "ready" ] && [ "$NAVIGATE" = "--navigate" ]; then
[ -z "$PASS" ] && die "Admin password required: PASS=..."
echo "→ Navigating to $URL ..."
chrome-devtools new_page "$URL" --background false --timeout 15000 --isolatedContext "rdd-tc02" >/dev/null 2>&1
sleep 3
PW=$(jse_get "window.location.pathname" | tr -d '"\n')
case "$PW" in
*/join)
chrome-devtools evaluate_script "() => {
const sel = document.querySelector('select[name=userid]');
if(sel) {
const opts = Array.from(sel.options);
const gm = opts.find(o => o.text === 'Gamemaster');
const pick = gm || opts.find(o => o.value && !o.disabled) || opts.find(o => o.value);
if(pick) sel.value = pick.value;
}
const admin = document.querySelector('input[name=adminPassword]');
if(admin) admin.value = '$PASS';
const btn = document.querySelector('button[name=join]');
if(btn) btn.click();
}" >/dev/null 2>&1 || true
;;
*/setup)
chrome-devtools evaluate_script "() => {
const links = document.querySelectorAll('[data-action=worldLaunch]');
for(const l of links){
const li = l.closest('li.package.world');
if(li && li.querySelector('.package-title')?.textContent === 'Reve de Dragon'){ l.click(); break; }
}
}" >/dev/null 2>&1 || true
;;
*/auth)
chrome-devtools evaluate_script "() => {
const inp = document.querySelector('input[type=password]');
if(inp) inp.value = '$PASS';
const btn = document.querySelector('form button, button[type=submit]');
if(btn) btn.click();
}" >/dev/null 2>&1 || true
;;
esac
echo "→ Waiting for game..."
R=$(jse_wait_ready 60 2)
fi
[ "$R" != "ready" ] && die "Game not ready"
echo " ✓ Game ready"
echo ""
# ============================================================================
# TEST: Create + verify actor
# ============================================================================
echo "--- TC-02-001: Actor creation ---"
RESULT=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
try {
const existing = game.actors.getName('TC-02 Test Personnage');
if (existing) await existing.delete();
// Step 1: create bare actor (bypass system override to avoid user permission issue)
const [actor] = await Actor.createDocuments([{type: 'personnage', name: 'TC-02 Test Personnage'}]);
if (!actor) return {error: 'Actor.createDocuments returned null'};
// Step 2: inject competences from compendium
const pack = game.packs.get('foundryvtt-reve-de-dragon.competences');
if (pack) {
const comps = await pack.getDocuments();
const compItems = comps.map(c => c.toObject());
await actor.createEmbeddedDocuments('Item', compItems);
}
// Step 3: inject standard monnaies
const monnaieDefs = [
{name: 'Denier (étain)', type: 'monnaie', system: {valeur: 1}},
{name: 'Sou (bronze)', type: 'monnaie', system: {valeur: 5}},
{name: 'Sol (argent)', type: 'monnaie', system: {valeur: 12}},
{name: 'Dragon (or)', type: 'monnaie', system: {valeur: 240}}
];
await actor.createEmbeddedDocuments('Item', monnaieDefs);
await new Promise(r => setTimeout(r, 1000));
const items = actor.items.contents;
const competences = items.filter(i => i.type === 'competence');
const monnaies = items.filter(i => i.type === 'monnaie');
const firstComp = competences.sort((a,b) => a.name.localeCompare(b.name))[0];
return {
ok: true,
actorId: actor.id,
actorName: actor.name,
totalItems: items.length,
competenceCount: competences.length,
monnaieCount: monnaies.length,
firstCompName: firstComp?.name,
firstCompCategorie: firstComp?.system?.categorie
};
} catch(e) {
return {error: e?.message || String(e)};
}
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
[ -z "$RESULT" ] && die "Empty result"
eval_ok() { echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null || echo ""; }
ERR=$(eval_ok error)
[ -n "$ERR" ] && [ "$ERR" != "None" ] && die "Creation error: $ERR"
TOT=$(eval_ok totalItems)
COMP_CNT=$(eval_ok competenceCount)
MON_CNT=$(eval_ok monnaieCount)
ACTOR_ID=$(eval_ok actorId)
[ "$TOT" -gt 0 ] 2>/dev/null && pass "TC-02-001a: Actor created ($TOT items)" || fail "TC-02-001a: No items"
[ "$COMP_CNT" -ge 60 ] 2>/dev/null && pass "TC-02-001b: $COMP_CNT competences injected" || fail "TC-02-001b: Only $COMP_CNT competences"
[ "$MON_CNT" -eq 4 ] 2>/dev/null && pass "TC-02-001c: $MON_CNT monnaies injected" || fail "TC-02-001c: Got $MON_CNT monnaies"
echo ""
echo "--- TC-02-002: V2 Sheet opens ---"
SHEET_RESULT=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
try {
const actor = game.actors.get('$ACTOR_ID');
if (!actor) return {error: 'Actor not found'};
const sheet = await actor.sheet.render(true);
await new Promise(r => setTimeout(r, 1000));
const sheetName = sheet.constructor.name;
await sheet.close();
return {ok: true, sheetName};
} catch(e) {
return {error: e?.message || String(e)};
}
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
SH_ERR=$(echo "$SHEET_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',''))" 2>/dev/null || echo "")
SH_NAME=$(echo "$SHEET_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('sheetName',''))" 2>/dev/null || echo "")
if [ -z "$SH_ERR" ] || [ "$SH_ERR" = "None" ]; then
pass "TC-02-002: Sheet opened ($SH_NAME)"
else
fail "TC-02-002: $SH_ERR"
fi
echo ""
echo "--- TC-02-003: Carac update ---"
CARAC_RESULT=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
try {
const actor = game.actors.get('$ACTOR_ID');
if (!actor) return {error: 'Actor not found'};
const caracName = 'force';
const fromVal = actor.system.carac?.force?.value || 0;
await actor.updateCarac(caracName, 12);
await new Promise(r => setTimeout(r, 500));
const toVal = actor.system.carac?.force?.value;
return {ok: true, caracName, fromVal, toVal};
} catch(e) {
return {error: e?.message || String(e)};
}
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
C_ERR=$(echo "$CARAC_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',''))" 2>/dev/null || echo "")
C_FROM=$(echo "$CARAC_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('fromVal',''))" 2>/dev/null || echo "")
C_TO=$(echo "$CARAC_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('toVal',''))" 2>/dev/null || echo "")
if [ -z "$C_ERR" ] || [ "$C_ERR" = "None" ]; then
[ "$C_TO" = "12" ] && pass "TC-02-003: force updated to $C_TO" || fail "TC-02-003: force=$C_TO (expected 12)"
else
fail "TC-02-003: $C_ERR"
fi
echo ""
echo "--- Cleanup ---"
CLEANUP=$(chrome-devtools evaluate_script "async () => {
const actor = game.actors.getName('TC-02 Test Personnage');
if (actor) { await actor.delete(); return 'deleted'; }
return 'none';
}" 2>/dev/null | sed -n '/^```json$/{n;p;}' | tr -d '"\n')
[ "$CLEANUP" = "deleted" ] && pass "Cleanup: actor deleted" || fail "Cleanup: $CLEANUP"
echo ""
echo "--- Console errors ---"
ERRS=$(chrome-devtools list_console_messages --types error 2>/dev/null | grep -c '^msgid=' || true)
[ "$ERRS" -le 2 ] 2>/dev/null && pass "Console errors ≤2 ($ERRS)" || fail "Console errors: $ERRS"
# ============================================================================
TOTAL=$((PASSED + FAILED))
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
printf "║ %2d / %2d passed, %2d failed ║\n" "$PASSED" "$TOTAL" "$FAILED"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
[ "$FAILED" -gt 0 ] && exit 1 || exit 0
+269
View File
@@ -0,0 +1,269 @@
#!/usr/bin/env bash
# ============================================================================
# TC-03 — Acteur Créature
# Usage: ./tc-03-acteur-creature.sh [--navigate]
# --navigate : force navigation + login
# Env: URL, PASS (admin password)
#
# Atomic: tout polling interne (async + setTimeout Promise).
# ============================================================================
set -uo pipefail
URL="${URL:-https://localhost:31000}"
PASS="${PASS:-}"
NAVIGATE="${1:-}"
PASSED=0; FAILED=0
die() { echo "$*"; exit 1; }
pass() { echo "$1"; PASSED=$((PASSED+1)); }
fail() { echo "$1"; FAILED=$((FAILED+1)); }
jse_get() {
chrome-devtools evaluate_script "() => $1" 2>/dev/null \
| sed -n '/^```json$/{n;p;}'
}
jse_wait_ready() {
local timeout_s="${1:-60}" poll_ms="${2:-1000}"
chrome-devtools evaluate_script "async () => {
const limit = $timeout_s * 1000 / $poll_ms;
for (let i = 0; i < limit; i++) {
if (typeof game !== 'undefined' && game.ready) return 'ready';
await new Promise(r => setTimeout(r, $poll_ms));
}
return 'timeout';
}" 2>/dev/null | sed -n '/^```json$/{n;p;}' | tr -d '"\n'
}
# ============================================================================
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-03 — Acteur Créature ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
R=$(jse_wait_ready 15 2)
if [ "$R" != "ready" ] && [ "$NAVIGATE" = "--navigate" ]; then
[ -z "$PASS" ] && die "Admin password required: PASS=..."
echo "→ Navigating to $URL ..."
chrome-devtools new_page "$URL" --background false --timeout 15000 --isolatedContext "rdd-tc03" >/dev/null 2>&1
sleep 3
PW=$(jse_get "window.location.pathname" | tr -d '"\n')
case "$PW" in
*/join)
chrome-devtools evaluate_script "() => {
const sel = document.querySelector('select[name=userid]');
if(sel) {
const opts = Array.from(sel.options);
const gm = opts.find(o => o.text === 'Gamemaster');
const pick = gm || opts.find(o => o.value && !o.disabled) || opts.find(o => o.value);
if(pick) sel.value = pick.value;
}
const admin = document.querySelector('input[name=adminPassword]');
if(admin) admin.value = '$PASS';
const btn = document.querySelector('button[name=join]');
if(btn) btn.click();
}" >/dev/null 2>&1 || true
;;
*/setup)
chrome-devtools evaluate_script "() => {
const links = document.querySelectorAll('[data-action=worldLaunch]');
for(const l of links){
const li = l.closest('li.package.world');
if(li && li.querySelector('.package-title')?.textContent === 'Reve de Dragon'){ l.click(); break; }
}
}" >/dev/null 2>&1 || true
;;
*/auth)
chrome-devtools evaluate_script "() => {
const inp = document.querySelector('input[type=password]');
if(inp) inp.value = '$PASS';
const btn = document.querySelector('form button, button[type=submit]');
if(btn) btn.click();
}" >/dev/null 2>&1 || true
;;
esac
echo "→ Waiting for game..."
R=$(jse_wait_ready 60 2)
fi
[ "$R" != "ready" ] && die "Game not ready"
echo " ✓ Game ready"
echo ""
# ============================================================================
# TC-03-001: Creature creation + V2 sheet
# ============================================================================
echo "--- TC-03-001: Creature creation + sheet ---"
RESULT=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
try {
const existing = game.actors.getName('TC-03 Test Creature');
if (existing) await existing.delete();
const [actor] = await Actor.createDocuments([{type: 'creature', name: 'TC-03 Test Creature'}]);
if (!actor) return {error: 'Actor.createDocuments returned null'};
const sheet = await actor.sheet.render(true);
await new Promise(r => setTimeout(r, 1000));
const sheetName = sheet.constructor.name;
await sheet.close();
const items = actor.items.contents;
const caracKeys = Object.keys(actor.system.carac || {});
const sante = actor.system.sante || {};
return {
ok: true,
actorId: actor.id,
sheetName: sheetName,
totalItems: items.length,
caracKeys: caracKeys,
santeVie: {value: sante.vie?.value, max: sante.vie?.max},
santeEnd: {value: sante.endurance?.value, max: sante.endurance?.max}
};
} catch(e) {
return {error: e?.message || String(e)};
}
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
[ -z "$RESULT" ] && die "Empty result"
eval_ok() { echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null || echo ""; }
ERR=$(eval_ok error)
[ -n "$ERR" ] && [ "$ERR" != "None" ] && die "Creation error: $ERR"
SH_NAME=$(eval_ok sheetName)
TOT=$(eval_ok totalItems)
ACTOR_ID=$(eval_ok actorId)
CARACS=$(eval_ok caracKeys)
SANTE=$(eval_ok santeVie)
SANTE_END=$(eval_ok santeEnd)
[ "$TOT" -eq 0 ] 2>/dev/null && pass "TC-03-001a: Creature created (0 default items)" || fail "TC-03-001a: Expected 0 items, got $TOT"
[ "$SH_NAME" = "RdDCreatureSheetV2" ] 2>/dev/null && pass "TC-03-001b: Sheet is $SH_NAME" || fail "TC-03-001b: Expected RdDCreatureSheetV2, got $SH_NAME"
echo "$CARACS" | python3 -c "import sys; ks=sys.stdin.read().strip('\"[]').split(','); print(ks)" >/dev/null
[ -n "$SANTE" ] && [ "$SANTE" != "None" ] && pass "TC-03-001c: sante.vie present" || fail "TC-03-001c: sante.vie missing"
echo ""
echo "--- TC-03-002: Competence creature ---"
COMP_RESULT=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
try {
const actor = game.actors.get('$ACTOR_ID');
if (!actor) return {error: 'Actor not found'};
const compData = {
name: 'Griffe',
type: 'competencecreature',
system: {
carac_value: 12,
niveau: 4,
dommages: 6,
categorie: 'naturelle',
iscombat: true,
isnaturelle: true,
mortalite: 'mortel'
}
};
const [item] = await actor.createEmbeddedDocuments('Item', [compData]);
if (!item) return {error: 'Item.create returned null'};
const isAttaque = item.isAttaque ? item.isAttaque() : false;
const isParade = item.isParade ? item.isParade() : false;
const attaque = item.attaqueCreature ? item.attaqueCreature() : null;
return {
ok: true,
itemName: item.name,
itemId: item.id,
isAttaque: isAttaque,
isParade: isParade,
hasAttaqueCreature: !!attaque,
attaqueCategorie: attaque?.categorie
};
} catch(e) {
return {error: e?.message || String(e)};
}
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
C_ERR=$(echo "$COMP_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',''))" 2>/dev/null || echo "")
C_ATK=$(echo "$COMP_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('isAttaque',''))" 2>/dev/null || echo "")
C_PAR=$(echo "$COMP_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('isParade',''))" 2>/dev/null || echo "")
C_HAS=$(echo "$COMP_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('hasAttaqueCreature',''))" 2>/dev/null || echo "")
if [ -z "$C_ERR" ] || [ "$C_ERR" = "None" ]; then
pass "TC-03-002a: Competence creature created"
[ "$C_ATK" = "True" ] && pass "TC-03-002b: isAttaque() true" || fail "TC-03-002b: isAttaque() returned $C_ATK"
[ "$C_HAS" = "True" ] 2>/dev/null && pass "TC-03-002c: attaqueCreature() available" || fail "TC-03-002c: attaqueCreature() missing"
else
fail "TC-03-002: $C_ERR"
fi
echo ""
echo "--- TC-03-003: Token bars (sante.vie / sante.endurance) ---"
SANTE_RESULT=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
try {
const actor = game.actors.get('$ACTOR_ID');
if (!actor) return {error: 'Actor not found'};
const sante = actor.system.sante || {};
const pt = actor.prototypeToken;
return {
ok: true,
santeVie: {value: sante.vie?.value, max: sante.vie?.max},
santeEnd: {value: sante.endurance?.value, max: sante.endurance?.max},
tokenDisposition: pt?.disposition,
tokenSight: pt?.sight?.enabled,
actorLink: pt?.actorLink
};
} catch(e) {
return {error: e?.message || String(e)};
}
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
S_ERR=$(echo "$SANTE_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',''))" 2>/dev/null || echo "")
S_VIE=$(echo "$SANTE_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('santeVie',''))" 2>/dev/null || echo "")
S_END=$(echo "$SANTE_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('santeEnd',''))" 2>/dev/null || echo "")
S_DISP=$(echo "$SANTE_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tokenDisposition',''))" 2>/dev/null || echo "")
if [ -z "$S_ERR" ] || [ "$S_ERR" = "None" ]; then
VIE_VAL=$(echo "$S_VIE" | python3 -c "import sys,json; d=json.loads(sys.stdin.read().replace(chr(39),chr(34))); print(d.get('max',''))" 2>/dev/null || echo "")
END_VAL=$(echo "$S_END" | python3 -c "import sys,json; d=json.loads(sys.stdin.read().replace(chr(39),chr(34))); print(d.get('max',''))" 2>/dev/null || echo "")
[ -n "$VIE_VAL" ] && [ "$VIE_VAL" != "0" ] && [ "$VIE_VAL" != "None" ] && pass "TC-03-003a: sante.vie.max = $VIE_VAL" || fail "TC-03-003a: sante.vie invalid ($S_VIE)"
[ -n "$END_VAL" ] && [ "$END_VAL" != "0" ] && [ "$END_VAL" != "None" ] && pass "TC-03-003b: sante.endurance.max = $END_VAL" || fail "TC-03-003b: sante.endurance invalid ($S_END)"
[ "$S_DISP" = "0" ] 2>/dev/null && pass "TC-03-003c: token disposition = NEUTRAL (0)" || fail "TC-03-003c: token disposition = $S_DISP (expected 0)"
else
fail "TC-03-003: $S_ERR"
fi
echo ""
echo "--- Cleanup ---"
CLEANUP=$(chrome-devtools evaluate_script "async () => {
const actor = game.actors.getName('TC-03 Test Creature');
if (actor) { await actor.delete(); return 'deleted'; }
return 'none';
}" 2>/dev/null | sed -n '/^```json$/{n;p;}' | tr -d '"\n')
[ "$CLEANUP" = "deleted" ] && pass "Cleanup: actor deleted" || fail "Cleanup: $CLEANUP"
echo ""
echo "--- Console errors ---"
ERRS=$(chrome-devtools list_console_messages --types error 2>/dev/null | grep -c '^msgid=' || true)
[ "$ERRS" -le 2 ] 2>/dev/null && pass "Console errors ≤2 ($ERRS)" || fail "Console errors: $ERRS"
# ============================================================================
TOTAL=$((PASSED + FAILED))
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
printf "║ %2d / %2d passed, %2d failed ║\n" "$PASSED" "$TOTAL" "$FAILED"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
[ "$FAILED" -gt 0 ] && exit 1 || exit 0
+313
View File
@@ -0,0 +1,313 @@
#!/usr/bin/env bash
# ============================================================================
# TC-04 — Acteur Entité
# Usage: ./tc-04-acteur-entite.sh [--navigate]
# --navigate : force navigation + login
# Env: URL, PASS (admin password)
#
# Atomic: tout polling interne (async + setTimeout Promise).
# ============================================================================
set -uo pipefail
URL="${URL:-https://localhost:31000}"
PASS="${PASS:-}"
NAVIGATE="${1:-}"
PASSED=0; FAILED=0
die() { echo "$*"; exit 1; }
pass() { echo "$1"; PASSED=$((PASSED+1)); }
fail() { echo "$1"; FAILED=$((FAILED+1)); }
jse_get() {
chrome-devtools evaluate_script "() => $1" 2>/dev/null \
| sed -n '/^```json$/{n;p;}'
}
jse_wait_ready() {
local timeout_s="${1:-60}" poll_ms="${2:-1000}"
chrome-devtools evaluate_script "async () => {
const limit = $timeout_s * 1000 / $poll_ms;
for (let i = 0; i < limit; i++) {
if (typeof game !== 'undefined' && game.ready) return 'ready';
await new Promise(r => setTimeout(r, $poll_ms));
}
return 'timeout';
}" 2>/dev/null | sed -n '/^```json$/{n;p;}' | tr -d '"\n'
}
# ============================================================================
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-04 — Acteur Entité ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
R=$(jse_wait_ready 15 2)
if [ "$R" != "ready" ] && [ "$NAVIGATE" = "--navigate" ]; then
[ -z "$PASS" ] && die "Admin password required: PASS=..."
echo "→ Navigating to $URL ..."
chrome-devtools new_page "$URL" --background false --timeout 15000 --isolatedContext "rdd-tc04" >/dev/null 2>&1
sleep 3
PW=$(jse_get "window.location.pathname" | tr -d '"\n')
case "$PW" in
*/join)
chrome-devtools evaluate_script "() => {
const sel = document.querySelector('select[name=userid]');
if(sel) {
const opts = Array.from(sel.options);
const gm = opts.find(o => o.text === 'Gamemaster');
const pick = gm || opts.find(o => o.value && !o.disabled) || opts.find(o => o.value);
if(pick) sel.value = pick.value;
}
const admin = document.querySelector('input[name=adminPassword]');
if(admin) admin.value = '$PASS';
const btn = document.querySelector('button[name=join]');
if(btn) btn.click();
}" >/dev/null 2>&1 || true
;;
*/setup)
chrome-devtools evaluate_script "() => {
const links = document.querySelectorAll('[data-action=worldLaunch]');
for(const l of links){
const li = l.closest('li.package.world');
if(li && li.querySelector('.package-title')?.textContent === 'Reve de Dragon'){ l.click(); break; }
}
}" >/dev/null 2>&1 || true
;;
*/auth)
chrome-devtools evaluate_script "() => {
const inp = document.querySelector('input[type=password]');
if(inp) inp.value = '$PASS';
const btn = document.querySelector('form button, button[type=submit]');
if(btn) btn.click();
}" >/dev/null 2>&1 || true
;;
esac
echo "→ Waiting for game..."
R=$(jse_wait_ready 60 2)
fi
[ "$R" != "ready" ] && die "Game not ready"
echo " ✓ Game ready"
echo ""
# Cleanup helper: delete named actors
cleanup_actors() {
local names=("$@")
chrome-devtools evaluate_script "async () => {
for (const n of $(printf '["%s"]' "${names[@]}")) {
const a = game.actors.getName(n);
if (a) await a.delete();
}
return 'ok';
}" >/dev/null 2>&1 || true
}
# ============================================================================
# TC-04-001: Entité incarnée
# ============================================================================
echo "--- TC-04-001: Entité incarnée ---"
cleanup_actors "TC-04 Entité Incarnée"
RESULT=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
try {
const [actor] = await Actor.createDocuments([{
type: 'entite',
name: 'TC-04 Entité Incarnée',
system: { definition: { typeentite: 'incarne', categorieentite: 'reve' } }
}]);
if (!actor) return {error: 'create failed'};
const sheet = await actor.sheet.render(true);
await new Promise(r => setTimeout(r, 800));
const sheetName = sheet.constructor.name;
await sheet.close();
return {
ok: true, actorId: actor.id,
sheetName: sheetName,
typeentite: actor.system.definition?.typeentite,
categorieentite: actor.system.definition?.categorieentite,
items: actor.items.contents.length
};
} catch(e) { return {error: e?.message || String(e)}; }
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
eval_ok() { echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
[ -z "$(eval_ok error)" ] 2>/dev/null || [ "$(eval_ok error)" = "None" ] || die "TC-04-001: $(eval_ok error)"
SH_NAME=$(eval_ok sheetName)
TTYPE=$(eval_ok typeentite)
CATEG=$(eval_ok categorieentite)
E1_ID=$(eval_ok actorId)
[ "$SH_NAME" = "RdDActorEntiteSheetV2" ] && pass "TC-04-001a: Sheet $SH_NAME" || fail "TC-04-001a: Expected RdDActorEntiteSheetV2 got $SH_NAME"
[ "$TTYPE" = "incarne" ] && pass "TC-04-001b: typeentite=$TTYPE" || fail "TC-04-001b: typeentite=$TTYPE (expected incarne)"
[ "$CATEG" = "reve" ] && pass "TC-04-001c: categorieentite=$CATEG" || fail "TC-04-001c: categorieentite=$CATEG (expected reve)"
echo ""
echo "--- TC-04-002: Entité non-incarnée ---"
cleanup_actors "TC-04 Entité Non-Incarnée"
RESULT2=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
try {
const [actor] = await Actor.createDocuments([{
type: 'entite',
name: 'TC-04 Entité Non-Incarnée',
system: { definition: { typeentite: 'nonincarne', categorieentite: 'cauchemar' } }
}]);
if (!actor) return {error: 'create failed'};
const sheet = await actor.sheet.render(true);
await new Promise(r => setTimeout(r, 800));
const sheetName = sheet.constructor.name;
await sheet.close();
return {
ok: true, actorId: actor.id,
sheetName: sheetName,
typeentite: actor.system.definition?.typeentite,
items: actor.items.contents.length
};
} catch(e) { return {error: e?.message || String(e)}; }
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
eval2() { echo "$RESULT2" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
[ -z "$(eval2 error)" ] 2>/dev/null || [ "$(eval2 error)" = "None" ] || die "TC-04-002: $(eval2 error)"
SH2=$(eval2 sheetName)
TT2=$(eval2 typeentite)
[ "$SH2" = "RdDActorEntiteSheetV2" ] && pass "TC-04-002a: Sheet $SH2" || fail "TC-04-002a: Expected RdDActorEntiteSheetV2 got $SH2"
[ "$TT2" = "nonincarne" ] && pass "TC-04-002b: typeentite=$TT2" || fail "TC-04-002b: typeentite=$TT2 (expected nonincarne)"
echo ""
echo "--- TC-04-003: Entité blurette ---"
cleanup_actors "TC-04 Entité Blurette"
RESULT3=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
try {
const [actor] = await Actor.createDocuments([{
type: 'entite',
name: 'TC-04 Entité Blurette',
system: { definition: { typeentite: 'blurette', categorieentite: 'cauchemar' } }
}]);
if (!actor) return {error: 'create failed'};
const sheet = await actor.sheet.render(true);
await new Promise(r => setTimeout(r, 800));
const sheetName = sheet.constructor.name;
await sheet.close();
return {
ok: true, actorId: actor.id,
sheetName: sheetName,
typeentite: actor.system.definition?.typeentite,
items: actor.items.contents.length
};
} catch(e) { return {error: e?.message || String(e)}; }
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
eval3() { echo "$RESULT3" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
[ -z "$(eval3 error)" ] 2>/dev/null || [ "$(eval3 error)" = "None" ] || die "TC-04-003: $(eval3 error)"
SH3=$(eval3 sheetName)
TT3=$(eval3 typeentite)
[ "$SH3" = "RdDActorEntiteSheetV2" ] && pass "TC-04-003a: Sheet $SH3" || fail "TC-04-003a: Expected RdDActorEntiteSheetV2 got $SH3"
[ "$TT3" = "blurette" ] && pass "TC-04-003b: typeentite=$TT3" || fail "TC-04-003b: typeentite=$TT3 (expected blurette)"
echo ""
echo "--- TC-04-004: Possession ---"
cleanup_actors "TC-04 Victime" "TC-04 Possesseur"
RESULT4=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
try {
// Create a personnage actor (victim)
const [victime] = await Actor.createDocuments([{type: 'personnage', name: 'TC-04 Victime'}]);
if (!victime) return {error: 'victime create failed'};
// Create an entite actor (possesseur)
const [entite] = await Actor.createDocuments([{
type: 'entite',
name: 'TC-04 Possesseur',
system: { definition: { typeentite: 'incarne', categorieentite: 'reve' } }
}]);
if (!entite) return {error: 'entite create failed'};
// Create possession item on the victim
const [possession] = await victime.createEmbeddedDocuments('Item', [{
name: 'Possession de TC-04 Possesseur',
type: 'possession',
system: {
entiteid: entite.id,
entitetokenid: entite.id,
victimeid: victime.id,
victimetokenid: victime.id,
compteur: 0,
typepossession: '',
possede: false
}
}]);
if (!possession) return {error: 'possession create failed'};
const sheet = await victime.sheet.render(true);
await new Promise(r => setTimeout(r, 800));
const sheetName = sheet.constructor.name;
await sheet.close();
return {
ok: true,
victimeId: victime.id,
entiteId: entite.id,
possessionId: possession.id,
possessionName: possession.name,
possessionEntiteId: possession.system.entiteid,
possessionVictimeId: possession.system.victimeid,
possessionCompteur: possession.system.compteur,
victimeSheet: sheetName
};
} catch(e) { return {error: e?.message || String(e)}; }
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
eval4() { echo "$RESULT4" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
E4_ERR=$(eval4 error)
[ -z "$E4_ERR" ] || [ "$E4_ERR" = "None" ] || die "TC-04-004: $E4_ERR"
PNAME=$(eval4 possessionName)
PEID=$(eval4 possessionEntiteId)
PVID=$(eval4 possessionVictimeId)
PCOMPT=$(eval4 possessionCompteur)
VSH=$(eval4 victimeSheet)
[ -n "$PNAME" ] && pass "TC-04-004a: Possession créée ($PNAME)" || fail "TC-04-004a: Possession non créée"
[ -n "$PEID" ] && pass "TC-04-004b: entiteid lié" || fail "TC-04-004b: entiteid manquant"
[ -n "$PVID" ] && pass "TC-04-004c: victimeid lié" || fail "TC-04-004c: victimeid manquant"
[ "$PCOMPT" = "0" ] && pass "TC-04-004d: compteur initial = $PCOMPT" || fail "TC-04-004d: compteur = $PCOMPT (expected 0)"
[ "$VSH" = "RdDActorSheetV2" ] && pass "TC-04-004e: Feuille victime = $VSH" || fail "TC-04-004e: Feuille victime = $VSH (expected RdDActorSheetV2)"
echo ""
echo "--- Cleanup ---"
cleanup_actors "TC-04 Entité Incarnée" "TC-04 Entité Non-Incarnée" "TC-04 Entité Blurette" "TC-04 Victime" "TC-04 Possesseur" > /dev/null 2>&1 || true
CLEANUP=$(chrome-devtools evaluate_script "async () => {
const names = ['TC-04 Entité Incarnée','TC-04 Entité Non-Incarnée','TC-04 Entité Blurette','TC-04 Victime','TC-04 Possesseur'];
let c = 0;
for (const n of names) { const a = game.actors.getName(n); if (a) { await a.delete(); c++; } }
return 'deleted ' + c;
}" 2>/dev/null | sed -n '/^```json$/{n;p;}' | tr -d '"\n')
echo " $CLEANUP" && pass "Cleanup: done" || fail "Cleanup: $CLEANUP"
echo ""
echo "--- Console errors ---"
ERRS=$(chrome-devtools list_console_messages --types error 2>/dev/null | grep -c '^msgid=' || true)
[ "$ERRS" -le 3 ] 2>/dev/null && pass "Console errors ≤3 ($ERRS)" || fail "Console errors: $ERRS"
# ============================================================================
TOTAL=$((PASSED + FAILED))
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
printf "║ %2d / %2d passed, %2d failed ║\n" "$PASSED" "$TOTAL" "$FAILED"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
[ "$FAILED" -gt 0 ] && exit 1 || exit 0
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env bash
# ============================================================================
# TC-05 — Acteur Véhicule
# Usage: ./tc-05-acteur-vehicule.sh [--navigate]
# --navigate : force navigation + login
# Env: URL, PASS (admin password)
#
# Atomic: tout polling interne (async + setTimeout Promise).
# ============================================================================
set -uo pipefail
URL="${URL:-https://localhost:31000}"
PASS="${PASS:-}"
NAVIGATE="${1:-}"
PASSED=0; FAILED=0
die() { echo "$*"; exit 1; }
pass() { echo "$1"; PASSED=$((PASSED+1)); }
fail() { echo "$1"; FAILED=$((FAILED+1)); }
jse_get() { chrome-devtools evaluate_script "() => $1" 2>/dev/null | sed -n '/^```json$/{n;p;}'; }
jse_wait_ready() {
local timeout_s="${1:-60}" poll_ms="${2:-1000}"
chrome-devtools evaluate_script "async () => {
const limit = $timeout_s * 1000 / $poll_ms;
for (let i = 0; i < limit; i++) {
if (typeof game !== 'undefined' && game.ready) return 'ready';
await new Promise(r => setTimeout(r, $poll_ms));
}
return 'timeout';
}" 2>/dev/null | sed -n '/^```json$/{n;p;}' | tr -d '"\n'
}
# ============================================================================
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-05 — Acteur Véhicule ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
R=$(jse_wait_ready 15 2)
if [ "$R" != "ready" ] && [ "$NAVIGATE" = "--navigate" ]; then
[ -z "$PASS" ] && die "Admin password required: PASS=..."
echo "→ Navigating to $URL ..."
chrome-devtools new_page "$URL" --background false --timeout 15000 --isolatedContext "rdd-tc05" >/dev/null 2>&1
sleep 3
PW=$(jse_get "window.location.pathname" | tr -d '"\n')
case "$PW" in
*/join) chrome-devtools evaluate_script "() => {
const sel = document.querySelector('select[name=userid]');
if(sel) { const opts = Array.from(sel.options); const gm = opts.find(o => o.text === 'Gamemaster'); const pick = gm || opts.find(o => o.value && !o.disabled) || opts.find(o => o.value); if(pick) sel.value = pick.value; }
const admin = document.querySelector('input[name=adminPassword]'); if(admin) admin.value = '$PASS';
const btn = document.querySelector('button[name=join]'); if(btn) btn.click();
}" >/dev/null 2>&1 || true ;;
*/setup) chrome-devtools evaluate_script "() => {
const links = document.querySelectorAll('[data-action=worldLaunch]');
for(const l of links){ const li = l.closest('li.package.world'); if(li && li.querySelector('.package-title')?.textContent === 'Reve de Dragon'){ l.click(); break; }
}}" >/dev/null 2>&1 || true ;;
*/auth) chrome-devtools evaluate_script "() => {
const inp = document.querySelector('input[type=password]'); if(inp) inp.value = '$PASS';
const btn = document.querySelector('form button, button[type=submit]'); if(btn) btn.click();
}" >/dev/null 2>&1 || true ;;
esac
echo "→ Waiting for game..."
R=$(jse_wait_ready 60 2)
fi
[ "$R" != "ready" ] && die "Game not ready"
echo " ✓ Game ready"
echo ""
# ============================================================================
# TC-05-001: Vehicle creation + sheet
# ============================================================================
echo "--- TC-05-001: Véhicule creation + sheet ---"
RESULT=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
try {
const existing = game.actors.getName('TC-05 Test Vehicule');
if (existing) await existing.delete();
const [actor] = await Actor.createDocuments([{type: 'vehicule', name: 'TC-05 Test Vehicule'}]);
if (!actor) return {error: 'create failed'};
const sheet = await actor.sheet.render(true);
await new Promise(r => setTimeout(r, 800));
const sheetName = sheet.constructor.name;
await sheet.close();
return {
ok: true, actorId: actor.id, sheetName,
items: actor.items.contents.length,
classe: actor.constructor.name,
isVehicule: typeof actor.isVehicule === 'function' ? actor.isVehicule() : false,
resistance: actor.system.etat?.resistance?.value,
resistanceMax: actor.system.etat?.resistance?.max,
structure: actor.system.etat?.structure?.value,
structureMax: actor.system.etat?.structure?.max
};
} catch(e) { return {error: e?.message || String(e)}; }
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
eval_ok() { echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
[ -z "$(eval_ok error)" ] 2>/dev/null || [ "$(eval_ok error)" = "None" ] || die "TC-05-001: $(eval_ok error)"
SH_NAME=$(eval_ok sheetName)
CLS=$(eval_ok isVehicule)
RES=$(eval_ok resistance)
RMAX=$(eval_ok resistanceMax)
STR=$(eval_ok structure)
SMAX=$(eval_ok structureMax)
AID=$(eval_ok actorId)
[ "$SH_NAME" = "RdDActorVehiculeSheetV2" ] && pass "TC-05-001a: Sheet $SH_NAME" || fail "TC-05-001a: Expected RdDActorVehiculeSheetV2 got $SH_NAME"
[ "$CLS" = "True" ] && pass "TC-05-001b: isVehicule() true" || fail "TC-05-001b: isVehicule()=$CLS"
[ "$RES" = "20" ] && pass "TC-05-001c: resistance = $RES" || fail "TC-05-001c: resistance = $RES (expected 20)"
[ "$STR" = "10" ] && pass "TC-05-001d: structure = $STR" || fail "TC-05-001d: structure = $STR (expected 10)"
echo ""
echo "--- TC-05-002: Structure / Resistance update ---"
RESULT2=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
try {
const actor = game.actors.get('$AID');
if (!actor) return {error: 'Actor not found'};
// Modify via direct update (vehicleIncDec is available)
await actor.update({'system.etat.resistance.value': 15, 'system.etat.structure.value': 5});
await new Promise(r => setTimeout(r, 300));
const res = actor.system.etat.resistance.value;
const str = actor.system.etat.structure.value;
// Use vehicleIncDec
await actor.vehicleIncDec('resistance', -3);
await actor.vehicleIncDec('structure', 2);
await new Promise(r => setTimeout(r, 300));
return {
ok: true,
afterUpdate: {resistance: res, structure: str},
afterIncDec: {resistance: actor.system.etat.resistance.value, structure: actor.system.etat.structure.value}
};
} catch(e) { return {error: e?.message || String(e)}; }
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
eval2() { echo "$RESULT2" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
E2=$(eval2 error)
[ -z "$E2" ] || [ "$E2" = "None" ] || die "TC-05-002: $E2"
AU=$(eval2 afterUpdate)
AI=$(eval2 afterIncDec)
echo "$AU" | grep -q "'resistance': 15" && pass "TC-05-002a: resistance → 15 via update" || fail "TC-05-002a: resistance update failed ($AU)"
echo "$AU" | grep -q "'structure': 5" && pass "TC-05-002b: structure → 5 via update" || fail "TC-05-002b: structure update failed ($AU)"
echo "$AI" | grep -q "'resistance': 12" && pass "TC-05-002c: resistance → 12 via vehicleIncDec" || fail "TC-05-002c: resistance incdec failed ($AI)"
echo "$AI" | grep -q "'structure': 7" && pass "TC-05-002d: structure → 7 via vehicleIncDec" || fail "TC-05-002d: structure incdec failed ($AI)"
echo ""
echo "--- Cleanup ---"
CLEANUP=$(chrome-devtools evaluate_script "async () => {
const actor = game.actors.getName('TC-05 Test Vehicule');
if (actor) { await actor.delete(); return 'deleted'; }
return 'none';
}" 2>/dev/null | sed -n '/^```json$/{n;p;}' | tr -d '"\n')
[ "$CLEANUP" = "deleted" ] && pass "Cleanup: actor deleted" || fail "Cleanup: $CLEANUP"
echo ""
echo "--- Console errors ---"
ERRS=$(chrome-devtools list_console_messages --types error 2>/dev/null | grep -c '^msgid=' || true)
[ "$ERRS" -le 2 ] 2>/dev/null && pass "Console errors ≤2 ($ERRS)" || fail "Console errors: $ERRS"
# ============================================================================
TOTAL=$((PASSED + FAILED))
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
printf "║ %2d / %2d passed, %2d failed ║\n" "$PASSED" "$TOTAL" "$FAILED"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
[ "$FAILED" -gt 0 ] && exit 1 || exit 0
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env bash
# ============================================================================
# TC-06 — Acteur Commerce
# Usage: ./tc-06-acteur-commerce.sh [--navigate]
# --navigate : force navigation + login
# Env: URL, PASS (admin password)
#
# Atomic single evaluate_script: no cross-call races.
# ============================================================================
set -uo pipefail
URL="${URL:-https://localhost:31000}"
PASS="${PASS:-}"
NAVIGATE="${1:-}"
PASSED=0; FAILED=0
die() { echo "$*"; exit 1; }
pass() { echo "$1"; PASSED=$((PASSED+1)); }
fail() { echo "$1"; FAILED=$((FAILED+1)); }
jse_get() { chrome-devtools evaluate_script "() => $1" 2>/dev/null | sed -n '/^```json$/{n;p;}'; }
jse_wait_ready() {
local t="${1:-60}" p="${2:-1000}"
chrome-devtools evaluate_script "async () => {
for(let i=0;i<$t*1000/$p;i++){ if(typeof game!=='undefined'&&game.ready) return 'ready'; await new Promise(r=>setTimeout(r,$p)); }
return 'timeout';
}" 2>/dev/null | sed -n '/^```json$/{n;p;}' | tr -d '"\n'
}
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-06 — Acteur Commerce ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
R=$(jse_wait_ready 15 2)
if [ "$R" != "ready" ] && [ "$NAVIGATE" = "--navigate" ]; then
[ -z "$PASS" ] && die "PASS=..."
echo "→ Navigating ..."
chrome-devtools new_page "$URL" --background false --timeout 15000 --isolatedContext "rdd-tc06" >/dev/null 2>&1; sleep 3
PW=$(jse_get "window.location.pathname" | tr -d '"\n')
case "$PW" in
*/join) chrome-devtools evaluate_script "() => {
const s=document.querySelector('select[name=userid]'); if(s){ const o=Array.from(s.options); const g=o.find(x=>x.text==='Gamemaster'); const p=g||o.find(x=>x.value&&!x.disabled)||o.find(x=>x.value); if(p) s.value=p.value; }
const a=document.querySelector('input[name=adminPassword]'); if(a) a.value='$PASS';
const b=document.querySelector('button[name=join]'); if(b) b.click();
}" >/dev/null 2>&1 || true ;;
*/setup) chrome-devtools evaluate_script "() => {
document.querySelectorAll('[data-action=worldLaunch]').forEach(l=>{ const li=l.closest('li.package.world'); if(li&&li.querySelector('.package-title')?.textContent==='Reve de Dragon') l.click(); });
}" >/dev/null 2>&1 || true ;;
*/auth) chrome-devtools evaluate_script "() => {
const i=document.querySelector('input[type=password]'); if(i) i.value='$PASS';
const b=document.querySelector('form button,button[type=submit]'); if(b) b.click();
}" >/dev/null 2>&1 || true ;;
esac
echo "→ Waiting..."; R=$(jse_wait_ready 60 2)
fi
[ "$R" != "ready" ] && die "Game not ready"
echo " ✓ Game ready"; echo ""
# ============================================================================
# Single atomic evaluate_script: all sections in one call
# ============================================================================
RESULT=$(chrome-devtools evaluate_script "async () => {
if (typeof game === 'undefined' || !game.ready) return {error: 'NOT_READY'};
const out = {};
// ==============================
// Cleanup any previous TC-06 actors
// ==============================
for (const n of ['TC-06 Marchand', 'TC-06 Acheteur']) {
const a = game.actors.getName(n);
if (a) await a.delete();
}
// ==============================
// TC-06-001: Commerce creation
// ==============================
try {
const [actor] = await Actor.createDocuments([{type: 'commerce', name: 'TC-06 Marchand'}]);
await actor.createEmbeddedDocuments('Item', [
{name: 'Denier (etain)', type: 'monnaie', system: {cout: 0.01, quantite: 0}},
{name: 'Sou (bronze)', type: 'monnaie', system: {cout: 0.05, quantite: 0}},
{name: 'Sol (argent)', type: 'monnaie', system: {cout: 1, quantite: 0}},
{name: 'Dragon (or)', type: 'monnaie', system: {cout: 240, quantite: 10}}
]);
const sheet = await actor.sheet.render(true);
await new Promise(r => setTimeout(r, 800));
out.sheetName = sheet.constructor.name;
await sheet.close();
out.monnaieCount = actor.items.filter(i => i.type === 'monnaie').length;
out.pourcentage = actor.system.pourcentage;
out.c001_1 = (out.sheetName === 'RdDCommerceSheetV2');
out.c001_2 = (out.monnaieCount >= 4);
out.c001_3 = (out.pourcentage === 100);
out.commerceId = actor.id;
} catch(e) { out.c001_error = e?.message || String(e); }
// ==============================
// TC-06-002: Purchase simulation
// ==============================
try {
const commerce = game.actors.get(out.commerceId);
if (!commerce) { out.c002_error = 'Commerce not found'; }
else {
const [item] = await commerce.createEmbeddedDocuments('Item', [{
name: 'Epee longue', type: 'objet',
system: { cout: 50, quantite: 3, encombrement: 2 }
}]);
out.prixCalcule = commerce.calculerPrix(item);
out.quantiteDispo = commerce.getQuantiteDisponible(item);
const [buyer] = await Actor.createDocuments([{type: 'personnage', name: 'TC-06 Acheteur'}]);
await buyer.createEmbeddedDocuments('Item', [{
name: 'Dragon (or)', type: 'monnaie', system: { cout: 240, quantite: 5 }
}]);
out.peutPayer = buyer.verifierFortune(out.prixCalcule);
const reste = await buyer.depenserSols(out.prixCalcule);
out.resteApres = reste;
out.c002_payeurResteOk = (reste >= 0);
await commerce.ajouterSols(out.prixCalcule);
await commerce.decrementerQuantiteItem(item.id, 1);
await buyer.createEmbeddedDocuments('Item', [{
name: item.name, type: item.type,
system: { cout: item.system.cout, quantite: 1, encombrement: item.system.encombrement }
}]);
out.itemTransfere = buyer.items.filter(i => i.name === 'Epee longue').length > 0;
out.c002_1 = (out.prixCalcule === 50);
out.c002_2 = (out.quantiteDispo === 3);
out.c002_3 = (out.peutPayer === true);
out.c002_4 = (out.c002_payeurResteOk === true);
out.c002_5 = (!(reste >= 0) || (item.system.cout === 50 && out.c002_payeurResteOk));
out.c002_6 = (out.itemTransfere === true);
}
} catch(e) { out.c002_error = e?.message || String(e); }
// ==============================
// TC-06-003: Sell simulation
// ==============================
try {
const commerce = game.actors.getName('TC-06 Marchand');
const buyer = game.actors.getName('TC-06 Acheteur');
if (!commerce || !buyer) { out.c003_error = 'actors not found for TC-06-003'; }
else {
const [item] = await buyer.createEmbeddedDocuments('Item', [{
name: 'Potion de soin', type: 'potion',
system: { cout: 30, quantite: 2, encombrement: 1 }
}]);
out.prixItem = item.calculerPrixCommercant();
out.prixVente = Math.floor(out.prixItem / 2);
out.commerceSuffisant = commerce.verifierFortune(out.prixVente);
const fortuneVendeurAvant = buyer.getFortune();
const depenseOk = await commerce.depenserSols(out.prixVente);
await buyer.ajouterSols(out.prixVente);
const fortuneVendeurApres = buyer.getFortune();
out.gainReel = fortuneVendeurApres - fortuneVendeurAvant;
out.c003_1 = (typeof out.prixItem === 'number' && out.prixItem > 0);
out.c003_2 = (typeof out.prixVente === 'number' && out.prixVente > 0);
out.c003_3 = (out.commerceSuffisant === true);
out.c003_4 = (out.gainReel === out.prixVente);
}
} catch(e) { out.c003_error = e?.message || String(e); }
// ==============================
// Cleanup
// ==============================
for (const n of ['TC-06 Marchand', 'TC-06 Acheteur']) {
const a = game.actors.getName(n);
if (a) await a.delete();
}
out.cleaned = true;
return out;
}" 2>/dev/null | sed -n '/^```json$/{n;p;}')
[ -z "$RESULT" ] && die "Empty result"
eval_ok() { echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
echo "--- TC-06-001: Commerce creation + sheet ---"
[ "$(eval_ok c001_1)" = "True" ] && pass "TC-06-001a: Sheet $(eval_ok sheetName)" || fail "TC-06-001a: Sheet = $(eval_ok sheetName) (expected RdDCommerceSheetV2)"
[ "$(eval_ok c001_2)" = "True" ] && pass "TC-06-001b: $(eval_ok monnaieCount) monnaies" || fail "TC-06-001b: monnaies = $(eval_ok monnaieCount) (expected ≥4)"
[ "$(eval_ok c001_3)" = "True" ] && pass "TC-06-001c: pourcentage = $(eval_ok pourcentage)" || fail "TC-06-001c: pourcentage = $(eval_ok pourcentage)"
[ -n "$(eval_ok c001_error)" ] && fail "TC-06-001: $(eval_ok c001_error)"
echo ""
echo "--- TC-06-002: Achat (simulation) ---"
[ "$(eval_ok c002_1)" = "True" ] && pass "TC-06-002a: prix = $(eval_ok prixCalcule) sols" || fail "TC-06-002a: prix = $(eval_ok prixCalcule)"
[ "$(eval_ok c002_2)" = "True" ] && pass "TC-06-002b: quantite = $(eval_ok quantiteDispo)" || fail "TC-06-002b: quantite = $(eval_ok quantiteDispo)"
[ "$(eval_ok c002_3)" = "True" ] && pass "TC-06-002c: peut payer $(eval_ok prixCalcule) sols" || fail "TC-06-002c: verifierFortune = $(eval_ok peutPayer)"
[ "$(eval_ok c002_4)" = "True" ] && pass "TC-06-002d: depenserSols (reste=$(eval_ok resteApres))" || fail "TC-06-002d: depenserSols returned $(eval_ok resteApres)"
[ "$(eval_ok c002_6)" = "True" ] && pass "TC-06-002e: item transfere" || fail "TC-06-002e: item transfere = $(eval_ok itemTransfere)"
[ -n "$(eval_ok c002_error)" ] && fail "TC-06-002: $(eval_ok c002_error)"
echo ""
echo "--- TC-06-003: Vente (simulation) ---"
[ "$(eval_ok c003_1)" = "True" ] && pass "TC-06-003a: prix item = $(eval_ok prixItem) sols" || fail "TC-06-003a: prix item = $(eval_ok prixItem)"
[ "$(eval_ok c003_2)" = "True" ] && pass "TC-06-003b: prix vente = $(eval_ok prixVente) sols" || fail "TC-06-003b: prix vente = $(eval_ok prixVente)"
[ "$(eval_ok c003_3)" = "True" ] && pass "TC-06-003c: commerce peut payer $(eval_ok prixVente)" || fail "TC-06-003c: commerceSuffisant = $(eval_ok commerceSuffisant)"
GE=$(eval_ok gainReel); PV=$(eval_ok prixVente)
[ -n "$GE" ] && [ "$GE" -ge "$PV" ] 2>/dev/null && pass "TC-06-003d: vendeur recoit $GE sols (prix vente $PV)" || fail "TC-06-003d: gain = $GE (expected >= $PV)"
[ -n "$(eval_ok c003_error)" ] && fail "TC-06-003: $(eval_ok c003_error)"
echo ""
echo "--- Cleanup ---"
[ "$(eval_ok cleaned)" = "True" ] && pass "Cleanup: done" || fail "Cleanup: not run"
echo ""
echo "--- Console errors ---"
ERRS=$(chrome-devtools list_console_messages --types error 2>/dev/null | grep -c '^msgid=' || true)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
TOTAL=$((PASSED + FAILED))
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
printf "║ %2d / %2d passed, %2d failed ║\n" "$PASSED" "$TOTAL" "$FAILED"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
[ "$FAILED" -gt 0 ] && exit 1 || exit 0