- 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>
549 lines
20 KiB
Python
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)
|