Code/tests/unit/test_generer.py
Stéphan Peccini 8e2556c2b0
test(unit): +381 tests unitaires — couverture 16%→35%
- 9 nouveaux fichiers de tests (persistance, translations, fiches, indices, IHH)
- Enrichissement des tests existants (graph_utils, gitea, widgets, tickets)
- 67→448 tests, tous passent

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 11:52:21 +01:00

549 lines
20 KiB
Python

"""Tests unitaires pour le module app.fiches.generer.
Ces tests vérifient les fonctions de transformation Markdown/HTML :
- remplacer_latex_par_mathml
- markdown_to_html_rgaa
- rendu_html
- render_fiche_markdown (via app.fiches.utils.fiche_utils)
"""
import re
from unittest.mock import patch
from bs4 import BeautifulSoup
from app.fiches.generer import (
markdown_to_html_rgaa,
remplacer_latex_par_mathml,
rendu_html,
)
from app.fiches.utils.fiche_utils import render_fiche_markdown
# ──────────────────────────────────────────────
# remplacer_latex_par_mathml
# ──────────────────────────────────────────────
class TestRemplacerLatexParMathml:
"""Tests pour la fonction remplacer_latex_par_mathml."""
def test_formule_inline_simple(self):
"""Test la conversion d'une formule LaTeX inline simple."""
texte = "La valeur est $x^2$ dans le calcul."
resultat = remplacer_latex_par_mathml(texte)
assert "$" not in resultat
assert '<span class="math-inline">' in resultat
assert "<math" in resultat
def test_formule_display_simple(self):
"""Test la conversion d'une formule LaTeX display (bloc)."""
texte = "Voici la formule :\n$$E = mc^2$$\nFin."
resultat = remplacer_latex_par_mathml(texte)
assert "$$" not in resultat
assert '<div class="math-block">' in resultat
assert "<math" in resultat
def test_formule_inline_fraction(self):
"""Test la conversion d'une fraction inline."""
texte = "Le ratio est $\\frac{a}{b}$ ici."
resultat = remplacer_latex_par_mathml(texte)
assert "$" not in resultat
assert '<span class="math-inline">' in resultat
assert "<math" in resultat
def test_formule_display_somme(self):
"""Test la conversion d'une somme en mode display."""
texte = "$$\\sum_{i=1}^{n} x_i$$"
resultat = remplacer_latex_par_mathml(texte)
assert "$$" not in resultat
assert '<div class="math-block">' in resultat
def test_texte_sans_formule(self):
"""Test qu'un texte sans LaTeX n'est pas modifie."""
texte = "Un texte normal sans formule."
resultat = remplacer_latex_par_mathml(texte)
assert resultat == texte
def test_formules_multiples_inline(self):
"""Test la conversion de plusieurs formules inline."""
texte = "Soit $a$ et $b$ deux variables."
resultat = remplacer_latex_par_mathml(texte)
assert resultat.count('<span class="math-inline">') == 2
assert "$" not in resultat
def test_formule_display_et_inline_combinees(self):
"""Test la combinaison de formules display et inline."""
texte = "Inline $x$ et display :\n$$y = x + 1$$\nFin."
resultat = remplacer_latex_par_mathml(texte)
assert '<span class="math-inline">' in resultat
assert '<div class="math-block">' in resultat
assert "$" not in resultat
def test_formule_display_multilignes(self):
"""Test une formule display sur plusieurs lignes."""
texte = "$$\na^2 +\nb^2 = c^2\n$$"
resultat = remplacer_latex_par_mathml(texte)
assert "$$" not in resultat
assert '<div class="math-block">' in resultat
def test_dollar_simple_non_latex(self):
"""Test que le texte entre doubles dollars est traite en display et non en inline."""
texte = "Le prix est de 100 dollars."
resultat = remplacer_latex_par_mathml(texte)
# Pas de dollar LaTeX, pas de conversion
assert resultat == texte
def test_formule_latex_invalide_inline(self):
"""Test qu'une formule LaTeX invalide renvoie un message d'erreur inline."""
texte = "Formule $\\invalid_command_xyz{}$ ici."
resultat = remplacer_latex_par_mathml(texte)
# Soit converti, soit message d'erreur encapsule dans <code>
assert "$" not in resultat or "Erreur LaTeX" in resultat
def test_formule_latex_invalide_display(self):
"""Test qu'une formule LaTeX display invalide renvoie un message d'erreur."""
texte = "$$\\invalid_command_xyz{}$$"
resultat = remplacer_latex_par_mathml(texte)
# Soit converti, soit message d'erreur encapsule dans <pre>
assert "$$" not in resultat or "Erreur LaTeX" in resultat
def test_indice_et_exposant(self):
"""Test les indices et exposants LaTeX."""
texte = "La formule $x_i^2$ est simple."
resultat = remplacer_latex_par_mathml(texte)
assert '<span class="math-inline">' in resultat
assert "<math" in resultat
def test_texte_vide(self):
"""Test avec un texte vide."""
resultat = remplacer_latex_par_mathml("")
assert resultat == ""
# ──────────────────────────────────────────────
# markdown_to_html_rgaa
# ──────────────────────────────────────────────
class TestMarkdownToHtmlRgaa:
"""Tests pour la fonction markdown_to_html_rgaa."""
def test_paragraphe_simple(self):
"""Test la conversion d'un paragraphe simple."""
md = "Un paragraphe simple."
resultat = markdown_to_html_rgaa(md, None)
assert "<p>" in resultat
assert "Un paragraphe simple." in resultat
def test_tableau_avec_caption(self):
"""Test qu'un tableau reçoit un caption RGAA."""
md = "| Col1 | Col2 |\n| --- | --- |\n| A | B |"
resultat = markdown_to_html_rgaa(md, "Mon tableau")
soup = BeautifulSoup(resultat, "html.parser")
table = soup.find("table")
assert table is not None
assert table.get("role") == "table"
assert table.get("summary") == "Mon tableau"
caption = table.find("caption")
assert caption is not None
assert caption.string == "Mon tableau"
def test_tableau_scope_col_sur_th(self):
"""Test que les en-tetes de tableau ont scope=col."""
md = "| Nom | Valeur |\n| --- | --- |\n| A | 1 |"
resultat = markdown_to_html_rgaa(md, "Donnees")
soup = BeautifulSoup(resultat, "html.parser")
for th in soup.find_all("th"):
assert th.get("scope") == "col"
def test_tableau_sans_caption(self):
"""Test un tableau sans caption (caption_text=None)."""
md = "| X | Y |\n| --- | --- |\n| 1 | 2 |"
resultat = markdown_to_html_rgaa(md, None)
soup = BeautifulSoup(resultat, "html.parser")
table = soup.find("table")
assert table is not None
assert table.get("role") == "table"
# Pas de caption quand caption_text est None
caption = table.find("caption")
assert caption is None
def test_tableau_summary_avec_caption_none(self):
"""Test que summary est vide quand caption_text est None."""
md = "| A | B |\n| --- | --- |\n| 1 | 2 |"
resultat = markdown_to_html_rgaa(md, None)
soup = BeautifulSoup(resultat, "html.parser")
table = soup.find("table")
assert table.get("summary") == ""
def test_titre_h2(self):
"""Test la conversion d'un titre de niveau 2."""
md = "## Titre niveau 2"
resultat = markdown_to_html_rgaa(md, None)
assert "<h2>" in resultat
assert "Titre niveau 2" in resultat
def test_liste_a_puces(self):
"""Test la conversion d'une liste a puces."""
md = "- item 1\n- item 2\n- item 3"
resultat = markdown_to_html_rgaa(md, None)
assert "<ul>" in resultat
assert "<li>" in resultat
assert resultat.count("<li>") == 3
def test_texte_en_gras_et_italique(self):
"""Test le formatage gras et italique."""
md = "Du texte **gras** et *italique*."
resultat = markdown_to_html_rgaa(md, None)
assert "<strong>" in resultat
assert "<em>" in resultat
def test_lien_html(self):
"""Test la conversion d'un lien Markdown."""
md = "[FabNum](https://fabnum.peccini.fr)"
resultat = markdown_to_html_rgaa(md, None)
soup = BeautifulSoup(resultat, "html.parser")
lien = soup.find("a")
assert lien is not None
assert lien.get("href") == "https://fabnum.peccini.fr"
assert lien.string == "FabNum"
def test_tableaux_multiples(self):
"""Test avec plusieurs tableaux dans le meme contenu."""
md = (
"| A | B |\n| --- | --- |\n| 1 | 2 |\n\n"
"| C | D |\n| --- | --- |\n| 3 | 4 |"
)
resultat = markdown_to_html_rgaa(md, "Donnees")
soup = BeautifulSoup(resultat, "html.parser")
tables = soup.find_all("table")
assert len(tables) == 2
for table in tables:
assert table.get("role") == "table"
def test_texte_vide(self):
"""Test avec un texte vide."""
resultat = markdown_to_html_rgaa("", None)
assert resultat == ""
def test_contenu_mixte_texte_et_tableau(self):
"""Test avec du texte et un tableau melanges."""
md = "Paragraphe avant.\n\n| X | Y |\n| --- | --- |\n| 1 | 2 |\n\nParagraphe apres."
resultat = markdown_to_html_rgaa(md, "Tableau")
assert "<p>" in resultat
assert "<table" in resultat
assert "Paragraphe avant." in resultat
assert "Paragraphe apres." in resultat
# ──────────────────────────────────────────────
# rendu_html
# ──────────────────────────────────────────────
class TestRenduHtml:
"""Tests pour la fonction rendu_html."""
def test_structure_section_region(self):
"""Test que le HTML genere contient une section avec role=region."""
md = "# Titre principal\n\nContenu intro."
resultat = rendu_html(md)
html = "\n".join(resultat)
assert '<section role="region"' in html
assert "</section>" in html
def test_titre_h1_genere(self):
"""Test que le titre h1 est genere correctement."""
md = "# Mon titre\n\nContenu."
resultat = rendu_html(md)
html = "\n".join(resultat)
assert "<h1" in html
assert "Mon titre" in html
def test_aria_labelledby_sur_section(self):
"""Test que aria-labelledby est lie au titre."""
md = "# Titre Test\n\nContenu."
resultat = rendu_html(md)
html = "\n".join(resultat)
assert 'aria-labelledby="titre-test"' in html
assert 'id="titre-test"' in html
def test_titre_id_normalise(self):
"""Test que l'ID du titre est normalise (minuscules, tirets)."""
md = "# Mon Titre Complexe !\n\nContenu."
resultat = rendu_html(md)
html = "\n".join(resultat)
# L'ID doit etre en minuscules avec des tirets, ponctuation supprimee
assert 'id="mon-titre-complexe"' in html
def test_section_n2_dans_details(self):
"""Test que les sous-sections (h2) sont dans des balises details."""
md = "# Titre\n\n## Sous-section\n\nContenu sous-section."
resultat = rendu_html(md)
html = "\n".join(resultat)
assert "<details>" in html
assert "<summary>" in html
assert "Sous-section" in html
def test_intro_avant_sections(self):
"""Test que le texte d'introduction est place avant les sous-sections."""
md = "# Titre\n\nIntroduction texte.\n\n## Section\n\nContenu."
resultat = rendu_html(md)
html = "\n".join(resultat)
idx_intro = html.find("Introduction texte")
idx_details = html.find("<details>")
assert idx_intro != -1
assert idx_details != -1
assert idx_intro < idx_details
def test_sections_multiples_n2(self):
"""Test le rendu de plusieurs sous-sections h2."""
md = "# Titre\n\n## Section A\n\nContenu A.\n\n## Section B\n\nContenu B."
resultat = rendu_html(md)
html = "\n".join(resultat)
assert html.count("<details>") == 2
assert "Section A" in html
assert "Section B" in html
def test_retour_type_liste(self):
"""Test que la fonction retourne bien une liste de chaines."""
md = "# Titre\n\nContenu."
resultat = rendu_html(md)
assert isinstance(resultat, list)
assert all(isinstance(item, str) for item in resultat)
def test_premier_element_est_section(self):
"""Test que le premier element est la balise section ouvrante."""
md = "# Titre\n\nContenu."
resultat = rendu_html(md)
assert resultat[0].startswith('<section role="region"')
def test_dernier_element_est_section_fermante(self):
"""Test que le dernier element est la balise section fermante."""
md = "# Titre\n\nContenu."
resultat = rendu_html(md)
assert resultat[-1] == "</section>"
def test_section_h1_multiples(self):
"""Test avec plusieurs titres h1 (sections de niveau 1)."""
md = "# Titre 1\n\nContenu 1.\n\n# Titre 2\n\nContenu 2."
resultat = rendu_html(md)
html = "\n".join(resultat)
# Le premier h1 est dans la balise <h1>, les suivants en <h2>
assert "<h1" in html
assert "<h2>" in html
assert "Titre 2" in html
def test_contenu_sans_titre_h1(self):
"""Test avec un contenu sans titre h1 explicite."""
md = "Juste du contenu sans titre."
resultat = rendu_html(md)
html = "\n".join(resultat)
# Doit utiliser 'fiche' comme titre par defaut
assert '<h1 id="fiche">fiche</h1>' in html
def test_latex_dans_intro_converti(self):
"""Test que le LaTeX dans l'introduction est converti en MathML."""
md = "# Titre\n\nFormule : $x^2$ dans le texte."
resultat = rendu_html(md)
html = "\n".join(resultat)
assert "$" not in html or "<math" in html
def test_latex_dans_section_n2_converti(self):
"""Test que le LaTeX dans une sous-section est converti en MathML."""
md = "# Titre\n\n## Calcul\n\nLa formule est $a + b$."
resultat = rendu_html(md)
html = "\n".join(resultat)
assert "<math" in html
def test_tableau_dans_section_n2(self):
"""Test qu'un tableau dans une sous-section est accessible."""
md = "# Titre\n\n## Donnees\n\n| A | B |\n| --- | --- |\n| 1 | 2 |"
resultat = rendu_html(md)
html = "\n".join(resultat)
soup = BeautifulSoup(html, "html.parser")
table = soup.find("table")
assert table is not None
assert table.get("role") == "table"
# ──────────────────────────────────────────────
# render_fiche_markdown
# ──────────────────────────────────────────────
class TestRenderFicheMarkdown:
"""Tests pour la fonction render_fiche_markdown (fiche_utils)."""
def _make_md(self, meta: dict, body: str) -> str:
"""Construit un document Markdown avec frontmatter YAML."""
lines = ["---"]
for k, v in meta.items():
lines.append(f"{k}: {v}")
lines.append("---")
lines.append(body)
return "\n".join(lines)
def test_titre_auto_insere(self, tmp_path):
"""Test que le titre h1 est insere automatiquement si absent."""
licence = tmp_path / "licence.md"
licence.write_text("Licence test", encoding="utf-8")
md = self._make_md(
{"indice": "Mon Indice", "indice_court": "MI"},
"Contenu de la fiche."
)
resultat = render_fiche_markdown(md, {}, license_path=str(licence))
assert resultat.startswith("# Mon Indice (MI)")
def test_titre_existant_non_duplique(self, tmp_path):
"""Test que le titre n'est pas duplique s'il est deja present."""
licence = tmp_path / "licence.md"
licence.write_text("Licence test", encoding="utf-8")
md = self._make_md(
{"indice": "Mon Indice", "indice_court": "MI"},
"# Titre existant\n\nContenu."
)
resultat = render_fiche_markdown(md, {}, license_path=str(licence))
# Ne doit pas avoir deux titres h1
h1_count = len(re.findall(r"^# ", resultat, re.M))
assert h1_count == 1
def test_licence_inseree_avant_h2(self, tmp_path):
"""Test que la licence est inseree avant le premier h2."""
licence = tmp_path / "licence.md"
licence.write_text("LICENCE_MARKER", encoding="utf-8")
md = self._make_md(
{"titre": "Fiche test"},
"# Fiche test\n\n## Section 1\n\nContenu."
)
resultat = render_fiche_markdown(md, {}, license_path=str(licence))
idx_licence = resultat.find("LICENCE_MARKER")
idx_h2 = resultat.find("## Section 1")
assert idx_licence != -1
assert idx_h2 != -1
assert idx_licence < idx_h2
def test_licence_en_fin_sans_h2(self, tmp_path):
"""Test que la licence est ajoutee a la fin s'il n'y a pas de h2."""
licence = tmp_path / "licence.md"
licence.write_text("LICENCE_FIN", encoding="utf-8")
md = self._make_md(
{"titre": "Fiche simple"},
"# Fiche simple\n\nContenu sans sous-sections."
)
resultat = render_fiche_markdown(md, {}, license_path=str(licence))
assert resultat.rstrip().endswith("LICENCE_FIN")
def test_placeholders_jinja_remplaces(self, tmp_path):
"""Test que les placeholders Jinja2 sont remplaces."""
licence = tmp_path / "licence.md"
licence.write_text("", encoding="utf-8")
md = self._make_md(
{"titre": "Test", "valeur": "42"},
"# Test\n\nLa valeur est {{ valeur }}."
)
resultat = render_fiche_markdown(md, {}, license_path=str(licence))
assert "42" in resultat
assert "{{ valeur }}" not in resultat
def test_seuils_accessibles_dans_template(self, tmp_path):
"""Test que les seuils sont accessibles dans le template Jinja2."""
licence = tmp_path / "licence.md"
licence.write_text("", encoding="utf-8")
seuils = {"ISG": {"vert": {"max": 40}}}
md = self._make_md(
{"titre": "Test"},
"# Test\n\nSeuil ISG vert max = {{ seuils.ISG.vert.max }}."
)
resultat = render_fiche_markdown(md, seuils, license_path=str(licence))
assert "40" in resultat
@patch("streamlit.error")
def test_licence_manquante_ne_plante_pas(self, mock_st_error):
"""Test que l'absence du fichier de licence ne provoque pas d'erreur."""
md = self._make_md(
{"titre": "Test"},
"# Test\n\nContenu."
)
# Chemin inexistant pour la licence
resultat = render_fiche_markdown(md, {}, license_path="/inexistant/licence.md")
# La fonction doit retourner un resultat meme sans licence
assert "Test" in resultat
assert "Contenu." in resultat
# streamlit.error doit avoir ete appele
mock_st_error.assert_called_once()
def test_migration_metadata_sheet_type(self, tmp_path):
"""Test la migration des anciennes cles YAML (sheet_type -> type_fiche)."""
licence = tmp_path / "licence.md"
licence.write_text("", encoding="utf-8")
md = self._make_md(
{"sheet_type": "indice", "titre": "Test Migration"},
"# Test Migration\n\nType = {{ type_fiche }}."
)
resultat = render_fiche_markdown(md, {}, license_path=str(licence))
assert "indice" in resultat
def test_retour_type_str(self, tmp_path):
"""Test que la fonction retourne bien une chaine."""
licence = tmp_path / "licence.md"
licence.write_text("", encoding="utf-8")
md = self._make_md({"titre": "Test"}, "# Test\n\nContenu.")
resultat = render_fiche_markdown(md, {}, license_path=str(licence))
assert isinstance(resultat, str)