Le document de cadrage produit cible des formateurs non techniques créant des serious games/quiz gamifiés — l'éditeur générique "document" (blocs de logique en nœuds, timeline d'animation, définitions d'objets/relations, templates réutilisables) est une complexité hors cible que l'effort d'ingénierie récent avait déjà abandonnée au profit du jeu_2d. - Onboarding : ne garde que le parcours "RPG" (jeu_2d), retire Quiz/Embranchement/Créer mon jeu de A à Z (tous document-only) - Suppression en bloc des modules exclusifs au document : routes/elements, routes/element_types, routes/objects, routes/legacy_actions, screens/elements, screens/element_types, screens/widgets, screens/legacy_actions, le rendu render_element_html.py et son cluster, templates/screen_edit.html, templates/game_dashboard.html, flow-editor.js/tabs-and-blocks.js/animation-timeline.js - Dashboard toujours simplifié (un seul mode possible désormais) - Tests document-only supprimés, tests de logique partagée (flow, événements personnalisés, animations) retargetés sur des écrans jeu_2d - Aucune régression jeu_2d : 299 tests passent Carte d'onboarding retravaillée : argumentaire RH non technique (liste à coche, badge "Compatible LMS"), taille et interaction de retournement ajustées.
123 lines
4.9 KiB
Python
123 lines
4.9 KiB
Python
"""Tests de la timeline d'animation : clips Animate.css ou personnalisés
|
|
(images-clés), posés sur la piste d'un élément, avec un instant de départ
|
|
et une durée sur l'axe du temps (secondes depuis l'affichage de l'écran)."""
|
|
import json
|
|
import re
|
|
|
|
import screens as screens_module
|
|
from conftest import play_js_bundle
|
|
|
|
|
|
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=None):
|
|
return screens_module.add_scene_object(slug, screen_id)
|
|
|
|
|
|
def test_animate_css_clip_persists(client, game):
|
|
screen_id = _create_screen(client, game)
|
|
el_id = _add_element(client, game, screen_id)
|
|
resp = client.post(
|
|
f"/game/{game}/screens/{screen_id}/animations/clips/add",
|
|
data=json.dumps({
|
|
"element_id": el_id, "kind": "animate_css", "animate_name": "bounce",
|
|
"start_time": "1.5", "duration": "2", "delay": "0", "easing": "ease", "iteration_count": "1",
|
|
}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
clip = resp.get_json()
|
|
assert clip["kind"] == "animate_css"
|
|
assert clip["animate_name"] == "bounce"
|
|
assert clip["start_time"] == 1.5
|
|
assert clip["duration"] == 2.0
|
|
assert clip["element_id"] == el_id
|
|
|
|
|
|
def test_custom_keyframes_clip_persists(client, game):
|
|
screen_id = _create_screen(client, game)
|
|
el_id = _add_element(client, game, screen_id)
|
|
keyframes = [{"percent": 0, "props": {"opacity": "0"}}, {"percent": 100, "props": {"opacity": "1"}}]
|
|
resp = client.post(
|
|
f"/game/{game}/screens/{screen_id}/animations/clips/add",
|
|
data=json.dumps({
|
|
"element_id": el_id, "kind": "custom", "custom_keyframes": keyframes,
|
|
"start_time": "0", "duration": "3",
|
|
}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
clip = resp.get_json()
|
|
assert clip["kind"] == "custom"
|
|
assert json.loads(clip["custom_keyframes"]) == keyframes
|
|
|
|
|
|
def test_clip_update_changes_timing(client, game):
|
|
screen_id = _create_screen(client, game)
|
|
el_id = _add_element(client, game, screen_id)
|
|
clip = client.post(
|
|
f"/game/{game}/screens/{screen_id}/animations/clips/add",
|
|
data=json.dumps({"element_id": el_id, "kind": "animate_css", "animate_name": "fadeIn"}),
|
|
content_type="application/json",
|
|
).get_json()
|
|
resp = client.post(
|
|
f"/game/{game}/animations/clips/{clip['id']}/update",
|
|
data=json.dumps({"start_time": "4", "duration": "0.5"}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
updated = resp.get_json()
|
|
assert updated["start_time"] == 4.0
|
|
assert updated["duration"] == 0.5
|
|
|
|
|
|
def test_clip_delete_removes_it(client, game):
|
|
screen_id = _create_screen(client, game)
|
|
el_id = _add_element(client, game, screen_id)
|
|
clip = client.post(
|
|
f"/game/{game}/screens/{screen_id}/animations/clips/add",
|
|
data=json.dumps({"element_id": el_id, "kind": "animate_css", "animate_name": "tada"}),
|
|
content_type="application/json",
|
|
).get_json()
|
|
resp = client.post(f"/game/{game}/animations/clips/{clip['id']}/delete")
|
|
assert resp.status_code == 200
|
|
import screens
|
|
assert screens.get_animation_clip(game, clip["id"]) is None
|
|
|
|
|
|
def test_scene_editor_loads_animate_css(client, game):
|
|
"""L'onglet dédié "Timeline d'animation" (animTabPanel) n'existe que
|
|
dans l'éditeur document, retiré — l'éditeur de scène 2D n'a pas encore
|
|
d'UI dédiée pour poser un clip (voir templates/scene_edit.html), mais
|
|
charge quand même animate.min.css (consommé par le runtime jouable,
|
|
voir test_play_page_exposes_animation_runtime_and_animate_css) et
|
|
l'API de clips ci-dessus reste pleinement fonctionnelle."""
|
|
screen_id = _create_screen(client, game)
|
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
|
assert "animate.min.css" in html
|
|
|
|
|
|
def test_play_page_exposes_animation_runtime_and_animate_css(client, game):
|
|
resp = client.get(f"/game/{game}/play")
|
|
html = resp.data.decode()
|
|
assert "animate.min.css" in html
|
|
js = play_js_bundle()
|
|
assert "runAnimationTimeline" in js
|
|
assert "injectCustomKeyframes" in js
|
|
|
|
|
|
def test_play_page_payload_includes_clip(client, game):
|
|
screen_id = _create_screen(client, game)
|
|
el_id = _add_element(client, game, screen_id, "titre")
|
|
client.post(
|
|
f"/game/{game}/screens/{screen_id}/animations/clips/add",
|
|
data=json.dumps({"element_id": el_id, "kind": "animate_css", "animate_name": "pulse", "start_time": "2"}),
|
|
content_type="application/json",
|
|
)
|
|
html = client.get(f"/game/{game}/play").data.decode()
|
|
assert '"animate_name": "pulse"' in html
|
|
assert '"start_time": 2.0' in html
|