"""Tests du guide téléchargeable et de sa couverture (#105, BUG-067). - Le Markdown/PDF du guide se génèrent depuis index.html + les locales. - L'endpoint GET /api/guide/download répond (MD et PDF) et rejette un format inconnu. - Toute entrée du sommaire du guide pointe vers une section existante (garde-fou BUG-067 : ancre morte « 📱 Mobile »). - Parité FR/EN des clés guide105.* et présence de toutes les clés utilisées. """ from __future__ import annotations import json import re from pathlib import Path import pytest ROOT = Path(__file__).resolve().parent.parent INDEX_HTML = ROOT / "frontend" / "index.html" LOCALES = ROOT / "frontend" / "locales" @pytest.fixture(scope="module") def index_html() -> str: return INDEX_HTML.read_text(encoding="utf-8") @pytest.fixture(scope="module") def guide_toc_nav(index_html: str) -> list[str]: """href #help-* de la liste du sommaire du GUIDE (pas celui de config).""" a = index_html.index('id="help-nav-list"') b = index_html.index("", a) return re.findall(r'href="#(help-[a-z-]+)"', index_html[a:b]) @pytest.fixture(scope="module") def guide_section_ids(index_html: str) -> set[str]: # La balise
de help-personnalisation casse la ligne entre # class= et id= : matcher les deux attributs quel que soit l'ordre. out: set[str] = set() for m in re.finditer(r"]*?>", index_html, re.S): tag = m.group(0) if 'class="help-section' not in tag.replace("\n", " ").replace(" ", " "): continue idm = re.search(r'id="(help-[a-z-]+)"', tag) if idm: out.add(idm.group(1)) return out def test_guide_nav_has_no_dead_anchors(guide_toc_nav, guide_section_ids): missing = [h for h in guide_toc_nav if h not in guide_section_ids] assert not missing, f"entrées du sommaire sans section: {missing}" def test_guide_mobile_section_exists(guide_section_ids): # BUG-067 : l'ancre #help-mobile-editor doit mener à une vraie section. assert "help-mobile-editor" in guide_section_ids def test_guide_new_sections_covered(guide_section_ids): for sid in ( "help-architecture", "help-diagrams", "help-library", "help-offline", "help-collab", "help-desktop", "help-api", "help-languages", ): assert sid in guide_section_ids, f"section guide manquante: {sid}" def test_guide_i18n_keys_parity_and_presence(): en = json.loads((LOCALES / "en.json").read_bytes().decode("utf-8")) fr = json.loads((LOCALES / "fr.json").read_bytes().decode("utf-8")) keys_en = {k for k in en if k.startswith("guide105.")} keys_fr = {k for k in fr if k.startswith("guide105.")} assert keys_en == keys_fr and len(keys_en) > 80 used = set(re.findall(r'data-i18n(?:-attr="title:)?="?(guide105\.[a-z0-9_]+)"?', INDEX_HTML.read_text(encoding="utf-8"))) used |= {m.split(":", 1)[1] for m in re.findall(r'data-i18n-attr="(title:guide105\.[a-z0-9_]+)"', INDEX_HTML.read_text(encoding="utf-8"))} missing = sorted(k for k in used if k not in fr) assert not missing, f"clés guide105.* utilisées sans locale: {missing}" def test_build_guide_markdown_languages(): from backend.guide_export import build_guide_markdown md_fr = build_guide_markdown("fr").decode("utf-8") md_en = build_guide_markdown("en").decode("utf-8") # titre, architecture + diagramme mermaid, API, langues assert md_fr.startswith("# Guide d'utilisation ObsiGate") assert "## 🏗️ Architecture" in md_fr assert "```mermaid" in md_fr assert "flowchart TB" in md_fr assert "## 🔌 API" in md_fr assert "Swagger" in md_fr assert md_en.startswith("# ObsiGate User Guide") assert "## 🏗️ Architecture" in md_en # le markdown FR ne contient pas de balises HTML résiduelles hors assert "" not in md_fr and " 20_000 def test_guide_pdf_embeds_rendered_diagram(): # #105 feedback : le bloc Mermaid de la section Architecture doit devenir # une VRAIE image dans le PDF (PNG pré-rendu commité), pas du code brut. import re from backend.guide_export import build_guide_html html = build_guide_html("fr") imgs = re.findall(r'