Audit complet de mise en forme — Titre/Paragraphe (1er élément du tableau)
Nouveau module partagé document_engine/rendering/box_style.py (padding/margin/background_color/border_radius/border par côté/height/ min-height/max-height/min-width/box-shadow/opacity/align_self) — réutilisable tel quel par tous les kinds suivants du tableau d'audit. Titre/Paragraphe gagnent : barré, police de caractère, taille de police, hauteur de ligne, espacement des lettres, majuscules/ minuscules/capitales, ombre du texte, et tous les attributs de boîte partagés ci-dessus. Interface entièrement à base de curseurs/cases à cocher/listes déroulantes/sélecteurs de couleur natifs — plus aucun champ de texte libre pour une valeur CSS (retour utilisateur). Deux bugs transversaux corrigés au passage (concernent tout l'éditeur) : - Le panneau Propriétés n'était jamais reconstruit après un clic sur un bouton (gras/alignement/segments...) — il fallait recharger la page pour voir l'état réel. Corrigé dans forgeDocUpdateAttributes, point d'entrée unique de toute mise à jour d'attribut. - Les cases à cocher et curseurs héritaient à tort le style d'un champ de texte (padding/bordure/fond/largeur 100%) via la règle générique .docField input. Ajoute docs/plan/AUDIT_MISE_EN_FORME.md : suivi de l'audit élément par élément (Titre/Paragraphe traité, Image ensuite). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e03bea39c5
commit
6c7675fad0
@@ -0,0 +1,78 @@
|
||||
"""Attributs de "boîte" partagés par plusieurs kinds de contenu (voir
|
||||
document_engine/rendering/box_style.py) — testés une seule fois ici,
|
||||
indépendamment de chaque kind qui les utilise (voir aussi
|
||||
test_document_elements.py pour leur usage réel via _render_text)."""
|
||||
|
||||
from document_engine.rendering.box_style import BORDER_SIDES, default_border, render_box_style
|
||||
|
||||
|
||||
def test_default_border_has_all_four_sides_set_to_none() -> None:
|
||||
border = default_border()
|
||||
assert set(border.keys()) == set(BORDER_SIDES)
|
||||
for side in BORDER_SIDES:
|
||||
assert border[side]["style"] == "none"
|
||||
|
||||
|
||||
def test_default_border_returns_a_fresh_dict_each_call() -> None:
|
||||
"""Jamais un littéral partagé muté par référence entre deux éléments
|
||||
(même précaution que DEFAULT_QUIZ_CONFIG côté labels)."""
|
||||
a = default_border()
|
||||
b = default_border()
|
||||
a["top"]["style"] = "solid"
|
||||
assert b["top"]["style"] == "none"
|
||||
|
||||
|
||||
def test_render_box_style_returns_empty_string_for_all_defaults() -> None:
|
||||
assert render_box_style({}) == ""
|
||||
assert render_box_style({"border": default_border(), "align_self": "stretch"}) == ""
|
||||
|
||||
|
||||
def test_render_box_style_includes_padding_margin_background_and_radius() -> None:
|
||||
style = render_box_style(
|
||||
{"padding": "12px", "margin": "0 auto", "background_color": "#eee", "border_radius": "8px"}
|
||||
)
|
||||
assert "padding:12px;" in style
|
||||
assert "margin:0 auto;" in style
|
||||
assert "background-color:#eee;" in style
|
||||
assert "border-radius:8px;" in style
|
||||
|
||||
|
||||
def test_render_box_style_includes_sizing_shadow_and_opacity() -> None:
|
||||
style = render_box_style(
|
||||
{
|
||||
"height": "200px",
|
||||
"min_height": "100px",
|
||||
"max_height": "400px",
|
||||
"min_width": "50px",
|
||||
"box_shadow": "0 4px 12px rgba(0,0,0,.2)",
|
||||
"opacity": "0.8",
|
||||
}
|
||||
)
|
||||
assert "height:200px;" in style
|
||||
assert "min-height:100px;" in style
|
||||
assert "max-height:400px;" in style
|
||||
assert "min-width:50px;" in style
|
||||
assert "box-shadow:0 4px 12px rgba(0,0,0,.2);" in style
|
||||
assert "opacity:0.8;" in style
|
||||
|
||||
|
||||
def test_render_box_style_includes_align_self_only_when_not_stretch() -> None:
|
||||
assert "align-self" not in render_box_style({"align_self": "stretch"})
|
||||
assert "align-self:center;" in render_box_style({"align_self": "center"})
|
||||
|
||||
|
||||
def test_render_box_style_renders_only_sides_with_a_non_none_style() -> None:
|
||||
border = default_border()
|
||||
border["top"] = {"style": "solid", "width": "2px", "color": "#ff0000"}
|
||||
style = render_box_style({"border": border})
|
||||
assert "border-top:2px solid #ff0000;" in style
|
||||
assert "border-right" not in style
|
||||
assert "border-bottom" not in style
|
||||
assert "border-left" not in style
|
||||
|
||||
|
||||
def test_render_box_style_escapes_malicious_values() -> None:
|
||||
border = default_border()
|
||||
border["top"] = {"style": "solid", "width": "1px", "color": '"><script>alert(1)</script>'}
|
||||
style = render_box_style({"padding": '"><script>alert(2)</script>', "border": border})
|
||||
assert "<script>" not in style
|
||||
@@ -12,6 +12,7 @@ from typing import Any
|
||||
|
||||
import db
|
||||
import document_engine as doc_engine
|
||||
from document_engine.rendering.box_style import default_border as doc_engine_box_default_border
|
||||
|
||||
|
||||
def _new_support(tmp_support_slug_cleanup: Any, name: str = "Support de test") -> tuple[str, int]:
|
||||
@@ -32,6 +33,129 @@ def test_add_and_get_document_element_uses_default_attributes(tmp_support_slug_c
|
||||
assert element["attributes"]["content"] == "Nouveau titre"
|
||||
|
||||
|
||||
def test_titre_default_attributes_include_all_new_formatting_fields(tmp_support_slug_cleanup: Any) -> None:
|
||||
"""Audit du 26/09/2026 (réglages manquants) — chaque nouveau champ
|
||||
doit exister avec une valeur "neutre" par défaut (comportement
|
||||
historique inchangé, voir _render_text)."""
|
||||
slug, page_id = _new_support(tmp_support_slug_cleanup)
|
||||
element_id = doc_engine.add_document_element(slug, "titre", page_id=page_id)
|
||||
element = doc_engine.get_document_element(slug, element_id)
|
||||
assert element is not None
|
||||
attrs = element["attributes"]
|
||||
assert attrs["strikethrough"] is False
|
||||
assert attrs["font_family"] == ""
|
||||
assert attrs["font_size"] == ""
|
||||
assert attrs["line_height"] == ""
|
||||
assert attrs["letter_spacing"] == ""
|
||||
assert attrs["text_transform"] == "none"
|
||||
assert attrs["text_shadow"] == ""
|
||||
assert attrs["padding"] == ""
|
||||
assert attrs["margin"] == ""
|
||||
assert attrs["background_color"] == ""
|
||||
assert attrs["border_radius"] == ""
|
||||
assert attrs["align_self"] == "stretch"
|
||||
assert attrs["height"] == ""
|
||||
assert attrs["min_height"] == ""
|
||||
assert attrs["max_height"] == ""
|
||||
assert attrs["min_width"] == ""
|
||||
assert attrs["box_shadow"] == ""
|
||||
assert attrs["opacity"] == ""
|
||||
assert attrs["border"] == doc_engine_box_default_border()
|
||||
|
||||
html = doc_engine.render_document([element])
|
||||
assert html == (
|
||||
f'<div class="docText" data-element-id="{element_id}" data-kind="titre" '
|
||||
f'style="font-size:clamp(1.6rem,4vw,2rem); font-weight:800; line-height:1.15; '
|
||||
f'font-style:normal; text-decoration:none; text-align:left; color:var(--forge-text);">'
|
||||
f"Nouveau titre</div>"
|
||||
)
|
||||
|
||||
|
||||
def test_titre_strikethrough_combines_with_underline(tmp_support_slug_cleanup: Any) -> None:
|
||||
slug, page_id = _new_support(tmp_support_slug_cleanup)
|
||||
element_id = doc_engine.add_document_element(slug, "titre", page_id=page_id)
|
||||
doc_engine.update_document_element_attributes(
|
||||
slug, element_id, {"content": "X", "underline": True, "strikethrough": True}
|
||||
)
|
||||
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
|
||||
assert "text-decoration:underline line-through;" in html
|
||||
|
||||
|
||||
def test_titre_font_size_and_line_height_override_the_style_preset(tmp_support_slug_cleanup: Any) -> None:
|
||||
slug, page_id = _new_support(tmp_support_slug_cleanup)
|
||||
element_id = doc_engine.add_document_element(slug, "titre", page_id=page_id)
|
||||
doc_engine.update_document_element_attributes(
|
||||
slug, element_id, {"content": "X", "style": "titre1", "font_size": "40px", "line_height": "1"}
|
||||
)
|
||||
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
|
||||
assert "font-size:40px;" in html
|
||||
assert "line-height:1;" in html
|
||||
assert "clamp(1.6rem" not in html
|
||||
|
||||
|
||||
def test_titre_font_family_letter_spacing_and_text_shadow_render_when_set(tmp_support_slug_cleanup: Any) -> None:
|
||||
slug, page_id = _new_support(tmp_support_slug_cleanup)
|
||||
element_id = doc_engine.add_document_element(slug, "titre", page_id=page_id)
|
||||
doc_engine.update_document_element_attributes(
|
||||
slug,
|
||||
element_id,
|
||||
{
|
||||
"content": "X",
|
||||
"font_family": "Georgia, serif",
|
||||
"letter_spacing": "0.05em",
|
||||
"text_shadow": "1px 1px 2px rgba(0,0,0,.3)",
|
||||
},
|
||||
)
|
||||
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
|
||||
assert "font-family:Georgia, serif;" in html
|
||||
assert "letter-spacing:0.05em;" in html
|
||||
assert "text-shadow:1px 1px 2px rgba(0,0,0,.3);" in html
|
||||
|
||||
|
||||
def test_titre_text_transform_uppercase_renders_and_none_does_not(tmp_support_slug_cleanup: Any) -> None:
|
||||
slug, page_id = _new_support(tmp_support_slug_cleanup)
|
||||
element_id = doc_engine.add_document_element(slug, "titre", page_id=page_id)
|
||||
html_default = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
|
||||
assert "text-transform" not in html_default
|
||||
|
||||
doc_engine.update_document_element_attributes(slug, element_id, {"content": "X", "text_transform": "uppercase"})
|
||||
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
|
||||
assert "text-transform:uppercase;" in html
|
||||
|
||||
|
||||
def test_titre_box_style_attributes_render_via_shared_box_style(tmp_support_slug_cleanup: Any) -> None:
|
||||
slug, page_id = _new_support(tmp_support_slug_cleanup)
|
||||
element_id = doc_engine.add_document_element(slug, "titre", page_id=page_id)
|
||||
border = doc_engine_box_default_border()
|
||||
border["bottom"] = {"style": "dashed", "width": "3px", "color": "#123456"}
|
||||
doc_engine.update_document_element_attributes(
|
||||
slug,
|
||||
element_id,
|
||||
{
|
||||
"content": "X",
|
||||
"padding": "10px",
|
||||
"margin": "5px",
|
||||
"background_color": "#fafafa",
|
||||
"border_radius": "4px",
|
||||
"align_self": "center",
|
||||
"border": border,
|
||||
"height": "300px",
|
||||
"box_shadow": "0 4px 12px rgba(0,0,0,.2)",
|
||||
"opacity": "0.9",
|
||||
},
|
||||
)
|
||||
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
|
||||
assert "padding:10px;" in html
|
||||
assert "margin:5px;" in html
|
||||
assert "background-color:#fafafa;" in html
|
||||
assert "border-radius:4px;" in html
|
||||
assert "align-self:center;" in html
|
||||
assert "border-bottom:3px dashed #123456;" in html
|
||||
assert "height:300px;" in html
|
||||
assert "box-shadow:0 4px 12px rgba(0,0,0,.2);" in html
|
||||
assert "opacity:0.9;" in html
|
||||
|
||||
|
||||
def test_get_document_element_returns_none_for_missing_id(tmp_support_slug_cleanup: Any) -> None:
|
||||
slug, _page_id = _new_support(tmp_support_slug_cleanup)
|
||||
assert doc_engine.get_document_element(slug, 999) is None
|
||||
|
||||
Reference in New Issue
Block a user