zonza/tests/test_lanceur_qt.py
Ralph Mayola aa63e94239 Un lanceur Qt pour Windows, et un assemblage partage
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 20:00:03 +02:00

125 lines
4.0 KiB
Python

"""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 "<ctrl>+<alt>+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()