- 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>
365 lines
12 KiB
Python
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
|