Coverage for tests/core/test_controller.py: 99%
279 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, MagicMock, call
6from PyQt6.QtCore import QSettings
7from PyQt6.QtWidgets import QApplication, QPushButton
9from main.controller import Controller
12class TestControllerInitialization:
13 """Tests for Controller Initialization"""
15 @pytest.fixture
16 def mock_components(self):
17 """Mock UI components"""
18 with patch('main.controller.ImportPage') as MockImportPage, \
19 patch('main.controller.MainWindow') as MockMainWindow, \
20 patch('main.controller.WorkspaceTreeView') as MockTreeView, \
21 patch('main.controller.NiftiViewer') as MockNiftiViewer, \
22 patch('main.controller.get_app_dir') as mock_get_app_dir:
23 temp_dir = tempfile.mkdtemp()
24 mock_get_app_dir.return_value = MockPath(temp_dir)
26 MockImportPage.return_value = Mock()
27 MockMainWindow.return_value = Mock()
28 MockTreeView.return_value = Mock()
29 MockNiftiViewer.return_value = Mock()
31 yield {
32 'import_page': MockImportPage,
33 'main_window': MockMainWindow,
34 'tree_view': MockTreeView,
35 'nifti_viewer': MockNiftiViewer,
36 'temp_dir': temp_dir
37 }
39 shutil.rmtree(temp_dir, ignore_errors=True)
41 def test_controller_initialization(self, qtbot, mock_components):
42 """Verify correct controller initialization"""
43 controller = Controller()
45 assert controller.context is not None
46 assert controller.workspace_path is not None
47 assert controller.current_page is not None
49 def test_context_contains_required_keys(self, qtbot, mock_components):
50 """Verify that the context contains all necessary keys"""
51 controller = Controller()
53 required_keys = [
54 "workspace_path",
55 "update_main_buttons",
56 "return_to_import",
57 "history",
58 "language_changed",
59 "create_buttons",
60 "selected_files_signal",
61 "open_nifti_viewer",
62 "settings"
63 ]
65 for key in required_keys:
66 assert key in controller.context
68 def test_components_created(self, qtbot, mock_components):
69 """Verify creation of all components"""
70 controller = Controller()
72 mock_components['import_page'].assert_called_once()
73 mock_components['main_window'].assert_called_once()
74 mock_components['tree_view'].assert_called_once()
75 mock_components['nifti_viewer'].assert_called_once()
77 def test_workspace_directory_created(self, qtbot, mock_components):
78 """Verify workspace directory creation"""
79 controller = Controller()
81 workspace_path = controller.workspace_path
82 # Verify that the path exists
83 assert workspace_path is not None
85 def test_start_page_set(self, qtbot, mock_components):
86 """Verify that the start page is ImportPage"""
87 controller = Controller()
89 assert controller.start_page == controller.context["import_page"]
90 assert controller.current_page == controller.start_page
92 def test_history_initialized(self, qtbot, mock_components):
93 """Verify history initialization"""
94 controller = Controller()
96 assert len(controller.context["history"]) > 0
97 assert controller.start_page in controller.context["history"]
100class TestControllerNavigation:
101 """Tests for page navigation"""
103 @pytest.fixture
104 def controller(self, qtbot):
105 """Create controller for testing"""
106 with patch('main.controller.ImportPage'), \
107 patch('main.controller.MainWindow'), \
108 patch('main.controller.WorkspaceTreeView'), \
109 patch('main.controller.NiftiViewer'), \
110 patch('main.controller.get_app_dir') as mock_app_dir:
111 temp_dir = tempfile.mkdtemp()
112 mock_app_dir.return_value = MockPath(temp_dir)
114 controller = Controller()
116 yield controller
118 shutil.rmtree(temp_dir, ignore_errors=True)
120 def test_go_to_next_page(self, controller):
121 """Verify navigation to the next page"""
122 mock_next_page = Mock()
123 controller.current_page.next.return_value = mock_next_page
125 result = controller.go_to_next_page()
127 assert controller.current_page == mock_next_page
128 assert result == mock_next_page
130 def test_go_to_next_page_none(self, controller):
131 """Verify behavior when next returns None"""
132 controller.current_page.next.return_value = None
133 initial_page = controller.current_page
135 result = controller.go_to_next_page()
137 assert controller.current_page == initial_page
138 assert result == initial_page
140 def test_go_to_previous_page(self, controller):
141 """Verify navigation to the previous page"""
142 mock_previous_page = Mock()
143 controller.current_page.back.return_value = mock_previous_page
145 result = controller.go_to_previous_page()
147 assert controller.current_page == mock_previous_page
148 assert result == mock_previous_page
150 def test_go_to_previous_page_none(self, controller):
151 """Verify behavior when back returns None"""
152 controller.current_page.back.return_value = None
153 initial_page = controller.current_page
155 result = controller.go_to_previous_page()
157 assert controller.current_page == initial_page
158 assert result == initial_page
160 def test_return_to_import(self, controller):
161 """Verify return to import page"""
162 # Simulate navigation
163 mock_page1 = Mock()
164 mock_page2 = Mock()
165 controller.context["history"] = [controller.start_page, mock_page1, mock_page2]
166 controller.current_page = mock_page2
168 result = controller.return_to_import()
170 assert controller.current_page == controller.start_page
171 assert result == controller.start_page
172 # Verify that reset_page is called on all pages
173 mock_page1.reset_page.assert_called_once()
174 mock_page2.reset_page.assert_called_once()
177class TestControllerButtons:
178 """Tests for button management"""
180 @pytest.fixture
181 def controller(self, qtbot):
182 with patch('main.controller.ImportPage'), \
183 patch('main.controller.MainWindow'), \
184 patch('main.controller.WorkspaceTreeView'), \
185 patch('main.controller.NiftiViewer'), \
186 patch('main.controller.get_app_dir') as mock_app_dir:
187 temp_dir = tempfile.mkdtemp()
188 mock_app_dir.return_value = MockPath(temp_dir)
190 controller = Controller()
192 yield controller
194 shutil.rmtree(temp_dir, ignore_errors=True)
196 def test_create_buttons(self, controller):
197 """Verify button creation"""
198 next_btn, back_btn = controller.create_buttons()
200 assert isinstance(next_btn, QPushButton)
201 assert isinstance(back_btn, QPushButton)
202 assert controller.next_button == next_btn
203 assert controller.back_button == back_btn
205 def test_buttons_connected(self, controller):
206 """Verify that the buttons are connected"""
207 controller.create_buttons()
209 # Buttons should have connected signals
210 # (difficult to test connections directly)
211 assert controller.next_button is not None
212 assert controller.back_button is not None
214 def test_update_buttons_state_ready(self, controller):
215 """Verify button state update when ready"""
216 controller.create_buttons()
217 controller.current_page.is_ready_to_advance.return_value = True
218 controller.current_page.is_ready_to_go_back.return_value = True
220 controller.update_buttons_state()
222 assert controller.next_button.isEnabled()
223 assert controller.back_button.isEnabled()
225 def test_update_buttons_state_not_ready(self, controller):
226 """Verify button state update when not ready"""
227 controller.create_buttons()
228 controller.current_page.is_ready_to_advance.return_value = False
229 controller.current_page.is_ready_to_go_back.return_value = False
231 controller.update_buttons_state()
233 assert not controller.next_button.isEnabled()
234 assert not controller.back_button.isEnabled()
236 def test_button_click_next(self, controller, qtbot):
237 """Verify click on next button"""
238 controller.create_buttons()
239 mock_next_page = Mock()
240 controller.current_page.next.return_value = mock_next_page
242 with qtbot.waitSignal(controller.next_button.clicked, timeout=1000):
243 controller.next_button.click()
245 assert controller.current_page == mock_next_page
247 def test_button_click_back(self, controller, qtbot):
248 """Verify click on back button"""
249 controller.create_buttons()
250 mock_prev_page = Mock()
251 controller.current_page.back.return_value = mock_prev_page
253 with qtbot.waitSignal(controller.back_button.clicked, timeout=1000):
254 controller.back_button.click()
256 assert controller.current_page == mock_prev_page
259class TestControllerLanguage:
260 """Tests for language management"""
262 @pytest.fixture
263 def controller(self, qtbot):
264 with patch('main.controller.ImportPage'), \
265 patch('main.controller.MainWindow'), \
266 patch('main.controller.WorkspaceTreeView'), \
267 patch('main.controller.NiftiViewer'), \
268 patch('main.controller.get_app_dir') as mock_app_dir:
269 temp_dir = tempfile.mkdtemp()
270 mock_app_dir.return_value = MockPath(temp_dir)
272 controller = Controller()
274 yield controller
276 shutil.rmtree(temp_dir, ignore_errors=True)
278 def test_set_language(self, controller):
279 """Verify language setting"""
280 with patch.object(controller.translator, 'load', return_value=True):
281 controller.set_language("it")
283 # Verify that it is saved
284 assert controller.settings.value("language") == "it"
286 def test_save_language(self, controller):
287 """Verify saving language"""
288 controller.save_language("it")
290 saved_lang = controller.settings.value("language")
291 assert saved_lang == "it"
293 def test_language_changed_signal(self, controller, qtbot):
294 """Verify language change signal emission"""
295 with patch.object(controller.translator, 'load', return_value=True):
296 with qtbot.waitSignal(controller.language_changed, timeout=1000):
297 controller.language_changed.emit("it")
299 def test_translator_load_called(self, controller):
300 """Verify that translator.load is called"""
301 with patch.object(controller.translator, 'load', return_value=True) as mock_load:
302 controller.set_language("en")
304 mock_load.assert_called_once()
305 # Verify that the path contains the language code
306 call_args = str(mock_load.call_args)
307 assert "en.qm" in call_args
310class TestControllerNiftiViewer:
311 """Tests for opening NIfTI viewer"""
313 @pytest.fixture
314 def controller(self, qtbot):
315 with patch('main.controller.ImportPage'), \
316 patch('main.controller.MainWindow'), \
317 patch('main.controller.WorkspaceTreeView'), \
318 patch('main.controller.NiftiViewer') as MockViewer, \
319 patch('main.controller.get_app_dir') as mock_app_dir:
320 temp_dir = tempfile.mkdtemp()
321 mock_app_dir.return_value = MockPath(temp_dir)
323 mock_viewer = Mock()
324 MockViewer.return_value = mock_viewer
326 controller = Controller()
328 yield controller
330 shutil.rmtree(temp_dir, ignore_errors=True)
332 def test_open_nifti_viewer(self, controller):
333 """Verify NIfTI viewer opening"""
334 test_path = "/path/to/scan.nii"
336 controller.open_nifti_viewer(test_path)
338 controller.context["nifti_viewer"].open_file.assert_called_once_with(test_path)
339 controller.context["nifti_viewer"].show.assert_called_once()
342class TestControllerStart:
343 """Tests for application start"""
345 @pytest.fixture
346 def controller(self, qtbot):
347 with patch('main.controller.ImportPage'), \
348 patch('main.controller.MainWindow') as MockMainWindow, \
349 patch('main.controller.WorkspaceTreeView'), \
350 patch('main.controller.NiftiViewer'), \
351 patch('main.controller.get_app_dir') as mock_app_dir:
352 temp_dir = tempfile.mkdtemp()
353 mock_app_dir.return_value = MockPath(temp_dir)
355 mock_window = Mock()
356 MockMainWindow.return_value = mock_window
358 controller = Controller()
360 yield controller
362 shutil.rmtree(temp_dir, ignore_errors=True)
364 def test_start_shows_main_window(self, controller):
365 """Verify that start() shows the main window"""
366 controller.start()
368 controller.main_window.show.assert_called_once()
371class TestControllerSettings:
372 """Tests for settings management"""
374 @pytest.fixture
375 def clean_settings(self):
376 """Cleans settings before and after test"""
377 settings = QSettings("GliAAns")
378 settings.clear()
379 yield settings
380 settings.clear()
382 def test_loads_saved_language(self, qtbot, clean_settings):
383 """Verify loading of saved language"""
384 clean_settings.setValue("language", "it")
386 with patch('main.controller.ImportPage'), \
387 patch('main.controller.MainWindow'), \
388 patch('main.controller.WorkspaceTreeView'), \
389 patch('main.controller.NiftiViewer'), \
390 patch('main.controller.get_app_dir') as mock_app_dir:
391 temp_dir = tempfile.mkdtemp()
392 mock_app_dir.return_value = MockPath(temp_dir)
394 controller = Controller()
396 assert controller.saved_lang == "it"
398 shutil.rmtree(temp_dir, ignore_errors=True)
400 def test_default_language_en(self, qtbot, clean_settings):
401 """Verify default language is English"""
402 with patch('main.controller.ImportPage'), \
403 patch('main.controller.MainWindow'), \
404 patch('main.controller.WorkspaceTreeView'), \
405 patch('main.controller.NiftiViewer'), \
406 patch('main.controller.get_app_dir') as mock_app_dir:
407 temp_dir = tempfile.mkdtemp()
408 mock_app_dir.return_value = MockPath(temp_dir)
410 controller = Controller()
412 assert controller.saved_lang == "en"
414 shutil.rmtree(temp_dir, ignore_errors=True)
416 @patch('main.controller.set_log_level')
417 def test_debug_log_setting(self, mock_set_log_level, qtbot, clean_settings):
418 """Verify debug log setting"""
419 import logging
420 clean_settings.setValue("debug_log", True)
422 with patch('main.controller.ImportPage'), \
423 patch('main.controller.MainWindow'), \
424 patch('main.controller.WorkspaceTreeView'), \
425 patch('main.controller.NiftiViewer'), \
426 patch('main.controller.get_app_dir') as mock_app_dir:
427 temp_dir = tempfile.mkdtemp()
428 mock_app_dir.return_value = MockPath(temp_dir)
430 controller = Controller()
432 mock_set_log_level.assert_called_with(logging.DEBUG)
434 shutil.rmtree(temp_dir, ignore_errors=True)
437class TestControllerIntegration:
438 """Integration tests for complete flows"""
440 @pytest.fixture
441 def controller(self, qtbot):
442 with patch('main.controller.ImportPage') as MockImport, \
443 patch('main.controller.MainWindow'), \
444 patch('main.controller.WorkspaceTreeView'), \
445 patch('main.controller.NiftiViewer'), \
446 patch('main.controller.get_app_dir') as mock_app_dir:
447 temp_dir = tempfile.mkdtemp()
448 mock_app_dir.return_value = MockPath(temp_dir)
450 # Setup mock pages with realistic behaviors
451 mock_import = Mock()
452 mock_import.is_ready_to_advance.return_value = True
453 mock_import.is_ready_to_go_back.return_value = False
454 MockImport.return_value = mock_import
456 controller = Controller()
458 yield controller
460 shutil.rmtree(temp_dir, ignore_errors=True)
462 def test_full_navigation_flow(self, controller):
463 """Test full navigation flow"""
464 # Setup mock pages
465 page2 = Mock()
466 page2.is_ready_to_advance.return_value = True
467 page2.is_ready_to_go_back.return_value = True
468 page2.back.return_value = controller.start_page
470 controller.current_page.next.return_value = page2
472 # Navigate forward
473 controller.go_to_next_page()
474 assert controller.current_page == page2
476 # Navigate back
477 controller.go_to_previous_page()
478 assert controller.current_page == controller.start_page
480 def test_navigation_with_button_updates(self, controller):
481 """Test navigation with button updates"""
482 controller.create_buttons()
484 page2 = Mock()
485 page2.is_ready_to_advance.return_value = False
486 page2.is_ready_to_go_back.return_value = True
488 controller.current_page.next.return_value = page2
490 # Navigate
491 controller.go_to_next_page()
493 # Buttons should be updated
494 assert not controller.next_button.isEnabled()
495 assert controller.back_button.isEnabled()
497 def test_return_to_import_resets_all(self, controller):
498 """Test that return_to_import resets everything"""
499 # Simulate deep navigation
500 page2 = Mock()
501 page3 = Mock()
502 controller.context["history"] = [controller.start_page, page2, page3]
503 controller.current_page = page3
505 # Return to import
506 controller.return_to_import()
508 # Verify reset
509 page2.reset_page.assert_called_once()
510 page3.reset_page.assert_called_once()
511 assert controller.current_page == controller.start_page
514# Helper class for Path mock
515class MockPath:
516 """Mock of pathlib.Path for testing"""
518 def __init__(self, path_str):
519 self.path_str = path_str
521 def __str__(self):
522 return self.path_str
524 def __truediv__(self, other):
525 return MockPath(os.path.join(self.path_str, other))
527 def __repr__(self):
528 return f"MockPath('{self.path_str}')"
531if __name__ == "__main__":
532 pytest.main([__file__, "-v", "--tb=short"])