Remplace l'ancien panneau du bas rétractable/redimensionnable à la souris (Logique de la scène, Timeline d'animation) par un système d'onglets au centre de l'éditeur - Écran / Logique de la scène / Timeline d'animation - un seul visible à la fois, occupant systématiquement tout l'espace disponible (switchBuilderTab() dans screen_edit.html). Changer d'onglet équivaut à "fermer" celui qu'on quitte ; plus besoin d'une poignée de redimensionnement séparée puisque l'onglet actif prend déjà toute la place. Supprime au passage tout l'ancien mécanisme (toggleFlowPanel/ toggleAnimPanel, poignées flowResizeHandle/animResizeHandle, classe CSS .flowPanel) devenu inutile. Déplace aussi le renommage de l'écran, le bouton "Jouer depuis le début" et le choix du format d'aperçu (Portrait/Paysage/Carré) - jusqu'ici au-dessus du canevas - dans le panneau flottant "Éléments" (celui qui porte déjà ce nom, à gauche) : des réglages qu'on touche rarement une fois l'écran en construction, qui n'ont plus besoin de rester en permanence visibles au-dessus de la zone de travail. Corrige au passage un bug découvert pendant ce tour : sur la page "Nouvel objet" (2 colonnes), le bouton "+ Ajouter un champ" avait disparu - placé APRÈS la zone de liste à défilement (flex-grow:1) dans la colonne de droite, un flex-grow imprévisible dans ce contexte le poussait hors de vue. Déplacé avant la liste (statique, toujours visible), pattern déjà éprouvé ailleurs sur cette même page. Deux tests mis à jour pour refléter intentionnellement la nouvelle structure : la présence de "animTabPanel" (au lieu de l'ancien "animPanel"), et un marqueur plus précis pour distinguer le bloc de propriétés "Onglets" d'une simple occurrence du même texte dans une liste déroulante de la Logique de la scène (qui apparaît désormais plus tôt dans le document, cet éditeur de flow étant maintenant un onglet du centre plutôt qu'un panneau tout en bas de page). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
116 lines
4.6 KiB
Python
116 lines
4.6 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
|
|
|
|
|
|
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))
|
|
|
|
|
|
def test_animate_css_clip_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}/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, "conteneur")
|
|
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, "bouton")
|
|
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, "bouton")
|
|
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
|
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
|
assert f'"id": {clip["id"]}' not in html
|
|
|
|
|
|
def test_screen_edit_exposes_timeline_panel_and_animate_css_cdn(client, game):
|
|
screen_id = _create_screen(client, game)
|
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
|
assert "animTabPanel" in html
|
|
assert "Timeline d'animation" in html
|
|
assert "animate.css" in html
|
|
|
|
|
|
def test_play_page_exposes_animation_runtime_and_cdn(client, game):
|
|
resp = client.get(f"/game/{game}/play")
|
|
html = resp.data.decode()
|
|
assert "animate.css" in html
|
|
assert "runAnimationTimeline" in html
|
|
assert "injectCustomKeyframes" in html
|
|
|
|
|
|
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
|