"""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 '' in resultat assert "' in resultat assert "' in resultat assert "' 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('') == 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 '' in resultat assert '
' 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 '
' 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 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
        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 '' in resultat
        assert "" 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 "

" 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 "
    " in resultat assert "
  • " in resultat assert resultat.count("
  • ") == 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 "" in resultat assert "" 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 "

    " in resultat assert "" 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 "" in html assert "

    " 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("
    ") 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("
    ") == 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('
    " 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

    , les suivants en

    assert "" 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 '

    fiche

    ' 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 " 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)