"""Tests for output formatters."""
import json
import pytest
from pathlib import Path
from src.data.storage import Storage
from src.models.verse import BookMetadata, TranslationOutput, VerseReference
from src.output.formatters import OutputFormatter
@pytest.fixture
def temp_data_dir(tmp_path: Path) -> Path:
"""Create temporary data directory with translations."""
(tmp_path / "output" / "john").mkdir(parents=True)
(tmp_path / "progress").mkdir(parents=True)
return tmp_path
@pytest.fixture
def storage_with_translations(temp_data_dir: Path) -> Storage:
"""Create storage with test translations."""
storage = Storage(temp_data_dir)
# Add translations for John chapter 1
for verse_num in range(1, 6):
ref = VerseReference(book="john", chapter=1, verse=verse_num)
output = TranslationOutput(
reference=ref,
text=f"ヨハネ第1章{verse_num}節の翻訳。",
model="test-model",
)
storage.save_translation(output)
# Add translations for John chapter 2
for verse_num in range(1, 4):
ref = VerseReference(book="john", chapter=2, verse=verse_num)
output = TranslationOutput(
reference=ref,
text=f"ヨハネ第2章{verse_num}節の翻訳。",
model="test-model",
)
storage.save_translation(output)
return storage
class TestOutputFormatter:
"""Tests for OutputFormatter."""
def test_export_chapter_json(
self, storage_with_translations: Storage, tmp_path: Path
) -> None:
"""Test exporting chapter as JSON."""
formatter = OutputFormatter(storage_with_translations)
output_dir = tmp_path / "exports"
path = formatter.export_chapter_json("john", 1, output_dir)
assert path.exists()
with path.open("r", encoding="utf-8") as f:
data = json.load(f)
assert data["book"] == "john"
assert data["chapter"] == 1
assert len(data["verses"]) == 5
def test_export_chapter_text(
self, storage_with_translations: Storage, tmp_path: Path
) -> None:
"""Test exporting chapter as plain text."""
formatter = OutputFormatter(storage_with_translations)
output_dir = tmp_path / "exports"
path = formatter.export_chapter_text("john", 1, output_dir)
assert path.exists()
content = path.read_text(encoding="utf-8")
assert "John Chapter 1" in content
assert "ヨハネ第1章1節の翻訳。" in content
def test_export_chapter_html(
self, storage_with_translations: Storage, tmp_path: Path
) -> None:
"""Test exporting chapter as HTML."""
formatter = OutputFormatter(storage_with_translations)
output_dir = tmp_path / "exports"
path = formatter.export_chapter_html("john", 1, output_dir)
assert path.exists()
content = path.read_text(encoding="utf-8")
assert "" in content
assert '' in content
assert "ヨハネ第1章1節の翻訳。" in content
def test_export_chapter_html_with_metadata(
self, storage_with_translations: Storage, tmp_path: Path
) -> None:
"""Test exporting chapter HTML with metadata."""
formatter = OutputFormatter(storage_with_translations)
output_dir = tmp_path / "exports"
metadata = BookMetadata(
book="john",
name_japanese="ヨハネによる福音書",
name_english="Gospel of John",
testament="NT",
author="John",
themes=[],
description="Test",
chapter_count=21,
verse_count=879,
)
path = formatter.export_chapter_html("john", 1, output_dir, metadata)
content = path.read_text(encoding="utf-8")
assert "ヨハネによる福音書" in content
assert "第1章" in content
def test_export_book_json(
self, storage_with_translations: Storage, tmp_path: Path
) -> None:
"""Test exporting entire book as JSON."""
formatter = OutputFormatter(storage_with_translations)
output_dir = tmp_path / "exports"
path = formatter.export_book_json("john", output_dir)
assert path.exists()
with path.open("r", encoding="utf-8") as f:
data = json.load(f)
assert data["book"] == "john"
assert "1" in data["chapters"]
assert "2" in data["chapters"]
def test_export_book_text(
self, storage_with_translations: Storage, tmp_path: Path
) -> None:
"""Test exporting entire book as plain text."""
formatter = OutputFormatter(storage_with_translations)
output_dir = tmp_path / "exports"
path = formatter.export_book_text("john", output_dir)
assert path.exists()
content = path.read_text(encoding="utf-8")
assert "John" in content
assert "Chapter 1" in content
assert "Chapter 2" in content
def test_export_book_json_empty(
self, temp_data_dir: Path, tmp_path: Path
) -> None:
"""Test exporting book with no translations."""
storage = Storage(temp_data_dir)
formatter = OutputFormatter(storage)
output_dir = tmp_path / "exports"
path = formatter.export_book_json("matthew", output_dir)
assert path.exists()
with path.open("r", encoding="utf-8") as f:
data = json.load(f)
assert data["book"] == "matthew"
assert data["chapters"] == {}
def test_export_book_text_empty(
self, temp_data_dir: Path, tmp_path: Path
) -> None:
"""Test exporting empty book as text."""
storage = Storage(temp_data_dir)
formatter = OutputFormatter(storage)
output_dir = tmp_path / "exports"
path = formatter.export_book_text("matthew", output_dir)
assert path.exists()
content = path.read_text(encoding="utf-8")
assert "Matthew" in content
def test_export_creates_directory(
self, storage_with_translations: Storage, tmp_path: Path
) -> None:
"""Test export creates output directory if needed."""
formatter = OutputFormatter(storage_with_translations)
output_dir = tmp_path / "nested" / "deep" / "exports"
path = formatter.export_chapter_json("john", 1, output_dir)
assert path.exists()
assert output_dir.exists()
def test_export_chapter_html_with_furigana(
self, temp_data_dir: Path, tmp_path: Path
) -> None:
"""Test exporting chapter HTML includes ruby tags for furigana."""
storage = Storage(temp_data_dir)
# Create translation with furigana
ref = VerseReference(book="john", chapter=1, verse=1)
output = TranslationOutput(
reference=ref,
text="神は愛です",
text_with_furigana="神[かみ]は愛[あい]です",
furigana_annotations=[
{
"start_offset": 0,
"end_offset": 1,
"base_text": "神",
"reading": "かみ",
},
{
"start_offset": 2,
"end_offset": 3,
"base_text": "愛",
"reading": "あい",
},
],
model="test-model",
)
storage.save_translation(output)
formatter = OutputFormatter(storage)
output_dir = tmp_path / "exports"
path = formatter.export_chapter_html("john", 1, output_dir)
content = path.read_text(encoding="utf-8")
# Check ruby tags are present
assert "神" in content
assert "愛" in content
def test_export_chapter_html_includes_ruby_css(
self, storage_with_translations: Storage, tmp_path: Path
) -> None:
"""Test HTML export includes CSS for ruby styling."""
formatter = OutputFormatter(storage_with_translations)
output_dir = tmp_path / "exports"
path = formatter.export_chapter_html("john", 1, output_dir)
content = path.read_text(encoding="utf-8")
assert "ruby { ruby-position: over; }" in content
assert "rt { font-size: 0.6em; color: #666; }" in content
def test_export_chapter_html_fallback_without_furigana(
self, storage_with_translations: Storage, tmp_path: Path
) -> None:
"""Test HTML export falls back to plain text without furigana."""
formatter = OutputFormatter(storage_with_translations)
output_dir = tmp_path / "exports"
path = formatter.export_chapter_html("john", 1, output_dir)
content = path.read_text(encoding="utf-8")
# Original test translations don't have furigana, should use plain text
assert "ヨハネ第1章1節の翻訳。" in content
# Should not have any ruby tags for these translations
assert "" not in content or "ヨハネ" not in content.split("")[0]