Vocabulary study with FSRS spaced repetition, AI tutoring (Ollama/Claude), essay marking, idioms browser, Anki export, and dashboard. 918 vocabulary entries across 39 categories. 41 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
205 lines
6.0 KiB
Python
205 lines
6.0 KiB
Python
"""Tests for modules/vocab.py — vocabulary search and flashcard logic."""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
SAMPLE_VOCAB = [
|
|
{
|
|
"id": "verb_go",
|
|
"section": "High-frequency language",
|
|
"category": "Common verbs",
|
|
"english": "to go",
|
|
"persian": "رفتن",
|
|
"finglish": "raftan",
|
|
},
|
|
{
|
|
"id": "verb_eat",
|
|
"section": "High-frequency language",
|
|
"category": "Common verbs",
|
|
"english": "to eat",
|
|
"persian": "خوردن",
|
|
"finglish": "khordan",
|
|
},
|
|
{
|
|
"id": "adj_big",
|
|
"section": "High-frequency language",
|
|
"category": "Common adjectives",
|
|
"english": "big",
|
|
"persian": "بزرگ",
|
|
"finglish": "bozorg",
|
|
},
|
|
{
|
|
"id": "colour_red",
|
|
"section": "High-frequency language",
|
|
"category": "Colours",
|
|
"english": "red",
|
|
"persian": "قرمز",
|
|
"finglish": "ghermez",
|
|
},
|
|
]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_vocab_and_db(tmp_path):
|
|
"""Mock vocabulary loading and use temp DB."""
|
|
import db as db_mod
|
|
import modules.vocab as vocab_mod
|
|
|
|
# Temp DB
|
|
db_mod._conn = None
|
|
db_mod.DB_PATH = tmp_path / "test.db"
|
|
db_mod.init_db()
|
|
|
|
# Mock vocab
|
|
vocab_mod._vocab_data = SAMPLE_VOCAB
|
|
|
|
yield vocab_mod
|
|
|
|
db_mod.close()
|
|
vocab_mod._vocab_data = None
|
|
|
|
|
|
def test_load_vocab(mock_vocab_and_db):
|
|
"""load_vocab should return the vocabulary data."""
|
|
data = mock_vocab_and_db.load_vocab()
|
|
assert len(data) == 4
|
|
|
|
|
|
def test_get_categories(mock_vocab_and_db):
|
|
"""get_categories should return unique sorted categories."""
|
|
cats = mock_vocab_and_db.get_categories()
|
|
assert "Colours" in cats
|
|
assert "Common verbs" in cats
|
|
assert "Common adjectives" in cats
|
|
|
|
|
|
def test_search_english(mock_vocab_and_db):
|
|
"""Search should find entries by English text."""
|
|
results = mock_vocab_and_db.search("go")
|
|
assert len(results) == 1
|
|
assert results[0]["id"] == "verb_go"
|
|
|
|
|
|
def test_search_persian(mock_vocab_and_db):
|
|
"""Search should find entries by Persian text."""
|
|
results = mock_vocab_and_db.search("رفتن")
|
|
assert len(results) == 1
|
|
assert results[0]["id"] == "verb_go"
|
|
|
|
|
|
def test_search_finglish(mock_vocab_and_db):
|
|
"""Search should find entries by Finglish text."""
|
|
results = mock_vocab_and_db.search("raftan")
|
|
assert len(results) == 1
|
|
assert results[0]["id"] == "verb_go"
|
|
|
|
|
|
def test_search_empty(mock_vocab_and_db):
|
|
"""Empty search should return empty list."""
|
|
assert mock_vocab_and_db.search("") == []
|
|
assert mock_vocab_and_db.search(None) == []
|
|
|
|
|
|
def test_search_no_match(mock_vocab_and_db):
|
|
"""Search with no match should return empty list."""
|
|
assert mock_vocab_and_db.search("zzzzz") == []
|
|
|
|
|
|
def test_get_random_word(mock_vocab_and_db):
|
|
"""get_random_word should return a valid entry."""
|
|
word = mock_vocab_and_db.get_random_word()
|
|
assert word is not None
|
|
assert "id" in word
|
|
assert "english" in word
|
|
assert "persian" in word
|
|
|
|
|
|
def test_get_random_word_with_category(mock_vocab_and_db):
|
|
"""get_random_word with category filter should only return matching entries."""
|
|
word = mock_vocab_and_db.get_random_word(category="Colours")
|
|
assert word is not None
|
|
assert word["category"] == "Colours"
|
|
|
|
|
|
def test_get_random_word_nonexistent_category(mock_vocab_and_db):
|
|
"""get_random_word with bad category should return None."""
|
|
word = mock_vocab_and_db.get_random_word(category="Nonexistent")
|
|
assert word is None
|
|
|
|
|
|
def test_check_answer_correct_en_to_fa(mock_vocab_and_db):
|
|
"""Correct Persian answer should be marked correct."""
|
|
correct, answer, entry = mock_vocab_and_db.check_answer(
|
|
"verb_go", "رفتن", direction="en_to_fa"
|
|
)
|
|
assert correct is True
|
|
|
|
|
|
def test_check_answer_incorrect_en_to_fa(mock_vocab_and_db):
|
|
"""Incorrect Persian answer should be marked incorrect with correct answer."""
|
|
correct, answer, entry = mock_vocab_and_db.check_answer(
|
|
"verb_go", "خوردن", direction="en_to_fa"
|
|
)
|
|
assert correct is False
|
|
assert answer == "رفتن"
|
|
|
|
|
|
def test_check_answer_fa_to_en(mock_vocab_and_db):
|
|
"""Correct English answer (case-insensitive) should be marked correct."""
|
|
correct, answer, entry = mock_vocab_and_db.check_answer(
|
|
"verb_go", "To Go", direction="fa_to_en"
|
|
)
|
|
assert correct is True
|
|
|
|
|
|
def test_check_answer_nonexistent_word(mock_vocab_and_db):
|
|
"""Checking answer for nonexistent word should return False."""
|
|
correct, answer, entry = mock_vocab_and_db.check_answer(
|
|
"nonexistent", "test", direction="en_to_fa"
|
|
)
|
|
assert correct is False
|
|
assert entry is None
|
|
|
|
|
|
def test_format_word_card(mock_vocab_and_db):
|
|
"""format_word_card should produce RTL HTML with correct content."""
|
|
entry = SAMPLE_VOCAB[0]
|
|
html = mock_vocab_and_db.format_word_card(entry, show_transliteration="Finglish")
|
|
assert "رفتن" in html
|
|
assert "to go" in html
|
|
assert "raftan" in html
|
|
|
|
|
|
def test_format_word_card_no_transliteration(mock_vocab_and_db):
|
|
"""format_word_card with transliteration off should not show finglish."""
|
|
entry = SAMPLE_VOCAB[0]
|
|
html = mock_vocab_and_db.format_word_card(entry, show_transliteration="off")
|
|
assert "raftan" not in html
|
|
|
|
|
|
def test_get_flashcard_batch(mock_vocab_and_db):
|
|
"""get_flashcard_batch should return a batch of entries."""
|
|
batch = mock_vocab_and_db.get_flashcard_batch(count=2)
|
|
assert len(batch) == 2
|
|
assert all("id" in e for e in batch)
|
|
|
|
|
|
def test_get_word_status_new(mock_vocab_and_db):
|
|
"""Unreviewed word should have status 'new'."""
|
|
assert mock_vocab_and_db.get_word_status("verb_go") == "new"
|
|
|
|
|
|
def test_get_word_status_learning(mock_vocab_and_db):
|
|
"""Recently reviewed word should have status 'learning'."""
|
|
import db
|
|
import fsrs
|
|
|
|
db.update_word_progress("verb_go", fsrs.Rating.Good)
|
|
assert mock_vocab_and_db.get_word_status("verb_go") == "learning"
|