Corrige l'enveloppe ouverte/fermée affichées en même temps (bug préexistant)
Ce bug n'est PAS lié à la refonte design system de cette session — le
fichier concerné (templates/play.html) n'avait plus été touché depuis
une session précédente (commit ed4dd78), bien avant les changements de
CSS/Bulma. Vérifié en cherchant "les données ont-elles été perdues ?" :
non, tous les objets/champs restent intacts en base (projects/test/game.db).
Root cause réel : refreshRuntimeData()::findCommentMarkedDescendants()
ne repérait, dans le HTML fraîchement régénéré après une action
"Modifier une donnée", que les éléments qui REDEVIENNENT VISIBLES sous
condition (repérés via le commentaire "<!--visibilityGated-->" posé après
leur contenu réel par _mark(), voir render_element_html.py) — jamais ceux
qui REDEVIENNENT CACHÉS, dont le placeholder ('<div class=
"visibilityGated" ... style="display:none;">') n'a pas de commentaire à
sa suite, juste une classe sur lui-même. Résultat : l'élément qui
redevient visible est bien patché dans le DOM, mais l'ancien élément
visible n'est jamais retiré — les deux restent affichés en même temps
(ex. "enveloppe fermée" jamais masquée à côté de "enveloppe ouverte" qui
apparaît après un clic).
Corrigé en faisant aussi reconnaître, dans findCommentMarkedDescendants(),
la classe "visibilityGated" directement sur la balise (cas caché), en
plus du commentaire (cas visible) — les deux sens du bascule sont
maintenant retrouvés et patchés.
tests/test_visibility_toggle_markers.py (nouveau) verrouille le contrat
côté serveur dont dépend ce correctif JS : un élément cité verrouille que
l'élément CACHÉ porte bien la classe (jamais de commentaire), l'élément
VISIBLE porte bien le commentaire (jamais la classe), et que ces rôles
s'inversent correctement quand la donnée change. 199 tests au total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c60884ec46
commit
6b9a491387
+14
-1
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 "<!--visibilityGated-->" posé après son contenu réel par
|
||||
_mark(), voir render_element_html.py) — jamais celui qui REDEVIENT
|
||||
CACHÉ, dont le placeholder ("<div class=\"visibilityGated\" ...
|
||||
style=\"display:none;\"></div>") 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 "<!--visibilityGated-->" 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 "<!--visibilityGated-->" 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 "<!--visibilityGated-->" in elements[open_id]["rendered_html"]
|
||||
Reference in New Issue
Block a user