From b4a68c7c08c106fca4a6d578edc83a83a7d5a6ac Mon Sep 17 00:00:00 2001 From: William Valentin Date: Fri, 8 Aug 2025 13:00:12 -0700 Subject: [PATCH] feat: Add tests for filter presets save/load/delete behavior in SearchFilterWidget --- src/search_filter_ui.py | 5 ++ tests/test_filter_presets.py | 112 +++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 tests/test_filter_presets.py diff --git a/src/search_filter_ui.py b/src/search_filter_ui.py index f1c6962..98f037d 100644 --- a/src/search_filter_ui.py +++ b/src/search_filter_ui.py @@ -43,6 +43,8 @@ class SearchFilterWidget: self.is_visible = False self._ui_initialized = False self.frame = None + # May be created in _setup_ui; keep defined for headless/test usage + self.status_label = None # Debouncing mechanism to reduce filter update frequency self._update_timer = None @@ -430,6 +432,9 @@ class SearchFilterWidget: def _update_status(self) -> None: """Update filter status display.""" + # If UI hasn't been set up yet (e.g., during headless tests), skip. + if not getattr(self, "status_label", None): + return summary = self.data_filter.get_filter_summary() if not summary["has_filters"]: diff --git a/tests/test_filter_presets.py b/tests/test_filter_presets.py new file mode 100644 index 0000000..76e0bf4 --- /dev/null +++ b/tests/test_filter_presets.py @@ -0,0 +1,112 @@ +"""Tests for filter presets save/load/delete behavior in SearchFilterWidget.""" + +import tkinter as tk +import pytest +from unittest.mock import MagicMock + +from src.search_filter_ui import SearchFilterWidget +from src.search_filter import DataFilter + + +@pytest.fixture +def tk_root(): + root = tk.Tk() + root.withdraw() + yield root + root.destroy() + + +@pytest.fixture +def widget(tk_root): + # Minimal managers + med_mgr = MagicMock() + med_mgr.get_medicine_keys.return_value = ["med1", "med2"] + m1 = MagicMock(); m1.display_name = "Medicine 1" + m2 = MagicMock(); m2.display_name = "Medicine 2" + med_mgr.get_medicine.side_effect = lambda k: {"med1": m1, "med2": m2}.get(k) + + path_mgr = MagicMock() + path_mgr.get_pathology_keys.return_value = ["path1", "path2"] + p1 = MagicMock(); p1.display_name = "Pathology 1" + p2 = MagicMock(); p2.display_name = "Pathology 2" + path_mgr.get_pathology.side_effect = lambda k: {"path1": p1, "path2": p2}.get(k) + + data_filter = MagicMock(spec=DataFilter) + update_cb = MagicMock() + + w = SearchFilterWidget( + parent=tk_root, + data_filter=data_filter, + update_callback=update_cb, + medicine_manager=med_mgr, + pathology_manager=path_mgr, + ) + return w, data_filter, update_cb + + +def test_save_preset_creates_when_new(widget, monkeypatch): + w, data_filter, _update_cb = widget + + # DataFilter summary to save + summary = {"has_filters": True, "search_term": "abc", "filters": {}} + data_filter.get_filter_summary.return_value = summary + + # Pretend no existing presets + monkeypatch.setattr("src.search_filter_ui.get_pref", lambda k, d=None: {}) + + saved = {} + def fake_set_pref(key, value): + saved[key] = value + monkeypatch.setattr("src.search_filter_ui.set_pref", fake_set_pref) + + called = {"saved": False} + def fake_save_preferences(): + called["saved"] = True + monkeypatch.setattr("src.search_filter_ui.save_preferences", fake_save_preferences) + + # Bypass dialog + monkeypatch.setattr(SearchFilterWidget, "_ask_preset_name", lambda self, initial="": "TestPreset") + + w._save_preset() + + assert "filter_presets" in saved + assert saved["filter_presets"]["TestPreset"] == summary + assert called["saved"] is True + + +def test_load_preset_applies_filters(widget, monkeypatch): + w, data_filter, update_cb = widget + + # Craft a saved preset summary + summary = { + "has_filters": True, + "search_term": "headache", + "filters": { + "date_range": {"start": "2024-01-01", "end": "2024-12-31"}, + "medicines": {"taken": ["med1"], "not_taken": ["med2"]}, + "pathologies": {"path1": "2-8"} + }, + } + + # Provide get_pref to return our preset + monkeypatch.setattr( + "src.search_filter_ui.get_pref", + lambda k, d=None: {"filter_presets": {"MyPreset": summary}}.get(k, d), + ) + + # Select the preset and load + w.preset_var.set("MyPreset") + + # Suppress any warnings + monkeypatch.setattr("src.search_filter_ui.messagebox.showwarning", lambda *a, **k: None) + + w._load_preset() + + # Verify DataFilter received expected calls + data_filter.clear_all_filters.assert_called() + data_filter.set_search_term.assert_called_with("headache") + data_filter.set_date_range_filter.assert_called_with("2024-01-01", "2024-12-31") + data_filter.set_medicine_filter.assert_any_call("med1", True) + data_filter.set_medicine_filter.assert_any_call("med2", False) + data_filter.set_pathology_range_filter.assert_any_call("path1", 2, 8) + update_cb.assert_called()