"""Tests Sprint 30 — polish, accessibilité et DX. Sprint 30 livre quatre durcissements transverses : 1. ``picarones/i18n.py`` : chargement thread-safe via verrou explicite, ``lru_cache`` sur ``get_labels``, ``reload_translations()`` exposé. 2. ``BaseOCREngine._safe_version()`` log la stacktrace en DEBUG au lieu de swallow vers ``"unknown"`` silencieusement. 3. Badges CER WCAG : icône unicode + pattern de bordure + ``aria-label`` contextuel — la couleur n'est plus la seule info visuelle. 4. Pre-commit hooks : ``.pre-commit-config.yaml`` + section ``CONTRIBUTING.md``. 5. ``CHANGELOG.md`` rattrapé Sprints 10-30. 6. ``SPECS.md`` annexé d'un addendum couvrant Sprints 16-30. """ from __future__ import annotations from pathlib import Path ROOT = Path(__file__).parent.parent.parent # --------------------------------------------------------------------------- # 1. i18n thread-safe + lru_cache # --------------------------------------------------------------------------- class TestI18nCache: def test_get_labels_returns_dict(self): from picarones.reports.i18n import get_labels labels = get_labels("fr") assert isinstance(labels, dict) assert len(labels) > 5 def test_get_labels_unknown_falls_back_to_fr(self): from picarones.reports.i18n import get_labels fr = get_labels("fr") unknown = get_labels("xx-pas-existante") # Le fallback doit être le contenu fr assert unknown == fr def test_get_labels_cached(self): from picarones.reports import i18n i18n.reload_translations() # Premier appel — peuple le cache i18n.get_labels("fr") # Inspection : le cache lru a un hit # (lru_cache expose cache_info() sur la fonction wrappée) info_before = i18n._get_labels_cached.cache_info() i18n.get_labels("fr") info_after = i18n._get_labels_cached.cache_info() assert info_after.hits > info_before.hits def test_reload_translations_clears_cache(self): from picarones.reports import i18n i18n.get_labels("fr") info_before = i18n._get_labels_cached.cache_info() assert info_before.currsize >= 1 i18n.reload_translations() info_after = i18n._get_labels_cached.cache_info() assert info_after.currsize == 0 # Section retirée au sprint H.2.d : ``BaseOCREngine._safe_version()`` # n'existe plus (BaseOCREngine supprimé avec ``adapters/legacy_engines/``). # Le contrat équivalent côté canonique (``BaseOCRAdapter`` n'a pas de # ``version()``) est testé dans # ``tests/app/test_sprint_h2b_canonical_in_runner.py`` via # ``test_canonical_adapter_version_unknown``. # --------------------------------------------------------------------------- # 3. Badges CER WCAG (rapport HTML) # --------------------------------------------------------------------------- class TestBadgesAccessibility: def test_app_js_exposes_tier_helpers(self): path = ROOT / "picarones" / "reports" / "html" / "templates" / "_app.js" src = path.read_text(encoding="utf-8") for fn in ("cerTier", "cerTierIcon", "cerTierLabel"): assert f"function {fn}" in src, ( f"_app.js doit exposer ``function {fn}`` (Sprint 30 a11y)" ) def test_styles_define_tier_patterns(self): path = ROOT / "picarones" / "reports" / "html" / "templates" / "_styles.css" src = path.read_text(encoding="utf-8") for tier in ("excellent", "acceptable", "mediocre", "critical"): assert f'data-cer-tier="{tier}"' in src, ( f"_styles.css doit définir un pattern pour le tier {tier!r}" ) # Au moins quatre styles de bordure différents assert "border: 1.5px solid" in src assert "border: 1.5px dashed" in src assert "border: 1.5px dotted" in src assert "border: 1.5px double" in src def test_main_badge_carries_data_attr_and_aria(self): path = ROOT / "picarones" / "reports" / "html" / "templates" / "_app.js" src = path.read_text(encoding="utf-8") assert "setAttribute('data-cer-tier'" in src assert "setAttribute('aria-label'" in src # --------------------------------------------------------------------------- # 4. Pre-commit + CONTRIBUTING # --------------------------------------------------------------------------- class TestPreCommitInfra: def test_pre_commit_config_exists(self): path = ROOT / ".pre-commit-config.yaml" assert path.exists() text = path.read_text(encoding="utf-8") # Doit référencer ruff (alignement avec le job CI ``lint``) assert "ruff" in text.lower() def test_pre_commit_yaml_is_well_formed(self): import yaml path = ROOT / ".pre-commit-config.yaml" data = yaml.safe_load(path.read_text(encoding="utf-8")) assert isinstance(data, dict) assert "repos" in data assert isinstance(data["repos"], list) assert any( "ruff" in (repo.get("repo") or "").lower() for repo in data["repos"] ) def test_contributing_documents_pre_commit(self): path = ROOT / "CONTRIBUTING.md" text = path.read_text(encoding="utf-8") assert "pre-commit" in text.lower() assert "pre-commit install" in text # --------------------------------------------------------------------------- # 5. Documentation rattrapée # --------------------------------------------------------------------------- class TestChangelogAndSpecsUpdated: def test_changelog_mentions_recent_sprints(self): text = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") # Backport Sprints 10-22 et 23-30 doivent être mentionnés for sprint in ("Sprint 11", "Sprint 17", "Sprint 19", "Sprint 22", "Sprint 24", "Sprint 27", "Sprint 30"): assert sprint in text, ( f"CHANGELOG.md doit mentionner {sprint} (Sprint 30 backport)" ) def test_specs_addendum_present(self): text = (ROOT / "SPECS.md").read_text(encoding="utf-8") assert "Addendum" in text # Au moins quatre des nouvelles fonctionnalités annexées for keyword in ("narrative", "Pareto", "glossaire", "snapshots"): assert keyword in text.lower() or keyword in text, ( f"SPECS.md addendum doit couvrir {keyword!r}" ) # --------------------------------------------------------------------------- # 6. Intégration : un rapport généré porte les attributs WCAG dans son JS # --------------------------------------------------------------------------- class TestGeneratedReportCarriesA11y: def test_generated_html_embeds_tier_helpers(self, tmp_path): from picarones.evaluation import synthetic as fixtures from picarones.reports.html.generator import ReportGenerator b = fixtures.generate_sample_benchmark(n_docs=4) out = tmp_path / "rapport.html" ReportGenerator(b, lang="fr").generate(out) html = out.read_text(encoding="utf-8") # Les fonctions JS doivent figurer dans le bundle inline assert "cerTier" in html assert "cerTierIcon" in html # Les règles CSS pour les patterns aussi assert 'data-cer-tier="excellent"' in html