Deux fonctionnalités demandées, développées et corrigées dans cet
échange :
1. "Donnée liée" (Texte/Titre) : le réglage à 2 filtres fixes (toujours
combinés en ET) devient une liste de conditions ILLIMITÉE, avec un
choix ET/OU pour les combiner (screens/widgets/controls/c_clause_list.py,
screens/clause_list_codec.py). Rétrocompatible avec les anciens
éléments (_data_filtre_champ/_data_filtre2_champ), convertis à la
volée à la lecture, sans migration. Après un premier essai à la
présentation trop compacte et technique (retour utilisateur : "pas de
champ technique, pas de notation bizarre {{ }}"), la présentation
finale reprend EXACTEMENT l'ancien style (labels "Champ"/"...est"/
"...cette valeur", même sélecteur de valeur fixe/dynamique/variable
déjà existant, jamais la syntaxe brute), simplement répétée par
condition (templates/partials/clause_row.html), avec un bouton
"+ Ajouter une condition" bien visible et une liste scrollable
(static/style.css, .clauseListWrap). Le même moteur (filter_repeater_
rows.py généralisé) profite aussi au Répéteur de données en interne.
2. Nœud Condition de la Logique de la scène : peut désormais tester une
VARIABLE GLOBALE en plus d'un champ d'objet (cond_source/cond_variable/
cond_variable_chemin — screens/flow/ensure_flow_schema.py), sur la
clause principale ET chaque clause supplémentaire (ET/OU). Évalué côté
CLIENT (templates/play.html, evaluateConditionClause), contre un
nouveau gameData.variables exposé par full_game_payload.py — tenu à
jour par refreshRuntimeData() après toute action qui modifie une
variable, sans changement supplémentaire nécessaire. Le panneau de
condition reste utilisable même sans aucun objet défini dans le jeu
(avant, il disparaissait entièrement).
Vérifié : 153 tests pytest (nouveaux : test_data_binding_clause_list.py,
test_condition_variable.py) + logique JS d'évaluation des conditions
vérifiée isolément avec Node (variable scalaire, objet avec chemin
chaîné, tableau par index, variable introuvable, booléen, rétrocompatibilité
legacy) + rendu des deux pages (éditeur/jeu) vérifié sur le vrai projet
"test" en plus des jeux de test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
477 lines
23 KiB
Python
477 lines
23 KiB
Python
"""Tests des points "Confort" (section 3 de la fiche de cadrage) :
|
||
3.1 interactions au survol, 3.2 séquences temporisées, 3.3 surbrillance
|
||
générique dynamique, 3.4 overlay/modale réutilisable, 3.5 verrouillage
|
||
d'un élément après décision."""
|
||
import json
|
||
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_element(client, slug, screen_id, widget):
|
||
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))
|
||
|
||
|
||
# ---------- 3.1 — Interactions au survol ----------
|
||
|
||
def test_hover_text_control_removed_from_properties_panel(client, game):
|
||
"""Le réglage "Survol" a été retiré du panneau de propriétés (voir
|
||
universal_controls.py) : survoler un élément est conceptuellement un
|
||
déclencheur de la Logique de la scène, pas une propriété statique —
|
||
reconstruit là-bas avec les déclencheurs "survol"/"fin_survol" (voir
|
||
plus bas). Poster ctrl_survol_texte ne doit donc plus avoir d'effet (le
|
||
mécanisme data-hover-text/bindHoverTexts sous-jacent reste en place, il
|
||
n'est simplement plus réglable depuis ce panneau)."""
|
||
screen_id = _create_screen(client, game)
|
||
el_id = _add_element(client, game, screen_id, "titre")
|
||
client.post(f"/game/{game}/elements/{el_id}/save", data={
|
||
"ctrl_content": "Mathilde Dubois", "ctrl_survol_texte": "mathilde.d@forgebase.fr",
|
||
})
|
||
resp = client.get(f"/game/{game}/play")
|
||
html = resp.data.decode()
|
||
assert 'data-hover-text="' not in html
|
||
|
||
|
||
def test_hover_text_absent_by_default_no_regression(client, game):
|
||
"""Sans réglage de survol, aucun attribut data-hover-text ne doit
|
||
apparaître — aucune régression sur les éléments existants."""
|
||
screen_id = _create_screen(client, game)
|
||
_add_element(client, game, screen_id, "titre")
|
||
resp = client.get(f"/game/{game}/play")
|
||
html = resp.data.decode()
|
||
# (le mot "data-hover-text" apparaît dans un commentaire JS du moteur
|
||
# lui-même — on vérifie donc l'absence de l'ATTRIBUT réellement posé sur
|
||
# un élément, pas la simple présence de la chaîne dans la page)
|
||
assert 'data-hover-text="' not in html
|
||
|
||
|
||
def test_play_page_exposes_hover_binding_runtime(client, game):
|
||
resp = client.get(f"/game/{game}/play")
|
||
html = resp.data.decode()
|
||
assert "bindHoverTexts" in html
|
||
|
||
|
||
def test_survol_trigger_node_persists(client, game):
|
||
screen_id = _create_screen(client, game)
|
||
el_id = _add_element(client, game, screen_id, "titre")
|
||
resp = client.post(
|
||
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||
data=json.dumps({"node_type": "trigger", "trigger_element_id": el_id, "trigger_event": "survol"}),
|
||
content_type="application/json",
|
||
)
|
||
assert resp.status_code == 200
|
||
node = resp.get_json()
|
||
assert node["trigger_event"] == "survol"
|
||
assert node["trigger_element_id"] == el_id
|
||
|
||
|
||
def test_play_page_exposes_hover_trigger_runtime(client, game):
|
||
"""bindHoverTriggers() (mouseenter/mouseleave -> runFlowFrom) doit être
|
||
exposé et appelé, exactement comme bindClicks() pour "Au clic"."""
|
||
resp = client.get(f"/game/{game}/play")
|
||
html = resp.data.decode()
|
||
assert "bindHoverTriggers" in html
|
||
assert "'survol'" in html
|
||
assert "'fin_survol'" in html
|
||
|
||
|
||
def test_modifier_element_contenu_action_persists(client, game):
|
||
""""Modifier un élément → Contenu" : c'est ce qui permet de reconstruire
|
||
l'ancien "texte affiché au survol" (et bien d'autres usages) à la main
|
||
dans la Logique de la scène, en le combinant avec un déclencheur
|
||
"Au survol" posé sur un AUTRE élément."""
|
||
screen_id = _create_screen(client, game)
|
||
source_id = _add_element(client, game, screen_id, "titre")
|
||
target_id = _add_element(client, game, screen_id, "texte")
|
||
resp = client.post(
|
||
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||
data=json.dumps({
|
||
"node_type": "action", "action_type": "modifier_element",
|
||
"target_element_id": target_id, "element_property": "contenu", "element_value": "Survol actif !",
|
||
}),
|
||
content_type="application/json",
|
||
)
|
||
assert resp.status_code == 200
|
||
node = resp.get_json()
|
||
assert node["element_property"] == "contenu"
|
||
assert node["element_value"] == "Survol actif !"
|
||
assert node["target_element_id"] == target_id
|
||
# source_id n'est utilisé que pour documenter le scénario (le
|
||
# déclencheur "Au survol" se poserait dessus) — non exercé ici, déjà
|
||
# couvert par test_survol_trigger_node_persists.
|
||
assert source_id != target_id
|
||
|
||
|
||
def test_play_page_exposes_contenu_property_runtime(client, game):
|
||
resp = client.get(f"/game/{game}/play")
|
||
html = resp.data.decode()
|
||
assert "'contenu'" in html
|
||
|
||
|
||
# ---------- 3.2 — Séquences temporisées ----------
|
||
|
||
def test_attendre_action_node_persists_delay(client, game):
|
||
screen_id = _create_screen(client, game)
|
||
resp = client.post(
|
||
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||
data=json.dumps({"node_type": "action", "action_type": "attendre", "data_value": "5"}),
|
||
content_type="application/json",
|
||
)
|
||
assert resp.status_code == 200
|
||
node = resp.get_json()
|
||
assert node["action_type"] == "attendre"
|
||
assert node["data_value"] == "5"
|
||
|
||
|
||
def test_play_page_exposes_wait_action_runtime(client, game):
|
||
resp = client.get(f"/game/{game}/play")
|
||
html = resp.data.decode()
|
||
assert "'attendre'" in html
|
||
assert "setTimeout" in html
|
||
|
||
|
||
# ---------- 3.3 — Surbrillance générique dynamique ----------
|
||
|
||
def test_surbrillance_action_node_persists(client, game):
|
||
screen_id = _create_screen(client, game)
|
||
el_id = _add_element(client, game, screen_id, "bouton")
|
||
resp = client.post(
|
||
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||
data=json.dumps({
|
||
"node_type": "action", "action_type": "modifier_element",
|
||
"target_element_id": el_id, "element_property": "surbrillance", "element_value": "on",
|
||
}),
|
||
content_type="application/json",
|
||
)
|
||
assert resp.status_code == 200
|
||
node = resp.get_json()
|
||
assert node["element_property"] == "surbrillance"
|
||
assert node["element_value"] == "on"
|
||
|
||
|
||
def test_play_page_exposes_highlight_runtime_and_css(client, game):
|
||
resp = client.get(f"/game/{game}/play")
|
||
html = resp.data.decode()
|
||
assert "forgeHighlight" in html
|
||
assert "'surbrillance'" in html
|
||
|
||
|
||
# ---------- 3.4 — Overlay / modale réutilisable ----------
|
||
|
||
def test_overlay_widget_renders_fullscreen_fixed_box(client, game):
|
||
screen_id = _create_screen(client, game)
|
||
overlay_id = _add_element(client, game, screen_id, "superposition")
|
||
client.post(f"/game/{game}/elements/{overlay_id}/save", data={"ctrl_couleur_boite": "#222222", "ctrl_arrondi": "20"})
|
||
client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "titre"})
|
||
|
||
resp = client.get(f"/game/{game}/play")
|
||
html = resp.data.decode()
|
||
idx = html.find(f'data-element-id="{overlay_id}"')
|
||
assert idx != -1
|
||
# Le tag qui porte data-element-id est celui rendu par _render_overlay :
|
||
# on regarde tout son contenu de balise ouvrante (jusqu'au ">" suivant)
|
||
# ainsi que ce qui suit immédiatement (la boîte centrée à l'intérieur).
|
||
tag_start = html.rfind("<div", 0, idx)
|
||
snippet = html[tag_start:idx + 400]
|
||
assert "position:fixed" in snippet
|
||
assert "forgeOverlayBox" in snippet
|
||
|
||
|
||
def test_overlay_box_default_text_color_survives_the_bulma_box_class(client, game):
|
||
"""Régression : la classe Bulma ".box" (voir render_overlay.py) impose
|
||
elle-même une couleur de texte SOMBRE, pensée pour un fond blanc. Un
|
||
texte posé dans la boîte SANS couleur personnalisée (le cas par défaut
|
||
— aucun widget ne fige de couleur à sa création, voir
|
||
default_style_for_widget.py) héritait donc de ce gris sombre, invisible
|
||
sur le fond sombre par défaut de la boîte — "j'ai mis un texte dedans
|
||
mais il ne se voit pas", sans la moindre erreur serveur. La boîte doit
|
||
donc fixer elle-même une couleur de texte claire par défaut, que Bulma
|
||
ne peut plus écraser (élément le plus proche gagne)."""
|
||
screen_id = _create_screen(client, game)
|
||
overlay_id = _add_element(client, game, screen_id, "superposition")
|
||
client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "texte"})
|
||
|
||
resp = client.get(f"/game/{game}/play")
|
||
html = resp.data.decode()
|
||
idx = html.find("forgeOverlayBox")
|
||
assert idx != -1
|
||
box_tag = html[html.rfind("<div", 0, idx):idx + 200]
|
||
assert "color:#e8eaf0" in box_tag
|
||
|
||
|
||
def test_visibility_dropdown_reflects_masque_even_when_visible_would_also_match(client, game):
|
||
"""Régression (la vraie cause derrière "je ne vois plus ma modale, ni
|
||
même en changeant Visibilité sur Visible") : le panneau de propriétés
|
||
détecte la valeur ACTUELLE d'un réglage "preset" (screens/widgets/
|
||
control_value.py) en cherchant la PREMIÈRE option de la liste dont les
|
||
critères correspondent au style stocké. L'option "Visible normalement"
|
||
de la Visibilité (visibility_control.py) ne vérifie QUE "visibility"
|
||
(jamais "display", par choix assumé) — un élément "Masqué" (qui ne pose
|
||
que "display:none", jamais "visibility") satisfaisait donc TOUJOURS,
|
||
trivialement, les critères de "Visible" en premier (testée avant
|
||
"Masqué" dans la liste) : le panneau affichait "Visible normalement"
|
||
sélectionné sur un élément EN RÉALITÉ masqué. Comme un <select> ne
|
||
déclenche un enregistrement que sur un changement RÉEL de valeur,
|
||
re-choisir l'option déjà affichée ne faisait RIEN : impossible de
|
||
rendre l'élément visible depuis le panneau. Corrigé en vérifiant les
|
||
options les plus SPÉCIFIQUES (le plus de propriétés non vides exigées)
|
||
en premier."""
|
||
from screens.widgets.control_value import _control_value
|
||
from screens.widgets.widget_meta import widget_meta
|
||
import screens
|
||
|
||
screen_id = _create_screen(client, game)
|
||
overlay_id = _add_element(client, game, screen_id, "superposition")
|
||
el = screens.get_element(game, overlay_id)
|
||
meta = widget_meta(el["widget"])
|
||
vis_control = next(c for c in meta["controls"] if c["key"] == "visibilite")
|
||
# "Masqué" par défaut à la création (default_style_for_widget.py) : le
|
||
# panneau doit détecter "masque", pas "visible".
|
||
assert _control_value(el, vis_control) == "masque"
|
||
|
||
|
||
def test_overlay_behaves_like_a_normal_container_in_the_editor(client, game):
|
||
"""Régression (deux retours utilisateur successifs) :
|
||
1. Un essai précédent forçait "display:flex" dans l'éditeur, quel que
|
||
soit le réglage "Visibilité" -> "je veux avoir la main sur la
|
||
visibilité de la modale, sinon elle s'affiche toujours sur la
|
||
scène [éditeur] et court-circuite ma logique". L'éditeur doit donc
|
||
respecter "Visibilité" normalement, EXACTEMENT comme n'importe quel
|
||
autre widget (masqué = display:none aussi dans l'éditeur).
|
||
2. La boîte gardait son voile plein écran (position:fixed + fond
|
||
assombri) même dans l'éditeur -> "je souhaite que rien ne soit
|
||
assombri, l'assombrissement ne se fait que quand la scène est
|
||
jouée". Le voile plein écran ne doit donc apparaître qu'en mode
|
||
JOUABLE — dans l'éditeur, ce widget se comporte comme un conteneur
|
||
normal (position/taille selon x/y/width/height, pas de voile)."""
|
||
screen_id = _create_screen(client, game)
|
||
overlay_id = _add_element(client, game, screen_id, "superposition")
|
||
client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "texte"})
|
||
|
||
# Par défaut ("Masqué" à la création) : invisible dans l'éditeur aussi,
|
||
# comme n'importe quel autre widget masqué (pas de forçage).
|
||
edit_html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
||
idx = edit_html.find(f'id="elt-{overlay_id}"')
|
||
assert idx != -1
|
||
tag = edit_html[edit_html.rfind("<div", 0, idx):idx + 30]
|
||
assert "display:none" in tag
|
||
assert "position:fixed" not in tag
|
||
assert "rgba(0,0,0,0.6)" not in tag
|
||
|
||
# Repassé "Visible" à la main (garder la main sur la visibilité) :
|
||
# apparaît dans l'éditeur, mais toujours SANS voile plein écran.
|
||
client.post(f"/game/{game}/elements/{overlay_id}/save", data={"ctrl_visibilite": "visible"})
|
||
edit_html2 = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
||
idx2 = edit_html2.find(f'id="elt-{overlay_id}"')
|
||
tag2 = edit_html2[edit_html2.rfind("<div", 0, idx2):idx2 + 30]
|
||
assert "display:none" not in tag2
|
||
assert "position:fixed" not in tag2
|
||
assert "rgba(0,0,0,0.6)" not in tag2
|
||
|
||
# En mode JOUABLE, le comportement plein écran + voile reste inchangé.
|
||
play_html = client.get(f"/game/{game}/play").data.decode()
|
||
idx3 = play_html.find(f'id="elt-{overlay_id}"')
|
||
tag3 = play_html[play_html.rfind("<div", 0, idx3):idx3 + 250]
|
||
assert "position:fixed" in tag3
|
||
assert "rgba(0,0,0,0.6)" in tag3
|
||
|
||
|
||
def test_overlay_element_type_instance_has_no_visible_wrapper_box(client, game):
|
||
"""Régression : poser un élément de jeu réutilisable ("Mes éléments de
|
||
jeu") dont le MODÈLE n'est qu'une superposition affichait, sur la
|
||
vraie scène, une boîte "conteneur" vide et TOUJOURS VISIBLE à
|
||
l'endroit où l'exemplaire a été déposé — "un conteneur vide apparaît,
|
||
pas la boîte de dialogue" (elle existe bien, masquée comme prévu ;
|
||
c'est l'enveloppe générique "conteneur" AUTOUR, posée par défaut pour
|
||
tout exemplaire (add_element.py), qui n'aurait jamais dû être visible
|
||
pour un modèle qui n'est QUE ça). Corrigé en court-circuitant cette
|
||
enveloppe dès que le modèle entier est une superposition."""
|
||
import screens
|
||
resp = client.post(f"/game/{game}/element-types", data={"name": "Dialogue"}, follow_redirects=False)
|
||
assert resp.status_code == 302
|
||
et = next(t for t in screens.list_element_types(game) if t["name"] == "Dialogue")
|
||
template_screen_id = et["template_screen_id"]
|
||
overlay_id = _add_element(client, game, template_screen_id, "superposition")
|
||
client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "texte"})
|
||
|
||
screen_id = _create_screen(client, game, "Scène")
|
||
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add",
|
||
data={"widget": "__catalogue__", "element_type_id": et["id"]}, follow_redirects=False)
|
||
assert resp.status_code == 302
|
||
# Un frère ajouté APRÈS -> z_index plus grand, comme dans le scénario
|
||
# rapporté (une scène avec d'autres éléments déjà en place).
|
||
_add_element(client, game, screen_id, "bouton")
|
||
|
||
html = client.get(f"/game/{game}/play").data.decode()
|
||
assert "forgeOverlayBox" in html
|
||
assert 'class="box"' not in html # l'enveloppe "conteneur" par défaut ne doit plus apparaître
|
||
idx = html.find('class="modal is-active"')
|
||
wrapper_start = html.rfind('<div class="playElement"', 0, idx)
|
||
wrapper_style = re.search(r'style="([^"]*)"', html[wrapper_start:idx]).group(1)
|
||
assert "z-index" not in wrapper_style
|
||
|
||
|
||
def test_overlay_element_type_instance_is_controllable_from_the_hosting_scene(client, game):
|
||
"""Régression : la logique posée sur LA SCÈNE qui accueille un
|
||
exemplaire de dialogue (ex. "Modifier un élément → Modale : Visibilité
|
||
= Rendre visible", ciblant l'exemplaire par son id SUR CETTE SCÈNE)
|
||
n'avait plus aucun effet une fois l'enveloppe "conteneur" entièrement
|
||
court-circuitée (un essai précédent) : son id disparaissait du DOM
|
||
(impossible à cibler), et même en le gardant, la superposition INTERNE
|
||
au modèle restait masquée indépendamment (double masquage — rendre
|
||
l'enveloppe visible n'aurait rien changé). Corrigé : l'enveloppe
|
||
GARDE son propre id/data-element-id (ciblable depuis la scène), et la
|
||
superposition interne au modèle ignore désormais son propre réglage
|
||
"Visibilité" une fois posée comme exemplaire — tout le masquage est
|
||
délégué à l'enveloppe (qui démarre elle-même masquée par défaut, voir
|
||
add_element.py)."""
|
||
import screens
|
||
resp = client.post(f"/game/{game}/element-types", data={"name": "Dialogue2"}, follow_redirects=False)
|
||
et = next(t for t in screens.list_element_types(game) if t["name"] == "Dialogue2")
|
||
overlay_id = _add_element(client, game, et["template_screen_id"], "superposition")
|
||
client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "texte"})
|
||
|
||
screen_id = _create_screen(client, game, "Scène2")
|
||
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add",
|
||
data={"widget": "__catalogue__", "element_type_id": et["id"]}, follow_redirects=False)
|
||
instance_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
|
||
|
||
html = client.get(f"/game/{game}/play").data.decode()
|
||
idx = html.find(f'data-element-id="{instance_id}"')
|
||
assert idx != -1
|
||
wrapper_tag = html[html.rfind("<div", 0, idx):idx + 40]
|
||
assert "display:none" in wrapper_tag # masqué par défaut, sur l'EXEMPLAIRE
|
||
|
||
# La superposition interne au modèle, elle, ne doit PLUS porter son
|
||
# propre display:none une fois rendue comme exemplaire (sinon la
|
||
# rendre visible depuis la scène resterait sans effet).
|
||
idx_modal = html.find('class="modal is-active"', idx)
|
||
modal_tag = html[html.rfind("<div", 0, idx_modal):idx_modal + 250]
|
||
assert "display:none" not in modal_tag
|
||
|
||
trig = client.post(f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||
data=json.dumps({"node_type": "trigger", "trigger_event": "affichage"}),
|
||
content_type="application/json").get_json()
|
||
act = client.post(f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||
data=json.dumps({
|
||
"node_type": "action", "action_type": "modifier_element",
|
||
"target_element_id": instance_id, "element_property": "visibilite", "element_value": "visible",
|
||
}), content_type="application/json").get_json()
|
||
assert act["target_element_id"] == instance_id
|
||
|
||
|
||
def test_overlay_wrapper_does_not_trap_its_own_z_index(client, game):
|
||
"""Régression : le cadre .playElement/.canvasElement partagé par TOUS
|
||
les widgets (voir filters/element_style_filter.py) posait quand même
|
||
"z-index:<sa place dans le canevas>" sur la superposition, MÊME SI son
|
||
propre contenu (render_overlay.py) ignore x/y/width/height et pose déjà
|
||
position:fixed + z-index:9999 lui-même. Un élément positionné avec un
|
||
z-index EXPLICITE crée un NOUVEAU contexte d'empilement CSS : le 9999
|
||
posé plus profond ne se comparait alors plus qu'AU SEIN de ce contexte,
|
||
et perdait face au z-index (plus grand) d'un élément normal ajouté
|
||
APRÈS l'overlay sur le canevas — qui s'affichait donc PAR-DESSUS le
|
||
dialogue censé tout couvrir. Le cadre garde position/left/top/width/
|
||
height comme tout widget (l'éditeur en a besoin pour glisser-déposer/
|
||
redimensionner ce cadre — les retirer a fait planter element_geometry
|
||
en régression), mais n'écrit plus DU TOUT de z-index pour ce widget :
|
||
position:absolute avec z-index:auto (omis) ne crée pas de contexte
|
||
d'empilement, donc le 9999 se compare directement aux autres éléments."""
|
||
screen_id = _create_screen(client, game)
|
||
overlay_id = _add_element(client, game, screen_id, "superposition")
|
||
# Ajouté APRÈS l'overlay -> z_index plus grand que le sien.
|
||
_add_element(client, game, screen_id, "bouton")
|
||
|
||
resp = client.get(f"/game/{game}/play")
|
||
html = resp.data.decode()
|
||
idx = html.find(f'data-el-id="{overlay_id}"')
|
||
assert idx != -1
|
||
tag_start = html.rfind("<div", 0, idx)
|
||
wrapper_tag = html[tag_start:idx + 200]
|
||
assert "position:absolute" in wrapper_tag
|
||
assert "z-index" not in wrapper_tag
|
||
|
||
|
||
def test_overlay_geometry_can_still_be_saved_from_the_editor(client, game):
|
||
"""Régression : la première version du correctif ci-dessus retirait
|
||
AUSSI position/left/top/width/height du cadre d'une superposition, ce
|
||
qui effondrait ce cadre à 0×0 dans l'ÉDITEUR (son contenu réel est en
|
||
position:fixed, hors flux) — le calcul de glisser-déposer/
|
||
redimensionnement (screen_edit.html) divise alors par une dimension
|
||
nulle, produit NaN, et JSON.stringify(NaN) envoie "null" : le serveur
|
||
plantait sur float(None) dans element_geometry.py. Le cadre doit donc
|
||
continuer à porter une position/taille en % normale pour ce widget."""
|
||
screen_id = _create_screen(client, game)
|
||
overlay_id = _add_element(client, game, screen_id, "superposition")
|
||
resp = client.post(
|
||
f"/game/{game}/elements/{overlay_id}/geometry",
|
||
data=json.dumps({"x": 12.5, "y": 20, "width": 55, "height": 45}),
|
||
content_type="application/json",
|
||
)
|
||
assert resp.status_code == 200
|
||
|
||
|
||
def test_overlay_starts_hidden_by_default(client, game):
|
||
"""Régression : une Superposition fraîchement posée ne doit PAS couvrir
|
||
tout l'écran dès sa création (position:fixed + inset:0 la ferait sinon
|
||
intercepter tous les clics de l'écran) — elle démarre masquée, à ouvrir
|
||
explicitement via une action."""
|
||
screen_id = _create_screen(client, game)
|
||
overlay_id = _add_element(client, game, screen_id, "superposition")
|
||
resp = client.get(f"/game/{game}/play")
|
||
html = resp.data.decode()
|
||
idx = html.find(f'data-element-id="{overlay_id}"')
|
||
assert idx != -1
|
||
tag_start = html.rfind("<div", 0, idx)
|
||
snippet = html[tag_start:idx]
|
||
assert "display:none" in snippet
|
||
|
||
|
||
def test_overlay_can_be_hidden_and_shown_like_any_element(client, game):
|
||
"""La fermeture manuelle de l'overlay réutilise l'action existante
|
||
"Modifier un élément → Visibilité" — pas de mécanisme dédié."""
|
||
screen_id = _create_screen(client, game)
|
||
overlay_id = _add_element(client, game, screen_id, "superposition")
|
||
resp = client.post(
|
||
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||
data=json.dumps({
|
||
"node_type": "action", "action_type": "modifier_element",
|
||
"target_element_id": overlay_id, "element_property": "visibilite", "element_value": "masque",
|
||
}),
|
||
content_type="application/json",
|
||
)
|
||
assert resp.status_code == 200
|
||
node = resp.get_json()
|
||
assert node["element_property"] == "visibilite"
|
||
assert node["target_element_id"] == overlay_id
|
||
|
||
|
||
# ---------- 3.5 — Verrouillage d'un élément après décision ----------
|
||
|
||
def test_desactive_action_node_persists(client, game):
|
||
screen_id = _create_screen(client, game)
|
||
el_id = _add_element(client, game, screen_id, "bouton")
|
||
resp = client.post(
|
||
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||
data=json.dumps({
|
||
"node_type": "action", "action_type": "modifier_element",
|
||
"target_element_id": el_id, "element_property": "desactive", "element_value": "on",
|
||
}),
|
||
content_type="application/json",
|
||
)
|
||
assert resp.status_code == 200
|
||
node = resp.get_json()
|
||
assert node["element_property"] == "desactive"
|
||
assert node["element_value"] == "on"
|
||
|
||
|
||
def test_play_page_exposes_lock_runtime_and_css(client, game):
|
||
resp = client.get(f"/game/{game}/play")
|
||
html = resp.data.decode()
|
||
assert "forgeDisabled" in html
|
||
assert "pointer-events:none" in html
|
||
assert "'desactive'" in html
|