Files
Forge-Engine/tests/test_visibility_condition.py
T
williamandClaude Sonnet 5 ff01b3903c Corrige la condition de visibilité (mode "objet") à l'intérieur d'un Répéteur
Bug remonté : dans une "Mail card" (élément de jeu réutilisable, deux
icônes - enveloppe fermée/ouverte - conditionnées sur le champ is_opened
de l'objet Email) posée dans un Répéteur de données, rien ne s'affichait
jamais correctement.

Cause : is_element_visible() (mode "objet") allait toujours chercher en
base la ligne la plus récente de l'objet ciblé (convention "1 seule ligne
= état de partie", correcte pour une Jauge suivant un état de partie),
sans jamais tenir compte de la ligne EN COURS DE RENDU dans un Répéteur -
donc tous les exemplaires du même modèle de ligne évaluaient la MÊME
ligne (la plus récente de tout l'objet Email) au lieu de chacun la
sienne, et affichaient donc tous exactement le même résultat.

Fix : is_element_visible() reçoit maintenant le ctx de rendu (les
{{champ}} de la ligne en cours, déjà posés par render_repeater.py) et,
si le champ réglé s'y trouve, utilise directement cette valeur plutôt que
d'interroger la base - un exemplaire de Répéteur voit donc bien SA propre
ligne. Hors Répéteur, le comportement (ligne la plus récente de l'objet)
est inchangé.

Ajoute tests/test_visibility_condition.py (mode variable, mode objet hors
Répéteur, absence dans l'éditeur, et ce cas précis dans un Répéteur) -
cette fonctionnalité n'avait jusqu'ici aucun test persistant, seulement
des scripts ad-hoc jetés après vérification.

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

123 lines
6.0 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_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