Coverage for tests/components/test_folder_card.py: 100%
357 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 pytest
3from unittest.mock import Mock, patch, MagicMock
4from PyQt6.QtWidgets import QDialog
5from PyQt6.QtCore import Qt
7from main.components.folder_card import FolderCard
10@pytest.fixture
11def test_folder_with_files(temp_workspace):
12 """Create a test folder with files."""
13 folder = os.path.join(temp_workspace, "test_output")
14 os.makedirs(folder)
16 # Create some initial files
17 initial_files = ["file1.txt", "file2.csv", "result.nii"]
18 for filename in initial_files:
19 with open(os.path.join(folder, filename), "w") as f:
20 f.write("test content")
22 return folder
25@pytest.fixture
26def empty_folder(temp_workspace):
27 """Create an empty folder."""
28 folder = os.path.join(temp_workspace, "empty_output")
29 os.makedirs(folder)
30 return folder
33@pytest.fixture
34def mock_context_card():
35 """Mock context for FolderCard."""
36 return {}
39class TestFolderCardInitialization:
40 """Tests for FolderCard initialization."""
42 def test_initialization_existing_folder(self, qtbot, mock_context_card, test_folder_with_files):
43 """Test initialization with existing folder."""
44 card = FolderCard(mock_context_card, test_folder_with_files)
45 qtbot.addWidget(card)
47 assert card.folder == test_folder_with_files
48 assert card.files == []
49 assert len(card.existing_files) == 3 # file1, file2, result
50 assert card.pulse_animation is None
51 assert card._scale == 1.0
52 assert card._glow_opacity == 0.0
54 def test_initialization_empty_folder(self, qtbot, mock_context_card, empty_folder):
55 """Test initialization with empty folder."""
56 card = FolderCard(mock_context_card, empty_folder)
57 qtbot.addWidget(card)
59 assert card.folder == empty_folder
60 assert len(card.existing_files) == 0
62 def test_initialization_nonexistent_folder(self, qtbot, mock_context_card, temp_workspace):
63 """Test initialization with nonexistent folder."""
64 nonexistent = os.path.join(temp_workspace, "nonexistent")
66 card = FolderCard(mock_context_card, nonexistent)
67 qtbot.addWidget(card)
69 assert card.folder == nonexistent
70 assert card.existing_files == set()
72 def test_ui_elements_created(self, qtbot, mock_context_card, test_folder_with_files):
73 """Test that all UI elements are created."""
74 card = FolderCard(mock_context_card, test_folder_with_files)
75 qtbot.addWidget(card)
77 assert card.card_frame is not None
78 assert card.folder_icon is not None
79 assert card.folder_name is not None
80 assert card.status_label is not None
81 assert card.action_btn is not None
83 def test_fixed_height_set(self, qtbot, mock_context_card, test_folder_with_files):
84 """Test that the height is fixed."""
85 card = FolderCard(mock_context_card, test_folder_with_files)
86 qtbot.addWidget(card)
88 assert card.height() == 100
90 def test_folder_name_display(self, qtbot, mock_context_card, test_folder_with_files):
91 """Test that the folder name is displayed."""
92 card = FolderCard(mock_context_card, test_folder_with_files)
93 qtbot.addWidget(card)
95 assert card.folder_name.text() == os.path.basename(test_folder_with_files)
97 def test_initial_status(self, qtbot, mock_context_card, test_folder_with_files):
98 """Test initial status."""
99 card = FolderCard(mock_context_card, test_folder_with_files)
100 qtbot.addWidget(card)
102 assert "Waiting" in card.status_label.text() or "wait" in card.status_label.text().lower()
103 assert not card.action_btn.isEnabled()
105 def test_initial_icon(self, qtbot, mock_context_card, test_folder_with_files):
106 """Test initial icon."""
107 card = FolderCard(mock_context_card, test_folder_with_files)
108 qtbot.addWidget(card)
110 assert "📁" in card.folder_icon.text()
113class TestAddFiles:
114 """Tests for the add_files method."""
116 def test_add_files_basic(self, qtbot, mock_context_card, test_folder_with_files):
117 """Test basic file addition."""
118 card = FolderCard(mock_context_card, test_folder_with_files)
119 qtbot.addWidget(card)
121 new_files = ["new1.txt", "new2.csv"]
122 card.add_files(new_files)
124 assert len(card.files) == 2
125 assert "new1.txt" in card.files
126 assert "new2.csv" in card.files
128 def test_add_files_updates_status(self, qtbot, mock_context_card, test_folder_with_files):
129 """Test that add_files updates status."""
130 card = FolderCard(mock_context_card, test_folder_with_files)
131 qtbot.addWidget(card)
133 card.add_files(["file1.txt"])
135 assert "1" in card.status_label.text()
137 def test_add_files_enables_button(self, qtbot, mock_context_card, test_folder_with_files):
138 """Test that add_files enables the button."""
139 card = FolderCard(mock_context_card, test_folder_with_files)
140 qtbot.addWidget(card)
142 assert not card.action_btn.isEnabled()
144 card.add_files(["file1.txt"])
146 assert card.action_btn.isEnabled()
148 def test_add_files_changes_icon(self, qtbot, mock_context_card, test_folder_with_files):
149 """Test that add_files changes the icon."""
150 card = FolderCard(mock_context_card, test_folder_with_files)
151 qtbot.addWidget(card)
153 initial_icon = card.folder_icon.text()
155 card.add_files(["file1.txt"])
157 assert card.folder_icon.text() != initial_icon
158 assert "✓" in card.folder_icon.text()
160 def test_add_files_multiple_times(self, qtbot, mock_context_card, test_folder_with_files):
161 """Test multiple file additions."""
162 card = FolderCard(mock_context_card, test_folder_with_files)
163 qtbot.addWidget(card)
165 card.add_files(["file1.txt"])
166 assert len(card.files) == 1
168 card.add_files(["file2.txt", "file3.txt"])
169 assert len(card.files) == 3
171 def test_add_files_empty_list(self, qtbot, mock_context_card, test_folder_with_files):
172 """Test adding an empty list."""
173 card = FolderCard(mock_context_card, test_folder_with_files)
174 qtbot.addWidget(card)
176 card.add_files([])
178 assert len(card.files) == 0
180 def test_add_files_starts_animation(self, qtbot, mock_context_card, test_folder_with_files):
181 """Test that add_files starts animation."""
182 card = FolderCard(mock_context_card, test_folder_with_files)
183 qtbot.addWidget(card)
185 with patch.object(card, 'start_pulse_animation') as mock_anim:
186 card.add_files(["file1.txt"])
188 mock_anim.assert_called_once()
191class TestStartPulseAnimation:
192 """Tests for the start_pulse_animation method."""
194 def test_start_pulse_animation_creates_animation(self, qtbot, mock_context_card, test_folder_with_files):
195 """Test that start_pulse_animation creates animation."""
196 card = FolderCard(mock_context_card, test_folder_with_files)
197 qtbot.addWidget(card)
199 card.start_pulse_animation()
201 assert card.pulse_animation is not None
203 def test_start_pulse_animation_stops_existing(self, qtbot, mock_context_card, test_folder_with_files):
204 """Test that it stops existing animation."""
205 card = FolderCard(mock_context_card, test_folder_with_files)
206 qtbot.addWidget(card)
208 # First animation
209 card.start_pulse_animation()
210 first_anim = card.pulse_animation
212 # Second animation
213 card.start_pulse_animation()
215 assert card.pulse_animation is not None
216 # Could be the same or a new one
218 def test_start_pulse_animation_multiple_calls(self, qtbot, mock_context_card, test_folder_with_files):
219 """Test multiple calls."""
220 card = FolderCard(mock_context_card, test_folder_with_files)
221 qtbot.addWidget(card)
223 for _ in range(5):
224 card.start_pulse_animation()
226 # Should not crash
227 assert card.pulse_animation is not None
230class TestResetState:
231 """Tests for the reset_state method."""
233 def test_reset_state_basic(self, qtbot, mock_context_card, test_folder_with_files):
234 """Test basic state reset."""
235 card = FolderCard(mock_context_card, test_folder_with_files)
236 qtbot.addWidget(card)
238 # Add files to change state
239 card.add_files(["file1.txt"])
241 # Reset
242 card.reset_state()
244 assert not card.action_btn.isEnabled()
245 assert "📁" in card.folder_icon.text()
247 def test_reset_state_stops_animation(self, qtbot, mock_context_card, test_folder_with_files):
248 """Test that reset stops animation."""
249 card = FolderCard(mock_context_card, test_folder_with_files)
250 qtbot.addWidget(card)
252 card.start_pulse_animation()
253 assert card.pulse_animation is not None
255 card.reset_state()
257 # Animation should be stopped (verifiable through state)
259 def test_reset_state_restores_status_text(self, qtbot, mock_context_card, test_folder_with_files):
260 """Test that reset restores status text."""
261 card = FolderCard(mock_context_card, test_folder_with_files)
262 qtbot.addWidget(card)
264 card.add_files(["file1.txt"])
266 card.reset_state()
268 assert "Waiting" in card.status_label.text() or "wait" in card.status_label.text().lower()
270 def test_reset_state_without_prior_changes(self, qtbot, mock_context_card, test_folder_with_files):
271 """Test reset without prior changes."""
272 card = FolderCard(mock_context_card, test_folder_with_files)
273 qtbot.addWidget(card)
275 # Reset without modifications
276 card.reset_state()
278 # Should not crash
279 assert not card.action_btn.isEnabled()
282class TestCheckNewFiles:
283 """Tests for the check_new_files method."""
285 def test_check_new_files_detects_new(self, qtbot, mock_context_card, test_folder_with_files):
286 """Test detection of new files."""
287 card = FolderCard(mock_context_card, test_folder_with_files)
288 qtbot.addWidget(card)
290 initial_count = len(card.existing_files)
292 # Add a new file to the folder
293 new_file = os.path.join(test_folder_with_files, "brand_new.txt")
294 with open(new_file, "w") as f:
295 f.write("new content")
297 card.check_new_files()
299 assert len(card.files) == 1
300 assert "brand_new.txt" in card.files
301 assert len(card.existing_files) == initial_count + 1
303 def test_check_new_files_no_changes(self, qtbot, mock_context_card, test_folder_with_files):
304 """Test when there are no new files."""
305 card = FolderCard(mock_context_card, test_folder_with_files)
306 qtbot.addWidget(card)
308 card.check_new_files()
310 assert len(card.files) == 0
312 def test_check_new_files_updates_existing(self, qtbot, mock_context_card, test_folder_with_files):
313 """Test that existing_files gets updated."""
314 card = FolderCard(mock_context_card, test_folder_with_files)
315 qtbot.addWidget(card)
317 initial_existing = card.existing_files.copy()
319 # Add file
320 new_file = os.path.join(test_folder_with_files, "new.txt")
321 with open(new_file, "w") as f:
322 f.write("content")
324 card.check_new_files()
326 assert len(card.existing_files) > len(initial_existing)
327 assert "new.txt" in card.existing_files
329 def test_check_new_files_nonexistent_folder(self, qtbot, mock_context_card, temp_workspace):
330 """Test with nonexistent folder."""
331 nonexistent = os.path.join(temp_workspace, "nonexistent")
332 card = FolderCard(mock_context_card, nonexistent)
333 qtbot.addWidget(card)
335 # Should not crash
336 card.check_new_files()
338 assert len(card.files) == 0
340 def test_check_new_files_multiple_new(self, qtbot, mock_context_card, test_folder_with_files):
341 """Test detection of multiple files."""
342 card = FolderCard(mock_context_card, test_folder_with_files)
343 qtbot.addWidget(card)
345 # Add multiple files
346 for i in range(5):
347 new_file = os.path.join(test_folder_with_files, f"new{i}.txt")
348 with open(new_file, "w") as f:
349 f.write("content")
351 card.check_new_files()
353 assert len(card.files) == 5
355 def test_check_new_files_calls_add_files(self, qtbot, mock_context_card, test_folder_with_files):
356 """Test that add_files is called."""
357 card = FolderCard(mock_context_card, test_folder_with_files)
358 qtbot.addWidget(card)
360 # Add file
361 new_file = os.path.join(test_folder_with_files, "new.txt")
362 with open(new_file, "w") as f:
363 f.write("content")
365 with patch.object(card, 'add_files', wraps=card.add_files) as mock_add:
366 card.check_new_files()
368 mock_add.assert_called_once()
370class TestShowFilesDialog:
371 """Tests for the show_files_dialog method."""
373 def test_show_files_dialog_with_files(self, qtbot, mock_context_card, test_folder_with_files):
374 """Test opening the dialog when files exist."""
375 card = FolderCard(mock_context_card, test_folder_with_files)
376 qtbot.addWidget(card)
378 card.add_files(["file1.txt", "file2.csv"])
380 with patch.object(QDialog, 'exec') as mock_exec:
381 card.show_files_dialog()
383 mock_exec.assert_called_once()
385 def test_show_files_dialog_empty(self, qtbot, mock_context_card, test_folder_with_files):
386 """Test that the dialog is not opened if there are no files."""
387 card = FolderCard(mock_context_card, test_folder_with_files)
388 qtbot.addWidget(card)
390 with patch.object(QDialog, 'exec') as mock_exec:
391 card.show_files_dialog()
393 mock_exec.assert_not_called()
395 def test_show_files_dialog_clears_files(self, qtbot, mock_context_card, test_folder_with_files):
396 """Test that files are cleared after closing."""
397 card = FolderCard(mock_context_card, test_folder_with_files)
398 qtbot.addWidget(card)
400 card.add_files(["file1.txt"])
402 with patch.object(QDialog, 'exec'):
403 card.show_files_dialog()
405 assert len(card.files) == 0
407 def test_show_files_dialog_resets_state(self, qtbot, mock_context_card, test_folder_with_files):
408 """Test that the state is reset after closing."""
409 card = FolderCard(mock_context_card, test_folder_with_files)
410 qtbot.addWidget(card)
412 card.add_files(["file1.txt"])
414 with patch.object(QDialog, 'exec'):
415 card.show_files_dialog()
417 assert not card.action_btn.isEnabled()
420class TestOpenFolderSignal:
421 """Tests for the open_folder_requested signal."""
423 def test_open_folder_signal_exists(self, qtbot, mock_context_card, test_folder_with_files):
424 """Test that the signal exists."""
425 card = FolderCard(mock_context_card, test_folder_with_files)
426 qtbot.addWidget(card)
428 assert hasattr(card, 'open_folder_requested')
430 def test_open_folder_signal_emitted(self, qtbot, mock_context_card, test_folder_with_files):
431 """Test that the signal is emitted (indirect test)."""
432 card = FolderCard(mock_context_card, test_folder_with_files)
433 qtbot.addWidget(card)
435 # The signal is normally emitted from the dialog when clicking its button.
436 # Here we test indirectly by connecting and emitting ourselves.
437 signal_received = []
438 card.open_folder_requested.connect(lambda path: signal_received.append(path))
440 # Simulate emission
441 card.open_folder_requested.emit(test_folder_with_files)
443 assert len(signal_received) == 1
444 assert signal_received[0] == test_folder_with_files
447class TestEdgeCases:
448 """Tests for edge cases."""
450 def test_very_long_folder_name(self, qtbot, mock_context_card, temp_workspace):
451 """Test with a very long folder name."""
452 long_name = "a" * 200
453 long_folder = os.path.join(temp_workspace, long_name)
454 os.makedirs(long_folder)
456 card = FolderCard(mock_context_card, long_folder)
457 qtbot.addWidget(card)
459 assert card.folder_name.text() == long_name
461 def test_unicode_folder_name(self, qtbot, mock_context_card, temp_workspace):
462 """Test with unicode characters in folder name."""
463 unicode_name = "папка_文件夹_φάκελος"
464 unicode_folder = os.path.join(temp_workspace, unicode_name)
465 os.makedirs(unicode_folder)
467 card = FolderCard(mock_context_card, unicode_folder)
468 qtbot.addWidget(card)
470 assert unicode_name in card.folder_name.text()
472 def test_many_files(self, qtbot, mock_context_card, test_folder_with_files):
473 """Test handling many files."""
474 card = FolderCard(mock_context_card, test_folder_with_files)
475 qtbot.addWidget(card)
477 many_files = [f"file{i}.txt" for i in range(100)]
478 card.add_files(many_files)
480 assert len(card.files) == 100
482 def test_special_characters_in_filename(self, qtbot, mock_context_card, test_folder_with_files):
483 """Test filenames with special characters."""
484 special_files = [
485 "file with spaces.txt",
486 "file-with-dashes.txt",
487 "file_with_underscores.txt",
488 "file.multiple.dots.txt"
489 ]
491 card = FolderCard(mock_context_card, test_folder_with_files)
492 qtbot.addWidget(card)
494 card.add_files(special_files)
496 assert len(card.files) == 4
497 assert all(f in card.files for f in special_files)
499 def test_rapid_file_additions(self, qtbot, mock_context_card, test_folder_with_files):
500 """Test rapid file additions."""
501 card = FolderCard(mock_context_card, test_folder_with_files)
502 qtbot.addWidget(card)
504 for i in range(20):
505 card.add_files([f"file{i}.txt"])
507 assert len(card.files) == 20
509 def test_folder_deleted_after_init(self, qtbot, mock_context_card, temp_workspace):
510 """Test behavior when the folder is deleted after initialization."""
511 folder = os.path.join(temp_workspace, "to_delete")
512 os.makedirs(folder)
514 card = FolderCard(mock_context_card, folder)
515 qtbot.addWidget(card)
517 # Delete the folder
518 os.rmdir(folder)
520 # check_new_files should not crash
521 card.check_new_files()
524class TestIntegration:
525 """Integration tests."""
527 def test_full_workflow(self, qtbot, mock_context_card, test_folder_with_files):
528 """Test the full workflow."""
529 card = FolderCard(mock_context_card, test_folder_with_files)
530 qtbot.addWidget(card)
532 # Initial state
533 assert not card.action_btn.isEnabled()
534 assert len(card.files) == 0
536 # Add new file to filesystem
537 new_file = os.path.join(test_folder_with_files, "new.txt")
538 with open(new_file, "w") as f:
539 f.write("content")
541 # Check new files
542 card.check_new_files()
544 # State check
545 assert card.action_btn.isEnabled()
546 assert len(card.files) == 1
548 # Show dialog and reset
549 with patch.object(QDialog, 'exec'):
550 card.show_files_dialog()
552 # Reset check
553 assert not card.action_btn.isEnabled()
554 assert len(card.files) == 0
556 def test_multiple_check_cycles(self, qtbot, mock_context_card, test_folder_with_files):
557 """Test multiple check cycles."""
558 card = FolderCard(mock_context_card, test_folder_with_files)
559 qtbot.addWidget(card)
561 for cycle in range(3):
562 # Add file
563 new_file = os.path.join(test_folder_with_files, f"cycle{cycle}.txt")
564 with open(new_file, "w") as f:
565 f.write("content")
567 # Check
568 card.check_new_files()
570 # Show and reset
571 with patch.object(QDialog, 'exec'):
572 if card.files:
573 card.show_files_dialog()
575 # Final state should be reset
576 assert len(card.files) == 0
579class TestStateConsistency:
580 """Tests for state consistency."""
582 def test_state_after_add_files(self, qtbot, mock_context_card, test_folder_with_files):
583 """Test state consistency after add_files."""
584 card = FolderCard(mock_context_card, test_folder_with_files)
585 qtbot.addWidget(card)
587 card.add_files(["file1.txt"])
589 # State should be consistent
590 assert len(card.files) > 0
591 assert card.action_btn.isEnabled()
592 assert "✓" in card.folder_icon.text()
594 def test_state_after_reset(self, qtbot, mock_context_card, test_folder_with_files):
595 """Test state consistency after reset."""
596 card = FolderCard(mock_context_card, test_folder_with_files)
597 qtbot.addWidget(card)
599 card.add_files(["file1.txt"])
600 card.reset_state()
602 # Should be initial state
603 assert not card.action_btn.isEnabled()
604 assert "📁" in card.folder_icon.text()
607class TestMemoryAndPerformance:
608 """Tests for memory and performance."""
610 def test_no_memory_leak_repeated_operations(self, qtbot, mock_context_card, test_folder_with_files):
611 """Test no memory leaks after repeated operations."""
612 card = FolderCard(mock_context_card, test_folder_with_files)
613 qtbot.addWidget(card)
615 for _ in range(50):
616 card.add_files(["file.txt"])
617 card.reset_state()
619 # Final state should be clean
620 assert len(card.files) == 0
622 def test_performance_many_checks(self, qtbot, mock_context_card, test_folder_with_files):
623 """Test performance with many checks."""
624 card = FolderCard(mock_context_card, test_folder_with_files)
625 qtbot.addWidget(card)
627 # Many checks with no new files
628 for _ in range(100):
629 card.check_new_files()
631 # Should not be slow
632 assert True
635class TestAccessibility:
636 """Tests for accessibility."""
638 def test_button_has_text(self, qtbot, mock_context_card, test_folder_with_files):
639 """Test that the button has text."""
640 card = FolderCard(mock_context_card, test_folder_with_files)
641 qtbot.addWidget(card)
643 assert len(card.action_btn.text()) > 0
645 def test_status_label_has_text(self, qtbot, mock_context_card, test_folder_with_files):
646 """Test that the status label has text."""
647 card = FolderCard(mock_context_card, test_folder_with_files)
648 qtbot.addWidget(card)
650 assert len(card.status_label.text()) > 0