Coverage for tests/ui/test_import_page.py: 98%

235 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, MagicMock, patch, PropertyMock 

6from PyQt6 import QtCore, QtWidgets 

7from PyQt6.QtCore import QSettings, pyqtSignal, QObject, QMimeData, QUrl, Qt 

8from PyQt6.QtWidgets import QMessageBox, QProgressDialog 

9from PyQt6.QtGui import QDropEvent, QDragEnterEvent 

10 

11from main.ui.import_page import ImportPage 

12 

13 

14@pytest.fixture 

15def import_page(qtbot, mock_context): 

16 page = ImportPage(mock_context) 

17 qtbot.addWidget(page) 

18 return page 

19 

20class TestImportPageSetup: 

21 """Tests for ImportPage initialization""" 

22 

23 def test_page_initialization(self, import_page): 

24 """Verify correct initialization""" 

25 assert import_page.workspace_path is not None 

26 assert import_page.threads == [] 

27 assert import_page.progress_dialogs == [] 

28 assert import_page.acceptDrops() 

29 

30 def test_drop_label_created(self, import_page): 

31 """Verify that the label is created""" 

32 assert import_page.drop_label is not None 

33 assert import_page.drop_label.text() != "" 

34 

35 def test_stylesheet_applied(self, import_page): 

36 """Verify that the stylesheet is applied""" 

37 stylesheet = import_page.styleSheet() 

38 assert "border" in stylesheet.lower() 

39 assert "dashed" in stylesheet.lower() 

40 

41 

42class TestImportPageReadiness: 

43 """Tests for advance/return logic""" 

44 

45 @pytest.fixture 

46 def temp_workspace(self): 

47 temp_dir = tempfile.mkdtemp() 

48 yield temp_dir 

49 shutil.rmtree(temp_dir, ignore_errors=True) 

50 

51 def test_not_ready_when_workspace_empty(self, import_page): 

52 """Verify it is not ready if workspace is empty""" 

53 assert not import_page.is_ready_to_advance() 

54 

55 def test_ready_when_workspace_has_folder(self, import_page, temp_workspace): 

56 """Verify it is ready if workspace has folders""" 

57 test_folder = os.path.join(temp_workspace, "patient_data") 

58 os.makedirs(test_folder) 

59 

60 assert import_page.is_ready_to_advance() 

61 

62 def test_ignores_hidden_files(self, import_page, temp_workspace): 

63 """Verify that hidden files are ignored""" 

64 hidden_file = os.path.join(temp_workspace, ".hidden") 

65 with open(hidden_file, "w") as f: 

66 f.write("test") 

67 

68 assert not import_page.is_ready_to_advance() 

69 

70 def test_cannot_go_back(self, import_page): 

71 """Verify that going back is not possible""" 

72 assert not import_page.is_ready_to_go_back() 

73 

74 

75class TestImportPageNavigation: 

76 """Tests for page navigation""" 

77 

78 

79 

80 def test_next_creates_patient_selection_page(self, import_page, mock_context): 

81 """Verify that next() creates PatientSelectionPage""" 

82 next_page = import_page.next(mock_context) 

83 

84 assert next_page is not None 

85 assert import_page.next_page is not None 

86 assert next_page in mock_context["history"] 

87 

88 def test_next_returns_cached_page(self, import_page, mock_context): 

89 """Verify that next() returns the cached page""" 

90 first_call = import_page.next(mock_context) 

91 second_call = import_page.next(mock_context) 

92 

93 assert first_call is second_call 

94 

95 def test_back_returns_false(self, import_page): 

96 """Verify that back() returns False""" 

97 assert import_page.back() == False 

98 

99 

100class TestImportPageDragDrop: 

101 """Tests for drag & drop functionality""" 

102 

103 @pytest.fixture 

104 def temp_source_folder(self): 

105 """Creates source folder for drag&drop""" 

106 temp_dir = tempfile.mkdtemp() 

107 test_file = os.path.join(temp_dir, "test.txt") 

108 with open(test_file, "w") as f: 

109 f.write("test content") 

110 yield temp_dir 

111 shutil.rmtree(temp_dir, ignore_errors=True) 

112 

113 def test_drag_enter_accepts_urls(self, import_page): 

114 """Verify that dragEnterEvent accepts URLs""" 

115 mime_data = QMimeData() 

116 mime_data.setUrls([QUrl.fromLocalFile("/tmp/test")]) 

117 

118 event = Mock() 

119 event.mimeData.return_value = mime_data 

120 

121 import_page.dragEnterEvent(event) 

122 event.acceptProposedAction.assert_called_once() 

123 

124 def test_drop_event_handles_folder(self, import_page, temp_source_folder): 

125 """Verify that dropEvent handles folders""" 

126 mime_data = QMimeData() 

127 mime_data.setUrls([QUrl.fromLocalFile(temp_source_folder)]) 

128 

129 event = Mock() 

130 event.mimeData.return_value = mime_data 

131 

132 with patch.object(import_page, '_handle_import') as mock_handle: 

133 import_page.dropEvent(event) 

134 mock_handle.assert_called_once() 

135 assert temp_source_folder in mock_handle.call_args[0][0] 

136 

137 

138class TestImportPageDialogs: 

139 """Tests for dialogs and user interactions""" 

140 

141 @pytest.fixture 

142 def temp_source_folder(self): 

143 temp_dir = tempfile.mkdtemp() 

144 yield temp_dir 

145 shutil.rmtree(temp_dir, ignore_errors=True) 

146 

147 def test_mouse_press_opens_dialog(self, import_page): 

148 """Verify that left click opens the dialog""" 

149 with patch.object(import_page, 'open_folder_dialog') as mock_dialog: 

150 event = Mock() 

151 event.button.return_value = Qt.MouseButton.LeftButton 

152 

153 import_page.mousePressEvent(event) 

154 mock_dialog.assert_called_once() 

155 

156 def test_open_folder_dialog_configuration(self, import_page, monkeypatch): 

157 """Verify folder dialog configuration""" 

158 mock_dialog = Mock() 

159 mock_dialog.exec.return_value = False 

160 mock_dialog.findChildren.return_value = [] 

161 

162 with patch('main.ui.import_page.QFileDialog', return_value=mock_dialog): 

163 import_page.open_folder_dialog() 

164 

165 # Verify that the dialog is configured correctly 

166 mock_dialog.setFileMode.assert_called_once() 

167 assert mock_dialog.setOption.call_count >= 1 

168 

169 

170class TestImportPageThreads: 

171 """Tests for import thread handling""" 

172 

173 @pytest.fixture 

174 def temp_source_folder(self): 

175 temp_dir = tempfile.mkdtemp() 

176 test_file = os.path.join(temp_dir, "test.txt") 

177 with open(test_file, "w") as f: 

178 f.write("test") 

179 yield temp_dir 

180 shutil.rmtree(temp_dir, ignore_errors=True) 

181 

182 def test_handle_import_creates_thread(self, import_page, temp_source_folder): 

183 """Verify that _handle_import creates a thread""" 

184 with patch('main.ui.import_page.ImportThread') as MockThread, \ 

185 patch('main.ui.import_page.QProgressDialog'): 

186 mock_thread_instance = Mock() 

187 mock_thread_instance.finished = Mock() 

188 mock_thread_instance.finished.connect = Mock() 

189 mock_thread_instance.error = Mock() 

190 mock_thread_instance.error.connect = Mock() 

191 mock_thread_instance.progress = Mock() 

192 mock_thread_instance.progress.connect = Mock() 

193 MockThread.return_value = mock_thread_instance 

194 

195 import_page._handle_import([temp_source_folder]) 

196 

197 assert len(import_page.threads) == 1 

198 mock_thread_instance.start.assert_called_once() 

199 

200 def test_handle_import_creates_progress_dialog(self, import_page, temp_source_folder): 

201 """Verify that _handle_import creates a progress dialog""" 

202 with patch('main.ui.import_page.ImportThread'), \ 

203 patch('main.ui.import_page.QProgressDialog') as MockDialog: 

204 mock_dialog_instance = Mock() 

205 mock_dialog_instance.canceled = Mock() 

206 mock_dialog_instance.canceled.connect = Mock() 

207 MockDialog.return_value = mock_dialog_instance 

208 

209 import_page._handle_import([temp_source_folder]) 

210 

211 assert len(import_page.progress_dialogs) == 1 

212 MockDialog.assert_called_once() 

213 

214 def test_on_import_finished_removes_thread(self, import_page): 

215 """Verify that on_import_finished removes the thread""" 

216 mock_thread = Mock() 

217 import_page.threads = [mock_thread] 

218 

219 mock_dialog = Mock() 

220 import_page.progress_dialogs = [mock_dialog] 

221 

222 with patch.object(import_page, 'sender', return_value=mock_thread): 

223 import_page.on_import_finished() 

224 

225 assert len(import_page.threads) == 0 

226 mock_dialog.close.assert_called_once() 

227 

228 def test_on_import_finished_updates_buttons(self, import_page, mock_context): 

229 """Verify that on_import_finished updates the buttons""" 

230 mock_thread = Mock() 

231 import_page.threads = [mock_thread] 

232 import_page.progress_dialogs = [Mock()] 

233 

234 with patch.object(import_page, 'sender', return_value=mock_thread): 

235 import_page.on_import_finished() 

236 

237 mock_context["update_main_buttons"].assert_called_once() 

238 

239 def test_on_import_error_shows_message(self, import_page, monkeypatch): 

240 """Verify that on_import_error shows a message""" 

241 mock_thread = Mock() 

242 import_page.threads = [mock_thread] 

243 

244 mock_dialog = Mock() 

245 import_page.progress_dialogs = [mock_dialog] 

246 

247 message_shown = False 

248 

249 def mock_critical(*args, **kwargs): 

250 nonlocal message_shown 

251 message_shown = True 

252 

253 monkeypatch.setattr(QMessageBox, 'critical', mock_critical) 

254 

255 with patch.object(import_page, 'sender', return_value=mock_thread): 

256 import_page.on_import_error("Test error") 

257 

258 assert message_shown 

259 mock_dialog.close.assert_called_once() 

260 

261 def test_on_import_error_handles_removed_thread(self, import_page): 

262 """Verify that on_import_error handles already removed threads""" 

263 mock_thread = Mock() 

264 # Thread not in list 

265 

266 with patch.object(import_page, 'sender', return_value=mock_thread): 

267 # Should not raise exceptions 

268 import_page.on_import_error("Test error") 

269 

270 def test_on_import_canceled_terminates_thread(self, import_page): 

271 """Verify that on_import_canceled terminates the thread""" 

272 mock_thread = Mock() 

273 mock_thread.cancel = Mock() 

274 mock_thread.terminate = Mock() 

275 mock_thread.wait = Mock() 

276 import_page.threads = [mock_thread] 

277 

278 mock_dialog = Mock() 

279 import_page.progress_dialogs = [mock_dialog] 

280 

281 with patch.object(import_page, 'sender', return_value=mock_dialog): 

282 import_page.on_import_canceled() 

283 

284 mock_thread.cancel.assert_called_once() 

285 mock_thread.terminate.assert_called_once() 

286 mock_thread.wait.assert_called_once() 

287 assert len(import_page.threads) == 0 

288 

289 

290class TestImportPageCleanup: 

291 """Tests for resource cleanup""" 

292 

293 def test_close_event_cleans_dialogs(self, import_page): 

294 """Verify that closeEvent cleans up dialogs""" 

295 mock_dialog = Mock() 

296 import_page.progress_dialogs = [mock_dialog] 

297 

298 from PyQt6.QtGui import QCloseEvent 

299 event = QCloseEvent() 

300 

301 import_page.closeEvent(event) 

302 

303 mock_dialog.destroy.assert_called_once() 

304 assert len(import_page.progress_dialogs) == 0 

305 

306 def test_close_event_cancels_threads(self, import_page): 

307 """Verify that closeEvent cancels threads""" 

308 mock_thread = Mock() 

309 mock_thread.cancel = Mock() 

310 import_page.threads = [mock_thread] 

311 

312 from PyQt6.QtGui import QCloseEvent 

313 event = QCloseEvent() 

314 

315 import_page.closeEvent(event) 

316 

317 mock_thread.cancel.assert_called_once() 

318 assert len(import_page.threads) == 0 

319 

320 

321class TestImportPageTranslation: 

322 """Tests for translations""" 

323 

324 def test_translate_ui_called_on_init(self, import_page): 

325 """Verify that _translate_ui is called on init""" 

326 assert import_page.drop_label.text() != "" 

327 

328 def test_translate_ui_updates_label(self, import_page): 

329 """Verify that _translate_ui updates the label""" 

330 original_text = import_page.drop_label.text() 

331 import_page._translate_ui() 

332 # The text should be the same (or translated) 

333 assert import_page.drop_label.text() != "" 

334 

335 def test_language_changed_signal_connected(self, import_page, signal_emitter, qtbot): 

336 """Verify that the language_changed signal is connected""" 

337 with patch.object(import_page, '_translate_ui') as mock_translate: 

338 signal_emitter.language_changed.emit("it") 

339 qtbot.wait(100) # Wait for signal processing 

340 # If connected, _translate_ui should be called 

341 

342# Integration tests 

343class TestImportPageIntegration: 

344 """Integration tests for complete flows""" 

345 @pytest.fixture 

346 def temp_workspace(self): 

347 temp_dir = tempfile.mkdtemp() 

348 yield temp_dir 

349 shutil.rmtree(temp_dir, ignore_errors=True) 

350 

351 @pytest.fixture 

352 def temp_source_folder(self): 

353 temp_dir = tempfile.mkdtemp() 

354 # Create test structure 

355 patient_dir = os.path.join(temp_dir, "patient_001") 

356 os.makedirs(patient_dir) 

357 with open(os.path.join(patient_dir, "data.txt"), "w") as f: 

358 f.write("patient data") 

359 yield temp_dir 

360 shutil.rmtree(temp_dir, ignore_errors=True) 

361 

362 def test_full_import_workflow(self, import_page, temp_source_folder, mock_context): 

363 """Test of the complete import workflow""" 

364 # Initially not ready 

365 assert not import_page.is_ready_to_advance() 

366 

367 # Add folder to workspace 

368 test_folder = os.path.join(import_page.workspace_path, "patient_001") 

369 os.makedirs(test_folder) 

370 

371 # Now it should be ready 

372 assert import_page.is_ready_to_advance() 

373 

374 # Navigate to the next page 

375 next_page = import_page.next(mock_context) 

376 assert next_page is not None 

377 

378if __name__ == "__main__": 

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