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

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 

10 

11# Ensure the path is correct to import MainWindow 

12from main.ui.main_window import MainWindow 

13 

14 

15# A QObject is necessary to host signals 

16class SignalHost(QObject): 

17 language_changed = pyqtSignal(str) 

18 

19 

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

28 

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

32 

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 

36 

37 # 3. Signal object 

38 signal_host = SignalHost() 

39 

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

50 

51 "_signal_host_ref": signal_host 

52 } 

53 

54 # 5. Add teardown (cleanup) 

55 def cleanup(): 

56 shutil.rmtree(temp_dir) 

57 settings.clear() 

58 

59 request.addfinalizer(cleanup) 

60 

61 return context 

62 

63 

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) 

71 

72 try: 

73 window.destroyed.disconnect(QApplication.instance().quit) 

74 except TypeError: 

75 pass 

76 

77 window.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False) 

78 

79 return window 

80 

81class TestMainWindowSetup: 

82 """Tests for MainWindow initial configuration""" 

83 

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 

89 

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 

94 

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 

99 

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 

104 

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) 

112 

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

119 

120 

121class TestLanguageManagement: 

122 """Tests for language management""" 

123 

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

128 

129 assert blocker.args == ["it"] 

130 

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

135 

136 main_window.set_language("en") 

137 assert main_window.language_actions["en"].isChecked() 

138 

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

144 

145 main_window.language_actions["it"].trigger() 

146 assert main_window.language_actions["it"].isChecked() 

147 assert not main_window.language_actions["en"].isChecked() 

148 

149 

150class TestWorkspaceOperations: 

151 """Tests for workspace operations""" 

152 

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) 

157 

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 ) 

164 

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 

168 

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) 

173 

174 # 1. Save the INITIAL number of files 

175 initial_file_count = len(os.listdir(main_window.workspace_path)) 

176 

177 # The workspace has 2 items (file1.txt, subdir) 

178 assert initial_file_count > 0 

179 

180 main_window.clear_folder( 

181 folder_path=main_window.workspace_path, 

182 folder_name="test", 

183 return_to_import=False 

184 ) 

185 

186 # 2. Verify that as many threads were created as there were INITIAL files 

187 assert len(main_window.threads) == initial_file_count 

188 

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) 

192 

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) 

197 

198 main_window.clear_folder( 

199 folder_path=main_window.workspace_path, 

200 folder_name="workspace", 

201 return_to_import=True 

202 ) 

203 

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) 

207 

208 main_window.context["return_to_import"].assert_called_once() 

209 

210 

211class TestThreadManagement: 

212 """Tests for thread management""" 

213 

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

223 

224 initial_count = len(main_window.threads) 

225 main_window.new_thread(mock_thread) 

226 

227 assert len(main_window.threads) == initial_count + 1 

228 mock_thread.start.assert_called_once() 

229 

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) 

234 

235 monkeypatch.setattr(QMessageBox, 'warning', lambda *args, **kwargs: None) 

236 monkeypatch.setattr(main_window, 'sender', lambda: mock_thread) 

237 

238 main_window.copydelete_thread_error("Test error") 

239 

240 assert mock_thread not in main_window.threads 

241 

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) 

246 

247 monkeypatch.setattr(QMessageBox, 'information', lambda *args, **kwargs: None) 

248 monkeypatch.setattr(main_window, 'sender', lambda: mock_thread) 

249 

250 main_window.copydelete_thread_success("Success message", show=True) 

251 

252 assert mock_thread not in main_window.threads 

253 

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

262 

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

269 

270 # Add threads to list 

271 main_window.threads = [mock_thread1, mock_thread2] 

272 

273 from PyQt6.QtGui import QCloseEvent 

274 event = QCloseEvent() 

275 main_window.closeEvent(event) 

276 

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

281 

282 mock_thread2.finished.disconnect.assert_called_once() 

283 mock_thread2.error.disconnect.assert_called_once() 

284 mock_thread2.wait.assert_called_once() 

285 

286 assert len(main_window.threads) == 0 

287 

288 

289class TestDebugLog: 

290 """Tests for debug log functionality""" 

291 

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) 

296 

297 assert main_window.settings.value("debug_log", type=bool) == True 

298 mock_set_log_level.assert_called_with(logging.DEBUG) 

299 

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) 

304 

305 assert main_window.settings.value("debug_log", type=bool) == False 

306 mock_set_log_level.assert_called_with(logging.ERROR) 

307 

308 

309class TestWidgetManagement: 

310 """Tests for widget management""" 

311 

312 def test_set_widgets_first_time(self, main_window, qtbot): 

313 """Verify initial widget addition""" 

314 from PyQt6.QtWidgets import QWidget 

315 

316 left_widget = QWidget() 

317 left_widget.adjust_tree_columns = Mock() 

318 left_widget.new_thread = Mock() 

319 left_widget.new_thread.connect = Mock() 

320 

321 right_widget = QWidget() 

322 

323 qtbot.addWidget(left_widget) 

324 qtbot.addWidget(right_widget) 

325 

326 main_window.set_widgets(left_widget, right_widget) 

327 

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) 

332 

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 

336 

337 def mock_information(*args, **kwargs): 

338 nonlocal message_shown 

339 message_shown = True 

340 

341 monkeypatch.setattr(QMessageBox, 'information', mock_information) 

342 

343 main_window.export_file_info() 

344 assert message_shown 

345 

346 

347# Integration tests 

348class TestIntegration: 

349 """Integration tests to verify the full flow""" 

350 

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

355 

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

360 

361 # Verify that the window is functional 

362 main_window.show() 

363 qtbot.waitExposed(main_window) 

364 assert main_window.isVisible() 

365 

366 # Close the window 

367 main_window.close() 

368 

369 # Wait for the window to be actually hidden 

370 qtbot.waitUntil(lambda: not main_window.isVisible()) 

371 assert not main_window.isVisible()