Phase -1 : découpe le moteur de play.html en modules JS + premiers tests JS
templates/play.html était un unique fichier HTML+CSS+JS de 1069 lignes,
tout le moteur de jeu vivant dans UN SEUL <script>, sans aucune
couverture de test sur cette logique (seuls le rendu HTML et la syntaxe
JS étaient vérifiés). La feuille de route à venir (état par joueur,
hasard, clavier/minuteur, position/collision, son — voir le plan) va
justement faire grossir ce moteur : "un fichier = une fonction, un
dossier = une responsabilité" s'applique aussi au JS, pas seulement au
Python — le moment de découper est avant d'ajouter encore plus de code,
pas après.
Découpage en 6 fichiers sous static/js/play/, calqués sur les sections
déjà présentes dans le code (aucune réorganisation de logique, une pure
extraction) : screens.js (affichage d'écran, timeline d'animation),
conditions.js (évaluation des conditions — la partie 100% PURE, sans
DOM, la plus testable), actions.js (exécution des actions), triggers.js
(recherche des nœuds déclencheurs, attache des écouteurs), bindings.js
(résolution des {{champ}}, rafraîchissement des données), flow-engine.js
(parcours du graphe, événements personnalisés).
Zéro nouvel outillage : plusieurs <script src> dans l'ordre, partageant
le même espace global qu'avant (aucun bundler, aucune étape de build).
Les 2 URLs de route dont ces fichiers ont besoin (flow_node_run_data/
run_variable, runtime_payload) ne peuvent plus être injectées par Jinja
directement dans le code (un fichier statique n'est jamais passé par le
moteur de templates) — elles sont maintenant posées une fois dans
window.FORGE_PLAY_URLS par le petit <script> inline restant dans
play.html, qui ne porte plus que les données Jinja (gameData) et
l'amorçage (bindClicks() etc. au chargement).
publish/build_package.py : ajoute static/js/play à la liste des fichiers
copiés dans l'exécutable exporté (le mode jouable en dépend désormais).
Premiers tests JS (static/js/play/__tests__/conditions.test.js, lancés
via `node --test`, zéro nouvelle dépendance npm — decision prise avec
l'utilisateur de commencer par la logique PURE seulement, pas par une
couverture DOM via jsdom) : compareValues, resolveVariablePath,
evaluateConditionClause/Node, exactement la logique que les phases à
venir (opérations mathématiques, condition de collision) vont étendre.
tests/conftest.py : nouveau helper play_js_bundle() (concatène tout
static/js/play/*.js) — 13 tests existants qui vérifiaient la présence de
telle fonction/chaîne dans le HTML de /game/<slug>/play (tout le JS y
était inline avant ce découpage) sont mis à jour pour chercher dans ce
bundle à la place ; les tests qui vérifient un CSS/HTML réellement resté
dans play.html (forgeHighlight, forgeDisabled, #playFrame...) continuent
de chercher dans le HTML.
Vérifié : 215 tests pytest passent (aucune régression comportementale,
juste une réorganisation), 13 tests node:test passent, node --check sur
chacun des 6 nouveaux fichiers. Test manuel recommandé (jeu joué de bout
en bout : navigation, clic, survol, répéteur, condition, animation)
avant de considérer le découpage définitivement sans risque.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a445b72a6e
commit
dbada333d5
@@ -0,0 +1,119 @@
|
||||
// Tests de la logique PURE du moteur (aucun accès DOM) — voir le plan de
|
||||
// modularisation JS. Lancé via `node --test static/js/play/__tests__/`.
|
||||
// Ces fonctions lisent des globals `gameData`/`window` (posés par
|
||||
// play.html en navigateur) — on les simule ici avant chaque test.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
global.window = global.window || {};
|
||||
const {
|
||||
compareValues,
|
||||
resolveVariablePath,
|
||||
readFieldValue,
|
||||
readVariableValue,
|
||||
evaluateConditionClause,
|
||||
evaluateConditionNode,
|
||||
} = require('../conditions.js');
|
||||
|
||||
function withGameData(data, fn) {
|
||||
const previous = global.gameData;
|
||||
global.gameData = data;
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
global.gameData = previous;
|
||||
}
|
||||
}
|
||||
|
||||
test('compareValues — comparaison numérique', () => {
|
||||
assert.equal(compareValues('10', 'superieur', '5'), true);
|
||||
assert.equal(compareValues('3', 'inferieur', '5'), true);
|
||||
assert.equal(compareValues('5', 'egal', '5'), true);
|
||||
assert.equal(compareValues('5', 'different', '5'), false);
|
||||
});
|
||||
|
||||
test('compareValues — repli sur comparaison de chaînes si non numérique', () => {
|
||||
assert.equal(compareValues('abc', 'egal', 'abc'), true);
|
||||
assert.equal(compareValues('abc', 'different', 'xyz'), true);
|
||||
});
|
||||
|
||||
test('compareValues — champ booléen, vocabulaire oui/non insensible à la casse', () => {
|
||||
assert.equal(compareValues(true, 'egal', 'Oui', 'booleen'), true);
|
||||
assert.equal(compareValues(false, 'egal', 'non', 'booleen'), true);
|
||||
assert.equal(compareValues(true, 'different', 'non', 'booleen'), true);
|
||||
});
|
||||
|
||||
test('resolveVariablePath — chemin vide renvoie la valeur brute', () => {
|
||||
assert.equal(resolveVariablePath('42', ''), '42');
|
||||
});
|
||||
|
||||
test('resolveVariablePath — chemin .champ / [index] chaînable', () => {
|
||||
const raw = JSON.stringify({ inventaire: [{ nom: 'clé' }, { nom: 'épée' }] });
|
||||
assert.equal(resolveVariablePath(raw, '.inventaire[1].nom'), 'épée');
|
||||
});
|
||||
|
||||
test('resolveVariablePath — JSON invalide ou chemin absent renvoie null', () => {
|
||||
assert.equal(resolveVariablePath('{pas du json', '.x'), null);
|
||||
assert.equal(resolveVariablePath('{"a":1}', '.b.c'), null);
|
||||
});
|
||||
|
||||
test('readFieldValue — résout CLICKED_ROW_ID (-1) via window.lastClickedRowId', () => {
|
||||
withGameData({ data: { '1': [{ id: 7, score: 42 }] } }, () => {
|
||||
window.lastClickedRowId = 7;
|
||||
assert.equal(readFieldValue(1, -1, 'score'), 42);
|
||||
});
|
||||
});
|
||||
|
||||
test('readVariableValue — lit gameData.variables par nom', () => {
|
||||
withGameData({ variables: { points: { type: 'nombre_entier', value: '10' } } }, () => {
|
||||
assert.equal(readVariableValue('points'), '10');
|
||||
assert.equal(readVariableValue('inconnue'), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
test('evaluateConditionClause — source "objet" (comportement historique)', () => {
|
||||
withGameData({ data: { '1': [{ id: 5, pv: '3' }] } }, () => {
|
||||
const clause = { cond_definition_id: 1, cond_row_id: 5, cond_field: 'pv', cond_operator: 'inferieur', cond_value: '10' };
|
||||
assert.equal(evaluateConditionClause(clause), true);
|
||||
});
|
||||
});
|
||||
|
||||
test('evaluateConditionClause — source "variable"', () => {
|
||||
withGameData({ variables: { pv: { type: 'nombre_entier', value: '3' } } }, () => {
|
||||
const clause = { cond_source: 'variable', cond_variable: 'pv', cond_operator: 'inferieur', cond_value: '10' };
|
||||
assert.equal(evaluateConditionClause(clause), true);
|
||||
});
|
||||
});
|
||||
|
||||
test('evaluateConditionNode — clause unique (pas de cond_clauses)', () => {
|
||||
withGameData({ variables: { pv: { type: 'nombre_entier', value: '3' } } }, () => {
|
||||
const node = { cond_source: 'variable', cond_variable: 'pv', cond_operator: 'egal', cond_value: '3' };
|
||||
assert.equal(evaluateConditionNode(node), true);
|
||||
});
|
||||
});
|
||||
|
||||
test('evaluateConditionNode — clauses combinées ET (toutes vraies requises)', () => {
|
||||
withGameData({ variables: { pv: { type: 'nombre_entier', value: '3' }, or: { type: 'nombre_entier', value: '0' } } }, () => {
|
||||
const node = {
|
||||
cond_combinator: 'et',
|
||||
cond_clauses: JSON.stringify([
|
||||
{ source: 'variable', variable: 'pv', operator: 'egal', value: '3' },
|
||||
{ source: 'variable', variable: 'or', operator: 'egal', value: '0' },
|
||||
]),
|
||||
};
|
||||
assert.equal(evaluateConditionNode(node), true);
|
||||
});
|
||||
});
|
||||
|
||||
test('evaluateConditionNode — clauses combinées OU (une seule vraie suffit)', () => {
|
||||
withGameData({ variables: { pv: { type: 'nombre_entier', value: '3' }, or: { type: 'nombre_entier', value: '0' } } }, () => {
|
||||
const node = {
|
||||
cond_combinator: 'ou',
|
||||
cond_clauses: JSON.stringify([
|
||||
{ source: 'variable', variable: 'pv', operator: 'egal', value: '999' },
|
||||
{ source: 'variable', variable: 'or', operator: 'egal', value: '0' },
|
||||
]),
|
||||
};
|
||||
assert.equal(evaluateConditionNode(node), true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user