diff --git a/changelog.txt b/changelog.txt index 15ecd0e..c6cbabb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,4 @@ +- Kategorie „vielleicht testen" abgeschlossen: 30 weitere Funktionen bekamen Tests (os_wrapper, qgiscore_wrapper, qgisui_wrapper, DataGrabber, pruef_ergebnis, Navigation, SettingsTab, PrintTab – Zustandslogik, Signal-Handling, Atlas-/Theme-Verwaltung), `PrintTab._build_ui` bekam den Vermerk „Test ausgesetzt, zunächst Refactor" (#67) - Testkatalog-Klassifizierung in Docstrings festgehalten: 43 als „vernachlässigbar" eingestufte Funktionen tragen jetzt den Vermerk „von Test abgesehen, weil vernachlässigbar", 41 Print-Bereich-Funktionen (`print_logic.py`, `print_layout.py`, `print_layout_baufreigabe.py`) den Vermerk „Test ausgesetzt, zunächst Refactor" – keine Verhaltensänderung - Tests für weitere zentrale Funktionen ergänzt: Verfahrensgebiet-Zustandsverwaltung, Dock-Verwaltung, Pruefmanager-/Dateipruefer-Entscheidungswege, Fundament-Wrapper (Pfade, Landkreise, Nachrichten, Layer-Existenz/-Stil, GPKG-Layer-Prüfung, QGIS-3/4-Geometriekompatibilität) und Projekteigenschaften-Tab (#65) - Tests für die Kernpipeline ergänzt: Datenabruf (WFS/REST/OGR-URL-Bau, Raumfilter), Datenschreiber (GPKG-Schreiben, Laden, Log), LayerLoader (CRS-Transformation, Raum-/Geometriefilter) und excel_importer (Linklisten-Import) waren zuvor ohne Testdatei (#63) diff --git a/tests/test_datagrabber.py b/tests/test_datagrabber.py index ed30d24..32ef99c 100644 --- a/tests/test_datagrabber.py +++ b/tests/test_datagrabber.py @@ -128,6 +128,26 @@ class TestDataGrabber(unittest.TestCase): self.assertFalse(summary.ok) self.assertEqual(summary.aktion, "keine_validen_eintraege") + def test_aggregate_results_mit_zeilen_liefert_erfolgsergebnis(self): + """Prüft, dass _aggregate_results bei vorhandenen Zeilen ein Erfolgsergebnis mit Zeilenzahl liefert.""" + grabber = self._make_grabber() + + ergebnis = grabber._aggregate_results("quelle.xlsx", {"rows": [{"a": 1}, {"b": 2}]}, []) + + self.assertTrue(ergebnis.ok) + self.assertEqual(ergebnis.aktion, "ok") + self.assertEqual(ergebnis.kontext["valid_entries"], 2) + + def test_aggregate_results_ohne_zeilen_liefert_keine_validen_eintraege(self): + """Prüft, dass _aggregate_results ohne Zeilen ok=False mit Aktion 'keine_validen_eintraege' liefert.""" + grabber = self._make_grabber() + + ergebnis = grabber._aggregate_results("quelle.xlsx", {"rows": []}, []) + + self.assertFalse(ergebnis.ok) + self.assertEqual(ergebnis.aktion, "keine_validen_eintraege") + self.assertEqual(ergebnis.kontext["eintraege_gesamt"], 0) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_navigation.py b/tests/test_navigation.py new file mode 100644 index 0000000..3eef259 --- /dev/null +++ b/tests/test_navigation.py @@ -0,0 +1,196 @@ +"""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 = object(), object() + 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_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() diff --git a/tests/test_os_wrapper.py b/tests/test_os_wrapper.py new file mode 100644 index 0000000..c20d73c --- /dev/null +++ b/tests/test_os_wrapper.py @@ -0,0 +1,27 @@ +"""Tests für sn_basis/functions/os_wrapper.py::is_case_sensitive_fs (Issue #67).""" + +import unittest +from unittest.mock import patch + +from sn_basis.functions import os_wrapper + + +class IsCaseSensitiveFsTests(unittest.TestCase): + def test_windows_ist_nicht_case_sensitiv(self): + """Prüft, dass Windows als nicht case-sensitives Dateisystem gilt.""" + with patch.object(os_wrapper, "IS_WINDOWS", True), patch.object(os_wrapper, "IS_MACOS", False): + self.assertFalse(os_wrapper.is_case_sensitive_fs()) + + def test_macos_ist_nicht_case_sensitiv(self): + """Prüft, dass macOS als nicht case-sensitives Dateisystem gilt.""" + with patch.object(os_wrapper, "IS_WINDOWS", False), patch.object(os_wrapper, "IS_MACOS", True): + self.assertFalse(os_wrapper.is_case_sensitive_fs()) + + def test_linux_ist_case_sensitiv(self): + """Prüft, dass Linux als case-sensitives Dateisystem gilt.""" + with patch.object(os_wrapper, "IS_WINDOWS", False), patch.object(os_wrapper, "IS_MACOS", False): + self.assertTrue(os_wrapper.is_case_sensitive_fs()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_print_tab.py b/tests/test_print_tab.py new file mode 100644 index 0000000..3c8f264 --- /dev/null +++ b/tests/test_print_tab.py @@ -0,0 +1,649 @@ +"""Tests für sn_basis/ui/tabs/print_tab.py::PrintTab (Issue #67). + +self.logic ist eine PrintLogic-Instanz (modules/print_logic.py) - die Klasse steht wegen einer +geplanten Grundüberarbeitung komplett unter "Test ausgesetzt, zunächst Refactor" (#63/#65). Wo eine +PrintTab-Methode self.logic.irgendwas(...) aufruft, wird self.logic hier durch ein Fake/Mock ersetzt; +geprüft wird nur, dass PrintTab die richtige Methode mit den richtigen Argumenten aufruft, nie das +tatsächliche PrintLogic-Verhalten. + +PrintTab(QWidget) erbt von der im Mock-Modus leeren _MockWidget-Klasse (kein setLayout() usw.) - wie bei +SettingsTab/BaseDockWidget wird nie PrintTab() echt konstruiert, sondern PrintTab.__new__(PrintTab) mit +den je Test nötigen Instanzattributen von Hand. +""" + +import unittest +from unittest.mock import MagicMock, call, patch + +from sn_basis.functions.qt_wrapper import ArrowDown, ArrowRight +from sn_basis.ui.tabs.print_tab import ( + ATLAS_ERZEUGEN_VAR, + DRUCKBEREICH_VAR, + DRUCKBEREICH_VERFAHRENSGEBIET, + DRUCKPUFFER_STANDARD_CM, + DRUCKPUFFER_VAR, + FORMFAKTOR_UI_VAR, + KARTENNAME_38, + KARTENNAME_PLACEHOLDER, + KARTENNAME_VAR, + MASSSTAB_VAR, + MASSSTAB_WIE_KARTENFENSTER, + PLOTMASSSTAB_BY_AUSWAHL, + THEMA_VAR, + THEMA_WIE_KARTENFENSTER, + DIN_STANDARD, + PrintTab, + ZIELGROESSE_UI_VAR, +) + + +def _neuer_tab() -> PrintTab: + return PrintTab.__new__(PrintTab) + + +class _FakeCombo: + def __init__(self, text: str = "") -> None: + self._text = text + self.added_items: list = [] + + def currentText(self) -> str: + return self._text + + def setCurrentText(self, value: str) -> None: + self._text = value + + def clear(self) -> None: + self.added_items = [] + + def addItems(self, items) -> None: + self.added_items.extend(items) + + def blockSignals(self, value: bool) -> None: + pass + + +class _FakeCheckbox: + def __init__(self, checked: bool = False) -> None: + self._checked = checked + + def isChecked(self) -> bool: + return self._checked + + def setChecked(self, value: bool) -> None: + self._checked = value + + +class _FakeButton: + def __init__(self, text: str = "") -> None: + self._text = text + + def setText(self, value: str) -> None: + self._text = value + + def text(self) -> str: + return self._text + + +class _FakePruefmanager: + def __init__(self, frage_text_ergebnis) -> None: + self._frage_text_ergebnis = frage_text_ergebnis + self.zeige_hinweis_calls: list = [] + + def frage_text(self, titel, meldung, default_text=""): + return self._frage_text_ergebnis + + def zeige_hinweis(self, titel, meldung) -> None: + self.zeige_hinweis_calls.append((titel, meldung)) + + +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 _FakeProjectSignals: + def __init__(self) -> None: + self.readProject = _FakeSignal() + self.newProjectCreated = _FakeSignal() + self.cleared = _FakeSignal() + + +class _FakeThemeCollection: + def __init__(self) -> None: + self.mapThemesChanged = _FakeSignal() + self.changed = _FakeSignal() + self.themeChanged = _FakeSignal() + + +class SetServicesTests(unittest.TestCase): + @patch("sn_basis.ui.tabs.print_tab.PrintLogic") + def test_ruft_alle_set_methoden_mit_combo_werten_auf(self, mock_logic_cls): + """Prüft, dass set_services alle set_*-Methoden von PrintLogic mit den aktuellen Combo-Werten aufruft.""" + tab = _neuer_tab() + tab._kartenname_combo = _FakeCombo("§38") + tab._massstab_combo = _FakeCombo("1:1000") + tab._thema_combo = _FakeCombo("Thema A") + tab._zielgroesse_combo = _FakeCombo("A3") + tab._druckbereich_combo = _FakeCombo("Verfahrensgebiet") + tab._endlosrolle_cb = _FakeCheckbox(True) + tab._get_current_canvas_scale = lambda: 5000.0 + mock_logic = mock_logic_cls.return_value + + pruefmanager = object() + tab.set_services(pruefmanager, object()) + + self.assertIs(tab.pruefmanager, pruefmanager) + mock_logic.set_kartenname_for_auswahl.assert_called_once_with("§38") + mock_logic.set_plotmassstab_for_auswahl.assert_called_once_with("1:1000", 5000.0) + mock_logic.set_view_for_auswahl.assert_called_once_with("Thema A") + mock_logic.set_zielgroesse_for_auswahl.assert_called_once_with("A3") + mock_logic.set_druckbereich_for_auswahl.assert_called_once_with("Verfahrensgebiet") + mock_logic.set_formfaktor.assert_called_once_with(True) + + @patch("sn_basis.ui.tabs.print_tab.PrintLogic") + def test_ohne_combos_ruft_keine_set_methode_auf(self, mock_logic_cls): + """Prüft, dass set_services ohne vorhandene Combos keine set_*-Methode von PrintLogic aufruft.""" + tab = _neuer_tab() + tab._kartenname_combo = None + tab._massstab_combo = None + tab._thema_combo = None + tab._zielgroesse_combo = None + tab._druckbereich_combo = None + tab._endlosrolle_cb = None + mock_logic = mock_logic_cls.return_value + + tab.set_services(object(), object()) + + mock_logic.set_kartenname_for_auswahl.assert_not_called() + mock_logic.set_plotmassstab_for_auswahl.assert_not_called() + mock_logic.set_view_for_auswahl.assert_not_called() + mock_logic.set_zielgroesse_for_auswahl.assert_not_called() + mock_logic.set_druckbereich_for_auswahl.assert_not_called() + mock_logic.set_formfaktor.assert_not_called() + self.assertIs(tab.logic, mock_logic) + + +class RestoreStateTests(unittest.TestCase): + def _neuer_tab_mit_widgets(self) -> PrintTab: + tab = _neuer_tab() + tab._kartenname_combo = _FakeCombo() + tab._massstab_combo = _FakeCombo() + tab._thema_combo = _FakeCombo() + tab._zielgroesse_combo = _FakeCombo() + tab._endlosrolle_cb = _FakeCheckbox() + tab._druckbereich_combo = _FakeCombo() + tab._druckpuffer_button = _FakeButton() + tab._atlas_erzeugen_cb = None + tab._atlas_layer_combo = None + tab._get_gespeicherte_themen = lambda: ["Thema A"] + return tab + + def test_gueltige_gespeicherte_werte_werden_uebernommen(self): + """Prüft, dass _restore_state gültige gespeicherte Werte in die jeweiligen Widgets übernimmt.""" + tab = self._neuer_tab_mit_widgets() + massstab_gueltig = next(iter(PLOTMASSSTAB_BY_AUSWAHL.keys())) + werte = { + KARTENNAME_VAR: KARTENNAME_38, + MASSSTAB_VAR: massstab_gueltig, + THEMA_VAR: THEMA_WIE_KARTENFENSTER, + ZIELGROESSE_UI_VAR: DIN_STANDARD, + FORMFAKTOR_UI_VAR: "Blatt", + DRUCKBEREICH_VAR: DRUCKBEREICH_VERFAHRENSGEBIET, + DRUCKPUFFER_VAR: "7", + } + with patch("sn_basis.ui.tabs.print_tab.get_variable", side_effect=lambda key, scope=None: werte.get(key)): + tab._restore_state() + + self.assertEqual(tab._kartenname_combo.currentText(), KARTENNAME_38) + self.assertEqual(tab._massstab_combo.currentText(), massstab_gueltig) + self.assertEqual(tab._druckbereich_combo.currentText(), DRUCKBEREICH_VERFAHRENSGEBIET) + self.assertEqual(tab._druckpuffer_button.text(), "7 cm") + + def test_ungueltige_oder_fehlende_werte_fallen_auf_defaults_zurueck(self): + """Prüft, dass _restore_state ohne (gültige) gespeicherte Werte auf die Default-Konstanten zurückfällt.""" + tab = self._neuer_tab_mit_widgets() + with patch("sn_basis.ui.tabs.print_tab.get_variable", return_value=None): + tab._restore_state() + + self.assertEqual(tab._kartenname_combo.currentText(), KARTENNAME_PLACEHOLDER) + self.assertEqual(tab._massstab_combo.currentText(), MASSSTAB_WIE_KARTENFENSTER) + self.assertEqual(tab._druckbereich_combo.currentText(), DRUCKBEREICH_VERFAHRENSGEBIET) + self.assertEqual(tab._druckpuffer_button.text(), f"{DRUCKPUFFER_STANDARD_CM} cm") + + +class OnDruckpufferClickedTests(unittest.TestCase): + def test_gueltige_zahl_setzt_variable_und_button_text(self): + """Prüft, dass eine bestätigte, gültige Zahl den Druckpuffer speichert und den Button-Text aktualisiert.""" + tab = _neuer_tab() + tab.pruefmanager = _FakePruefmanager(("5", True)) + tab._druckpuffer_button = _FakeButton() + with patch("sn_basis.ui.tabs.print_tab.get_variable", return_value=None), \ + patch("sn_basis.ui.tabs.print_tab.set_variable") as mock_set: + tab._on_druckpuffer_clicked() + + mock_set.assert_called_once_with(DRUCKPUFFER_VAR, "5", scope="project") + self.assertEqual(tab._druckpuffer_button.text(), "5 cm") + + def test_abgebrochen_aendert_nichts(self): + """Prüft, dass ein abgebrochener Dialog den Druckpuffer nicht verändert.""" + tab = _neuer_tab() + tab.pruefmanager = _FakePruefmanager(("5", False)) + tab._druckpuffer_button = _FakeButton() + with patch("sn_basis.ui.tabs.print_tab.get_variable", return_value=None), \ + patch("sn_basis.ui.tabs.print_tab.set_variable") as mock_set: + tab._on_druckpuffer_clicked() + + mock_set.assert_not_called() + + def test_ungueltige_zahl_zeigt_hinweis_und_setzt_nichts(self): + """Prüft, dass eine nicht-numerische Eingabe einen Hinweis zeigt und den Druckpuffer nicht setzt.""" + tab = _neuer_tab() + pruefmanager = _FakePruefmanager(("abc", True)) + tab.pruefmanager = pruefmanager + tab._druckpuffer_button = _FakeButton() + with patch("sn_basis.ui.tabs.print_tab.get_variable", return_value=None), \ + patch("sn_basis.ui.tabs.print_tab.set_variable") as mock_set: + tab._on_druckpuffer_clicked() + + mock_set.assert_not_called() + self.assertEqual(len(pruefmanager.zeige_hinweis_calls), 1) + + +class OnVorlageErstellenTests(unittest.TestCase): + def test_ruft_druckvorlage_anlegen_mit_combo_werten_auf(self): + """Prüft, dass _on_vorlage_erstellen druckvorlage_anlegen mit den aktuellen Combo-Werten aufruft.""" + tab = _neuer_tab() + mock_logic = MagicMock() + tab.logic = mock_logic + tab.pruefmanager = object() + tab._kartenname_combo = _FakeCombo("§38") + tab._massstab_combo = _FakeCombo("1:1000") + tab._zielgroesse_combo = _FakeCombo("A3") + tab._endlosrolle_cb = _FakeCheckbox(True) + tab._druckbereich_combo = _FakeCombo("Verfahrensgebiet") + + tab._on_vorlage_erstellen() + + mock_logic.druckvorlage_anlegen.assert_called_once_with( + layer=None, + kartenname_auswahl="§38", + massstab_auswahl="1:1000", + zielgroesse="A3", + formfaktor=True, + druckbereich_auswahl="Verfahrensgebiet", + ) + + def test_ohne_logic_tut_nichts(self): + """Prüft, dass _on_vorlage_erstellen ohne gesetzte PrintLogic keine Ausnahme wirft.""" + tab = _neuer_tab() + tab.logic = None + tab.pruefmanager = object() + + tab._on_vorlage_erstellen() # darf keine Ausnahme werfen + + +class ProjectSignalsTests(unittest.TestCase): + def test_connect_verbindet_alle_drei_signale(self): + """Prüft, dass _connect_project_signals alle drei Projektsignale mit _on_project_changed verbindet.""" + tab = _neuer_tab() + projekt = _FakeProjectSignals() + with patch("sn_basis.ui.tabs.print_tab.QgsProject") as mock_project: + mock_project.instance.return_value = projekt + tab._connect_project_signals() + + self.assertEqual(projekt.readProject.connected, [tab._on_project_changed]) + self.assertEqual(projekt.newProjectCreated.connected, [tab._on_project_changed]) + self.assertEqual(projekt.cleared.connected, [tab._on_project_changed]) + + def test_disconnect_trennt_alle_drei_signale(self): + """Prüft, dass _disconnect_project_signals alle drei zuvor verbundenen Projektsignale trennt.""" + tab = _neuer_tab() + projekt = _FakeProjectSignals() + projekt.readProject.connect(tab._on_project_changed) + projekt.newProjectCreated.connect(tab._on_project_changed) + projekt.cleared.connect(tab._on_project_changed) + with patch("sn_basis.ui.tabs.print_tab.QgsProject") as mock_project: + mock_project.instance.return_value = projekt + tab._disconnect_project_signals() + + self.assertEqual(projekt.readProject.connected, []) + self.assertEqual(projekt.newProjectCreated.connected, []) + self.assertEqual(projekt.cleared.connected, []) + + def test_instance_wirft_ausnahme_wird_abgefangen(self): + """Prüft, dass eine Ausnahme bei QgsProject.instance() in beiden Signalmethoden abgefangen wird.""" + tab = _neuer_tab() + with patch("sn_basis.ui.tabs.print_tab.QgsProject") as mock_project: + mock_project.instance.side_effect = RuntimeError("kaputt") + tab._connect_project_signals() # darf keine Ausnahme werfen + tab._disconnect_project_signals() # darf keine Ausnahme werfen + + +class CleanupTests(unittest.TestCase): + def test_ruft_beide_disconnect_methoden_auf(self): + """Prüft, dass cleanup sowohl die Theme- als auch die Projektsignale trennt.""" + tab = _neuer_tab() + tab._disconnect_theme_collection_signals = MagicMock() + tab._disconnect_project_signals = MagicMock() + + tab.cleanup() + + tab._disconnect_theme_collection_signals.assert_called_once() + tab._disconnect_project_signals.assert_called_once() + + +class ToggleAtlasTests(unittest.TestCase): + def test_checked_true_setzt_arrow_down_und_zeigt_inhalt(self): + """Prüft, dass _toggle_atlas bei checked=True den Pfeil nach unten dreht und den Inhalt einblendet.""" + tab = _neuer_tab() + tab._atlas_button = MagicMock() + tab._atlas_content = MagicMock() + + tab._toggle_atlas(True) + + tab._atlas_button.setArrowType.assert_called_once_with(ArrowDown) + tab._atlas_content.setVisible.assert_called_once_with(True) + + def test_checked_false_setzt_arrow_right_und_versteckt_inhalt(self): + """Prüft, dass _toggle_atlas bei checked=False den Pfeil nach rechts dreht und den Inhalt ausblendet.""" + tab = _neuer_tab() + tab._atlas_button = MagicMock() + tab._atlas_content = MagicMock() + + tab._toggle_atlas(False) + + tab._atlas_button.setArrowType.assert_called_once_with(ArrowRight) + tab._atlas_content.setVisible.assert_called_once_with(False) + + +class RestoreAtlasLayerSelectionTests(unittest.TestCase): + def test_vorhandene_layer_id_setzt_layer_in_combo(self): + """Prüft, dass eine gespeicherte Layer-ID den zugehörigen Layer im Atlas-Dropdown auswählt.""" + tab = _neuer_tab() + tab._atlas_layer_combo = MagicMock(spec=["setLayer"]) + fake_layer = object() + with patch("sn_basis.ui.tabs.print_tab.get_variable", return_value="layer_1"), \ + patch("sn_basis.ui.tabs.print_tab.QgsProject") as mock_project: + mock_project.instance.return_value.mapLayer.return_value = fake_layer + tab._restore_atlas_layer_selection() + + tab._atlas_layer_combo.setLayer.assert_called_once_with(fake_layer) + + def test_leere_gespeicherte_layer_id_tut_nichts(self): + """Prüft, dass eine leere gespeicherte Layer-ID das Projekt gar nicht erst anfragt.""" + tab = _neuer_tab() + tab._atlas_layer_combo = MagicMock(spec=["setLayer"]) + with patch("sn_basis.ui.tabs.print_tab.get_variable", return_value=""), \ + patch("sn_basis.ui.tabs.print_tab.QgsProject") as mock_project: + tab._restore_atlas_layer_selection() + + tab._atlas_layer_combo.setLayer.assert_not_called() + mock_project.instance.assert_not_called() + + +class ThemeCollectionSignalsTests(unittest.TestCase): + def test_connect_verbindet_alle_signale(self): + """Prüft, dass _connect_theme_collection_signals alle Signale der Theme-Collection verbindet.""" + tab = _neuer_tab() + tab._theme_signal_connected = False + collection = _FakeThemeCollection() + with patch("sn_basis.ui.tabs.print_tab.QgsProject") as mock_project: + mock_project.instance.return_value.mapThemeCollection.return_value = collection + tab._connect_theme_collection_signals() + + self.assertEqual(collection.mapThemesChanged.connected, [tab._refresh_thema_combo_live]) + self.assertTrue(tab._theme_signal_connected) + self.assertIs(tab._connected_theme_collection, collection) + + def test_connect_ist_idempotent_wenn_schon_verbunden(self): + """Prüft, dass _connect_theme_collection_signals bei bereits verbundenem Signal nichts erneut tut.""" + tab = _neuer_tab() + tab._theme_signal_connected = True + with patch("sn_basis.ui.tabs.print_tab.QgsProject") as mock_project: + tab._connect_theme_collection_signals() + + mock_project.instance.assert_not_called() + + def test_disconnect_trennt_alle_signale(self): + """Prüft, dass _disconnect_theme_collection_signals alle verbundenen Theme-Signale trennt und zurücksetzt.""" + tab = _neuer_tab() + collection = _FakeThemeCollection() + collection.mapThemesChanged.connect(tab._refresh_thema_combo_live) + collection.changed.connect(tab._refresh_thema_combo_live) + collection.themeChanged.connect(tab._refresh_thema_combo_live) + tab._connected_theme_collection = collection + tab._theme_signal_connected = True + + tab._disconnect_theme_collection_signals() + + self.assertEqual(collection.mapThemesChanged.connected, []) + self.assertIsNone(tab._connected_theme_collection) + self.assertFalse(tab._theme_signal_connected) + + +class RefreshThemaComboLiveTests(unittest.TestCase): + def test_erhaelt_vorherige_auswahl_wenn_noch_vorhanden(self): + """Prüft, dass _refresh_thema_combo_live die vorherige Auswahl beibehält, wenn sie noch existiert.""" + tab = _neuer_tab() + combo = _FakeCombo("Thema A") + tab._thema_combo = combo + tab._get_gespeicherte_themen = lambda: ["Thema A", "Thema B"] + tab._on_thema_changed = MagicMock() + + tab._refresh_thema_combo_live() + + self.assertEqual(combo.currentText(), "Thema A") + tab._on_thema_changed.assert_called_once_with("Thema A") + + def test_faellt_auf_wie_kartenfenster_zurueck_wenn_auswahl_verschwunden(self): + """Prüft, dass _refresh_thema_combo_live auf 'wie Kartenfenster' zurückfällt, wenn die Auswahl verschwunden ist.""" + tab = _neuer_tab() + combo = _FakeCombo("Alte Auswahl") + tab._thema_combo = combo + tab._get_gespeicherte_themen = lambda: ["Thema B"] + tab._on_thema_changed = MagicMock() + + tab._refresh_thema_combo_live() + + self.assertEqual(combo.currentText(), THEMA_WIE_KARTENFENSTER) + tab._on_thema_changed.assert_called_once_with(THEMA_WIE_KARTENFENSTER) + + +class GetGespeicherteThemenTests(unittest.TestCase): + def test_dedupliziert_und_filtert_leere_eintraege(self): + """Prüft, dass _get_gespeicherte_themen Duplikate und leere/None-Einträge entfernt.""" + tab = _neuer_tab() + with patch("sn_basis.ui.tabs.print_tab.QgsProject") as mock_project: + mock_project.instance.return_value.mapThemeCollection.return_value.mapThemes.return_value = [ + "A", "A", "", "B", None, + ] + ergebnis = tab._get_gespeicherte_themen() + + self.assertEqual(ergebnis, ["A", "B"]) + + def test_fehler_liefert_leere_liste(self): + """Prüft, dass ein Fehler beim Zugriff auf die Theme-Collection eine leere Liste liefert.""" + tab = _neuer_tab() + with patch("sn_basis.ui.tabs.print_tab.QgsProject") as mock_project: + mock_project.instance.side_effect = RuntimeError("kaputt") + ergebnis = tab._get_gespeicherte_themen() + + self.assertEqual(ergebnis, []) + + +class GetCurrentCanvasScaleTests(unittest.TestCase): + def test_liefert_skalierung_als_float(self): + """Prüft, dass _get_current_canvas_scale die Canvas-Skalierung als float liefert.""" + tab = _neuer_tab() + fake_iface = MagicMock() + fake_iface.mapCanvas.return_value.scale.return_value = 5000 + with patch("sn_basis.ui.tabs.print_tab.iface", fake_iface): + self.assertEqual(tab._get_current_canvas_scale(), 5000.0) + + def test_ohne_canvas_liefert_none(self): + """Prüft, dass _get_current_canvas_scale ohne verfügbares Kartenfenster None liefert.""" + tab = _neuer_tab() + fake_iface = MagicMock() + fake_iface.mapCanvas.return_value = None + with patch("sn_basis.ui.tabs.print_tab.iface", fake_iface): + self.assertIsNone(tab._get_current_canvas_scale()) + + def test_skalierung_null_liefert_none(self): + """Prüft, dass eine Skalierung von 0 als None statt als 0.0 gilt.""" + tab = _neuer_tab() + fake_iface = MagicMock() + fake_iface.mapCanvas.return_value.scale.return_value = 0 + with patch("sn_basis.ui.tabs.print_tab.iface", fake_iface): + self.assertIsNone(tab._get_current_canvas_scale()) + + +class OnAtlasLayerChangedTests(unittest.TestCase): + def test_none_oder_leer_setzt_variable_leer(self): + """Prüft, dass None oder ein leerer Wert die Abdeckungslayer-Variable leeren.""" + tab = _neuer_tab() + with patch("sn_basis.ui.tabs.print_tab.set_variable") as mock_set: + tab._on_atlas_layer_changed(None) + tab._on_atlas_layer_changed("") + + mock_set.assert_has_calls([ + call("sn_abdeckungslayer", "", scope="project"), + call("sn_abdeckungslayer", "", scope="project"), + ]) + + def test_layer_objekt_mit_id_methode_setzt_dessen_id(self): + """Prüft, dass ein echtes Layer-Objekt (mit id()-Methode) dessen ID speichert.""" + tab = _neuer_tab() + fake_layer = MagicMock() + fake_layer.id.return_value = "layer_42" + with patch("sn_basis.ui.tabs.print_tab.set_variable") as mock_set: + tab._on_atlas_layer_changed(fake_layer) + + mock_set.assert_called_once_with("sn_abdeckungslayer", "layer_42", scope="project") + + def test_string_fallback_sucht_layer_ueber_namen(self): + """Prüft, dass ohne id()-Methode über den Layernamen im Projekt gesucht und dessen ID gespeichert wird.""" + tab = _neuer_tab() + fake_layer = MagicMock(spec=["name"]) + fake_layer.name.return_value = "Mein Layer" + with patch("sn_basis.ui.tabs.print_tab.set_variable") as mock_set, \ + patch("sn_basis.ui.tabs.print_tab.QgsProject") as mock_project: + mock_project.instance.return_value.mapLayers.return_value = {"layer_7": fake_layer} + tab._on_atlas_layer_changed("Mein Layer") + + mock_set.assert_called_once_with("sn_abdeckungslayer", "layer_7", scope="project") + + +class OnAuswahlAsAtlasClickedTests(unittest.TestCase): + def test_erfolgreicher_layer_aktualisiert_atlas_ui(self): + """Prüft, dass ein erfolgreich erzeugter Atlas-Layer den Atlas-Bereich öffnet und ins Dropdown übernimmt.""" + tab = _neuer_tab() + fake_layer = object() + mock_logic = MagicMock() + mock_logic.create_atlas_layer_from_selection.return_value = (fake_layer, "layer_9") + tab.logic = mock_logic + tab.pruefmanager = object() + tab._atlas_button = MagicMock() + tab._atlas_content = MagicMock() + tab._atlas_layer_combo = MagicMock(spec=["setLayer"]) + tab._toggle_atlas = MagicMock() + + tab._on_auswahl_as_atlas_clicked() + + tab._atlas_button.setChecked.assert_called_once_with(True) + tab._toggle_atlas.assert_called_once_with(True) + tab._atlas_layer_combo.setLayer.assert_called_once_with(fake_layer) + + def test_ohne_erzeugten_layer_tut_nichts_weiter(self): + """Prüft, dass ohne erzeugten Atlas-Layer keine UI-Aktualisierung stattfindet.""" + tab = _neuer_tab() + mock_logic = MagicMock() + mock_logic.create_atlas_layer_from_selection.return_value = (None, None) + tab.logic = mock_logic + tab.pruefmanager = object() + tab._atlas_button = MagicMock() + tab._atlas_content = MagicMock() + tab._atlas_layer_combo = MagicMock() + + tab._on_auswahl_as_atlas_clicked() # darf keine Ausnahme werfen + + tab._atlas_button.setChecked.assert_not_called() + + +class PopulateAtlasLayerComboTests(unittest.TestCase): + def test_befuellt_combo_ohne_druckrechteck_und_leere_namen(self): + """Prüft, dass _populate_atlas_layer_combo alle Layernamen außer 'Druckrechteck' und leeren Namen einträgt.""" + tab = _neuer_tab() + combo = MagicMock(spec=["clear", "addItem", "blockSignals"]) + tab._atlas_layer_combo = combo + + layer_normal = MagicMock() + layer_normal.name.return_value = "Flurstuecke" + layer_druckrechteck = MagicMock() + layer_druckrechteck.name.return_value = "Druckrechteck" + layer_leer = MagicMock() + layer_leer.name.return_value = "" + + with patch("sn_basis.ui.tabs.print_tab.QgsProject") as mock_project: + mock_project.instance.return_value.mapLayers.return_value = { + "1": layer_normal, "2": layer_druckrechteck, "3": layer_leer, + } + tab._populate_atlas_layer_combo() + + alle_aufrufe = [c.args[0] for c in combo.addItem.call_args_list] + self.assertIn("", alle_aufrufe) + self.assertIn("Flurstuecke", alle_aufrufe) + self.assertNotIn("Druckrechteck", alle_aufrufe) + self.assertEqual(alle_aufrufe.count(""), 1) + + def test_ohne_atlas_layer_combo_tut_nichts(self): + """Prüft, dass _populate_atlas_layer_combo ohne Atlas-Dropdown keine Ausnahme wirft.""" + tab = _neuer_tab() + tab._atlas_layer_combo = None + + tab._populate_atlas_layer_combo() # darf keine Ausnahme werfen + + +class OnAtlasErzeugenChangedTests(unittest.TestCase): + def test_aktiviert_speichert_variable_und_aktiviert_widgets(self): + """Prüft, dass ein aktivierter Atlas-Schalter die Variable setzt und Layer-Combo/Button aktiviert.""" + tab = _neuer_tab() + tab._atlas_layer_combo = MagicMock() + tab._btn_auswahl_as_atlas = MagicMock() + + with patch("sn_basis.ui.tabs.print_tab.set_variable") as mock_set_variable: + tab._on_atlas_erzeugen_changed(1) + + mock_set_variable.assert_called_once_with(ATLAS_ERZEUGEN_VAR, "1", scope="project") + tab._atlas_layer_combo.setEnabled.assert_called_once_with(True) + tab._btn_auswahl_as_atlas.setEnabled.assert_called_once_with(True) + + def test_deaktiviert_speichert_variable_und_deaktiviert_widgets(self): + """Prüft, dass ein deaktivierter Atlas-Schalter die Variable setzt und Layer-Combo/Button deaktiviert.""" + tab = _neuer_tab() + tab._atlas_layer_combo = MagicMock() + tab._btn_auswahl_as_atlas = MagicMock() + + with patch("sn_basis.ui.tabs.print_tab.set_variable") as mock_set_variable: + tab._on_atlas_erzeugen_changed(0) + + mock_set_variable.assert_called_once_with(ATLAS_ERZEUGEN_VAR, "0", scope="project") + tab._atlas_layer_combo.setEnabled.assert_called_once_with(False) + tab._btn_auswahl_as_atlas.setEnabled.assert_called_once_with(False) + + def test_ohne_widgets_wirft_keine_ausnahme(self): + """Prüft, dass _on_atlas_erzeugen_changed ohne Atlas-Combo/-Button keine Ausnahme wirft.""" + tab = _neuer_tab() + tab._atlas_layer_combo = None + tab._btn_auswahl_as_atlas = None + + with patch("sn_basis.ui.tabs.print_tab.set_variable"): + tab._on_atlas_erzeugen_changed(1) # darf keine Ausnahme werfen + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_pruef_ergebnis.py b/tests/test_pruef_ergebnis.py index 344cb6c..e1616cb 100644 --- a/tests/test_pruef_ergebnis.py +++ b/tests/test_pruef_ergebnis.py @@ -1,6 +1,7 @@ -"""Tests für sn_basis/modules/pruef_ergebnis.py::ist_verfahrens_db_aktion/erlaubte_persistierung (Issue #65).""" +"""Tests für sn_basis/modules/pruef_ergebnis.py::ist_verfahrens_db_aktion/erlaubte_persistierung/dateipfad (Issue #65/#67).""" import unittest +from pathlib import Path from sn_basis.modules.pruef_ergebnis import pruef_ergebnis @@ -43,5 +44,19 @@ class ErlaubtePersistierungTests(unittest.TestCase): self.assertFalse(ergebnis.erlaubte_persistierung) +class DateipfadTests(unittest.TestCase): + def test_liefert_path_wenn_kontext_ein_path_ist(self): + """Prüft, dass dateipfad den Kontext liefert, wenn dieser bereits ein Path-Objekt ist.""" + ergebnis = pruef_ergebnis(ok=True, kontext=Path("C:/tmp/verfahren.gpkg")) + + self.assertEqual(ergebnis.dateipfad, Path("C:/tmp/verfahren.gpkg")) + + def test_liefert_none_wenn_kontext_kein_path_ist(self): + """Prüft, dass dateipfad None liefert, wenn der Kontext kein Path-Objekt ist (z. B. String oder Layer).""" + self.assertIsNone(pruef_ergebnis(ok=True, kontext="C:/tmp/verfahren.gpkg").dateipfad) + self.assertIsNone(pruef_ergebnis(ok=True, kontext=None).dateipfad) + self.assertIsNone(pruef_ergebnis(ok=True, kontext=object()).dateipfad) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_qgiscore_wrapper_helpers.py b/tests/test_qgiscore_wrapper_helpers.py index 16b95ed..643d3e6 100644 --- a/tests/test_qgiscore_wrapper_helpers.py +++ b/tests/test_qgiscore_wrapper_helpers.py @@ -1,4 +1,4 @@ -"""Tests für sn_basis/functions/qgiscore_wrapper.py::polygon_zu_linie/layer_exists_in_gpkg (Issue #65).""" +"""Tests für sn_basis/functions/qgiscore_wrapper.py::polygon_zu_linie/layer_exists_in_gpkg/create_buffer_layer (Issue #65/#67).""" import sqlite3 import tempfile @@ -164,5 +164,25 @@ class LayerExistsInGpkgTests(unittest.TestCase): self.assertFalse(qc.layer_exists_in_gpkg(pfad, "Flurstuecke")) +class CreateBufferLayerTests(unittest.TestCase): + """Deckt nur die Wächter ab: die eigentliche Pufferbildung importiert `qgis.core.QgsFeature` + direkt und ist im Mock-Modus nicht sinnvoll prüfbar (siehe Docstring der Funktion).""" + + def test_ohne_qgis_liefert_none(self): + """Prüft, dass create_buffer_layer ohne verfügbares QGIS None liefert.""" + with patch.object(qc, "QGIS_AVAILABLE", False): + self.assertIsNone(qc.create_buffer_layer(source_layer=object(), distance_m=10.0)) + + def test_fehlender_source_layer_liefert_none(self): + """Prüft, dass ein fehlender Quelllayer (None) None liefert, auch wenn QGIS verfügbar wäre.""" + with patch.object(qc, "QGIS_AVAILABLE", True): + self.assertIsNone(qc.create_buffer_layer(source_layer=None, distance_m=10.0)) + + def test_source_layer_ohne_getfeatures_liefert_none(self): + """Prüft, dass ein Quelllayer ohne getFeatures()-Methode None liefert.""" + with patch.object(qc, "QGIS_AVAILABLE", True): + self.assertIsNone(qc.create_buffer_layer(source_layer=object(), distance_m=10.0)) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_qgisui_wrapper_helpers.py b/tests/test_qgisui_wrapper_helpers.py index 1a3fce7..6634780 100644 --- a/tests/test_qgisui_wrapper_helpers.py +++ b/tests/test_qgisui_wrapper_helpers.py @@ -1,4 +1,4 @@ -"""Tests für sn_basis/functions/qgisui_wrapper.py::add_layer_to_project (Issue #65).""" +"""Tests für sn_basis/functions/qgisui_wrapper.py::add_layer_to_project/find_dock_widgets/add_menu/remove_menu (Issue #65/#67).""" import unittest from unittest.mock import MagicMock, patch @@ -34,5 +34,74 @@ class AddLayerToProjectTests(unittest.TestCase): self.assertFalse(qui.add_layer_to_project(object())) +class FindDockWidgetsTests(unittest.TestCase): + def test_ohne_main_window_liefert_leere_liste(self): + """Prüft, dass find_dock_widgets ohne verfügbares Hauptfenster eine leere Liste liefert.""" + with patch.object(qui, "get_main_window", return_value=None): + self.assertEqual(qui.find_dock_widgets(), []) + + def test_liefert_dock_widgets_des_main_window(self): + """Prüft, dass find_dock_widgets die vom Hauptfenster gefundenen Dock-Widgets durchreicht.""" + fake_docks = [object(), object()] + fake_window = MagicMock() + fake_window.findChildren.return_value = fake_docks + with patch.object(qui, "get_main_window", return_value=fake_window): + self.assertEqual(qui.find_dock_widgets(), fake_docks) + + def test_fehler_beim_suchen_liefert_leere_liste(self): + """Prüft, dass ein Fehler beim Suchen der Dock-Widgets abgefangen wird und eine leere Liste liefert.""" + fake_window = MagicMock() + fake_window.findChildren.side_effect = RuntimeError("kaputt") + with patch.object(qui, "get_main_window", return_value=fake_window): + self.assertEqual(qui.find_dock_widgets(), []) + + +class _FakeMenu: + def __init__(self) -> None: + self.menuAction = lambda: "MENU_ACTION" + + +class AddRemoveMenuTests(unittest.TestCase): + def test_add_menu_ohne_main_window_tut_nichts(self): + """Prüft, dass add_menu ohne Hauptfenster keine Ausnahme wirft.""" + fake_iface = MagicMock() + fake_iface.mainWindow.return_value = None + with patch.object(qui, "iface", fake_iface): + qui.add_menu(_FakeMenu()) # darf keine Ausnahme werfen + + def test_add_menu_mit_echtem_qt_menu_fuegt_es_der_menubar_hinzu(self): + """Prüft, dass ein echtes Qt-Menü (mit menuAction) der Menüleiste hinzugefügt wird.""" + menu = _FakeMenu() + fake_iface = MagicMock() + with patch.object(qui, "iface", fake_iface): + qui.add_menu(menu) + + fake_iface.mainWindow.return_value.menuBar.return_value.addMenu.assert_called_once_with(menu) + + def test_add_menu_ohne_menuaction_wird_ignoriert(self): + """Prüft, dass ein Objekt ohne menuAction nicht an die Menüleiste übergeben wird.""" + fake_iface = MagicMock() + with patch.object(qui, "iface", fake_iface): + qui.add_menu(object()) + + fake_iface.mainWindow.return_value.menuBar.return_value.addMenu.assert_not_called() + + def test_remove_menu_entfernt_ueber_menuaction(self): + """Prüft, dass remove_menu das Menü über dessen menuAction() aus der Menüleiste entfernt.""" + menu = _FakeMenu() + fake_iface = MagicMock() + with patch.object(qui, "iface", fake_iface): + qui.remove_menu(menu) + + fake_iface.mainWindow.return_value.menuBar.return_value.removeAction.assert_called_once_with("MENU_ACTION") + + def test_remove_menu_ohne_main_window_tut_nichts(self): + """Prüft, dass remove_menu ohne Hauptfenster keine Ausnahme wirft.""" + fake_iface = MagicMock() + fake_iface.mainWindow.return_value = None + with patch.object(qui, "iface", fake_iface): + qui.remove_menu(_FakeMenu()) # darf keine Ausnahme werfen + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_settings_tab.py b/tests/test_settings_tab.py index c3d1b8c..f5ea4d7 100644 --- a/tests/test_settings_tab.py +++ b/tests/test_settings_tab.py @@ -1,7 +1,8 @@ -"""Tests für sn_basis/ui/tabs/settings_tab.py::save_data/load_data (Issue #65).""" +"""Tests für sn_basis/ui/tabs/settings_tab.py::save_data/load_data/_build_input (Issue #65/#67).""" import unittest +from sn_basis.functions.landkreise import LANDKREISE from sn_basis.functions.qt_wrapper import QComboBox, QLineEdit from sn_basis.ui.tabs.settings_tab import SettingsTab @@ -113,5 +114,25 @@ class LoadDataTests(unittest.TestCase): self.assertEqual(combo.currentText(), "") +class BuildInputTests(unittest.TestCase): + def test_landkreis_feld_liefert_combobox_mit_allen_landkreisen(self): + """Prüft, dass _build_input für das Landkreis-Feld eine ComboBox mit Platzhalter und allen Landkreisen liefert.""" + tab = _neuer_tab(_FakeLogic()) + + widget = tab._build_input("landkreis_user") + + self.assertIsInstance(widget, QComboBox) + self.assertEqual(widget.count(), 1 + len(LANDKREISE)) + + def test_anderes_feld_liefert_lineedit(self): + """Prüft, dass _build_input für ein normales Feld ein leeres Textfeld liefert.""" + tab = _neuer_tab(_FakeLogic()) + + widget = tab._build_input("amt") + + self.assertIsInstance(widget, QLineEdit) + self.assertEqual(widget.text(), "") + + if __name__ == "__main__": unittest.main() diff --git a/ui/tabs/print_tab.py b/ui/tabs/print_tab.py index fcd15e3..37ff357 100644 --- a/ui/tabs/print_tab.py +++ b/ui/tabs/print_tab.py @@ -113,6 +113,7 @@ class PrintTab(QWidget): self.logic.set_formfaktor(self._endlosrolle_cb.isChecked()) def _build_ui(self) -> None: + """von Test abgesehen, weil vernachlässigbar; HD 18.09.2026""" main_layout = QVBoxLayout() main_layout.setSpacing(4) main_layout.setContentsMargins(4, 4, 4, 4)