remove_toolbar()/remove_menu() nahmen Toolbar und Menü bisher nur aus der sichtbaren Leiste, das Qt-Objekt blieb als Kind von main_window bestehen. Beim Neuladen mit dem Plugin Reloader meldete QGIS deshalb "removing duplicated widget(s) not cleaned up by the plugin during unload: LnoSachsenToolbar" - der defensive Workaround in init_ui() (Suche nach übrig gebliebenen Toolbars) kaschierte das nur beim nächsten Laden, nicht beim Unload selbst. remove_all() ruft jetzt deleteLater() auf beide Objekte, bevor die Referenzen verworfen werden - dieselbe Behandlung, die init_ui() schon für Altlasten nutzt. deleteLater() ist in PyQt5 und PyQt6 identisch, keine QGIS3/4-Fallunterscheidung nötig. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
211 lines
7.4 KiB
Python
211 lines
7.4 KiB
Python
"""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()
|
||
fake_menu, fake_toolbar = Mock(), Mock()
|
||
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)
|
||
|
||
@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()
|
||
|
||
@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()
|