Spaces:
Sleeping
Sleeping
Claude
feat(sprint-A7): WCAG niveau AA — palette daltonien + i18n résiduel + déclaration a11y
17cc547 unverified | """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 | |
| 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") | |
| 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)}" | |
| ) | |