Files
williamandClaude Sonnet 5 b2e933f322
Build and deploy / test-python (push) Successful in 11m12s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / lint-python (push) Successful in 3m56s
Build and deploy / lint-js (push) Successful in 3m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 3m58s
Reorganisation game/document : renommage screens->game_engine + sous-dossiers game/ dans routes, scripts, static, templates, tests
Prepare la scission a venir entre l'editeur Jeu 2D et le futur editeur
Support de formation (voir docs/plan/PLAN.md), sans toucher a
l'architecture en couches existante :

- screens/ renomme en game_engine/ (nom clair pour le moteur du jeu 2D,
  avant l'arrivee d'un second "moteur" cote document) : ~85 imports
  corriges, contrat import-linter mis a jour, meme forme de couches.
- routes/, scripts/, static/, templates/, tests/ : tout ce qui est
  propre au jeu 2D deplace dans un sous-dossier game/ de chacun
  (routes/game/, static/game/, templates/game/, tests/game/,
  scripts/game/) ; ce qui est partage par le site (auth, onboarding,
  dashboard, uploads, db/) reste a la racine de chaque dossier. Un
  sous-dossier document/ (vide) cree dans chacun pour le futur chantier.
- styles/ volontairement inchange : les 3 fichiers sources sont
  concatenes en un seul static/style.css charge par tout le site,
  scinder leur CONTENU (editeur vs partage) serait un refactor CSS
  distinct, pas un deplacement mecanique.
- Chaine d'export SCORM (publish/build_scorm_package.py) mise a jour en
  profondeur : copie des assets, URLs d'icones relatives a
  static/style.css (qui ne bouge pas), manifeste, wrapper SCORM.
- Deux regressions d'un sweep de renommage anterieur corrigees au passage
  (screens.js/screens/scene-objects incorrectement convertis en
  game_engine.js/game_engine/scene-objects dans des commentaires).
- Effet de bord Windows decouvert et corrige : git mv + Path.write_text
  convertissent des fichiers en CRLF (core.autocrlf=true) - ~189 fichiers
  normalises en LF.
- .eslintrc.json/package.json : uniquement les chemins de glob mis a jour
  (static/game/js/...) ; la preparation eslint-plugin-unicorn du lot 7
  reste volontairement non committee (package-lock.json restaure a la
  version precedente).

Verifications : ruff, mypy --strict (391 fichiers), vulture, bandit,
lint-imports tous verts ; 591/591 tests Python, 276/276 tests JS ;
demarrage serveur + requetes HTTP manuelles confirmant que les assets
deplaces repondent en 200 au nouvel emplacement et 404 a l'ancien.

SKIP=djlint : backlog H021 (styles inline) deja documente comme dette
assumee dans CODE_QUALITY.md section 6, aucun template touche par ce
commit au-dela d'un deplacement de fichier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 12:27:53 +02:00

167 lines
5.8 KiB
Python

"""Tests du bornage automatique des champs numériques (2.2)."""
import json
import re
from typing import Any
from flask.testing import FlaskClient
import db
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 _create_numeric_object(
client: FlaskClient,
slug: str,
field_name: str = "reputation",
min_v: str = "0",
max_v: str = "100",
start: str = "50",
) -> Any:
field: dict[str, Any] = {"name": field_name, "type": "nombre_entier"}
if min_v != "":
field["min_value"] = min_v
if max_v != "":
field["max_value"] = max_v
def_id = db.create_definition(slug, "Partie", [field])
definition = not_none(db.get_definition(slug, def_id))
db.insert_row(slug, definition, {field_name: start})
return def_id
def _get_row_value(client: FlaskClient, slug: str, def_id: int, field_name: str) -> Any:
import db
definition = not_none(db.get_definition(slug, def_id))
rows = db.list_rows(slug, definition)
return rows[0].get(db.slugify(field_name).replace("-", "_"))
def test_field_bounds_are_stored_at_creation(client: FlaskClient, game: str) -> None:
def_id = _create_numeric_object(client, game, "reputation", "0", "100", "50")
import db
definition = not_none(db.get_definition(game, def_id))
field = definition["fields"][0]
assert field["min_value"] == 0
assert field["max_value"] == 100
def test_incrementer_clamps_to_max(client: FlaskClient, game: str) -> None:
"""Régression 2.2 : une action "Augmenter de..." ne doit jamais faire
dépasser la borne maxi réglée sur le champ, même si le montant ajouté
dépasse largement ce qu'il reste avant la limite."""
def_id = _create_numeric_object(client, game, "reputation", "0", "100", "95")
screen_id = _create_screen(client, game)
resp = client.post(
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
data=json.dumps(
{
"node_type": "action",
"action_type": "modifier_donnee",
"target_definition_id": def_id,
"target_row_id": 1,
"target_field": "reputation",
"data_operation": "incrementer",
"data_value": "50",
}
),
content_type="application/json",
)
node = resp.get_json()
resp = client.post(f"/game/{game}/flow/nodes/{node['id']}/run-data")
assert resp.get_json() == {"ok": True}
new_value = _get_row_value(client, game, def_id, "reputation")
assert new_value == 100, f"la valeur aurait dû être bornée à 100, vaut {new_value}"
def test_decrementer_clamps_to_min(client: FlaskClient, game: str) -> None:
"""Régression 2.2 : symétrique pour "Diminuer de..." et la borne mini."""
def_id = _create_numeric_object(client, game, "reputation", "0", "100", "5")
screen_id = _create_screen(client, game)
resp = client.post(
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
data=json.dumps(
{
"node_type": "action",
"action_type": "modifier_donnee",
"target_definition_id": def_id,
"target_row_id": 1,
"target_field": "reputation",
"data_operation": "decrementer",
"data_value": "50",
}
),
content_type="application/json",
)
node = resp.get_json()
client.post(f"/game/{game}/flow/nodes/{node['id']}/run-data")
new_value = _get_row_value(client, game, def_id, "reputation")
assert new_value == 0, f"la valeur aurait dû être bornée à 0, vaut {new_value}"
def test_definir_nombre_also_clamped(client: FlaskClient, game: str) -> None:
"""Régression 2.2 : "Donner cette valeur (nombre)" doit aussi être
bornée, pas seulement les incréments/décréments."""
def_id = _create_numeric_object(client, game, "reputation", "0", "100", "50")
screen_id = _create_screen(client, game)
resp = client.post(
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
data=json.dumps(
{
"node_type": "action",
"action_type": "modifier_donnee",
"target_definition_id": def_id,
"target_row_id": 1,
"target_field": "reputation",
"data_operation": "definir_nombre",
"data_value": "9999",
}
),
content_type="application/json",
)
node = resp.get_json()
client.post(f"/game/{game}/flow/nodes/{node['id']}/run-data")
new_value = _get_row_value(client, game, def_id, "reputation")
assert new_value == 100
def test_no_bounds_set_means_unlimited(client: FlaskClient, game: str) -> None:
"""Sans bornes réglées (champ laissé vide), le comportement doit rester
celui d'avant 2.2 : aucune limite appliquée."""
def_id = _create_numeric_object(client, game, "reputation", "", "", "50")
screen_id = _create_screen(client, game)
resp = client.post(
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
data=json.dumps(
{
"node_type": "action",
"action_type": "modifier_donnee",
"target_definition_id": def_id,
"target_row_id": 1,
"target_field": "reputation",
"data_operation": "incrementer",
"data_value": "500",
}
),
content_type="application/json",
)
node = resp.get_json()
client.post(f"/game/{game}/flow/nodes/{node['id']}/run-data")
new_value = _get_row_value(client, game, def_id, "reputation")
assert new_value == 550