Corrige le contenu de "Donnée liée" figé après un changement de variable/donnée
Bug rapporté : changer une variable globale utilisée comme valeur de comparaison d'une "Donnée liée" (Texte/Titre, dans une boîte de dialogue posée comme élément de jeu réutilisable) recalculait bien, côté SERVEUR, la bonne ligne à afficher — mais le contenu affiché en jeu restait figé sur son ancienne ligne tant que la page n'était pas complètement rechargée. Cause : templates/play.html ne régénère, après une action "Modifier une variable"/"Modifier une donnée", QUE les éléments dont le rendered_html porte un marqueur connu (voir refreshRuntimeData()/hasMarker() — "repeaterItem", "jaugeBar", "visibilityGated"). Un Texte/Titre "Donnée liée" n'en portait AUCUN : jamais identifié comme "dépendant de la donnée", donc jamais régénéré, même si le nouveau HTML était déjà prêt côté serveur à chaque rendu. Correctif : nouveau marqueur "dataBound" (render_element_html.py), posé dès que _data_definition_id est réglé, reconnu par hasMarker(). Un exemplaire d'élément de jeu (ex. la boîte de dialogue posée sur une scène) porte ce marqueur EN PROFONDEUR dans son propre rendered_html dès qu'un de ses descendants internes en a un — il se retrouve donc bien régénéré dans son ensemble, sans changement supplémentaire nécessaire. Deux nouveaux tests, confirmés en échec sur l'ancien code (même scénario que le rapport : reproduit avec objet "dialog" + variable "dialog_order" + élément de jeu réutilisable) puis au vert avec le correctif. 155 tests au vert au total. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8cffbeac68
commit
124f250d2b
@@ -52,8 +52,27 @@ def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_
|
|||||||
if play_mode and has_visibility_condition and not is_element_visible(slug, attrs_raw, ctx):
|
if play_mode and has_visibility_condition and not is_element_visible(slug, attrs_raw, ctx):
|
||||||
return f'<div class="visibilityGated" data-element-id="{el["id"]}" style="display:none;"></div>'
|
return f'<div class="visibilityGated" data-element-id="{el["id"]}" style="display:none;"></div>'
|
||||||
|
|
||||||
|
# "Donnée liée" (DATA_BINDING_CONTROLS, Texte/Titre) : son contenu
|
||||||
|
# ({{champ}}) dépend de la ligne trouvée par ses conditions au moment du
|
||||||
|
# rendu — potentiellement via une "Variable globale" en valeur de
|
||||||
|
# comparaison (voir _resolve_filter_value). Comme "visibilityGated"
|
||||||
|
# juste au-dessus, un marqueur ("dataBound") reste posé sur le HTML pour
|
||||||
|
# que refreshRuntimeData() (play.html) sache qu'il faut réévaluer cet
|
||||||
|
# élément après toute action "Modifier une donnée"/"Modifier une
|
||||||
|
# variable" — sans lui, changer la variable choisie changeait bien QUELLE
|
||||||
|
# ligne aurait dû s'afficher (recalculé correctement côté serveur à
|
||||||
|
# chaque rendu), mais le client ne redemandait jamais le nouveau HTML de
|
||||||
|
# CET élément précis : le texte affiché restait figé sur son ancienne
|
||||||
|
# ligne jusqu'au prochain rechargement complet de la page.
|
||||||
|
has_data_binding = bool(attrs_raw.get("_data_definition_id"))
|
||||||
|
|
||||||
def _mark(html_str):
|
def _mark(html_str):
|
||||||
return html_str + "<!--visibilityGated-->" if has_visibility_condition else html_str
|
markers = ""
|
||||||
|
if has_visibility_condition:
|
||||||
|
markers += "<!--visibilityGated-->"
|
||||||
|
if has_data_binding:
|
||||||
|
markers += "<!--dataBound-->"
|
||||||
|
return html_str + markers
|
||||||
|
|
||||||
bound_ctx = _resolve_bound_row_ctx(slug, attrs_raw)
|
bound_ctx = _resolve_bound_row_ctx(slug, attrs_raw)
|
||||||
if bound_ctx:
|
if bound_ctx:
|
||||||
|
|||||||
+9
-3
@@ -538,8 +538,9 @@
|
|||||||
// le reste de la partie, sans le moindre message d'erreur visible.
|
// le reste de la partie, sans le moindre message d'erreur visible.
|
||||||
try {
|
try {
|
||||||
// "repeaterItem" (liste de données), "jaugeBar" (2.1 — jauge liée
|
// "repeaterItem" (liste de données), "jaugeBar" (2.1 — jauge liée
|
||||||
// à une donnée) et "visibilityGated" (condition de visibilité —
|
// à une donnée), "visibilityGated" (condition de visibilité) et
|
||||||
// voir render_element_html.py) sont les types d'éléments dont le
|
// "dataBound" ("Donnée liée" d'un Texte/Titre — voir
|
||||||
|
// render_element_html.py) sont les types d'éléments dont le
|
||||||
// contenu/l'affichage dépend de la donnée elle-même plutôt que
|
// contenu/l'affichage dépend de la donnée elle-même plutôt que
|
||||||
// d'une action "Modifier un élément" posée à la main dans le
|
// d'une action "Modifier un élément" posée à la main dans le
|
||||||
// graphe — eux seuls doivent être régénérés après un changement
|
// graphe — eux seuls doivent être régénérés après un changement
|
||||||
@@ -568,7 +569,12 @@
|
|||||||
const byId = {};
|
const byId = {};
|
||||||
screenData.elements.forEach(function(e){ byId[e.id] = e; });
|
screenData.elements.forEach(function(e){ byId[e.id] = e; });
|
||||||
function hasMarker(e) {
|
function hasMarker(e) {
|
||||||
return !!(e.rendered_html && (e.rendered_html.indexOf('repeaterItem') !== -1 || e.rendered_html.indexOf('jaugeBar') !== -1 || e.rendered_html.indexOf('visibilityGated') !== -1));
|
return !!(e.rendered_html && (
|
||||||
|
e.rendered_html.indexOf('repeaterItem') !== -1 ||
|
||||||
|
e.rendered_html.indexOf('jaugeBar') !== -1 ||
|
||||||
|
e.rendered_html.indexOf('visibilityGated') !== -1 ||
|
||||||
|
e.rendered_html.indexOf('dataBound') !== -1
|
||||||
|
));
|
||||||
}
|
}
|
||||||
function isDescendantOf(e, ancestorId) {
|
function isDescendantOf(e, ancestorId) {
|
||||||
let p = e.parent_id ? byId[e.parent_id] : null;
|
let p = e.parent_id ? byId[e.parent_id] : null;
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""Régression : changer une variable globale (ou une donnée) pendant la
|
||||||
|
partie recalculait bien, côté SERVEUR, quelle ligne "Donnée liée" (Texte/
|
||||||
|
Titre) doit afficher — mais templates/play.html ne régénère, après une
|
||||||
|
telle action, que les éléments dont le rendered_html porte un marqueur
|
||||||
|
connu ("repeaterItem"/"jaugeBar"/"visibilityGated" — voir
|
||||||
|
refreshRuntimeData()/hasMarker()). Un Texte/Titre "Donnée liée" n'en
|
||||||
|
portait AUCUN : son contenu restait donc figé sur son ancienne ligne
|
||||||
|
jusqu'au prochain rechargement complet de page, même si le bon contenu
|
||||||
|
était déjà prêt côté serveur — "la variable change mais pas le contenu de
|
||||||
|
la modale". Corrigé en ajoutant un marqueur "dataBound" (render_element_html.py),
|
||||||
|
reconnu par hasMarker()."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
import screens
|
||||||
|
|
||||||
|
|
||||||
|
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_data_bound_element_carries_the_refresh_marker(client, game):
|
||||||
|
resp = client.post(f"/game/{game}/objects/new", data={
|
||||||
|
"object_name": "Niveau", "field_name[]": ["numero", "spiker"],
|
||||||
|
"field_type[]": ["nombre_entier", "texte"],
|
||||||
|
"field_relation[]": ["", ""], "field_required[]": ["0", "0"],
|
||||||
|
"field_min[]": ["", ""], "field_max[]": ["", ""],
|
||||||
|
}, follow_redirects=False)
|
||||||
|
def_id = int(resp.headers["Location"].rstrip("/").split("/")[-1])
|
||||||
|
client.post(f"/game/{game}/objects/{def_id}/data/new", data={"numero": "1", "spiker": "un"})
|
||||||
|
client.post(f"/game/{game}/objects/{def_id}/data/new", data={"numero": "2", "spiker": "deux"})
|
||||||
|
client.post(f"/game/{game}/variables/new", data={"name": "n", "type": "nombre_entier", "default_value": "1"})
|
||||||
|
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add", data={"widget": "texte"}, follow_redirects=False)
|
||||||
|
el_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
|
||||||
|
client.post(f"/game/{game}/elements/{el_id}/save", data={
|
||||||
|
"ctrl_content": "{{spiker}}",
|
||||||
|
"ctrl_data_definition_id": str(def_id),
|
||||||
|
"ctrl_data_filtres_champ[]": ["numero"],
|
||||||
|
"ctrl_data_filtres_operateur[]": ["egal"],
|
||||||
|
"ctrl_data_filtres_valeur[]": ["{{$n}}"],
|
||||||
|
})
|
||||||
|
|
||||||
|
payload = client.get(f"/game/{game}/runtime-payload").get_json()
|
||||||
|
el = payload["screens"][0]["elements"][0]
|
||||||
|
assert "dataBound" in el["rendered_html"]
|
||||||
|
assert "un" in el["rendered_html"]
|
||||||
|
|
||||||
|
# Change la variable -> le rendu recalculé (déjà correct côté serveur
|
||||||
|
# avant ce correctif) doit refléter la nouvelle ligne.
|
||||||
|
import db
|
||||||
|
variable = db.get_global_variable(game, "n")
|
||||||
|
db.update_global_variable(game, variable["id"], "nombre_entier", "2")
|
||||||
|
|
||||||
|
payload2 = client.get(f"/game/{game}/runtime-payload").get_json()
|
||||||
|
el2 = payload2["screens"][0]["elements"][0]
|
||||||
|
assert "deux" in el2["rendered_html"]
|
||||||
|
assert "dataBound" in el2["rendered_html"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_element_type_instance_bubbles_up_the_data_bound_marker(client, game):
|
||||||
|
"""Même vérification, mais pour un Texte "Donnée liée" posé DANS un
|
||||||
|
élément de jeu réutilisable : le marqueur porté par l'élément interne
|
||||||
|
doit se retrouver dans le rendered_html de l'EXEMPLAIRE (le seul élément
|
||||||
|
réellement présent dans la liste plate de la scène qui l'accueille —
|
||||||
|
voir list_elements.py, les descendants d'un exemplaire sont exclus)."""
|
||||||
|
resp = client.post(f"/game/{game}/objects/new", data={
|
||||||
|
"object_name": "dialog", "field_name[]": ["order", "spiker"],
|
||||||
|
"field_type[]": ["nombre_entier", "texte"],
|
||||||
|
"field_relation[]": ["", ""], "field_required[]": ["0", "0"],
|
||||||
|
"field_min[]": ["", ""], "field_max[]": ["", ""],
|
||||||
|
}, follow_redirects=False)
|
||||||
|
def_id = int(resp.headers["Location"].rstrip("/").split("/")[-1])
|
||||||
|
client.post(f"/game/{game}/objects/{def_id}/data/new", data={"order": "1", "spiker": "Un"})
|
||||||
|
client.post(f"/game/{game}/objects/{def_id}/data/new", data={"order": "2", "spiker": "Deux"})
|
||||||
|
client.post(f"/game/{game}/variables/new", data={"name": "dialog_order", "type": "nombre_entier", "default_value": "1"})
|
||||||
|
|
||||||
|
client.post(f"/game/{game}/element-types", data={"name": "dialog"}, follow_redirects=False)
|
||||||
|
et = next(t for t in screens.list_element_types(game) if t["name"] == "dialog")
|
||||||
|
resp = client.post(f"/game/{game}/screens/{et['template_screen_id']}/elements/add", data={"widget": "superposition"}, follow_redirects=False)
|
||||||
|
overlay_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
|
||||||
|
resp = client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "texte"}, follow_redirects=False)
|
||||||
|
spiker_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
|
||||||
|
client.post(f"/game/{game}/elements/{spiker_id}/save", data={
|
||||||
|
"ctrl_content": "{{spiker}}",
|
||||||
|
"ctrl_data_definition_id": str(def_id),
|
||||||
|
"ctrl_data_filtres_champ[]": ["order"],
|
||||||
|
"ctrl_data_filtres_operateur[]": ["egal"],
|
||||||
|
"ctrl_data_filtres_valeur[]": ["{{$dialog_order}}"],
|
||||||
|
})
|
||||||
|
|
||||||
|
screen_id = _create_screen(client, game, "Scene")
|
||||||
|
client.post(f"/game/{game}/screens/{screen_id}/elements/add",
|
||||||
|
data={"widget": "__catalogue__", "element_type_id": et["id"]}, follow_redirects=False)
|
||||||
|
|
||||||
|
payload = client.get(f"/game/{game}/runtime-payload").get_json()
|
||||||
|
instance = payload["screens"][0]["elements"][0]
|
||||||
|
assert "dataBound" in instance["rendered_html"]
|
||||||
|
assert "Un" in instance["rendered_html"]
|
||||||
Reference in New Issue
Block a user