youtube-tui/tests/integration/test_screens.py

665 lines
21 KiB
Python

"""
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"""
from youtube_tui.screens.search import SearchScreen
screen = SearchScreen()
# Use app.run_test() to ensure proper screen mounting
async with app.run_test() as pilot:
app.push_screen(screen)
# Wait for the screen to be fully mounted
await pilot.pause()
# Mock the update_status method to track calls
update_calls = []
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"
from youtube_tui.screens.search import SearchScreen
screen = SearchScreen()
async with app.run_test() as pilot:
app.push_screen(screen)
# 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"
from youtube_tui.screens.search import SearchScreen
screen = SearchScreen()
async with app.run_test() as pilot:
app.push_screen(screen)
# 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