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>
182 lines
9.5 KiB
Python
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
|