Audit complet de mise en forme — Image (2e élément du tableau)

Implémente toutes les options manquantes identifiées pour l'élément
Image : dimensionnement/ratio/object-fit, filtres CSS, upload de
fichier (en plus de l'URL), lien/plein écran au clic, chargement
différé, légende, et tous les attributs de boîte partagés déjà créés
pour Titre/Paragraphe (padding/margin/fond/bordure/ombre/opacité/
position du bloc).

Système de pages : un support peut désormais avoir 0 page (un nouveau
support démarre vide), suppression de toutes les pages en un clic, et
la pagination automatique insère intelligemment la nouvelle page juste
après celle qui déborde plutôt qu'en toute fin de liste.

Bugs réels trouvés et corrigés en cours de route : le style de bloc
(dont align-self) ciblait l'élément interne au lieu de son enveloppe
(légende/lien) ; une image à sa taille native pouvait déclencher une
pagination infinie ; upload/mise à jour d'attribut ne déclenchaient
jamais le contrôle de débordement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-26 12:06:44 +02:00
co-authored by Claude Sonnet 5
parent 6c7675fad0
commit 9ad50c58b8
27 changed files with 1299 additions and 174 deletions
+195 -2
View File
@@ -16,9 +16,14 @@ from document_engine.rendering.box_style import default_border as doc_engine_box
def _new_support(tmp_support_slug_cleanup: Any, name: str = "Support de test") -> tuple[str, int]:
# db.create_support ne crée plus aucune page par défaut (retour
# utilisateur du 26/09/2026, voir document_engine/pages/pages.md) —
# ce helper en ajoute une explicitement, puisque tous les tests de ce
# fichier portent sur des éléments qui doivent bien appartenir à une
# page existante.
slug = db.create_support(name, owner_folder="90")
tmp_support_slug_cleanup(slug)
page_id = doc_engine.list_document_pages(slug)[0]["id"]
page_id = doc_engine.add_document_page(slug)
return slug, page_id
@@ -468,7 +473,25 @@ def test_image_default_attributes_include_empty_svg_markup(tmp_support_slug_clea
element_id = doc_engine.add_document_element(slug, "image", page_id=page_id)
element = doc_engine.get_document_element(slug, element_id)
assert element is not None
assert element["attributes"] == {"src": "", "alt": "", "svg_markup": ""}
attrs = element["attributes"]
assert attrs["src"] == ""
assert attrs["alt"] == ""
assert attrs["svg_markup"] == ""
# Cadre FIXE par défaut (retour utilisateur du 26/09/2026 : "ce cadre
# ne devrait pas changer de taille en fonction de la taille de
# l'image mais être fixe et contraindre l'image dedans") — une photo
# importée est TOUJOURS rognée pour remplir ce cadre par défaut ;
# "Taille réelle" (object_fit="") reste un choix explicite possible
# via le panneau Propriétés, jamais le défaut d'un nouvel élément.
assert attrs["object_fit"] == "cover"
assert attrs["height"] == "220px"
assert attrs["aspect_ratio"] == ""
assert attrs["filter_preset"] == ""
assert attrs["click_behavior"] == ""
assert attrs["link_url"] == ""
assert attrs["lazy_load"] is False
assert attrs["caption"] == ""
assert attrs["border"] == doc_engine_box_default_border()
def test_image_with_svg_markup_takes_priority_over_src(tmp_support_slug_cleanup: Any) -> None:
@@ -499,6 +522,176 @@ def test_image_svg_markup_strips_script_tag(tmp_support_slug_cleanup: Any) -> No
assert "alert(" not in html
def test_image_object_fit_renders_only_known_values(tmp_support_slug_cleanup: Any) -> None:
slug, page_id = _new_support(tmp_support_slug_cleanup)
element_id = doc_engine.add_document_element(slug, "image", page_id=page_id)
doc_engine.update_document_element_attributes(
slug, element_id, {"src": "https://exemple.test/a.png", "alt": "", "object_fit": "cover"}
)
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
assert "object-fit:cover;" in html
doc_engine.update_document_element_attributes(
slug, element_id, {"src": "https://exemple.test/a.png", "alt": "", "object_fit": "n-importe-quoi"}
)
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
assert "object-fit" not in html
def test_image_aspect_ratio_renders_and_is_escaped(tmp_support_slug_cleanup: Any) -> None:
slug, page_id = _new_support(tmp_support_slug_cleanup)
element_id = doc_engine.add_document_element(slug, "image", page_id=page_id)
doc_engine.update_document_element_attributes(
slug, element_id, {"src": "https://exemple.test/a.png", "alt": "", "aspect_ratio": "16 / 9"}
)
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
assert "aspect-ratio:16 / 9;" in html
def test_image_filter_preset_maps_to_a_real_css_filter(tmp_support_slug_cleanup: Any) -> None:
slug, page_id = _new_support(tmp_support_slug_cleanup)
element_id = doc_engine.add_document_element(slug, "image", page_id=page_id)
doc_engine.update_document_element_attributes(
slug, element_id, {"src": "https://exemple.test/a.png", "alt": "", "filter_preset": "grayscale"}
)
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
assert "filter:grayscale(1);" in html
def test_image_lazy_load_adds_the_loading_attribute(tmp_support_slug_cleanup: Any) -> None:
slug, page_id = _new_support(tmp_support_slug_cleanup)
element_id = doc_engine.add_document_element(slug, "image", page_id=page_id)
doc_engine.update_document_element_attributes(
slug, element_id, {"src": "https://exemple.test/a.png", "alt": "", "lazy_load": True}
)
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
assert 'loading="lazy"' in html
def test_image_caption_wraps_in_a_figure(tmp_support_slug_cleanup: Any) -> None:
slug, page_id = _new_support(tmp_support_slug_cleanup)
element_id = doc_engine.add_document_element(slug, "image", page_id=page_id)
doc_engine.update_document_element_attributes(
slug, element_id, {"src": "https://exemple.test/a.png", "alt": "", "caption": "Une légende <script>x</script>"}
)
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
assert '<figure class="docImageFigure">' in html
assert '<figcaption class="docImageCaption">' in html
assert "<script>" not in html
assert "&lt;script&gt;" in html
def test_image_click_behavior_link_wraps_in_an_anchor(tmp_support_slug_cleanup: Any) -> None:
slug, page_id = _new_support(tmp_support_slug_cleanup)
element_id = doc_engine.add_document_element(slug, "image", page_id=page_id)
doc_engine.update_document_element_attributes(
slug,
element_id,
{
"src": "https://exemple.test/a.png",
"alt": "",
"click_behavior": "link",
"link_url": "https://exemple.test/cible",
},
)
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
assert (
'<a class="docImageLink" href="https://exemple.test/cible" target="_blank" rel="noopener noreferrer">' in html
)
def test_image_click_behavior_link_without_url_does_not_wrap(tmp_support_slug_cleanup: Any) -> None:
slug, page_id = _new_support(tmp_support_slug_cleanup)
element_id = doc_engine.add_document_element(slug, "image", page_id=page_id)
doc_engine.update_document_element_attributes(
slug, element_id, {"src": "https://exemple.test/a.png", "alt": "", "click_behavior": "link", "link_url": ""}
)
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
assert "docImageLink" not in html
def test_image_click_behavior_lightbox_wraps_in_a_trigger_div(tmp_support_slug_cleanup: Any) -> None:
slug, page_id = _new_support(tmp_support_slug_cleanup)
element_id = doc_engine.add_document_element(slug, "image", page_id=page_id)
doc_engine.update_document_element_attributes(
slug, element_id, {"src": "https://exemple.test/a.png", "alt": "", "click_behavior": "lightbox"}
)
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
assert '<div class="docImageLightboxTrigger">' in html
def test_image_box_style_attributes_render_and_placeholder_gets_them_too(tmp_support_slug_cleanup: Any) -> None:
slug, page_id = _new_support(tmp_support_slug_cleanup)
element_id = doc_engine.add_document_element(slug, "image", page_id=page_id)
border = doc_engine_box_default_border()
border["left"] = {"style": "dotted", "width": "2px", "color": "#00ff00"}
doc_engine.update_document_element_attributes(
slug,
element_id,
{
"src": "",
"alt": "",
"padding": "8px",
"border_radius": "20px",
"width": "300px",
"border": border,
},
)
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
assert "docImagePlaceholder" in html
assert "padding:8px;" in html
assert "border-radius:20px;" in html
assert "width:300px;" in html
assert "border-left:2px dotted #00ff00;" in html
def test_image_box_style_targets_the_figure_when_a_caption_is_present(tmp_support_slug_cleanup: Any) -> None:
# Bug réel constaté le 26/09/2026 : "la position de bloc ne fonctionne
# pas sur l'image" — align-self (et le reste de render_box_style) doit
# se poser sur l'élément RÉELLEMENT top-niveau (l'enfant direct du
# flex-column de la page), jamais sur l'<img> interne quand une
# légende l'enveloppe : sans quoi align-self n'a aucun effet visuel.
slug, page_id = _new_support(tmp_support_slug_cleanup)
element_id = doc_engine.add_document_element(slug, "image", page_id=page_id)
doc_engine.update_document_element_attributes(
slug,
element_id,
{
"src": "https://exemple.test/a.png",
"alt": "",
"caption": "Légende",
"align_self": "center",
"width": "300px",
},
)
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
assert '<figure class="docImageFigure" style="width:300px; align-self:center;">' in html
# align-self ne doit apparaître qu'UNE fois (sur la figure), jamais
# aussi sur l'<img> interne (ce qui n'aurait aucun effet CSS).
assert html.count("align-self") == 1
def test_image_box_style_targets_the_link_wrapper_when_no_caption(tmp_support_slug_cleanup: Any) -> None:
slug, page_id = _new_support(tmp_support_slug_cleanup)
element_id = doc_engine.add_document_element(slug, "image", page_id=page_id)
doc_engine.update_document_element_attributes(
slug,
element_id,
{
"src": "https://exemple.test/a.png",
"alt": "",
"click_behavior": "link",
"link_url": "https://exemple.test/cible",
"align_self": "center",
},
)
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
assert (
'<a class="docImageLink" href="https://exemple.test/cible" target="_blank" rel="noopener noreferrer" '
'style="align-self:center;">' in html
)
def test_bouton_default_attributes_include_empty_attachment_fields(tmp_support_slug_cleanup: Any) -> None:
slug, page_id = _new_support(tmp_support_slug_cleanup)
element_id = doc_engine.add_document_element(slug, "bouton", page_id=page_id)