Régression du commit précédent (d87d6ad, classes Bulma sur la
superposition) : la classe ".box" impose elle-même une couleur de texte
SOMBRE (pensée pour un fond blanc). Comme aucun widget ne fige de couleur
de texte à sa création (default_style_for_widget.py), un titre/texte posé
dans la boîte SANS couleur personnalisée héritait de ce gris sombre
imposé par Bulma — invisible sur le fond sombre par défaut de cette boîte
de dialogue. Ni erreur serveur ni régression de test visible : juste un
texte "présent mais invisible", exactement ce qui a été rapporté ("j'ai
mis un texte dedans mais il ne se voit pas").
Correctif : la boîte fixe elle-même une couleur de texte claire par
défaut (color:#e8eaf0) dans son propre style inline — l'élément le plus
proche gagne, donc ça écrase la couleur imposée par la classe Bulma, tout
en restant surchargeable : un titre/texte qui personnalise sa propre
couleur (réglage "Couleur du texte") continue de l'emporter normalement.
Nouveau test de régression, confirmé en échec sur le commit précédent
(même style Bulma, pas encore cette couleur) puis au vert avec le
correctif. 138 tests au vert au total. Jeu de démo régénéré.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
316 lines
14 KiB
Python
316 lines
14 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_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
|