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

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 

10 

11from main.ui.patient_selection_page import PatientSelectionPage 

12 

13 

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 

21 

22 

23class TestPatientSelectionPageSetup: 

24 """Tests for PatientSelectionPage initialization.""" 

25 

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) 

31 

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() != "" 

36 

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 

41 

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 

46 

47 def test_column_count_default(self, patient_page): 

48 """Ensure default column count is valid.""" 

49 assert patient_page.column_count >= 1 

50 

51 

52class TestPatientSelectionPagePatientLoading: 

53 """Tests for patient loading and directory scanning.""" 

54 

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) 

65 

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 

74 

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 

80 

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 

87 

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 

91 

92 

93class TestPatientSelectionPageSelection: 

94 """Tests for patient selection logic.""" 

95 

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" 

102 

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 

109 

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() 

115 

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() 

122 

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() 

130 

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 

139 

140 

141class TestPatientSelectionPageReadiness: 

142 """Tests for readiness logic.""" 

143 

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() 

148 

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() 

153 

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() 

157 

158 

159class TestPatientSelectionPageNavigation: 

160 """Tests for page navigation.""" 

161 

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() 

167 

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() 

177 

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")) 

188 

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 

196 

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"] 

206 

207 

208class TestPatientSelectionPageCleanup: 

209 """Tests for cleanup operations.""" 

210 

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) 

220 

221 @pytest.fixture 

222 def patient_page(self, qtbot, mock_context): 

223 page = PatientSelectionPage(mock_context, Mock()) 

224 qtbot.addWidget(page) 

225 return page 

226 

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")) 

236 

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")) 

246 

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 

256 

257 

258class TestPatientSelectionPageLayout: 

259 """Tests for dynamic layout behavior.""" 

260 

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) 

268 

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 

274 

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 

280 

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 

288 

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 

295 

296 

297class TestPatientSelectionPageReset: 

298 """Tests for page reset behavior.""" 

299 

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 

305 

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 

311 

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 

317 

318 

319class TestPatientSelectionPageTranslation: 

320 """Tests for UI translation updates.""" 

321 

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() != "" 

326 

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() != "" 

332 

333 

334class TestPatientSelectionPageIntegration: 

335 """Integration tests for full user workflows.""" 

336 

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) 

347 

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 

358 

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")) 

371 

372 

373if __name__ == "__main__": 

374 pytest.main([__file__, "-v", "--tb=short"])