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
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 15:51 +0000
1import os
2import tempfile
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
10from main.components.nifti_file_dialog import NiftiFileDialog
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)
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")
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")
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")
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")
48 return temp_dir
51@pytest.fixture
52def mock_context_nifti(nifti_workspace):
53 """Context for NiftiFileDialog."""
54 return {"workspace_path": nifti_workspace}
57@pytest.fixture
58def mock_has_existing():
59 """Mock for has_existing_func."""
60 return Mock(return_value=False)
63class TestNiftiFileDialogInitialization:
64 """Tests for dialog initialization."""
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)
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 == []
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)
92 assert dialog.allow_multiple is False
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)
103 # Should use default lambda
104 assert callable(dialog.has_existing_func)
105 assert dialog.has_existing_func("any_path", "any_workspace") is False
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"}
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)
120 assert dialog.forced_filters == filters
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)
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
143class TestPopulateFiles:
144 """Tests for the _populate_files method."""
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)
156 # Should find 5 files (.nii and .nii.gz) + 1 derivatives
157 assert len(dialog.all_nii_files) >= 5
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)
169 # Should have at least 2 subjects + "All subjects"
170 assert dialog.subject_combo.count() >= 3
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)
182 # Should have ses-01 + "All sessions"
183 assert dialog.session_combo.count() >= 2
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)
195 # Should have T1w, T2w, FLAIR, pet + "All modalities"
196 assert dialog.modality_combo.count() >= 4
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)
208 # Should have anat, pet + "All types"
209 assert dialog.datatype_combo.count() >= 3
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)
221 # Should be called for each file
222 assert mock_has_existing.call_count > 0
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)
234 assert dialog.file_list.count() > 0
237class TestApplyFilters:
238 """Tests for the _apply_filters method."""
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)
250 initial_visible = sum(1 for i in range(dialog.file_list.count())
251 if not dialog.file_list.item(i).isHidden())
253 dialog.search_bar.setText("T1w")
255 visible_after = sum(1 for i in range(dialog.file_list.count())
256 if not dialog.file_list.item(i).isHidden())
258 assert visible_after < initial_visible
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)
270 dialog.subject_combo.setCurrentText("sub-01")
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()
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)
288 dialog.session_combo.setCurrentText("ses-01")
290 visible = sum(1 for i in range(dialog.file_list.count())
291 if not dialog.file_list.item(i).isHidden())
293 assert visible >= 1
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)
305 dialog.modality_combo.setCurrentText("FLAIR")
307 visible = sum(1 for i in range(dialog.file_list.count())
308 if not dialog.file_list.item(i).isHidden())
310 assert visible >= 1
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)
322 dialog.datatype_combo.setCurrentText("anat")
324 visible = sum(1 for i in range(dialog.file_list.count())
325 if not dialog.file_list.item(i).isHidden())
327 assert visible >= 1
329 def test_apply_filters_no_flag(self, qtbot, mock_context_nifti):
330 """Test no-flag filter."""
332 # has_existing returns True for some files
333 def mock_has_existing_selective(path, workspace):
334 return "sub-01_T1w" in path
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)
344 dialog.no_flag_checkbox.setChecked(True)
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
352 def test_apply_filters_with_flag(self, qtbot, mock_context_nifti):
353 """Test with-flag filter."""
355 def mock_has_existing_selective(path, workspace):
356 return "sub-01_T1w" in path
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)
366 dialog.with_flag_checkbox.setChecked(True)
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
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)
384 dialog.subject_combo.setCurrentText("sub-01")
385 dialog.search_bar.setText("T1w")
387 visible = sum(1 for i in range(dialog.file_list.count())
388 if not dialog.file_list.item(i).isHidden())
390 # Should only find T1w of sub-01
391 assert visible >= 1
394class TestCheckboxBehavior:
395 """Tests for checkbox behavior."""
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)
407 dialog.with_flag_checkbox.setChecked(True)
408 dialog.no_flag_checkbox.setChecked(True)
410 assert not dialog.with_flag_checkbox.isChecked()
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)
422 dialog.no_flag_checkbox.setChecked(True)
423 dialog.with_flag_checkbox.setChecked(True)
425 assert not dialog.no_flag_checkbox.isChecked()
428class TestResetFilters:
429 """Tests for the _reset_filters method."""
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)
441 # Set filters
442 dialog.search_bar.setText("test")
443 dialog.subject_combo.setCurrentIndex(1)
444 dialog.no_flag_checkbox.setChecked(True)
446 # Reset
447 dialog._reset_filters()
449 assert dialog.search_bar.text() == ""
450 assert dialog.subject_combo.currentIndex() == 0
451 assert not dialog.no_flag_checkbox.isChecked()
454class TestSelectAllVisible:
455 """Tests for the _select_all_visible method."""
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)
467 dialog._select_all_visible()
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()
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)
485 dialog.search_bar.setText("T1w")
486 dialog._select_all_visible()
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())
493 assert selected_count == visible_count
496class TestAccept:
497 """Tests for the _accept method."""
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)
509 # Select first item
510 dialog.file_list.item(0).setSelected(True)
512 with patch.object(dialog, 'accept', wraps=dialog.accept) as mock_accept:
513 dialog._accept()
515 mock_accept.assert_called_once()
516 assert len(dialog.selected_files) == 1
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)
528 with patch.object(QMessageBox, 'warning') as mock_warning:
529 dialog._accept()
531 mock_warning.assert_called_once()
533 def test_accept_with_existing_flag_warning(self, qtbot, mock_context_nifti):
534 """Test warning when there are files with flags."""
536 def mock_has_existing_selective(path, workspace):
537 return "sub-01_T1w" in path
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)
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
554 with patch.object(QMessageBox, 'exec', return_value=QMessageBox.StandardButton.Yes):
555 with patch.object(dialog, 'accept'):
556 dialog._accept()
558 def test_accept_warning_cancelled(self, qtbot, mock_context_nifti):
559 """Test cancellation after warning."""
561 def mock_has_existing_selective(path, workspace):
562 return True # All have flags
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)
572 dialog.file_list.item(0).setSelected(True)
574 with patch.object(QMessageBox, 'exec', return_value=QMessageBox.StandardButton.No):
575 dialog._accept()
577 # Should not have set selected_files
578 assert len(dialog.selected_files) == 0
581class TestGetFilesStaticMethod:
582 """Tests for the static method get_files."""
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
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 )
598 assert result == dialog_instance.selected_files
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 )
610 assert result is None
613class TestForcedFilters:
614 """Tests for forced_filters."""
616 def test_forced_filters_search(self, qtbot, mock_context_nifti, mock_has_existing):
617 """Test forced filter for search."""
618 filters = {"search": "T1w"}
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)
629 assert dialog.search_bar.text() == "T1w"
630 assert not dialog.search_bar.isEnabled()
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"}
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)
645 assert dialog.subject_combo.currentText() == "sub-01"
646 assert not dialog.subject_combo.isEnabled()
648 def test_forced_filters_modality(self, qtbot, mock_context_nifti, mock_has_existing):
649 """Test forced filter for modality."""
650 filters = {"modality": "T1w"}
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)
661 assert dialog.modality_combo.currentText() == "T1w"
662 assert not dialog.modality_combo.isEnabled()
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}
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)
677 assert dialog.no_flag_checkbox.isChecked()
678 assert not dialog.no_flag_checkbox.isEnabled()
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 }
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)
697 assert dialog.subject_combo.currentText() == "sub-01"
698 assert dialog.modality_combo.currentText() == "T1w"
699 assert dialog.no_flag_checkbox.isChecked()
702class TestFileListBehavior:
703 """Tests for file list behavior."""
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)
715 assert dialog.file_list.selectionMode() == dialog.file_list.SelectionMode.ExtendedSelection
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)
727 assert dialog.file_list.selectionMode() == dialog.file_list.SelectionMode.SingleSelection
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)
739 for i in range(dialog.file_list.count()):
740 item = dialog.file_list.item(i)
741 assert item.toolTip() != ""
744class TestEdgeCases:
745 """Tests for edge cases."""
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}
752 dialog = NiftiFileDialog(
753 context,
754 allow_multiple=True,
755 has_existing_func=mock_has_existing,
756 label="test"
757 )
758 qtbot.addWidget(dialog)
760 assert len(dialog.all_nii_files) == 0
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}
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")
772 dialog = NiftiFileDialog(
773 context,
774 allow_multiple=True,
775 has_existing_func=mock_has_existing,
776 label="test"
777 )
778 qtbot.addWidget(dialog)
780 assert len(dialog.all_nii_files) == 0
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}
787 sub_dir = os.path.join(temp_dir, "sub-01", "anat")
788 os.makedirs(sub_dir)
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")
795 dialog = NiftiFileDialog(
796 context,
797 allow_multiple=True,
798 has_existing_func=mock_has_existing,
799 label="test"
800 )
801 qtbot.addWidget(dialog)
803 assert len(dialog.all_nii_files) == 3
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}
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")
814 dialog = NiftiFileDialog(
815 context,
816 allow_multiple=True,
817 has_existing_func=mock_has_existing,
818 label="test"
819 )
820 qtbot.addWidget(dialog)
822 assert len(dialog.all_nii_files) >= 1
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)
834 assert not dialog.no_flag_checkbox.isVisible()
835 assert not dialog.with_flag_checkbox.isVisible()
838class TestInfoLabel:
839 """Tests for info_label."""
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)
851 info_text = dialog.info_label.text()
852 assert "files" in info_text.lower() or "file" in info_text.lower()
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)
864 initial_text = dialog.info_label.text()
866 dialog.search_bar.setText("T1w")
868 filtered_text = dialog.info_label.text()
870 # The text should change
871 assert initial_text != filtered_text
874class TestRelativeToAbsolute:
875 """Tests for the relative_to_absolute mapping."""
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)
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)
893class TestFilesWithFlag:
894 """Tests for files_with_flag."""
896 def test_files_with_flag_populated(self, qtbot, mock_context_nifti):
897 """Test that files_with_flag is populated."""
899 def mock_has_existing_some(path, workspace):
900 return "sub-01_T1w" in path
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)
910 assert len(dialog.files_with_flag) > 0
912 def test_files_with_flag_color(self, qtbot, mock_context_nifti):
913 """Test that files with flags have a different color."""
915 def mock_has_existing_some(path, workspace):
916 return "sub-01_T1w" in path
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)
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
936class TestButtonConnections:
937 """Tests for button connections."""
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)
949 dialog.search_bar.setText("test")
950 dialog._reset_button.click()
952 assert dialog.search_bar.text() == ""
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)
964 dialog._select_all_button.click()
966 assert len(dialog.file_list.selectedItems()) > 0
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)
978 dialog._select_all_button.click()
979 dialog._deselect_all_button.click()
981 assert len(dialog.file_list.selectedItems()) == 0
984class TestIntegration:
985 """Integration tests."""
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)
997 # Filter
998 dialog.search_bar.setText("T1w")
1000 # Select all visible files
1001 dialog._select_all_button.click()
1003 # Accept
1004 with patch.object(dialog, 'accept'):
1005 dialog._accept()
1007 assert len(dialog.selected_files) > 0
1009 def test_full_workflow_with_warning(self, qtbot, mock_context_nifti):
1010 """Test workflow with warning."""
1012 def mock_has_existing_some(path, workspace):
1013 return "sub-01_T1w" in path
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)
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
1030 # Accept with warning
1031 with patch.object(QMessageBox, 'exec', return_value=QMessageBox.StandardButton.Yes):
1032 with patch.object(dialog, 'accept'):
1033 dialog._accept()
1035 assert len(dialog.selected_files) > 0
1038class TestStateConsistency:
1039 """Tests for state consistency."""
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)
1051 # Change multiple filters
1052 dialog.subject_combo.setCurrentIndex(1)
1053 dialog.search_bar.setText("T1w")
1054 dialog.no_flag_checkbox.setChecked(True)
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())
1060 # There should be at least some visible files or none
1061 assert visible >= 0
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)
1073 # Set filters
1074 dialog.search_bar.setText("test")
1075 dialog.subject_combo.setCurrentIndex(1)
1077 # Reset
1078 dialog._reset_filters()
1080 # All filters should return to default
1081 assert dialog.search_bar.text() == ""
1082 assert dialog.subject_combo.currentIndex() == 0
1085class TestMemoryAndPerformance:
1086 """Tests for memory and performance."""
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}
1093 # Create many files
1094 sub_dir = os.path.join(temp_dir, "sub-01", "anat")
1095 os.makedirs(sub_dir)
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")
1101 dialog = NiftiFileDialog(
1102 context,
1103 allow_multiple=True,
1104 has_existing_func=mock_has_existing,
1105 label="test"
1106 )
1107 qtbot.addWidget(dialog)
1109 # Should not be too slow
1110 assert len(dialog.all_nii_files) == 100
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)
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())
1127 # Should not crash
1128 assert True
1131class TestAccessibility:
1132 """Tests for accessibility."""
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)
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
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)
1158 assert dialog.info_label.text() != ""
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)
1170 assert dialog.search_bar.placeholderText() != ""
1173class TestTranslations:
1174 """Tests for translations."""
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)
1186 title = dialog.windowTitle()
1187 assert "mask" in title.lower() or len(title) > 0
1190class TestErrorHandling:
1191 """Tests for error handling."""
1193 def test_has_existing_func_exception(self, qtbot, mock_context_nifti):
1194 """Test when has_existing_func raises an exception."""
1196 def mock_has_existing_error(path, workspace):
1197 raise Exception("Test error")
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
1212 def test_invalid_workspace_path(self, qtbot, mock_has_existing):
1213 """Test with invalid workspace path."""
1214 context = {"workspace_path": "/nonexistent/path"}
1216 dialog = NiftiFileDialog(
1217 context,
1218 allow_multiple=True,
1219 has_existing_func=mock_has_existing,
1220 label="test"
1221 )
1222 qtbot.addWidget(dialog)
1224 # Should handle gracefully
1225 assert len(dialog.all_nii_files) == 0
1228class TestComboBoxBehavior:
1229 """Tests for combo box behavior."""
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)
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)
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)
1256 # Subjects should be sorted (after "All")
1257 subjects = [dialog.subject_combo.itemText(i)
1258 for i in range(1, dialog.subject_combo.count())]
1260 if len(subjects) > 1:
1261 assert subjects == sorted(subjects)