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
« 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
11from main.ui.import_page import ImportPage
14@pytest.fixture
15def import_page(qtbot, mock_context):
16 page = ImportPage(mock_context)
17 qtbot.addWidget(page)
18 return page
20class TestImportPageSetup:
21 """Tests for ImportPage initialization"""
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()
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() != ""
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()
42class TestImportPageReadiness:
43 """Tests for advance/return logic"""
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)
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()
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)
60 assert import_page.is_ready_to_advance()
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")
68 assert not import_page.is_ready_to_advance()
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()
75class TestImportPageNavigation:
76 """Tests for page navigation"""
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)
84 assert next_page is not None
85 assert import_page.next_page is not None
86 assert next_page in mock_context["history"]
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)
93 assert first_call is second_call
95 def test_back_returns_false(self, import_page):
96 """Verify that back() returns False"""
97 assert import_page.back() == False
100class TestImportPageDragDrop:
101 """Tests for drag & drop functionality"""
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)
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")])
118 event = Mock()
119 event.mimeData.return_value = mime_data
121 import_page.dragEnterEvent(event)
122 event.acceptProposedAction.assert_called_once()
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)])
129 event = Mock()
130 event.mimeData.return_value = mime_data
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]
138class TestImportPageDialogs:
139 """Tests for dialogs and user interactions"""
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)
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
153 import_page.mousePressEvent(event)
154 mock_dialog.assert_called_once()
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 = []
162 with patch('main.ui.import_page.QFileDialog', return_value=mock_dialog):
163 import_page.open_folder_dialog()
165 # Verify that the dialog is configured correctly
166 mock_dialog.setFileMode.assert_called_once()
167 assert mock_dialog.setOption.call_count >= 1
170class TestImportPageThreads:
171 """Tests for import thread handling"""
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)
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
195 import_page._handle_import([temp_source_folder])
197 assert len(import_page.threads) == 1
198 mock_thread_instance.start.assert_called_once()
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
209 import_page._handle_import([temp_source_folder])
211 assert len(import_page.progress_dialogs) == 1
212 MockDialog.assert_called_once()
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]
219 mock_dialog = Mock()
220 import_page.progress_dialogs = [mock_dialog]
222 with patch.object(import_page, 'sender', return_value=mock_thread):
223 import_page.on_import_finished()
225 assert len(import_page.threads) == 0
226 mock_dialog.close.assert_called_once()
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()]
234 with patch.object(import_page, 'sender', return_value=mock_thread):
235 import_page.on_import_finished()
237 mock_context["update_main_buttons"].assert_called_once()
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]
244 mock_dialog = Mock()
245 import_page.progress_dialogs = [mock_dialog]
247 message_shown = False
249 def mock_critical(*args, **kwargs):
250 nonlocal message_shown
251 message_shown = True
253 monkeypatch.setattr(QMessageBox, 'critical', mock_critical)
255 with patch.object(import_page, 'sender', return_value=mock_thread):
256 import_page.on_import_error("Test error")
258 assert message_shown
259 mock_dialog.close.assert_called_once()
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
266 with patch.object(import_page, 'sender', return_value=mock_thread):
267 # Should not raise exceptions
268 import_page.on_import_error("Test error")
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]
278 mock_dialog = Mock()
279 import_page.progress_dialogs = [mock_dialog]
281 with patch.object(import_page, 'sender', return_value=mock_dialog):
282 import_page.on_import_canceled()
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
290class TestImportPageCleanup:
291 """Tests for resource cleanup"""
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]
298 from PyQt6.QtGui import QCloseEvent
299 event = QCloseEvent()
301 import_page.closeEvent(event)
303 mock_dialog.destroy.assert_called_once()
304 assert len(import_page.progress_dialogs) == 0
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]
312 from PyQt6.QtGui import QCloseEvent
313 event = QCloseEvent()
315 import_page.closeEvent(event)
317 mock_thread.cancel.assert_called_once()
318 assert len(import_page.threads) == 0
321class TestImportPageTranslation:
322 """Tests for translations"""
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() != ""
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() != ""
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
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)
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)
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()
367 # Add folder to workspace
368 test_folder = os.path.join(import_page.workspace_path, "patient_001")
369 os.makedirs(test_folder)
371 # Now it should be ready
372 assert import_page.is_ready_to_advance()
374 # Navigate to the next page
375 next_page = import_page.next(mock_context)
376 assert next_page is not None
378if __name__ == "__main__":
379 pytest.main([__file__, "-v", "--tb=short"])