From baa3da1035b4f2fc2745f7cfe8efe0bd7195847e Mon Sep 17 00:00:00 2001 From: william Date: Tue, 25 Aug 2026 12:52:27 +0200 Subject: [PATCH] =?UTF-8?q?Corrige=20la=20comparaison=20bool=C3=A9enne=20:?= =?UTF-8?q?=20"Oui"/"Non"=20n'=C3=A9tait=20pas=20reconnu=20comme=20valeur?= =?UTF-8?q?=20vraie/fausse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- screens/rendering/filter_repeater_rows.py | 9 +++++- templates/play.html | 8 +++++- tests/test_visibility_condition.py | 35 +++++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/screens/rendering/filter_repeater_rows.py b/screens/rendering/filter_repeater_rows.py index ea84a966..a800acc2 100644 --- a/screens/rendering/filter_repeater_rows.py +++ b/screens/rendering/filter_repeater_rows.py @@ -60,7 +60,14 @@ def _compare(actual, operator, expected, field_type): else: truthy = bool(actual) a = 1 if truthy else 0 - e = 1 if str(expected).strip().lower() in ("1", "true", "vrai") else 0 + # "oui"/"non" est le vocabulaire affiché partout ailleurs dans + # l'app pour un champ booléen (voir data_list.html, "Oui" si vrai + # sinon "Non") — une valeur de comparaison fixe tapée "Oui" doit + # donc être reconnue comme vraie ici aussi, pas seulement "1"/ + # "true"/"vrai" (sans quoi "Oui" retombait silencieusement à faux, + # et un widget conditionné sur "est égal à Oui" ne s'affichait + # jamais quand il aurait dû). + e = 1 if str(expected).strip().lower() in ("1", "true", "vrai", "oui") else 0 return a != e if operator == "different" else a == e try: an, en = float(actual), float(expected) diff --git a/templates/play.html b/templates/play.html index 2ec358f7..818dbb3a 100644 --- a/templates/play.html +++ b/templates/play.html @@ -557,7 +557,13 @@ function compareValues(actual, operator, expected, fieldType) { if (fieldType === 'booleen') { const a = actual ? 1 : 0; - const e = (expected === '1' || expected === 'true' || expected === true) ? 1 : 0; + // "oui"/"non" (voir data_list.html) est le vocabulaire affiché + // partout ailleurs pour un champ booléen — une valeur de comparaison + // fixe tapée "Oui" doit donc être reconnue vraie ici aussi, pas + // seulement "1"/"true" (et insensible à la casse, aligné avec + // _compare() côté Python, voir filter_repeater_rows.py). + const expectedStr = String(expected).trim().toLowerCase(); + const e = (expected === true || ['1', 'true', 'vrai', 'oui'].includes(expectedStr)) ? 1 : 0; return operator === 'different' ? a !== e : a === e; } const an = parseFloat(actual), en = parseFloat(expected); diff --git a/tests/test_visibility_condition.py b/tests/test_visibility_condition.py index 7ed63bc4..278020d0 100644 --- a/tests/test_visibility_condition.py +++ b/tests/test_visibility_condition.py @@ -79,6 +79,41 @@ def test_objet_mode_uses_most_recent_row_outside_a_repeater(client, game): 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