MIL-005: Step 4 - Save progress
Review record RC-027 (Go, autonomous mode, not independent). Task: MIL-005#1 Task: MIL-005#2 Task: MIL-005#3 Task: MIL-005#4 Task: MIL-005#5
This commit is contained in:
@@ -7,15 +7,18 @@
|
||||
import time
|
||||
import tkinter as tk
|
||||
from collections.abc import Callable, Iterator, Sequence
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from flash_card.application.use_cases.open_study_session import open_study_session
|
||||
from flash_card.application.use_cases.study_session import StudySession
|
||||
from flash_card.constants import (
|
||||
BACK_TEXT_COLOR,
|
||||
BACKGROUND_COLOR,
|
||||
CARD_HEIGHT,
|
||||
CARD_WIDTH,
|
||||
FINISHED_MESSAGE,
|
||||
FLIP_DELAY_MS,
|
||||
FRONT_TEXT_COLOR,
|
||||
LANGUAGE_FONT,
|
||||
@@ -26,6 +29,12 @@ from flash_card.constants import (
|
||||
from flash_card.domain.entities.card import Card
|
||||
from flash_card.domain.entities.deck import Deck
|
||||
from flash_card.domain.value_objects.language_pair import LanguagePair
|
||||
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
||||
CsvProgressRepository,
|
||||
)
|
||||
from flash_card.infrastructure.persistence.csv_vocabulary_repository import (
|
||||
CsvVocabularyRepository,
|
||||
)
|
||||
from flash_card.presentation.features.flash_card import flash_card_view
|
||||
from flash_card.presentation.features.flash_card.flash_card_view import FlashCardView
|
||||
from flash_card.presentation.features.flash_card.flash_card_view_model import (
|
||||
@@ -39,9 +48,20 @@ def _first(cards: Sequence[Card]) -> Card:
|
||||
return cards[0]
|
||||
|
||||
|
||||
class _MemoryProgress:
|
||||
"""Progress store that keeps nothing: the shared window never deletes cards."""
|
||||
|
||||
def load(self) -> Deck | None:
|
||||
return None
|
||||
|
||||
def save(self, deck: Deck) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def _build_view(root: tk.Tk) -> FlashCardView:
|
||||
deck = Deck(LanguagePair("ms", "da"), CARDS)
|
||||
return FlashCardView(root, FlashCardViewModel(StudySession(deck, _first)))
|
||||
session = StudySession(deck, _first, _MemoryProgress())
|
||||
return FlashCardView(root, FlashCardViewModel(session))
|
||||
|
||||
|
||||
def _shown(view: FlashCardView) -> tuple[str, str]:
|
||||
@@ -129,14 +149,14 @@ def test_buttons_sit_in_row_one_with_their_images(
|
||||
assert str(view.right_button.cget("command"))
|
||||
|
||||
|
||||
def test_each_button_press_shows_next_card(
|
||||
def test_wrong_button_press_shows_next_card(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
_root, view = window
|
||||
assert _shown(view) == ("Malay", "aku")
|
||||
view.wrong_button.invoke()
|
||||
assert _shown(view) == ("Malay", "kau")
|
||||
view.right_button.invoke()
|
||||
view.wrong_button.invoke()
|
||||
assert _shown(view) == ("Malay", "aku")
|
||||
|
||||
|
||||
@@ -195,7 +215,7 @@ def test_next_card_after_flip_shows_front_again(
|
||||
root, view = window
|
||||
_reset(root, view)
|
||||
view.flip_card()
|
||||
view.right_button.invoke()
|
||||
view.wrong_button.invoke()
|
||||
assert _image(view) == str(view.card_front_image)
|
||||
assert _fill(view) == (FRONT_TEXT_COLOR, FRONT_TEXT_COLOR)
|
||||
assert _shown(view)[0] == "Malay"
|
||||
@@ -245,3 +265,65 @@ def test_real_timer_flips_card_after_delay(
|
||||
assert _image(view) == str(view.card_back_image)
|
||||
assert _shown(view)[0] == "Danish"
|
||||
_reset(root, view)
|
||||
|
||||
|
||||
HEADER_ONLY = "ms;da\n"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def files_view(
|
||||
window: tuple[tk.Tk, FlashCardView], tmp_path: Path
|
||||
) -> Iterator[tuple[FlashCardView, Path]]:
|
||||
"""A second view on the shared root, over real repositories in tmp_path."""
|
||||
root, _shared = window
|
||||
words = tmp_path / "words.csv"
|
||||
words.write_text("ms;da\naku;mig\nkau;du\nini;denne\n", encoding="utf-8")
|
||||
progress = tmp_path / "words_to_learn.csv"
|
||||
session = open_study_session(
|
||||
CsvVocabularyRepository(words), CsvProgressRepository(progress), _first
|
||||
)
|
||||
view = FlashCardView(root, FlashCardViewModel(session))
|
||||
yield view, progress
|
||||
if view.flip_timer is not None:
|
||||
root.after_cancel(view.flip_timer)
|
||||
for widget in (view.canvas, view.wrong_button, view.right_button):
|
||||
widget.destroy()
|
||||
|
||||
|
||||
def _saved(progress: Path) -> str:
|
||||
return progress.read_bytes().decode("utf-8-sig")
|
||||
|
||||
|
||||
def test_right_button_removes_card_and_writes_progress_file(
|
||||
files_view: tuple[FlashCardView, Path],
|
||||
) -> None:
|
||||
view, progress = files_view
|
||||
assert _shown(view) == ("Malay", "aku")
|
||||
view.right_button.invoke()
|
||||
assert _shown(view) == ("Malay", "kau")
|
||||
assert _saved(progress) == "ms;da\nkau;du\nini;denne\n"
|
||||
|
||||
|
||||
def test_wrong_button_shows_next_card_and_leaves_progress_file_absent(
|
||||
files_view: tuple[FlashCardView, Path],
|
||||
) -> None:
|
||||
view, progress = files_view
|
||||
view.wrong_button.invoke()
|
||||
assert _shown(view) == ("Malay", "kau")
|
||||
assert not progress.exists()
|
||||
|
||||
|
||||
def test_buttons_do_nothing_visible_when_finished(
|
||||
files_view: tuple[FlashCardView, Path],
|
||||
) -> None:
|
||||
view, progress = files_view
|
||||
for _ in range(3):
|
||||
view.right_button.invoke()
|
||||
assert _shown(view) == ("", FINISHED_MESSAGE)
|
||||
assert view.flip_timer is None
|
||||
assert _saved(progress) == HEADER_ONLY
|
||||
view.right_button.invoke()
|
||||
view.wrong_button.invoke()
|
||||
assert _shown(view) == ("", FINISHED_MESSAGE)
|
||||
assert view.flip_timer is None
|
||||
assert _saved(progress) == HEADER_ONLY
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Integration tests of saving and resuming progress over real CSV files."""
|
||||
|
||||
from collections.abc import Callable, Sequence
|
||||
from pathlib import Path
|
||||
|
||||
from flash_card.application.use_cases.open_study_session import open_study_session
|
||||
from flash_card.application.use_cases.study_session import StudySession
|
||||
from flash_card.domain.entities.card import Card
|
||||
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
||||
CsvProgressRepository,
|
||||
)
|
||||
from flash_card.infrastructure.persistence.csv_vocabulary_repository import (
|
||||
CsvVocabularyRepository,
|
||||
)
|
||||
|
||||
WORDS = "fr;en\nchaque;each\nparlé;speak\narrivé;come\n"
|
||||
|
||||
Choose = Callable[[Sequence[Card]], Card]
|
||||
|
||||
|
||||
def _first(cards: Sequence[Card]) -> Card:
|
||||
return cards[0]
|
||||
|
||||
|
||||
def _choose_front(front: str) -> Choose:
|
||||
def choose(cards: Sequence[Card]) -> Card:
|
||||
return next(card for card in cards if card.front == front)
|
||||
|
||||
return choose
|
||||
|
||||
|
||||
def _open(folder: Path, choose: Choose = _first) -> StudySession:
|
||||
return open_study_session(
|
||||
CsvVocabularyRepository(folder / "words.csv"),
|
||||
CsvProgressRepository(folder / "words_to_learn.csv"),
|
||||
choose,
|
||||
)
|
||||
|
||||
|
||||
def _setup(folder: Path) -> Path:
|
||||
(folder / "words.csv").write_text(WORDS, encoding="utf-8")
|
||||
return folder / "words_to_learn.csv"
|
||||
|
||||
|
||||
def _text(path: Path) -> str:
|
||||
return path.read_bytes().decode("utf-8-sig")
|
||||
|
||||
|
||||
def test_mark_known_writes_remaining_cards_in_order(tmp_path: Path) -> None:
|
||||
progress = _setup(tmp_path)
|
||||
session = _open(tmp_path, _choose_front("parlé"))
|
||||
session.next_card()
|
||||
session.mark_known()
|
||||
assert _text(progress) == "fr;en\nchaque;each\narrivé;come\n"
|
||||
|
||||
|
||||
def test_restart_draws_only_from_progress_file(tmp_path: Path) -> None:
|
||||
_setup(tmp_path)
|
||||
first = _open(tmp_path, _choose_front("parlé"))
|
||||
first.next_card()
|
||||
first.mark_known()
|
||||
|
||||
second = _open(tmp_path)
|
||||
fronts: set[str] = set()
|
||||
for _ in range(6):
|
||||
card = second.next_card()
|
||||
assert card is not None
|
||||
fronts.add(card.front_text)
|
||||
|
||||
assert second.remaining == 2
|
||||
assert fronts == {"chaque", "arrivé"}
|
||||
|
||||
|
||||
def test_progress_file_is_readable_as_vocabulary(tmp_path: Path) -> None:
|
||||
progress = _setup(tmp_path)
|
||||
session = _open(tmp_path, _choose_front("chaque"))
|
||||
session.next_card()
|
||||
session.mark_known()
|
||||
vocabulary = CsvVocabularyRepository(progress).load()
|
||||
assert vocabulary.cards == (Card("parlé", "speak"), Card("arrivé", "come"))
|
||||
|
||||
|
||||
def test_header_only_progress_file_gives_finished_session(tmp_path: Path) -> None:
|
||||
progress = _setup(tmp_path)
|
||||
progress.write_text("fr;en\n", encoding="utf-8")
|
||||
session = _open(tmp_path)
|
||||
assert session.remaining == 0
|
||||
assert session.next_card() is None
|
||||
|
||||
|
||||
def test_unknown_card_never_creates_progress_file(tmp_path: Path) -> None:
|
||||
progress = _setup(tmp_path)
|
||||
session = _open(tmp_path)
|
||||
session.next_card()
|
||||
session.next_card()
|
||||
assert not progress.exists()
|
||||
@@ -1,9 +1,18 @@
|
||||
"""Integration tests of the word list reader on the delivered file."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from flash_card.application.use_cases.open_study_session import open_study_session
|
||||
from flash_card.constants import DEFAULT_DATA_DIR, VOCABULARY_FILE_NAME
|
||||
from flash_card.constants import (
|
||||
DEFAULT_DATA_DIR,
|
||||
PROGRESS_FILE_NAME,
|
||||
VOCABULARY_FILE_NAME,
|
||||
)
|
||||
from flash_card.domain.entities.card import Card
|
||||
from flash_card.domain.value_objects.language_pair import LanguagePair
|
||||
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
||||
CsvProgressRepository,
|
||||
)
|
||||
from flash_card.infrastructure.persistence.csv_vocabulary_repository import (
|
||||
CsvVocabularyRepository,
|
||||
)
|
||||
@@ -19,10 +28,11 @@ def test_read_word_list_reads_real_file() -> None:
|
||||
assert cards[0] == Card("aku", "mig")
|
||||
|
||||
|
||||
def test_open_study_session_yields_card_from_real_file() -> None:
|
||||
def test_open_study_session_yields_card_from_real_file(tmp_path: Path) -> None:
|
||||
repository = CsvVocabularyRepository(PATH)
|
||||
cards = repository.load().cards
|
||||
session = open_study_session(repository, lambda candidates: candidates[0])
|
||||
progress = CsvProgressRepository(tmp_path / PROGRESS_FILE_NAME)
|
||||
session = open_study_session(repository, progress, lambda candidates: candidates[0])
|
||||
|
||||
card = session.next_card()
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import pytest
|
||||
from flash_card.application.dto.card_dto import CardDto
|
||||
from flash_card.application.use_cases.open_study_session import open_study_session
|
||||
from flash_card.domain.entities.card import Card
|
||||
from flash_card.domain.entities.deck import Deck
|
||||
from flash_card.domain.entities.vocabulary import Vocabulary
|
||||
from flash_card.domain.errors.vocabulary_errors import VocabularyNotFoundError
|
||||
from flash_card.domain.value_objects.language_pair import LanguagePair
|
||||
@@ -20,6 +21,18 @@ class _FakeRepository:
|
||||
return self._vocabulary
|
||||
|
||||
|
||||
class _RecordingProgress:
|
||||
def __init__(self, stored: Deck | None = None) -> None:
|
||||
self.stored = stored
|
||||
self.saved: list[tuple[Card, ...]] = []
|
||||
|
||||
def load(self) -> Deck | None:
|
||||
return self.stored
|
||||
|
||||
def save(self, deck: Deck) -> None:
|
||||
self.saved.append(deck.cards)
|
||||
|
||||
|
||||
class _MissingRepository:
|
||||
def load(self) -> Vocabulary:
|
||||
raise VocabularyNotFoundError("missing")
|
||||
@@ -33,7 +46,7 @@ def test_open_study_session_builds_session_over_all_cards() -> None:
|
||||
cards = (Card("aku", "mig"), Card("kau", "du"))
|
||||
repository = _FakeRepository(Vocabulary(LanguagePair("ms", "da"), cards))
|
||||
|
||||
session = open_study_session(repository, _first)
|
||||
session = open_study_session(repository, _RecordingProgress(), _first)
|
||||
|
||||
assert session.remaining == 2
|
||||
assert session.next_card() == CardDto("aku", "mig", "ms", "da")
|
||||
@@ -41,4 +54,33 @@ def test_open_study_session_builds_session_over_all_cards() -> None:
|
||||
|
||||
def test_open_study_session_propagates_repository_error() -> None:
|
||||
with pytest.raises(VocabularyNotFoundError):
|
||||
open_study_session(_MissingRepository(), _first)
|
||||
open_study_session(_MissingRepository(), _RecordingProgress(), _first)
|
||||
|
||||
|
||||
def test_open_study_session_prefers_saved_progress() -> None:
|
||||
pair = LanguagePair("ms", "da")
|
||||
vocabulary = Vocabulary(pair, (Card("aku", "mig"), Card("kau", "du")))
|
||||
progress = _RecordingProgress(Deck(pair, [Card("kau", "du")]))
|
||||
|
||||
session = open_study_session(_FakeRepository(vocabulary), progress, _first)
|
||||
|
||||
assert session.remaining == 1
|
||||
assert session.next_card() == CardDto("kau", "du", "ms", "da")
|
||||
|
||||
|
||||
def test_open_study_session_skips_vocabulary_when_progress_exists() -> None:
|
||||
progress = _RecordingProgress(Deck(LanguagePair("ms", "da"), []))
|
||||
|
||||
session = open_study_session(_MissingRepository(), progress, _first)
|
||||
|
||||
assert session.remaining == 0
|
||||
|
||||
|
||||
def test_open_study_session_never_saves_on_start() -> None:
|
||||
vocabulary = Vocabulary(LanguagePair("ms", "da"), (Card("aku", "mig"),))
|
||||
progress = _RecordingProgress()
|
||||
|
||||
session = open_study_session(_FakeRepository(vocabulary), progress, _first)
|
||||
session.next_card()
|
||||
|
||||
assert progress.saved == []
|
||||
|
||||
@@ -17,8 +17,20 @@ def _first(cards: Sequence[Card]) -> Card:
|
||||
return cards[0]
|
||||
|
||||
|
||||
def _session(*cards: Card) -> StudySession:
|
||||
return StudySession(Deck(PAIR, cards), _first)
|
||||
class _RecordingProgress:
|
||||
def __init__(self) -> None:
|
||||
self.saved: list[tuple[Card, ...]] = []
|
||||
|
||||
def load(self) -> Deck | None:
|
||||
return None
|
||||
|
||||
def save(self, deck: Deck) -> None:
|
||||
self.saved.append(deck.cards)
|
||||
|
||||
|
||||
def _session(*cards: Card, progress: _RecordingProgress | None = None) -> StudySession:
|
||||
store = _RecordingProgress() if progress is None else progress
|
||||
return StudySession(Deck(PAIR, cards), _first, store)
|
||||
|
||||
|
||||
def test_current_card_is_none_before_first_draw() -> None:
|
||||
@@ -58,3 +70,38 @@ def test_remaining_counts_cards_in_deck() -> None:
|
||||
session = _session(A, B)
|
||||
session.next_card()
|
||||
assert session.remaining == 2
|
||||
|
||||
|
||||
def test_mark_known_removes_current_card_and_saves_remaining_once() -> None:
|
||||
progress = _RecordingProgress()
|
||||
session = _session(A, B, progress=progress)
|
||||
session.next_card()
|
||||
session.mark_known()
|
||||
assert session.remaining == 1
|
||||
assert session.current_card is None
|
||||
assert progress.saved == [(B,)]
|
||||
|
||||
|
||||
def test_mark_known_saves_nothing_when_no_current_card() -> None:
|
||||
progress = _RecordingProgress()
|
||||
session = _session(A, progress=progress)
|
||||
session.mark_known()
|
||||
assert session.remaining == 1
|
||||
assert progress.saved == []
|
||||
|
||||
|
||||
def test_mark_known_on_last_card_leaves_session_finished() -> None:
|
||||
progress = _RecordingProgress()
|
||||
session = _session(A, progress=progress)
|
||||
session.next_card()
|
||||
session.mark_known()
|
||||
assert progress.saved == [()]
|
||||
assert session.next_card() is None
|
||||
|
||||
|
||||
def test_next_card_never_saves() -> None:
|
||||
progress = _RecordingProgress()
|
||||
session = _session(A, B, progress=progress)
|
||||
session.next_card()
|
||||
session.next_card()
|
||||
assert progress.saved == []
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Tests of write_word_list and CsvProgressRepository."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from flash_card.domain.entities.card import Card
|
||||
from flash_card.domain.entities.deck import Deck
|
||||
from flash_card.domain.errors.vocabulary_errors import VocabularyFormatError
|
||||
from flash_card.domain.value_objects.language_pair import LanguagePair
|
||||
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
||||
CsvProgressRepository,
|
||||
)
|
||||
from flash_card.infrastructure.persistence.csv_word_list import (
|
||||
read_word_list,
|
||||
write_word_list,
|
||||
)
|
||||
|
||||
PAIR = LanguagePair("ms", "da")
|
||||
CARDS = (Card("aku", "mig"), Card("kau", "du"))
|
||||
|
||||
|
||||
def test_write_word_list_round_trips_through_read_word_list(tmp_path: Path) -> None:
|
||||
path = tmp_path / "w.csv"
|
||||
write_word_list(path, PAIR, CARDS)
|
||||
assert read_word_list(path) == (PAIR, CARDS)
|
||||
|
||||
|
||||
def test_write_word_list_writes_header_and_rows_with_semicolon(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
path = tmp_path / "w.csv"
|
||||
write_word_list(path, PAIR, CARDS)
|
||||
raw = path.read_bytes()
|
||||
assert raw.decode("utf-8-sig") == "ms;da\naku;mig\nkau;du\n"
|
||||
assert b"\r" not in raw
|
||||
|
||||
|
||||
def test_write_word_list_keeps_non_ascii_text(tmp_path: Path) -> None:
|
||||
path = tmp_path / "w.csv"
|
||||
cards = (Card("chaque", "each"), Card("parlé", "speak"))
|
||||
write_word_list(path, LanguagePair("fr", "en"), cards)
|
||||
assert read_word_list(path)[1] == cards
|
||||
|
||||
|
||||
def test_write_word_list_writes_header_only_for_no_cards(tmp_path: Path) -> None:
|
||||
path = tmp_path / "w.csv"
|
||||
write_word_list(path, PAIR, ())
|
||||
assert read_word_list(path) == (PAIR, ())
|
||||
|
||||
|
||||
def test_write_word_list_creates_parent_folder(tmp_path: Path) -> None:
|
||||
path = tmp_path / "data" / "deep" / "w.csv"
|
||||
write_word_list(path, PAIR, CARDS)
|
||||
assert path.is_file()
|
||||
|
||||
|
||||
def test_write_word_list_replaces_file_and_leaves_no_temporary_file(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
path = tmp_path / "w.csv"
|
||||
write_word_list(path, PAIR, CARDS)
|
||||
write_word_list(path, PAIR, CARDS[:1])
|
||||
assert read_word_list(path)[1] == CARDS[:1]
|
||||
assert [item.name for item in tmp_path.iterdir()] == ["w.csv"]
|
||||
|
||||
|
||||
def test_write_word_list_keeps_old_file_and_no_temporary_when_write_fails(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
path = tmp_path / "w.csv"
|
||||
write_word_list(path, PAIR, CARDS)
|
||||
unencodable = (Card("aku", "\ud800"),)
|
||||
with pytest.raises(UnicodeEncodeError):
|
||||
write_word_list(path, PAIR, unencodable)
|
||||
assert read_word_list(path)[1] == CARDS
|
||||
assert [item.name for item in tmp_path.iterdir()] == ["w.csv"]
|
||||
|
||||
|
||||
def test_progress_load_returns_none_when_file_is_missing(tmp_path: Path) -> None:
|
||||
assert CsvProgressRepository(tmp_path / "p.csv").load() is None
|
||||
|
||||
|
||||
def test_progress_load_returns_empty_deck_for_header_only_file(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
path = tmp_path / "p.csv"
|
||||
path.write_text("ms;da\n", encoding="utf-8")
|
||||
deck = CsvProgressRepository(path).load()
|
||||
assert deck is not None
|
||||
assert deck.is_empty
|
||||
assert deck.language_pair == PAIR
|
||||
|
||||
|
||||
def test_progress_load_raises_format_error_and_keeps_malformed_file(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
path = tmp_path / "p.csv"
|
||||
content = b"ms;da\naku;mig;extra\n"
|
||||
path.write_bytes(content)
|
||||
with pytest.raises(VocabularyFormatError):
|
||||
CsvProgressRepository(path).load()
|
||||
assert path.read_bytes() == content
|
||||
|
||||
|
||||
def test_progress_save_then_load_returns_same_cards(tmp_path: Path) -> None:
|
||||
repository = CsvProgressRepository(tmp_path / "p.csv")
|
||||
repository.save(Deck(PAIR, CARDS))
|
||||
deck = repository.load()
|
||||
assert deck is not None
|
||||
assert deck.cards == CARDS
|
||||
assert deck.language_pair == PAIR
|
||||
@@ -20,8 +20,22 @@ def _first(cards: Sequence[Card]) -> Card:
|
||||
return cards[0]
|
||||
|
||||
|
||||
def _session(code: str, *cards: Card) -> StudySession:
|
||||
return StudySession(Deck(LanguagePair(code, "da"), cards), _first)
|
||||
class _RecordingProgress:
|
||||
def __init__(self) -> None:
|
||||
self.saved: list[tuple[Card, ...]] = []
|
||||
|
||||
def load(self) -> Deck | None:
|
||||
return None
|
||||
|
||||
def save(self, deck: Deck) -> None:
|
||||
self.saved.append(deck.cards)
|
||||
|
||||
|
||||
def _session(
|
||||
code: str, *cards: Card, progress: _RecordingProgress | None = None
|
||||
) -> StudySession:
|
||||
store = _RecordingProgress() if progress is None else progress
|
||||
return StudySession(Deck(LanguagePair(code, "da"), cards), _first, store)
|
||||
|
||||
|
||||
def test_view_model_draws_first_card_on_construction() -> None:
|
||||
@@ -90,3 +104,45 @@ def test_next_card_resets_side_to_front() -> None:
|
||||
assert view_model.side is Side.FRONT
|
||||
assert view_model.language_name == "Malay"
|
||||
assert view_model.text == "kau"
|
||||
|
||||
|
||||
def test_is_known_saves_remaining_and_shows_next_front() -> None:
|
||||
progress = _RecordingProgress()
|
||||
view_model = FlashCardViewModel(_session("ms", A, B, progress=progress))
|
||||
view_model.flip_card()
|
||||
view_model.is_known()
|
||||
assert progress.saved == [(B,)]
|
||||
assert view_model.side is Side.FRONT
|
||||
assert view_model.text == "kau"
|
||||
assert not view_model.is_finished
|
||||
|
||||
|
||||
def test_is_known_on_last_card_finishes() -> None:
|
||||
progress = _RecordingProgress()
|
||||
view_model = FlashCardViewModel(_session("ms", A, progress=progress))
|
||||
view_model.is_known()
|
||||
assert view_model.is_finished
|
||||
assert view_model.text == FINISHED_MESSAGE
|
||||
assert progress.saved == [()]
|
||||
|
||||
|
||||
def test_is_known_does_nothing_when_finished() -> None:
|
||||
progress = _RecordingProgress()
|
||||
view_model = FlashCardViewModel(_session("ms", progress=progress))
|
||||
view_model.is_known()
|
||||
assert view_model.is_finished
|
||||
assert progress.saved == []
|
||||
|
||||
|
||||
def test_next_card_does_nothing_when_finished() -> None:
|
||||
view_model = FlashCardViewModel(_session("ms"))
|
||||
view_model.next_card()
|
||||
assert view_model.is_finished
|
||||
assert view_model.text == FINISHED_MESSAGE
|
||||
|
||||
|
||||
def test_next_card_never_saves() -> None:
|
||||
progress = _RecordingProgress()
|
||||
view_model = FlashCardViewModel(_session("ms", A, B, progress=progress))
|
||||
view_model.next_card()
|
||||
assert progress.saved == []
|
||||
|
||||
Reference in New Issue
Block a user