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
@@ -87,6 +87,26 @@ def game(client):
|
||||
shutil.rmtree(game_dir)
|
||||
|
||||
|
||||
_PLAY_JS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "static", "js", "play")
|
||||
|
||||
|
||||
def play_js_bundle():
|
||||
"""Concatène tout le JS du moteur jouable (voir le plan de
|
||||
modularisation — static/js/play/, un fichier par responsabilité,
|
||||
extrait de l'ancien <script> unique de templates/play.html). Plusieurs
|
||||
tests vérifient que "le JS livré au joueur" contient telle fonction/
|
||||
telle chaîne, sans dépendre d'un navigateur — avant le découpage, ça
|
||||
revenait à chercher dans le HTML de /game/<slug>/play (tout était
|
||||
inline) ; maintenant la logique vit dans ces fichiers statiques."""
|
||||
parts = []
|
||||
for name in sorted(os.listdir(_PLAY_JS_DIR)):
|
||||
path = os.path.join(_PLAY_JS_DIR, name)
|
||||
if os.path.isfile(path) and name.endswith(".js"):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
parts.append(f.read())
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mail_object(client, game):
|
||||
"""Un objet "Mail" avec deux champs texte (sujet, expediteur), pour les
|
||||
|
||||
@@ -4,6 +4,8 @@ et une durée sur l'axe du temps (secondes depuis l'affichage de l'écran)."""
|
||||
import json
|
||||
import re
|
||||
|
||||
from conftest import play_js_bundle
|
||||
|
||||
|
||||
def _create_screen(client, slug, name="Accueil"):
|
||||
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
||||
@@ -98,8 +100,9 @@ def test_play_page_exposes_animation_runtime_and_animate_css(client, game):
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
html = resp.data.decode()
|
||||
assert "animate.min.css" in html
|
||||
assert "runAnimationTimeline" in html
|
||||
assert "injectCustomKeyframes" in html
|
||||
js = play_js_bundle()
|
||||
assert "runAnimationTimeline" in js
|
||||
assert "injectCustomKeyframes" in js
|
||||
|
||||
|
||||
def test_play_page_payload_includes_clip(client, game):
|
||||
|
||||
@@ -10,6 +10,8 @@ vérifient les tests de non-régression ci-dessous, en plus des tests dédiés
|
||||
import json
|
||||
import re
|
||||
|
||||
from conftest import play_js_bundle
|
||||
|
||||
|
||||
def _create_screen(client, slug, name="Accueil"):
|
||||
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
||||
@@ -49,10 +51,9 @@ def test_legacy_single_clause_condition_still_works_without_cond_clauses(client,
|
||||
)
|
||||
node = resp.get_json()
|
||||
assert node["cond_clauses"] is None
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
html = resp.data.decode()
|
||||
assert "evaluateConditionClause" in html
|
||||
assert "cond_clauses" in html
|
||||
js = play_js_bundle()
|
||||
assert "evaluateConditionClause" in js
|
||||
assert "cond_clauses" in js
|
||||
|
||||
|
||||
def test_condition_node_persists_clauses_and_combinator(client, game):
|
||||
@@ -102,7 +103,6 @@ def test_condition_combinator_defaults_to_et_when_absent(client, game):
|
||||
|
||||
|
||||
def test_play_page_exposes_combinator_logic(client, game):
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
html = resp.data.decode()
|
||||
assert "cond_combinator" in html
|
||||
assert "'ou'" in html
|
||||
js = play_js_bundle()
|
||||
assert "cond_combinator" in js
|
||||
assert "'ou'" in js
|
||||
|
||||
+12
-14
@@ -5,6 +5,8 @@ d'un élément après décision."""
|
||||
import json
|
||||
import re
|
||||
|
||||
from conftest import play_js_bundle
|
||||
|
||||
|
||||
def _create_screen(client, slug, name="Accueil"):
|
||||
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
||||
@@ -72,11 +74,10 @@ def test_survol_trigger_node_persists(client, game):
|
||||
def test_play_page_exposes_hover_trigger_runtime(client, game):
|
||||
"""bindHoverTriggers() (mouseenter/mouseleave -> runFlowFrom) doit être
|
||||
exposé et appelé, exactement comme bindClicks() pour "Au clic"."""
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
html = resp.data.decode()
|
||||
assert "bindHoverTriggers" in html
|
||||
assert "'survol'" in html
|
||||
assert "'fin_survol'" in html
|
||||
js = play_js_bundle()
|
||||
assert "bindHoverTriggers" in js
|
||||
assert "'survol'" in js
|
||||
assert "'fin_survol'" in js
|
||||
|
||||
|
||||
def test_modifier_element_contenu_action_persists(client, game):
|
||||
@@ -107,9 +108,7 @@ def test_modifier_element_contenu_action_persists(client, game):
|
||||
|
||||
|
||||
def test_play_page_exposes_contenu_property_runtime(client, game):
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
html = resp.data.decode()
|
||||
assert "'contenu'" in html
|
||||
assert "'contenu'" in play_js_bundle()
|
||||
|
||||
|
||||
# ---------- 3.2 — Séquences temporisées ----------
|
||||
@@ -128,10 +127,9 @@ def test_attendre_action_node_persists_delay(client, game):
|
||||
|
||||
|
||||
def test_play_page_exposes_wait_action_runtime(client, game):
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
html = resp.data.decode()
|
||||
assert "'attendre'" in html
|
||||
assert "setTimeout" in html
|
||||
js = play_js_bundle()
|
||||
assert "'attendre'" in js
|
||||
assert "setTimeout" in js
|
||||
|
||||
|
||||
# ---------- 3.3 — Surbrillance générique dynamique ----------
|
||||
@@ -157,7 +155,7 @@ def test_play_page_exposes_highlight_runtime_and_css(client, game):
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
html = resp.data.decode()
|
||||
assert "forgeHighlight" in html
|
||||
assert "'surbrillance'" in html
|
||||
assert "'surbrillance'" in play_js_bundle()
|
||||
|
||||
|
||||
# ---------- 3.4 — Overlay / modale réutilisable ----------
|
||||
@@ -473,4 +471,4 @@ def test_play_page_exposes_lock_runtime_and_css(client, game):
|
||||
html = resp.data.decode()
|
||||
assert "forgeDisabled" in html
|
||||
assert "pointer-events:none" in html
|
||||
assert "'desactive'" in html
|
||||
assert "'desactive'" in play_js_bundle()
|
||||
|
||||
+9
-9
@@ -2,6 +2,8 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from conftest import play_js_bundle
|
||||
|
||||
|
||||
def _create_screen(client, slug, name="Accueil"):
|
||||
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
||||
@@ -143,11 +145,10 @@ def test_play_page_exposes_row_bindings_helpers(client, game):
|
||||
résoudre les {{champ}} de la ligne ouverte (voir README, section
|
||||
"Cliquer une ligne de Répéteur") — vérifie que le JS livré au joueur
|
||||
contient bien les briques attendues, sans dépendre d'un navigateur."""
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
html = resp.data.decode()
|
||||
assert "applyOpenRowBindings" in html
|
||||
assert "lastClickedRowId" in html
|
||||
assert "'ouvrir_ligne'" in html
|
||||
js = play_js_bundle()
|
||||
assert "applyOpenRowBindings" in js
|
||||
assert "lastClickedRowId" in js
|
||||
assert "'ouvrir_ligne'" in js
|
||||
|
||||
|
||||
def test_repeater_filter_literal_value(client, game, mail_object):
|
||||
@@ -224,7 +225,6 @@ def test_play_page_exposes_screen_show_trigger_helper(client, game):
|
||||
"""Le runtime jouable doit exécuter les nœuds "affichage" à chaque fois
|
||||
qu'un écran est montré (premier affichage, retour en arrière, données
|
||||
modifiées) — voir README, section correspondante."""
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
html = resp.data.decode()
|
||||
assert "runScreenShowTriggers" in html
|
||||
assert "'affichage'" in html
|
||||
js = play_js_bundle()
|
||||
assert "runScreenShowTriggers" in js
|
||||
assert "'affichage'" in js
|
||||
|
||||
+5
-4
@@ -3,6 +3,8 @@ import re
|
||||
|
||||
import db
|
||||
|
||||
from conftest import play_js_bundle
|
||||
|
||||
|
||||
def _create_screen(client, slug, name="Accueil"):
|
||||
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
||||
@@ -265,7 +267,6 @@ def test_play_page_click_binding_guards_against_duplicate_listeners(client, game
|
||||
ré-attachement — un vrai test de non-régression comportementale (clics
|
||||
répétés via un navigateur réel) a été exécuté manuellement pour valider
|
||||
le correctif : 5 clics sur +10 -> exactement +50, jamais plus."""
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
html = resp.data.decode()
|
||||
assert "clickBound" in html
|
||||
assert "if (elDiv.dataset.clickBound) return;" in html
|
||||
js = play_js_bundle()
|
||||
assert "clickBound" in js
|
||||
assert "if (elDiv.dataset.clickBound) return;" in js
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from conftest import play_js_bundle
|
||||
|
||||
|
||||
def _create_screen(client, slug, name="Accueil"):
|
||||
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
||||
@@ -36,7 +38,6 @@ def test_play_page_exposes_activer_onglet_runtime(client, game):
|
||||
"""Le runtime jouable doit savoir exécuter "activer_onglet" (montrer un
|
||||
élément, masquer ses frères) — vérifie que le JS livré au joueur
|
||||
contient bien cette logique, sans dépendre d'un navigateur."""
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
html = resp.data.decode()
|
||||
assert "'activer_onglet'" in html
|
||||
assert "parentKey" in html
|
||||
js = play_js_bundle()
|
||||
assert "'activer_onglet'" in js
|
||||
assert "parentKey" in js
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
ne se déforment pas selon la fenêtre du joueur."""
|
||||
import re
|
||||
|
||||
from conftest import play_js_bundle
|
||||
|
||||
|
||||
def _create_screen(client, slug, name="Accueil"):
|
||||
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
||||
@@ -137,4 +139,4 @@ def test_play_payload_includes_preview_aspect_per_screen(client, game):
|
||||
def test_play_page_has_letterboxing_frame(client, game):
|
||||
html = client.get(f"/game/{game}/play").data.decode()
|
||||
assert 'id="playFrame"' in html
|
||||
assert "applyScreenAspect" in html
|
||||
assert "applyScreenAspect" in play_js_bundle()
|
||||
|
||||
Reference in New Issue
Block a user