2026-09-18 13:06:06 +02:00
|
|
|
|
"""Tests für sn_basis/ui/navigation.py::Navigation (Issue #67, ohne init_ui – s. Docstring dort)."""
|
|
|
|
|
|
|
|
|
|
|
|
import unittest
|
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
|
|
|
|
|
|
|
|
from sn_basis.ui.navigation import Navigation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeQAction:
|
|
|
|
|
|
def __init__(self, text, parent=None) -> None:
|
|
|
|
|
|
self.text = text
|
|
|
|
|
|
self.parent = parent
|
|
|
|
|
|
self.tooltip = None
|
|
|
|
|
|
self.checkable = None
|
|
|
|
|
|
self.checked = None
|
|
|
|
|
|
self.triggered = SimpleNamespace(connect=Mock())
|
|
|
|
|
|
|
|
|
|
|
|
def setToolTip(self, text) -> None:
|
|
|
|
|
|
self.tooltip = text
|
|
|
|
|
|
|
|
|
|
|
|
def setCheckable(self, value: bool) -> None:
|
|
|
|
|
|
self.checkable = value
|
|
|
|
|
|
|
|
|
|
|
|
def setChecked(self, value: bool) -> None:
|
|
|
|
|
|
self.checked = value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeGroup:
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
|
self.added = []
|
|
|
|
|
|
|
|
|
|
|
|
def addAction(self, action) -> None:
|
|
|
|
|
|
self.added.append(action)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeMenuOrToolbar:
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
|
self.cleared = False
|
|
|
|
|
|
self.added = []
|
|
|
|
|
|
self.removed = []
|
|
|
|
|
|
|
|
|
|
|
|
def clear(self) -> None:
|
|
|
|
|
|
self.cleared = True
|
|
|
|
|
|
self.added = []
|
|
|
|
|
|
|
|
|
|
|
|
def addAction(self, action) -> None:
|
|
|
|
|
|
self.added.append(action)
|
|
|
|
|
|
|
|
|
|
|
|
def removeAction(self, action) -> None:
|
|
|
|
|
|
self.removed.append(action)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AddActionTests(unittest.TestCase):
|
|
|
|
|
|
def test_ohne_plugin_group_liefert_none(self):
|
|
|
|
|
|
"""Prüft, dass add_action ohne initialisierte Aktionsgruppe None liefert und nichts anlegt."""
|
|
|
|
|
|
nav = Navigation()
|
|
|
|
|
|
|
|
|
|
|
|
ergebnis = nav.add_action("Text", callback=lambda: None)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertIsNone(ergebnis)
|
|
|
|
|
|
self.assertEqual(nav.actions, [])
|
|
|
|
|
|
|
|
|
|
|
|
@patch("sn_basis.ui.navigation.get_main_window", return_value=object())
|
|
|
|
|
|
@patch("sn_basis.ui.navigation.QAction", _FakeQAction)
|
|
|
|
|
|
def test_erzeugt_action_und_fuegt_sie_gruppe_und_liste_hinzu(self, _mock_window):
|
|
|
|
|
|
"""Prüft, dass add_action eine Action erzeugt, ihr die Gruppe zuweist und sie priorisiert in der Liste ablegt."""
|
|
|
|
|
|
nav = Navigation()
|
|
|
|
|
|
nav.plugin_group = _FakeGroup()
|
|
|
|
|
|
callback = Mock()
|
|
|
|
|
|
|
|
|
|
|
|
action = nav.add_action("Mein Text", callback, tooltip="Tooltip", priority=5)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(action.text, "Mein Text")
|
|
|
|
|
|
self.assertEqual(action.tooltip, "Tooltip")
|
|
|
|
|
|
self.assertTrue(action.checkable)
|
|
|
|
|
|
action.triggered.connect.assert_called_once_with(callback)
|
|
|
|
|
|
self.assertIn(action, nav.plugin_group.added)
|
|
|
|
|
|
self.assertEqual(nav.actions, [(5, action)])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FinalizeMenuAndToolbarTests(unittest.TestCase):
|
|
|
|
|
|
def test_ohne_menu_oder_toolbar_tut_nichts(self):
|
|
|
|
|
|
"""Prüft, dass finalize_menu_and_toolbar ohne Menü oder Toolbar keine Ausnahme wirft."""
|
|
|
|
|
|
nav = Navigation()
|
|
|
|
|
|
|
|
|
|
|
|
nav.finalize_menu_and_toolbar() # darf keine Ausnahme werfen
|
|
|
|
|
|
|
|
|
|
|
|
def test_sortiert_und_fuegt_aktionen_in_prioritaetsreihenfolge_hinzu(self):
|
|
|
|
|
|
"""Prüft, dass finalize_menu_and_toolbar Aktionen nach Priorität sortiert in Menü und Toolbar einträgt."""
|
|
|
|
|
|
nav = Navigation()
|
|
|
|
|
|
nav.menu = _FakeMenuOrToolbar()
|
|
|
|
|
|
nav.toolbar = _FakeMenuOrToolbar()
|
|
|
|
|
|
nav.actions = [(20, "a2"), (10, "a1")]
|
|
|
|
|
|
|
|
|
|
|
|
nav.finalize_menu_and_toolbar()
|
|
|
|
|
|
|
|
|
|
|
|
self.assertTrue(nav.menu.cleared)
|
|
|
|
|
|
self.assertTrue(nav.toolbar.cleared)
|
|
|
|
|
|
self.assertEqual(nav.menu.added, ["a1", "a2"])
|
|
|
|
|
|
self.assertEqual(nav.toolbar.added, ["a1", "a2"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SetActivePluginTests(unittest.TestCase):
|
|
|
|
|
|
def test_setzt_nur_aktive_action_auf_checked(self):
|
|
|
|
|
|
"""Prüft, dass set_active_plugin nur die übergebene Action markiert, alle anderen abwählt."""
|
|
|
|
|
|
action1, action2 = _FakeQAction("a1"), _FakeQAction("a2")
|
|
|
|
|
|
nav = Navigation()
|
|
|
|
|
|
nav.actions = [(1, action1), (2, action2)]
|
|
|
|
|
|
|
|
|
|
|
|
nav.set_active_plugin(action2)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertFalse(action1.checked)
|
|
|
|
|
|
self.assertTrue(action2.checked)
|
|
|
|
|
|
|
|
|
|
|
|
def test_ohne_aktive_action_setzt_alle_auf_false(self):
|
|
|
|
|
|
"""Prüft, dass set_active_plugin ohne aktive Action alle bestehenden Actions abwählt."""
|
|
|
|
|
|
action1 = _FakeQAction("a1")
|
|
|
|
|
|
nav = Navigation()
|
|
|
|
|
|
nav.actions = [(1, action1)]
|
|
|
|
|
|
|
|
|
|
|
|
nav.set_active_plugin(None) # darf keine Ausnahme werfen
|
|
|
|
|
|
|
|
|
|
|
|
self.assertFalse(action1.checked)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RemoveActionTests(unittest.TestCase):
|
|
|
|
|
|
def test_none_action_tut_nichts(self):
|
|
|
|
|
|
"""Prüft, dass remove_action mit None keine Ausnahme wirft und die Liste unverändert lässt."""
|
|
|
|
|
|
nav = Navigation()
|
|
|
|
|
|
nav.actions = [(1, "a1")]
|
|
|
|
|
|
|
|
|
|
|
|
nav.remove_action(None)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(nav.actions, [(1, "a1")])
|
|
|
|
|
|
|
|
|
|
|
|
def test_entfernt_action_aus_menu_toolbar_und_liste(self):
|
|
|
|
|
|
"""Prüft, dass remove_action die Action aus Menü, Toolbar und der internen Liste entfernt."""
|
|
|
|
|
|
nav = Navigation()
|
|
|
|
|
|
nav.menu = _FakeMenuOrToolbar()
|
|
|
|
|
|
nav.toolbar = _FakeMenuOrToolbar()
|
|
|
|
|
|
nav.actions = [(1, "a1"), (2, "a2")]
|
|
|
|
|
|
|
|
|
|
|
|
nav.remove_action("a1")
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(nav.menu.removed, ["a1"])
|
|
|
|
|
|
self.assertEqual(nav.toolbar.removed, ["a1"])
|
|
|
|
|
|
self.assertEqual(nav.actions, [(2, "a2")])
|
|
|
|
|
|
|
|
|
|
|
|
def test_ohne_menu_oder_toolbar_entfernt_trotzdem_aus_liste(self):
|
|
|
|
|
|
"""Prüft, dass remove_action ohne Menü/Toolbar trotzdem aus der internen Liste entfernt."""
|
|
|
|
|
|
nav = Navigation()
|
|
|
|
|
|
nav.actions = [(1, "a1")]
|
|
|
|
|
|
|
|
|
|
|
|
nav.remove_action("a1")
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(nav.actions, [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RemoveAllTests(unittest.TestCase):
|
|
|
|
|
|
@patch("sn_basis.ui.navigation.remove_toolbar")
|
|
|
|
|
|
@patch("sn_basis.ui.navigation.remove_menu")
|
|
|
|
|
|
def test_entfernt_menu_toolbar_und_setzt_zustand_zurueck(self, mock_remove_menu, mock_remove_toolbar):
|
|
|
|
|
|
"""Prüft, dass remove_all Menü und Toolbar über die Wrapper entfernt und den Navigationszustand zurücksetzt."""
|
|
|
|
|
|
nav = Navigation()
|
2026-09-21 10:22:29 +02:00
|
|
|
|
fake_menu, fake_toolbar = Mock(), Mock()
|
2026-09-18 13:06:06 +02:00
|
|
|
|
nav.menu = fake_menu
|
|
|
|
|
|
nav.toolbar = fake_toolbar
|
|
|
|
|
|
nav.actions = [(1, "a1")]
|
|
|
|
|
|
nav.plugin_group = object()
|
|
|
|
|
|
|
|
|
|
|
|
nav.remove_all()
|
|
|
|
|
|
|
|
|
|
|
|
mock_remove_menu.assert_called_once_with(fake_menu)
|
|
|
|
|
|
mock_remove_toolbar.assert_called_once_with(fake_toolbar)
|
|
|
|
|
|
self.assertIsNone(nav.menu)
|
|
|
|
|
|
self.assertIsNone(nav.toolbar)
|
|
|
|
|
|
self.assertEqual(nav.actions, [])
|
|
|
|
|
|
self.assertIsNone(nav.plugin_group)
|
|
|
|
|
|
|
2026-09-21 10:22:29 +02:00
|
|
|
|
@patch("sn_basis.ui.navigation.remove_toolbar")
|
|
|
|
|
|
@patch("sn_basis.ui.navigation.remove_menu")
|
|
|
|
|
|
def test_loescht_menu_und_toolbar_objekt_damit_kein_duplikat_uebrig_bleibt(self, mock_remove_menu, mock_remove_toolbar):
|
|
|
|
|
|
"""Prüft, dass remove_all deleteLater() auf Menü und Toolbar aufruft, damit sie den Qt-Objektbaum tatsächlich verlassen (Issue #72)."""
|
|
|
|
|
|
nav = Navigation()
|
|
|
|
|
|
fake_menu, fake_toolbar = Mock(), Mock()
|
|
|
|
|
|
nav.menu = fake_menu
|
|
|
|
|
|
nav.toolbar = fake_toolbar
|
|
|
|
|
|
|
|
|
|
|
|
nav.remove_all()
|
|
|
|
|
|
|
|
|
|
|
|
fake_menu.deleteLater.assert_called_once()
|
|
|
|
|
|
fake_toolbar.deleteLater.assert_called_once()
|
|
|
|
|
|
|
2026-09-18 13:06:06 +02:00
|
|
|
|
@patch("sn_basis.ui.navigation.remove_toolbar")
|
|
|
|
|
|
@patch("sn_basis.ui.navigation.remove_menu")
|
|
|
|
|
|
def test_ohne_menu_und_toolbar_ruft_wrapper_nicht_auf(self, mock_remove_menu, mock_remove_toolbar):
|
|
|
|
|
|
"""Prüft, dass remove_all ohne vorhandenes Menü/Toolbar die Wrapper nicht aufruft, aber die Liste leert."""
|
|
|
|
|
|
nav = Navigation()
|
|
|
|
|
|
nav.actions = [(1, "a1")]
|
|
|
|
|
|
|
|
|
|
|
|
nav.remove_all()
|
|
|
|
|
|
|
|
|
|
|
|
mock_remove_menu.assert_not_called()
|
|
|
|
|
|
mock_remove_toolbar.assert_not_called()
|
|
|
|
|
|
self.assertEqual(nav.actions, [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
unittest.main()
|