File size: 7,231 Bytes
17cc547
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Tests Sprint A7 — accessibilité WCAG niveau AA.

Items m-1, m-2, m-5, m-6, M-9 de l'audit institutional-readiness-2026-05.

Valide le **niveau AA** :
- WCAG 1.4.3 (Contrast Minimum) — palette Okabe-Ito par défaut
- WCAG 1.4.5 / 1.4.10 — pas de chaîne de fallback FR hardcodée dans
  le JS (i18n complet pour les messages d'absence de données)
- WCAG 1.4.11 — toggle palette daltonien-friendly disponible et
  persistant via URL
- WCAG 3.1.2 — locale BCP-47 par langue dans i18n
- M-9 — ACCESSIBILITY.md publié et linké
"""

from __future__ import annotations

import json
import re

import pytest

from picarones.fixtures import generate_sample_benchmark
from picarones.report.generator import ReportGenerator


@pytest.fixture(scope="module")
def demo_html(tmp_path_factory) -> str:
    out = tmp_path_factory.mktemp("a11y_aa") / "report.html"
    bench = generate_sample_benchmark(n_docs=4)
    ReportGenerator(bench, lang="fr").generate(out)
    return out.read_text(encoding="utf-8")


@pytest.fixture(scope="module")
def demo_html_en(tmp_path_factory) -> str:
    out = tmp_path_factory.mktemp("a11y_aa_en") / "report_en.html"
    bench = generate_sample_benchmark(n_docs=4)
    ReportGenerator(bench, lang="en").generate(out)
    return out.read_text(encoding="utf-8")


# ---------------------------------------------------------------------------
# m-1 + m-2 : i18n des messages d'absence de données
# ---------------------------------------------------------------------------


def test_no_anchor_data_uses_i18n(demo_html: str) -> None:
    """``_app.js:1092`` doit utiliser ``I18N.no_anchor_data`` et non
    une chaîne FR hardcodée."""
    # On cherche le pattern d'utilisation dans le JS embarqué.
    assert "I18N.no_anchor_data" in demo_html, (
        "Le fallback du chart d'ancrage doit utiliser I18N.no_anchor_data."
    )


def test_no_gini_uses_i18n(demo_html: str) -> None:
    """``_app.js:1054`` doit utiliser ``I18N.no_gini``."""
    assert "I18N.no_gini" in demo_html


def test_no_anchor_keys_in_both_languages(
    demo_html: str, demo_html_en: str
) -> None:
    """Les clés ``no_anchor_data`` et ``no_gini`` existent dans les
    deux dictionnaires i18n embarqués."""
    for html in (demo_html, demo_html_en):
        assert re.search(r'"no_anchor_data"\s*:\s*"', html)
        assert re.search(r'"no_gini"\s*:\s*"', html)


# ---------------------------------------------------------------------------
# m-5 : palette daltonien-friendly par défaut + toggle
# ---------------------------------------------------------------------------


def test_default_palette_is_okabe_ito(demo_html: str) -> None:
    """Les hex Okabe-Ito doivent apparaître dans le HTML rendu (au
    moins une fois — ``_cer_color`` les injecte sur les badges)."""
    okabe_hex = ["#0072B2", "#F0E442", "#E69F00", "#D55E00"]
    found = [h for h in okabe_hex if h in demo_html]
    assert len(found) >= 2, (
        f"Au moins 2 couleurs Okabe-Ito attendues, trouvé : {found}"
    )


def test_classic_palette_still_available_via_module() -> None:
    """``CLASSIC_*`` reste exportable pour rétrocompat."""
    from picarones.report.colors import (
        CLASSIC_GREEN,
        CLASSIC_ORANGE,
        CLASSIC_RED,
        CLASSIC_YELLOW,
    )

    assert CLASSIC_GREEN == "#16a34a"
    assert CLASSIC_YELLOW == "#ca8a04"
    assert CLASSIC_ORANGE == "#ea580c"
    assert CLASSIC_RED == "#dc2626"


def test_palette_toggle_present_in_advanced_panel(demo_html: str) -> None:
    """Le panneau Avancé doit contenir la case à cocher de bascule
    palette."""
    assert 'id="palette-toggle-cb"' in demo_html
    assert "togglePalette" in demo_html


def test_palette_classic_class_styled(demo_html: str) -> None:
    """Le CSS doit définir le style ``body.palette-classic``
    (override de palette)."""
    assert "body.palette-classic" in demo_html


def test_palette_url_persistence(demo_html: str) -> None:
    """La fonction ``_initPaletteFromURL`` doit lire ``?palette=classic``
    au démarrage."""
    assert "_initPaletteFromURL" in demo_html
    assert "palette=classic" in demo_html or "'palette'" in demo_html


# ---------------------------------------------------------------------------
# m-6 : locale + toLocaleString
# ---------------------------------------------------------------------------


def test_locale_in_i18n_fr(demo_html: str) -> None:
    """L'i18n FR doit déclarer ``locale: "fr-FR"``."""
    assert re.search(r'"locale"\s*:\s*"fr-FR"', demo_html)


def test_locale_in_i18n_en(demo_html_en: str) -> None:
    """L'i18n EN doit déclarer ``locale: "en-GB"``."""
    assert re.search(r'"locale"\s*:\s*"en-GB"', demo_html_en)


def test_fmtnum_helper_present(demo_html: str) -> None:
    """Le helper ``fmtNum`` qui utilise ``toLocaleString(I18N.locale)``
    est défini dans le JS."""
    assert "function fmtNum" in demo_html
    assert "toLocaleString" in demo_html


# ---------------------------------------------------------------------------
# M-9 : ACCESSIBILITY.md
# ---------------------------------------------------------------------------


def test_accessibility_md_exists() -> None:
    """``ACCESSIBILITY.md`` doit exister à la racine du repo."""
    from pathlib import Path

    repo_root = Path(__file__).resolve().parents[2]
    a11y_md = repo_root / "ACCESSIBILITY.md"
    assert a11y_md.exists(), (
        "ACCESSIBILITY.md absent — pré-requis M-9 non satisfait."
    )


def test_accessibility_md_mentions_wcag_aa() -> None:
    """Le fichier doit déclarer l'engagement WCAG 2.1 AA + RGAA 4.1."""
    from pathlib import Path

    repo_root = Path(__file__).resolve().parents[2]
    text = (repo_root / "ACCESSIBILITY.md").read_text(encoding="utf-8")
    assert "WCAG 2.1" in text
    assert "AA" in text
    assert "RGAA" in text


def test_accessibility_md_lists_remediation_items() -> None:
    """Le fichier doit lister les dérogations et l'audit externe planifié."""
    from pathlib import Path

    repo_root = Path(__file__).resolve().parents[2]
    text = (repo_root / "ACCESSIBILITY.md").read_text(encoding="utf-8")
    # Doit mentionner l'audit externe (Sprint A15)
    assert "A15" in text or "audit externe" in text.lower()
    # Doit mentionner Okabe-Ito (palette validée)
    assert "Okabe-Ito" in text


# ---------------------------------------------------------------------------
# Cohérence i18n FR/EN
# ---------------------------------------------------------------------------


def test_i18n_fr_en_have_same_keys() -> None:
    """Les fichiers ``fr.json`` et ``en.json`` doivent avoir
    *exactement* le même set de clés (pas de dérive)."""
    from pathlib import Path

    repo_root = Path(__file__).resolve().parents[2]
    fr_keys = set(
        json.loads((repo_root / "picarones/report/i18n/fr.json").read_text(encoding="utf-8")).keys()
    )
    en_keys = set(
        json.loads((repo_root / "picarones/report/i18n/en.json").read_text(encoding="utf-8")).keys()
    )
    only_fr = fr_keys - en_keys
    only_en = en_keys - fr_keys
    assert not only_fr and not only_en, (
        f"Divergence i18n :\n  FR seulement: {sorted(only_fr)}\n"
        f"  EN seulement: {sorted(only_en)}"
    )