feat(gui): addenda Dom GUI V6 — sous-onglet Profils, libellés, aide, bêta
Suite des retours Dom sur la GUI V6 (par-dessus 6a0a581).
Addendum Profils / Réglages :
- Nouveau sous-onglet Administration « 👤 Profils » : le profil actif devient
un objet lisible (nom, description, masque requis, template, listes locales
avec compteurs) — données réelles lues depuis profile_defaults.
- Fenêtre « Tableau des termes » (terms_table_window.py) : table scrollable
avec recherche/filtre, colonnes Type/Terme/Source ; reste lisible à 50+
termes. Ajouter/éditer/supprimer désactivés « (à venir) » (écriture par
profil non câblée).
- Réglages : « Profil métier » → « Profil d'anonymisation », « Sortie… » →
« Dossier de sortie… » (+ infobulle), hints moteurs (standard/optionnel/
plus lent), bouton « Voir le profil », « Ouvrir le tableau des termes ».
- Aide « ? » + infobulles (ui_kit.attach_tooltip) près des éléments ambigus.
- profile_view.py : logique pure (résumé profil + lignes du tableau),
testable sans display.
Addendum bêta : en-tête « aivanonym » + badge « bêta », titre fenêtre
« … — bêta ». Détail version conservé dans À propos.
tests/unit/test_gui_v6_profiles.py + ajouts shell. 237 tests unit OK
(228 → 237, 0 régression), self-test GUI V6 OK, navigation des 5 sous-onglets
+ thème OK. V5/moteur/app_aivanov/profile_defaults non touchés, 0 dépendance.
Aucun build/push sans GO Dom — validation visuelle Dom attendue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
160
tests/unit/test_gui_v6_profiles.py
Normal file
160
tests/unit/test_gui_v6_profiles.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""Vue lisible d'un profil d'anonymisation (logique pure, sans display).
|
||||
|
||||
Sous-tend le sous-onglet « Profils » et la fenêtre « Tableau des termes ».
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from gui_v6.profile_view import (
|
||||
filter_term_rows,
|
||||
profile_term_rows,
|
||||
summarize_profile,
|
||||
)
|
||||
|
||||
_PROFILE = {
|
||||
"label": "Standard local",
|
||||
"description": "Profil par défaut.",
|
||||
"require_manual_mask": True,
|
||||
"force_disable_vlm": True,
|
||||
"preferred_manual_mask_template": "config/mask_templates/x.json",
|
||||
"param_lists": {
|
||||
"whitelist_phrases": ["classification internationale", "prise en charge"],
|
||||
"blacklist_force_mask_terms": ["CHUXX"],
|
||||
"additional_stopwords": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_summarize_profile_reads_real_fields():
|
||||
s = summarize_profile("standard_local", _PROFILE)
|
||||
assert s.key == "standard_local"
|
||||
assert s.label == "Standard local"
|
||||
assert s.description == "Profil par défaut."
|
||||
assert s.require_manual_mask is True
|
||||
assert s.disable_vlm is True
|
||||
assert s.mask_template == "config/mask_templates/x.json"
|
||||
assert s.list_counts == {"À conserver": 2, "À masquer": 1, "À ignorer": 0}
|
||||
|
||||
|
||||
def test_summarize_profile_tolerates_empty():
|
||||
s = summarize_profile("vide", {})
|
||||
assert s.label == "vide"
|
||||
assert s.description == ""
|
||||
assert s.require_manual_mask is False
|
||||
assert s.mask_template == ""
|
||||
assert s.list_counts == {"À conserver": 0, "À masquer": 0, "À ignorer": 0}
|
||||
|
||||
s2 = summarize_profile("none", None)
|
||||
assert s2.list_counts["À masquer"] == 0
|
||||
|
||||
|
||||
def test_profile_term_rows_type_term_source():
|
||||
rows = profile_term_rows(_PROFILE)
|
||||
assert ("À conserver", "classification internationale", "Standard local") in rows
|
||||
assert ("À masquer", "CHUXX", "Standard local") in rows
|
||||
# 2 whitelist + 1 blacklist + 0 stopwords
|
||||
assert len(rows) == 3
|
||||
|
||||
|
||||
def test_filter_term_rows_by_query():
|
||||
rows = profile_term_rows(_PROFILE)
|
||||
assert len(filter_term_rows(rows, "")) == 3
|
||||
assert filter_term_rows(rows, "chuxx") == [("À masquer", "CHUXX", "Standard local")]
|
||||
assert filter_term_rows(rows, "conserver") == [
|
||||
r for r in rows if r[0] == "À conserver"
|
||||
]
|
||||
assert filter_term_rows(rows, "zzz") == []
|
||||
|
||||
|
||||
# --- Smokes headless (fenêtre tableau + infobulle) --------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def ctk_root():
|
||||
ctk = pytest.importorskip("customtkinter")
|
||||
try:
|
||||
root = ctk.CTk()
|
||||
except Exception as exc:
|
||||
pytest.skip(f"display Tk indisponible: {exc}")
|
||||
root.withdraw()
|
||||
try:
|
||||
yield root
|
||||
finally:
|
||||
try:
|
||||
root.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_terms_table_window_filters_and_disables_add(ctk_root):
|
||||
from gui_v6 import theme as theme_mod
|
||||
from gui_v6.terms_table_window import TermsTableWindow
|
||||
|
||||
p = theme_mod.get_palette(theme_mod.DEFAULT_THEME)
|
||||
win = TermsTableWindow(ctk_root, p, profile_term_rows(_PROFILE), profile_label="Standard local")
|
||||
ctk_root.update_idletasks()
|
||||
assert win.visible_count() == 3
|
||||
assert win.add_is_disabled() # action non câblée → désactivée
|
||||
win.set_query("chuxx")
|
||||
assert win.visible_count() == 1
|
||||
win.set_query("")
|
||||
assert win.visible_count() == 3
|
||||
win.destroy()
|
||||
|
||||
|
||||
def test_attach_tooltip_does_not_break_widget(ctk_root):
|
||||
import customtkinter as ctk
|
||||
|
||||
from gui_v6 import ui_kit
|
||||
|
||||
lbl = ctk.CTkLabel(ctk_root, text="x")
|
||||
lbl.pack()
|
||||
ctk_root.update_idletasks()
|
||||
tip = ui_kit.attach_tooltip(lbl, "aide contextuelle")
|
||||
tip.show()
|
||||
ctk_root.update_idletasks()
|
||||
tip.hide()
|
||||
assert lbl.winfo_exists()
|
||||
|
||||
|
||||
def test_subtabs_include_profils():
|
||||
from gui_v6.tabs.tab_config import _SUBTABS
|
||||
|
||||
keys = [k for k, _ in _SUBTABS]
|
||||
labels = [lbl for _, lbl in _SUBTABS]
|
||||
assert "pro" in keys
|
||||
assert any("Profils" in lbl for lbl in labels)
|
||||
|
||||
|
||||
def _all_texts(widget):
|
||||
out = []
|
||||
try:
|
||||
out.append(str(widget.cget("text")))
|
||||
except Exception:
|
||||
pass
|
||||
for child in widget.winfo_children():
|
||||
out += _all_texts(child)
|
||||
return out
|
||||
|
||||
|
||||
def test_reglages_labels_renamed_and_profile_readable(ctk_root, tmp_path, monkeypatch):
|
||||
from gui_v6.tabs import tab_config
|
||||
|
||||
monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path)
|
||||
tab = tab_config.ConfigTab(ctk_root)
|
||||
tab.update_idletasks()
|
||||
|
||||
texts = " | ".join(_all_texts(tab))
|
||||
assert "Profil d'anonymisation" in texts # addendum : renommage
|
||||
assert "Profil métier" not in texts
|
||||
assert "Dossier de sortie" in texts # addendum : « Sortie… » clarifié
|
||||
|
||||
# profil lisible : résumé avec les 3 listes
|
||||
summary = tab._active_profile_summary()
|
||||
assert set(summary.list_counts.keys()) == {"À conserver", "À masquer", "À ignorer"}
|
||||
|
||||
# tableau des termes ouvrable sans erreur
|
||||
tab._open_terms_table()
|
||||
tab.update_idletasks()
|
||||
tab.destroy()
|
||||
Reference in New Issue
Block a user