"""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(" 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(" 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('
" 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("