Implemente le mini-jeu Scenario (situation, choix, consequences)
Build and deploy / test-python (push) Failing after 59s
Build and deploy / test-js (push) Successful in 50s
Build and deploy / lint-python (push) Failing after 59s
Build and deploy / lint-js (push) Failing after 1m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 59s
Build and deploy / test-python (push) Failing after 59s
Build and deploy / test-js (push) Successful in 50s
Build and deploy / lint-python (push) Failing after 59s
Build and deploy / lint-js (push) Failing after 1m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 59s
Cinquieme mini-jeu du support de formation : le createur ecrit une situation initiale, definit 2 a 4 choix, indique lequel est le bon, et redige la consequence de chaque choix. Plusieurs scenarios peuvent etre crees, joues dans l'ordre d'ecriture (jamais melanges, contrairement a Association/Memory/Mots meles : ce sont des mises en situation sequentielles). En Apercu, l'apprenant lit la situation, choisit une option, decouvre la consequence de SON choix et si c'etait le bon, puis passe au scenario suivant. Comme Association/Memory/Mots meles, le dernier scenario reste affiche une fois repondu : seul le bouton Recommencer apparait, jamais un ecran de resultat separe (reserve au Quiz). Reutilise les classes visuelles du Quiz (docQuizOptions/ docQuizFeedback/docQuizNextBar) plutot que de dupliquer ces regles. Verifie via simulation DOM reelle (jsdom) : progression entre plusieurs scenarios, choix correct/incorrect avec revelation de la bonne reponse, comportement de fin de partie, panneau Proprietes (ajout/suppression de scenario, changement du nombre de choix, selection du bon choix). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d8be80ebd1
commit
b2292de122
@@ -0,0 +1,147 @@
|
||||
"""Modèle de données du mini-jeu Scénario
|
||||
(document_engine/labels/scenario_config.py) — sans Flask, teste
|
||||
directement sanitize_scenario_config, et le rendu du plateau."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import document_engine as doc_engine
|
||||
|
||||
|
||||
def test_sanitize_scenario_config_on_missing_input_returns_full_defaults() -> None:
|
||||
assert doc_engine.sanitize_scenario_config(None) == {"theme_color": "#ff5f2e", "scenarios": []}
|
||||
|
||||
|
||||
def test_sanitize_scenario_config_keeps_a_valid_scenario_as_is() -> None:
|
||||
raw = {
|
||||
"theme_color": "#123456",
|
||||
"scenarios": [
|
||||
{
|
||||
"situation": "Un collègue vous demande son mot de passe.",
|
||||
"choices": [
|
||||
{"text": "Le lui donner", "consequence": "Violation de sécurité."},
|
||||
{"text": "Refuser et l'orienter vers l'IT", "consequence": "Bonne pratique respectée."},
|
||||
],
|
||||
"correct_index": 1,
|
||||
}
|
||||
],
|
||||
}
|
||||
config = doc_engine.sanitize_scenario_config(raw)
|
||||
assert config["theme_color"] == "#123456"
|
||||
assert config["scenarios"] == [
|
||||
{
|
||||
"situation": "Un collègue vous demande son mot de passe.",
|
||||
"choices": [
|
||||
{"text": "Le lui donner", "consequence": "Violation de sécurité."},
|
||||
{"text": "Refuser et l'orienter vers l'IT", "consequence": "Bonne pratique respectée."},
|
||||
],
|
||||
"correct_index": 1,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_sanitize_scenario_config_allows_an_empty_consequence() -> None:
|
||||
raw = {"scenarios": [{"situation": "Situation", "choices": [{"text": "A"}, {"text": "B"}]}]}
|
||||
config = doc_engine.sanitize_scenario_config(raw)
|
||||
assert config["scenarios"][0]["choices"] == [{"text": "A", "consequence": ""}, {"text": "B", "consequence": ""}]
|
||||
|
||||
|
||||
def test_sanitize_scenario_config_drops_a_scenario_with_empty_situation() -> None:
|
||||
raw = {"scenarios": [{"situation": " ", "choices": [{"text": "A"}, {"text": "B"}]}]}
|
||||
assert doc_engine.sanitize_scenario_config(raw)["scenarios"] == []
|
||||
|
||||
|
||||
def test_sanitize_scenario_config_drops_a_scenario_with_fewer_than_two_choices() -> None:
|
||||
raw = {"scenarios": [{"situation": "Situation", "choices": [{"text": "Seul choix"}]}]}
|
||||
assert doc_engine.sanitize_scenario_config(raw)["scenarios"] == []
|
||||
|
||||
|
||||
def test_sanitize_scenario_config_drops_blank_choices_before_counting() -> None:
|
||||
raw = {"scenarios": [{"situation": "Situation", "choices": [{"text": "A"}, {"text": " "}, {"text": ""}]}]}
|
||||
assert doc_engine.sanitize_scenario_config(raw)["scenarios"] == []
|
||||
|
||||
|
||||
def test_sanitize_scenario_config_caps_choices_at_max() -> None:
|
||||
raw = {
|
||||
"scenarios": [
|
||||
{
|
||||
"situation": "Situation",
|
||||
"choices": [{"text": f"Choix {i}"} for i in range(doc_engine.MAX_SCENARIO_CHOICES + 3)],
|
||||
}
|
||||
]
|
||||
}
|
||||
config = doc_engine.sanitize_scenario_config(raw)
|
||||
assert len(config["scenarios"][0]["choices"]) == doc_engine.MAX_SCENARIO_CHOICES
|
||||
|
||||
|
||||
def test_sanitize_scenario_config_resets_out_of_range_correct_index() -> None:
|
||||
raw = {"scenarios": [{"situation": "Situation", "choices": [{"text": "A"}, {"text": "B"}], "correct_index": 7}]}
|
||||
config = doc_engine.sanitize_scenario_config(raw)
|
||||
assert config["scenarios"][0]["correct_index"] == 0
|
||||
|
||||
|
||||
def test_sanitize_scenario_config_rejects_a_boolean_as_correct_index() -> None:
|
||||
"""True/False sont des int en Python (bool hérite de int) — même
|
||||
garde-fou que sanitize_quiz_config::correct_index."""
|
||||
raw = {"scenarios": [{"situation": "Situation", "choices": [{"text": "A"}, {"text": "B"}], "correct_index": True}]}
|
||||
config = doc_engine.sanitize_scenario_config(raw)
|
||||
assert config["scenarios"][0]["correct_index"] == 0
|
||||
|
||||
|
||||
def test_sanitize_scenario_config_keeps_multiple_scenarios_in_order() -> None:
|
||||
raw = {
|
||||
"scenarios": [
|
||||
{"situation": "Situation 1", "choices": [{"text": "A"}, {"text": "B"}]},
|
||||
{"situation": "Situation 2", "choices": [{"text": "C"}, {"text": "D"}]},
|
||||
]
|
||||
}
|
||||
config = doc_engine.sanitize_scenario_config(raw)
|
||||
assert [s["situation"] for s in config["scenarios"]] == ["Situation 1", "Situation 2"]
|
||||
|
||||
|
||||
def test_sanitize_scenario_config_ignores_garbage_top_level_input() -> None:
|
||||
assert doc_engine.sanitize_scenario_config("n'importe quoi") == doc_engine.DEFAULT_SCENARIO_CONFIG
|
||||
|
||||
|
||||
def _scenario_element(attributes: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"id": 1, "kind": "scenario", "parent_id": None, "order_index": 0, "attributes": attributes}
|
||||
|
||||
|
||||
def test_render_scenario_without_scenarios_shows_only_the_badge() -> None:
|
||||
html = doc_engine.render_document_element(_scenario_element(doc_engine.DEFAULT_SCENARIO_CONFIG), {})
|
||||
assert "0 scénario" in html
|
||||
assert "docScenarioPlayer" not in html
|
||||
|
||||
|
||||
def test_render_scenario_with_scenarios_includes_the_interactive_player() -> None:
|
||||
config = doc_engine.sanitize_scenario_config(
|
||||
{
|
||||
"scenarios": [
|
||||
{
|
||||
"situation": "Un collègue vous demande son mot de passe.",
|
||||
"choices": [{"text": "Refuser"}, {"text": "Accepter"}],
|
||||
"correct_index": 0,
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
html = doc_engine.render_document_element(_scenario_element(config), {})
|
||||
assert "docScenarioPlayer" in html
|
||||
assert "docQuizOptions" in html
|
||||
assert "data-scenario-config=" in html
|
||||
assert "1 scénario" in html
|
||||
|
||||
|
||||
def test_render_scenario_escapes_situation_text_in_embedded_json() -> None:
|
||||
config = doc_engine.sanitize_scenario_config(
|
||||
{
|
||||
"scenarios": [
|
||||
{
|
||||
"situation": '"><script>alert(1)</script>',
|
||||
"choices": [{"text": "A"}, {"text": "B"}],
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
html = doc_engine.render_document_element(_scenario_element(config), {})
|
||||
assert "<script>alert(1)</script>" not in html
|
||||
assert "<script>" in html
|
||||
Reference in New Issue
Block a user