Add "survol"/"fin_survol" trigger events (mouseenter/mouseleave, same anti-doublon pattern as bindClicks()) so hovering an element can run flow nodes, instead of the old static hover-text-only control removed from the properties panel. Also add "Contenu" as a settable "Modifier un élément" property so an action can display a defined text on another element on hover — the concrete use case that motivated this. Verified live with Playwright: a "Au survol" trigger on one element correctly updates another element's text via the action, and text stays put with no "Fin du survol" wired (explicit, no implicit revert, consistent with "Au clic").
244 lines
10 KiB
Python
244 lines
10 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_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
|