"""Ce qui est testable dans le lanceur Qt SANS boucle d'événements. `ZonzaTray.__init__` construit le menu et l'icône de façon synchrone (aucun `.show()`, aucune boucle) : c'est déjà éprouvable. Le marshalage par signal Qt.QueuedConnection, lui, N'EST PAS testé ici — sa livraison dépend d'une boucle d'événements qui tourne, ce qu'aucun test de ce fichier ne fait tourner. On teste donc directement les méthodes qu'un slot appellerait (`_apply_title`, `toggle_sounds`), pas le trajet signal → file → slot. """ import pytest PySide6 = pytest.importorskip("PySide6") from PySide6.QtWidgets import QApplication # noqa: E402 from core import CONFIG # noqa: E402 from lanceur_qt import _STATE_TITLES, ZonzaTray # noqa: E402 @pytest.fixture(scope="module", autouse=True) def _app_qt(): # QIcon/QPixmap/QSystemTrayIcon exigent une QApplication déjà construite — # sans .exec() : on ne fait jamais tourner la boucle d'événements. app = QApplication.instance() or QApplication([]) yield app class _ControleurFactice: """Ni toggle() ni transcripteur ne sont appelés tant que .show() ne l'est pas — ce que ces tests évitent justement.""" def toggle(self): raise AssertionError("ne doit pas être appelé sans .show()") class _SystemeFactice: def raccourci_defaut(self): return "++d" @pytest.fixture def tray(): t = ZonzaTray(_ControleurFactice(), _SystemeFactice()) yield t # évite qu'un menu/tray fantôme traîne d'un test à l'autre (pas de .show() # appelé ici, donc rien n'a jamais été rendu visible). t.menu.deleteLater() # --- construction du menu ----------------------------------------------- def test_le_menu_contient_exactement_sons_et_quitter(): t = ZonzaTray(_ControleurFactice(), _SystemeFactice()) titres = [a.text() for a in t.menu.actions() if not a.isSeparator()] assert titres == ["Sons", "Quitter"] def test_sons_est_une_case_a_cocher(): t = ZonzaTray(_ControleurFactice(), _SystemeFactice()) assert t.sounds_action.isCheckable() def test_l_etat_initial_de_la_case_suit_la_config(): CONFIG["sounds"] = True t = ZonzaTray(_ControleurFactice(), _SystemeFactice()) assert t.sounds_action.isChecked() is True def test_le_menu_est_bien_celui_du_tray(): t = ZonzaTray(_ControleurFactice(), _SystemeFactice()) assert t.tray.contextMenu() is t.menu # --- bascule des sons ----------------------------------------------------- def test_toggle_sounds_coupe_le_son(tray): CONFIG["sounds"] = True tray.toggle_sounds(False) assert CONFIG["sounds"] is False assert tray.sounds_action.isChecked() is False def test_toggle_sounds_reactive_le_son(tray): CONFIG["sounds"] = False tray.toggle_sounds(True) assert CONFIG["sounds"] is True assert tray.sounds_action.isChecked() is True def test_toggle_sounds_ne_touche_a_rien_d_autre_dans_la_config(tray): avant = dict(CONFIG) tray.toggle_sounds(True) apres = dict(CONFIG) del avant["sounds"] del apres["sounds"] assert avant == apres # --- correspondance état → icône ------------------------------------------ def test_trois_etats_sont_couverts(): assert set(_STATE_TITLES) == {"idle", "recording", "transcribing"} def test_etat_idle_par_defaut(tray): assert tray._current_state == "idle" @pytest.mark.parametrize("etat", ["idle", "recording", "transcribing"]) def test_apply_title_memorise_l_etat_recu(tray, etat): tray._apply_title(etat) assert tray._current_state == etat def test_apply_title_pose_une_icone_non_nulle_pour_chaque_etat(tray): for etat in _STATE_TITLES: tray._apply_title(etat) assert not tray.tray.icon().isNull() def test_un_etat_inconnu_retombe_sur_l_icone_de_repos(tray): # _apply_title ne doit jamais planter sur un état imprévu — repli sur # "idle" (même garde que app.py : `_STATE_TITLES.get(state, "🎙️")`). tray._apply_title("etat-invente") assert not tray.tray.icon().isNull()