diff --git a/templates/play.html b/templates/play.html index fb991a48..e65ed213 100644 --- a/templates/play.html +++ b/templates/play.html @@ -605,7 +605,20 @@ const found = []; root.querySelectorAll('[data-element-id]').forEach(function(node) { const sib = node.nextSibling; - if (sib && sib.nodeType === Node.COMMENT_NODE && COMMENT_MARKERS.indexOf(sib.data) !== -1) { + const hasCommentMarker = !!(sib && sib.nodeType === Node.COMMENT_NODE && COMMENT_MARKERS.indexOf(sib.data) !== -1); + // Un élément actuellement CACHÉ par une condition de + // visibilité (voir render_element_html.py) n'a pas de + // commentaire marqueur à sa suite — sa balise porte + // directement la classe "visibilityGated" à la place. Sans + // ce second cas, un élément qui passe de VISIBLE à CACHÉ + // n'était jamais retrouvé ici (seul le sens caché->visible + // l'était, via le commentaire) : son ancien nœud visible + // restait donc affiché indéfiniment à côté du nouveau, + // maintenant visible, qui le remplace normalement (ex. une + // icône "enveloppe fermée" jamais masquée en même temps que + // "enveloppe ouverte" qui apparaît après avoir cliqué). + const isHiddenPlaceholder = node.classList.contains('visibilityGated'); + if (hasCommentMarker || isHiddenPlaceholder) { found.push(node); } }); diff --git a/tests/test_visibility_toggle_markers.py b/tests/test_visibility_toggle_markers.py new file mode 100644 index 00000000..0419a9c0 --- /dev/null +++ b/tests/test_visibility_toggle_markers.py @@ -0,0 +1,98 @@ +"""Régression : après une action "Modifier une donnée" qui fait BASCULER +une condition de visibilité (ex. une icône "enveloppe fermée" visible +quand is_opened=faux, une "enveloppe ouverte" visible quand is_opened=vrai), +templates/play.html::refreshRuntimeData() ne retrouvait, dans le HTML +fraîchement régénéré, que l'élément qui REDEVIENT visible (repéré via le +commentaire "" posé après son contenu réel par +_mark(), voir render_element_html.py) — jamais celui qui REDEVIENT +CACHÉ, dont le placeholder ("
") n'a pas de commentaire à sa suite, juste +une classe CSS sur lui-même. Résultat : l'ancien élément visible restait +affiché indéfiniment à côté du nouveau qui apparaît en plus (les deux +enveloppes en même temps). + +Ces tests-ci ne peuvent pas simuler le DOM d'un navigateur (refreshRuntimeData +est du JS) — ils verrouillent seulement le CONTRAT côté serveur dont +dépend le correctif JS (findCommentMarkedDescendants, voir play.html) : +un élément CACHÉ par condition de visibilité doit porter la classe +"visibilityGated" directement sur sa balise, un élément VISIBLE sous +condition doit porter le commentaire juste après. Voir aussi +tests/test_data_binding_refresh_marker.py pour le marqueur "dataBound".""" +import re + + +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_icon_with_visibility(client, slug, screen_id, def_id, field_name, expected): + resp = client.post(f"/game/{slug}/screens/{screen_id}/elements/add", data={"widget": "icone"}, follow_redirects=False) + el_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1)) + client.post(f"/game/{slug}/elements/{el_id}/save", data={ + "ctrl_visibilite_cond_mode": "objet", + "ctrl_visibilite_cond_definition_id": str(def_id), + "ctrl_visibilite_cond_champ": field_name, + "ctrl_visibilite_cond_operateur": "egal", + "ctrl_visibilite_cond_valeur": expected, + }) + return el_id + + +def test_hidden_element_carries_the_class_marker_not_a_comment(client, game): + 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": "0"}) + + screen_id = _create_screen(client, game) + closed_id = _add_icon_with_visibility(client, game, screen_id, def_id, "is_opened", "Non") + open_id = _add_icon_with_visibility(client, game, screen_id, def_id, "is_opened", "Oui") + + payload = client.get(f"/game/{game}/runtime-payload").get_json() + elements = {e["id"]: e for e in payload["screens"][0]["elements"]} + + closed_html = elements[closed_id]["rendered_html"] + open_html = elements[open_id]["rendered_html"] + + # is_opened=faux : l'enveloppe "fermée" est visible (marqueur en + # commentaire, après son vrai contenu) ; l'enveloppe "ouverte" est + # cachée (marqueur en CLASSE, directement sur le placeholder). + assert "" in closed_html + assert f'data-element-id="{closed_id}"' in closed_html + assert "display:none" not in closed_html + + assert 'class="visibilityGated"' in open_html + assert "display:none" in open_html + assert f'data-element-id="{open_id}"' in open_html + assert "" not in open_html + + +def test_toggling_the_field_swaps_which_element_carries_which_marker(client, game): + resp = client.post(f"/game/{game}/objects/new", data={ + "object_name": "Email2", "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]) + resp = client.post(f"/game/{game}/objects/{def_id}/data/new", data={"is_opened": "0"}, follow_redirects=False) + + screen_id = _create_screen(client, game) + closed_id = _add_icon_with_visibility(client, game, screen_id, def_id, "is_opened", "Non") + open_id = _add_icon_with_visibility(client, game, screen_id, def_id, "is_opened", "Oui") + + import db + rows = db.list_rows(game, db.get_definition(game, def_id)) + row_id = rows[0]["id"] + db.update_row(game, db.get_definition(game, def_id), row_id, {"is_opened": "on"}) + + payload = client.get(f"/game/{game}/runtime-payload").get_json() + elements = {e["id"]: e for e in payload["screens"][0]["elements"]} + + # is_opened=vrai maintenant : les rôles s'inversent complètement, aucun + # des deux ne doit garder son ancien marqueur. + assert 'class="visibilityGated"' in elements[closed_id]["rendered_html"] + assert "" in elements[open_id]["rendered_html"]