Coverage for tests/ui/test_tool_selection_page.py: 99%

215 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-03 15:51 +0000

1import pytest 

2import platform 

3from unittest.mock import Mock, patch, MagicMock 

4from PyQt6.QtCore import QSettings, pyqtSignal, QObject 

5from PyQt6.QtWidgets import QMessageBox, QRadioButton 

6 

7from main.ui.tool_selection_page import ToolSelectionPage 

8 

9 

10@pytest.fixture 

11def tool_page(qtbot, mock_context): 

12 """Fixture that creates a ToolSelectionPage instance.""" 

13 previous_page = Mock() 

14 page = ToolSelectionPage(mock_context, previous_page) 

15 qtbot.addWidget(page) 

16 return page 

17 

18 

19class TestToolSelectionPageSetup: 

20 """Tests for ToolSelectionPage initialization.""" 

21 

22 def test_page_initialization(self, tool_page): 

23 """Verify correct page initialization.""" 

24 assert tool_page.context is not None 

25 assert tool_page.previous_page is not None 

26 assert tool_page.selected_option is None 

27 

28 def test_title_created(self, tool_page): 

29 """Verify that the title label is created.""" 

30 assert tool_page.title is not None 

31 assert tool_page.title.text() != "" 

32 

33 def test_radio_buttons_created(self, tool_page): 

34 """Verify that all radio buttons are created.""" 

35 assert tool_page.radio_skull is not None 

36 assert tool_page.radio_draw is not None 

37 assert tool_page.radio_dl is not None 

38 assert tool_page.radio_analysis is not None 

39 

40 def test_radio_group_created(self, tool_page): 

41 """Verify that the button group is created.""" 

42 assert tool_page.radio_group is not None 

43 assert len(tool_page.radio_group.buttons()) == 4 

44 

45 def test_all_buttons_unchecked_initially(self, tool_page): 

46 """Verify that all radio buttons are initially unchecked.""" 

47 assert tool_page.radio_group.checkedId() == -1 

48 

49 

50class TestToolSelectionPageSelection: 

51 """Tests for radio button selection logic.""" 

52 

53 def test_select_skull_stripping(self, tool_page, qtbot): 

54 """Verify skull stripping option selection.""" 

55 with qtbot.waitSignal(tool_page.radio_group.buttonToggled, timeout=1000): 

56 tool_page.radio_skull.setChecked(True) 

57 assert tool_page.selected_option == 0 

58 

59 def test_select_automatic_drawing(self, tool_page, qtbot): 

60 """Verify automatic drawing option selection.""" 

61 with qtbot.waitSignal(tool_page.radio_group.buttonToggled, timeout=1000): 

62 tool_page.radio_draw.setChecked(True) 

63 assert tool_page.selected_option == 1 

64 

65 def test_select_deep_learning(self, tool_page, qtbot): 

66 """Verify deep learning option selection.""" 

67 with qtbot.waitSignal(tool_page.radio_group.buttonToggled, timeout=1000): 

68 tool_page.radio_dl.setChecked(True) 

69 assert tool_page.selected_option == 2 

70 

71 def test_select_full_pipeline(self, tool_page, qtbot): 

72 """Verify full pipeline option selection.""" 

73 with qtbot.waitSignal(tool_page.radio_group.buttonToggled, timeout=1000): 

74 tool_page.radio_analysis.setChecked(True) 

75 assert tool_page.selected_option == 3 

76 

77 def test_on_selection_updates_buttons(self, tool_page, qtbot): 

78 """Verify that button selection updates main buttons.""" 

79 with qtbot.waitSignal(tool_page.radio_group.buttonToggled, timeout=1000): 

80 tool_page.radio_skull.setChecked(True) 

81 tool_page.context["update_main_buttons"].assert_called() 

82 

83 def test_change_selection(self, tool_page, qtbot): 

84 """Verify that changing selection updates the state.""" 

85 tool_page.radio_skull.setChecked(True) 

86 assert tool_page.selected_option == 0 

87 tool_page.radio_draw.setChecked(True) 

88 assert tool_page.selected_option == 1 

89 

90 

91class TestToolSelectionPageReadiness: 

92 """Tests for readiness logic (next/back).""" 

93 

94 def test_not_ready_without_selection(self, tool_page): 

95 """Verify not ready to advance without selection.""" 

96 assert not tool_page.is_ready_to_advance() 

97 

98 def test_ready_with_selection(self, tool_page): 

99 """Verify ready to advance when a selection is made.""" 

100 tool_page.radio_skull.setChecked(True) 

101 assert tool_page.is_ready_to_advance() 

102 

103 def test_can_go_back(self, tool_page): 

104 """Verify that the page can always go back.""" 

105 assert tool_page.is_ready_to_go_back() 

106 

107 

108class TestToolSelectionPageNavigation: 

109 """Tests for navigation between pages.""" 

110 

111 def test_back_returns_previous_page(self, tool_page): 

112 """Verify that back() returns the previous page.""" 

113 result = tool_page.back() 

114 assert result == tool_page.previous_page 

115 tool_page.previous_page.on_enter.assert_called_once() 

116 

117 @patch('main.ui.tool_selection_page.SkullStrippingPage') 

118 def test_next_skull_stripping(self, MockPage, tool_page): 

119 """Verify navigation to SkullStrippingPage.""" 

120 mock_page = Mock() 

121 MockPage.return_value = mock_page 

122 tool_page.radio_skull.setChecked(True) 

123 result = tool_page.next(tool_page.context) 

124 assert result == mock_page 

125 mock_page.on_enter.assert_called_once() 

126 

127 @patch('main.ui.tool_selection_page.MaskNiftiSelectionPage') 

128 def test_next_automatic_drawing(self, MockPage, tool_page): 

129 """Verify navigation to MaskNiftiSelectionPage.""" 

130 mock_page = Mock() 

131 MockPage.return_value = mock_page 

132 tool_page.radio_draw.setChecked(True) 

133 result = tool_page.next(tool_page.context) 

134 assert result == mock_page 

135 mock_page.on_enter.assert_called_once() 

136 

137 @patch('main.ui.tool_selection_page.PipelinePatientSelectionPage') 

138 def test_next_full_pipeline(self, MockPage, tool_page): 

139 """Verify navigation to PipelinePatientSelectionPage.""" 

140 mock_page = Mock() 

141 MockPage.return_value = mock_page 

142 tool_page.radio_analysis.setChecked(True) 

143 result = tool_page.next(tool_page.context) 

144 assert result == mock_page 

145 mock_page.on_enter.assert_called_once() 

146 

147 def test_next_caches_page(self, tool_page): 

148 """Verify that pages are cached after creation.""" 

149 with patch('main.ui.tool_selection_page.SkullStrippingPage') as MockPage: 

150 mock_page = Mock() 

151 MockPage.return_value = mock_page 

152 tool_page.radio_skull.setChecked(True) 

153 result1 = tool_page.next(tool_page.context) 

154 result2 = tool_page.next(tool_page.context) 

155 assert result1 == result2 

156 MockPage.assert_called_once() 

157 

158 

159class TestToolSelectionPageDeepLearning: 

160 """Tests for Deep Learning selection and GPU checks.""" 

161 

162 @patch('platform.system', return_value='Linux') 

163 @patch('torch.cuda.is_available', return_value=True) 

164 @patch('main.ui.tool_selection_page.DlNiftiSelectionPage') 

165 def test_dl_available_on_linux_with_gpu(self, MockPage, mock_cuda, mock_platform, tool_page): 

166 """Verify DL page loads correctly on Linux with GPU.""" 

167 mock_page = Mock() 

168 MockPage.return_value = mock_page 

169 tool_page.radio_dl.setChecked(True) 

170 result = tool_page.next(tool_page.context) 

171 assert result == mock_page 

172 mock_page.on_enter.assert_called_once() 

173 

174 @patch('platform.system', return_value='Windows') 

175 @patch('torch.cuda.is_available', return_value=True) 

176 def test_dl_not_available_on_windows(self, mock_cuda, mock_platform, tool_page, monkeypatch): 

177 """Verify DL not available on Windows.""" 

178 message_shown = False 

179 def mock_warning(*args, **kwargs): 

180 nonlocal message_shown 

181 message_shown = True 

182 monkeypatch.setattr(QMessageBox, 'warning', mock_warning) 

183 tool_page.radio_dl.setChecked(True) 

184 result = tool_page.next(tool_page.context) 

185 assert message_shown 

186 assert result == tool_page 

187 

188 @patch('platform.system', return_value='Linux') 

189 @patch('torch.cuda.is_available', return_value=False) 

190 def test_dl_not_available_without_gpu(self, mock_cuda, mock_platform, tool_page, monkeypatch): 

191 """Verify DL not available without a GPU.""" 

192 message_shown = False 

193 def mock_warning(*args, **kwargs): 

194 nonlocal message_shown 

195 message_shown = True 

196 monkeypatch.setattr(QMessageBox, 'warning', mock_warning) 

197 tool_page.radio_dl.setChecked(True) 

198 result = tool_page.next(tool_page.context) 

199 assert message_shown 

200 assert result == tool_page 

201 

202 @patch('platform.system', return_value='Darwin') 

203 @patch('torch.cuda.is_available', return_value=False) 

204 def test_dl_not_available_on_macos(self, mock_cuda, mock_platform, tool_page, monkeypatch): 

205 """Verify DL not available on macOS.""" 

206 message_shown = False 

207 def mock_warning(*args, **kwargs): 

208 nonlocal message_shown 

209 message_shown = True 

210 monkeypatch.setattr(QMessageBox, 'warning', mock_warning) 

211 tool_page.radio_dl.setChecked(True) 

212 result = tool_page.next(tool_page.context) 

213 assert message_shown 

214 

215 

216class TestToolSelectionPageReset: 

217 """Tests for page reset behavior.""" 

218 

219 def test_reset_clears_selection(self, tool_page): 

220 """Verify reset clears selected option.""" 

221 tool_page.radio_skull.setChecked(True) 

222 assert tool_page.selected_option == 0 

223 tool_page.reset_page() 

224 assert tool_page.selected_option is None 

225 assert tool_page.radio_group.checkedId() == -1 

226 

227 def test_reset_unchecks_all_buttons(self, tool_page): 

228 """Verify reset unchecks all radio buttons.""" 

229 tool_page.radio_draw.setChecked(True) 

230 tool_page.reset_page() 

231 for button in tool_page.radio_group.buttons(): 

232 assert not button.isChecked() 

233 

234 

235class TestToolSelectionPageResize: 

236 """Tests for dynamic resizing behavior.""" 

237 

238 def test_resize_event_updates_fonts(self, tool_page): 

239 """Verify font size updates after resize.""" 

240 from PyQt6.QtGui import QResizeEvent 

241 from PyQt6.QtCore import QSize 

242 initial_font_size = tool_page.title.font().pointSize() 

243 event = QResizeEvent(QSize(1200, 800), QSize(800, 600)) 

244 tool_page.resizeEvent(event) 

245 new_font_size = tool_page.title.font().pointSize() 

246 assert new_font_size >= 14 

247 

248 def test_resize_maintains_minimum_font_size(self, tool_page): 

249 """Verify minimum font size is maintained.""" 

250 from PyQt6.QtGui import QResizeEvent 

251 from PyQt6.QtCore import QSize 

252 event = QResizeEvent(QSize(300, 200), QSize(800, 600)) 

253 tool_page.resizeEvent(event) 

254 title_font_size = tool_page.title.font().pointSize() 

255 assert title_font_size >= 14 

256 button_font_size = tool_page.radio_skull.font().pointSize() 

257 assert button_font_size >= 10 

258 

259 

260class TestToolSelectionPageTranslation: 

261 """Tests for UI translation updates.""" 

262 

263 def test_translate_ui_updates_title(self, tool_page): 

264 """Verify title text is updated.""" 

265 tool_page._translate_ui() 

266 assert tool_page.title.text() != "" 

267 

268 def test_translate_ui_updates_buttons(self, tool_page): 

269 """Verify radio button texts are updated.""" 

270 tool_page._translate_ui() 

271 assert tool_page.radio_skull.text() != "" 

272 assert tool_page.radio_draw.text() != "" 

273 assert tool_page.radio_dl.text() != "" 

274 assert tool_page.radio_analysis.text() != "" 

275 

276 def test_translate_ui_updates_group_box(self, tool_page): 

277 """Verify group box title is updated.""" 

278 tool_page._translate_ui() 

279 assert tool_page.radio_group_box.title() != "" 

280 

281 

282class TestToolSelectionPageInfoLabel: 

283 """Tests for Deep Learning info label.""" 

284 

285 def test_dl_has_info_label(self, tool_page): 

286 """Verify DL info label exists.""" 

287 assert hasattr(tool_page, 'dl_info_label') 

288 assert tool_page.dl_info_label is not None 

289 

290 def test_info_label_has_tooltip(self, tool_page): 

291 """Verify info label has a valid tooltip.""" 

292 tooltip = tool_page.dl_info_label.toolTip() 

293 assert tooltip != "" 

294 assert "GPU" in tooltip or "CUDA" in tooltip or tooltip == "" 

295 

296 

297class TestToolSelectionPageIntegration: 

298 """Integration tests for full workflow.""" 

299 

300 def test_full_selection_workflow(self, tool_page, qtbot): 

301 """Verify complete selection and readiness workflow.""" 

302 assert not tool_page.is_ready_to_advance() 

303 with qtbot.waitSignal(tool_page.radio_group.buttonToggled, timeout=1000): 

304 tool_page.radio_skull.setChecked(True) 

305 assert tool_page.is_ready_to_advance() 

306 assert tool_page.selected_option == 0 

307 

308 @patch('main.ui.tool_selection_page.SkullStrippingPage') 

309 def test_navigation_and_cache(self, MockPage, tool_page): 

310 """Verify navigation caching and repeated access.""" 

311 mock_page = Mock() 

312 MockPage.return_value = mock_page 

313 tool_page.radio_skull.setChecked(True) 

314 result1 = tool_page.next(tool_page.context) 

315 result2 = tool_page.next(tool_page.context) 

316 assert result1 is result2 

317 MockPage.assert_called_once() 

318 assert mock_page.on_enter.call_count == 2 

319 

320 

321if __name__ == "__main__": 

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