""" Integration tests for TUI screens using Textual's testing framework Tests are structured to use Textual's App.run_test() which provides: - A running App instance with proper screen stack - Async test context - Headless mode for non-interactive testing - Pilot object for simulating user interactions """ from unittest.mock import patch import pytest class TestSearchScreen: """Integration tests for SearchScreen using Textual's testing framework""" @pytest.fixture def app(self): """Create a test app with mocked YouTube service""" from youtube_tui.app import YouTubeTUI # Create app without actually running it with patch.object(YouTubeTUI, "_check_yt_dlp"): with patch.object(YouTubeTUI, "load_search_history"): app = YouTubeTUI() app._testing = True yield app def test_search_screen_compose(self, app): """Test search screen composition""" from youtube_tui.screens.search import SearchScreen screen = SearchScreen() # Compose should yield widgets widgets = list(screen.compose()) assert len(widgets) > 0 assert screen is not None async def test_search_action_with_empty_input(self, app): """Test search action with empty input""" # Use app.run_test() to ensure proper screen mounting async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Mock the update_status method to track calls update_calls = [] screen = app.screen screen.update_status = lambda message: update_calls.append(message) # Simulate pressing Enter with empty input await pilot.press("enter") await pilot.pause() # Verify that update_status was called (error message) assert len(update_calls) > 0 async def test_search_action_with_valid_input(self, app): """Test search action with valid input""" search_term = "python tutorial" async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Type the search term await pilot.press(*search_term) await pilot.pause() # Press Enter to search await pilot.press("enter") await pilot.pause() # Get the screen and verify search term was set screen = app.screen assert screen.search_term == search_term async def test_search_action_from_anywhere(self, app): """Test search from anywhere action""" search_term = "music" async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Type and press enter await pilot.press(*search_term) await pilot.pause() await pilot.press("enter") await pilot.pause() # Get the screen and verify search term was set screen = app.screen assert screen.search_term == search_term async def test_cancel_action(self, app): """Test cancel action""" async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Press Escape to trigger cancel await pilot.press("escape") await pilot.pause() async def test_go_back_action(self, app): """Test go back action""" async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Press Escape to trigger go_back await pilot.press("escape") await pilot.pause() async def test_quit_action(self, app): """Test quit action""" async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Press q to quit await pilot.press("q") await pilot.pause() async def test_button_pressed_search(self, app): """Test button press for search""" async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Press Enter to trigger search await pilot.press("enter") await pilot.pause() async def test_button_pressed_cancel(self, app): """Test button press for cancel""" async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Press Escape to cancel await pilot.press("escape") await pilot.pause() async def test_input_submitted(self, app): """Test input submitted event""" async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Type something and press Enter await pilot.press("t", "e", "s", "t") await pilot.pause() await pilot.press("enter") await pilot.pause() class TestResultsScreen: """Integration tests for ResultsScreen""" @pytest.fixture def app(self): """Create a test app with mocked YouTube service""" from youtube_tui.app import YouTubeTUI with patch.object(YouTubeTUI, "_check_yt_dlp"): with patch.object(YouTubeTUI, "load_search_history"): app = YouTubeTUI() app._testing = True yield app def test_results_screen_compose(self, app): """Test results screen composition""" from youtube_tui.screens.results import ResultsScreen screen = ResultsScreen("test query") # Compose should yield widgets widgets = list(screen.compose()) assert len(widgets) > 0 assert screen is not None async def test_load_results_success(self, app, mock_search_results): """Test loading results successfully""" from youtube_tui.screens.results import ResultsScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Get the current screen (should be SearchScreen) # We need to push a ResultsScreen screen = ResultsScreen("test query") app.push_screen(screen) # Mock the YouTube service to return results with patch.object( screen.youtube_service, "search_videos", return_value=mock_search_results, ): # Wait for the screen to load results await pilot.pause() async def test_load_results_error(self, app): """Test loading results with error""" from youtube_tui.screens.results import ResultsScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a ResultsScreen screen = ResultsScreen("test query") app.push_screen(screen) # Mock the YouTube service to raise an error with patch.object( screen.youtube_service, "search_videos", side_effect=Exception("Network error"), ): await pilot.pause() async def test_update_table(self, app, mock_search_results): """Test updating the results table""" from youtube_tui.screens.results import ResultsScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a ResultsScreen screen = ResultsScreen("test query") app.push_screen(screen) # Set videos directly screen.videos = mock_search_results[:5] # Wait for the screen to be fully mounted await pilot.pause() # Now we can update the table screen.update_table() await pilot.pause() async def test_update_pagination(self, app, mock_search_results): """Test pagination update""" from youtube_tui.screens.results import ResultsScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a ResultsScreen screen = ResultsScreen("test query") app.push_screen(screen) # Set pagination values screen.videos = mock_search_results[:10] screen.page = 1 screen.total_pages = 2 # Wait for the screen to be fully mounted await pilot.pause() # Now we can update pagination screen.update_pagination() await pilot.pause() async def test_download_action_no_selection(self, app, mock_search_results): """Test download with no selection""" from youtube_tui.screens.results import ResultsScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a ResultsScreen screen = ResultsScreen("test query") app.push_screen(screen) screen.videos = mock_search_results # Wait for the screen to be fully mounted await pilot.pause() # Try to download without selecting a row await pilot.press("enter") await pilot.pause() async def test_download_action_with_selection(self, app, mock_video): """Test download with video selection""" from youtube_tui.screens.results import ResultsScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a ResultsScreen screen = ResultsScreen("test query") app.push_screen(screen) screen.videos = [mock_video] # Wait for the screen to be fully mounted await pilot.pause() # Select a row first await pilot.press("down") await pilot.pause() # Then press enter to download await pilot.press("enter") await pilot.pause() async def test_next_page_action(self, app, mock_search_results): """Test next page navigation""" from youtube_tui.screens.results import ResultsScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a ResultsScreen screen = ResultsScreen("test query") screen.page = 1 screen.total_pages = 2 app.push_screen(screen) # Mock the service to return different results for each page def mock_search(search_term, page=1, per_page=15): if page == 1: return mock_search_results[:15] else: return [] with patch.object( screen.youtube_service, "search_videos", side_effect=mock_search ): # Wait for the screen to be fully mounted await pilot.pause() # Press 'n' for next page await pilot.press("n") await pilot.pause() # Page should have incremented assert screen.page == 2 async def test_previous_page_action(self, app): """Test previous page navigation""" from youtube_tui.screens.results import ResultsScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a ResultsScreen screen = ResultsScreen("test query") screen.page = 2 screen.total_pages = 2 app.push_screen(screen) # Wait for the screen to be fully mounted await pilot.pause() # Press 'p' for previous page await pilot.press("p") await pilot.pause() # Page should have decremented assert screen.page == 1 async def test_button_pressed_previous(self, app, mock_search_results): """Test previous button press""" from youtube_tui.screens.results import ResultsScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a ResultsScreen screen = ResultsScreen("test query") screen.page = 2 screen.total_pages = 2 app.push_screen(screen) # Wait for the screen to be fully mounted await pilot.pause() # Press 'p' key to go to previous page await pilot.press("p") await pilot.pause() async def test_button_pressed_next(self, app, mock_search_results): """Test next button press""" from youtube_tui.screens.results import ResultsScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a ResultsScreen screen = ResultsScreen("test query") screen.page = 1 screen.total_pages = 2 app.push_screen(screen) # Wait for the screen to be fully mounted await pilot.pause() # Press 'n' key to go to next page await pilot.press("n") await pilot.pause() async def test_data_table_row_selected(self, app, mock_video): """Test data table row selection""" from youtube_tui.screens.results import ResultsScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a ResultsScreen screen = ResultsScreen("test query") screen.videos = [mock_video] app.push_screen(screen) # Wait for the screen to be fully mounted await pilot.pause() # Select a row await pilot.press("down") await pilot.pause() # Press enter to trigger row selection await pilot.press("enter") await pilot.pause() class TestDownloadScreen: """Integration tests for DownloadScreen""" @pytest.fixture def app(self): """Create a test app with mocked YouTube service""" from youtube_tui.app import YouTubeTUI with patch.object(YouTubeTUI, "_check_yt_dlp"): with patch.object(YouTubeTUI, "load_search_history"): app = YouTubeTUI() app._testing = True yield app def test_download_screen_compose(self, app, mock_video): """Test download screen composition""" from youtube_tui.screens.download import DownloadScreen screen = DownloadScreen(mock_video) # Compose should yield widgets widgets = list(screen.compose()) assert len(widgets) > 0 assert screen is not None async def test_start_download_success(self, app, mock_video): """Test successful download""" from youtube_tui.screens.download import DownloadScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a DownloadScreen screen = DownloadScreen(mock_video) app.push_screen(screen) # Mock successful download async def mock_download(video, category): return True with patch.object( screen.youtube_service, "download_video", side_effect=mock_download, ): await pilot.pause() async def test_start_download_failure(self, app, mock_video): """Test download failure""" from youtube_tui.screens.download import DownloadScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a DownloadScreen screen = DownloadScreen(mock_video) app.push_screen(screen) # Mock failed download async def mock_download(video, category): return False with patch.object( screen.youtube_service, "download_video", side_effect=mock_download, ): await pilot.pause() async def test_start_download_exception(self, app, mock_video): """Test download with exception""" from youtube_tui.screens.download import DownloadScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a DownloadScreen screen = DownloadScreen(mock_video) app.push_screen(screen) # Mock exception during download async def mock_download(video, category): raise Exception("Download failed") with patch.object( screen.youtube_service, "download_video", side_effect=mock_download, ): await pilot.pause() async def test_cancel_download(self, app, mock_video): """Test download cancellation""" from youtube_tui.screens.download import DownloadScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a DownloadScreen screen = DownloadScreen(mock_video) app.push_screen(screen) # Wait for the screen to be fully mounted await pilot.pause() # Press escape to cancel await pilot.press("escape") await pilot.pause() async def test_refresh_screen(self, app, mock_video): """Test refresh screen action""" from youtube_tui.screens.download import DownloadScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a DownloadScreen screen = DownloadScreen(mock_video) app.push_screen(screen) # Wait for the screen to be fully mounted await pilot.pause() # Press ctrl+r to refresh await pilot.press("ctrl+r") await pilot.pause() async def test_unload_success(self, app, mock_video): """Test screen unload with success""" from youtube_tui.screens.download import DownloadScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a DownloadScreen screen = DownloadScreen(mock_video) screen.download_complete = True app.push_screen(screen) await pilot.pause() async def test_unload_error(self, app, mock_video): """Test screen unload with error""" from youtube_tui.screens.download import DownloadScreen async with app.run_test() as pilot: # Wait for the screen to be fully mounted await pilot.pause() # Push a DownloadScreen screen = DownloadScreen(mock_video) screen.download_error = True app.push_screen(screen) await pilot.pause() # Fixtures for test data @pytest.fixture def mock_search_results(): """Mock search results""" from youtube_tui.models.video import Video videos = [ Video( video_id=f"video{i}", title=f"Video {i}", channel=f"Channel {i}", channel_id=f"channel{i}", duration=f"{i}:00", view_count=str(i * 1000), upload_date="20240101", description=f"Description {i}", is_short=(i % 3 == 0), url=f"https://www.youtube.com/watch?v=video{i}", ) for i in range(1, 16) ] return videos @pytest.fixture def mock_video(): """Mock Video object""" from youtube_tui.models.video import Video video = Video( video_id="test123", title="Test Video", channel="Test Channel", channel_id="channel123", duration="5:00", view_count="1000", upload_date="20240101", description="Test description", thumbnail_url="https://example.com/thumb.jpg", url="https://www.youtube.com/watch?v=test123", is_short=False, ) return video