Coverage for tests/ui/test_main_window.py: 99%
196 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
5import logging
6from unittest.mock import Mock, MagicMock, patch
7from PyQt6 import QtCore, QtWidgets
8from PyQt6.QtWidgets import QMessageBox, QPushButton, QApplication
9from PyQt6.QtCore import QSettings, pyqtSignal, QObject, Qt
11# Ensure the path is correct to import MainWindow
12from main.ui.main_window import MainWindow
15# A QObject is necessary to host signals
16class SignalHost(QObject):
17 language_changed = pyqtSignal(str)
20@pytest.fixture(scope="function")
21def mock_context(request, qtbot):
22 """
23 Creates a complete mock context for the MainWindow.
24 Runs once per test function.
25 """
26 # 1. Create a real temporary workspace
27 temp_dir = tempfile.mkdtemp()
29 # Create some dummy files for the 'clear_folder' test
30 open(os.path.join(temp_dir, "file1.txt"), "w").close()
31 os.mkdir(os.path.join(temp_dir, "subdir"))
33 # 2. Create a real but isolated QSettings
34 settings = QSettings(QSettings.Format.IniFormat, QSettings.Scope.UserScope, "GliAAnsTest", "GliAAnsTestApp")
35 settings.clear() # Ensures a clean state
37 # 3. Signal object
38 signal_host = SignalHost()
40 # 4. Assemble the context
41 context = {
42 "language_changed": signal_host.language_changed,
43 "settings": settings,
44 "workspace_path": temp_dir,
45 "create_buttons": lambda: (QPushButton("Back"), QPushButton("Next")),
46 "tree_view": MagicMock(),
47 "import_page": MagicMock(),
48 "return_to_import": Mock(),
49 "update_main_buttons": Mock(),
51 "_signal_host_ref": signal_host
52 }
54 # 5. Add teardown (cleanup)
55 def cleanup():
56 shutil.rmtree(temp_dir)
57 settings.clear()
59 request.addfinalizer(cleanup)
61 return context
64@pytest.fixture
65def main_window(qtbot, mock_context):
66 """
67 Fixture that creates the MainWindow and "neutralizes" it for testing.
68 """
69 window = MainWindow(mock_context)
70 qtbot.addWidget(window)
72 try:
73 window.destroyed.disconnect(QApplication.instance().quit)
74 except TypeError:
75 pass
77 window.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False)
79 return window
81class TestMainWindowSetup:
82 """Tests for MainWindow initial configuration"""
84 def test_window_initialization(self, main_window):
85 """Verify that the window initializes correctly"""
86 assert main_window.windowTitle() != ""
87 assert main_window.width() >= 950
88 assert main_window.height() >= 650 # Corrected from 650 to 700 as per code
90 def test_menu_bar_created(self, main_window):
91 """Verify that the menu bar is created with all menus"""
92 menu_bar = main_window.menuBar()
93 assert menu_bar is not None
95 menus = [action.menu().title() for action in menu_bar.actions() if action.menu()]
96 assert "File" in menus
97 assert "Workspace" in menus
98 assert "Settings" in menus or "Impostazioni" in menus
100 def test_splitter_created(self, main_window):
101 """Verify that the splitter has been created"""
102 assert main_window.splitter is not None
103 assert main_window.splitter.orientation() == QtCore.Qt.Orientation.Horizontal
105 def test_footer_with_buttons(self, main_window):
106 """Verify that the footer contains the buttons"""
107 assert main_window.footer is not None
108 assert main_window.next_button is not None
109 assert main_window.back_button is not None
110 assert isinstance(main_window.next_button, QPushButton)
111 assert isinstance(main_window.back_button, QPushButton)
113 def test_language_actions_created(self, main_window):
114 """Verify that language actions are created"""
115 assert "en" in main_window.language_actions
116 assert "it" in main_window.language_actions
117 assert main_window.language_actions["en"].isCheckable()
118 assert main_window.language_actions["it"].isCheckable()
121class TestLanguageManagement:
122 """Tests for language management"""
124 def test_set_language_emits_signal(self, main_window, qtbot):
125 """Verify that set_language emits the correct signal"""
126 with qtbot.waitSignal(main_window.context["language_changed"], timeout=1000) as blocker:
127 main_window.set_language("it")
129 assert blocker.args == ["it"]
131 def test_set_language_checks_correct_action(self, main_window):
132 """Verify that the correct action is checked"""
133 main_window.set_language("it")
134 assert main_window.language_actions["it"].isChecked()
136 main_window.set_language("en")
137 assert main_window.language_actions["en"].isChecked()
139 def test_language_action_group_exclusive(self, main_window):
140 """Verify that only one language can be selected"""
141 main_window.language_actions["en"].trigger()
142 assert main_window.language_actions["en"].isChecked()
143 assert not main_window.language_actions["it"].isChecked()
145 main_window.language_actions["it"].trigger()
146 assert main_window.language_actions["it"].isChecked()
147 assert not main_window.language_actions["en"].isChecked()
150class TestWorkspaceOperations:
151 """Tests for workspace operations"""
153 def test_clear_folder_confirms_deletion(self, main_window, monkeypatch):
154 """Verify that confirmation is requested before deleting"""
155 monkeypatch.setattr(QMessageBox, 'question',
156 lambda *args, **kwargs: QMessageBox.StandardButton.No)
158 initial_files = os.listdir(main_window.workspace_path)
159 main_window.clear_folder(
160 folder_path=main_window.workspace_path,
161 folder_name="test",
162 return_to_import=False
163 )
165 # Files should still be present
166 assert len(os.listdir(main_window.workspace_path)) == len(initial_files)
167 assert len(main_window.threads) == 0 # No threads started
169 def test_clear_folder_deletes_on_confirmation(self, main_window, monkeypatch, qtbot):
170 """Verify that files are deleted after confirmation"""
171 monkeypatch.setattr(QMessageBox, 'question',
172 lambda *args, **kwargs: QMessageBox.StandardButton.Yes)
174 # 1. Save the INITIAL number of files
175 initial_file_count = len(os.listdir(main_window.workspace_path))
177 # The workspace has 2 items (file1.txt, subdir)
178 assert initial_file_count > 0
180 main_window.clear_folder(
181 folder_path=main_window.workspace_path,
182 folder_name="test",
183 return_to_import=False
184 )
186 # 2. Verify that as many threads were created as there were INITIAL files
187 assert len(main_window.threads) == initial_file_count
189 # Wait for threads to finish so as not to "pollute" the next test
190 for t in main_window.threads:
191 t.wait(1000)
193 def test_clear_folder_returns_to_import(self, main_window, monkeypatch, qtbot):
194 """Verify that return_to_import is called when requested"""
195 monkeypatch.setattr(QMessageBox, 'question',
196 lambda *args, **kwargs: QMessageBox.StandardButton.Yes)
198 main_window.clear_folder(
199 folder_path=main_window.workspace_path,
200 folder_name="workspace",
201 return_to_import=True
202 )
204 # Wait for threads to finish so as not to "pollute" the next test
205 for t in main_window.threads:
206 t.wait(1000)
208 main_window.context["return_to_import"].assert_called_once()
211class TestThreadManagement:
212 """Tests for thread management"""
214 def test_new_thread_adds_to_list(self, main_window):
215 """Verify that a new thread is added to the list"""
216 mock_thread = Mock(spec=['error', 'finished', 'start', 'wait'])
217 mock_thread.error = Mock()
218 mock_thread.error.connect = Mock()
219 mock_thread.error.disconnect = Mock()
220 mock_thread.finished = Mock()
221 mock_thread.finished.connect = Mock()
222 mock_thread.finished.disconnect = Mock()
224 initial_count = len(main_window.threads)
225 main_window.new_thread(mock_thread)
227 assert len(main_window.threads) == initial_count + 1
228 mock_thread.start.assert_called_once()
230 def test_copydelete_thread_error_removes_thread(self, main_window, monkeypatch):
231 """Verify that a thread with error is removed"""
232 mock_thread = Mock()
233 main_window.threads.append(mock_thread)
235 monkeypatch.setattr(QMessageBox, 'warning', lambda *args, **kwargs: None)
236 monkeypatch.setattr(main_window, 'sender', lambda: mock_thread)
238 main_window.copydelete_thread_error("Test error")
240 assert mock_thread not in main_window.threads
242 def test_copydelete_thread_success_removes_thread(self, main_window, monkeypatch):
243 """Verify that a completed thread is removed"""
244 mock_thread = Mock()
245 main_window.threads.append(mock_thread)
247 monkeypatch.setattr(QMessageBox, 'information', lambda *args, **kwargs: None)
248 monkeypatch.setattr(main_window, 'sender', lambda: mock_thread)
250 main_window.copydelete_thread_success("Success message", show=True)
252 assert mock_thread not in main_window.threads
254 def test_close_event_stops_threads(self, main_window, qtbot):
255 """Verify that threads are stopped on close"""
256 mock_thread1 = Mock(spec=['finished', 'error', 'wait'])
257 mock_thread1.finished = Mock()
258 mock_thread1.finished.disconnect = Mock()
259 mock_thread1.error = Mock()
260 mock_thread1.error.disconnect = Mock()
261 mock_thread1.wait = Mock()
263 mock_thread2 = Mock(spec=['finished', 'error', 'wait'])
264 mock_thread2.finished = Mock()
265 mock_thread2.finished.disconnect = Mock()
266 mock_thread2.error = Mock()
267 mock_thread2.error.disconnect = Mock()
268 mock_thread2.wait = Mock()
270 # Add threads to list
271 main_window.threads = [mock_thread1, mock_thread2]
273 from PyQt6.QtGui import QCloseEvent
274 event = QCloseEvent()
275 main_window.closeEvent(event)
277 # Verify that methods were called
278 mock_thread1.finished.disconnect.assert_called_once()
279 mock_thread1.error.disconnect.assert_called_once()
280 mock_thread1.wait.assert_called_once()
282 mock_thread2.finished.disconnect.assert_called_once()
283 mock_thread2.error.disconnect.assert_called_once()
284 mock_thread2.wait.assert_called_once()
286 assert len(main_window.threads) == 0
289class TestDebugLog:
290 """Tests for debug log functionality"""
292 @patch('main.ui.main_window.set_log_level')
293 def test_toggle_debug_log_enables(self, mock_set_log_level, main_window):
294 """Verify that debug log can be enabled"""
295 main_window.toggle_debug_log(True)
297 assert main_window.settings.value("debug_log", type=bool) == True
298 mock_set_log_level.assert_called_with(logging.DEBUG)
300 @patch('main.ui.main_window.set_log_level')
301 def test_toggle_debug_log_disables(self, mock_set_log_level, main_window):
302 """Verify that debug log can be disabled"""
303 main_window.toggle_debug_log(False)
305 assert main_window.settings.value("debug_log", type=bool) == False
306 mock_set_log_level.assert_called_with(logging.ERROR)
309class TestWidgetManagement:
310 """Tests for widget management"""
312 def test_set_widgets_first_time(self, main_window, qtbot):
313 """Verify initial widget addition"""
314 from PyQt6.QtWidgets import QWidget
316 left_widget = QWidget()
317 left_widget.adjust_tree_columns = Mock()
318 left_widget.new_thread = Mock()
319 left_widget.new_thread.connect = Mock()
321 right_widget = QWidget()
323 qtbot.addWidget(left_widget)
324 qtbot.addWidget(right_widget)
326 main_window.set_widgets(left_widget, right_widget)
328 assert main_window.splitter.count() == 2
329 assert main_window.left_panel == left_widget
330 assert main_window.right_panel == right_widget
331 left_widget.new_thread.connect.assert_called_once_with(main_window.new_thread)
333 def test_export_file_info_shows_message(self, main_window, monkeypatch):
334 """Verify that export_file_info shows an informational message"""
335 message_shown = False
337 def mock_information(*args, **kwargs):
338 nonlocal message_shown
339 message_shown = True
341 monkeypatch.setattr(QMessageBox, 'information', mock_information)
343 main_window.export_file_info()
344 assert message_shown
347# Integration tests
348class TestIntegration:
349 """Integration tests to verify the full flow"""
351 def test_full_workflow(self, main_window, qtbot):
352 """Test full workflow: creation, language change, operations"""
353 # Verify initialization (not visible by default)
354 assert not main_window.isVisible()
356 # Change language
357 with qtbot.waitSignal(main_window.context["language_changed"], timeout=1000):
358 main_window.set_language("it")
359 assert main_window.language_actions["it"].isChecked()
361 # Verify that the window is functional
362 main_window.show()
363 qtbot.waitExposed(main_window)
364 assert main_window.isVisible()
366 # Close the window
367 main_window.close()
369 # Wait for the window to be actually hidden
370 qtbot.waitUntil(lambda: not main_window.isVisible())
371 assert not main_window.isVisible()