test: add TUI test suite (moved from youtube-cli)

This commit is contained in:
Jarian Cottingham 2026-08-21 17:11:12 +00:00
parent 6884c1f0ef
commit 96247567d9
6 changed files with 2013 additions and 0 deletions

235
tests/conftest.py Normal file
View File

@ -0,0 +1,235 @@
"""
Test fixtures and configuration for YouTube TUI tests
"""
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# Test fixtures
@pytest.fixture
def sample_video_data():
"""Sample video data for testing"""
return {
"video_id": "dQw4w9WgXcQ",
"title": "Rick Astley - Never Gonna Give You Up",
"channel": "RickAstleyVEVO",
"channel_id": "UCuZqHn2U8f4o7bVlY8v8w",
"duration": "3:33",
"view_count": "1000000",
"upload_date": "20091025",
"description": "The official video for Rick Astley's hit song.",
"thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"is_short": False,
}
@pytest.fixture
def sample_video_object(sample_video_data):
"""Sample Video object for testing"""
from youtube_tui.models.video import Video
return Video(**sample_video_data)
@pytest.fixture
def mock_yt_dlp_result():
"""Mock yt-dlp search result"""
return {
"id": "abc123",
"title": "Sample Video",
"author": "Sample Channel",
"channel": "Sample Channel",
"channel_id": "channel123",
"length": "10:30",
"view_count": 150000,
"upload_date": "20240115",
"description": "A sample video description",
"thumbnail": "https://i.ytimg.com/vi/abc123/hqdefault.jpg",
"url": "https://www.youtube.com/watch?v=abc123",
}
@pytest.fixture
def mock_youtube_service():
"""Mock YouTubeService for testing"""
from youtube_cli.main import YouTubeCLI
from youtube_tui.services.youtube import YouTubeService
# Create a real service with mocked cli
with patch.object(YouTubeCLI, "__init__", return_value=None):
service = YouTubeService.__new__(YouTubeService)
service.cli = MagicMock()
service.console = MagicMock()
# Mock async methods
service.search_videos = AsyncMock()
service.download_video = AsyncMock()
service.download_playlist = AsyncMock()
service.get_categories = AsyncMock()
service.is_video_downloaded = AsyncMock()
service.add_to_archive = AsyncMock()
service.get_archive = AsyncMock()
service.get_downloaded_video_ids = AsyncMock()
service.remove_from_archive = AsyncMock()
service._create_video_from_result = MagicMock()
return service
@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
@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 app_config(tmp_path):
"""Temporary app configuration"""
config = {
"download_dir": str(tmp_path / "downloads"),
"default_locations": [
str(tmp_path / "downloads"),
str(tmp_path / "movies"),
],
"max_videos_per_page": 15,
"yt_dlp_args": {
"format": "bestvideo[height=1080]+bestaudio",
},
"network_share_path": str(tmp_path / "network"),
"default_network_subfolder": "General",
}
return config
@pytest.fixture
def mock_app():
"""Mock Textual App for testing screens"""
from textual.app import App
mock_app = MagicMock(spec=App)
mock_app.screen_stack = [None, None, None] # Simulate screen stack
mock_app.notify = MagicMock()
mock_app.exit = MagicMock()
# Mock push_screen and pop_screen
mock_app.push_screen = MagicMock()
mock_app.push_results_screen = MagicMock()
mock_app.pop_screen = MagicMock()
mock_app.action_open_search = MagicMock()
mock_app.run_background = MagicMock()
return mock_app
# Async test utilities
@pytest.fixture
def event_loop():
"""Create an event loop for async tests"""
loop = asyncio.new_event_loop()
yield loop
loop.close()
@pytest.fixture
async def async_mock_search_results():
"""Async 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
# Mock patches
@pytest.fixture
def mock_yt_dlp():
"""Patch yt-dlp for testing"""
with patch("youtube_tui.services.youtube.subprocess") as mock_subprocess:
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "2024.01.01"
mock_subprocess.run.return_value = mock_result
yield mock_subprocess
@pytest.fixture
def mock_archive():
"""Mock archive data"""
return {
"video123": {
"url": "https://www.youtube.com/watch?v=video123",
"id": "video123",
"title": "Downloaded Video",
}
}
@pytest.fixture
def mock_archive_file(tmp_path, mock_archive):
"""Create a mock archive file"""
archive_path = tmp_path / "archive.json"
import json
with open(archive_path, "w") as f:
json.dump(mock_archive, f)
return archive_path

View File

@ -0,0 +1,324 @@
#!/usr/bin/env python3
"""
Integration test for the download queue system
"""
import tempfile
from pathlib import Path
from unittest.mock import patch
from youtube_tui.models.queue_item import QueueStatus
from youtube_tui.models.video import Video
from youtube_tui.services.queue import DownloadQueue
class TestDownloadQueue:
"""Tests for the DownloadQueue service"""
def test_queue_initialization(self):
"""Test queue initialization"""
with tempfile.TemporaryDirectory() as tmpdir:
# Create a queue without loading from file
queue = DownloadQueue.__new__(DownloadQueue)
queue._queue = []
queue._archive_file = Path(tmpdir) / "download_queue.json"
assert queue.get_stats()["total"] == 0
def test_add_video_to_queue(self):
"""Test adding a video to the queue"""
with tempfile.TemporaryDirectory() as tmpdir:
with patch.object(
DownloadQueue,
"_archive_file",
Path(tmpdir) / "download_queue.json",
):
queue = DownloadQueue()
# Create a test video
video = Video(
video_id="test123",
title="Test Video",
channel="Test Channel",
channel_id="channel123",
duration="10:00",
view_count="1000",
upload_date="20240101",
description="Test description",
)
# Add to queue
item = queue.add_video(video, category="Tech")
assert item.status == QueueStatus.PENDING
assert item.category == "Tech"
assert item.video.video_id == "test123"
# Check stats
stats = queue.get_stats()
assert stats["total"] == 1
assert stats["pending"] == 1
def test_remove_video_from_queue(self):
"""Test removing a video from the queue"""
with tempfile.TemporaryDirectory() as tmpdir:
with patch.object(
DownloadQueue,
"_archive_file",
Path(tmpdir) / "download_queue.json",
):
queue = DownloadQueue()
# Create a test video
video = Video(
video_id="test123",
title="Test Video",
channel="Test Channel",
channel_id="channel123",
duration="10:00",
view_count="1000",
upload_date="20240101",
description="Test description",
)
# Add to queue
queue.add_video(video)
# Remove from queue
removed = queue.remove_video("test123")
assert removed is True
# Check stats
stats = queue.get_stats()
assert stats["total"] == 0
def test_update_status_and_progress(self):
"""Test updating status and progress"""
with tempfile.TemporaryDirectory() as tmpdir:
with patch.object(
DownloadQueue,
"_archive_file",
Path(tmpdir) / "download_queue.json",
):
queue = DownloadQueue()
# Create a test video
video = Video(
video_id="test123",
title="Test Video",
channel="Test Channel",
channel_id="channel123",
duration="10:00",
view_count="1000",
upload_date="20240101",
description="Test description",
)
# Add to queue
queue.add_video(video)
# Update status to downloading
queue.update_status(
"test123", QueueStatus.DOWNLOADING, progress=50
)
# Get the item and check status
items = queue.get_queue()
assert len(items) == 1
assert items[0].status == QueueStatus.DOWNLOADING
assert items[0].progress == 50
# Update status to completed
queue.update_status(
"test123", QueueStatus.COMPLETED, progress=100
)
items = queue.get_queue()
assert items[0].status == QueueStatus.COMPLETED
assert items[0].progress == 100
def test_cancel_video(self):
"""Test cancelling a video in the queue"""
with tempfile.TemporaryDirectory() as tmpdir:
with patch.object(
DownloadQueue,
"_archive_file",
Path(tmpdir) / "download_queue.json",
):
queue = DownloadQueue()
# Create a test video
video = Video(
video_id="test123",
title="Test Video",
channel="Test Channel",
channel_id="channel123",
duration="10:00",
view_count="1000",
upload_date="20240101",
description="Test description",
)
# Add to queue
queue.add_video(video)
# Cancel the video
cancelled = queue.cancel_video("test123")
assert cancelled is True
# Check status
items = queue.get_queue()
assert items[0].status == QueueStatus.CANCELLED
def test_get_next_pending(self):
"""Test getting the next pending item"""
with tempfile.TemporaryDirectory() as tmpdir:
with patch.object(
DownloadQueue,
"_archive_file",
Path(tmpdir) / "download_queue.json",
):
queue = DownloadQueue()
# Create test videos
video1 = Video(
video_id="test1",
title="Test Video 1",
channel="Test Channel",
channel_id="channel1",
duration="10:00",
view_count="1000",
upload_date="20240101",
description="Test",
)
video2 = Video(
video_id="test2",
title="Test Video 2",
channel="Test Channel",
channel_id="channel2",
duration="10:00",
view_count="1000",
upload_date="20240101",
description="Test",
)
# Add to queue
queue.add_video(video1)
queue.add_video(video2)
# Get next pending
next_item = queue.get_next_pending()
assert next_item is not None
assert next_item.video.video_id == "test1"
# Update first item to downloading
queue.update_status("test1", QueueStatus.DOWNLOADING)
# Get next pending - should be test2
next_item = queue.get_next_pending()
assert next_item.video.video_id == "test2"
# Update test2 to downloading
queue.update_status("test2", QueueStatus.DOWNLOADING)
# No more pending items
next_item = queue.get_next_pending()
assert next_item is None
def test_clear_completed(self):
"""Test clearing completed and cancelled items"""
with tempfile.TemporaryDirectory() as tmpdir:
with patch.object(
DownloadQueue,
"_archive_file",
Path(tmpdir) / "download_queue.json",
):
queue = DownloadQueue()
# Create test videos
video1 = Video(
video_id="test1",
title="Test Video 1",
channel="Test Channel",
channel_id="channel1",
duration="10:00",
view_count="1000",
upload_date="20240101",
description="Test",
)
video2 = Video(
video_id="test2",
title="Test Video 2",
channel="Test Channel",
channel_id="channel2",
duration="10:00",
view_count="1000",
upload_date="20240101",
description="Test",
)
video3 = Video(
video_id="test3",
title="Test Video 3",
channel="Test Channel",
channel_id="channel3",
duration="10:00",
view_count="1000",
upload_date="20240101",
description="Test",
)
# Add to queue
queue.add_video(video1) # pending
queue.add_video(video2) # pending
# Mark test1 as completed
queue.update_status("test1", QueueStatus.COMPLETED)
# Mark test2 as cancelled
queue.cancel_video("test2")
# Mark test3 as completed
queue.add_video(video3)
queue.update_status("test3", QueueStatus.COMPLETED)
# Clear completed and cancelled
removed = queue.clear_completed()
assert removed == 3 # All three should be removed
# Check stats
stats = queue.get_stats()
assert stats["total"] == 0
def test_clear_failed(self):
"""Test clearing failed items"""
with tempfile.TemporaryDirectory() as tmpdir:
with patch.object(
DownloadQueue,
"_archive_file",
Path(tmpdir) / "download_queue.json",
):
queue = DownloadQueue()
# Create test videos
video1 = Video(
video_id="test1",
title="Test Video 1",
channel="Test Channel",
channel_id="channel1",
duration="10:00",
view_count="1000",
upload_date="20240101",
description="Test",
)
# Add to queue and fail it
queue.add_video(video1)
queue.update_status("test1", QueueStatus.FAILED)
# Clear failed
removed = queue.clear_failed()
assert removed == 1
# Check stats
stats = queue.get_stats()
assert stats["total"] == 0
assert stats["failed"] == 0

View File

@ -0,0 +1,664 @@
"""
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

219
tests/unit/test_models.py Normal file
View File

@ -0,0 +1,219 @@
"""
Unit tests for Video model
"""
from youtube_tui.models.video import Video
class TestVideoModel:
"""Tests for the Video dataclass"""
def test_video_creation(self, sample_video_data):
"""Test creating a Video object from data"""
video = Video(**sample_video_data)
assert video.video_id == "dQw4w9WgXcQ"
assert video.title == "Rick Astley - Never Gonna Give You Up"
assert video.channel == "RickAstleyVEVO"
assert video.duration == "3:33"
assert video.view_count == "1000000"
assert video.upload_date == "20091025"
assert video.is_short is False
def test_video_url_generation(self, sample_video_data):
"""Test URL is generated from video_id"""
video = Video(**sample_video_data)
assert video.url == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
def test_video_short_detection(self, sample_video_data):
"""Test short video detection"""
# Test normal video
video = Video(**sample_video_data)
assert video.is_short is False
# Test short video via URL
short_data = sample_video_data.copy()
short_data["url"] = "https://www.youtube.com/shorts/dQw4w9WgXcQ"
short_data["is_short"] = False # Reset to test detection
video = Video(**short_data)
assert video.is_short is True
def test_video_short_detection_duration(self, sample_video_data):
"""Test short video detection via duration"""
short_data = sample_video_data.copy()
short_data["duration"] = "0:00" # Shorts have 0:00 duration
video = Video(**short_data)
assert video.is_short is True
def test_display_title_with_short(self, sample_video_data):
"""Test display title for short videos"""
short_data = sample_video_data.copy()
short_data["is_short"] = True
video = Video(**short_data)
assert (
video.display_title
== "(short) Rick Astley - Never Gonna Give You Up"
)
def test_display_title_without_short(self, sample_video_data):
"""Test display title for normal videos"""
video = Video(**sample_video_data)
assert video.display_title == "Rick Astley - Never Gonna Give You Up"
def test_display_duration_short(self, sample_video_data):
"""Test display duration for short videos"""
short_data = sample_video_data.copy()
short_data["is_short"] = True
video = Video(**short_data)
assert video.display_duration == "Short"
def test_display_duration_normal(self, sample_video_data):
"""Test display duration for normal videos"""
video = Video(**sample_video_data)
assert video.display_duration == "3:33"
def test_to_dict(self, sample_video_data):
"""Test Video to_dict conversion"""
video = Video(**sample_video_data)
result = video.to_dict()
assert result["video_id"] == "dQw4w9WgXcQ"
assert result["title"] == "Rick Astley - Never Gonna Give You Up"
assert result["is_short"] is False
assert result["url"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
def test_to_dict_default_thumbnail(self, sample_video_data):
"""Test to_dict with None thumbnail"""
video = Video(**sample_video_data)
video.thumbnail_url = None
result = video.to_dict()
assert result["thumbnail_url"] is None
def test_from_dict(self, sample_video_data):
"""Test Video from_dict creation"""
video_dict = {
"video_id": "abc123",
"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=abc123",
}
video = Video.from_dict(video_dict)
assert video.video_id == "abc123"
assert video.title == "Test Video"
assert video.channel == "Test Channel"
assert video.description == "Test description"
def test_from_dict_optional_fields(self, sample_video_data):
"""Test from_dict with optional fields"""
video_dict = {
"video_id": "abc123",
"title": "Test Video",
"channel": "Test Channel",
"channel_id": "channel123",
"duration": "5:00",
"view_count": "1000",
"upload_date": "20240101",
# description is optional
}
video = Video.from_dict(video_dict)
assert video.description == ""
assert video.thumbnail_url is None
def test_video_equality(self, sample_video_data):
"""Test Video equality comparison"""
video1 = Video(**sample_video_data)
video2 = Video(**sample_video_data)
video3 = Video(**{**sample_video_data, "title": "Different Title"})
# Dataclass should have automatic equality
assert video1 == video2
assert video1 != video3
def test_video_repr(self, sample_video_data):
"""Test Video string representation"""
video = Video(**sample_video_data)
repr_str = repr(video)
assert "Video" in repr_str
assert "dQw4w9WgXcQ" in repr_str
def test_video_hash(self, sample_video_data):
"""Test Video hash (for set usage)"""
video = Video(**sample_video_data)
# Dataclass should be hashable (unless frozen=True, then not)
try:
video_hash = hash(video)
assert isinstance(video_hash, int)
except TypeError:
# If not hashable, that's also acceptable for mutable dataclasses
pass
def test_video_with_custom_url(self, sample_video_data):
"""Test Video with custom URL"""
custom_url = "https://youtu.be/dQw4w9WgXcQ"
video_data = sample_video_data.copy()
video_data["url"] = custom_url
video = Video(**video_data)
assert video.url == custom_url
# is_short should be detected from URL
assert video.is_short is False # youtu.be doesn't have /shorts/
def test_video_short_youtu_be(self, sample_video_data):
"""Test short detection with youtu.be URL"""
short_url = "https://youtu.be/dQw4w9WgXcQ?t=0"
video_data = sample_video_data.copy()
video_data["url"] = short_url
video_data["is_short"] = False # Reset to test detection
# Note: Our detection only checks for /shorts/ in URL, not youtu.be shorts
video = Video(**video_data)
# This should be False since we don't detect youtu.be shorts URLs
assert video.is_short is False
def test_video_empty_description(self, sample_video_data):
"""Test Video with empty description"""
video_data = sample_video_data.copy()
video_data["description"] = ""
video = Video(**video_data)
assert video.description == ""
def test_video_none_values(self, sample_video_data):
"""Test Video with None values"""
video_data = {
"video_id": "test123",
"title": "Test",
"channel": "Channel",
"channel_id": "channel123",
"duration": "1:00",
"view_count": "0",
"upload_date": "20240101",
"description": "",
"thumbnail_url": None,
}
video = Video(**video_data)
assert video.thumbnail_url is None
assert video.description == ""
assert video.url == "https://www.youtube.com/watch?v=test123"

340
tests/unit/test_service.py Normal file
View File

@ -0,0 +1,340 @@
"""
Unit tests for YouTubeService
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
class TestYouTubeService:
"""Tests for YouTubeService class"""
@pytest.fixture
def service(self):
"""Create YouTubeService instance"""
from unittest.mock import MagicMock
from youtube_tui.services.youtube import YouTubeService
with patch("youtube_cli.main.YouTubeCLI"):
service = YouTubeService.__new__(YouTubeService)
service.cli = MagicMock()
# Set up format_duration to use the real implementation
def real_format_duration(seconds):
if not seconds:
return "0:00"
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
if hours > 0:
return f"{hours}:{minutes:02d}:{secs:02d}"
else:
return f"{minutes}:{secs:02d}"
service.cli.format_duration = real_format_duration
service.console = MagicMock()
return service
@pytest.fixture
def mock_yt_dlp_result(self):
"""Mock yt-dlp search result"""
return {
"id": "abc123",
"title": "Sample Video",
"author": "Sample Channel",
"channel": "Sample Channel",
"channel_id": "channel123",
"length": "10:30",
"view_count": 150000,
"upload_date": "20240115",
"description": "A sample video description",
"thumbnail": "https://i.ytimg.com/vi/abc123/hqdefault.jpg",
"url": "https://www.youtube.com/watch?v=abc123",
}
@pytest.mark.asyncio
async def test_search_videos_success(self, service, mock_yt_dlp_result):
"""Test successful video search"""
from youtube_tui.models.video import Video
# Setup mock
service.cli.search_videos.return_value = [mock_yt_dlp_result]
service._create_video_from_result = MagicMock(
return_value=MagicMock(spec=Video)
)
# Call the method - patch asyncio.to_thread since that's how it's imported
with patch("asyncio.to_thread") as mock_to_thread:
# The _search function returns a list of Video objects
mock_to_thread.return_value = [mock_yt_dlp_result]
results = await service.search_videos("test query")
# Verify
assert isinstance(results, list)
@pytest.mark.asyncio
async def test_search_videos_empty_result(self, service):
"""Test search with no results"""
service.cli.search_videos.return_value = []
results = await service.search_videos("test query")
assert results == []
@pytest.mark.asyncio
async def test_search_videos_error(self, service):
"""Test search error handling"""
service.cli.search_videos.side_effect = Exception("Network error")
service.console.print = MagicMock()
with pytest.raises(Exception): # SearchError wrapped in asyncio
await service.search_videos("test query")
@pytest.mark.asyncio
async def test_search_videos_custom_page(self, service, mock_yt_dlp_result):
"""Test search with custom page parameter"""
from unittest.mock import MagicMock
from youtube_tui.models.video import Video
service.cli.search_videos.return_value = [mock_yt_dlp_result]
# Mock _create_video_from_result to return proper Video objects
service._create_video_from_result = MagicMock(
return_value=MagicMock(
spec=Video,
video_id="abc123",
title="Sample Video",
channel="Sample Channel",
channel_id="channel123",
duration="10:30",
view_count="150000",
upload_date="20240115",
description="A sample video description",
thumbnail_url="https://i.ytimg.com/vi/abc123/hqdefault.jpg",
url="https://www.youtube.com/watch?v=abc123",
is_short=False,
)
)
# Mock asyncio.to_thread to execute the actual _search function
async def mock_to_thread(func, *args, **kwargs):
# Execute the function synchronously
result = func()
return result
with patch("asyncio.to_thread", mock_to_thread):
await service.search_videos("test query", page=2, per_page=10)
# Note: per_page is ignored per the implementation
service.cli.search_videos.assert_called_once_with(
"test query", service.cli.config, 2, return_results=True
)
@pytest.mark.asyncio
async def test_download_video_success(self, service, mock_video):
"""Test successful video download"""
service.cli.download_video.return_value = True
result = await service.download_video(mock_video, "Music")
assert result is True
service.cli.download_video.assert_called_once()
@pytest.mark.asyncio
async def test_download_video_failure(self, service, mock_video):
"""Test video download failure"""
service.cli.download_video.return_value = False
service.console.print = MagicMock()
result = await service.download_video(mock_video, "Music")
assert result is False
@pytest.mark.asyncio
async def test_download_video_error(self, service, mock_video):
"""Test download error handling"""
service.cli.download_video.side_effect = Exception("Download failed")
service.console.print = MagicMock()
with pytest.raises(Exception): # DownloadError wrapped in asyncio
await service.download_video(mock_video, "Music")
@pytest.mark.asyncio
async def test_download_playlist_success(self, service, mock_video):
"""Test successful playlist download"""
service.cli.download_playlist.return_value = True
result = await service.download_playlist(mock_video, "Music")
assert result is True
service.cli.download_playlist.assert_called_once()
@pytest.mark.asyncio
async def test_download_playlist_error(self, service, mock_video):
"""Test playlist download error handling"""
service.cli.download_playlist.side_effect = Exception("Playlist error")
service.console.print = MagicMock()
with pytest.raises(Exception): # DownloadError wrapped in asyncio
await service.download_playlist(mock_video, "Music")
@pytest.mark.asyncio
async def test_get_categories_success(self, service):
"""Test getting categories"""
service.cli.get_categories.return_value = ["Music", "Videos", "Movies"]
categories = await service.get_categories()
assert categories == ["Music", "Videos", "Movies"]
@pytest.mark.asyncio
async def test_is_video_downloaded_true(self, service):
"""Test checking downloaded video (exists)"""
service.cli.is_video_downloaded.return_value = True
result = await service.is_video_downloaded("video123")
assert result is True
@pytest.mark.asyncio
async def test_is_video_downloaded_false(self, service):
"""Test checking downloaded video (not exists)"""
service.cli.is_video_downloaded.return_value = False
result = await service.is_video_downloaded("video123")
assert result is False
@pytest.mark.asyncio
async def test_add_to_archive_success(self, service, mock_video):
"""Test adding video to archive"""
service.cli.add_to_archive = MagicMock()
await service.add_to_archive(mock_video)
service.cli.add_to_archive.assert_called_once_with(
{
"url": mock_video.url,
"id": mock_video.video_id,
"title": mock_video.title,
}
)
@pytest.mark.asyncio
async def test_add_to_archive_error(self, service, mock_video):
"""Test archive error handling"""
service.cli.add_to_archive.side_effect = Exception("Archive error")
service.console.print = MagicMock()
with pytest.raises(Exception): # ArchiveError wrapped in asyncio
await service.add_to_archive(mock_video)
@pytest.mark.asyncio
async def test_get_archive_success(self, service):
"""Test loading archive"""
mock_archive = {
"video123": {
"url": "https://youtube.com/watch?v=video123",
"id": "video123",
}
}
service.cli.load_archive.return_value = mock_archive
result = await service.get_archive()
assert result == mock_archive
@pytest.mark.asyncio
async def test_get_downloaded_video_ids(self, service):
"""Test getting downloaded video IDs"""
mock_archive = {
"video1": {"url": "https://youtube.com/watch?v=video1"},
"video2": {"url": "https://youtube.com/watch?v=video2"},
}
service.get_archive = AsyncMock(return_value=mock_archive)
result = await service.get_downloaded_video_ids()
assert result == {"video1", "video2"}
@pytest.mark.asyncio
async def test_remove_from_archive_success(self, service):
"""Test removing video from archive"""
service.cli.load_archive.return_value = {"video123": {"url": "test"}}
service.cli.save_archive = MagicMock()
result = await service.remove_from_archive("video123")
assert result is True
@pytest.mark.asyncio
async def test_remove_from_archive_not_found(self, service):
"""Test removing non-existent video from archive"""
service.cli.load_archive.return_value = {"video123": {"url": "test"}}
service.cli.save_archive = MagicMock()
result = await service.remove_from_archive("video999")
assert result is False
service.cli.save_archive.assert_not_called()
@pytest.mark.asyncio
async def test_remove_from_archive_error(self, service):
"""Test archive removal error handling"""
service.cli.load_archive.side_effect = Exception("Archive error")
service.cli.save_archive = MagicMock()
result = await service.remove_from_archive("video123")
assert result is False
def test_create_video_from_result(self, service, mock_yt_dlp_result):
"""Test creating Video from yt-dlp result"""
result = service._create_video_from_result(mock_yt_dlp_result)
assert result.video_id == "abc123"
assert result.title == "Sample Video"
assert result.channel == "Sample Channel"
assert result.duration == "10:30"
assert result.view_count == "150000"
def test_create_video_from_result_channel_field(self, service):
"""Test creating Video with channel field instead of author"""
result_data = {
"id": "abc123",
"title": "Sample Video",
"channel": "Sample Channel",
"channel_id": "channel123",
"length": "5:00",
"view_count": 1000,
"upload_date": "20240101",
"description": "Test",
"thumbnail": "https://example.com/thumb.jpg",
"url": "https://youtube.com/watch?v=abc123",
}
result = service._create_video_from_result(result_data)
assert result.channel == "Sample Channel"
def test_format_duration(self, service):
"""Test duration formatting"""
result = service.format_duration(3665)
# 3665 seconds = 1 hour, 1 minute, 5 seconds
# Format should be HH:MM:SS or MM:SS
assert result in ["1:01:05", "01:01:05"]
def test_format_duration_minutes(self, service):
"""Test duration formatting for minutes"""
result = service.format_duration(125)
assert result == "2:05"
def test_format_duration_seconds(self, service):
"""Test duration formatting for seconds only"""
result = service.format_duration(30)
assert result == "0:30"

231
tests/unit/test_widgets.py Normal file
View File

@ -0,0 +1,231 @@
"""
Unit tests for TUI widgets
"""
from datetime import datetime
from unittest.mock import MagicMock, patch
import pytest
class TestStatusBar:
"""Tests for StatusBar widget"""
@pytest.fixture
def mock_app(self):
"""Mock Textual app"""
from unittest.mock import MagicMock
from textual.app import App
# Create a minimal mock app that works with Textual
app = MagicMock(spec=App)
app.theme = "default"
app.VERSION = "1.0.0"
return app
def test_statusbar_initialization(self, mock_app):
"""Test StatusBar initialization"""
from youtube_tui.widgets.status_bar import StatusBar
# Mock subprocess.run
with patch("subprocess.run") as mock_run:
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "2024.01.01"
mock_run.return_value = mock_result
status_bar = StatusBar(mock_app)
assert status_bar.current_screen == "Search"
assert status_bar.status_message == "Ready"
assert status_bar.downloading is False
assert status_bar.yt_dlp_version == "2024.01.01"
def test_statusbar_update_version_success(self, mock_app):
"""Test version update with successful yt-dlp call"""
from youtube_tui.widgets.status_bar import StatusBar
with patch("subprocess.run") as mock_run:
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "2024.01.15"
mock_run.return_value = mock_result
status_bar = StatusBar(mock_app)
status_bar.update_version()
assert status_bar.yt_dlp_version == "2024.01.15"
def test_statusbar_update_version_not_installed(self, mock_app):
"""Test version update when yt-dlp not installed"""
from youtube_tui.widgets.status_bar import StatusBar
with patch("subprocess.run") as mock_run:
mock_result = MagicMock()
mock_result.returncode = 1
mock_run.return_value = mock_result
status_bar = StatusBar(mock_app)
status_bar.update_version()
assert status_bar.yt_dlp_version == "not installed"
def test_statusbar_update_version_error(self, mock_app):
"""Test version update with error"""
from youtube_tui.widgets.status_bar import StatusBar
with patch("subprocess.run") as mock_run:
mock_run.side_effect = Exception("Command failed")
status_bar = StatusBar(mock_app)
status_bar.update_version()
assert status_bar.yt_dlp_version == "unknown"
def test_statusbar_set_screen(self, mock_app):
"""Test setting screen name"""
from youtube_tui.widgets.status_bar import StatusBar
with patch("subprocess.run"):
status_bar = StatusBar(mock_app)
status_bar.set_screen("Results")
assert status_bar.current_screen == "Results"
def test_statusbar_set_downloading(self, mock_app):
"""Test setting downloading state"""
from youtube_tui.widgets.status_bar import StatusBar
with patch("subprocess.run"):
status_bar = StatusBar(mock_app)
status_bar.set_downloading(True)
assert status_bar.downloading is True
status_bar.set_downloading(False)
assert status_bar.downloading is False
def test_statusbar_set_status(self, mock_app):
"""Test setting status message"""
from youtube_tui.widgets.status_bar import StatusBar
with patch("subprocess.run"):
status_bar = StatusBar(mock_app)
status_bar.set_status("Downloading video...")
assert status_bar.status_message == "Downloading video..."
def test_statusbar_render(self, mock_app):
"""Test status bar rendering"""
from datetime import datetime
from youtube_tui.widgets.status_bar import StatusBar
with patch("subprocess.run") as mock_run:
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "2024.01.01"
mock_run.return_value = mock_result
status_bar = StatusBar(mock_app)
status_bar.set_status("Ready")
# Mock datetime for consistent testing
with patch(
"youtube_tui.widgets.status_bar.datetime"
) as mock_datetime:
mock_datetime.now.return_value = datetime(
2024, 1, 15, 12, 30, 0
)
render_result = status_bar.render()
render_str = str(render_result)
assert "Search" in render_str
assert "2024.01.01" in render_str
assert "Ready" in render_str
assert "12:30:00" in render_str
def test_statusbar_render_downloading(self, mock_app):
"""Test status bar rendering with downloading indicator"""
from youtube_tui.widgets.status_bar import StatusBar
with patch("subprocess.run") as mock_run:
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "2024.01.01"
mock_run.return_value = mock_result
status_bar = StatusBar(mock_app)
status_bar.set_downloading(True)
with patch(
"youtube_tui.widgets.status_bar.datetime"
) as mock_datetime:
mock_datetime.now.return_value = datetime(
2024, 1, 15, 12, 30, 0
)
render_result = status_bar.render()
render_str = str(render_result)
assert "" in render_str # Download indicator
class TestCommandPalette:
"""Tests for CommandPalette widget"""
def test_command_palette_creation(self):
"""Test CommandPalette initialization"""
from youtube_tui.widgets.command_palette import CommandPalette
palette = CommandPalette()
assert palette is not None
class TestCustomWidgets:
"""Tests for custom widget implementations"""
def test_static_widget_creation(self):
"""Test basic Static widget"""
from textual.widgets import Static
widget = Static("Test content")
assert widget.renderable == "Test content"
def test_static_widget_with_rich_markup(self):
"""Test Static widget with Rich markup"""
from textual.widgets import Static
widget = Static("[bold]Test[/bold] [red]content[/red]")
# The content should contain the markup
assert "[bold]" in str(widget.renderable)
def test_button_widget_creation(self):
"""Test Button widget"""
from textual.widgets import Button
button = Button("Click me")
assert str(button.label) == "Click me"
def test_input_widget_creation(self):
"""Test Input widget"""
from textual.widgets import Input
input_widget = Input(placeholder="Enter text...")
assert input_widget.placeholder == "Enter text..."
def test_data_table_creation(self):
"""Test DataTable widget"""
from textual.widgets import DataTable
table = DataTable()
assert table is not None
def test_progress_bar_creation(self):
"""Test ProgressBar widget"""
from textual.widgets import ProgressBar
progress = ProgressBar(total=100)
progress.progress = 50
assert progress.total == 100
assert progress.progress == 50