Coverage for tests/components/test_nifti_file_dialog.py: 99%

505 statements  

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

1import os 

2import tempfile 

3 

4import pytest 

5from unittest.mock import Mock, patch, MagicMock 

6from PyQt6.QtWidgets import QDialog, QMessageBox, QListWidgetItem 

7from PyQt6.QtCore import Qt 

8from PyQt6.QtGui import QColor 

9 

10from main.components.nifti_file_dialog import NiftiFileDialog 

11 

12 

13@pytest.fixture 

14def nifti_workspace(): 

15 """Create BIDS workspace with NIfTI files.""" 

16 temp_dir = tempfile.mkdtemp() 

17 # Subject 1 - anat 

18 sub1_anat = os.path.join(temp_dir, "sub-01", "anat") 

19 os.makedirs(sub1_anat) 

20 

21 files = [ 

22 "sub-01_T1w.nii", 

23 "sub-01_T2w.nii.gz", 

24 "sub-01_FLAIR.nii" 

25 ] 

26 for f in files: 

27 with open(os.path.join(sub1_anat, f), "w") as file: 

28 file.write("nifti data") 

29 

30 # Subject 1 - ses-01 pet 

31 sub1_ses1_pet = os.path.join(temp_dir, "sub-01", "ses-01", "pet") 

32 os.makedirs(sub1_ses1_pet) 

33 with open(os.path.join(sub1_ses1_pet, "sub-01_ses-01_pet.nii"), "w") as f: 

34 f.write("pet data") 

35 

36 # Subject 2 - anat 

37 sub2_anat = os.path.join(temp_dir, "sub-02", "anat") 

38 os.makedirs(sub2_anat) 

39 with open(os.path.join(sub2_anat, "sub-02_T1w.nii.gz"), "w") as f: 

40 f.write("nifti data") 

41 

42 # Derivatives - skullstrips 

43 deriv_skull = os.path.join(temp_dir, "derivatives", "skullstrips", "sub-01", "anat") 

44 os.makedirs(deriv_skull, exist_ok=True) 

45 with open(os.path.join(deriv_skull, "sub-01_T1w_brain.nii"), "w") as f: 

46 f.write("skull stripped") 

47 

48 return temp_dir 

49 

50 

51@pytest.fixture 

52def mock_context_nifti(nifti_workspace): 

53 """Context for NiftiFileDialog.""" 

54 return {"workspace_path": nifti_workspace} 

55 

56 

57@pytest.fixture 

58def mock_has_existing(): 

59 """Mock for has_existing_func.""" 

60 return Mock(return_value=False) 

61 

62 

63class TestNiftiFileDialogInitialization: 

64 """Tests for dialog initialization.""" 

65 

66 def test_initialization_basic(self, qtbot, mock_context_nifti, mock_has_existing): 

67 """Basic initialization test.""" 

68 dialog = NiftiFileDialog( 

69 mock_context_nifti, 

70 allow_multiple=True, 

71 has_existing_func=mock_has_existing, 

72 label="mask" 

73 ) 

74 qtbot.addWidget(dialog) 

75 

76 assert dialog.workspace_path == mock_context_nifti["workspace_path"] 

77 assert dialog.allow_multiple is True 

78 assert dialog.has_existing_func == mock_has_existing 

79 assert dialog.label == "mask" 

80 assert dialog.selected_files == [] 

81 

82 def test_initialization_single_selection(self, qtbot, mock_context_nifti, mock_has_existing): 

83 """Initialization test with single selection mode.""" 

84 dialog = NiftiFileDialog( 

85 mock_context_nifti, 

86 allow_multiple=False, 

87 has_existing_func=mock_has_existing, 

88 label="test" 

89 ) 

90 qtbot.addWidget(dialog) 

91 

92 assert dialog.allow_multiple is False 

93 

94 def test_initialization_without_has_existing_func(self, qtbot, mock_context_nifti): 

95 """Initialization test without has_existing_func.""" 

96 dialog = NiftiFileDialog( 

97 mock_context_nifti, 

98 allow_multiple=True, 

99 label="test" 

100 ) 

101 qtbot.addWidget(dialog) 

102 

103 # Should use default lambda 

104 assert callable(dialog.has_existing_func) 

105 assert dialog.has_existing_func("any_path", "any_workspace") is False 

106 

107 def test_initialization_with_forced_filters(self, qtbot, mock_context_nifti, mock_has_existing): 

108 """Initialization test with forced_filters.""" 

109 filters = {"subject": "sub-01", "modality": "T1w"} 

110 

111 dialog = NiftiFileDialog( 

112 mock_context_nifti, 

113 allow_multiple=True, 

114 has_existing_func=mock_has_existing, 

115 label="test", 

116 forced_filters=filters 

117 ) 

118 qtbot.addWidget(dialog) 

119 

120 assert dialog.forced_filters == filters 

121 

122 def test_ui_elements_created(self, qtbot, mock_context_nifti, mock_has_existing): 

123 """Test that all UI elements are created.""" 

124 dialog = NiftiFileDialog( 

125 mock_context_nifti, 

126 allow_multiple=True, 

127 has_existing_func=mock_has_existing, 

128 label="test" 

129 ) 

130 qtbot.addWidget(dialog) 

131 

132 assert dialog.search_bar is not None 

133 assert dialog.subject_combo is not None 

134 assert dialog.session_combo is not None 

135 assert dialog.modality_combo is not None 

136 assert dialog.datatype_combo is not None 

137 assert dialog.no_flag_checkbox is not None 

138 assert dialog.with_flag_checkbox is not None 

139 assert dialog.file_list is not None 

140 assert dialog.info_label is not None 

141 

142 

143class TestPopulateFiles: 

144 """Tests for the _populate_files method.""" 

145 

146 def test_populate_files_finds_nifti(self, qtbot, mock_context_nifti, mock_has_existing): 

147 """Test that it finds NIfTI files.""" 

148 dialog = NiftiFileDialog( 

149 mock_context_nifti, 

150 allow_multiple=True, 

151 has_existing_func=mock_has_existing, 

152 label="test" 

153 ) 

154 qtbot.addWidget(dialog) 

155 

156 # Should find 5 files (.nii and .nii.gz) + 1 derivatives 

157 assert len(dialog.all_nii_files) >= 5 

158 

159 def test_populate_files_extracts_subjects(self, qtbot, mock_context_nifti, mock_has_existing): 

160 """Test subject extraction.""" 

161 dialog = NiftiFileDialog( 

162 mock_context_nifti, 

163 allow_multiple=True, 

164 has_existing_func=mock_has_existing, 

165 label="test" 

166 ) 

167 qtbot.addWidget(dialog) 

168 

169 # Should have at least 2 subjects + "All subjects" 

170 assert dialog.subject_combo.count() >= 3 

171 

172 def test_populate_files_extracts_sessions(self, qtbot, mock_context_nifti, mock_has_existing): 

173 """Test session extraction.""" 

174 dialog = NiftiFileDialog( 

175 mock_context_nifti, 

176 allow_multiple=True, 

177 has_existing_func=mock_has_existing, 

178 label="test" 

179 ) 

180 qtbot.addWidget(dialog) 

181 

182 # Should have ses-01 + "All sessions" 

183 assert dialog.session_combo.count() >= 2 

184 

185 def test_populate_files_extracts_modalities(self, qtbot, mock_context_nifti, mock_has_existing): 

186 """Test modality extraction.""" 

187 dialog = NiftiFileDialog( 

188 mock_context_nifti, 

189 allow_multiple=True, 

190 has_existing_func=mock_has_existing, 

191 label="test" 

192 ) 

193 qtbot.addWidget(dialog) 

194 

195 # Should have T1w, T2w, FLAIR, pet + "All modalities" 

196 assert dialog.modality_combo.count() >= 4 

197 

198 def test_populate_files_extracts_datatypes(self, qtbot, mock_context_nifti, mock_has_existing): 

199 """Test data type extraction.""" 

200 dialog = NiftiFileDialog( 

201 mock_context_nifti, 

202 allow_multiple=True, 

203 has_existing_func=mock_has_existing, 

204 label="test" 

205 ) 

206 qtbot.addWidget(dialog) 

207 

208 # Should have anat, pet + "All types" 

209 assert dialog.datatype_combo.count() >= 3 

210 

211 def test_populate_files_calls_has_existing(self, qtbot, mock_context_nifti, mock_has_existing): 

212 """Test that has_existing_func is called.""" 

213 dialog = NiftiFileDialog( 

214 mock_context_nifti, 

215 allow_multiple=True, 

216 has_existing_func=mock_has_existing, 

217 label="test" 

218 ) 

219 qtbot.addWidget(dialog) 

220 

221 # Should be called for each file 

222 assert mock_has_existing.call_count > 0 

223 

224 def test_populate_files_list_widget(self, qtbot, mock_context_nifti, mock_has_existing): 

225 """Test that the list widget is populated.""" 

226 dialog = NiftiFileDialog( 

227 mock_context_nifti, 

228 allow_multiple=True, 

229 has_existing_func=mock_has_existing, 

230 label="test" 

231 ) 

232 qtbot.addWidget(dialog) 

233 

234 assert dialog.file_list.count() > 0 

235 

236 

237class TestApplyFilters: 

238 """Tests for the _apply_filters method.""" 

239 

240 def test_apply_filters_search(self, qtbot, mock_context_nifti, mock_has_existing): 

241 """Test search filter.""" 

242 dialog = NiftiFileDialog( 

243 mock_context_nifti, 

244 allow_multiple=True, 

245 has_existing_func=mock_has_existing, 

246 label="test" 

247 ) 

248 qtbot.addWidget(dialog) 

249 

250 initial_visible = sum(1 for i in range(dialog.file_list.count()) 

251 if not dialog.file_list.item(i).isHidden()) 

252 

253 dialog.search_bar.setText("T1w") 

254 

255 visible_after = sum(1 for i in range(dialog.file_list.count()) 

256 if not dialog.file_list.item(i).isHidden()) 

257 

258 assert visible_after < initial_visible 

259 

260 def test_apply_filters_subject(self, qtbot, mock_context_nifti, mock_has_existing): 

261 """Test subject filter.""" 

262 dialog = NiftiFileDialog( 

263 mock_context_nifti, 

264 allow_multiple=True, 

265 has_existing_func=mock_has_existing, 

266 label="test" 

267 ) 

268 qtbot.addWidget(dialog) 

269 

270 dialog.subject_combo.setCurrentText("sub-01") 

271 

272 # Verify that only sub-01 files are visible 

273 for i in range(dialog.file_list.count()): 

274 item = dialog.file_list.item(i) 

275 if not item.isHidden(): 

276 assert "sub-01" in item.text() 

277 

278 def test_apply_filters_session(self, qtbot, mock_context_nifti, mock_has_existing): 

279 """Test session filter.""" 

280 dialog = NiftiFileDialog( 

281 mock_context_nifti, 

282 allow_multiple=True, 

283 has_existing_func=mock_has_existing, 

284 label="test" 

285 ) 

286 qtbot.addWidget(dialog) 

287 

288 dialog.session_combo.setCurrentText("ses-01") 

289 

290 visible = sum(1 for i in range(dialog.file_list.count()) 

291 if not dialog.file_list.item(i).isHidden()) 

292 

293 assert visible >= 1 

294 

295 def test_apply_filters_modality(self, qtbot, mock_context_nifti, mock_has_existing): 

296 """Test modality filter.""" 

297 dialog = NiftiFileDialog( 

298 mock_context_nifti, 

299 allow_multiple=True, 

300 has_existing_func=mock_has_existing, 

301 label="test" 

302 ) 

303 qtbot.addWidget(dialog) 

304 

305 dialog.modality_combo.setCurrentText("FLAIR") 

306 

307 visible = sum(1 for i in range(dialog.file_list.count()) 

308 if not dialog.file_list.item(i).isHidden()) 

309 

310 assert visible >= 1 

311 

312 def test_apply_filters_datatype(self, qtbot, mock_context_nifti, mock_has_existing): 

313 """Test data type filter.""" 

314 dialog = NiftiFileDialog( 

315 mock_context_nifti, 

316 allow_multiple=True, 

317 has_existing_func=mock_has_existing, 

318 label="test" 

319 ) 

320 qtbot.addWidget(dialog) 

321 

322 dialog.datatype_combo.setCurrentText("anat") 

323 

324 visible = sum(1 for i in range(dialog.file_list.count()) 

325 if not dialog.file_list.item(i).isHidden()) 

326 

327 assert visible >= 1 

328 

329 def test_apply_filters_no_flag(self, qtbot, mock_context_nifti): 

330 """Test no-flag filter.""" 

331 

332 # has_existing returns True for some files 

333 def mock_has_existing_selective(path, workspace): 

334 return "sub-01_T1w" in path 

335 

336 dialog = NiftiFileDialog( 

337 mock_context_nifti, 

338 allow_multiple=True, 

339 has_existing_func=mock_has_existing_selective, 

340 label="test" 

341 ) 

342 qtbot.addWidget(dialog) 

343 

344 dialog.no_flag_checkbox.setChecked(True) 

345 

346 # Verify that files with flag are hidden 

347 for i in range(dialog.file_list.count()): 

348 item = dialog.file_list.item(i) 

349 if not item.isHidden(): 

350 assert item.text() not in dialog.files_with_flag 

351 

352 def test_apply_filters_with_flag(self, qtbot, mock_context_nifti): 

353 """Test with-flag filter.""" 

354 

355 def mock_has_existing_selective(path, workspace): 

356 return "sub-01_T1w" in path 

357 

358 dialog = NiftiFileDialog( 

359 mock_context_nifti, 

360 allow_multiple=True, 

361 has_existing_func=mock_has_existing_selective, 

362 label="test" 

363 ) 

364 qtbot.addWidget(dialog) 

365 

366 dialog.with_flag_checkbox.setChecked(True) 

367 

368 # Verify that only files with flag are visible 

369 for i in range(dialog.file_list.count()): 

370 item = dialog.file_list.item(i) 

371 if not item.isHidden(): 

372 assert item.text() in dialog.files_with_flag 

373 

374 def test_apply_filters_combined(self, qtbot, mock_context_nifti, mock_has_existing): 

375 """Test combined filters.""" 

376 dialog = NiftiFileDialog( 

377 mock_context_nifti, 

378 allow_multiple=True, 

379 has_existing_func=mock_has_existing, 

380 label="test" 

381 ) 

382 qtbot.addWidget(dialog) 

383 

384 dialog.subject_combo.setCurrentText("sub-01") 

385 dialog.search_bar.setText("T1w") 

386 

387 visible = sum(1 for i in range(dialog.file_list.count()) 

388 if not dialog.file_list.item(i).isHidden()) 

389 

390 # Should only find T1w of sub-01 

391 assert visible >= 1 

392 

393 

394class TestCheckboxBehavior: 

395 """Tests for checkbox behavior.""" 

396 

397 def test_no_flag_unchecks_with_flag(self, qtbot, mock_context_nifti, mock_has_existing): 

398 """Test that no_flag unchecks with_flag.""" 

399 dialog = NiftiFileDialog( 

400 mock_context_nifti, 

401 allow_multiple=True, 

402 has_existing_func=mock_has_existing, 

403 label="test" 

404 ) 

405 qtbot.addWidget(dialog) 

406 

407 dialog.with_flag_checkbox.setChecked(True) 

408 dialog.no_flag_checkbox.setChecked(True) 

409 

410 assert not dialog.with_flag_checkbox.isChecked() 

411 

412 def test_with_flag_unchecks_no_flag(self, qtbot, mock_context_nifti, mock_has_existing): 

413 """Test that with_flag unchecks no_flag.""" 

414 dialog = NiftiFileDialog( 

415 mock_context_nifti, 

416 allow_multiple=True, 

417 has_existing_func=mock_has_existing, 

418 label="test" 

419 ) 

420 qtbot.addWidget(dialog) 

421 

422 dialog.no_flag_checkbox.setChecked(True) 

423 dialog.with_flag_checkbox.setChecked(True) 

424 

425 assert not dialog.no_flag_checkbox.isChecked() 

426 

427 

428class TestResetFilters: 

429 """Tests for the _reset_filters method.""" 

430 

431 def test_reset_filters_basic(self, qtbot, mock_context_nifti, mock_has_existing): 

432 """Basic filter reset test.""" 

433 dialog = NiftiFileDialog( 

434 mock_context_nifti, 

435 allow_multiple=True, 

436 has_existing_func=mock_has_existing, 

437 label="test" 

438 ) 

439 qtbot.addWidget(dialog) 

440 

441 # Set filters 

442 dialog.search_bar.setText("test") 

443 dialog.subject_combo.setCurrentIndex(1) 

444 dialog.no_flag_checkbox.setChecked(True) 

445 

446 # Reset 

447 dialog._reset_filters() 

448 

449 assert dialog.search_bar.text() == "" 

450 assert dialog.subject_combo.currentIndex() == 0 

451 assert not dialog.no_flag_checkbox.isChecked() 

452 

453 

454class TestSelectAllVisible: 

455 """Tests for the _select_all_visible method.""" 

456 

457 def test_select_all_visible_basic(self, qtbot, mock_context_nifti, mock_has_existing): 

458 """Test selection of all visible items.""" 

459 dialog = NiftiFileDialog( 

460 mock_context_nifti, 

461 allow_multiple=True, 

462 has_existing_func=mock_has_existing, 

463 label="test" 

464 ) 

465 qtbot.addWidget(dialog) 

466 

467 dialog._select_all_visible() 

468 

469 # All visible items should be selected 

470 for i in range(dialog.file_list.count()): 

471 item = dialog.file_list.item(i) 

472 if not item.isHidden(): 

473 assert item.isSelected() 

474 

475 def test_select_all_visible_with_filters(self, qtbot, mock_context_nifti, mock_has_existing): 

476 """Test selection after applying filters.""" 

477 dialog = NiftiFileDialog( 

478 mock_context_nifti, 

479 allow_multiple=True, 

480 has_existing_func=mock_has_existing, 

481 label="test" 

482 ) 

483 qtbot.addWidget(dialog) 

484 

485 dialog.search_bar.setText("T1w") 

486 dialog._select_all_visible() 

487 

488 # Only visible items should be selected 

489 selected_count = len(dialog.file_list.selectedItems()) 

490 visible_count = sum(1 for i in range(dialog.file_list.count()) 

491 if not dialog.file_list.item(i).isHidden()) 

492 

493 assert selected_count == visible_count 

494 

495 

496class TestAccept: 

497 """Tests for the _accept method.""" 

498 

499 def test_accept_with_selection(self, qtbot, mock_context_nifti, mock_has_existing): 

500 """Test accept with a selection.""" 

501 dialog = NiftiFileDialog( 

502 mock_context_nifti, 

503 allow_multiple=True, 

504 has_existing_func=mock_has_existing, 

505 label="test" 

506 ) 

507 qtbot.addWidget(dialog) 

508 

509 # Select first item 

510 dialog.file_list.item(0).setSelected(True) 

511 

512 with patch.object(dialog, 'accept', wraps=dialog.accept) as mock_accept: 

513 dialog._accept() 

514 

515 mock_accept.assert_called_once() 

516 assert len(dialog.selected_files) == 1 

517 

518 def test_accept_without_selection(self, qtbot, mock_context_nifti, mock_has_existing): 

519 """Test accept without a selection.""" 

520 dialog = NiftiFileDialog( 

521 mock_context_nifti, 

522 allow_multiple=True, 

523 has_existing_func=mock_has_existing, 

524 label="test" 

525 ) 

526 qtbot.addWidget(dialog) 

527 

528 with patch.object(QMessageBox, 'warning') as mock_warning: 

529 dialog._accept() 

530 

531 mock_warning.assert_called_once() 

532 

533 def test_accept_with_existing_flag_warning(self, qtbot, mock_context_nifti): 

534 """Test warning when there are files with flags.""" 

535 

536 def mock_has_existing_selective(path, workspace): 

537 return "sub-01_T1w" in path 

538 

539 dialog = NiftiFileDialog( 

540 mock_context_nifti, 

541 allow_multiple=True, 

542 has_existing_func=mock_has_existing_selective, 

543 label="mask" 

544 ) 

545 qtbot.addWidget(dialog) 

546 

547 # Select file with flag 

548 for i in range(dialog.file_list.count()): 

549 item = dialog.file_list.item(i) 

550 if "sub-01_T1w" in item.text(): 

551 item.setSelected(True) 

552 break 

553 

554 with patch.object(QMessageBox, 'exec', return_value=QMessageBox.StandardButton.Yes): 

555 with patch.object(dialog, 'accept'): 

556 dialog._accept() 

557 

558 def test_accept_warning_cancelled(self, qtbot, mock_context_nifti): 

559 """Test cancellation after warning.""" 

560 

561 def mock_has_existing_selective(path, workspace): 

562 return True # All have flags 

563 

564 dialog = NiftiFileDialog( 

565 mock_context_nifti, 

566 allow_multiple=True, 

567 has_existing_func=mock_has_existing_selective, 

568 label="mask" 

569 ) 

570 qtbot.addWidget(dialog) 

571 

572 dialog.file_list.item(0).setSelected(True) 

573 

574 with patch.object(QMessageBox, 'exec', return_value=QMessageBox.StandardButton.No): 

575 dialog._accept() 

576 

577 # Should not have set selected_files 

578 assert len(dialog.selected_files) == 0 

579 

580 

581class TestGetFilesStaticMethod: 

582 """Tests for the static method get_files.""" 

583 

584 def test_get_files_accepted(self, qtbot, mock_context_nifti, mock_has_existing): 

585 """Test get_files with accept.""" 

586 dialog_instance = MagicMock() 

587 dialog_instance.selected_files = ["/path/file1.nii", "/path/file2.nii"] 

588 dialog_instance.exec.return_value = QDialog.DialogCode.Accepted 

589 

590 with patch('main.components.nifti_file_dialog.NiftiFileDialog', return_value=dialog_instance): 

591 result = NiftiFileDialog.get_files( 

592 mock_context_nifti, 

593 allow_multiple=True, 

594 has_existing_func=mock_has_existing, 

595 label="test" 

596 ) 

597 

598 assert result == dialog_instance.selected_files 

599 

600 def test_get_files_rejected(self, qtbot, mock_context_nifti, mock_has_existing): 

601 """Test get_files with reject.""" 

602 with patch.object(NiftiFileDialog, 'exec', return_value=QDialog.DialogCode.Rejected): 

603 result = NiftiFileDialog.get_files( 

604 mock_context_nifti, 

605 allow_multiple=True, 

606 has_existing_func=mock_has_existing, 

607 label="test" 

608 ) 

609 

610 assert result is None 

611 

612 

613class TestForcedFilters: 

614 """Tests for forced_filters.""" 

615 

616 def test_forced_filters_search(self, qtbot, mock_context_nifti, mock_has_existing): 

617 """Test forced filter for search.""" 

618 filters = {"search": "T1w"} 

619 

620 dialog = NiftiFileDialog( 

621 mock_context_nifti, 

622 allow_multiple=True, 

623 has_existing_func=mock_has_existing, 

624 label="test", 

625 forced_filters=filters 

626 ) 

627 qtbot.addWidget(dialog) 

628 

629 assert dialog.search_bar.text() == "T1w" 

630 assert not dialog.search_bar.isEnabled() 

631 

632 def test_forced_filters_subject(self, qtbot, mock_context_nifti, mock_has_existing): 

633 """Test forced filter for subject.""" 

634 filters = {"subject": "sub-01"} 

635 

636 dialog = NiftiFileDialog( 

637 mock_context_nifti, 

638 allow_multiple=True, 

639 has_existing_func=mock_has_existing, 

640 label="test", 

641 forced_filters=filters 

642 ) 

643 qtbot.addWidget(dialog) 

644 

645 assert dialog.subject_combo.currentText() == "sub-01" 

646 assert not dialog.subject_combo.isEnabled() 

647 

648 def test_forced_filters_modality(self, qtbot, mock_context_nifti, mock_has_existing): 

649 """Test forced filter for modality.""" 

650 filters = {"modality": "T1w"} 

651 

652 dialog = NiftiFileDialog( 

653 mock_context_nifti, 

654 allow_multiple=True, 

655 has_existing_func=mock_has_existing, 

656 label="test", 

657 forced_filters=filters 

658 ) 

659 qtbot.addWidget(dialog) 

660 

661 assert dialog.modality_combo.currentText() == "T1w" 

662 assert not dialog.modality_combo.isEnabled() 

663 

664 def test_forced_filters_no_flag(self, qtbot, mock_context_nifti, mock_has_existing): 

665 """Test forced filter for no_flag.""" 

666 filters = {"no_flag": True} 

667 

668 dialog = NiftiFileDialog( 

669 mock_context_nifti, 

670 allow_multiple=True, 

671 has_existing_func=mock_has_existing, 

672 label="test", 

673 forced_filters=filters 

674 ) 

675 qtbot.addWidget(dialog) 

676 

677 assert dialog.no_flag_checkbox.isChecked() 

678 assert not dialog.no_flag_checkbox.isEnabled() 

679 

680 def test_forced_filters_multiple(self, qtbot, mock_context_nifti, mock_has_existing): 

681 """Test multiple forced filters.""" 

682 filters = { 

683 "subject": "sub-01", 

684 "modality": "T1w", 

685 "no_flag": True 

686 } 

687 

688 dialog = NiftiFileDialog( 

689 mock_context_nifti, 

690 allow_multiple=True, 

691 has_existing_func=mock_has_existing, 

692 label="test", 

693 forced_filters=filters 

694 ) 

695 qtbot.addWidget(dialog) 

696 

697 assert dialog.subject_combo.currentText() == "sub-01" 

698 assert dialog.modality_combo.currentText() == "T1w" 

699 assert dialog.no_flag_checkbox.isChecked() 

700 

701 

702class TestFileListBehavior: 

703 """Tests for file list behavior.""" 

704 

705 def test_file_list_selection_mode_multiple(self, qtbot, mock_context_nifti, mock_has_existing): 

706 """Test multiple selection mode.""" 

707 dialog = NiftiFileDialog( 

708 mock_context_nifti, 

709 allow_multiple=True, 

710 has_existing_func=mock_has_existing, 

711 label="test" 

712 ) 

713 qtbot.addWidget(dialog) 

714 

715 assert dialog.file_list.selectionMode() == dialog.file_list.SelectionMode.ExtendedSelection 

716 

717 def test_file_list_selection_mode_single(self, qtbot, mock_context_nifti, mock_has_existing): 

718 """Test single selection mode.""" 

719 dialog = NiftiFileDialog( 

720 mock_context_nifti, 

721 allow_multiple=False, 

722 has_existing_func=mock_has_existing, 

723 label="test" 

724 ) 

725 qtbot.addWidget(dialog) 

726 

727 assert dialog.file_list.selectionMode() == dialog.file_list.SelectionMode.SingleSelection 

728 

729 def test_file_list_tooltips(self, qtbot, mock_context_nifti, mock_has_existing): 

730 """Test that files have tooltips.""" 

731 dialog = NiftiFileDialog( 

732 mock_context_nifti, 

733 allow_multiple=True, 

734 has_existing_func=mock_has_existing, 

735 label="test" 

736 ) 

737 qtbot.addWidget(dialog) 

738 

739 for i in range(dialog.file_list.count()): 

740 item = dialog.file_list.item(i) 

741 assert item.toolTip() != "" 

742 

743 

744class TestEdgeCases: 

745 """Tests for edge cases.""" 

746 

747 def test_empty_workspace(self, qtbot, mock_has_existing): 

748 """Test with an empty workspace.""" 

749 temp_dir = tempfile.mkdtemp() 

750 context = {"workspace_path": temp_dir} 

751 

752 dialog = NiftiFileDialog( 

753 context, 

754 allow_multiple=True, 

755 has_existing_func=mock_has_existing, 

756 label="test" 

757 ) 

758 qtbot.addWidget(dialog) 

759 

760 assert len(dialog.all_nii_files) == 0 

761 

762 def test_workspace_with_non_nifti(self, qtbot, temp_workspace, mock_has_existing): 

763 """Test workspace with only non-NIfTI files.""" 

764 temp_dir = tempfile.mkdtemp() 

765 context = {"workspace_path": temp_dir} 

766 

767 # Create non-NIfTI file 

768 os.makedirs(os.path.join(temp_dir, "sub-01", "anat")) 

769 with open(os.path.join(temp_dir, "sub-01", "anat", "file.txt"), "w") as f: 

770 f.write("text") 

771 

772 dialog = NiftiFileDialog( 

773 context, 

774 allow_multiple=True, 

775 has_existing_func=mock_has_existing, 

776 label="test" 

777 ) 

778 qtbot.addWidget(dialog) 

779 

780 assert len(dialog.all_nii_files) == 0 

781 

782 def test_unicode_in_filenames(self, qtbot, mock_has_existing): 

783 """Test with Unicode characters in filenames.""" 

784 temp_dir = tempfile.mkdtemp() 

785 context = {"workspace_path": temp_dir} 

786 

787 sub_dir = os.path.join(temp_dir, "sub-01", "anat") 

788 os.makedirs(sub_dir) 

789 

790 unicode_files = ["файл.nii", "文件.nii.gz", "αρχείο.nii"] 

791 for filename in unicode_files: 

792 with open(os.path.join(sub_dir, filename), "w") as f: 

793 f.write("data") 

794 

795 dialog = NiftiFileDialog( 

796 context, 

797 allow_multiple=True, 

798 has_existing_func=mock_has_existing, 

799 label="test" 

800 ) 

801 qtbot.addWidget(dialog) 

802 

803 assert len(dialog.all_nii_files) == 3 

804 

805 def test_very_deep_directory_structure(self, qtbot, temp_workspace, mock_has_existing): 

806 """Test with a very deep directory structure.""" 

807 context = {"workspace_path": temp_workspace} 

808 

809 deep_path = os.path.join(temp_workspace, "sub-01", "ses-01", "extra", "nested", "anat") 

810 os.makedirs(deep_path) 

811 with open(os.path.join(deep_path, "file.nii"), "w") as f: 

812 f.write("data") 

813 

814 dialog = NiftiFileDialog( 

815 context, 

816 allow_multiple=True, 

817 has_existing_func=mock_has_existing, 

818 label="test" 

819 ) 

820 qtbot.addWidget(dialog) 

821 

822 assert len(dialog.all_nii_files) >= 1 

823 

824 def test_label_none_hides_checkboxes(self, qtbot, mock_context_nifti, mock_has_existing): 

825 """Test that label None hides the checkboxes.""" 

826 dialog = NiftiFileDialog( 

827 mock_context_nifti, 

828 allow_multiple=True, 

829 has_existing_func=mock_has_existing, 

830 label=None 

831 ) 

832 qtbot.addWidget(dialog) 

833 

834 assert not dialog.no_flag_checkbox.isVisible() 

835 assert not dialog.with_flag_checkbox.isVisible() 

836 

837 

838class TestInfoLabel: 

839 """Tests for info_label.""" 

840 

841 def test_info_label_shows_count(self, qtbot, mock_context_nifti, mock_has_existing): 

842 """Test that info_label shows the count.""" 

843 dialog = NiftiFileDialog( 

844 mock_context_nifti, 

845 allow_multiple=True, 

846 has_existing_func=mock_has_existing, 

847 label="test" 

848 ) 

849 qtbot.addWidget(dialog) 

850 

851 info_text = dialog.info_label.text() 

852 assert "files" in info_text.lower() or "file" in info_text.lower() 

853 

854 def test_info_label_updates_with_filters(self, qtbot, mock_context_nifti, mock_has_existing): 

855 """Test that info_label updates with filters.""" 

856 dialog = NiftiFileDialog( 

857 mock_context_nifti, 

858 allow_multiple=True, 

859 has_existing_func=mock_has_existing, 

860 label="test" 

861 ) 

862 qtbot.addWidget(dialog) 

863 

864 initial_text = dialog.info_label.text() 

865 

866 dialog.search_bar.setText("T1w") 

867 

868 filtered_text = dialog.info_label.text() 

869 

870 # The text should change 

871 assert initial_text != filtered_text 

872 

873 

874class TestRelativeToAbsolute: 

875 """Tests for the relative_to_absolute mapping.""" 

876 

877 def test_relative_to_absolute_mapping(self, qtbot, mock_context_nifti, mock_has_existing): 

878 """Test that the mapping is correct.""" 

879 dialog = NiftiFileDialog( 

880 mock_context_nifti, 

881 allow_multiple=True, 

882 has_existing_func=mock_has_existing, 

883 label="test" 

884 ) 

885 qtbot.addWidget(dialog) 

886 

887 for relative_path in dialog.all_nii_files: 

888 assert relative_path in dialog.relative_to_absolute 

889 absolute_path = dialog.relative_to_absolute[relative_path] 

890 assert os.path.isabs(absolute_path) 

891 

892 

893class TestFilesWithFlag: 

894 """Tests for files_with_flag.""" 

895 

896 def test_files_with_flag_populated(self, qtbot, mock_context_nifti): 

897 """Test that files_with_flag is populated.""" 

898 

899 def mock_has_existing_some(path, workspace): 

900 return "sub-01_T1w" in path 

901 

902 dialog = NiftiFileDialog( 

903 mock_context_nifti, 

904 allow_multiple=True, 

905 has_existing_func=mock_has_existing_some, 

906 label="test" 

907 ) 

908 qtbot.addWidget(dialog) 

909 

910 assert len(dialog.files_with_flag) > 0 

911 

912 def test_files_with_flag_color(self, qtbot, mock_context_nifti): 

913 """Test that files with flags have a different color.""" 

914 

915 def mock_has_existing_some(path, workspace): 

916 return "sub-01_T1w" in path 

917 

918 dialog = NiftiFileDialog( 

919 mock_context_nifti, 

920 allow_multiple=True, 

921 has_existing_func=mock_has_existing_some, 

922 label="test" 

923 ) 

924 qtbot.addWidget(dialog) 

925 

926 # Find an item with a flag 

927 for i in range(dialog.file_list.count()): 

928 item = dialog.file_list.item(i) 

929 if item.text() in dialog.files_with_flag: 

930 # Should have yellow/warning color 

931 color = item.foreground().color() 

932 assert color == QColor(255, 193, 7) 

933 break 

934 

935 

936class TestButtonConnections: 

937 """Tests for button connections.""" 

938 

939 def test_reset_button_connected(self, qtbot, mock_context_nifti, mock_has_existing): 

940 """Test that the reset button is connected.""" 

941 dialog = NiftiFileDialog( 

942 mock_context_nifti, 

943 allow_multiple=True, 

944 has_existing_func=mock_has_existing, 

945 label="test" 

946 ) 

947 qtbot.addWidget(dialog) 

948 

949 dialog.search_bar.setText("test") 

950 dialog._reset_button.click() 

951 

952 assert dialog.search_bar.text() == "" 

953 

954 def test_select_all_button_connected(self, qtbot, mock_context_nifti, mock_has_existing): 

955 """Test that the select all button is connected.""" 

956 dialog = NiftiFileDialog( 

957 mock_context_nifti, 

958 allow_multiple=True, 

959 has_existing_func=mock_has_existing, 

960 label="test" 

961 ) 

962 qtbot.addWidget(dialog) 

963 

964 dialog._select_all_button.click() 

965 

966 assert len(dialog.file_list.selectedItems()) > 0 

967 

968 def test_deselect_all_button_connected(self, qtbot, mock_context_nifti, mock_has_existing): 

969 """Test that the deselect all button is connected.""" 

970 dialog = NiftiFileDialog( 

971 mock_context_nifti, 

972 allow_multiple=True, 

973 has_existing_func=mock_has_existing, 

974 label="test" 

975 ) 

976 qtbot.addWidget(dialog) 

977 

978 dialog._select_all_button.click() 

979 dialog._deselect_all_button.click() 

980 

981 assert len(dialog.file_list.selectedItems()) == 0 

982 

983 

984class TestIntegration: 

985 """Integration tests.""" 

986 

987 def test_full_workflow_filter_select_accept(self, qtbot, mock_context_nifti, mock_has_existing): 

988 """Test full workflow.""" 

989 dialog = NiftiFileDialog( 

990 mock_context_nifti, 

991 allow_multiple=True, 

992 has_existing_func=mock_has_existing, 

993 label="test" 

994 ) 

995 qtbot.addWidget(dialog) 

996 

997 # Filter 

998 dialog.search_bar.setText("T1w") 

999 

1000 # Select all visible files 

1001 dialog._select_all_button.click() 

1002 

1003 # Accept 

1004 with patch.object(dialog, 'accept'): 

1005 dialog._accept() 

1006 

1007 assert len(dialog.selected_files) > 0 

1008 

1009 def test_full_workflow_with_warning(self, qtbot, mock_context_nifti): 

1010 """Test workflow with warning.""" 

1011 

1012 def mock_has_existing_some(path, workspace): 

1013 return "sub-01_T1w" in path 

1014 

1015 dialog = NiftiFileDialog( 

1016 mock_context_nifti, 

1017 allow_multiple=True, 

1018 has_existing_func=mock_has_existing_some, 

1019 label="mask" 

1020 ) 

1021 qtbot.addWidget(dialog) 

1022 

1023 # Select file with flag 

1024 for i in range(dialog.file_list.count()): 

1025 item = dialog.file_list.item(i) 

1026 if item.text() in dialog.files_with_flag: 

1027 item.setSelected(True) 

1028 break 

1029 

1030 # Accept with warning 

1031 with patch.object(QMessageBox, 'exec', return_value=QMessageBox.StandardButton.Yes): 

1032 with patch.object(dialog, 'accept'): 

1033 dialog._accept() 

1034 

1035 assert len(dialog.selected_files) > 0 

1036 

1037 

1038class TestStateConsistency: 

1039 """Tests for state consistency.""" 

1040 

1041 def test_state_after_filter_changes(self, qtbot, mock_context_nifti, mock_has_existing): 

1042 """Test consistency after changing filters.""" 

1043 dialog = NiftiFileDialog( 

1044 mock_context_nifti, 

1045 allow_multiple=True, 

1046 has_existing_func=mock_has_existing, 

1047 label="test" 

1048 ) 

1049 qtbot.addWidget(dialog) 

1050 

1051 # Change multiple filters 

1052 dialog.subject_combo.setCurrentIndex(1) 

1053 dialog.search_bar.setText("T1w") 

1054 dialog.no_flag_checkbox.setChecked(True) 

1055 

1056 # Check that state is coherent 

1057 visible = sum(1 for i in range(dialog.file_list.count()) 

1058 if not dialog.file_list.item(i).isHidden()) 

1059 

1060 # There should be at least some visible files or none 

1061 assert visible >= 0 

1062 

1063 def test_state_after_reset(self, qtbot, mock_context_nifti, mock_has_existing): 

1064 """Test state after reset.""" 

1065 dialog = NiftiFileDialog( 

1066 mock_context_nifti, 

1067 allow_multiple=True, 

1068 has_existing_func=mock_has_existing, 

1069 label="test" 

1070 ) 

1071 qtbot.addWidget(dialog) 

1072 

1073 # Set filters 

1074 dialog.search_bar.setText("test") 

1075 dialog.subject_combo.setCurrentIndex(1) 

1076 

1077 # Reset 

1078 dialog._reset_filters() 

1079 

1080 # All filters should return to default 

1081 assert dialog.search_bar.text() == "" 

1082 assert dialog.subject_combo.currentIndex() == 0 

1083 

1084 

1085class TestMemoryAndPerformance: 

1086 """Tests for memory and performance.""" 

1087 

1088 def test_many_files_performance(self, qtbot, mock_has_existing): 

1089 """Test performance with many files.""" 

1090 temp_dir = tempfile.mkdtemp() 

1091 context = {"workspace_path": temp_dir} 

1092 

1093 # Create many files 

1094 sub_dir = os.path.join(temp_dir, "sub-01", "anat") 

1095 os.makedirs(sub_dir) 

1096 

1097 for i in range(100): 

1098 with open(os.path.join(sub_dir, f"file{i}.nii"), "w") as f: 

1099 f.write("data") 

1100 

1101 dialog = NiftiFileDialog( 

1102 context, 

1103 allow_multiple=True, 

1104 has_existing_func=mock_has_existing, 

1105 label="test" 

1106 ) 

1107 qtbot.addWidget(dialog) 

1108 

1109 # Should not be too slow 

1110 assert len(dialog.all_nii_files) == 100 

1111 

1112 def test_rapid_filter_changes(self, qtbot, mock_context_nifti, mock_has_existing): 

1113 """Test with rapid filter changes.""" 

1114 dialog = NiftiFileDialog( 

1115 mock_context_nifti, 

1116 allow_multiple=True, 

1117 has_existing_func=mock_has_existing, 

1118 label="test" 

1119 ) 

1120 qtbot.addWidget(dialog) 

1121 

1122 # Change filters rapidly 

1123 for i in range(20): 

1124 dialog.search_bar.setText(f"test{i}") 

1125 dialog.subject_combo.setCurrentIndex(i % dialog.subject_combo.count()) 

1126 

1127 # Should not crash 

1128 assert True 

1129 

1130 

1131class TestAccessibility: 

1132 """Tests for accessibility.""" 

1133 

1134 def test_buttons_have_text(self, qtbot, mock_context_nifti, mock_has_existing): 

1135 """Test that all buttons have text.""" 

1136 dialog = NiftiFileDialog( 

1137 mock_context_nifti, 

1138 allow_multiple=True, 

1139 has_existing_func=mock_has_existing, 

1140 label="test" 

1141 ) 

1142 qtbot.addWidget(dialog) 

1143 

1144 assert len(dialog._reset_button.text()) > 0 

1145 assert len(dialog._select_all_button.text()) > 0 

1146 assert len(dialog._deselect_all_button.text()) > 0 

1147 

1148 def test_labels_have_text(self, qtbot, mock_context_nifti, mock_has_existing): 

1149 """Test that labels have text.""" 

1150 dialog = NiftiFileDialog( 

1151 mock_context_nifti, 

1152 allow_multiple=True, 

1153 has_existing_func=mock_has_existing, 

1154 label="test" 

1155 ) 

1156 qtbot.addWidget(dialog) 

1157 

1158 assert dialog.info_label.text() != "" 

1159 

1160 def test_search_bar_placeholder(self, qtbot, mock_context_nifti, mock_has_existing): 

1161 """Test that search bar has a placeholder.""" 

1162 dialog = NiftiFileDialog( 

1163 mock_context_nifti, 

1164 allow_multiple=True, 

1165 has_existing_func=mock_has_existing, 

1166 label="test" 

1167 ) 

1168 qtbot.addWidget(dialog) 

1169 

1170 assert dialog.search_bar.placeholderText() != "" 

1171 

1172 

1173class TestTranslations: 

1174 """Tests for translations.""" 

1175 

1176 def test_window_title_includes_label(self, qtbot, mock_context_nifti, mock_has_existing): 

1177 """Test that the window title includes the label.""" 

1178 dialog = NiftiFileDialog( 

1179 mock_context_nifti, 

1180 allow_multiple=True, 

1181 has_existing_func=mock_has_existing, 

1182 label="mask" 

1183 ) 

1184 qtbot.addWidget(dialog) 

1185 

1186 title = dialog.windowTitle() 

1187 assert "mask" in title.lower() or len(title) > 0 

1188 

1189 

1190class TestErrorHandling: 

1191 """Tests for error handling.""" 

1192 

1193 def test_has_existing_func_exception(self, qtbot, mock_context_nifti): 

1194 """Test when has_existing_func raises an exception.""" 

1195 

1196 def mock_has_existing_error(path, workspace): 

1197 raise Exception("Test error") 

1198 

1199 # Should not crash during initialization 

1200 try: 

1201 dialog = NiftiFileDialog( 

1202 mock_context_nifti, 

1203 allow_multiple=True, 

1204 has_existing_func=mock_has_existing_error, 

1205 label="test" 

1206 ) 

1207 qtbot.addWidget(dialog) 

1208 except Exception: 

1209 # Acceptable if the exception propagates 

1210 pass 

1211 

1212 def test_invalid_workspace_path(self, qtbot, mock_has_existing): 

1213 """Test with invalid workspace path.""" 

1214 context = {"workspace_path": "/nonexistent/path"} 

1215 

1216 dialog = NiftiFileDialog( 

1217 context, 

1218 allow_multiple=True, 

1219 has_existing_func=mock_has_existing, 

1220 label="test" 

1221 ) 

1222 qtbot.addWidget(dialog) 

1223 

1224 # Should handle gracefully 

1225 assert len(dialog.all_nii_files) == 0 

1226 

1227 

1228class TestComboBoxBehavior: 

1229 """Tests for combo box behavior.""" 

1230 

1231 def test_combo_boxes_have_all_option(self, qtbot, mock_context_nifti, mock_has_existing): 

1232 """Test that combo boxes have the 'All' option.""" 

1233 dialog = NiftiFileDialog( 

1234 mock_context_nifti, 

1235 allow_multiple=True, 

1236 has_existing_func=mock_has_existing, 

1237 label="test" 

1238 ) 

1239 qtbot.addWidget(dialog) 

1240 

1241 assert "All" in dialog.subject_combo.itemText(0) 

1242 assert "All" in dialog.session_combo.itemText(0) 

1243 assert "All" in dialog.modality_combo.itemText(0) 

1244 assert "All" in dialog.datatype_combo.itemText(0) 

1245 

1246 def test_combo_boxes_sorted(self, qtbot, mock_context_nifti, mock_has_existing): 

1247 """Test that combo boxes are sorted.""" 

1248 dialog = NiftiFileDialog( 

1249 mock_context_nifti, 

1250 allow_multiple=True, 

1251 has_existing_func=mock_has_existing, 

1252 label="test" 

1253 ) 

1254 qtbot.addWidget(dialog) 

1255 

1256 # Subjects should be sorted (after "All") 

1257 subjects = [dialog.subject_combo.itemText(i) 

1258 for i in range(1, dialog.subject_combo.count())] 

1259 

1260 if len(subjects) > 1: 

1261 assert subjects == sorted(subjects)