Code/tests/unit/test_translations.py
Stéphan Peccini 8e2556c2b0
test(unit): +381 tests unitaires — couverture 16%→35%
- 9 nouveaux fichiers de tests (persistance, translations, fiches, indices, IHH)
- Enrichissement des tests existants (graph_utils, gitea, widgets, tickets)
- 67→448 tests, tous passent

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 11:52:21 +01:00

365 lines
12 KiB
Python

"""Tests unitaires pour le module utils.translations.
Ces tests vérifient le chargement des traductions, la résolution
hiérarchique des clés et la gestion des erreurs.
"""
import json
from unittest.mock import patch
import pytest
class _SessionState(dict):
"""Simule st.session_state en tant que dict avec accès par attribut."""
def __getattr__(self, key):
try:
return self[key]
except KeyError as err:
raise AttributeError(key) from err
def __setattr__(self, key, value):
self[key] = value
def __delattr__(self, key):
try:
del self[key]
except KeyError as err:
raise AttributeError(key) from err
class TestLoadTranslations:
"""Tests pour la fonction load_translations."""
def test_load_translations_fr(self, tmp_path, monkeypatch):
"""Test le chargement du fichier de traduction français."""
locales_dir = tmp_path / "assets" / "locales"
locales_dir.mkdir(parents=True)
translations_data = {"header": {"title": "Titre test"}}
(locales_dir / "fr.json").write_text(
json.dumps(translations_data, ensure_ascii=False), encoding="utf-8"
)
monkeypatch.chdir(tmp_path)
from utils.translations import load_translations
result = load_translations("fr")
assert result == translations_data
assert result["header"]["title"] == "Titre test"
def test_load_translations_default_lang(self, tmp_path, monkeypatch):
"""Test que la langue par défaut est le français."""
locales_dir = tmp_path / "assets" / "locales"
locales_dir.mkdir(parents=True)
translations_data = {"app": {"title": "Mon app"}}
(locales_dir / "fr.json").write_text(
json.dumps(translations_data), encoding="utf-8"
)
monkeypatch.chdir(tmp_path)
from utils.translations import load_translations
result = load_translations()
assert result == translations_data
def test_load_translations_missing_file(self, tmp_path, monkeypatch):
"""Test le retour d'un dict vide si le fichier n'existe pas."""
locales_dir = tmp_path / "assets" / "locales"
locales_dir.mkdir(parents=True)
monkeypatch.chdir(tmp_path)
from utils.translations import load_translations
result = load_translations("zz")
assert result == {}
def test_load_translations_invalid_json(self, tmp_path, monkeypatch):
"""Test le retour d'un dict vide en cas de JSON invalide."""
locales_dir = tmp_path / "assets" / "locales"
locales_dir.mkdir(parents=True)
(locales_dir / "broken.json").write_text("{invalid json!!}", encoding="utf-8")
monkeypatch.chdir(tmp_path)
from utils.translations import load_translations
result = load_translations("broken")
assert result == {}
def test_load_translations_other_lang(self, tmp_path, monkeypatch):
"""Test le chargement d'une langue autre que le français."""
locales_dir = tmp_path / "assets" / "locales"
locales_dir.mkdir(parents=True)
en_data = {"header": {"title": "English title"}}
(locales_dir / "en.json").write_text(
json.dumps(en_data), encoding="utf-8"
)
monkeypatch.chdir(tmp_path)
from utils.translations import load_translations
result = load_translations("en")
assert result == en_data
def test_load_translations_unicode(self, tmp_path, monkeypatch):
"""Test le chargement de traductions contenant des caractères Unicode."""
locales_dir = tmp_path / "assets" / "locales"
locales_dir.mkdir(parents=True)
data = {"footer": {"eco_note": "Calculs CO\u2082 via"}}
(locales_dir / "fr.json").write_text(
json.dumps(data, ensure_ascii=False), encoding="utf-8"
)
monkeypatch.chdir(tmp_path)
from utils.translations import load_translations
result = load_translations("fr")
assert "\u2082" in result["footer"]["eco_note"]
class TestGetTranslation:
"""Tests pour la fonction get_translation."""
@pytest.fixture(autouse=True)
def _setup_session_state(self):
"""Prépare un mock de st.session_state pour chaque test."""
self.mock_state = _SessionState(
{
"translations": {
"header": {"title": "Titre", "subtitle": "Sous-titre"},
"app": {"title": "Mon app", "description": "Description"},
"simple_key": "Valeur simple",
},
"lang": "fr",
}
)
with patch("utils.translations.st") as mock_st:
mock_st.session_state = self.mock_state
yield
def test_get_simple_key(self):
"""Test la récupération d'une clé simple (non hiérarchique)."""
from utils.translations import get_translation
result = get_translation("simple_key")
assert result == "Valeur simple"
def test_get_nested_key(self):
"""Test la récupération d'une clé hiérarchique (dot-separated)."""
from utils.translations import get_translation
result = get_translation("header.title")
assert result == "Titre"
def test_get_second_nested_key(self):
"""Test la récupération d'une autre clé hiérarchique."""
from utils.translations import get_translation
result = get_translation("app.description")
assert result == "Description"
def test_get_deeply_nested_key(self):
"""Test la récupération d'une clé avec trois niveaux de profondeur."""
self.mock_state["translations"]["level1"] = {
"level2": {"level3": "Profond"}
}
from utils.translations import get_translation
result = get_translation("level1.level2.level3")
assert result == "Profond"
def test_get_missing_key_returns_fallback(self):
"""Test qu'une clé manquante retourne la chaîne de repli."""
from utils.translations import get_translation
result = get_translation("inexistant.key")
assert result == "\u2297\u2907 inexistant.key \u2906\u2297"
def test_get_partial_path_returns_fallback(self):
"""Test qu'un chemin partiel incorrect retourne la chaîne de repli."""
from utils.translations import get_translation
result = get_translation("header.nonexistent")
assert result == "\u2297\u2907 header.nonexistent \u2906\u2297"
def test_get_key_traverses_non_dict(self):
"""Test le cas où la traversée atteint une valeur non-dict avant la fin."""
from utils.translations import get_translation
# "header.title" est une string, donc "header.title.extra" doit échouer
result = get_translation("header.title.extra")
assert result == "\u2297\u2907 header.title.extra \u2906\u2297"
def test_get_translation_empty_translations(self):
"""Test le retour de la chaîne de repli quand les traductions sont vides."""
self.mock_state["translations"] = {}
from utils.translations import get_translation
result = get_translation("header.title")
assert result == "\u2297\u2907 header.title \u2906\u2297"
def test_get_translation_returns_dict_for_partial_key(self):
"""Test qu'une clé partielle retourne le sous-dict correspondant."""
from utils.translations import get_translation
result = get_translation("header")
assert isinstance(result, dict)
assert result["title"] == "Titre"
def test_underscore_alias(self):
"""Test que _ est un alias pour get_translation."""
from utils.translations import _, get_translation
assert _ is get_translation
class TestGetTranslationAutoInit:
"""Tests pour l'initialisation automatique dans get_translation."""
def test_auto_init_when_no_translations(self, tmp_path, monkeypatch):
"""Test que get_translation initialise les traductions si absentes."""
locales_dir = tmp_path / "assets" / "locales"
locales_dir.mkdir(parents=True)
data = {"auto": {"init": "Valeur auto"}}
(locales_dir / "fr.json").write_text(
json.dumps(data), encoding="utf-8"
)
monkeypatch.chdir(tmp_path)
mock_state = _SessionState()
with patch("utils.translations.st") as mock_st:
mock_st.session_state = mock_state
from utils.translations import get_translation
result = get_translation("auto.init")
assert result == "Valeur auto"
assert mock_state["lang"] == "fr"
assert mock_state["translations"] == data
class TestSetLanguage:
"""Tests pour la fonction set_language."""
def test_set_language_updates_state(self, tmp_path, monkeypatch):
"""Test que set_language met à jour la langue et les traductions."""
locales_dir = tmp_path / "assets" / "locales"
locales_dir.mkdir(parents=True)
en_data = {"greeting": "Hello"}
(locales_dir / "en.json").write_text(
json.dumps(en_data), encoding="utf-8"
)
monkeypatch.chdir(tmp_path)
mock_state = _SessionState()
with patch("utils.translations.st") as mock_st:
mock_st.session_state = mock_state
from utils.translations import set_language
set_language("en")
assert mock_state["lang"] == "en"
assert mock_state["translations"] == en_data
def test_set_language_default_fr(self, tmp_path, monkeypatch):
"""Test que set_language utilise le français par défaut."""
locales_dir = tmp_path / "assets" / "locales"
locales_dir.mkdir(parents=True)
fr_data = {"bonjour": "Salut"}
(locales_dir / "fr.json").write_text(
json.dumps(fr_data), encoding="utf-8"
)
monkeypatch.chdir(tmp_path)
mock_state = _SessionState()
with patch("utils.translations.st") as mock_st:
mock_st.session_state = mock_state
from utils.translations import set_language
set_language()
assert mock_state["lang"] == "fr"
assert mock_state["translations"] == fr_data
def test_set_language_missing_file(self, tmp_path, monkeypatch):
"""Test que set_language gère un fichier manquant sans erreur."""
locales_dir = tmp_path / "assets" / "locales"
locales_dir.mkdir(parents=True)
monkeypatch.chdir(tmp_path)
mock_state = _SessionState()
with patch("utils.translations.st") as mock_st:
mock_st.session_state = mock_state
from utils.translations import set_language
set_language("xx")
assert mock_state["lang"] == "xx"
assert mock_state["translations"] == {}
class TestInitTranslations:
"""Tests pour la fonction init_translations."""
def test_init_when_no_translations(self, tmp_path, monkeypatch):
"""Test que init_translations charge le français si pas de traductions."""
locales_dir = tmp_path / "assets" / "locales"
locales_dir.mkdir(parents=True)
fr_data = {"init": "oui"}
(locales_dir / "fr.json").write_text(
json.dumps(fr_data), encoding="utf-8"
)
monkeypatch.chdir(tmp_path)
mock_state = _SessionState()
with patch("utils.translations.st") as mock_st:
mock_st.session_state = mock_state
from utils.translations import init_translations
init_translations()
assert mock_state["lang"] == "fr"
assert mock_state["translations"] == fr_data
def test_init_skips_if_already_loaded(self):
"""Test que init_translations ne recharge pas si déjà présent."""
existing = {"already": "loaded"}
mock_state = _SessionState({"translations": existing, "lang": "fr"})
with patch("utils.translations.st") as mock_st:
mock_st.session_state = mock_state
from utils.translations import init_translations
init_translations()
# Les traductions ne doivent pas avoir été modifiées
assert mock_state["translations"] is existing