Files
Forge-Engine/tests/test_animal_sprite_library.py
T
williamandClaude Sonnet 5 e82eb7ce88
Build and deploy / test-python (push) Successful in 7m5s
Build and deploy / test-js (push) Successful in 58s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Retire l'export exécutable (.exe) et la partie publique en ligne (/jouer)
Décision produit : seul l'export Web/SCORM (LMS) est pertinent — l'export
exécutable Windows autonome (publish/build_package.py, bouton "Publier")
et la partie publique par joueur (/jouer/<slug>, routes/public_play/,
"Publier en ligne") sont jugés redondants et retirés.

Conserve le mécanisme d'état "par joueur" (db/global_vars,
db/rows, per_player) : infrastructure générique déjà utilisée par
Score/Progression et testée indépendamment de toute route publique
(voir tests/test_player_state.py), aucune raison de la retirer.

_STATIC_ITEMS/_copy_characters (copie sélective des sprites CraftPix
réellement utilisés) migrent de build_package.py vers
build_scorm_package.py, seul appelant restant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 10:59:28 +02:00

165 lines
8.1 KiB
Python

"""Bibliothèque de sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY,
screens/labels/animal_sprite_library.py) — réservée au rôle admin
(licence CraftPix, voir core/sprite_gate.py et le plan "Bibliothèque de
sprites animaux CraftPix"). Un compte "user" ne doit ni la voir dans la
galerie de l'éditeur, ni pouvoir l'assigner par un POST direct."""
import json
import screens
def test_admin_sprite_library_loaded_from_manifest():
assert len(screens.ADMIN_SPRITE_LIBRARY) > 0
bunny = screens.ADMIN_SPRITE_LIBRARY["animal-bunny-01"]
assert bunny["family"] == "bunny"
assert bunny["animations"]["idle"][0] == "/static/characters/animals/bunny/01/idle0.png"
assert len(bunny["animations"]["idle"]) == 20
def test_sprite_library_is_public_plus_admin_merged():
assert set(screens.SPRITE_LIBRARY) == set(screens.PUBLIC_SPRITE_LIBRARY) | set(screens.ADMIN_SPRITE_LIBRARY)
assert screens.ADMIN_ONLY_CHARACTER_SLUGS == set(screens.ADMIN_SPRITE_LIBRARY)
assert "male-adventurer" not in screens.ADMIN_ONLY_CHARACTER_SLUGS
def test_sprite_gallery_families_shows_one_tile_per_animal():
families = screens.sprite_gallery_families(screens.SPRITE_LIBRARY)
slugs = [slug for slug, _ in families]
assert len(slugs) == len(screens.PUBLIC_SPRITE_LIBRARY) + 14 # 6 Kenney + 14 familles animales
assert "animal-bunny-01" in slugs
assert "animal-bunny-02" not in slugs
def test_document_editor_hides_animal_characters_from_a_user_account(user_game, user_client):
resp = user_client.post(f"/game/{user_game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
screen_id = resp.headers["Location"].rstrip("/").split("/")[-2]
html = user_client.get(f"/game/{user_game}/screens/{screen_id}/edit").get_data(as_text=True)
assert "animal-bunny-01" not in html
assert "male-adventurer" in html
def test_document_editor_shows_animal_characters_to_admin(game, client):
resp = client.post(f"/game/{game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
screen_id = resp.headers["Location"].rstrip("/").split("/")[-2]
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
assert "animal-bunny-01" in html
def _create_jeu2d_game_as(post_client, name, slug_owner_client=None):
# Fusion des moteurs (voir screens/screens_repo/ensure_schema.py) : un
# jeu n'a plus de type propre — seul l'écran créé ensuite par chaque
# test (screens.create_screen(..., kind="jeu_2d")) est une scène.
resp = post_client.post("/games/new", data={"name": name}, follow_redirects=False)
assert resp.status_code == 302
return resp.headers["Location"].rstrip("/").split("/")[-1]
def test_scene_editor_hides_animal_characters_from_a_user_account(client, user_client, tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_user"))
with user_client.session_transaction() as sess:
import auth
auth.set_project_slug(sess["user_id"], slug)
screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
html = user_client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
assert "animal-bunny-01" not in html
def test_scene_object_add_rejects_animal_character_for_a_user_account(client, user_client, tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_gate"))
with user_client.session_transaction() as sess:
import auth
auth.set_project_slug(sess["user_id"], slug)
screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
resp = user_client.post(
f"/game/{slug}/screens/{screen_id}/scene-objects/add",
data={"kind": "personnage", "forge_character": "animal-bunny-01"},
)
assert resp.status_code == 403
def test_scene_object_add_allows_animal_character_for_admin(client, tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_admin"))
screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
resp = client.post(
f"/game/{slug}/screens/{screen_id}/scene-objects/add",
data={"kind": "personnage", "forge_character": "animal-bunny-01"},
follow_redirects=False,
)
assert resp.status_code == 302
obj_id = int(resp.headers["Location"].rsplit("selected=", 1)[1])
obj = screens.get_scene_object(slug, obj_id)
assert screens.resolve_personnage_data(obj)["forge_character"] == "animal-bunny-01"
def test_scene_object_personnage_swap_rejects_animal_character_for_a_user_account(client, user_client, tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_swap_gate"))
with user_client.session_transaction() as sess:
import auth
auth.set_project_slug(sess["user_id"], slug)
screen_id = screens.create_screen(slug, "Scène 1")
obj_id = screens.add_scene_object(slug, screen_id)
resp = user_client.post(
f"/game/{slug}/scene-objects/{obj_id}/personnage",
data=json.dumps({"source": "forge", "forge_character": "animal-bunny-01"}),
content_type="application/json",
)
assert resp.status_code == 403
def test_element_add_rejects_animal_character_for_a_user_account(user_game, user_client):
resp = user_client.post(f"/game/{user_game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
screen_id = resp.headers["Location"].rstrip("/").split("/")[-2]
resp = user_client.post(
f"/game/{user_game}/screens/{screen_id}/elements/add",
data={"widget": "personnage", "forge_character": "animal-bunny-01"},
)
assert resp.status_code == 403
def test_element_set_personnage_data_rejects_animal_character_for_a_user_account(user_game, user_client):
resp = user_client.post(f"/game/{user_game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
screen_id = resp.headers["Location"].rstrip("/").split("/")[-2]
resp = user_client.post(
f"/game/{user_game}/screens/{screen_id}/elements/add", data={"widget": "personnage"}, follow_redirects=False
)
element_id = int(resp.headers["Location"].rsplit("selected=", 1)[1])
resp = user_client.post(
f"/game/{user_game}/elements/{element_id}/personnage",
data=json.dumps({"source": "forge", "forge_character": "animal-bunny-01"}),
content_type="application/json",
)
assert resp.status_code == 403
def test_list_used_forge_characters_scans_elements_and_scene_objects(client, tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_used_characters"))
screen_id = screens.create_screen(slug, "Scène 1")
screens.add_scene_object(slug, screen_id, forge_character="animal-bunny-01")
screens.add_scene_object(slug, screen_id, kind="decor")
used = screens.list_used_forge_characters(slug)
assert used == {"animal-bunny-01"}
def test_build_scorm_package_only_copies_used_animal_character_folders(client, tmp_game_slug_cleanup, tmp_path):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_scorm_animals"))
screen_id = screens.create_screen(slug, "Scène 1")
screens.add_scene_object(slug, screen_id, forge_character="animal-bunny-01")
with client.application.test_request_context():
from publish import build_scorm_package
zip_path, _name = build_scorm_package.build_scorm_zip(slug)
try:
import zipfile
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
animal_paths = {n for n in names if "static/characters/animals/" in n and not n.endswith("manifest.json")}
animals_used = {n.split("static/characters/animals/")[1].split("/")[0] for n in animal_paths}
assert animals_used == {"bunny"}
assert any("static/characters/animals/bunny/01/" in n for n in animal_paths)
assert not any("static/characters/animals/bear/" in n for n in animal_paths)
assert any(n.endswith("static/characters/animals/manifest.json") for n in names)
assert any("static/characters/male-adventurer/" in n for n in names)
finally:
import os
os.remove(zip_path)