Même chantier que le commit précédent (moteur de jeu, play.html) — templates/screen_edit.html était un unique fichier HTML+CSS+JS de 3312 lignes, tout l'éditeur (arborescence, panneaux flottants, canevas, formulaire de propriétés, éditeur de flow à nœuds, blocs de logique, timeline d'animation) vivant dans UN SEUL <script>. Ce fichier est plus imbriqué que play.html : de nombreux appels s'exécutent au niveau racine du script (pas seulement des déclarations de fonctions), et JavaScript hoiste les déclarations `function` sur TOUT le script — un appel au niveau racine peut donc référencer une fonction déclarée PLUS LOIN dans le même fichier. Découper naïvement casserait cet ordre implicite. Un audit dédié (analyse ligne par ligne de chaque appel racine + son graphe d'appel transitif) a identifié 3 références "en avance" réelles, toutes regroupées dans la même zone (initBuilderPanel()/toggleActionFields() → bindAspectButtons/ toggleElementPropertyValue/onDataDefinitionChange) — le découpage respecte cette contrainte : chaque fichier est une TRANCHE SÉQUENTIELLE de l'original (jamais une réorganisation), et cette zone spécifique reste un seul fichier (panel-init.js) pour que le hoisting continue de fonctionner exactement comme avant. 5 fichiers sous static/js/screen_edit/ : - tree-panels.js — arborescence, menu contextuel, panneaux flottants gauche/droite, galerie d'icônes, modale de suppression/choix d'icône, glisser-déposer du canevas, panneau de propriétés (autosave). - panel-init.js — (ré)initialisation du panneau central après chaque changement de sélection, filtres de répéteur/donnée liée, condition de visibilité, champs d'action du formulaire de nœud. - flow-editor.js — éditeur de flow à nœuds (rendu du graphe, formulaire d'ajout de nœud, blocs de logique — currentBlockNodes/Edges). - tabs-and-blocks.js — onglets du centre, panneaux flottants génériques (drag/resize/plein écran), modale d'un bloc de logique. - animation-timeline.js — timeline d'animation (clips Animate.css/ personnalisés). Toutes les données injectées par Jinja (GAME_SLUG, SCREEN_ID, DEFINITIONS_DATA, FLOW_NODES_INITIAL, ELEMENTS_LABELS, CUSTOM_EVENTS_MAP, ANIM_CLIPS...) sont posées UNE FOIS par un petit <script> inline resté dans le template, avant les <script src> — même patron que static/js/play/. Le seul bout de logique resté inline est la toute petite IIFE d'ouverture initiale (?tab=/?block=), qui dépend directement de request.args et doit s'exécuter après que tous les fichiers soient chargés. tests/conftest.py : screen_edit_js_bundle() (même principe que play_js_bundle(), Phase -1 précédente) — 4 tests qui vérifiaient la présence de telle fonction/chaîne dans le HTML de l'éditeur (le JS y était inline) sont mis à jour pour chercher dans ce bundle. Un des deux échecs révélait un test déjà fragile (assert "Ligne cliquée" in html vérifiait en réalité le TEXTE SOURCE d'un <script> inline, jamais du HTML réellement rendu — ce texte ne peut plus s'y trouver une fois la fonction qui le construit dynamiquement déplacée dans un fichier externe) : corrigé pour vérifier le bundle JS + la disponibilité de la route séparément. Vérifié : 215 tests passent, syntaxe JS validée sur les 5 nouveaux fichiers (node --check) et sur les <script> inline restants (rendus via le client de test). Test manuel recommandé (édition complète d'une scène : arborescence, propriétés, glisser-déposer, logique de flow, blocs, timeline) avant de considérer ce découpage définitivement sans risque — comme pour play.html, ce fichier n'a pas de harnais de test DOM automatisé. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
107 lines
5.3 KiB
Python
107 lines
5.3 KiB
Python
"""Un nœud Condition du graphe de logique peut désormais tester une
|
|
VARIABLE GLOBALE, pas seulement un champ d'objet (voir
|
|
ensure_flow_schema.py : cond_source/cond_variable/cond_variable_chemin).
|
|
Évalué côté CLIENT (templates/play.html, evaluateConditionClause) contre
|
|
gameData.variables — exposé par full_game_payload.py, vérifié ici via
|
|
/runtime-payload. La logique JS elle-même (résolution de chemin, lecture
|
|
de gameData.variables, repli sur le comportement historique) a été vérifiée
|
|
manuellement avec Node.js (fonctions extraites de play.html) : variable
|
|
scalaire, objet avec chemin chaîné, tableau par index, variable
|
|
introuvable (ne matche rien plutôt que planter), booléen "Oui", et clause
|
|
sans "source" (comportement legacy, champ d'objet) — les 7 scénarios
|
|
passent."""
|
|
import json
|
|
import re
|
|
|
|
from conftest import screen_edit_js_bundle
|
|
|
|
|
|
def _create_screen(client, slug, name="Accueil"):
|
|
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
|
return int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
|
|
|
|
|
|
def test_condition_node_stores_variable_source(client, game):
|
|
client.post(f"/game/{game}/variables/new", data={"name": "score", "type": "nombre_entier", "default_value": "5"})
|
|
screen_id = _create_screen(client, game)
|
|
|
|
resp = client.post(
|
|
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
|
data=json.dumps({
|
|
"node_type": "condition", "cond_source": "variable",
|
|
"cond_variable": "score", "cond_variable_chemin": "",
|
|
"cond_operator": "superieur_egal", "cond_value": "3",
|
|
}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
node = resp.get_json()
|
|
assert node["cond_source"] == "variable"
|
|
assert node["cond_variable"] == "score"
|
|
assert node["cond_operator"] == "superieur_egal"
|
|
# Aucun champ d'objet renseigné pour une clause "variable".
|
|
assert node["cond_definition_id"] is None
|
|
assert node["cond_field"] is None
|
|
|
|
|
|
def test_condition_node_with_combined_variable_and_object_clauses(client, game):
|
|
client.post(f"/game/{game}/variables/new", data={"name": "score", "type": "nombre_entier", "default_value": "5"})
|
|
resp = client.post(f"/game/{game}/objects/new", data={
|
|
"object_name": "Niveau", "field_name[]": ["numero"], "field_type[]": ["nombre_entier"],
|
|
"field_relation[]": [""], "field_required[]": ["0"], "field_min[]": [""], "field_max[]": [""],
|
|
}, follow_redirects=False)
|
|
def_id = int(resp.headers["Location"].rstrip("/").split("/")[-1])
|
|
screen_id = _create_screen(client, game)
|
|
|
|
clauses = [
|
|
{"source": "variable", "variable": "score", "operator": "superieur_egal", "value": "3"},
|
|
{"source": "objet", "definition_id": def_id, "row_id": None, "field": "numero",
|
|
"field_type": "nombre_entier", "operator": "egal", "value": "1"},
|
|
]
|
|
resp = client.post(
|
|
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
|
data=json.dumps({
|
|
"node_type": "condition", "cond_clauses": json.dumps(clauses), "cond_combinator": "ou",
|
|
}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
node = resp.get_json()
|
|
stored = json.loads(node["cond_clauses"])
|
|
assert stored[0]["source"] == "variable"
|
|
assert stored[1]["source"] == "objet"
|
|
assert node["cond_combinator"] == "ou"
|
|
|
|
|
|
def test_runtime_payload_exposes_global_variables_for_condition_evaluation(client, game):
|
|
client.post(f"/game/{game}/variables/new", data={"name": "score", "type": "nombre_entier", "default_value": "5"})
|
|
client.post(f"/game/{game}/variables/new", data={"name": "perso", "type": "objet",
|
|
"default_value": json.dumps({"nom": "Alice"})})
|
|
payload = client.get(f"/game/{game}/runtime-payload").get_json()
|
|
assert payload["variables"]["score"] == {"value": "5", "type": "nombre_entier"}
|
|
assert payload["variables"]["perso"]["type"] == "objet"
|
|
assert json.loads(payload["variables"]["perso"]["value"]) == {"nom": "Alice"}
|
|
|
|
|
|
def test_screen_edit_panel_exposes_variable_source_condition_ui(client, game):
|
|
client.post(f"/game/{game}/variables/new", data={"name": "score", "type": "nombre_entier", "default_value": "5"})
|
|
screen_id = _create_screen(client, game)
|
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
|
|
assert 'id="nf-cond-source"' in html
|
|
assert 'id="nf-cond-variable-fields"' in html
|
|
assert "onFlowConditionSourceChange" in html
|
|
assert "onFlowClauseSourceChange" in screen_edit_js_bundle()
|
|
|
|
|
|
def test_condition_builder_still_available_without_any_object_defined(client, game):
|
|
"""Régression : le panneau de condition était entièrement masqué quand
|
|
le jeu n'avait AUCUN objet ("Aucun objet défini..."), rendant
|
|
impossible de créer une condition sur une variable dans ce cas. Seule
|
|
la partie "Champ d'un objet" doit rester masquée ; "Variable globale"
|
|
doit rester utilisable."""
|
|
client.post(f"/game/{game}/variables/new", data={"name": "score", "type": "nombre_entier", "default_value": "5"})
|
|
screen_id = _create_screen(client, game)
|
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
|
|
assert 'id="nf-cond-source"' in html
|
|
assert 'id="nf-cond-variable-fields"' in html
|