Files
Plugin_SN_Basis/tests/test_navigation.py
T
DanielandClaude Sonnet 5 bbda2cd61b Navigation.remove_all() räumt Toolbar/Menü beim Unload tatsächlich auf (#72)
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>
2026-09-21 10:22:29 +02:00

211 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()