Files
Forge-Engine/tests/test_scene_edit_view.py
T
williamandClaude Sonnet 5 14ec76dddd
Build and deploy / test-python (push) Failing after 1m20s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Éditeur : le canevas affiche tout le monde (fond agrandi), pas que la scène
Suite au fix du glisser figé à (0,0) : le canevas de l'éditeur restait
malgré tout limité à la scène nominale (960×540 par ex.) — la partie
d'un "fond" plus grand qui dépasse restait invisible/injoignable à la
souris. Le canevas affiche désormais tout le "monde" (max de la scène
et de l'étendue de chaque "fond", voir routes/scenes/scene_edit_view.py),
comme la caméra en jeu le fait déjà (personnage-controller.js).

Nouveau repère visuel "🎥 Champ de la caméra" : un cadre en pointillés
marquant où s'arrête la scène nominale à l'intérieur de ce monde agrandi
(le reste est assombri), pour que l'auteur voie clairement ce que le
joueur verra réellement à l'écran une fois le monde plus grand que le
viewport.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 16:55:09 +02:00

242 lines
12 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.
"""Vue de l'éditeur de scène 2D (routes/scenes/scene_edit_view.py) —
appelée par routes/screens/screen_edit.py selon screen["kind"] (fusion des
moteurs, voir screens/screens_repo/ensure_schema.py), RÉUTILISE tel quel
le moteur de logique de flow partagé (flow-editor.js/tabs-and-blocks.js/
animation-timeline.js)."""
import json
import re
import db
import screens
from conftest import screen_edit_js_bundle
def _create_jeu2d_game(client, tmp_game_slug_cleanup, name="pytest_scene_edit"):
resp = client.post("/games/new", data={"name": name}, follow_redirects=False)
slug = tmp_game_slug_cleanup(resp.headers["Location"].rstrip("/").split("/")[-1])
screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
return slug, screen_id
def test_document_game_still_uses_generic_editor(client, game):
"""Non-régression — critère de succès n°1 du plan : un jeu "document"
existant continue de fonctionner à l'identique."""
resp = client.post(f"/game/{game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
screen_id = int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
assert "ÉLÉMENTS DE CET ÉCRAN" in html.upper() or "Éléments de cet écran" in html
assert "scene-editor.js" not in html
def test_jeu2d_game_dispatches_to_scene_editor(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
assert "Objets de cette scène" in html
assert "scene-editor.js" in html
assert "🎭 Personnages" in html
def test_scene_edit_view_renders_with_a_personnage_object_selected(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
resp = client.post(
f"/game/{slug}/screens/{screen_id}/scene-objects/add",
data={"kind": "personnage", "forge_character": "zombie"},
follow_redirects=False,
)
assert resp.status_code == 302
obj_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
html = client.get(f"/game/{slug}/screens/{screen_id}/edit?selected={obj_id}").get_data(as_text=True)
assert "/static/characters/zombie/idle.png" in html
assert f'data-object-id="{obj_id}"' in html
assert "Personnage Forge" in html
def test_scene_object_geometry_route_persists_pixels(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id)
resp = client.post(
f"/game/{slug}/scene-objects/{obj_id}/geometry",
data=json.dumps({"x": 111, "y": 222, "width": 48, "height": 96}),
content_type="application/json",
)
assert resp.status_code == 200
obj = screens.get_scene_object(slug, obj_id)
assert (obj["x"], obj["y"], obj["width"], obj["height"]) == (111, 222, 48, 96)
def test_scene_object_delete_route(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id)
resp = client.post(f"/game/{slug}/scene-objects/{obj_id}/delete", follow_redirects=False)
assert resp.status_code == 302
assert screens.get_scene_object(slug, obj_id) is None
def test_flow_node_targets_scene_object(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id, forge_character="zombie")
resp = client.post(
f"/game/{slug}/screens/{screen_id}/flow/nodes/add",
data=json.dumps({
"node_type": "action", "action_type": "jouer_animation_sprite",
"target_object_id": obj_id, "data_value": json.dumps({"animation": "walk", "fps": 8, "loop": True}),
}),
content_type="application/json",
)
assert resp.status_code == 200
node_id = resp.get_json()["id"]
node = screens.get_flow_node(slug, node_id)
assert node["target_object_id"] == obj_id
assert node["target_element_id"] is None
def test_held_key_trigger_persists_and_is_in_scene_palette(client, tmp_game_slug_cleanup):
"""Mouvement continu (touche maintenue) — répond au manque signalé par
l'utilisateur : "clavier" (keydown) ne se déclenche qu'une fois par
appui, insuffisant pour faire avancer un personnage en continu tant
qu'une touche reste enfoncée. Voir runScreenHeldKeyTriggers()
(static/js/play/triggers.js) et TRIGGER_EVENTS (screens/flow/constants.py)."""
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
assert "touche_maintenue" in dict(screens.TRIGGER_EVENTS_2D)
assert "touche_relachee" in dict(screens.TRIGGER_EVENTS_2D)
resp = client.post(
f"/game/{slug}/screens/{screen_id}/flow/nodes/add",
data=json.dumps({"node_type": "trigger", "trigger_event": "touche_maintenue", "trigger_key": "ArrowRight"}),
content_type="application/json",
)
assert resp.status_code == 200
node = screens.get_flow_node(slug, resp.get_json()["id"])
assert node["trigger_event"] == "touche_maintenue"
assert node["trigger_key"] == "ArrowRight"
def test_flow_node_collision_condition_targets_scene_objects(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_a = screens.add_scene_object(slug, screen_id)
obj_b = screens.add_scene_object(slug, screen_id, kind="decor")
resp = client.post(
f"/game/{slug}/screens/{screen_id}/flow/nodes/add",
data=json.dumps({
"node_type": "condition", "cond_source": "collision",
"cond_element_a": obj_a, "cond_element_b": obj_b,
}),
content_type="application/json",
)
assert resp.status_code == 200
node = screens.get_flow_node(slug, resp.get_json()["id"])
assert node["cond_element_a"] == obj_a
assert node["cond_element_b"] == obj_b
def test_play_payload_renders_scene_objects_for_jeu2d_game(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
screens.add_scene_object(slug, screen_id, forge_character="robot")
payload = screens.full_game_payload(slug)
scene_payload = next(s for s in payload["screens"] if s["id"] == screen_id)
assert len(scene_payload["elements"]) == 1
assert "/static/characters/robot/idle.png" in scene_payload["elements"][0]["rendered_html"]
assert any(payload["personnage_animations"].values())
def test_play_html_renders_fixed_size_scene(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
screens.add_scene_object(slug, screen_id, forge_character="zombie")
html = client.get(f"/game/{slug}/play").get_data(as_text=True)
assert "playScene" in html
assert "/static/characters/zombie/idle.png" in html
assert f"width:{db.game_meta(slug)['name'] and 960}px" in html or "width:960px" in html
def test_render_scene_object_carries_data_anim_target(client, tmp_game_slug_cleanup):
"""Bug remonté par l'utilisateur : "les animations ne se jouent pas
dans l'aperçu" — applyAnimationClip() (static/js/play/screens.js) et
animation-timeline.js sélectionnent TOUJOURS leur cible via
[data-anim-target=...], jamais data-element-id/data-object-id. Sans cet
attribut sur l'objet de scène, AUCUN clip de Timeline (sprite, Animate.css
ou personnalisé) ne trouvait jamais sa cible."""
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id)
html = screens.render_scene_object(screens.get_scene_object(slug, obj_id))
assert f'data-anim-target="{obj_id}"' in html
def test_screen_edit_js_bundle_contains_scene_flow_helpers():
js = screen_edit_js_bundle()
assert "FLOW_TARGETS_OBJECTS" in js
assert "toggleFlowObjectPropertyValue" in js
def test_animation_clip_add_route_accepts_a_scene_object(client, tmp_game_slug_cleanup):
"""Bug remonté par l'utilisateur : _animation_clips.element_id portait
une VRAIE contrainte FK vers _screen_elements — poser un clip de
Timeline sur un objet de scène (_scene_objects, un id space distinct)
faisait échouer l'insertion avec "FOREIGN KEY constraint failed"."""
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id)
resp = client.post(
f"/game/{slug}/screens/{screen_id}/animations/clips/add",
data=json.dumps({"element_id": obj_id, "kind": "animate_css", "animate_name": "fadeIn"}),
content_type="application/json",
)
assert resp.status_code == 200
clip = resp.get_json()
assert clip["element_id"] == obj_id
def test_deleting_scene_object_cleans_up_its_animation_clips(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
obj_id = screens.add_scene_object(slug, screen_id)
screens.add_animation_clip(slug, screen_id, obj_id, kind="animate_css", animate_name="fadeIn")
screens.delete_scene_object(slug, obj_id)
assert screens.list_animation_clips(slug, screen_id) == []
def test_deleting_element_cleans_up_its_animation_clips(client, game):
resp = client.post(f"/game/{game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
screen_id = int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
el_resp = client.post(
f"/game/{game}/screens/{screen_id}/elements/add",
data={"widget": "texte"},
follow_redirects=False,
)
element_id = int(re.search(r"selected=(\d+)", el_resp.headers["Location"]).group(1))
screens.add_animation_clip(game, screen_id, element_id, kind="animate_css", animate_name="fadeIn")
screens.delete_element(game, element_id)
assert screens.list_animation_clips(game, screen_id) == []
def test_scene_edit_view_world_matches_scene_when_no_oversized_fond(client, tmp_game_slug_cleanup):
"""Pas de "fond" plus grand que la scène -> le monde affiché reste
exactement la scène nominale, et le repère "🎥 Champ de la caméra"
(voir static/style.css .sceneCameraFrame) ne s'affiche pas — inutile
tant que rien ne dépasse."""
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
assert 'id="canvas" style="width:960px; height:540px;' in html
assert "sceneCameraFrame" not in html
def test_scene_edit_view_expands_world_for_an_oversized_fond(client, tmp_game_slug_cleanup):
"""Bug corrigé (glisser-déposer figé à (0,0) pour un objet plus grand
que la scène, voir static/js/scenes/scene-editor.js) : le canevas de
l'éditeur doit maintenant afficher tout le "fond", pas seulement la
scène nominale (960×540) qu'il dépasse — sinon la partie qui dépasse
resterait invisible/injoignable à la souris malgré le fix du glisser."""
slug, screen_id = _create_jeu2d_game(client, 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=1920, height=1080)
html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
assert 'id="canvas" style="width:1920px; height:1080px;' in html
# Le repère de la caméra marque toujours les bornes de la scène NOMINALE (960×540),
# même si le canevas affiché est désormais bien plus grand.
assert 'class="sceneCameraFrame" style="width:960px; height:540px;' in html
def test_scene_edit_view_world_bounds_are_used_for_drag_clamping(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(client, 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=1920, height=1080)
html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
assert "var SCENE_WIDTH = 1920;" in html
assert "var SCENE_HEIGHT = 1080;" in html