mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-30 14:40:39 -08:00
480 lines
19 KiB
Python
480 lines
19 KiB
Python
"""
|
|
Tests for the player controls menu functionality.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
import threading
|
|
|
|
from fastanime.cli.interactive.menus.player_controls import player_controls
|
|
from fastanime.cli.interactive.state import ControlFlow, State, MediaApiState, ProviderState
|
|
from fastanime.libs.players.types import PlayerResult
|
|
from fastanime.libs.providers.anime.types import Server, StreamLink
|
|
from fastanime.libs.api.types import MediaItem
|
|
|
|
|
|
class TestPlayerControlsMenu:
|
|
"""Test cases for the player controls menu."""
|
|
|
|
def test_player_controls_menu_missing_data(self, mock_context, empty_state):
|
|
"""Test player controls menu with missing data."""
|
|
result = player_controls(mock_context, empty_state)
|
|
|
|
# Should go back when required data is missing
|
|
assert result == ControlFlow.BACK
|
|
|
|
def test_player_controls_menu_successful_playback(self, mock_context, full_state):
|
|
"""Test player controls menu after successful playback."""
|
|
# Setup state with player result
|
|
state_with_result = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=full_state.provider.anime,
|
|
episode_number="1",
|
|
last_player_result=PlayerResult(success=True, exit_code=0)
|
|
)
|
|
)
|
|
|
|
# Mock user choice to go back
|
|
mock_context.selector.choose.return_value = "🔙 Back to Episodes"
|
|
|
|
result = player_controls(mock_context, state_with_result)
|
|
|
|
# Should go back to episodes
|
|
assert result == ControlFlow.BACK
|
|
|
|
def test_player_controls_menu_playback_failure(self, mock_context, full_state):
|
|
"""Test player controls menu after playback failure."""
|
|
# Setup state with failed player result
|
|
state_with_failure = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=full_state.provider.anime,
|
|
episode_number="1",
|
|
last_player_result=PlayerResult(success=False, exit_code=1)
|
|
)
|
|
)
|
|
|
|
# Mock user choice to retry
|
|
mock_context.selector.choose.return_value = "🔄 Try Different Server"
|
|
|
|
result = player_controls(mock_context, state_with_failure)
|
|
|
|
# Should transition back to SERVERS state
|
|
assert isinstance(result, State)
|
|
assert result.menu_name == "SERVERS"
|
|
|
|
def test_player_controls_next_episode_available(self, mock_context, full_state):
|
|
"""Test next episode option when available."""
|
|
# Mock anime with multiple episodes
|
|
from fastanime.libs.providers.anime.types import Episodes
|
|
provider_anime = full_state.provider.anime
|
|
provider_anime.episodes = Episodes(sub=["1", "2", "3"], dub=["1", "2", "3"])
|
|
|
|
state_with_next = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=provider_anime,
|
|
episode_number="1", # Currently on episode 1, so 2 is available
|
|
last_player_result=PlayerResult(success=True, exit_code=0)
|
|
)
|
|
)
|
|
|
|
# Mock user choice to play next episode
|
|
mock_context.selector.choose.return_value = "▶️ Next Episode (2)"
|
|
|
|
result = player_controls(mock_context, state_with_next)
|
|
|
|
# Should transition to SERVERS state with next episode
|
|
assert isinstance(result, State)
|
|
assert result.menu_name == "SERVERS"
|
|
assert result.provider.episode_number == "2"
|
|
|
|
def test_player_controls_no_next_episode(self, mock_context, full_state):
|
|
"""Test when no next episode is available."""
|
|
# Mock anime with only one episode
|
|
from fastanime.libs.providers.anime.types import Episodes
|
|
provider_anime = full_state.provider.anime
|
|
provider_anime.episodes = Episodes(sub=["1"], dub=["1"])
|
|
|
|
state_last_episode = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=provider_anime,
|
|
episode_number="1", # Last episode
|
|
last_player_result=PlayerResult(success=True, exit_code=0)
|
|
)
|
|
)
|
|
|
|
# Mock back selection since no next episode
|
|
mock_context.selector.choose.return_value = "🔙 Back to Episodes"
|
|
|
|
result = player_controls(mock_context, state_last_episode)
|
|
|
|
# Should go back
|
|
assert result == ControlFlow.BACK
|
|
|
|
# Verify next episode option is not in choices
|
|
call_args = mock_context.selector.choose.call_args
|
|
choices = call_args[1]['choices']
|
|
next_episode_options = [choice for choice in choices if "Next Episode" in choice]
|
|
assert len(next_episode_options) == 0
|
|
|
|
def test_player_controls_replay_episode(self, mock_context, full_state):
|
|
"""Test replaying current episode."""
|
|
state_with_result = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=full_state.provider.anime,
|
|
episode_number="1",
|
|
last_player_result=PlayerResult(success=True, exit_code=0)
|
|
)
|
|
)
|
|
|
|
# Mock user choice to replay
|
|
mock_context.selector.choose.return_value = "🔄 Replay Episode"
|
|
|
|
result = player_controls(mock_context, state_with_result)
|
|
|
|
# Should transition back to SERVERS state with same episode
|
|
assert isinstance(result, State)
|
|
assert result.menu_name == "SERVERS"
|
|
assert result.provider.episode_number == "1"
|
|
|
|
def test_player_controls_change_server(self, mock_context, full_state):
|
|
"""Test changing server option."""
|
|
state_with_result = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=full_state.provider.anime,
|
|
episode_number="1",
|
|
last_player_result=PlayerResult(success=True, exit_code=0)
|
|
)
|
|
)
|
|
|
|
# Mock user choice to try different server
|
|
mock_context.selector.choose.return_value = "🔄 Try Different Server"
|
|
|
|
result = player_controls(mock_context, state_with_result)
|
|
|
|
# Should transition back to SERVERS state
|
|
assert isinstance(result, State)
|
|
assert result.menu_name == "SERVERS"
|
|
|
|
def test_player_controls_mark_as_watched(self, mock_context, full_state):
|
|
"""Test marking episode as watched."""
|
|
state_with_result = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=full_state.provider.anime,
|
|
episode_number="1",
|
|
last_player_result=PlayerResult(success=True, exit_code=0)
|
|
)
|
|
)
|
|
|
|
# Mock authenticated user
|
|
mock_context.media_api.is_authenticated.return_value = True
|
|
|
|
# Mock user choice to mark as watched
|
|
mock_context.selector.choose.return_value = "✅ Mark as Watched"
|
|
|
|
with patch('fastanime.cli.interactive.menus.player_controls._update_progress_in_background') as mock_update:
|
|
result = player_controls(mock_context, state_with_result)
|
|
|
|
# Should update progress in background
|
|
mock_update.assert_called_once()
|
|
|
|
# Should continue
|
|
assert result == ControlFlow.CONTINUE
|
|
|
|
def test_player_controls_not_authenticated_no_mark_option(self, mock_context, full_state):
|
|
"""Test that mark as watched option is not shown when not authenticated."""
|
|
state_with_result = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=full_state.provider.anime,
|
|
episode_number="1",
|
|
last_player_result=PlayerResult(success=True, exit_code=0)
|
|
)
|
|
)
|
|
|
|
# Mock unauthenticated user
|
|
mock_context.media_api.is_authenticated.return_value = False
|
|
mock_context.selector.choose.return_value = "🔙 Back to Episodes"
|
|
|
|
result = player_controls(mock_context, state_with_result)
|
|
|
|
# Verify mark as watched option is not in choices
|
|
call_args = mock_context.selector.choose.call_args
|
|
choices = call_args[1]['choices']
|
|
mark_options = [choice for choice in choices if "Mark as Watched" in choice]
|
|
assert len(mark_options) == 0
|
|
|
|
def test_player_controls_auto_next_enabled(self, mock_context, full_state):
|
|
"""Test auto next episode when enabled in config."""
|
|
# Enable auto next in config
|
|
mock_context.config.stream.auto_next = True
|
|
|
|
# Mock anime with multiple episodes
|
|
from fastanime.libs.providers.anime.types import Episodes
|
|
provider_anime = full_state.provider.anime
|
|
provider_anime.episodes = Episodes(sub=["1", "2", "3"], dub=["1", "2", "3"])
|
|
|
|
state_with_auto_next = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=provider_anime,
|
|
episode_number="1",
|
|
last_player_result=PlayerResult(success=True, exit_code=0)
|
|
)
|
|
)
|
|
|
|
result = player_controls(mock_context, state_with_auto_next)
|
|
|
|
# Should automatically transition to next episode
|
|
assert isinstance(result, State)
|
|
assert result.menu_name == "SERVERS"
|
|
assert result.provider.episode_number == "2"
|
|
|
|
# Selector should not be called for auto next
|
|
mock_context.selector.choose.assert_not_called()
|
|
|
|
def test_player_controls_auto_next_last_episode(self, mock_context, full_state):
|
|
"""Test auto next when on last episode."""
|
|
# Enable auto next in config
|
|
mock_context.config.stream.auto_next = True
|
|
|
|
# Mock anime with only one episode
|
|
from fastanime.libs.providers.anime.types import Episodes
|
|
provider_anime = full_state.provider.anime
|
|
provider_anime.episodes = Episodes(sub=["1"], dub=["1"])
|
|
|
|
state_last_episode = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=provider_anime,
|
|
episode_number="1",
|
|
last_player_result=PlayerResult(success=True, exit_code=0)
|
|
)
|
|
)
|
|
|
|
# Mock back selection since auto next can't proceed
|
|
mock_context.selector.choose.return_value = "🔙 Back to Episodes"
|
|
|
|
result = player_controls(mock_context, state_last_episode)
|
|
|
|
# Should show menu when auto next can't proceed
|
|
assert result == ControlFlow.BACK
|
|
|
|
def test_player_controls_no_choice_made(self, mock_context, full_state):
|
|
"""Test player controls when no choice is made."""
|
|
state_with_result = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=full_state.provider.anime,
|
|
episode_number="1",
|
|
last_player_result=PlayerResult(success=True, exit_code=0)
|
|
)
|
|
)
|
|
|
|
# Mock no selection
|
|
mock_context.selector.choose.return_value = None
|
|
|
|
result = player_controls(mock_context, state_with_result)
|
|
|
|
# Should go back when no selection is made
|
|
assert result == ControlFlow.BACK
|
|
|
|
def test_player_controls_icons_enabled(self, mock_context, full_state):
|
|
"""Test player controls menu with icons enabled."""
|
|
mock_context.config.general.icons = True
|
|
|
|
state_with_result = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=full_state.provider.anime,
|
|
episode_number="1",
|
|
last_player_result=PlayerResult(success=True, exit_code=0)
|
|
)
|
|
)
|
|
|
|
mock_context.selector.choose.return_value = "🔙 Back to Episodes"
|
|
|
|
result = player_controls(mock_context, state_with_result)
|
|
|
|
# Should work with icons enabled
|
|
assert result == ControlFlow.BACK
|
|
|
|
def test_player_controls_icons_disabled(self, mock_context, full_state):
|
|
"""Test player controls menu with icons disabled."""
|
|
mock_context.config.general.icons = False
|
|
|
|
state_with_result = State(
|
|
menu_name="PLAYER_CONTROLS",
|
|
media_api=full_state.media_api,
|
|
provider=ProviderState(
|
|
anime=full_state.provider.anime,
|
|
episode_number="1",
|
|
last_player_result=PlayerResult(success=True, exit_code=0)
|
|
)
|
|
)
|
|
|
|
mock_context.selector.choose.return_value = "Back to Episodes"
|
|
|
|
result = player_controls(mock_context, state_with_result)
|
|
|
|
# Should work with icons disabled
|
|
assert result == ControlFlow.BACK
|
|
|
|
|
|
class TestPlayerControlsHelperFunctions:
|
|
"""Test the helper functions in player controls menu."""
|
|
|
|
def test_calculate_completion_valid_times(self):
|
|
"""Test calculating completion percentage with valid times."""
|
|
from fastanime.cli.interactive.menus.player_controls import _calculate_completion
|
|
|
|
# 30 minutes out of 60 minutes = 50%
|
|
result = _calculate_completion("00:30:00", "01:00:00")
|
|
|
|
assert result == 50.0
|
|
|
|
def test_calculate_completion_zero_duration(self):
|
|
"""Test calculating completion with zero duration."""
|
|
from fastanime.cli.interactive.menus.player_controls import _calculate_completion
|
|
|
|
result = _calculate_completion("00:30:00", "00:00:00")
|
|
|
|
assert result == 0
|
|
|
|
def test_calculate_completion_invalid_format(self):
|
|
"""Test calculating completion with invalid time format."""
|
|
from fastanime.cli.interactive.menus.player_controls import _calculate_completion
|
|
|
|
result = _calculate_completion("invalid", "01:00:00")
|
|
|
|
assert result == 0
|
|
|
|
def test_calculate_completion_partial_episode(self):
|
|
"""Test calculating completion for partial episode viewing."""
|
|
from fastanime.cli.interactive.menus.player_controls import _calculate_completion
|
|
|
|
# 15 minutes out of 24 minutes = 62.5%
|
|
result = _calculate_completion("00:15:00", "00:24:00")
|
|
|
|
assert result == 62.5
|
|
|
|
def test_update_progress_in_background_authenticated(self, mock_context):
|
|
"""Test updating progress in background when authenticated."""
|
|
from fastanime.cli.interactive.menus.player_controls import _update_progress_in_background
|
|
|
|
# Mock authenticated user
|
|
mock_context.media_api.user_profile = Mock()
|
|
mock_context.media_api.update_list_entry = Mock()
|
|
|
|
# Call the function
|
|
_update_progress_in_background(mock_context, 123, 5)
|
|
|
|
# Give the thread a moment to execute
|
|
import time
|
|
time.sleep(0.1)
|
|
|
|
# Should call update_list_entry
|
|
mock_context.media_api.update_list_entry.assert_called_once()
|
|
|
|
def test_update_progress_in_background_not_authenticated(self, mock_context):
|
|
"""Test updating progress in background when not authenticated."""
|
|
from fastanime.cli.interactive.menus.player_controls import _update_progress_in_background
|
|
|
|
# Mock unauthenticated user
|
|
mock_context.media_api.user_profile = None
|
|
mock_context.media_api.update_list_entry = Mock()
|
|
|
|
# Call the function
|
|
_update_progress_in_background(mock_context, 123, 5)
|
|
|
|
# Give the thread a moment to execute
|
|
import time
|
|
time.sleep(0.1)
|
|
|
|
# Should still call update_list_entry (comment suggests it should)
|
|
mock_context.media_api.update_list_entry.assert_called_once()
|
|
|
|
def test_get_next_episode_number(self):
|
|
"""Test getting next episode number."""
|
|
from fastanime.cli.interactive.menus.player_controls import _get_next_episode_number
|
|
|
|
available_episodes = ["1", "2", "3", "4", "5"]
|
|
current_episode = "3"
|
|
|
|
result = _get_next_episode_number(available_episodes, current_episode)
|
|
|
|
assert result == "4"
|
|
|
|
def test_get_next_episode_number_last_episode(self):
|
|
"""Test getting next episode when on last episode."""
|
|
from fastanime.cli.interactive.menus.player_controls import _get_next_episode_number
|
|
|
|
available_episodes = ["1", "2", "3"]
|
|
current_episode = "3"
|
|
|
|
result = _get_next_episode_number(available_episodes, current_episode)
|
|
|
|
assert result is None
|
|
|
|
def test_get_next_episode_number_not_found(self):
|
|
"""Test getting next episode when current episode not found."""
|
|
from fastanime.cli.interactive.menus.player_controls import _get_next_episode_number
|
|
|
|
available_episodes = ["1", "2", "3"]
|
|
current_episode = "5" # Not in the list
|
|
|
|
result = _get_next_episode_number(available_episodes, current_episode)
|
|
|
|
assert result is None
|
|
|
|
def test_should_show_mark_as_watched_authenticated(self, mock_context):
|
|
"""Test should show mark as watched when authenticated."""
|
|
from fastanime.cli.interactive.menus.player_controls import _should_show_mark_as_watched
|
|
|
|
mock_context.media_api.is_authenticated.return_value = True
|
|
player_result = PlayerResult(success=True, exit_code=0)
|
|
|
|
result = _should_show_mark_as_watched(mock_context, player_result)
|
|
|
|
assert result is True
|
|
|
|
def test_should_show_mark_as_watched_not_authenticated(self, mock_context):
|
|
"""Test should not show mark as watched when not authenticated."""
|
|
from fastanime.cli.interactive.menus.player_controls import _should_show_mark_as_watched
|
|
|
|
mock_context.media_api.is_authenticated.return_value = False
|
|
player_result = PlayerResult(success=True, exit_code=0)
|
|
|
|
result = _should_show_mark_as_watched(mock_context, player_result)
|
|
|
|
assert result is False
|
|
|
|
def test_should_show_mark_as_watched_playback_failed(self, mock_context):
|
|
"""Test should not show mark as watched when playback failed."""
|
|
from fastanime.cli.interactive.menus.player_controls import _should_show_mark_as_watched
|
|
|
|
mock_context.media_api.is_authenticated.return_value = True
|
|
player_result = PlayerResult(success=False, exit_code=1)
|
|
|
|
result = _should_show_mark_as_watched(mock_context, player_result)
|
|
|
|
assert result is False
|