test: add TUI test suite (moved from youtube-cli)
This commit is contained in:
parent
6884c1f0ef
commit
96247567d9
235
tests/conftest.py
Normal file
235
tests/conftest.py
Normal 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
|
||||
324
tests/integration/test_queue.py
Normal file
324
tests/integration/test_queue.py
Normal 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
|
||||
664
tests/integration/test_screens.py
Normal file
664
tests/integration/test_screens.py
Normal 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
219
tests/unit/test_models.py
Normal 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
340
tests/unit/test_service.py
Normal 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
231
tests/unit/test_widgets.py
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user