"""Tests für sn_basis/functions/verfahrensgebiet_manager.py (zentrale Layer-Verfolgung, Issue #65).""" import unittest from unittest.mock import patch from sn_basis.functions import verfahrensgebiet_manager import sn_basis.functions.verfahrensgebiet_manager as vgm class _Scope: def __init__(self, value: str): self._value = value def variable(self, _name: str) -> str: return self._value class TestVerfahrensgebietManager(unittest.TestCase): @patch("sn_basis.functions.verfahrensgebiet_manager.get_variable", return_value="layer_123") def test_get_verfahrensgebiet_layer_id_returns_value(self, _mock_get_variable): """Prüft, dass get_verfahrensgebiet_layer_id() die in der Projektvariable gespeicherte Layer-ID liefert.""" result = verfahrensgebiet_manager.get_verfahrensgebiet_layer_id() self.assertEqual(result, "layer_123") @patch("sn_basis.functions.verfahrensgebiet_manager.get_variable", return_value="") def test_get_verfahrensgebiet_layer_id_returns_none_for_empty_value(self, _mock_get_variable): """Prüft, dass get_verfahrensgebiet_layer_id() bei leerer Projektvariable None statt eines leeren Strings liefert.""" result = verfahrensgebiet_manager.get_verfahrensgebiet_layer_id() self.assertIsNone(result) @patch("sn_basis.functions.verfahrensgebiet_manager.set_variable") def test_set_verfahrensgebiet_layer_id_writes_project_variable(self, mock_set_variable): """Prüft, dass set_verfahrensgebiet_layer_id() die Layer-ID als Projektvariable 'verfahrensgebiet_layer' speichert.""" verfahrensgebiet_manager.set_verfahrensgebiet_layer_id("layer_abc") mock_set_variable.assert_called_once_with("verfahrensgebiet_layer", "layer_abc", scope="project") @patch("sn_basis.functions.verfahrensgebiet_manager._find_verfahrensgebiet_layer") @patch("sn_basis.functions.variable_wrapper.QgsProject.instance") @patch("sn_basis.functions.variable_wrapper.QgsExpressionContextUtils.setProjectVariable") @patch("sn_basis.functions.variable_wrapper.QgsExpressionContextUtils.projectScope") def test_update_without_layer_does_not_write_when_already_empty( self, mock_project_scope, mock_set_project_variable, mock_project_instance, mock_find_layer, ): """Prüft, dass ohne gefundenen Verfahrensgebiet-Layer keine Schreibaktion erfolgt, wenn die Variable bereits leer ist.""" mock_find_layer.return_value = (None, None) mock_project_instance.return_value = object() mock_project_scope.return_value = _Scope("") result = verfahrensgebiet_manager._update_verfahrensgebiet_from_project() self.assertFalse(result) mock_set_project_variable.assert_not_called() @patch("sn_basis.functions.verfahrensgebiet_manager._find_verfahrensgebiet_layer") @patch("sn_basis.functions.verfahrensgebiet_manager.set_variable") def test_update_with_layer_writes_variable(self, mock_set_variable, mock_find_layer): """Prüft, dass ein gefundener Verfahrensgebiet-Layer seine ID als Projektvariable speichert.""" mock_layer = object() mock_find_layer.return_value = (mock_layer, "layer_99") result = verfahrensgebiet_manager._update_verfahrensgebiet_from_project() self.assertTrue(result) mock_set_variable.assert_called_once_with("verfahrensgebiet_layer", "layer_99", scope="project") class _FakeSignal: def __init__(self) -> None: self.connected: list = [] def connect(self, fn) -> None: self.connected.append(fn) def disconnect(self, fn) -> None: self.connected.remove(fn) class _FakeLayer: def __init__(self, layer_id: str, name: str = "Verfahrensgebiet") -> None: self._id = layer_id self._name = name def id(self) -> str: return self._id class _FakeProject: def __init__(self, layers=None) -> None: self.layersAdded = _FakeSignal() self.layerRemoved = _FakeSignal() self.readProject = _FakeSignal() self.newProjectCreated = _FakeSignal() self.cleared = _FakeSignal() self._layers = layers or [] def mapLayersByName(self, name: str): return [l for l in self._layers if l._name == name] def _mit_projekt(projekt: _FakeProject): return patch.object(vgm, "QgsProject", type("_P", (), {"instance": staticmethod(lambda: projekt)})) class OnHandlerTests(unittest.TestCase): def setUp(self) -> None: patch.object(vgm, "QGIS_AVAILABLE", True).start() self.addCleanup(patch.stopall) vgm.set_variable("verfahrensgebiet_layer", "", scope="project") def test_on_project_changed_ohne_layer_setzt_variable_leer(self): """Prüft, dass _on_project_changed die Variable leert, wenn kein Verfahrensgebiet-Layer existiert.""" vgm.set_variable("verfahrensgebiet_layer", "alter_layer", scope="project") with _mit_projekt(_FakeProject(layers=[])): vgm._on_project_changed() self.assertIsNone(vgm.get_verfahrensgebiet_layer_id()) def test_on_project_changed_mit_layer_setzt_variable(self): """Prüft, dass _on_project_changed die Layer-ID eines gefundenen Verfahrensgebiet-Layers übernimmt.""" with _mit_projekt(_FakeProject(layers=[_FakeLayer("layer123")])): vgm._on_project_changed() self.assertEqual(vgm.get_verfahrensgebiet_layer_id(), "layer123") def test_on_layers_added_findet_neuen_layer(self): """Prüft, dass _on_layers_added nach neu hinzugefügten Layern sucht und die Variable aktualisiert.""" with _mit_projekt(_FakeProject(layers=[_FakeLayer("layer456")])): vgm._on_layers_added(["irrelevant"]) self.assertEqual(vgm.get_verfahrensgebiet_layer_id(), "layer456") @patch.object(vgm, "_update_verfahrensgebiet_from_project") def test_on_layer_removed_bei_aktuellem_layer_sucht_neu(self, mock_update): """Prüft, dass das Entfernen des aktuell aktiven Verfahrensgebiet-Layers eine Neusuche auslöst.""" vgm.set_variable("verfahrensgebiet_layer", "layer123", scope="project") vgm._on_layer_removed("layer123") mock_update.assert_called_once() @patch.object(vgm, "_update_verfahrensgebiet_from_project") def test_on_layer_removed_bei_anderem_layer_aendert_nichts(self, mock_update): """Prüft, dass das Entfernen eines anderen Layers keine Neusuche auslöst.""" vgm.set_variable("verfahrensgebiet_layer", "layer123", scope="project") vgm._on_layer_removed("ganz_anderer_layer") mock_update.assert_not_called() class ListenerSetupTests(unittest.TestCase): def setUp(self) -> None: patch.object(vgm, "_listener_installed", False).start() self.addCleanup(patch.stopall) def test_setup_ohne_qgis_tut_nichts(self): """Prüft, dass setup_verfahrensgebiet_listener ohne QGIS keine Signale registriert.""" with patch.object(vgm, "QGIS_AVAILABLE", False): vgm.setup_verfahrensgebiet_listener() self.assertFalse(vgm._listener_installed) def test_setup_verbindet_signale_und_ist_idempotent(self): """Prüft, dass setup_verfahrensgebiet_listener die Projektsignale verbindet und ein zweiter Aufruf nichts doppelt registriert.""" projekt = _FakeProject(layers=[]) with patch.object(vgm, "QGIS_AVAILABLE", True), _mit_projekt(projekt): vgm.setup_verfahrensgebiet_listener() vgm.setup_verfahrensgebiet_listener() self.assertEqual(len(projekt.layersAdded.connected), 1) self.assertEqual(len(projekt.layerRemoved.connected), 1) self.assertEqual(len(projekt.readProject.connected), 1) self.assertTrue(vgm._listener_installed) def test_teardown_trennt_alle_signale_und_ist_idempotent(self): """Prüft, dass teardown_verfahrensgebiet_listener alle Signale trennt und mehrfacher Aufruf nicht fehlschlägt.""" projekt = _FakeProject(layers=[]) with patch.object(vgm, "QGIS_AVAILABLE", True), _mit_projekt(projekt): vgm.setup_verfahrensgebiet_listener() vgm.teardown_verfahrensgebiet_listener() vgm.teardown_verfahrensgebiet_listener() self.assertEqual(projekt.layersAdded.connected, []) self.assertEqual(projekt.layerRemoved.connected, []) self.assertEqual(projekt.readProject.connected, []) self.assertFalse(vgm._listener_installed) if __name__ == "__main__": unittest.main()