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:
@@ -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%)*
|
||||
Executable
+170
@@ -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
|
||||
@@ -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
|
||||
Executable
+269
@@ -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
|
||||
Executable
+313
@@ -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
|
||||
Executable
+186
@@ -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
|
||||
Executable
+224
@@ -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
|
||||
Reference in New Issue
Block a user