Files
Forge-Engine/tests/test_dialogue_box.py
T
williamandClaude Sonnet 5 d705f58c4a
Build and deploy / test-python (push) Failing after 1m36s
Build and deploy / test-js (push) Successful in 55s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Fix accès au canevas masqué + quiz : header quête, réponse fausse n'avance jamais bloquée, animé
- Bug corrigé : les panneaux flottants "🧩 Objets"/"⚙️ Propriétés" sont en
  position:fixed (hors du flux flex de .builder3) — ils flottaient
  PAR-DESSUS le canevas sans jamais réduire sa largeur, rendant sa
  partie droite/gauche inaccessible au clic/glisser tant qu'un panneau
  restait ouvert. .builderCanvasArea réserve maintenant leur largeur
  (marge) dès qu'un panneau est ouvert (:has()).

- Boîte à quiz : header = "Quête : <titre>" (au lieu du texte de la
  question), corps = la question ET ses choix ensemble.

- Mauvaise réponse : ne bloque plus JAMAIS la progression (bug signalé :
  "je suis obligé de bien répondre sinon j'avance pas") — surligne la
  bonne réponse en vert (le choix cliqué en rouge s'il était faux) puis
  avance automatiquement après un court délai, sans jamais octroyer de
  points. Un second clic pendant la révélation est ignoré.

- Animations : la boîte à quiz rejoue une entrée (pop-in) à CHAQUE
  nouvelle question, la bonne réponse pulse en vert, une mauvaise
  réponse "secoue" en rouge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 16:08:59 +02:00

219 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Widget d'interface "🖥️ Interface -> 💬 Boîte de dialogue" (voir
screens/rendering/dialogue_box_style.py, screens/scenes/add_scene_object.py
kind="dialogue_box") : affiche automatiquement, en jeu, la réplique en
cours d'une quête déclenchée par une règle de collision "quete" (voir
static/js/play/dialogue-box-controller.js) — header/body/footer
stylisables. Et le nom d'un personnage ("ℹ️ Informations",
screens/scenes/set_scene_object_name.py), utilisé comme "qui parle" dans
l'éditeur de dialogue de quête (screens/rendering/scene_object_names.py)."""
import db
import screens
def _create_jeu2d_game(tmp_game_slug_cleanup, name="pytest_dialogue_box"):
slug = tmp_game_slug_cleanup(db.create_game(name))
screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
return slug, screen_id
def test_add_dialogue_box_has_sensible_default_size(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="dialogue_box")
obj = screens.get_scene_object(slug, obj_id)
assert obj["kind"] == "dialogue_box"
assert obj["width"] == 320
assert obj["height"] == 150
def test_dialogue_box_default_style(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="dialogue_box")
obj = screens.get_scene_object(slug, obj_id)
style = screens.resolve_dialogue_box_style(obj)
assert style == screens.DEFAULT_DIALOGUE_BOX_STYLE
def test_render_dialogue_box_has_header_body_footer(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="dialogue_box")
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert "dialogueBoxHeader" in html
assert "dialogueBoxBody" in html
assert "dialogueBoxFooter" in html
assert f'data-object-id="{obj_id}"' in html
# Bug corrigé : le footer n'avait PAS data-dialogue-role="footer" (seul
# son bouton "Suivant" interne l'avait, sous "next-btn") — le JS
# (forgeShowQuestOffer, static/js/play/dialogue-box-controller.js) le
# cherche par ce sélecteur pour y injecter Accepter/Refuser en fin de
# dialogue d'une quête "nouvelle" ; sans lui, l'écran d'offre gardait
# silencieusement le bouton "Suivant" au lieu des deux nouveaux boutons.
assert 'data-dialogue-role="header"' in html
assert 'data-dialogue-role="body"' in html
assert 'data-dialogue-role="footer"' in html
def test_set_scene_object_dialogue_box_style_persists_and_sanitizes(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="dialogue_box")
style = screens.set_scene_object_dialogue_box_style(slug, obj_id, {
"font_family": "Georgia", "font_size": 999, "font_weight": "bold",
"text_color": "#ff0000", "header_bg": "#000000", "unknown_key": "ignored",
})
assert style["font_family"] == "Georgia"
assert style["font_size"] == 48 # bornée au maximum autorisé
assert style["font_weight"] == "bold"
assert style["text_color"] == "#ff0000"
assert style["header_bg"] == "#000000"
obj = screens.get_scene_object(slug, obj_id)
assert screens.resolve_dialogue_box_style(obj) == style
def test_dialogue_box_route_persists_style(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="dialogue_box")
resp = client.post(
f"/game/{slug}/scene-objects/{obj_id}/dialogue-box-style",
data='{"style": {"header_bg": "#123456"}}', content_type="application/json",
)
assert resp.get_json()["style"]["header_bg"] == "#123456"
def test_dialogue_box_route_404s_for_missing_object(client, game):
resp = client.post(f"/game/{game}/scene-objects/999999/dialogue-box-style", data='{"style": {}}', content_type="application/json")
assert resp.status_code == 404
def test_dialogue_box_excluded_from_collision_rules_payload(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
screens.add_scene_object(slug, screen_id, kind="dialogue_box")
payload = screens.full_game_payload(slug)
scene = next(s for s in payload["screens"] if s["id"] == screen_id)
box = next(o for o in scene["elements"] if o["kind"] == "dialogue_box")
assert "collision_rules" not in box
assert "collision" not in box
assert box["dialogue_box_style"] == screens.DEFAULT_DIALOGUE_BOX_STYLE
def test_full_game_payload_exposes_quests_game_wide(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
db.create_quest(slug, "Trouver la clé")
payload = screens.full_game_payload(slug)
assert [q["title"] for q in payload["quests"]] == ["Trouver la clé"]
# ---------- Nom d'un personnage ("ℹ️ Informations") ----------
def test_scene_object_name_is_none_by_default(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="personnage")
obj = screens.get_scene_object(slug, obj_id)
assert obj["name"] is None
def test_set_scene_object_name_persists_and_trims(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="personnage")
screens.set_scene_object_name(slug, obj_id, " Le Sorcier ")
assert screens.get_scene_object(slug, obj_id)["name"] == "Le Sorcier"
def test_set_scene_object_name_blank_clears_it(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="personnage")
screens.set_scene_object_name(slug, obj_id, "Le Sorcier")
screens.set_scene_object_name(slug, obj_id, " ")
assert screens.get_scene_object(slug, obj_id)["name"] is None
def test_scene_object_name_route_persists(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="personnage")
resp = client.post(f"/game/{slug}/scene-objects/{obj_id}/name", data='{"name": "Le Sorcier"}', content_type="application/json")
assert resp.status_code == 200
assert screens.get_scene_object(slug, obj_id)["name"] == "Le Sorcier"
def test_list_named_scene_object_names_is_alphabetical_deduped_and_includes_any_kind(tmp_game_slug_cleanup):
""""il peut sélectionner le nom du personnage qui parle" -> étendu à
N'IMPORTE QUEL objet de scène nommé (pas seulement un personnage),
voir screens/rendering/scene_object_names.py."""
slug = tmp_game_slug_cleanup(db.create_game("pytest_scene_object_names"))
scene_a = screens.create_screen(slug, "Scène A", kind="jeu_2d")
scene_b = screens.create_screen(slug, "Scène B", kind="jeu_2d")
zed_id = screens.add_scene_object(slug, scene_a, kind="personnage")
screens.set_scene_object_name(slug, zed_id, "Zed le forgeron")
anna_id = screens.add_scene_object(slug, scene_b, kind="personnage")
screens.set_scene_object_name(slug, anna_id, "Anna")
unnamed_id = screens.add_scene_object(slug, scene_a, kind="personnage") # sans nom, ignoré
decor_id = screens.add_scene_object(slug, scene_a, kind="decor") # nommé -> INCLUS (pas réservé au personnage)
screens.set_scene_object_name(slug, decor_id, "Un coffre")
dialogue_box_id = screens.add_scene_object(slug, scene_a, kind="dialogue_box") # widget d'UI, jamais un "qui parle"
screens.set_scene_object_name(slug, dialogue_box_id, "Boîte nommée par erreur")
assert screens.list_named_scene_object_names(slug) == ["Anna", "Un coffre", "Zed le forgeron"]
# ---------- Widgets "❓ Boîte à quiz" / "🏆 Score" ----------
def test_add_quiz_box_has_sensible_default_size(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
obj = screens.get_scene_object(slug, obj_id)
assert obj["kind"] == "quiz_box"
assert obj["width"] == 340
assert obj["height"] == 240
def test_add_score_widget_has_sensible_default_size(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="score_widget")
obj = screens.get_scene_object(slug, obj_id)
assert obj["kind"] == "score_widget"
assert obj["width"] == 140
assert obj["height"] == 50
def test_render_quiz_box_has_header_and_choices(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert 'data-quiz-role="header"' in html # "Quête : <titre>", rempli en jeu
assert 'data-quiz-role="question"' in html # le texte de la question, dans le corps
assert 'data-quiz-role="choices"' in html
assert f'data-object-id="{obj_id}"' in html
assert "quizBoxFooter" not in html # pas de pied, une question se résout au clic
def test_render_score_widget_shows_zero_by_default(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="score_widget")
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert 'data-score-role="value"' in html
assert f'data-object-id="{obj_id}"' in html
def test_quiz_box_and_score_widget_share_the_same_style_system_as_dialogue_box(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
quiz_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
style = screens.set_scene_object_dialogue_box_style(slug, quiz_id, {"header_bg": "#abcdef"})
assert style["header_bg"] == "#abcdef"
obj = screens.get_scene_object(slug, quiz_id)
assert screens.resolve_dialogue_box_style(obj)["header_bg"] == "#abcdef"
def test_quiz_box_and_score_widget_excluded_from_collision_rules_payload(tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.add_scene_object(slug, screen_id, kind="score_widget")
payload = screens.full_game_payload(slug)
scene = next(s for s in payload["screens"] if s["id"] == screen_id)
quiz = next(o for o in scene["elements"] if o["kind"] == "quiz_box")
score = next(o for o in scene["elements"] if o["kind"] == "score_widget")
for widget in (quiz, score):
assert "collision_rules" not in widget
assert "collision" not in widget
assert widget["dialogue_box_style"] == screens.DEFAULT_DIALOGUE_BOX_STYLE