""" Tests for the Glossary Editor UI component (Story 7.19). """ import pytest from pathlib import Path import tempfile import json from PyQt6.QtWidgets import QApplication from PyQt6.QtCore import Qt from src.glossary.models import Glossary, GlossaryEntry, TermCategory from src.ui.glossary_editor import ( GlossaryTableModel, GlossaryEditor, EntryEditDialog, CATEGORY_NAMES, ) @pytest.fixture def app(qtbot): """Create QApplication for tests.""" return QApplication.instance() or QApplication([]) @pytest.fixture def sample_glossary(): """Create a sample glossary for testing.""" glossary = Glossary() glossary.add(GlossaryEntry("林风", "Lin Feng", TermCategory.CHARACTER, "Main protagonist")) glossary.add(GlossaryEntry("火球术", "Fireball", TermCategory.SKILL, "Basic fire spell")) glossary.add(GlossaryEntry("东方大陆", "Eastern Continent", TermCategory.LOCATION, "")) glossary.add(GlossaryEntry("龙剑", "Dragon Sword", TermCategory.ITEM, "Legendary weapon")) return glossary @pytest.fixture def temp_json_file(): """Create a temporary JSON file for testing.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: data = [ { "source": "测试术语", "target": "Test Term", "category": "other", "context": "Test context" } ] json.dump(data, f, ensure_ascii=False) return Path(f.name) class TestGlossaryTableModel: """Tests for GlossaryTableModel.""" def test_initialization(self, app, sample_glossary): """Test model initialization.""" model = GlossaryTableModel(sample_glossary) assert model.rowCount() == 4 assert model.columnCount() == 4 def test_data_retrieval(self, app, sample_glossary): """Test data retrieval from model.""" model = GlossaryTableModel(sample_glossary) index = model.index(0, 0) # First row, source column assert model.data(index) == "林风" index = model.index(0, 1) # First row, target column assert model.data(index) == "Lin Feng" index = model.index(0, 2) # First row, category column assert model.data(index) == "人名" def test_category_filter(self, app, sample_glossary): """Test category filtering.""" model = GlossaryTableModel(sample_glossary) # Filter by character model.set_category_filter(TermCategory.CHARACTER) assert model.rowCount() == 1 assert model.filtered_count == 1 assert model.entry_count == 4 # Filter by skill model.set_category_filter(TermCategory.SKILL) assert model.rowCount() == 1 # Clear filter model.set_category_filter(None) assert model.rowCount() == 4 def test_search_filter(self, app, sample_glossary): """Test search filtering.""" model = GlossaryTableModel(sample_glossary) # Search for "Lin" model.set_search_text("Lin") assert model.rowCount() == 2 # "Lin Feng" and "Dragon Sword" # Search for "火" model.set_search_text("火") assert model.rowCount() == 2 # "火球术" and "龙剑" # Clear search model.set_search_text("") assert model.rowCount() == 4 def test_get_entry_at_row(self, app, sample_glossary): """Test getting entry at specific row.""" model = GlossaryTableModel(sample_glossary) entry = model.get_entry_at_row(0) assert entry is not None assert entry.source == "林风" entry = model.get_entry_at_row(10) assert entry is None def test_refresh(self, app, sample_glossary): """Test model refresh.""" model = GlossaryTableModel(sample_glossary) assert model.rowCount() == 4 # Add new entry sample_glossary.add(GlossaryEntry("新术语", "New Term", TermCategory.OTHER)) model.refresh() assert model.rowCount() == 5 class TestGlossaryEntry: """Tests for GlossaryEntry validation.""" def test_valid_entry(self): """Test creating a valid entry.""" entry = GlossaryEntry("测试", "Test", TermCategory.OTHER) assert entry.source == "测试" assert entry.target == "Test" assert entry.category == TermCategory.OTHER def test_empty_source(self): """Test that empty source raises error.""" with pytest.raises(ValueError): GlossaryEntry("", "Test", TermCategory.OTHER) def test_whitespace_only_source(self): """Test that whitespace-only source raises error.""" with pytest.raises(ValueError): GlossaryEntry(" ", "Test", TermCategory.OTHER) def test_empty_target(self): """Test that empty target raises error.""" with pytest.raises(ValueError): GlossaryEntry("测试", "", TermCategory.OTHER) def test_length_property(self): """Test the length property.""" entry = GlossaryEntry("测试术语", "Test", TermCategory.OTHER) assert entry.length == 4 @pytest.mark.skipif(not QApplication.instance(), reason="Requires QApplication") class TestGlossaryEditor: """Tests for GlossaryEditor widget.""" def test_initialization(self, qtbot, sample_glossary): """Test editor initialization.""" editor = GlossaryEditor(sample_glossary) qtbot.addWidget(editor) assert editor.glossary == sample_glossary # Editor should display all entries assert editor._model.entry_count == 4 def test_add_entry(self, qtbot, sample_glossary): """Test adding a new entry.""" editor = GlossaryEditor(sample_glossary) qtbot.addWidget(editor) initial_count = len(sample_glossary) # Add new entry new_entry = GlossaryEntry("魔法", "Magic", TermCategory.SKILL) sample_glossary.add(new_entry) editor.refresh_data() assert len(sample_glossary) == initial_count + 1 def test_delete_entry(self, qtbot, sample_glossary): """Test deleting an entry.""" editor = GlossaryEditor(sample_glossary) qtbot.addWidget(editor) initial_count = len(sample_glossary) # Remove entry sample_glossary.remove("林风") editor.refresh_data() assert len(sample_glossary) == initial_count - 1 def test_set_glossary(self, qtbot, sample_glossary): """Test setting a new glossary.""" editor = GlossaryEditor(sample_glossary) qtbot.addWidget(editor) new_glossary = Glossary() new_glossary.add(GlossaryEntry("测试", "Test", TermCategory.OTHER)) editor.set_glossary(new_glossary) assert editor.glossary == new_glossary assert editor._model.entry_count == 1 class TestGlossaryJson: """Tests for JSON import/export functionality.""" def test_save_to_file(self, sample_glossary, tmp_path): """Test saving glossary to JSON file.""" json_path = tmp_path / "test_glossary.json" sample_glossary.save_to_file(json_path) assert json_path.exists() # Verify content with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) assert len(data) == 4 assert data[0]["source"] == "林风" assert data[0]["target"] == "Lin Feng" assert data[0]["category"] == "character" def test_load_from_file(self, temp_json_file): """Test loading glossary from JSON file.""" glossary = Glossary() glossary.load_from_file(temp_json_file) assert len(glossary) == 1 entry = glossary.get("测试术语") assert entry is not None assert entry.target == "Test Term" assert entry.category == TermCategory.OTHER def test_load_nonexistent_file(self): """Test loading from non-existent file.""" glossary = Glossary() with pytest.raises(FileNotFoundError): glossary.load_from_file(Path("/nonexistent/path.json")) def test_atomic_write(self, sample_glossary, tmp_path): """Test that save uses atomic write.""" json_path = tmp_path / "test_glossary.json" # Save should be atomic sample_glossary.save_to_file(json_path) # File should exist and be complete assert json_path.exists() with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) # Should have all entries assert len(data) == 4 class TestCategoryNames: """Tests for category name mappings.""" def test_all_categories_have_names(self): """Test that all categories have display names.""" for category in TermCategory: assert category in CATEGORY_NAMES assert CATEGORY_NAMES[category] def test_chinese_names(self): """Test Chinese category names.""" assert CATEGORY_NAMES[TermCategory.CHARACTER] == "人名" assert CATEGORY_NAMES[TermCategory.SKILL] == "技能" assert CATEGORY_NAMES[TermCategory.LOCATION] == "地名" assert CATEGORY_NAMES[TermCategory.ITEM] == "物品" assert CATEGORY_NAMES[TermCategory.ORGANIZATION] == "组织" assert CATEGORY_NAMES[TermCategory.OTHER] == "其他"