Nouveaux attributs par élément (svg_markup/width/border_radius/bold/ uppercase, tous vides ou False par défaut = comportement historique inchangé), réglables depuis le panneau Propriétés — même esprit que bold/align/color déjà présents sur titre/paragraphe. Fixer une largeur implique toujours de sortir de l'étirement pleine largeur par défaut (align-self:flex-start posé automatiquement avec elle). Le kicker "Module obligatoire" du thème Sécurité Incendie s'en sert maintenant (icône flamme, largeur au contenu, arrondi complet, gras, majuscules) pour être conforme à la maquette d'origine — la puce décorative CSS générique qui la remplaçait disparaît du thème. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
120 lines
5.2 KiB
Python
120 lines
5.2 KiB
Python
"""Catalogue de thèmes visuels (document_engine/themes/) — conversion du
|
|
contenu-seed en éléments "à plat" pour l'aperçu (seed_blocks_to_elements),
|
|
et respect par CHAQUE thème du catalogue des règles structurelles du
|
|
moteur (ex. un mini-jeu seul sur sa page, voir routes/document/
|
|
document_element_add.py) — vérifié ici statiquement sur les données du
|
|
catalogue, sans passer par une route HTTP."""
|
|
|
|
from typing import Any
|
|
|
|
import document_engine as doc_engine
|
|
|
|
|
|
def test_get_document_theme_entry_returns_none_for_an_unknown_id() -> None:
|
|
assert doc_engine.get_document_theme_entry("n-importe-quoi") is None
|
|
|
|
|
|
def test_get_document_theme_entry_returns_the_matching_entry() -> None:
|
|
entry = doc_engine.get_document_theme_entry("securite-incendie")
|
|
assert entry is not None
|
|
assert entry["id"] == "securite-incendie"
|
|
assert entry["name"] == "Sécurité Incendie"
|
|
assert len(entry["seed_pages"]) > 0
|
|
|
|
|
|
def test_securite_incendie_title_page_is_vertically_centered() -> None:
|
|
"""Retour utilisateur du 24/09/2026 : la page de garde doit être
|
|
centrée verticalement, comme une page de titre — les pages suivantes
|
|
restent alignées en haut (comportement par défaut)."""
|
|
entry = doc_engine.get_document_theme_entry("securite-incendie")
|
|
assert entry is not None
|
|
assert entry["seed_pages"][0]["vertical_align"] == "center"
|
|
assert entry["seed_pages"][1].get("vertical_align", "top") == "top"
|
|
|
|
|
|
def test_securite_incendie_kicker_badge_uses_the_new_badge_properties() -> None:
|
|
"""Retour utilisateur du 24/09/2026 : l'étiquette "Module obligatoire"
|
|
doit avoir une icône, une largeur au contenu, un arrondi complet et
|
|
du gras/majuscules, comme dans la maquette d'origine."""
|
|
entry = doc_engine.get_document_theme_entry("securite-incendie")
|
|
assert entry is not None
|
|
kicker = entry["seed_pages"][0]["blocks"][0]
|
|
assert kicker["kind"] == "badge"
|
|
attrs = kicker["attributes"]
|
|
assert attrs["svg_markup"]
|
|
assert attrs["width"] == "fit-content"
|
|
assert attrs["border_radius"] == "999px"
|
|
assert attrs["bold"] is True
|
|
assert attrs["uppercase"] is True
|
|
|
|
|
|
def test_securite_incendie_intro_paragraph_has_a_max_width() -> None:
|
|
"""Retour utilisateur du 24/09/2026 : le sous-titre de la page de
|
|
garde doit rester aussi étroit que dans la maquette, pas étiré sur
|
|
toute la largeur de la page."""
|
|
entry = doc_engine.get_document_theme_entry("securite-incendie")
|
|
assert entry is not None
|
|
intro_paragraph = entry["seed_pages"][0]["blocks"][2]
|
|
assert intro_paragraph["kind"] == "paragraphe"
|
|
assert intro_paragraph["attributes"]["max_width"] == "60ch"
|
|
|
|
|
|
def test_seed_blocks_to_elements_assigns_unique_synthetic_ids() -> None:
|
|
blocks: list[dict[str, Any]] = [
|
|
{"kind": "titre", "attributes": {"content": "Titre"}},
|
|
{
|
|
"kind": "row",
|
|
"attributes": {"gap": 10},
|
|
"children": [
|
|
{"kind": "paragraphe", "attributes": {"content": "A"}},
|
|
{"kind": "paragraphe", "attributes": {"content": "B"}},
|
|
],
|
|
},
|
|
]
|
|
elements = doc_engine.seed_blocks_to_elements(blocks)
|
|
ids = [el["id"] for el in elements]
|
|
assert len(ids) == len(set(ids))
|
|
assert len(elements) == 4
|
|
|
|
row = next(el for el in elements if el["kind"] == "row")
|
|
children = [el for el in elements if el["parent_id"] == row["id"]]
|
|
assert len(children) == 2
|
|
|
|
|
|
def test_seed_blocks_to_elements_merges_onto_default_attributes() -> None:
|
|
elements = doc_engine.seed_blocks_to_elements([{"kind": "titre", "attributes": {"content": "Contenu seul"}}])
|
|
assert elements[0]["attributes"]["content"] == "Contenu seul"
|
|
assert elements[0]["attributes"]["style"] == "titre1"
|
|
|
|
|
|
def test_seed_blocks_to_elements_renders_without_error() -> None:
|
|
theme = doc_engine.get_document_theme_entry("securite-incendie")
|
|
assert theme is not None
|
|
elements = doc_engine.seed_blocks_to_elements(theme["seed_pages"][0]["blocks"])
|
|
html = doc_engine.render_document(elements)
|
|
assert "docText" in html
|
|
|
|
|
|
def test_every_theme_seed_page_has_a_valid_vertical_align() -> None:
|
|
for theme in doc_engine.DOCUMENT_THEMES:
|
|
for seed_page in theme["seed_pages"]:
|
|
align = seed_page.get("vertical_align", "top")
|
|
assert align in doc_engine.VERTICAL_ALIGNS, f"thème {theme['id']!r} : vertical_align invalide {align!r}"
|
|
|
|
|
|
def test_every_theme_seed_page_respects_the_minigame_exclusivity_rule() -> None:
|
|
"""Un mini-jeu doit toujours être SEUL sur sa page (règle appliquée
|
|
côté route pour un ajout manuel, voir routes/document/
|
|
document_element_add.py) — le contenu-seed d'un thème la respecte
|
|
dès sa conception puisque document_engine.replace_document_content
|
|
ne la revérifie pas elle-même (contenu fourni par le thème, pas par
|
|
l'utilisateur)."""
|
|
for theme in doc_engine.DOCUMENT_THEMES:
|
|
for seed_page in theme["seed_pages"]:
|
|
page_blocks = seed_page["blocks"]
|
|
minigame_blocks = [b for b in page_blocks if b["kind"] in doc_engine.MINIGAME_KINDS]
|
|
if minigame_blocks:
|
|
assert len(page_blocks) == 1, (
|
|
f"thème {theme['id']!r} : une page avec mini-jeu doit contenir uniquement ce mini-jeu"
|
|
)
|