Coverage for tests/ui/test_patient_selection_page.py: 99%
251 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 15:51 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 15:51 +0000
1import pytest
2import os
3import tempfile
4import shutil
5from unittest.mock import Mock, patch
6from PyQt6 import QtCore
7from PyQt6.QtCore import QSettings
8from PyQt6.QtTest import QTest
9from PyQt6.QtWidgets import QMessageBox, QPushButton
11from main.ui.patient_selection_page import PatientSelectionPage
14@pytest.fixture
15def patient_page(qtbot, mock_context):
16 """Fixture that creates and returns a PatientSelectionPage instance."""
17 previous_page = Mock()
18 page = PatientSelectionPage(mock_context, previous_page)
19 qtbot.addWidget(page)
20 return page
23class TestPatientSelectionPageSetup:
24 """Tests for PatientSelectionPage initialization."""
26 def test_page_initialization(self, patient_page):
27 """Ensure page initializes correctly."""
28 assert patient_page.workspace_path is not None
29 assert patient_page.selected_patients == set()
30 assert isinstance(patient_page.patient_buttons, dict)
32 def test_title_created(self, patient_page):
33 """Ensure title label is created."""
34 assert patient_page.title is not None
35 assert patient_page.title.text() != ""
37 def test_select_buttons_created(self, patient_page):
38 """Ensure select/deselect all buttons exist."""
39 assert patient_page.select_all_btn is not None
40 assert patient_page.deselect_all_btn is not None
42 def test_scroll_area_created(self, patient_page):
43 """Ensure scroll area and grid layout are initialized."""
44 assert patient_page.scroll_area is not None
45 assert patient_page.grid_layout is not None
47 def test_column_count_default(self, patient_page):
48 """Ensure default column count is valid."""
49 assert patient_page.column_count >= 1
52class TestPatientSelectionPagePatientLoading:
53 """Tests for patient loading and directory scanning."""
55 @pytest.fixture
56 def temp_workspace(self):
57 """Temporary workspace for testing patient directories."""
58 temp_dir = tempfile.mkdtemp()
59 os.makedirs(os.path.join(temp_dir, "sub-01", "anat"))
60 os.makedirs(os.path.join(temp_dir, "sub-02", "pet"))
61 os.makedirs(os.path.join(temp_dir, "sub-03", "anat"))
62 os.makedirs(os.path.join(temp_dir, "derivatives", "sub-01"))
63 yield temp_dir
64 shutil.rmtree(temp_dir, ignore_errors=True)
66 def test_find_patient_dirs(self, patient_page):
67 """Check that patient directories are correctly found."""
68 patient_dirs = patient_page._find_patient_dirs()
69 assert len(patient_dirs) == 3
70 patient_ids = [os.path.basename(d) for d in patient_dirs]
71 assert "sub-01" in patient_ids
72 assert "sub-02" in patient_ids
73 assert "sub-03" in patient_ids
75 def test_find_patient_dirs_ignores_derivatives(self, patient_page):
76 """Ensure derivatives folder is ignored."""
77 patient_dirs = patient_page._find_patient_dirs()
78 for path in patient_dirs:
79 assert "derivatives" not in path
81 def test_load_patients_creates_buttons(self, patient_page):
82 """Check that a button is created for each patient."""
83 assert len(patient_page.patient_buttons) == 3
84 assert "sub-01" in patient_page.patient_buttons
85 assert "sub-02" in patient_page.patient_buttons
86 assert "sub-03" in patient_page.patient_buttons
88 def test_load_patients_adds_to_grid(self, patient_page):
89 """Ensure patient widgets are added to the grid layout."""
90 assert patient_page.grid_layout.count() >= 3
93class TestPatientSelectionPageSelection:
94 """Tests for patient selection logic."""
96 def test_toggle_patient_select(self, patient_page):
97 """Select a single patient."""
98 button = patient_page.patient_buttons["sub-01"]
99 patient_page._toggle_patient("sub-01", True, button)
100 assert "sub-01" in patient_page.selected_patients
101 assert button.text() != "Select"
103 def test_toggle_patient_deselect(self, patient_page):
104 """Deselect a previously selected patient."""
105 button = patient_page.patient_buttons["sub-01"]
106 patient_page._toggle_patient("sub-01", True, button)
107 patient_page._toggle_patient("sub-01", False, button)
108 assert "sub-01" not in patient_page.selected_patients
110 def test_toggle_updates_main_buttons(self, patient_page):
111 """Ensure toggling updates main buttons."""
112 button = patient_page.patient_buttons["sub-01"]
113 patient_page._toggle_patient("sub-01", True, button)
114 patient_page.context["update_main_buttons"].assert_called()
116 def test_select_all_patients(self, patient_page):
117 """Select all patients at once."""
118 patient_page._select_all_patients()
119 assert len(patient_page.selected_patients) == 2
120 for button in patient_page.patient_buttons.values():
121 assert button.isChecked()
123 def test_deselect_all_patients(self, patient_page):
124 """Deselect all patients."""
125 patient_page._select_all_patients()
126 patient_page._deselect_all_patients()
127 assert len(patient_page.selected_patients) == 0
128 for button in patient_page.patient_buttons.values():
129 assert not button.isChecked()
131 def test_get_selected_patients(self, patient_page):
132 """Retrieve selected patient list."""
133 patient_page.selected_patients = {"sub-01", "sub-02"}
134 selected = patient_page.get_selected_patients()
135 assert isinstance(selected, list)
136 assert len(selected) == 2
137 assert "sub-01" in selected
138 assert "sub-02" in selected
141class TestPatientSelectionPageReadiness:
142 """Tests for readiness logic."""
144 def test_not_ready_without_selection(self, patient_page):
145 """Page is not ready without selection."""
146 patient_page.selected_patients.clear()
147 assert not patient_page.is_ready_to_advance()
149 def test_ready_with_selection(self, patient_page):
150 """Page is ready when patients are selected."""
151 patient_page.selected_patients.add("sub-01")
152 assert patient_page.is_ready_to_advance()
154 def test_can_go_back(self, patient_page):
155 """Page should always allow going back."""
156 assert patient_page.is_ready_to_go_back()
159class TestPatientSelectionPageNavigation:
160 """Tests for page navigation."""
162 def test_back_returns_previous_page(self, patient_page):
163 """Back should return previous page."""
164 result = patient_page.back()
165 assert result == patient_page.previous_page
166 patient_page.previous_page.on_enter.assert_called_once()
168 def test_next_without_cleanup(self, patient_page):
169 """Advance without cleanup confirmation."""
170 patient_page.selected_patients = {"sub-01", "sub-02"}
171 with patch('main.ui.patient_selection_page.ToolSelectionPage') as MockPage:
172 mock_page_instance = Mock()
173 MockPage.return_value = mock_page_instance
174 result = patient_page.next(patient_page.context)
175 assert result is not None
176 mock_page_instance.on_enter.assert_called_once()
178 def test_next_with_cleanup_confirmed(self, patient_page, monkeypatch, temp_workspace):
179 """Confirm cleanup before advancing."""
180 patient_page.selected_patients = {"sub-01"}
181 monkeypatch.setattr(QMessageBox, 'question',
182 lambda *args, **kwargs: QMessageBox.StandardButton.Yes)
183 with patch('main.ui.patient_selection_page.ToolSelectionPage') as MockPage:
184 MockPage.return_value = Mock()
185 result = patient_page.next(patient_page.context)
186 assert not os.path.exists(os.path.join(temp_workspace, "sub-02"))
187 assert os.path.exists(os.path.join(temp_workspace, "sub-01"))
189 def test_next_with_cleanup_cancelled(self, patient_page, monkeypatch):
190 """Cancel cleanup confirmation."""
191 patient_page.selected_patients = {"sub-01"}
192 monkeypatch.setattr(QMessageBox, 'question',
193 lambda *args, **kwargs: QMessageBox.StandardButton.No)
194 result = patient_page.next(patient_page.context)
195 assert result is None
197 def test_next_creates_tool_selection_page(self, patient_page):
198 """Ensure ToolSelectionPage is created on next."""
199 patient_page.selected_patients = {"sub-01", "sub-02"}
200 with patch('main.ui.patient_selection_page.ToolSelectionPage') as MockPage:
201 mock_page_instance = Mock()
202 MockPage.return_value = mock_page_instance
203 patient_page.next(patient_page.context)
204 MockPage.assert_called_once()
205 assert mock_page_instance in patient_page.context["history"]
208class TestPatientSelectionPageCleanup:
209 """Tests for cleanup operations."""
211 @pytest.fixture
212 def temp_workspace(self):
213 temp_dir = tempfile.mkdtemp()
214 os.makedirs(os.path.join(temp_dir, "sub-01", "anat"))
215 os.makedirs(os.path.join(temp_dir, "sub-02", "pet"))
216 os.makedirs(os.path.join(temp_dir, "derivatives", "pipeline1", "sub-01"))
217 os.makedirs(os.path.join(temp_dir, "derivatives", "pipeline1", "sub-02"))
218 yield temp_dir
219 shutil.rmtree(temp_dir, ignore_errors=True)
221 @pytest.fixture
222 def patient_page(self, qtbot, mock_context):
223 page = PatientSelectionPage(mock_context, Mock())
224 qtbot.addWidget(page)
225 return page
227 def test_cleanup_removes_unselected_patients(self, patient_page, monkeypatch, temp_workspace):
228 """Ensure unselected patients are deleted."""
229 patient_page.selected_patients = {"sub-01"}
230 monkeypatch.setattr(QMessageBox, 'question',
231 lambda *args, **kwargs: QMessageBox.StandardButton.Yes)
232 with patch('main.ui.patient_selection_page.ToolSelectionPage'):
233 patient_page.next(patient_page.context)
234 assert os.path.exists(os.path.join(temp_workspace, "sub-01"))
235 assert not os.path.exists(os.path.join(temp_workspace, "sub-02"))
237 def test_cleanup_removes_from_derivatives(self, patient_page, monkeypatch, temp_workspace):
238 """Ensure cleanup also removes from derivatives."""
239 patient_page.selected_patients = {"sub-01"}
240 monkeypatch.setattr(QMessageBox, 'question',
241 lambda *args, **kwargs: QMessageBox.StandardButton.Yes)
242 with patch('main.ui.patient_selection_page.ToolSelectionPage'):
243 patient_page.next(patient_page.context)
244 assert not os.path.exists(os.path.join(temp_workspace, "derivatives", "pipeline1", "sub-02"))
245 assert os.path.exists(os.path.join(temp_workspace, "derivatives", "pipeline1", "sub-01"))
247 def test_cleanup_handles_errors(self, patient_page, monkeypatch):
248 """Ensure cleanup errors are handled gracefully."""
249 patient_page.selected_patients = {"sub-01"}
250 monkeypatch.setattr(QMessageBox, 'question',
251 lambda *args, **kwargs: QMessageBox.StandardButton.Yes)
252 with patch('main.ui.patient_selection_page.ToolSelectionPage'), \
253 patch('main.ui.patient_selection_page.shutil.rmtree', side_effect=Exception("Delete error")):
254 result = patient_page.next(patient_page.context)
255 assert result is not None
258class TestPatientSelectionPageLayout:
259 """Tests for dynamic layout behavior."""
261 @pytest.fixture
262 def temp_workspace(self):
263 temp_dir = tempfile.mkdtemp()
264 for i in range(6):
265 os.makedirs(os.path.join(temp_dir, f"sub-{i:02d}"))
266 yield temp_dir
267 shutil.rmtree(temp_dir, ignore_errors=True)
269 def test_update_column_count_wide(self, patient_page):
270 """Ensure column count updates for wide window."""
271 patient_page.scroll_area.resize(800, 400)
272 patient_page._update_column_count()
273 assert patient_page.column_count >= 1
275 def test_update_column_count_narrow(self, patient_page):
276 """Ensure narrow view results in 1 column."""
277 with patch.object(patient_page.scroll_area.viewport(), 'width', return_value=200):
278 patient_page._update_column_count()
279 assert patient_page.column_count == 1
281 def test_resize_event_updates_columns(self, patient_page):
282 """Ensure resize event triggers column update."""
283 from PyQt6.QtGui import QResizeEvent
284 from PyQt6.QtCore import QSize
285 event = QResizeEvent(QSize(800, 600), QSize(400, 600))
286 patient_page.resizeEvent(event)
287 assert patient_page.column_count >= 1
289 def test_reload_patient_grid_maintains_selection(self, patient_page):
290 """Ensure reloading grid preserves selected patients."""
291 patient_page.selected_patients = {"sub-00", "sub-01"}
292 patient_page._reload_patient_grid()
293 assert "sub-00" in patient_page.selected_patients
294 assert "sub-01" in patient_page.selected_patients
297class TestPatientSelectionPageReset:
298 """Tests for page reset behavior."""
300 def test_reset_page_clears_selections(self, patient_page):
301 """Reset should clear selected patients."""
302 patient_page.selected_patients = {"sub-001", "sub-002"}
303 patient_page.reset_page()
304 assert len(patient_page.selected_patients) == 0
306 def test_reset_page_clears_buttons(self, patient_page):
307 """Reset should reload patient buttons."""
308 initial_buttons = len(patient_page.patient_buttons)
309 patient_page.reset_page()
310 assert len(patient_page.patient_buttons) == initial_buttons
312 def test_on_enter_maintains_selections(self, patient_page):
313 """on_enter should preserve selections."""
314 patient_page.selected_patients = {"sub-01"}
315 patient_page.on_enter()
316 assert "sub-01" in patient_page.selected_patients
319class TestPatientSelectionPageTranslation:
320 """Tests for UI translation updates."""
322 def test_translate_ui_updates_title(self, patient_page):
323 """Ensure title text updates after translation."""
324 patient_page._translate_ui()
325 assert patient_page.title.text() != ""
327 def test_translate_ui_updates_buttons(self, patient_page):
328 """Ensure button labels update after translation."""
329 patient_page._translate_ui()
330 assert patient_page.select_all_btn.text() != ""
331 assert patient_page.deselect_all_btn.text() != ""
334class TestPatientSelectionPageIntegration:
335 """Integration tests for full user workflows."""
337 @pytest.fixture
338 def temp_workspace(self):
339 temp_dir = tempfile.mkdtemp()
340 for i in range(3):
341 patient_id = f"sub-{i:02d}"
342 os.makedirs(os.path.join(temp_dir, patient_id, "anat"))
343 os.makedirs(os.path.join(temp_dir, patient_id, "pet"))
344 os.makedirs(os.path.join(temp_dir, "derivatives"))
345 yield temp_dir
346 shutil.rmtree(temp_dir, ignore_errors=True)
348 def test_full_workflow_select_and_advance(self, patient_page, monkeypatch, temp_workspace):
349 """Full flow: load patients, select all, and advance."""
350 assert len(patient_page.patient_buttons) == 3
351 patient_page._select_all_patients()
352 assert len(patient_page.selected_patients) == 3
353 assert patient_page.is_ready_to_advance()
354 with patch('main.ui.patient_selection_page.ToolSelectionPage') as MockPage:
355 MockPage.return_value = Mock()
356 result = patient_page.next(patient_page.context)
357 assert result is not None
359 def test_full_workflow_with_cleanup(self, patient_page, monkeypatch, temp_workspace):
360 """Full flow with cleanup confirmation."""
361 button = patient_page.patient_buttons["sub-00"]
362 patient_page._toggle_patient("sub-00", True, button)
363 assert len(patient_page.selected_patients) == 1
364 monkeypatch.setattr(QMessageBox, 'question',
365 lambda *args, **kwargs: QMessageBox.StandardButton.Yes)
366 with patch('main.ui.patient_selection_page.ToolSelectionPage'):
367 result = patient_page.next(patient_page.context)
368 assert os.path.exists(os.path.join(temp_workspace, "sub-00"))
369 assert not os.path.exists(os.path.join(temp_workspace, "sub-01"))
370 assert not os.path.exists(os.path.join(temp_workspace, "sub-02"))
373if __name__ == "__main__":
374 pytest.main([__file__, "-v", "--tb=short"])