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

1import os 

2import pytest 

3from unittest.mock import Mock, patch, MagicMock 

4from PyQt6.QtWidgets import QDialog 

5from PyQt6.QtCore import Qt 

6 

7from main.components.folder_card import FolderCard 

8 

9 

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) 

15 

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

21 

22 return folder 

23 

24 

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 

31 

32 

33@pytest.fixture 

34def mock_context_card(): 

35 """Mock context for FolderCard.""" 

36 return {} 

37 

38 

39class TestFolderCardInitialization: 

40 """Tests for FolderCard initialization.""" 

41 

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) 

46 

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 

53 

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) 

58 

59 assert card.folder == empty_folder 

60 assert len(card.existing_files) == 0 

61 

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

65 

66 card = FolderCard(mock_context_card, nonexistent) 

67 qtbot.addWidget(card) 

68 

69 assert card.folder == nonexistent 

70 assert card.existing_files == set() 

71 

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) 

76 

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 

82 

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) 

87 

88 assert card.height() == 100 

89 

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) 

94 

95 assert card.folder_name.text() == os.path.basename(test_folder_with_files) 

96 

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) 

101 

102 assert "Waiting" in card.status_label.text() or "wait" in card.status_label.text().lower() 

103 assert not card.action_btn.isEnabled() 

104 

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) 

109 

110 assert "📁" in card.folder_icon.text() 

111 

112 

113class TestAddFiles: 

114 """Tests for the add_files method.""" 

115 

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) 

120 

121 new_files = ["new1.txt", "new2.csv"] 

122 card.add_files(new_files) 

123 

124 assert len(card.files) == 2 

125 assert "new1.txt" in card.files 

126 assert "new2.csv" in card.files 

127 

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) 

132 

133 card.add_files(["file1.txt"]) 

134 

135 assert "1" in card.status_label.text() 

136 

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) 

141 

142 assert not card.action_btn.isEnabled() 

143 

144 card.add_files(["file1.txt"]) 

145 

146 assert card.action_btn.isEnabled() 

147 

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) 

152 

153 initial_icon = card.folder_icon.text() 

154 

155 card.add_files(["file1.txt"]) 

156 

157 assert card.folder_icon.text() != initial_icon 

158 assert "✓" in card.folder_icon.text() 

159 

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) 

164 

165 card.add_files(["file1.txt"]) 

166 assert len(card.files) == 1 

167 

168 card.add_files(["file2.txt", "file3.txt"]) 

169 assert len(card.files) == 3 

170 

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) 

175 

176 card.add_files([]) 

177 

178 assert len(card.files) == 0 

179 

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) 

184 

185 with patch.object(card, 'start_pulse_animation') as mock_anim: 

186 card.add_files(["file1.txt"]) 

187 

188 mock_anim.assert_called_once() 

189 

190 

191class TestStartPulseAnimation: 

192 """Tests for the start_pulse_animation method.""" 

193 

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) 

198 

199 card.start_pulse_animation() 

200 

201 assert card.pulse_animation is not None 

202 

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) 

207 

208 # First animation 

209 card.start_pulse_animation() 

210 first_anim = card.pulse_animation 

211 

212 # Second animation 

213 card.start_pulse_animation() 

214 

215 assert card.pulse_animation is not None 

216 # Could be the same or a new one 

217 

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) 

222 

223 for _ in range(5): 

224 card.start_pulse_animation() 

225 

226 # Should not crash 

227 assert card.pulse_animation is not None 

228 

229 

230class TestResetState: 

231 """Tests for the reset_state method.""" 

232 

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) 

237 

238 # Add files to change state 

239 card.add_files(["file1.txt"]) 

240 

241 # Reset 

242 card.reset_state() 

243 

244 assert not card.action_btn.isEnabled() 

245 assert "📁" in card.folder_icon.text() 

246 

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) 

251 

252 card.start_pulse_animation() 

253 assert card.pulse_animation is not None 

254 

255 card.reset_state() 

256 

257 # Animation should be stopped (verifiable through state) 

258 

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) 

263 

264 card.add_files(["file1.txt"]) 

265 

266 card.reset_state() 

267 

268 assert "Waiting" in card.status_label.text() or "wait" in card.status_label.text().lower() 

269 

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) 

274 

275 # Reset without modifications 

276 card.reset_state() 

277 

278 # Should not crash 

279 assert not card.action_btn.isEnabled() 

280 

281 

282class TestCheckNewFiles: 

283 """Tests for the check_new_files method.""" 

284 

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) 

289 

290 initial_count = len(card.existing_files) 

291 

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

296 

297 card.check_new_files() 

298 

299 assert len(card.files) == 1 

300 assert "brand_new.txt" in card.files 

301 assert len(card.existing_files) == initial_count + 1 

302 

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) 

307 

308 card.check_new_files() 

309 

310 assert len(card.files) == 0 

311 

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) 

316 

317 initial_existing = card.existing_files.copy() 

318 

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

323 

324 card.check_new_files() 

325 

326 assert len(card.existing_files) > len(initial_existing) 

327 assert "new.txt" in card.existing_files 

328 

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) 

334 

335 # Should not crash 

336 card.check_new_files() 

337 

338 assert len(card.files) == 0 

339 

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) 

344 

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

350 

351 card.check_new_files() 

352 

353 assert len(card.files) == 5 

354 

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) 

359 

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

364 

365 with patch.object(card, 'add_files', wraps=card.add_files) as mock_add: 

366 card.check_new_files() 

367 

368 mock_add.assert_called_once() 

369 

370class TestShowFilesDialog: 

371 """Tests for the show_files_dialog method.""" 

372 

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) 

377 

378 card.add_files(["file1.txt", "file2.csv"]) 

379 

380 with patch.object(QDialog, 'exec') as mock_exec: 

381 card.show_files_dialog() 

382 

383 mock_exec.assert_called_once() 

384 

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) 

389 

390 with patch.object(QDialog, 'exec') as mock_exec: 

391 card.show_files_dialog() 

392 

393 mock_exec.assert_not_called() 

394 

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) 

399 

400 card.add_files(["file1.txt"]) 

401 

402 with patch.object(QDialog, 'exec'): 

403 card.show_files_dialog() 

404 

405 assert len(card.files) == 0 

406 

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) 

411 

412 card.add_files(["file1.txt"]) 

413 

414 with patch.object(QDialog, 'exec'): 

415 card.show_files_dialog() 

416 

417 assert not card.action_btn.isEnabled() 

418 

419 

420class TestOpenFolderSignal: 

421 """Tests for the open_folder_requested signal.""" 

422 

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) 

427 

428 assert hasattr(card, 'open_folder_requested') 

429 

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) 

434 

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

439 

440 # Simulate emission 

441 card.open_folder_requested.emit(test_folder_with_files) 

442 

443 assert len(signal_received) == 1 

444 assert signal_received[0] == test_folder_with_files 

445 

446 

447class TestEdgeCases: 

448 """Tests for edge cases.""" 

449 

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) 

455 

456 card = FolderCard(mock_context_card, long_folder) 

457 qtbot.addWidget(card) 

458 

459 assert card.folder_name.text() == long_name 

460 

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) 

466 

467 card = FolderCard(mock_context_card, unicode_folder) 

468 qtbot.addWidget(card) 

469 

470 assert unicode_name in card.folder_name.text() 

471 

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) 

476 

477 many_files = [f"file{i}.txt" for i in range(100)] 

478 card.add_files(many_files) 

479 

480 assert len(card.files) == 100 

481 

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 ] 

490 

491 card = FolderCard(mock_context_card, test_folder_with_files) 

492 qtbot.addWidget(card) 

493 

494 card.add_files(special_files) 

495 

496 assert len(card.files) == 4 

497 assert all(f in card.files for f in special_files) 

498 

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) 

503 

504 for i in range(20): 

505 card.add_files([f"file{i}.txt"]) 

506 

507 assert len(card.files) == 20 

508 

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) 

513 

514 card = FolderCard(mock_context_card, folder) 

515 qtbot.addWidget(card) 

516 

517 # Delete the folder 

518 os.rmdir(folder) 

519 

520 # check_new_files should not crash 

521 card.check_new_files() 

522 

523 

524class TestIntegration: 

525 """Integration tests.""" 

526 

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) 

531 

532 # Initial state 

533 assert not card.action_btn.isEnabled() 

534 assert len(card.files) == 0 

535 

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

540 

541 # Check new files 

542 card.check_new_files() 

543 

544 # State check 

545 assert card.action_btn.isEnabled() 

546 assert len(card.files) == 1 

547 

548 # Show dialog and reset 

549 with patch.object(QDialog, 'exec'): 

550 card.show_files_dialog() 

551 

552 # Reset check 

553 assert not card.action_btn.isEnabled() 

554 assert len(card.files) == 0 

555 

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) 

560 

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

566 

567 # Check 

568 card.check_new_files() 

569 

570 # Show and reset 

571 with patch.object(QDialog, 'exec'): 

572 if card.files: 

573 card.show_files_dialog() 

574 

575 # Final state should be reset 

576 assert len(card.files) == 0 

577 

578 

579class TestStateConsistency: 

580 """Tests for state consistency.""" 

581 

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) 

586 

587 card.add_files(["file1.txt"]) 

588 

589 # State should be consistent 

590 assert len(card.files) > 0 

591 assert card.action_btn.isEnabled() 

592 assert "✓" in card.folder_icon.text() 

593 

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) 

598 

599 card.add_files(["file1.txt"]) 

600 card.reset_state() 

601 

602 # Should be initial state 

603 assert not card.action_btn.isEnabled() 

604 assert "📁" in card.folder_icon.text() 

605 

606 

607class TestMemoryAndPerformance: 

608 """Tests for memory and performance.""" 

609 

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) 

614 

615 for _ in range(50): 

616 card.add_files(["file.txt"]) 

617 card.reset_state() 

618 

619 # Final state should be clean 

620 assert len(card.files) == 0 

621 

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) 

626 

627 # Many checks with no new files 

628 for _ in range(100): 

629 card.check_new_files() 

630 

631 # Should not be slow 

632 assert True 

633 

634 

635class TestAccessibility: 

636 """Tests for accessibility.""" 

637 

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) 

642 

643 assert len(card.action_btn.text()) > 0 

644 

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) 

649 

650 assert len(card.status_label.text()) > 0