2
0

test_glossary_editor.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. """
  2. Tests for the Glossary Editor UI component (Story 7.19).
  3. """
  4. import pytest
  5. from pathlib import Path
  6. import tempfile
  7. import json
  8. from PyQt6.QtWidgets import QApplication
  9. from PyQt6.QtCore import Qt
  10. from src.glossary.models import Glossary, GlossaryEntry, TermCategory
  11. from src.ui.glossary_editor import (
  12. GlossaryTableModel,
  13. GlossaryEditor,
  14. EntryEditDialog,
  15. CATEGORY_NAMES,
  16. )
  17. @pytest.fixture
  18. def app(qtbot):
  19. """Create QApplication for tests."""
  20. return QApplication.instance() or QApplication([])
  21. @pytest.fixture
  22. def sample_glossary():
  23. """Create a sample glossary for testing."""
  24. glossary = Glossary()
  25. glossary.add(GlossaryEntry("林风", "Lin Feng", TermCategory.CHARACTER, "Main protagonist"))
  26. glossary.add(GlossaryEntry("火球术", "Fireball", TermCategory.SKILL, "Basic fire spell"))
  27. glossary.add(GlossaryEntry("东方大陆", "Eastern Continent", TermCategory.LOCATION, ""))
  28. glossary.add(GlossaryEntry("龙剑", "Dragon Sword", TermCategory.ITEM, "Legendary weapon"))
  29. return glossary
  30. @pytest.fixture
  31. def temp_json_file():
  32. """Create a temporary JSON file for testing."""
  33. with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
  34. data = [
  35. {
  36. "source": "测试术语",
  37. "target": "Test Term",
  38. "category": "other",
  39. "context": "Test context"
  40. }
  41. ]
  42. json.dump(data, f, ensure_ascii=False)
  43. return Path(f.name)
  44. class TestGlossaryTableModel:
  45. """Tests for GlossaryTableModel."""
  46. def test_initialization(self, app, sample_glossary):
  47. """Test model initialization."""
  48. model = GlossaryTableModel(sample_glossary)
  49. assert model.rowCount() == 4
  50. assert model.columnCount() == 4
  51. def test_data_retrieval(self, app, sample_glossary):
  52. """Test data retrieval from model."""
  53. model = GlossaryTableModel(sample_glossary)
  54. index = model.index(0, 0) # First row, source column
  55. assert model.data(index) == "林风"
  56. index = model.index(0, 1) # First row, target column
  57. assert model.data(index) == "Lin Feng"
  58. index = model.index(0, 2) # First row, category column
  59. assert model.data(index) == "人名"
  60. def test_category_filter(self, app, sample_glossary):
  61. """Test category filtering."""
  62. model = GlossaryTableModel(sample_glossary)
  63. # Filter by character
  64. model.set_category_filter(TermCategory.CHARACTER)
  65. assert model.rowCount() == 1
  66. assert model.filtered_count == 1
  67. assert model.entry_count == 4
  68. # Filter by skill
  69. model.set_category_filter(TermCategory.SKILL)
  70. assert model.rowCount() == 1
  71. # Clear filter
  72. model.set_category_filter(None)
  73. assert model.rowCount() == 4
  74. def test_search_filter(self, app, sample_glossary):
  75. """Test search filtering."""
  76. model = GlossaryTableModel(sample_glossary)
  77. # Search for "Lin"
  78. model.set_search_text("Lin")
  79. assert model.rowCount() == 2 # "Lin Feng" and "Dragon Sword"
  80. # Search for "火"
  81. model.set_search_text("火")
  82. assert model.rowCount() == 2 # "火球术" and "龙剑"
  83. # Clear search
  84. model.set_search_text("")
  85. assert model.rowCount() == 4
  86. def test_get_entry_at_row(self, app, sample_glossary):
  87. """Test getting entry at specific row."""
  88. model = GlossaryTableModel(sample_glossary)
  89. entry = model.get_entry_at_row(0)
  90. assert entry is not None
  91. assert entry.source == "林风"
  92. entry = model.get_entry_at_row(10)
  93. assert entry is None
  94. def test_refresh(self, app, sample_glossary):
  95. """Test model refresh."""
  96. model = GlossaryTableModel(sample_glossary)
  97. assert model.rowCount() == 4
  98. # Add new entry
  99. sample_glossary.add(GlossaryEntry("新术语", "New Term", TermCategory.OTHER))
  100. model.refresh()
  101. assert model.rowCount() == 5
  102. class TestGlossaryEntry:
  103. """Tests for GlossaryEntry validation."""
  104. def test_valid_entry(self):
  105. """Test creating a valid entry."""
  106. entry = GlossaryEntry("测试", "Test", TermCategory.OTHER)
  107. assert entry.source == "测试"
  108. assert entry.target == "Test"
  109. assert entry.category == TermCategory.OTHER
  110. def test_empty_source(self):
  111. """Test that empty source raises error."""
  112. with pytest.raises(ValueError):
  113. GlossaryEntry("", "Test", TermCategory.OTHER)
  114. def test_whitespace_only_source(self):
  115. """Test that whitespace-only source raises error."""
  116. with pytest.raises(ValueError):
  117. GlossaryEntry(" ", "Test", TermCategory.OTHER)
  118. def test_empty_target(self):
  119. """Test that empty target raises error."""
  120. with pytest.raises(ValueError):
  121. GlossaryEntry("测试", "", TermCategory.OTHER)
  122. def test_length_property(self):
  123. """Test the length property."""
  124. entry = GlossaryEntry("测试术语", "Test", TermCategory.OTHER)
  125. assert entry.length == 4
  126. @pytest.mark.skipif(not QApplication.instance(), reason="Requires QApplication")
  127. class TestGlossaryEditor:
  128. """Tests for GlossaryEditor widget."""
  129. def test_initialization(self, qtbot, sample_glossary):
  130. """Test editor initialization."""
  131. editor = GlossaryEditor(sample_glossary)
  132. qtbot.addWidget(editor)
  133. assert editor.glossary == sample_glossary
  134. # Editor should display all entries
  135. assert editor._model.entry_count == 4
  136. def test_add_entry(self, qtbot, sample_glossary):
  137. """Test adding a new entry."""
  138. editor = GlossaryEditor(sample_glossary)
  139. qtbot.addWidget(editor)
  140. initial_count = len(sample_glossary)
  141. # Add new entry
  142. new_entry = GlossaryEntry("魔法", "Magic", TermCategory.SKILL)
  143. sample_glossary.add(new_entry)
  144. editor.refresh_data()
  145. assert len(sample_glossary) == initial_count + 1
  146. def test_delete_entry(self, qtbot, sample_glossary):
  147. """Test deleting an entry."""
  148. editor = GlossaryEditor(sample_glossary)
  149. qtbot.addWidget(editor)
  150. initial_count = len(sample_glossary)
  151. # Remove entry
  152. sample_glossary.remove("林风")
  153. editor.refresh_data()
  154. assert len(sample_glossary) == initial_count - 1
  155. def test_set_glossary(self, qtbot, sample_glossary):
  156. """Test setting a new glossary."""
  157. editor = GlossaryEditor(sample_glossary)
  158. qtbot.addWidget(editor)
  159. new_glossary = Glossary()
  160. new_glossary.add(GlossaryEntry("测试", "Test", TermCategory.OTHER))
  161. editor.set_glossary(new_glossary)
  162. assert editor.glossary == new_glossary
  163. assert editor._model.entry_count == 1
  164. class TestGlossaryJson:
  165. """Tests for JSON import/export functionality."""
  166. def test_save_to_file(self, sample_glossary, tmp_path):
  167. """Test saving glossary to JSON file."""
  168. json_path = tmp_path / "test_glossary.json"
  169. sample_glossary.save_to_file(json_path)
  170. assert json_path.exists()
  171. # Verify content
  172. with open(json_path, "r", encoding="utf-8") as f:
  173. data = json.load(f)
  174. assert len(data) == 4
  175. assert data[0]["source"] == "林风"
  176. assert data[0]["target"] == "Lin Feng"
  177. assert data[0]["category"] == "character"
  178. def test_load_from_file(self, temp_json_file):
  179. """Test loading glossary from JSON file."""
  180. glossary = Glossary()
  181. glossary.load_from_file(temp_json_file)
  182. assert len(glossary) == 1
  183. entry = glossary.get("测试术语")
  184. assert entry is not None
  185. assert entry.target == "Test Term"
  186. assert entry.category == TermCategory.OTHER
  187. def test_load_nonexistent_file(self):
  188. """Test loading from non-existent file."""
  189. glossary = Glossary()
  190. with pytest.raises(FileNotFoundError):
  191. glossary.load_from_file(Path("/nonexistent/path.json"))
  192. def test_atomic_write(self, sample_glossary, tmp_path):
  193. """Test that save uses atomic write."""
  194. json_path = tmp_path / "test_glossary.json"
  195. # Save should be atomic
  196. sample_glossary.save_to_file(json_path)
  197. # File should exist and be complete
  198. assert json_path.exists()
  199. with open(json_path, "r", encoding="utf-8") as f:
  200. data = json.load(f)
  201. # Should have all entries
  202. assert len(data) == 4
  203. class TestCategoryNames:
  204. """Tests for category name mappings."""
  205. def test_all_categories_have_names(self):
  206. """Test that all categories have display names."""
  207. for category in TermCategory:
  208. assert category in CATEGORY_NAMES
  209. assert CATEGORY_NAMES[category]
  210. def test_chinese_names(self):
  211. """Test Chinese category names."""
  212. assert CATEGORY_NAMES[TermCategory.CHARACTER] == "人名"
  213. assert CATEGORY_NAMES[TermCategory.SKILL] == "技能"
  214. assert CATEGORY_NAMES[TermCategory.LOCATION] == "地名"
  215. assert CATEGORY_NAMES[TermCategory.ITEM] == "物品"
  216. assert CATEGORY_NAMES[TermCategory.ORGANIZATION] == "组织"
  217. assert CATEGORY_NAMES[TermCategory.OTHER] == "其他"