"""Tests Sprint 17 — refactor du générateur HTML en templates Jinja2. Objectif : garantir que le découpage de ``_HTML_TEMPLATE`` (3100 lignes monolithiques) en templates séparés (``base.html.j2`` + 9 partials) n'a pas altéré la sortie du rapport. Après ce sprint, toute modification future doit conserver ces invariants. """ from __future__ import annotations import hashlib import json import re from pathlib import Path import pytest from picarones import fixtures from picarones.report.generator import ( ReportGenerator, _build_jinja_env, _TEMPLATES_DIR, ) # --------------------------------------------------------------------------- # Structure des fichiers attendus # --------------------------------------------------------------------------- EXPECTED_TEMPLATE_FILES = { "base.html.j2", "_header.html", "_footer.html", "_styles.css", "_app.js", "view_ranking.html", "view_gallery.html", "view_document.html", "view_analyses.html", "view_characters.html", } class TestTemplateStructure: def test_all_expected_template_files_exist(self): present = {p.name for p in _TEMPLATES_DIR.iterdir() if p.is_file()} missing = EXPECTED_TEMPLATE_FILES - present assert not missing, f"Templates manquants : {missing}" def test_jinja_env_can_load_base_template(self): env = _build_jinja_env() tpl = env.get_template("base.html.j2") assert tpl is not None def test_no_dangling_format_placeholders_in_templates(self): """Aucun {placeholder} style .format() ne doit traîner — tout doit être en syntaxe Jinja2 {{ variable }}.""" suspicious_pattern = re.compile(r"(? 10_000 # Chart.js inline à lui seul def test_report_contains_expected_markers(self, benchmark_result, tmp_path): out = tmp_path / "rapport.html" ReportGenerator(benchmark_result).generate(out) html = out.read_text(encoding="utf-8") # Structure HTML attendue assert "" in html assert "" in html assert "Picarones" in html # Les 5 vues doivent être présentes for view in ("view-ranking", "view-gallery", "view-document", "view-analyses", "view-characters"): assert f'id="{view}"' in html, f"Vue '{view}' absente du rapport" # Données embarquées assert "const DATA =" in html assert "const I18N =" in html # Chart.js inline assert "Chart.js" in html def test_report_has_no_nested_script_tags(self, benchmark_result, tmp_path): """Un bug classique du refactor : les `") assert opens == closes, f"Script tags déséquilibrés : {opens} ouvertures vs {closes} fermetures" def test_report_deterministic_given_same_data(self, benchmark_result, tmp_path): """Deux générations sur le MÊME benchmark produisent du HTML identique (garde-fou pour le moteur narratif Sprint 4 qui doit être déterministe).""" out1 = tmp_path / "r1.html" out2 = tmp_path / "r2.html" ReportGenerator(benchmark_result).generate(out1) ReportGenerator(benchmark_result).generate(out2) h1 = hashlib.sha256(out1.read_bytes()).hexdigest() h2 = hashlib.sha256(out2.read_bytes()).hexdigest() assert h1 == h2, "La génération du rapport doit être déterministe" def test_english_locale_renders(self, benchmark_result, tmp_path): out = tmp_path / "report_en.html" ReportGenerator(benchmark_result, lang="en").generate(out) html = out.read_text(encoding="utf-8") assert '' in html # --------------------------------------------------------------------------- # Chargement i18n depuis JSON # --------------------------------------------------------------------------- class TestI18nFromJSON: def test_i18n_directory_exists_and_has_json(self): i18n_dir = Path(__file__).parent.parent / "picarones" / "report" / "i18n" assert i18n_dir.is_dir() files = {p.name for p in i18n_dir.glob("*.json")} assert "fr.json" in files assert "en.json" in files def test_all_i18n_files_parse_as_json(self): i18n_dir = Path(__file__).parent.parent / "picarones" / "report" / "i18n" for f in i18n_dir.glob("*.json"): data = json.loads(f.read_text(encoding="utf-8")) assert isinstance(data, dict) assert len(data) > 50 # raisonnable : on a 101 clés def test_fr_and_en_have_same_keys(self): """Garde-fou contre les traductions manquantes.""" from picarones.i18n import TRANSLATIONS fr_keys = set(TRANSLATIONS.get("fr", {}).keys()) en_keys = set(TRANSLATIONS.get("en", {}).keys()) missing_in_en = fr_keys - en_keys missing_in_fr = en_keys - fr_keys assert not missing_in_en, f"Clés manquantes en anglais : {missing_in_en}" assert not missing_in_fr, f"Clés manquantes en français : {missing_in_fr}" def test_translations_load_via_public_api(self): from picarones.i18n import get_labels, SUPPORTED_LANGS assert "fr" in SUPPORTED_LANGS assert "en" in SUPPORTED_LANGS fr = get_labels("fr") en = get_labels("en") assert fr["html_lang"] == "fr" assert en["html_lang"] == "en" # Fallback sur fr si langue inconnue assert get_labels("xx") == fr # --------------------------------------------------------------------------- # Validation du contenu extrait (pas de régression sur le HTML rendu) # --------------------------------------------------------------------------- class TestTemplateContent: def test_css_file_contains_expected_rules(self): css = (_TEMPLATES_DIR / "_styles.css").read_text(encoding="utf-8") # Quelques règles canoniques du rapport qui doivent rester for marker in ("nav", ".cer-badge", ".gallery-card", ".tab-btn"): assert marker in css, f"Règle CSS '{marker}' manquante" def test_app_js_starts_with_use_strict(self): js = (_TEMPLATES_DIR / "_app.js").read_text(encoding="utf-8") first_nonblank = next((l for l in js.splitlines() if l.strip()), "") assert "'use strict'" in first_nonblank def test_app_js_has_no_residual_script_tag(self): """Garde-fou contre un futur refactor qui ré-inclurait par erreur.""" js = (_TEMPLATES_DIR / "_app.js").read_text(encoding="utf-8") assert "" not in js def test_view_files_contain_root_section_element(self): """Chaque vue HTML doit avoir un élément racine avec id='view-'.""" view_ids = { "view_ranking.html": "view-ranking", "view_gallery.html": "view-gallery", "view_document.html": "view-document", "view_analyses.html": "view-analyses", "view_characters.html": "view-characters", } for fname, expected_id in view_ids.items(): content = (_TEMPLATES_DIR / fname).read_text(encoding="utf-8") assert f'id="{expected_id}"' in content, ( f"{fname} devrait contenir id='{expected_id}'" )