"""Tests for the TUI module.""" from __future__ import annotations import json from datetime import datetime from pathlib import Path from unittest.mock import MagicMock, patch import pytest from rich.text import Text from src.models.evaluation import ( ChapterEvaluation, EvaluationScore, VerseEvaluation, ) from src.tui.data_provider import TUIDataProvider, VerseData from src.tui.widgets import ( DetailPanel, EvaluationSummaryTable, VerseSelected, VerseTree, ) @pytest.fixture def temp_data_dir(tmp_path: Path) -> Path: """Create a temporary data directory with test data.""" # Create source directories sources_dir = tmp_path / "sources" (sources_dir / "greek").mkdir(parents=True) (sources_dir / "english" / "kjv").mkdir(parents=True) (sources_dir / "japanese" / "kougo").mkdir(parents=True) # Create Greek source greek_data = {"1:1": {"text": "Παῦλος δοῦλος θεοῦ", "translit": "Paulos doulos theou"}} with (sources_dir / "greek" / "titus.json").open("w") as f: json.dump(greek_data, f) # Create KJV source kjv_data = {"1:1": "Paul, a servant of God, and an apostle of Jesus Christ"} with (sources_dir / "english" / "kjv" / "titus.json").open("w") as f: json.dump(kjv_data, f) # Create Kougo source kougo_data = {"1:1": "神のしもべ、イエス・キリストの使徒パウロから"} with (sources_dir / "japanese" / "kougo" / "titus.json").open("w") as f: json.dump(kougo_data, f) # Create output directory with translations output_dir = tmp_path / "output" / "titus" output_dir.mkdir(parents=True) chapter_data = { "book": "titus", "chapter": 1, "verses": { "1": { "text": "神の僕、イエス・キリストの使徒パウロから。", "model": "claude-opus-4-5", "timestamp": "2026-01-13T00:00:00Z", "glossary_terms_applied": ["θεός"], }, "2": { "text": "この信仰と知識は、永遠の命の希望に基づいています。", "model": "claude-opus-4-5", "timestamp": "2026-01-13T00:00:00Z", "glossary_terms_applied": [], }, }, } with (output_dir / "chapter_01.json").open("w") as f: json.dump(chapter_data, f) # Create evaluation file eval_data = { "book": "titus", "chapter": 1, "evaluator_model": "claude-opus-4-5", "evaluated_at": "2026-01-13T00:00:00Z", "verses": { "1": { "scores": { "theological_accuracy": { "score": 95, "confidence": 90, "reasoning": "Excellent", }, "source_fidelity": { "score": 88, "confidence": 85, "reasoning": "Good", }, "naturalness": { "score": 92, "confidence": 95, "reasoning": "Very natural", }, "readability": { "score": 90, "confidence": 92, "reasoning": "Easy to read", }, "glossary_consistency": { "score": 100, "confidence": 100, "reasoning": "Perfect", }, "literary_quality": { "score": 85, "confidence": 80, "reasoning": "Good quality", }, }, "overall_score": 91.7, "flags": [], "notes": None, "timestamp": "2026-01-13T00:00:00Z", }, "2": { "scores": { "theological_accuracy": { "score": 70, "confidence": 65, "reasoning": "Some concerns", }, "source_fidelity": { "score": 65, "confidence": 60, "reasoning": "Could be better", }, "naturalness": { "score": 80, "confidence": 85, "reasoning": "OK", }, "readability": { "score": 75, "confidence": 80, "reasoning": "Average", }, "glossary_consistency": { "score": 90, "confidence": 95, "reasoning": "Good", }, "literary_quality": { "score": 70, "confidence": 75, "reasoning": "Could improve", }, }, "overall_score": 75.0, "flags": ["theological_concern", "source_fidelity_concern"], "notes": "Needs review", "timestamp": "2026-01-13T00:00:00Z", }, }, "aggregate": { "scores": { "theological_accuracy": 82.5, "source_fidelity": 76.5, }, "overall": 83.3, "flagged_count": 1, "flagged_verses": [2], }, } with (output_dir / "chapter_01_eval.json").open("w") as f: json.dump(eval_data, f) # Create metadata directory metadata_dir = tmp_path / "metadata" metadata_dir.mkdir(parents=True) return tmp_path class TestTUIDataProvider: """Tests for TUIDataProvider.""" def test_get_available_books(self, temp_data_dir: Path) -> None: """Test getting available books.""" provider = TUIDataProvider(temp_data_dir) books = provider.get_available_books() assert books == ["titus"] def test_get_available_books_empty(self, tmp_path: Path) -> None: """Test getting available books with no data.""" provider = TUIDataProvider(tmp_path) books = provider.get_available_books() assert books == [] def test_get_book_chapters(self, temp_data_dir: Path) -> None: """Test getting chapters for a book.""" provider = TUIDataProvider(temp_data_dir) chapters = provider.get_book_chapters("titus") assert chapters == [1] def test_get_book_chapters_nonexistent(self, temp_data_dir: Path) -> None: """Test getting chapters for a nonexistent book.""" provider = TUIDataProvider(temp_data_dir) chapters = provider.get_book_chapters("genesis") assert chapters == [] def test_get_chapter_verses(self, temp_data_dir: Path) -> None: """Test getting verses for a chapter.""" provider = TUIDataProvider(temp_data_dir) verses = provider.get_chapter_verses("titus", 1) assert verses == [1, 2] def test_get_chapter_verses_nonexistent(self, temp_data_dir: Path) -> None: """Test getting verses for a nonexistent chapter.""" provider = TUIDataProvider(temp_data_dir) verses = provider.get_chapter_verses("titus", 99) assert verses == [] def test_get_flagged_verses(self, temp_data_dir: Path) -> None: """Test getting flagged verses.""" provider = TUIDataProvider(temp_data_dir) flagged = provider.get_flagged_verses("titus", 1) assert flagged == {2} def test_get_flagged_verses_no_eval(self, temp_data_dir: Path) -> None: """Test getting flagged verses when no evaluation exists.""" provider = TUIDataProvider(temp_data_dir) flagged = provider.get_flagged_verses("titus", 99) assert flagged == set() def test_is_chapter_evaluated(self, temp_data_dir: Path) -> None: """Test checking if chapter is evaluated.""" provider = TUIDataProvider(temp_data_dir) assert provider.is_chapter_evaluated("titus", 1) is True assert provider.is_chapter_evaluated("titus", 99) is False def test_get_verse_data(self, temp_data_dir: Path) -> None: """Test getting complete verse data.""" provider = TUIDataProvider(temp_data_dir) verse_data = provider.get_verse_data("titus", 1, 1) assert verse_data.book == "titus" assert verse_data.chapter == 1 assert verse_data.verse == 1 assert verse_data.greek_text == "Παῦλος δοῦλος θεοῦ" assert verse_data.kjv_text == "Paul, a servant of God, and an apostle of Jesus Christ" assert verse_data.japanese_reference == "神のしもべ、イエス・キリストの使徒パウロから" assert verse_data.glossary_terms == ["θεός"] assert verse_data.translation_text == "神の僕、イエス・キリストの使徒パウロから。" assert verse_data.translation_model == "claude-opus-4-5" assert verse_data.evaluation is not None assert verse_data.evaluation.overall_score == pytest.approx(91.7, rel=0.1) def test_get_verse_data_no_translation(self, temp_data_dir: Path) -> None: """Test getting verse data when translation doesn't exist.""" provider = TUIDataProvider(temp_data_dir) # Verse 3 doesn't exist in translations verse_data = provider.get_verse_data("titus", 1, 3) assert verse_data.translation_text is None assert verse_data.translation_model is None assert verse_data.evaluation is None class TestVerseSelected: """Tests for VerseSelected message.""" def test_verse_selected_message(self) -> None: """Test creating a verse selected message.""" msg = VerseSelected("titus", 1, 5) assert msg.book == "titus" assert msg.chapter == 1 assert msg.verse == 5 class TestDetailPanel: """Tests for DetailPanel widget.""" def _render_to_string(self, panel: DetailPanel) -> str: """Render panel content to a string for testing.""" from io import StringIO from rich.console import Console rendered = panel.render() console = Console(file=StringIO(), force_terminal=True) console.print(rendered) return console.file.getvalue() def test_render_no_verse(self, temp_data_dir: Path) -> None: """Test rendering with no verse selected.""" provider = TUIDataProvider(temp_data_dir) panel = DetailPanel(provider, id="detail") rendered_str = self._render_to_string(panel) # Should show placeholder message assert "Select a verse" in rendered_str def test_render_with_verse(self, temp_data_dir: Path) -> None: """Test rendering with a verse selected.""" provider = TUIDataProvider(temp_data_dir) panel = DetailPanel(provider, id="detail") panel.update_verse("titus", 1, 1) rendered_str = self._render_to_string(panel) # Check that key elements are present assert "TITUS 1:1" in rendered_str assert "SOURCE TEXTS" in rendered_str assert "TRANSLATION" in rendered_str assert "EVALUATION" in rendered_str def test_render_with_flagged_verse(self, temp_data_dir: Path) -> None: """Test rendering a flagged verse.""" provider = TUIDataProvider(temp_data_dir) panel = DetailPanel(provider, id="detail") panel.update_verse("titus", 1, 2) rendered_str = self._render_to_string(panel) # Should show flags assert "theological_concern" in rendered_str or "Flags" in rendered_str def test_render_verse_without_evaluation(self, temp_data_dir: Path) -> None: """Test rendering a verse without evaluation.""" provider = TUIDataProvider(temp_data_dir) panel = DetailPanel(provider, id="detail") # Create a verse data without evaluation panel.current_verse = VerseData( book="titus", chapter=1, verse=99, greek_text=None, kjv_text=None, japanese_reference=None, glossary_terms=[], translation_text=None, translation_model=None, translation_timestamp=None, evaluation=None, ) rendered_str = self._render_to_string(panel) assert "not evaluated" in rendered_str class TestEvaluationSummaryTable: """Tests for EvaluationSummaryTable widget.""" def _render_to_string(self, widget: EvaluationSummaryTable) -> str: """Render widget content to a string for testing.""" from io import StringIO from rich.console import Console rendered = widget.render() console = Console(file=StringIO(), force_terminal=True) console.print(rendered) return console.file.getvalue() def test_render_no_scores(self) -> None: """Test rendering with no scores.""" table = EvaluationSummaryTable(id="eval-table") rendered = table.render() assert isinstance(rendered, Text) rendered_str = self._render_to_string(table) assert "No evaluation" in rendered_str def test_render_with_scores(self) -> None: """Test rendering with scores.""" table = EvaluationSummaryTable(id="eval-table") table.set_scores({ "theological_accuracy": (95.0, 90.0), "source_fidelity": (70.0, 85.0), "naturalness": (55.0, 80.0), }) rendered_str = self._render_to_string(table) # Should contain dimension name (abbreviated names like "Theology" are also acceptable) assert "Theolog" in rendered_str or "theological" in rendered_str.lower() class TestVerseTree: """Tests for VerseTree widget.""" def test_tree_initialization(self, temp_data_dir: Path) -> None: """Test tree widget initialization.""" provider = TUIDataProvider(temp_data_dir) tree = VerseTree(provider, id="tree") assert tree.data_provider == provider def test_tree_populate(self, temp_data_dir: Path) -> None: """Test tree population.""" provider = TUIDataProvider(temp_data_dir) tree = VerseTree(provider, id="tree") tree._populate_tree() # Check that the tree has nodes assert len(list(tree.root.children)) > 0 class TestTranslationViewerApp: """Tests for TranslationViewerApp.""" def test_app_initialization(self, temp_data_dir: Path) -> None: """Test app initialization.""" from src.tui.app import TranslationViewerApp app = TranslationViewerApp(data_dir=temp_data_dir) assert app.data_dir == temp_data_dir assert app.data_provider is not None def test_app_has_bindings(self, temp_data_dir: Path) -> None: """Test app has expected bindings.""" from src.tui.app import TranslationViewerApp binding_keys = [b.key for b in TranslationViewerApp.BINDINGS] assert "q" in binding_keys assert "d" in binding_keys assert "r" in binding_keys def test_app_css_defined(self, temp_data_dir: Path) -> None: """Test app has CSS defined.""" from src.tui.app import TranslationViewerApp app = TranslationViewerApp(data_dir=temp_data_dir) assert app.CSS is not None assert len(app.CSS) > 0 class TestMainTuiCommand: """Tests for the TUI command in main.py.""" def test_run_tui_callable(self) -> None: """Test run_tui is callable.""" from src.tui.app import run_tui assert callable(run_tui) def test_tui_cmd_import(self) -> None: """Test tui_cmd can be imported from main.""" from main import tui_cmd assert callable(tui_cmd) class TestDataProviderEdgeCases: """Tests for edge cases in TUIDataProvider.""" def test_get_book_chapters_invalid_filename(self, tmp_path: Path) -> None: """Test handling of invalid chapter filenames.""" output_dir = tmp_path / "output" / "testbook" output_dir.mkdir(parents=True) # Create a file with invalid name that can't be parsed (output_dir / "chapter_invalid.json").write_text("{}") (output_dir / "chapter_.json").write_text("{}") # No number part provider = TUIDataProvider(tmp_path) chapters = provider.get_book_chapters("testbook") # Should return empty list since no valid chapter numbers assert chapters == [] class TestWidgetsEdgeCases: """Tests for edge cases in widgets.""" def test_verse_tree_on_mount(self, temp_data_dir: Path) -> None: """Test the on_mount method populates the tree.""" provider = TUIDataProvider(temp_data_dir) tree = VerseTree(provider, id="tree") # Manually call on_mount tree.on_mount() # Tree should be populated assert len(list(tree.root.children)) > 0 class TestAppMethods: """Tests for TranslationViewerApp methods.""" def test_app_compose_is_generator(self, temp_data_dir: Path) -> None: """Test that compose is a generator method.""" from src.tui.app import TranslationViewerApp app = TranslationViewerApp(data_dir=temp_data_dir) compose_gen = app.compose() # compose() returns a generator import types assert isinstance(compose_gen, types.GeneratorType) def test_app_title_and_css(self, temp_data_dir: Path) -> None: """Test app has title and CSS defined.""" from src.tui.app import TranslationViewerApp assert TranslationViewerApp.TITLE == "Japanese Bible Translator Viewer" assert TranslationViewerApp.CSS is not None assert "#tree-container" in TranslationViewerApp.CSS class TestDetailPanelWithTranslation: """Additional tests for DetailPanel rendering.""" def _render_to_string(self, panel: DetailPanel) -> str: """Render panel content to a string for testing.""" from io import StringIO from rich.console import Console rendered = panel.render() console = Console(file=StringIO(), force_terminal=True) console.print(rendered) return console.file.getvalue() def test_render_verse_with_notes(self, temp_data_dir: Path) -> None: """Test rendering a verse with notes.""" provider = TUIDataProvider(temp_data_dir) panel = DetailPanel(provider, id="detail") # Create verse data with notes panel.current_verse = VerseData( book="titus", chapter=1, verse=2, greek_text="Test Greek", kjv_text="Test KJV", japanese_reference="Test Japanese", glossary_terms=["term1", "term2"], translation_text="Test translation", translation_model="test-model", translation_timestamp="2026-01-13", evaluation=VerseEvaluation( verse_number=2, evaluator_model="test-model", timestamp=datetime.utcnow(), scores={ "theological_accuracy": EvaluationScore( dimension="theological_accuracy", score=85.0, confidence=90.0, reasoning="Good", ), }, flags=["test_flag"], notes="This is a test note", ), ) rendered_str = self._render_to_string(panel) assert "test note" in rendered_str.lower() or "Notes" in rendered_str