Files
williamandClaude Sonnet 5 c57420c8c9 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>
2026-09-15 16:06:15 +02:00

108 lines
4.4 KiB
Python

"""Son — Phase 6 du plan d'extension du moteur : musique de fond par écran
(en boucle, suit le cycle de vie de l'écran) et action ponctuelle "Jouer un
son" (effet sonore, non bouclé). Comme le reste du graphe de logique côté
client, la lecture audio RÉELLE n'est pas testable sans navigateur (voir
play_js_bundle() — on vérifie que la logique existe dans le JS livré et que
les données persistent, pas qu'elles jouent un son)."""
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
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_flow_node(client: FlaskClient, slug: str, screen_id: int, **fields: Any) -> Any:
resp = client.post(
f"/game/{slug}/screens/{screen_id}/flow/nodes/add",
data=json.dumps(fields),
content_type="application/json",
)
assert resp.status_code == 200
return resp.get_json()["id"]
def test_screen_background_music_persists(client: FlaskClient, game: str) -> None:
screen_id = _create_screen(client, game)
resp = client.post(
f"/game/{game}/screens/{screen_id}/background-music",
data=json.dumps({"url": "https://example.com/song.mp3"}),
content_type="application/json",
)
assert resp.status_code == 200
screen = not_none(screens.get_screen(game, screen_id))
assert screen["background_music_url"] == "https://example.com/song.mp3"
def test_screen_background_music_empty_string_clears_it(client: FlaskClient, game: str) -> None:
screen_id = _create_screen(client, game)
client.post(
f"/game/{game}/screens/{screen_id}/background-music",
data=json.dumps({"url": "https://example.com/song.mp3"}),
content_type="application/json",
)
client.post(
f"/game/{game}/screens/{screen_id}/background-music",
data=json.dumps({"url": ""}),
content_type="application/json",
)
screen = not_none(screens.get_screen(game, screen_id))
assert screen["background_music_url"] is None
def test_play_payload_exposes_background_music_per_screen(client: FlaskClient, game: str) -> None:
screen_id = _create_screen(client, game)
client.post(
f"/game/{game}/screens/{screen_id}/background-music",
data=json.dumps({"url": "https://example.com/song.mp3"}),
content_type="application/json",
)
html = client.get(f"/game/{game}/runtime-payload").get_json()
screen_payload = next(s for s in html["screens"] if s["id"] == screen_id)
assert screen_payload["background_music_url"] == "https://example.com/song.mp3"
def test_sound_action_node_persists_url(client: FlaskClient, game: str) -> None:
screen_id = _create_screen(client, game)
node_id = _add_flow_node(
client,
game,
screen_id,
node_type="action",
action_type="jouer_son",
data_value="https://example.com/effect.wav",
)
node = not_none(screens.get_flow_node(game, node_id))
assert node["action_type"] == "jouer_son"
assert node["data_value"] == "https://example.com/effect.wav"
def test_play_page_exposes_sound_runtime(client: FlaskClient, game: str) -> None:
js = play_js_bundle()
assert "runScreenBackgroundMusic" in js
assert "'jouer_son'" in js
assert "new Audio" in js
def test_scene_editor_exposes_sound_action(client: FlaskClient, game: str) -> None:
"""L'UI de réglage de la musique de fond par écran (screen-bg-music-url)
et le formulaire générique de nœud d'action (nf-action-sound-fields)
n'existaient que dans l'éditeur document, retiré — l'action "Jouer un
son" reste disponible pour jeu_2d via l'assistant "+ Action" de
l'onglet Collision (voir screens/scenes/flow_palette.py) ; le réglage
de musique de fond par écran (screen_set_background_music.py) reste
fonctionnel côté backend/API (voir les tests ci-dessus), même sans UI
dédiée dans le nouvel éditeur."""
screen_id = _create_screen(client, game)
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
assert "jouer_son" in html