"""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 from typing import Any from conftest import play_js_bundle # type: ignore[import-not-found] # resolu par pytest, pas mypy from flask.testing import FlaskClient import screens as screens_module from tests.conftest import not_none def _create_screen(client: FlaskClient, slug: str, name: str = "Accueil") -> Any: resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False) return int(not_none(re.search(r"/screens/(\d+)/edit", resp.headers["Location"])).group(1)) def _add_element(client: FlaskClient, slug: str, screen_id: int, widget: str | None = None) -> Any: return screens_module.add_scene_object(slug, screen_id) def test_animate_css_clip_persists(client: FlaskClient, game: str) -> None: 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: FlaskClient, game: str) -> None: 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: FlaskClient, game: str) -> None: 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: FlaskClient, game: str) -> None: 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: FlaskClient, game: str) -> None: """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: FlaskClient, game: str) -> None: 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: FlaskClient, game: str) -> None: 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