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:
william
2026-09-15 16:06:15 +02:00
co-authored by Claude Sonnet 5
parent 7db4803b93
commit c57420c8c9
454 changed files with 16448 additions and 6967 deletions
+38 -19
View File
@@ -1,31 +1,44 @@
"""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 conftest import play_js_bundle
from tests.conftest import not_none
def _create_screen(client, slug, name="Accueil"):
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(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
return int(not_none(re.search(r"/screens/(\d+)/edit", resp.headers["Location"])).group(1))
def _add_element(client, slug, screen_id, widget=None):
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, game):
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",
}),
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
@@ -37,16 +50,21 @@ def test_animate_css_clip_persists(client, game):
assert clip["element_id"] == el_id
def test_custom_keyframes_clip_persists(client, game):
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",
}),
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
@@ -55,7 +73,7 @@ def test_custom_keyframes_clip_persists(client, game):
assert json.loads(clip["custom_keyframes"]) == keyframes
def test_clip_update_changes_timing(client, game):
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(
@@ -74,7 +92,7 @@ def test_clip_update_changes_timing(client, game):
assert updated["duration"] == 0.5
def test_clip_delete_removes_it(client, game):
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(
@@ -85,10 +103,11 @@ def test_clip_delete_removes_it(client, game):
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, game):
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
@@ -100,7 +119,7 @@ def test_scene_editor_loads_animate_css(client, game):
assert "animate.min.css" in html
def test_play_page_exposes_animation_runtime_and_animate_css(client, game):
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
@@ -109,7 +128,7 @@ def test_play_page_exposes_animation_runtime_and_animate_css(client, game):
assert "injectCustomKeyframes" in js
def test_play_page_payload_includes_clip(client, game):
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(