yomitalk / tests /unit /test_app_complex_methods.py
Kyosuke Ichikawa
bugfix: Remove invalid resume_from_part parameter from _create_progress_html tests (#1)
2ec4204 unverified
"""Unit tests for complex methods in app.py."""
import tempfile
from pathlib import Path
from typing import Any, Dict
from unittest.mock import Mock, patch
from yomitalk.app import PaperPodcastApp
from yomitalk.user_session import UserSession
class TestUpdateAudioButtonStateWithResumeCheck:
"""Test update_audio_button_state_with_resume_check method."""
def setup_method(self):
"""Set up test fixtures."""
self.app = PaperPodcastApp()
self.user_session = UserSession("test_session")
self.mock_request = Mock()
self.mock_request.session_hash = "test_session_hash"
# Base browser state
self.browser_state: Dict[str, Any] = {
"app_session_id": "test_session",
"audio_generation_state": {
"is_generating": False,
"progress": 0.0,
"status": "idle",
"current_script": "",
"generated_parts": [],
"final_audio_path": None,
"streaming_parts": [],
"generation_id": None,
"start_time": None,
"estimated_total_parts": 1,
"script_changed": False,
},
"user_settings": {},
"ui_state": {},
}
def test_unchecked_checkbox_returns_disabled_state(self):
"""Test that unchecked checkbox returns disabled state with message."""
result = self.app.update_audio_button_state_with_resume_check(checked=False, podcast_text="Some valid text", user_session=self.user_session, browser_state=self.browser_state)
assert result["interactive"] is False
assert result["variant"] == "secondary"
assert "VOICEVOX利用規約に同意が必要です" in result["value"]
def test_empty_text_returns_disabled_state(self):
"""Test that empty text returns disabled state with message."""
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="", user_session=self.user_session, browser_state=self.browser_state)
assert result["interactive"] is False
assert result["variant"] == "secondary"
assert "トーク原稿が必要です" in result["value"]
def test_whitespace_only_text_returns_disabled_state(self):
"""Test that whitespace-only text returns disabled state."""
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text=" \n\t ", user_session=self.user_session, browser_state=self.browser_state)
assert result["interactive"] is False
assert result["variant"] == "secondary"
assert "トーク原稿が必要です" in result["value"]
def test_none_text_returns_disabled_state(self):
"""Test that None text returns disabled state."""
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text=None, user_session=self.user_session, browser_state=self.browser_state)
assert result["interactive"] is False
assert result["variant"] == "secondary"
assert "トーク原稿が必要です" in result["value"]
def test_new_script_returns_generate_button(self):
"""Test that new script returns generate button state."""
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="New script content", user_session=self.user_session, browser_state=self.browser_state)
assert result["interactive"] is True
assert result["variant"] == "primary"
assert result["value"] == "音声を生成"
def test_script_unchanged_with_streaming_parts_returns_resume_button(self):
"""Test that unchanged script with streaming parts returns resume button."""
# Set up browser state with streaming parts
self.browser_state["audio_generation_state"]["current_script"] = "Test script"
self.browser_state["audio_generation_state"]["streaming_parts"] = ["part1.wav", "part2.wav"]
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="Test script", user_session=self.user_session, browser_state=self.browser_state)
assert result["interactive"] is True
assert result["variant"] == "primary"
assert result["value"] == "音声生成を再開"
def test_script_unchanged_with_preparing_status_returns_resume_button(self):
"""Test that unchanged script with preparing status returns resume button."""
# Set up browser state with preparing status
self.browser_state["audio_generation_state"]["current_script"] = "Test script"
self.browser_state["audio_generation_state"]["status"] = "preparing"
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="Test script", user_session=self.user_session, browser_state=self.browser_state)
assert result["interactive"] is True
assert result["variant"] == "primary"
assert result["value"] == "音声生成を再開"
def test_script_unchanged_with_final_audio_returns_completed_button(self):
"""Test that unchanged script with final audio returns completed button."""
# Set up browser state with final audio
self.browser_state["audio_generation_state"]["current_script"] = "Test script"
self.browser_state["audio_generation_state"]["final_audio_path"] = "/path/to/final.wav"
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="Test script", user_session=self.user_session, browser_state=self.browser_state)
assert result["interactive"] is False
assert result["variant"] == "secondary"
assert result["value"] == "音声生成済み"
def test_script_changed_after_completion_returns_generate_button(self):
"""Test that changed script after completion returns generate button."""
# Set up browser state with final audio for old script
self.browser_state["audio_generation_state"]["current_script"] = "Old script"
self.browser_state["audio_generation_state"]["final_audio_path"] = "/path/to/final.wav"
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="New script content", user_session=self.user_session, browser_state=self.browser_state)
assert result["interactive"] is True
assert result["variant"] == "primary"
assert result["value"] == "音声を生成"
def test_discovers_final_audio_on_disk_when_not_in_browser_state(self):
"""Test that method discovers final audio on disk when not in browser state."""
# Set up browser state with matching script but no final audio
self.browser_state["audio_generation_state"]["current_script"] = "Test script"
self.browser_state["audio_generation_state"]["final_audio_path"] = None
# Create a temporary directory and audio file
with tempfile.TemporaryDirectory() as temp_dir:
audio_file = Path(temp_dir) / "audio_final.wav"
audio_file.write_text("fake audio content")
# Mock user_session.get_output_dir to return our temp directory
with patch.object(self.user_session, "get_output_dir", return_value=Path(temp_dir)):
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="Test script", user_session=self.user_session, browser_state=self.browser_state)
# Should discover the file and update browser state
assert result["interactive"] is False
assert result["variant"] == "secondary"
assert result["value"] == "音声生成済み"
# Browser state should be updated
assert self.browser_state["audio_generation_state"]["final_audio_path"] == str(audio_file)
assert self.browser_state["audio_generation_state"]["status"] == "completed"
assert self.browser_state["audio_generation_state"]["is_generating"] is False
assert self.browser_state["audio_generation_state"]["progress"] == 1.0
def test_no_discovery_when_script_changed_flag_is_set(self):
"""Test that no disk discovery happens when script_changed flag is set."""
# Set up browser state with matching script but script_changed flag
self.browser_state["audio_generation_state"]["current_script"] = "Test script"
self.browser_state["audio_generation_state"]["final_audio_path"] = None
self.browser_state["audio_generation_state"]["script_changed"] = True
# Create a temporary directory and audio file
with tempfile.TemporaryDirectory() as temp_dir:
audio_file = Path(temp_dir) / "audio_final.wav"
audio_file.write_text("fake audio content")
# Mock user_session.get_output_dir to return our temp directory
with patch.object(self.user_session, "get_output_dir", return_value=Path(temp_dir)):
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="Test script", user_session=self.user_session, browser_state=self.browser_state)
# Should not discover the file and return resume button
assert result["interactive"] is True
assert result["variant"] == "primary"
assert result["value"] == "音声生成を再開"
# Browser state should not be updated
assert self.browser_state["audio_generation_state"]["final_audio_path"] is None
def test_fallback_to_legacy_session_methods_when_no_browser_state(self):
"""Test fallback to legacy UserSession methods when browser_state is None."""
# Mock UserSession methods
mock_audio_state = {"current_script": "Test script", "status": "completed"}
with (
patch.object(self.user_session, "get_audio_generation_status", return_value=mock_audio_state),
patch.object(self.user_session, "has_generated_audio", return_value=True),
patch.object(self.user_session.audio_generator, "final_audio_path", "/path/to/final.wav"),
patch("os.path.exists", return_value=True),
):
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="Test script", user_session=self.user_session, browser_state=None)
assert result["interactive"] is False
assert result["variant"] == "secondary"
assert result["value"] == "音声生成済み"
def test_fallback_to_legacy_resume_when_no_final_audio_file(self):
"""Test fallback to legacy resume when no final audio file exists."""
# Mock UserSession methods
mock_audio_state = {"current_script": "Test script", "status": "generating"}
with (
patch.object(self.user_session, "get_audio_generation_status", return_value=mock_audio_state),
patch.object(self.user_session, "has_generated_audio", return_value=True),
patch.object(self.user_session.audio_generator, "final_audio_path", "/path/to/final.wav"),
patch("os.path.exists", return_value=False),
):
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="Test script", user_session=self.user_session, browser_state=None)
assert result["interactive"] is True
assert result["variant"] == "primary"
assert result["value"] == "音声生成を再開"
def test_empty_current_script_in_browser_state_returns_generate_button(self):
"""Test that empty current_script in browser state returns generate button."""
# Set up browser state with empty current_script
self.browser_state["audio_generation_state"]["current_script"] = ""
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="New script content", user_session=self.user_session, browser_state=self.browser_state)
assert result["interactive"] is True
assert result["variant"] == "primary"
assert result["value"] == "音声を生成"
def test_script_mismatch_returns_generate_button(self):
"""Test that script mismatch returns generate button."""
# Set up browser state with different script
self.browser_state["audio_generation_state"]["current_script"] = "Old script"
self.browser_state["audio_generation_state"]["streaming_parts"] = ["part1.wav"]
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="New script content", user_session=self.user_session, browser_state=self.browser_state)
assert result["interactive"] is True
assert result["variant"] == "primary"
assert result["value"] == "音声を生成"
def test_gradio_update_structure(self):
"""Test that result has correct Gradio update structure."""
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="Test script", user_session=self.user_session, browser_state=self.browser_state)
# Check that result has the correct structure for gr.update
assert "value" in result
assert "interactive" in result
assert "variant" in result
assert isinstance(result["value"], str)
assert isinstance(result["interactive"], bool)
assert result["variant"] in ["primary", "secondary"]
def test_multiple_audio_files_on_disk_uses_first_match(self):
"""Test that when multiple audio files exist on disk, it uses the first match."""
# Set up browser state with matching script but no final audio
self.browser_state["audio_generation_state"]["current_script"] = "Test script"
self.browser_state["audio_generation_state"]["final_audio_path"] = None
# Create a temporary directory with multiple audio files
with tempfile.TemporaryDirectory() as temp_dir:
audio_file1 = Path(temp_dir) / "audio_part1.wav"
audio_file2 = Path(temp_dir) / "audio_final.wav"
audio_file1.write_text("fake audio content 1")
audio_file2.write_text("fake audio content 2")
# Mock user_session.get_output_dir to return our temp directory
with patch.object(self.user_session, "get_output_dir", return_value=Path(temp_dir)):
result = self.app.update_audio_button_state_with_resume_check(checked=True, podcast_text="Test script", user_session=self.user_session, browser_state=self.browser_state)
# Should discover a file and update browser state
assert result["interactive"] is False
assert result["variant"] == "secondary"
assert result["value"] == "音声生成済み"
# Browser state should be updated with one of the files
final_audio_path = self.browser_state["audio_generation_state"]["final_audio_path"]
assert final_audio_path in [str(audio_file1), str(audio_file2)]
assert self.browser_state["audio_generation_state"]["status"] == "completed"
class TestCreateProgressHTML:
"""Test _create_progress_html method."""
def setup_method(self):
"""Set up test fixtures."""
self.app = PaperPodcastApp()
def test_basic_progress_display(self):
"""Test basic progress display without completion."""
result = self.app._create_progress_html(current_part=3, total_parts=10, status_message="音声生成中...", is_completed=False)
assert "音声生成中..." in result
assert "パート 3/10" in result
assert "30.0%" in result
assert "🎵" in result
assert "✅" not in result
def test_completed_progress_display(self):
"""Test completed progress display."""
result = self.app._create_progress_html(current_part=10, total_parts=10, status_message="生成完了", is_completed=True)
assert "生成完了" in result
assert "パート 10/10" in result
assert "100%" in result
assert "✅" in result
assert "🎵" not in result
def test_progress_calculation_with_zero_total(self):
"""Test progress calculation when total_parts is 0."""
result = self.app._create_progress_html(current_part=0, total_parts=0, status_message="準備中...", is_completed=False)
assert "準備中..." in result
assert "パート 0/0" in result
assert "0%" in result
def test_progress_calculation_caps_at_95_percent(self):
"""Test that progress calculation caps at 95% when not completed."""
result = self.app._create_progress_html(current_part=100, total_parts=100, status_message="最終処理中...", is_completed=False)
assert "最終処理中..." in result
assert "パート 100/100" in result
assert "95%" in result
def test_time_calculation_with_start_time(self):
"""Test time calculation with start_time provided."""
import time
start_time = time.time() - 65 # 1 minute 5 seconds ago
result = self.app._create_progress_html(current_part=5, total_parts=10, status_message="音声生成中...", is_completed=False, start_time=start_time)
assert "経過: 01:05" in result
assert "推定残り:" in result
def test_time_calculation_when_completed(self):
"""Test time calculation when generation is completed."""
import time
start_time = time.time() - 120 # 2 minutes ago
result = self.app._create_progress_html(current_part=10, total_parts=10, status_message="生成完了", is_completed=True, start_time=start_time)
assert "完了時間: 02:00" in result
assert "推定残り:" not in result
def test_time_calculation_at_start(self):
"""Test time calculation when current_part is 0."""
import time
start_time = time.time() - 30 # 30 seconds ago
result = self.app._create_progress_html(current_part=0, total_parts=10, status_message="開始準備中...", is_completed=False, start_time=start_time)
assert "経過: 00:30" in result
assert "推定残り:" not in result
def test_resume_from_part_display(self):
"""Test display of resume from part information."""
result = self.app._create_progress_html(current_part=7, total_parts=10, status_message="再開中...", is_completed=False)
assert "再開中..." in result
# Resume functionality is handled elsewhere in the app, not in _create_progress_html
def test_resume_from_part_zero_not_displayed(self):
"""Test that resume from part 0 is not displayed."""
result = self.app._create_progress_html(current_part=3, total_parts=10, status_message="生成中...", is_completed=False)
assert "生成中..." in result
# Resume functionality is handled elsewhere in the app, not in _create_progress_html
def test_resume_from_part_none_not_displayed(self):
"""Test that resume from part None is not displayed."""
result = self.app._create_progress_html(current_part=3, total_parts=10, status_message="生成中...", is_completed=False)
assert "生成中..." in result
# Resume functionality is handled elsewhere in the app, not in _create_progress_html
def test_html_structure_contains_required_elements(self):
"""Test that HTML structure contains all required elements."""
result = self.app._create_progress_html(current_part=3, total_parts=10, status_message="音声生成中...", is_completed=False)
# Check for HTML structure
assert "<div" in result
assert "style=" in result
assert "background-color:" in result
assert "border-radius:" in result
assert "width:" in result
assert "height:" in result
def test_estimated_remaining_time_calculation(self):
"""Test estimated remaining time calculation accuracy."""
import time
start_time = time.time() - 60 # 1 minute ago
result = self.app._create_progress_html(current_part=2, total_parts=10, status_message="音声生成中...", is_completed=False, start_time=start_time)
# With 2 parts in 60 seconds, that's 30 seconds per part
# 8 remaining parts would be 240 seconds = 4 minutes
assert "推定残り: 04:00" in result
def test_edge_case_single_part_generation(self):
"""Test edge case with single part generation."""
result = self.app._create_progress_html(current_part=1, total_parts=1, status_message="音声生成中...", is_completed=False)
assert "音声生成中..." in result
assert "パート 1/1" in result
assert "95%" in result # Should cap at 95% when not completed
def test_edge_case_single_part_completed(self):
"""Test edge case with single part completed."""
result = self.app._create_progress_html(current_part=1, total_parts=1, status_message="生成完了", is_completed=True)
assert "生成完了" in result
assert "パート 1/1" in result
assert "100%" in result # Should be 100% when completed
def test_long_elapsed_time_formatting(self):
"""Test formatting of long elapsed times."""
import time
start_time = time.time() - 3665 # 61 minutes 5 seconds ago
result = self.app._create_progress_html(current_part=5, total_parts=10, status_message="音声生成中...", is_completed=False, start_time=start_time)
assert "経過: 61:05" in result