Files
Forge-Engine/tests/test_quiz_box_config.py
T
williamandClaude Sonnet 5 7db4803b93
Build and deploy / test-python (push) Successful in 12m10s
Build and deploy / test-js (push) Successful in 1m27s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Ajoute la fonctionnalite quiz autonome/plein ecran a la boite a quiz
Introduit la double categorie de modeles (boite de dialogue / page de
quiz plein ecran) avec plein ecran, minuteur, score integre et ecran de
resultat pour les modeles page ; ajoute les modeles "Manga" (boite et
page) et "Classique" (page), pilotables aussi par l'assistant IA Ruby.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 15:07:23 +02:00

631 lines
35 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.
"""Réglages "❓ Boîte à quiz" propres au quiz AUTONOME/plein écran (demande
explicite : "étendre les fonctionnalités de la boîte à quiz pour les RH
qui veulent juste faire un quiz") — voir screens/rendering/
quiz_box_config.py, screens/rendering/quiz_box_templates.py. DISTINCT de
"🎨 Style" (dialogue_box_style.py, partagé avec dialogue_box/score_widget,
voir test_dialogue_box.py) : ce fichier ne teste que plein écran/
minuteur/modèle."""
import db
import screens
def _create_jeu2d_game(tmp_game_slug_cleanup, name="pytest_quiz_config"):
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
# ---------- Sanitizer/résolution ----------
def test_default_quiz_box_config_has_no_fullscreen_no_timer_and_the_defaut_template(tmp_game_slug_cleanup):
""""Aucun" minuteur/plein écran désactivé par défaut — demande
explicite : jamais imposé, seulement au choix du créateur."""
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)
config = screens.resolve_quiz_box_config(obj)
assert config == screens.DEFAULT_QUIZ_BOX_CONFIG
assert config["fullscreen"] is False
assert config["timer_mode"] == "aucun"
assert config["dialog_template"] == "defaut"
assert config["page_template"] == "classique"
def test_sanitize_quiz_box_config_clamps_timer_seconds(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")
config = screens.set_scene_object_quiz_config(slug, obj_id, {"timer_seconds": 999999})
assert config["timer_seconds"] == 600
config = screens.set_scene_object_quiz_config(slug, obj_id, {"timer_seconds": 0})
assert config["timer_seconds"] == 3
def test_sanitize_quiz_box_config_rejects_unknown_timer_mode_and_templates(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")
config = screens.set_scene_object_quiz_config(slug, obj_id, {
"timer_mode": "toutes-les-secondes", "dialog_template": "arc-en-ciel", "page_template": "aquarelle",
})
assert config["timer_mode"] == "aucun"
assert config["dialog_template"] == "defaut"
assert config["page_template"] == "classique"
def test_sanitize_quiz_box_config_keeps_dialog_and_page_template_independent(tmp_game_slug_cleanup):
"""Demande explicite : les deux réglages sont conservés
INDÉPENDAMMENT — un modèle "boîte" invalide ne doit jamais affecter le
modèle "page" valide, et réciproquement ("verre" n'existe plus depuis
la suppression des modèles "boîte" non-base, sert ici justement à
vérifier qu'une valeur invalide retombe sur "defaut" SANS toucher au
page_template valide à côté)."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
config = screens.set_scene_object_quiz_config(slug, obj_id, {"dialog_template": "verre", "page_template": "manga"})
assert config["dialog_template"] == "defaut"
assert config["page_template"] == "manga"
def test_sanitize_quiz_box_config_ignores_a_non_dict_input(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")
config = screens.set_scene_object_quiz_config(slug, obj_id, "pas-un-dict")
assert config == screens.DEFAULT_QUIZ_BOX_CONFIG
def test_set_scene_object_quiz_config_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="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {
"fullscreen": True, "timer_mode": "question", "timer_seconds": 15,
"dialog_template": "defaut", "page_template": "manga",
})
obj = screens.get_scene_object(slug, obj_id)
config = screens.resolve_quiz_box_config(obj)
assert config == {
"fullscreen": True, "timer_mode": "question", "timer_seconds": 15,
"dialog_template": "defaut", "page_template": "manga",
}
def test_set_scene_object_quiz_config_returns_none_for_unknown_object(tmp_game_slug_cleanup):
slug, _ = _create_jeu2d_game(tmp_game_slug_cleanup)
assert screens.set_scene_object_quiz_config(slug, 999999, {}) is None
# ---------- Route ----------
def test_quiz_config_route_persists_config_and_returns_rendered_html(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="quiz_box")
resp = client.post(
f"/game/{slug}/scene-objects/{obj_id}/quiz-config",
data='{"config": {"fullscreen": false, "timer_mode": "quiz", "timer_seconds": 30, "dialog_template": "defaut"}}',
content_type="application/json",
)
assert resp.status_code == 200
body = resp.get_json()
assert body["config"]["fullscreen"] is False
assert body["config"]["dialog_template"] == "defaut"
assert 'class="quizBoxWidget"' in body["rendered_html"]
obj = screens.get_scene_object(slug, obj_id)
assert screens.resolve_quiz_box_config(obj)["dialog_template"] == "defaut"
def test_quiz_config_route_page_template_only_applies_when_fullscreen(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="quiz_box")
resp = client.post(
f"/game/{slug}/scene-objects/{obj_id}/quiz-config",
data='{"config": {"fullscreen": true, "page_template": "manga"}}',
content_type="application/json",
)
assert resp.status_code == 200
body = resp.get_json()
assert body["config"]["page_template"] == "manga"
assert "quizBoxWidget--manga" in body["rendered_html"]
def test_quiz_config_route_404s_for_missing_object(client, game):
resp = client.post(f"/game/{game}/scene-objects/999999/quiz-config", data='{"config": {}}', content_type="application/json")
assert resp.status_code == 404
# ---------- Rendu — "contrat" de points d'ancrage (voir §3 du plan) :
# chaque modèle doit exposer les mêmes data-quiz-role, quelle que soit sa
# présentation, pour que le moteur JS partagé fonctionne quel que soit le
# modèle choisi ----------
def test_render_quiz_box_defaut_is_unchanged_for_a_non_reconfigured_object(tmp_game_slug_cleanup):
"""Non-régression explicite : un quiz existant (jamais reconfiguré)
doit rester visuellement identique à avant cette extension."""
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 "quizBoxWidget--" not in html
assert 'data-quiz-role="header"' in html
assert 'data-quiz-role="question"' in html
assert 'data-quiz-role="choices"' in html
assert "Quête : Titre de la quête" in html
assert html.count("quizBoxChoiceBtn") == 2
assert "quizBoxFooter" not in html
def test_render_quiz_box_header_and_topbar_share_the_same_header_row(tmp_game_slug_cleanup):
"""Demande explicite : "le score doit être à l'intérieur de la boîte
à quiz, pas à l'extérieur" — le score (topbar) et le titre (header)
doivent être frères DANS le même bandeau ("quizBoxHeaderRow"), jamais
deux bandes empilées de couleurs différentes."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
for template in screens.QUIZ_BOX_DIALOG_TEMPLATES:
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"dialog_template": template})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
row_start = html.index("quizBoxHeaderRow")
row_end = html.index("quizBoxBody") # le bandeau se termine avant le corps
assert row_start < html.index('data-quiz-role="header"', row_start) < row_end, template
assert row_start < html.index('data-quiz-role="topbar"', row_start) < row_end, template
def test_render_quiz_box_every_dialog_template_exposes_the_required_data_quiz_role_anchors(tmp_game_slug_cleanup):
"""Contrat commun (voir §3 du plan) — vérifié ici pour les modèles
"boîte de dialogue" (fullscreen=False, voir le test jumeau pour les
modèles "page de quiz")."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
for template in screens.QUIZ_BOX_DIALOG_TEMPLATES:
screens.set_scene_object_quiz_config(slug, obj_id, {"dialog_template": template})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
for role in ("header", "question", "choices", "topbar", "score", "timer"):
assert f'data-quiz-role="{role}"' in html, f"modèle {template!r} : ancrage {role!r} manquant"
assert f'data-quiz-template="{template}"' in html
assert 'class="quizBoxWidget' in html
def test_render_quiz_box_every_page_template_exposes_the_required_data_quiz_role_anchors(tmp_game_slug_cleanup):
"""Même contrat, pour les modèles "page de quiz" (fullscreen=True) —
"topbar" n'est PAS exigé ici (voir le contrat assoupli dans
screens/rendering/quiz_box_templates.py : un modèle "page" n'a pas
besoin de regrouper score/minuteur à masquer, il n'existe QUE plein
écran)."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True})
for template in screens.QUIZ_BOX_PAGE_TEMPLATES:
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "page_template": template})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
for role in ("header", "question", "choices", "score", "timer"):
assert f'data-quiz-role="{role}"' in html, f"modèle {template!r} : ancrage {role!r} manquant"
assert f'data-quiz-template="{template}"' in html
assert 'class="quizBoxWidget' in html
def test_render_quiz_box_reflects_fullscreen_and_timer_data_attributes(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")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "timer_mode": "question", "timer_seconds": 42})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert 'data-fullscreen="1"' in html
assert 'data-timer-mode="question"' in html
assert 'data-timer-seconds="42"' in html
def test_render_quiz_box_manga_shows_aucun_under_temps_when_no_timer_is_configured(tmp_game_slug_cleanup):
"""Demande explicite : "garde le temps mais si il n'y a pas de chrono
marque en dessous de temps aucun" — la plaque "Temps" reste TOUJOURS
visible (jamais masquée), avec "Aucun" en son absence."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "timer_mode": "aucun", "page_template": "manga"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<span class="mangaPlateValue quizBoxTimerDisplay" data-quiz-role="timer">Aucun</span>' in html
def test_render_quiz_box_manga_leaves_the_timer_value_empty_when_a_timer_is_configured(tmp_game_slug_cleanup):
"""Un vrai minuteur est rempli par le moteur JS (compte à rebours), pas
par le rendu serveur — jamais "Aucun" affiché en même temps qu'un
minuteur réellement configuré."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "timer_mode": "question", "timer_seconds": 20, "page_template": "manga"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<span class="mangaPlateValue quizBoxTimerDisplay" data-quiz-role="timer"></span>' in html
def test_render_quiz_box_manga_has_a_progress_stepper_placeholder(tmp_game_slug_cleanup):
"""Demande explicite : "il n'y a pas le steper" — le HTML doit exposer
un conteneur `data-manga-role="progress"` que le JS (forgeQuizMangaOnShow,
static/js/play/quiz-box-templates.js) remplit au jeu (nombre de questions
inconnu à la création de l'objet)."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "page_template": "manga"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<div class="mangaProgress" data-manga-role="progress"></div>' in html
def test_render_quiz_box_manga_has_an_always_empty_result_zone_placeholder(tmp_game_slug_cleanup):
"""Demande explicite : "il faut aussi adapter le modele manga pour
avoir sa page de résultat" — voir forgeQuizMangaOnQuizComplete,
static/js/play/quiz-box-templates.js, qui remplit/affiche ce
conteneur EN JEU seulement (toujours vide côté rendu serveur)."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "page_template": "manga"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<div class="mangaResultZone" data-manga-role="result"></div>' in html
def test_render_quiz_box_manga_dialogue_applies_its_modifier_class_and_frame(tmp_game_slug_cleanup):
""""Manga" (boîte de dialogue) — demande explicite : "il faut créer un
modele de boite themé mangas qui va avec le mode de page mangas",
validé via une maquette interactive. Reste une petite carte flottante
(fullscreen=False), structure PARTAGÉE avec les autres modèles
"boîte" (contrat déjà vérifié génériquement, voir
test_render_quiz_box_every_dialog_template_exposes_the_required_data_quiz_role_anchors)."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"dialog_template": "manga_dialogue"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert 'class="quizBoxWidget quizBoxWidget--mangaDialogue"' in html
assert '<div class="quizMangaDialogueFrame">' in html
assert 'data-quiz-template="manga_dialogue"' in html
def test_render_quiz_box_manga_dialogue_has_an_always_visible_footer_band(tmp_game_slug_cleanup):
"""Demande explicite : "le modele dois etre une copie conforme a la
maquete au pixel pret" — un bandeau de pied de page sombre reste
TOUJOURS visible (même principe que le modèle "page" "Classique"),
même avant toute réponse (le bouton "Continuer →" du moteur partagé,
lui, n'apparaît qu'après coup, posé par-dessus ce bandeau en CSS)."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"dialog_template": "manga_dialogue"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<div class="quizMangaDialogueFooterBand" aria-hidden="true"></div>' in html
# "Titre de la quête" seul (jamais "Quête : Titre de la quête") — le
# préfixe "Quête : " est posé en CSS (::before), pas dans le HTML,
# sinon il apparaîtrait EN DOUBLE dans l'aperçu éditeur.
assert '>Titre de la quête<' in html
assert '>Quête : Titre de la quête<' not in html
def test_render_quiz_box_manga_dialogue_shows_a_disabled_continue_button_placeholder(tmp_game_slug_cleanup):
"""Demande explicite (répétée : "il est ou le bouton continuer !!!!!",
puis "le bouton continuer est toujours visible mais activer seulement
quand un choix est fait") — le vrai bouton, comme ce repère d'aperçu
éditeur, est maintenant TOUJOURS présent dès la question (posé
désactivé par forgeShowQuizBox, activé par forgeQuizBoxAnswer une
fois répondu — voir static/js/play/dialogue-box-controller.js)."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"dialog_template": "manga_dialogue"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<button type="button" class="quizBoxContinueBtn" disabled>Continuer →</button>' in html
def test_render_quiz_box_manga_dialogue_body_has_no_extra_wrapper_around_the_continue_button(tmp_game_slug_cleanup):
"""Demande explicite : "le contener qui contien les choix a les bord
du bas qui ne sont pas pointue" — le coin dentelé du bas est un
simple `background` (dégradé) sur .quizBoxBody, PAS un clip-path ni
un décor séparé à part (deux essais précédents, tous deux abandonnés :
l'un rognait, l'autre emprisonnait dans un contexte d'empilement, le
bouton "Continuer →" du moteur JS partagé — voir static/style.css).
Le HTML reste donc le plus simple possible : le bouton reste un
DESCENDANT DIRECT de .quizBoxChoices, sans wrapper intermédiaire."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"dialog_template": "manga_dialogue"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert "quizMangaDialogueBodyBg" not in html
assert (
'<div class="quizBoxChoices" data-quiz-role="choices">'
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix B</button>'
'<button type="button" class="quizBoxContinueBtn" disabled>Continuer →</button>'
'</div>'
) in html
def test_render_quiz_box_classique_is_the_default_page_template(tmp_game_slug_cleanup):
""""Classique" — modèle de PAGE DE QUIZ DE BASE, devient le réglage
par défaut (au même titre que "defaut"/Classique côté "boîte de
dialogue") : un créateur qui active juste "Plein écran" sans jamais
toucher aux modèles obtient ce modèle sobre, jamais "manga"."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True})
obj = screens.get_scene_object(slug, obj_id)
config = screens.resolve_quiz_box_config(obj)
assert config["page_template"] == "classique"
html = screens.render_scene_object(obj)
assert 'class="quizBoxWidget quizBoxWidget--classique"' in html
def test_render_quiz_box_classique_uses_the_object_name_field_as_its_title(tmp_game_slug_cleanup):
"""Demande explicite : "aujourd'hui un quiz n'a pas de titre mais il y
a un champs nom qui peut servir de titre pour le modele" — pas de
nouveau champ, le champ "Nom" (déjà existant, "ℹ️ Informations")
devient le titre affiché dans le bandeau de ce modèle."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_name(slug, obj_id, "Formation Sécurité Incendie")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "page_template": "classique"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<span class="classiqueHeaderTitle">Formation Sécurité Incendie</span>' in html
def test_render_quiz_box_classique_falls_back_to_a_generic_title_without_a_name(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")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "page_template": "classique"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<span class="classiqueHeaderTitle">Quiz</span>' in html
def test_render_quiz_box_classique_shows_aucun_under_temps_when_no_timer_is_configured(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")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "page_template": "classique", "timer_mode": "aucun"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<span class="classiqueChipValue quizBoxTimerDisplay" data-quiz-role="timer">Aucun</span>' in html
def test_render_quiz_box_classique_timer_and_score_chips_share_the_exact_same_pill_container(tmp_game_slug_cleanup):
"""Demande explicite (répétée) : "le conteneur de temps + aucun
doivent avoir la background mais pas le mot aucun" — UN SEUL fond
(celui de ".classiqueChip", identique pour "Temps" et "Score", avec
ou sans minuteur configuré) ; la pastille parasite que
".quizBoxTimerDisplay" pose d'ordinaire autour de sa propre valeur
(voir static/style.css) est neutralisée pour ce modèle — un bug de
classe partagée qui collait un second fond/coin arrondi UNIQUEMENT
autour du mot "Aucun", jamais autour de "Score : 0"."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "page_template": "classique", "timer_mode": "aucun"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert 'class="classiqueChip classiqueChip--timer"' in html
assert 'class="classiqueChip classiqueChip--score"' in html
assert 'class="classiqueChipValue quizBoxTimerDisplay"' in html
def test_render_quiz_box_classique_has_an_always_empty_result_zone_placeholder(tmp_game_slug_cleanup):
"""Écran de résultat de fin de quiz (demande explicite : "elle affiche
un conteneur qui reaffiche le contenu du quiz au lieu du resultat" —
fonctionnalité désormais réellement implémentée, voir
forgeQuizClassiqueOnQuizComplete/forgeQuizResultRestart, static/js/
play/quiz-box-templates.js et dialogue-box-controller.js) : le HTML
expose toujours ce conteneur vide, rempli/affiché EN JEU seulement."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "page_template": "classique"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<div class="classiqueResultZone" data-classique-role="result"></div>' in html
def test_render_quiz_box_classique_has_an_always_visible_footer_band(tmp_game_slug_cleanup):
"""Demande explicite : "il manque le footer foncé avec le bouton" —
un bandeau de pied de page (couleur footer_bg du panneau "🎨 Style")
reste TOUJOURS visible, même avant toute réponse (le bouton
"Continuer →" du moteur partagé, lui, n'apparaît qu'après coup, posé
par-dessus ce bandeau en CSS)."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "page_template": "classique"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<div class="classiqueFooterBand" aria-hidden="true"></div>' in html
def test_render_quiz_box_classique_has_a_progress_stepper_placeholder(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")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "page_template": "classique"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<div class="classiqueProgress" data-classique-role="progress"></div>' in html
def test_render_quiz_box_classique_ships_an_always_empty_explain_placeholder(tmp_game_slug_cleanup):
"""Encart d'explication réservé pour une fonctionnalité FUTURE non
encore implémentée (demande explicite : "tu peut le mettre dans le
modele mais ne fonctionnera pas pour le moment") — présent dans le
HTML mais toujours vide aujourd'hui, aucune donnée ne l'alimente."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "page_template": "classique"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert '<div class="classiqueExplain" data-quiz-role="explain"></div>' in html
def test_quiz_box_style_panel_colors_still_apply_regardless_of_template(tmp_game_slug_cleanup):
"""Le panneau "🎨 Style" (couleurs/police) continue de s'appliquer à
TOUS les modèles — un modèle change la disposition/décoration, jamais
une raison d'ignorer les couleurs déjà choisies par le créateur."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_dialogue_box_style(slug, obj_id, {"text_color": "#123456"})
screens.set_scene_object_quiz_config(slug, obj_id, {"dialog_template": "defaut"})
obj = screens.get_scene_object(slug, obj_id)
html = screens.render_scene_object(obj)
assert "#123456" in html
# ---------- Payload ----------
def test_full_game_payload_exposes_quiz_config_for_quiz_box(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")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True, "page_template": "manga"})
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"] == "quiz_box")
assert box["quiz_config"]["fullscreen"] is True
assert box["quiz_config"]["page_template"] == "manga"
def test_full_game_payload_exposes_any_quiz_box_fullscreen_per_screen(tmp_game_slug_cleanup):
"""Bug signalé : "il faut régler la caméra sur plein écran et
désactiver ce réglage quand la boîte à quiz est plein écran" — ce
drapeau permet à templates/play.html de rendre l'ÉCRAN lui-même en
plein viewport (voir .playScene--quizFullscreen), pas seulement le
widget."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
payload = screens.full_game_payload(slug)
scene = next(s for s in payload["screens"] if s["id"] == screen_id)
assert scene["any_quiz_box_fullscreen"] is False
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True})
payload = screens.full_game_payload(slug)
scene = next(s for s in payload["screens"] if s["id"] == screen_id)
assert scene["any_quiz_box_fullscreen"] is True
def test_play_route_renders_the_screen_itself_fullscreen_when_a_quiz_box_is_fullscreen(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="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True})
html = client.get(f"/game/{slug}/play").get_data(as_text=True)
assert "playScene--quizFullscreen" in html
# Le réglage "Caméra (px)" (scene_width/scene_height) ne doit plus du
# tout dimensionner l'écran dans ce cas.
assert "width:960px" not in html
def test_play_route_keeps_the_camera_size_without_a_fullscreen_quiz_box(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
screens.add_scene_object(slug, screen_id, kind="quiz_box") # pas plein écran
html = client.get(f"/game/{slug}/play").get_data(as_text=True)
# La règle CSS elle-même (toujours présente dans <style>) ne compte
# pas — seule la classe posée sur LA DIV de cet écran importe ici.
assert f'data-screen-id="{screen_id}"' in html
assert 'class="playScreen playScene "' in html
assert "width:960px; height:540px;" in html
# ---------- Éditeur — panneau propriétés (rendu, pas de logique JS ici) ----------
def test_scene_edit_view_renders_dialog_template_gallery_when_not_fullscreen(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="quiz_box")
html = client.get(f"/game/{slug}/screens/{screen_id}/edit?selected={obj_id}").get_data(as_text=True)
assert 'id="quiz-fullscreen"' in html
assert 'id="quiz-timer-mode"' in html
assert 'id="quiz-timer-seconds"' in html
assert 'id="quizDialogTemplateGrid"' in html
assert 'id="quizPageTemplateGrid"' not in html
for template in screens.QUIZ_BOX_DIALOG_TEMPLATES:
assert f"selectQuizBoxDialogTemplate({obj_id}, '{template}')" in html
def test_scene_edit_view_renders_page_template_gallery_when_fullscreen(client, tmp_game_slug_cleanup):
"""Demande explicite : les modèles "page de quiz" ne se voient QUE
quand la boîte à quiz est plein écran — jamais les deux galeries en
même temps."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True})
html = client.get(f"/game/{slug}/screens/{screen_id}/edit?selected={obj_id}").get_data(as_text=True)
assert 'id="quizPageTemplateGrid"' in html
assert 'id="quizDialogTemplateGrid"' not in html
for template in screens.QUIZ_BOX_PAGE_TEMPLATES:
assert f"selectQuizBoxPageTemplate({obj_id}, '{template}')" in html
def test_scene_edit_view_does_not_render_quiz_box_options_for_a_dialogue_box(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")
html = client.get(f"/game/{slug}/screens/{screen_id}/edit?selected={obj_id}").get_data(as_text=True)
assert 'id="quiz-fullscreen"' not in html
assert 'id="quizDialogTemplateGrid"' not in html
assert 'id="quizPageTemplateGrid"' not in html
# ---------- "Plein écran" dans L'ÉDITEUR (demande explicite : "elle doit
# occuper tout l'écran", "être visible par défaut", "la notion de caméra
# disparaît" — pas seulement au jeu) ----------
def test_scene_edit_view_marks_a_fullscreen_quiz_box_for_the_editor_preview(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="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True})
html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
assert f'id="el-{obj_id}"' in html
# La classe qui fait occuper tout le canevas (voir static/style.css
# .canvasElement.isQuizFullscreenPreview) doit être posée sur le
# conteneur de CET objet précis.
import re
m = re.search(r'<div class="canvasElement[^"]*"\s+id="el-' + str(obj_id) + '"', html)
assert m and "isQuizFullscreenPreview" in m.group(0)
def test_scene_edit_view_canvas_itself_fills_the_frame_when_a_quiz_box_is_fullscreen(client, tmp_game_slug_cleanup):
"""Bug signalé : "il ne devait plus y avoir de notion de caméra" —
masquer seulement le cadre "🎥 Champ de la caméra" ne suffisait pas,
le CANEVAS restait lui-même dimensionné en pixels fixes
(world_width/world_height, une notion de caméra/scène nominale),
laissant une bande vide visible autour de la boîte plein écran."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, obj_id, {"fullscreen": True})
html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
import re
m = re.search(r'<div class="canvas" id="canvas" style="([^"]*)"', html)
assert m
style = m.group(1)
assert "width:100%; height:100%;" in style
assert "px" not in style.split(";")[0] and "px" not in style.split(";")[1]
def test_scene_edit_view_canvas_keeps_pixel_size_without_a_fullscreen_quiz_box(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
screens.add_scene_object(slug, screen_id, kind="quiz_box") # pas plein écran
html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
import re
m = re.search(r'<div class="canvas" id="canvas" style="([^"]*)"', html)
assert m
assert "width:960px; height:540px;" in m.group(1)
def test_scene_edit_view_hides_the_camera_frame_when_a_quiz_box_is_fullscreen(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
# Un fond plus grand que la scène nominale, pour que "🎥 Champ de la
# caméra" soit normalement affiché (voir resolve_scene_world_size).
fond_id = screens.add_scene_object(slug, screen_id, kind="fond")
screens.update_scene_object_geometry(slug, fond_id, x=0, y=0, width=2000, height=2000)
html_before = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
assert "sceneCameraFrame" in html_before
quiz_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
screens.set_scene_object_quiz_config(slug, quiz_id, {"fullscreen": True})
html_after = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
assert "sceneCameraFrame" not in html_after
def test_scene_edit_view_keeps_the_camera_frame_when_no_quiz_box_is_fullscreen(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
fond_id = screens.add_scene_object(slug, screen_id, kind="fond")
screens.update_scene_object_geometry(slug, fond_id, x=0, y=0, width=2000, height=2000)
screens.add_scene_object(slug, screen_id, kind="quiz_box") # pas plein écran
html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
assert "sceneCameraFrame" in html