Files
Forge-Engine/tests/test_visibility_condition.py
T
williamandClaude Sonnet 5 baa3da1035 Corrige la comparaison booléenne : "Oui"/"Non" n'était pas reconnu comme valeur vraie/fausse
Bug remonté avec une "Mail card" (icône enveloppe fermée si is_opened
est à "Non", ouverte si "Oui") : les DEUX variantes s'affichaient (ou
aucune), selon la ligne.

Cause : _compare() (filter_repeater_rows.py, utilisé par la condition de
visibilité, le Répéteur et Donnée liée) ne reconnaissait "1"/"true"/"vrai"
comme valeur vraie pour un champ booléen — jamais "oui", pourtant le SEUL
vocabulaire que l'app affiche elle-même pour ce type de champ partout
ailleurs (voir data_list.html : "Oui" si vrai sinon "Non"). Une valeur de
comparaison fixe tapée "Oui" retombait donc silencieusement à "faux",
et comme l'opérateur et le champ étaient par ailleurs corrects, ça
donnait l'impression que la condition "ne voyait" rien : sur la ligne où
is_opened=faux, les DEUX cartes ("égal à Oui" et "égal à Non", toutes
deux évaluées comme "égal à faux") s'affichaient ensemble ; sur la ligne
où is_opened=vrai, aucune des deux.

Fix : "oui" ajouté à l'ensemble des valeurs reconnues comme vraies,
côté Python (_compare) ET côté JS (compareValues() dans play.html, qui
doit rester alignée — utilisée par les nœuds Condition de la Logique de
la scène), cette dernière au passage rendue insensible à la casse comme
son équivalent Python (elle ne l'était pas du tout).

Ajoute un test de régression dédié à ce cas précis.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 12:52:27 +02:00

182 lines
9.5 KiB
Python

"""Tests de la "condition de visibilité" (panneau de propriétés, tout
widget) : masque un élément en mode jouable selon une variable globale ou
le champ d'un objet de données. Jamais appliquée dans l'éditeur."""
import re
import db
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 _add_element(client, slug, screen_id, widget="texte"):
resp = client.post(f"/game/{slug}/screens/{screen_id}/elements/add", data={"widget": widget}, follow_redirects=False)
return int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
def _rendered_html(client, slug):
payload = client.get(f"/game/{slug}/runtime-payload").get_json()
return payload["screens"][0]["elements"][0]["rendered_html"]
def test_variable_mode_hides_and_shows_element(client, game):
screen_id = _create_screen(client, game)
element_id = _add_element(client, game, screen_id)
client.post(f"/game/{game}/variables/new", data={"name": "porte_ouverte", "type": "booleen", "default_value": "0"})
client.post(f"/game/{game}/elements/{element_id}/save", data={
"ctrl_visibilite_cond_mode": "variable",
"ctrl_visibilite_cond_variable": "porte_ouverte",
"ctrl_visibilite_cond_operateur": "egal",
"ctrl_visibilite_cond_valeur": "1",
})
html = _rendered_html(client, game)
assert "display:none" in html
db.update_global_variable_value(game, "porte_ouverte", "1")
html = _rendered_html(client, game)
assert "display:none" not in html
def test_visibility_condition_never_hides_in_the_editor(client, game):
screen_id = _create_screen(client, game)
element_id = _add_element(client, game, screen_id)
client.post(f"/game/{game}/variables/new", data={"name": "porte_ouverte", "type": "booleen", "default_value": "0"})
client.post(f"/game/{game}/elements/{element_id}/save", data={
"ctrl_visibilite_cond_mode": "variable",
"ctrl_visibilite_cond_variable": "porte_ouverte",
"ctrl_visibilite_cond_operateur": "egal",
"ctrl_visibilite_cond_valeur": "1",
})
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
assert 'class="visibilityGated"' not in html
assert f'id="el-{element_id}"' in html
def test_objet_mode_uses_most_recent_row_outside_a_repeater(client, game):
screen_id = _create_screen(client, game)
element_id = _add_element(client, game, screen_id)
resp = client.post(f"/game/{game}/objects/new", data={
"object_name": "Partie", "field_name[]": ["score"], "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])
client.post(f"/game/{game}/objects/{def_id}/data/new", data={"score": "3"})
client.post(f"/game/{game}/elements/{element_id}/save", data={
"ctrl_visibilite_cond_mode": "objet",
"ctrl_visibilite_cond_definition_id": str(def_id),
"ctrl_visibilite_cond_champ": "score",
"ctrl_visibilite_cond_operateur": "superieur",
"ctrl_visibilite_cond_valeur": "10",
})
assert "display:none" in _rendered_html(client, game)
client.post(f"/game/{game}/objects/{def_id}/data/1/edit", data={"score": "20"})
assert "display:none" not in _rendered_html(client, game)
def test_objet_mode_recognizes_oui_non_as_boolean_literal(client, game):
"""Régression : le sélecteur "Valeur fixe" du panneau de propriétés
n'offre pas "vrai"/"faux" pour un champ booléen — "Oui"/"Non" est le
seul vocabulaire visible dans toute l'app (voir data_list.html, qui
affiche "Oui"/"Non" pour ce type de champ). Une condition réglée
"est égal à Oui" doit donc reconnaître "Oui" comme vrai, pas seulement
"1"/"true"/"vrai" — sinon elle ne s'affichait jamais quand elle aurait
dû (repérée avec une "Mail card" affichant les DEUX variantes, ouverte
ET fermée, au lieu d'une seule)."""
screen_id = _create_screen(client, game)
closed_id = _add_element(client, game, screen_id, "icone")
resp = client.post(f"/game/{game}/objects/new", data={
"object_name": "Email", "field_name[]": ["is_opened"], "field_type[]": ["booleen"],
"field_relation[]": [""], "field_required[]": ["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={"is_opened": "on"})
client.post(f"/game/{game}/elements/{closed_id}/save", data={
"ctrl_visibilite_cond_mode": "objet", "ctrl_visibilite_cond_definition_id": str(def_id),
"ctrl_visibilite_cond_champ": "is_opened", "ctrl_visibilite_cond_operateur": "egal", "ctrl_visibilite_cond_valeur": "Non",
})
assert "display:none" in _rendered_html(client, game)
open_id = _add_element(client, game, screen_id, "icone")
client.post(f"/game/{game}/elements/{open_id}/save", data={
"ctrl_visibilite_cond_mode": "objet", "ctrl_visibilite_cond_definition_id": str(def_id),
"ctrl_visibilite_cond_champ": "is_opened", "ctrl_visibilite_cond_operateur": "egal", "ctrl_visibilite_cond_valeur": "Oui",
})
payload = client.get(f"/game/{game}/runtime-payload").get_json()
els = {el["id"]: el["rendered_html"] for el in payload["screens"][0]["elements"]}
assert "display:none" in els[closed_id]
assert "display:none" not in els[open_id]
def test_visible_special_render_widget_carries_refresh_marker(client, game):
"""Régression : une icône (special_render, pas le chemin générique)
actuellement VISIBLE sous condition doit quand même porter le
marqueur "visibilityGated" — sinon refreshRuntimeData() (play.html) ne
sait jamais qu'il faut la réévaluer après une action "Modifier une
donnée/variable", et elle reste figée dans son état précédent. Cas
concret remonté : une icône "enveloppe fermée" (visible tant que
is_opened=0) reste affichée EN MÊME TEMPS que l'icône "enveloppe
ouverte" (qui, elle, se met à jour normalement puisqu'un élément
CACHÉ porte toujours son marqueur via son placeholder)."""
screen_id = _create_screen(client, game)
element_id = _add_element(client, game, screen_id, "icone")
client.post(f"/game/{game}/variables/new", data={"name": "porte_ouverte", "type": "booleen", "default_value": "0"})
client.post(f"/game/{game}/elements/{element_id}/save", data={
"ctrl_visibilite_cond_mode": "variable",
"ctrl_visibilite_cond_variable": "porte_ouverte",
"ctrl_visibilite_cond_operateur": "egal",
"ctrl_visibilite_cond_valeur": "0",
})
html = _rendered_html(client, game)
assert "display:none" not in html
assert "visibilityGated" in html
def test_objet_mode_inside_a_repeater_evaluates_each_row_independently(client, game):
"""Régression : une "Mail card" (élément de jeu réutilisable posé dans
un Répéteur d'e-mails) doit afficher l'icône "ouvert"/"fermé" qui
correspond à SA PROPRE ligne — pas toujours la même ligne (la plus
récente de l'objet) pour tous les exemplaires du Répéteur."""
resp = client.post(f"/game/{game}/objects/new", data={
"object_name": "Email", "field_name[]": ["sujet", "is_opened"], "field_type[]": ["texte", "booleen"],
"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={"sujet": "Fermé"})
client.post(f"/game/{game}/objects/{def_id}/data/new", data={"sujet": "Ouvert", "is_opened": "on"})
resp = client.post(f"/game/{game}/element-types", data={"name": "Mail card", "icon": "📧"}, follow_redirects=False)
et = next(t for t in screens.list_element_types(game) if t["name"] == "Mail card")
template_screen_id = et["template_screen_id"]
closed_icon_id = _add_element(client, game, template_screen_id, "icone")
client.post(f"/game/{game}/elements/{closed_icon_id}/save", data={
"ctrl_visibilite_cond_mode": "objet", "ctrl_visibilite_cond_definition_id": str(def_id),
"ctrl_visibilite_cond_champ": "is_opened", "ctrl_visibilite_cond_operateur": "egal", "ctrl_visibilite_cond_valeur": "0",
})
open_icon_id = _add_element(client, game, template_screen_id, "icone")
client.post(f"/game/{game}/elements/{open_icon_id}/save", data={
"ctrl_visibilite_cond_mode": "objet", "ctrl_visibilite_cond_definition_id": str(def_id),
"ctrl_visibilite_cond_champ": "is_opened", "ctrl_visibilite_cond_operateur": "egal", "ctrl_visibilite_cond_valeur": "1",
})
screen_id = _create_screen(client, game, "Boîte de réception")
repeater_id = _add_element(client, game, screen_id, "repeteur_donnees")
client.post(f"/game/{game}/elements/{repeater_id}/save", data={
"ctrl_definition_id": str(def_id), "ctrl_template_type_id": str(et["id"]),
})
html = _rendered_html(client, game)
items = html.split('class="repeaterItem"')
assert len(items) == 3
row_closed, row_open = items[1], items[2]
assert row_closed.count('class="visibilityGated"') == 1
assert row_open.count('class="visibilityGated"') == 1
assert row_closed != row_open