File size: 8,789 Bytes
a0dc23e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b20a7dc
a0dc23e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
"""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"(?<!\{)\{[a-z_]+\}(?!\})")
        for tpl_file in _TEMPLATES_DIR.iterdir():
            if tpl_file.suffix in (".html", ".j2", ".css"):
                content = tpl_file.read_text(encoding="utf-8")
                matches = suspicious_pattern.findall(content)
                assert not matches, (
                    f"{tpl_file.name} contient des placeholders style .format() : {matches}"
                )


# ---------------------------------------------------------------------------
# Génération et validité du rapport
# ---------------------------------------------------------------------------

@pytest.fixture(scope="module")
def benchmark_result():
    return fixtures.generate_sample_benchmark(n_docs=3)


class TestReportGeneration:
    def test_generate_produces_file(self, benchmark_result, tmp_path):
        out = tmp_path / "rapport.html"
        gen = ReportGenerator(benchmark_result)
        result_path = gen.generate(out)
        assert result_path.exists()
        assert result_path.stat().st_size > 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 "<!DOCTYPE html>" in html
        assert "<html lang=\"fr\">" 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 `<script>` dupliqués quand on
        oublie de les retirer du contenu extrait."""
        out = tmp_path / "rapport.html"
        ReportGenerator(benchmark_result).generate(out)
        html = out.read_text(encoding="utf-8")

        # Chaque bloc script doit avoir un fermeture correspondante
        opens = html.count("<script>")
        # On tolère aussi `<script type="...">` mais on n'en utilise pas actuellement
        closes = html.count("</script>")
        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 '<html lang="en">' 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((line for line in js.splitlines() if line.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 "<script" not in js
        assert "</script>" not in js

    def test_view_files_contain_root_section_element(self):
        """Chaque vue HTML doit avoir un élément racine avec id='view-<nom>'."""
        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}'"
            )