665 lines
21 KiB
Python
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
|