Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y
Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
+101
-47
@@ -1,21 +1,31 @@
|
||||
"""Vue de l'éditeur de scène 2D (routes/scenes/scene_edit_view.py) —
|
||||
appelée par routes/screens/screen_edit.py, l'unique moteur d'écran
|
||||
(jeu_2d)."""
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from flask.testing import FlaskClient
|
||||
|
||||
import db
|
||||
import screens
|
||||
from tests.conftest import not_none
|
||||
|
||||
|
||||
def _create_jeu2d_game(client, tmp_game_slug_cleanup, name="pytest_scene_edit"):
|
||||
def _create_jeu2d_game(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str], name: str = "pytest_scene_edit"
|
||||
) -> Any:
|
||||
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_jeu2d_game_dispatches_to_scene_editor(client, tmp_game_slug_cleanup):
|
||||
def test_jeu2d_game_dispatches_to_scene_editor(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
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
|
||||
@@ -23,7 +33,9 @@ def test_jeu2d_game_dispatches_to_scene_editor(client, tmp_game_slug_cleanup):
|
||||
assert "Personnages" in html
|
||||
|
||||
|
||||
def test_scene_edit_view_splits_my_assets_into_fond_decor_sons_videos_subsections(client, tmp_game_slug_cleanup):
|
||||
def test_scene_edit_view_splits_my_assets_into_fond_decor_sons_videos_subsections(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
"""Demande explicite : "des sous-menu pour voir, utiliser ou
|
||||
supprimer des son, image et video", PUIS "un bouton d'import séparé
|
||||
pour les images de fond et les objets" — "Mes assets" doit séparer
|
||||
@@ -32,23 +44,26 @@ def test_scene_edit_view_splits_my_assets_into_fond_decor_sons_videos_subsection
|
||||
suppression ; le sélecteur de fichier des actions "son"/"vidéo"
|
||||
(USER_ASSETS_OPTIONS), lui, voit TOUJOURS tout, peu importe le type."""
|
||||
import auth
|
||||
|
||||
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
||||
with client.session_transaction() as sess:
|
||||
user_id = sess["user_id"]
|
||||
fond_id = auth.create_user_asset(user_id, "ciel.png", original_name="ciel.png", source="upload", scene_kind="fond")
|
||||
decor_id = auth.create_user_asset(user_id, "photo.png", original_name="photo.png", source="upload", scene_kind="decor")
|
||||
decor_id = auth.create_user_asset(
|
||||
user_id, "photo.png", original_name="photo.png", source="upload", scene_kind="decor"
|
||||
)
|
||||
audio_id = auth.create_user_asset(user_id, "bip.mp3", original_name="bip.mp3", source="upload")
|
||||
video_id = auth.create_user_asset(user_id, "clip.mp4", original_name="clip.mp4", source="upload")
|
||||
|
||||
html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
|
||||
m = re.search(r"var USER_ASSETS_OPTIONS = (\[.*?\]);", html)
|
||||
m = not_none(re.search(r"var USER_ASSETS_OPTIONS = (\[.*?\]);", html))
|
||||
names = [o["name"] for o in json.loads(m.group(1))]
|
||||
assert set(names) == {"ciel.png", "photo.png", "bip.mp3", "clip.mp4"}
|
||||
|
||||
fond_section = html.split('<summary>Fonds</summary>', 1)[1].split("</details>", 1)[0]
|
||||
decor_section = html.split('<summary>Décors/objets</summary>', 1)[1].split("</details>", 1)[0]
|
||||
sons_section = html.split('<summary>Sons</summary>', 1)[1].split("</details>", 1)[0]
|
||||
videos_section = html.split('<summary>Vidéos</summary>', 1)[1].split("</details>", 1)[0]
|
||||
fond_section = html.split("<summary>Fonds</summary>", 1)[1].split("</details>", 1)[0]
|
||||
decor_section = html.split("<summary>Décors/objets</summary>", 1)[1].split("</details>", 1)[0]
|
||||
sons_section = html.split("<summary>Sons</summary>", 1)[1].split("</details>", 1)[0]
|
||||
videos_section = html.split("<summary>Vidéos</summary>", 1)[1].split("</details>", 1)[0]
|
||||
|
||||
assert "ciel.png" in fond_section and "photo.png" not in fond_section
|
||||
assert "addSceneObject('fond', null, null, {})".format(fond_id) in fond_section
|
||||
@@ -62,24 +77,29 @@ def test_scene_edit_view_splits_my_assets_into_fond_decor_sons_videos_subsection
|
||||
assert "deleteUserAsset({},".format(asset_id) in html
|
||||
|
||||
|
||||
def test_scene_edit_view_defaults_a_legacy_image_without_scene_kind_to_decor(client, tmp_game_slug_cleanup):
|
||||
def test_scene_edit_view_defaults_a_legacy_image_without_scene_kind_to_decor(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
"""Une image importée AVANT l'ajout de scene_kind (NULL en base) doit
|
||||
rester utilisable — comportement inchangé : "decor", jamais perdue
|
||||
dans un vide entre les deux sous-sections."""
|
||||
import auth
|
||||
|
||||
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
||||
with client.session_transaction() as sess:
|
||||
user_id = sess["user_id"]
|
||||
auth.create_user_asset(user_id, "ancienne.png", original_name="ancienne.png", source="upload")
|
||||
|
||||
html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
|
||||
decor_section = html.split('<summary>Décors/objets</summary>', 1)[1].split("</details>", 1)[0]
|
||||
fond_section = html.split('<summary>Fonds</summary>', 1)[1].split("</details>", 1)[0]
|
||||
decor_section = html.split("<summary>Décors/objets</summary>", 1)[1].split("</details>", 1)[0]
|
||||
fond_section = html.split("<summary>Fonds</summary>", 1)[1].split("</details>", 1)[0]
|
||||
assert "ancienne.png" in decor_section
|
||||
assert "ancienne.png" not in fond_section
|
||||
|
||||
|
||||
def test_scene_edit_view_renders_with_a_personnage_object_selected(client, tmp_game_slug_cleanup):
|
||||
def test_scene_edit_view_renders_with_a_personnage_object_selected(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
||||
resp = client.post(
|
||||
f"/game/{slug}/screens/{screen_id}/scene-objects/add",
|
||||
@@ -87,14 +107,16 @@ def test_scene_edit_view_renders_with_a_personnage_object_selected(client, tmp_g
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
obj_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
|
||||
obj_id = int(not_none(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):
|
||||
def test_scene_object_geometry_route_persists_pixels(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
||||
obj_id = screens.add_scene_object(slug, screen_id)
|
||||
resp = client.post(
|
||||
@@ -103,11 +125,11 @@ def test_scene_object_geometry_route_persists_pixels(client, tmp_game_slug_clean
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
obj = screens.get_scene_object(slug, obj_id)
|
||||
obj = not_none(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):
|
||||
def test_scene_object_delete_route(client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]) -> None:
|
||||
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)
|
||||
@@ -115,25 +137,31 @@ def test_scene_object_delete_route(client, tmp_game_slug_cleanup):
|
||||
assert screens.get_scene_object(slug, obj_id) is None
|
||||
|
||||
|
||||
def test_flow_node_targets_scene_object(client, tmp_game_slug_cleanup):
|
||||
def test_flow_node_targets_scene_object(client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]) -> None:
|
||||
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}),
|
||||
}),
|
||||
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)
|
||||
node = not_none(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):
|
||||
def test_held_key_trigger_persists_and_is_in_scene_palette(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
"""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
|
||||
@@ -148,30 +176,38 @@ def test_held_key_trigger_persists_and_is_in_scene_palette(client, tmp_game_slug
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
node = screens.get_flow_node(slug, resp.get_json()["id"])
|
||||
node = not_none(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):
|
||||
def test_flow_node_collision_condition_targets_scene_objects(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
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,
|
||||
}),
|
||||
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"])
|
||||
node = not_none(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):
|
||||
def test_play_payload_renders_scene_objects_for_jeu2d_game(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
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)
|
||||
@@ -181,7 +217,7 @@ def test_play_payload_renders_scene_objects_for_jeu2d_game(client, tmp_game_slug
|
||||
assert any(payload["personnage_animations"].values())
|
||||
|
||||
|
||||
def test_play_html_renders_fixed_size_scene(client, tmp_game_slug_cleanup):
|
||||
def test_play_html_renders_fixed_size_scene(client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]) -> None:
|
||||
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)
|
||||
@@ -190,7 +226,9 @@ def test_play_html_renders_fixed_size_scene(client, tmp_game_slug_cleanup):
|
||||
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):
|
||||
def test_render_scene_object_carries_data_anim_target(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
"""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
|
||||
@@ -199,11 +237,13 @@ def test_render_scene_object_carries_data_anim_target(client, tmp_game_slug_clea
|
||||
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))
|
||||
html = screens.render_scene_object(not_none(screens.get_scene_object(slug, obj_id)))
|
||||
assert f'data-anim-target="{obj_id}"' in html
|
||||
|
||||
|
||||
def test_animation_clip_add_route_accepts_a_scene_object(client, tmp_game_slug_cleanup):
|
||||
def test_animation_clip_add_route_accepts_a_scene_object(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
"""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)
|
||||
@@ -220,7 +260,9 @@ def test_animation_clip_add_route_accepts_a_scene_object(client, tmp_game_slug_c
|
||||
assert clip["element_id"] == obj_id
|
||||
|
||||
|
||||
def test_deleting_scene_object_cleans_up_its_animation_clips(client, tmp_game_slug_cleanup):
|
||||
def test_deleting_scene_object_cleans_up_its_animation_clips(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
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")
|
||||
@@ -228,7 +270,9 @@ def test_deleting_scene_object_cleans_up_its_animation_clips(client, tmp_game_sl
|
||||
assert screens.list_animation_clips(slug, screen_id) == []
|
||||
|
||||
|
||||
def test_scene_edit_view_world_matches_scene_when_no_oversized_fond(client, tmp_game_slug_cleanup):
|
||||
def test_scene_edit_view_world_matches_scene_when_no_oversized_fond(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
"""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
|
||||
@@ -245,7 +289,9 @@ def test_scene_edit_view_world_matches_scene_when_no_oversized_fond(client, tmp_
|
||||
assert 'id="scene-camera-h" class="input is-small" style="width:80px;" min="1" value="540"' in html
|
||||
|
||||
|
||||
def test_scene_edit_view_expands_world_for_an_oversized_fond(client, tmp_game_slug_cleanup):
|
||||
def test_scene_edit_view_expands_world_for_an_oversized_fond(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
"""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
|
||||
@@ -261,7 +307,9 @@ def test_scene_edit_view_expands_world_for_an_oversized_fond(client, tmp_game_sl
|
||||
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):
|
||||
def test_scene_edit_view_world_bounds_are_used_for_drag_clamping(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
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)
|
||||
@@ -270,23 +318,26 @@ def test_scene_edit_view_world_bounds_are_used_for_drag_clamping(client, tmp_gam
|
||||
assert "var SCENE_HEIGHT = 1080;" in html
|
||||
|
||||
|
||||
def test_set_scene_size_updates_screen_dimensions(tmp_game_slug_cleanup):
|
||||
""""Caméra (px)" (voir templates/scene_edit.html) : jusqu'ici
|
||||
def test_set_scene_size_updates_screen_dimensions(tmp_game_slug_cleanup: Callable[[str], str]) -> None:
|
||||
""" "Caméra (px)" (voir templates/scene_edit.html) : jusqu'ici
|
||||
scene_width/scene_height n'étaient fixées qu'à la création de
|
||||
l'écran, jamais modifiables ensuite."""
|
||||
slug = tmp_game_slug_cleanup(db.create_game("pytest_scene_size"))
|
||||
screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
|
||||
screens.set_scene_size(slug, screen_id, 1920, 1080)
|
||||
screen = screens.get_screen(slug, screen_id)
|
||||
screen = not_none(screens.get_screen(slug, screen_id))
|
||||
assert screen["scene_width"] == 1920
|
||||
assert screen["scene_height"] == 1080
|
||||
|
||||
|
||||
def test_screen_set_scene_size_route_persists_and_reflects_in_editor(client, tmp_game_slug_cleanup):
|
||||
def test_screen_set_scene_size_route_persists_and_reflects_in_editor(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
||||
resp = client.post(
|
||||
f"/game/{slug}/screens/{screen_id}/scene-size",
|
||||
data=json.dumps({"width": 1920, "height": 1080}), content_type="application/json",
|
||||
data=json.dumps({"width": 1920, "height": 1080}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.get_json() == {"ok": True}
|
||||
html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
|
||||
@@ -299,10 +350,13 @@ def test_screen_set_scene_size_route_persists_and_reflects_in_editor(client, tmp
|
||||
assert 'id="scene-camera-h" class="input is-small" style="width:80px;" min="1" value="1080"' in html
|
||||
|
||||
|
||||
def test_screen_set_scene_size_route_rejects_invalid_dimensions(client, tmp_game_slug_cleanup):
|
||||
def test_screen_set_scene_size_route_rejects_invalid_dimensions(
|
||||
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
|
||||
) -> None:
|
||||
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
||||
resp = client.post(
|
||||
f"/game/{slug}/screens/{screen_id}/scene-size",
|
||||
data=json.dumps({"width": "oops", "height": 540}), content_type="application/json",
|
||||
data=json.dumps({"width": "oops", "height": 540}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
Reference in New Issue
Block a user