MIL-004: Step 3 - Flip the cards
Review record RC-026 (Go, autonomous mode, not independent). Task: MIL-004#1 Task: MIL-004#2 Task: MIL-004#3
This commit is contained in:
+247
-136
@@ -1,136 +1,247 @@
|
||||
"""End-to-end tests of the flash card window.
|
||||
|
||||
@file test_flash_card_window.py
|
||||
@brief Builds the real window with Tk and checks it against the wireframe.
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from collections.abc import Iterator, Sequence
|
||||
|
||||
import pytest
|
||||
|
||||
from flash_card.application.use_cases.study_session import StudySession
|
||||
from flash_card.constants import (
|
||||
BACKGROUND_COLOR,
|
||||
CARD_HEIGHT,
|
||||
CARD_WIDTH,
|
||||
FRONT_TEXT_COLOR,
|
||||
LANGUAGE_FONT,
|
||||
WINDOW_PADDING,
|
||||
WINDOW_TITLE,
|
||||
WORD_FONT,
|
||||
)
|
||||
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.presentation.features.flash_card.flash_card_view import FlashCardView
|
||||
from flash_card.presentation.features.flash_card.flash_card_view_model import (
|
||||
FlashCardViewModel,
|
||||
)
|
||||
|
||||
CARDS = (Card("aku", "mig"), Card("kau", "du"), Card("ini", "denne"))
|
||||
|
||||
|
||||
def _first(cards: Sequence[Card]) -> Card:
|
||||
return cards[0]
|
||||
|
||||
|
||||
def _build_view(root: tk.Tk) -> FlashCardView:
|
||||
deck = Deck(LanguagePair("ms", "da"), CARDS)
|
||||
return FlashCardView(root, FlashCardViewModel(StudySession(deck, _first)))
|
||||
|
||||
|
||||
def _shown(view: FlashCardView) -> tuple[str, str]:
|
||||
return (
|
||||
str(view.canvas.itemcget(view.language_text_id, "text")),
|
||||
str(view.canvas.itemcget(view.word_text_id, "text")),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def window() -> Iterator[tuple[tk.Tk, FlashCardView]]:
|
||||
# One Tk per module: a second interpreter right after destroying the
|
||||
# first fails to find init.tcl on some Windows Python installs.
|
||||
try:
|
||||
root = tk.Tk()
|
||||
except tk.TclError:
|
||||
pytest.skip("no display available for Tk")
|
||||
view = _build_view(root)
|
||||
root.update_idletasks()
|
||||
yield root, view
|
||||
root.destroy()
|
||||
|
||||
|
||||
def test_window_has_title_background_and_padding(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
root, _view = window
|
||||
assert root.title() == WINDOW_TITLE
|
||||
assert root.cget("bg") == BACKGROUND_COLOR
|
||||
assert int(str(root.cget("padx"))) == WINDOW_PADDING
|
||||
assert int(str(root.cget("pady"))) == WINDOW_PADDING
|
||||
|
||||
|
||||
def test_canvas_has_card_size_and_spans_two_columns(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
_root, view = window
|
||||
assert int(str(view.canvas.cget("width"))) == CARD_WIDTH
|
||||
assert int(str(view.canvas.cget("height"))) == CARD_HEIGHT
|
||||
info = view.canvas.grid_info()
|
||||
assert (int(info["row"]), int(info["column"])) == (0, 0)
|
||||
assert int(info["columnspan"]) == 2
|
||||
|
||||
|
||||
def test_canvas_holds_one_image_and_two_texts(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
_root, view = window
|
||||
types = [view.canvas.type(item) for item in view.canvas.find_all()]
|
||||
assert types.count("image") == 1
|
||||
assert types.count("text") == 2
|
||||
assert len(types) == 3
|
||||
|
||||
|
||||
def test_texts_show_first_card_with_font_and_colour(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
_root, view = window
|
||||
canvas = view.canvas
|
||||
expected = [
|
||||
(view.language_text_id, "Malay", LANGUAGE_FONT),
|
||||
(view.word_text_id, "aku", WORD_FONT),
|
||||
]
|
||||
for item, text, font in expected:
|
||||
assert canvas.itemcget(item, "text") == text
|
||||
assert canvas.itemcget(item, "fill") == FRONT_TEXT_COLOR
|
||||
family, size, style = font
|
||||
assert canvas.itemcget(item, "font").split() == [family, str(size), style]
|
||||
|
||||
|
||||
def test_buttons_sit_in_row_one_with_their_images(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
_root, view = window
|
||||
wrong = view.wrong_button.grid_info()
|
||||
right = view.right_button.grid_info()
|
||||
assert (int(wrong["row"]), int(wrong["column"])) == (1, 0)
|
||||
assert (int(right["row"]), int(right["column"])) == (1, 1)
|
||||
wrong_image = str(view.wrong_button.cget("image"))
|
||||
right_image = str(view.right_button.cget("image"))
|
||||
assert wrong_image
|
||||
assert right_image
|
||||
assert wrong_image != right_image
|
||||
assert str(view.wrong_button.cget("command"))
|
||||
assert str(view.right_button.cget("command"))
|
||||
|
||||
|
||||
def test_each_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()
|
||||
assert _shown(view) == ("Malay", "aku")
|
||||
"""End-to-end tests of the flash card window.
|
||||
|
||||
@file test_flash_card_window.py
|
||||
@brief Builds the real window with Tk and checks it against the wireframe.
|
||||
"""
|
||||
|
||||
import time
|
||||
import tkinter as tk
|
||||
from collections.abc import Callable, Iterator, Sequence
|
||||
|
||||
import pytest
|
||||
|
||||
from flash_card.application.use_cases.study_session import StudySession
|
||||
from flash_card.constants import (
|
||||
BACK_TEXT_COLOR,
|
||||
BACKGROUND_COLOR,
|
||||
CARD_HEIGHT,
|
||||
CARD_WIDTH,
|
||||
FLIP_DELAY_MS,
|
||||
FRONT_TEXT_COLOR,
|
||||
LANGUAGE_FONT,
|
||||
WINDOW_PADDING,
|
||||
WINDOW_TITLE,
|
||||
WORD_FONT,
|
||||
)
|
||||
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.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 (
|
||||
FlashCardViewModel,
|
||||
)
|
||||
|
||||
CARDS = (Card("aku", "mig"), Card("kau", "du"), Card("ini", "denne"))
|
||||
|
||||
|
||||
def _first(cards: Sequence[Card]) -> Card:
|
||||
return cards[0]
|
||||
|
||||
|
||||
def _build_view(root: tk.Tk) -> FlashCardView:
|
||||
deck = Deck(LanguagePair("ms", "da"), CARDS)
|
||||
return FlashCardView(root, FlashCardViewModel(StudySession(deck, _first)))
|
||||
|
||||
|
||||
def _shown(view: FlashCardView) -> tuple[str, str]:
|
||||
return (
|
||||
str(view.canvas.itemcget(view.language_text_id, "text")),
|
||||
str(view.canvas.itemcget(view.word_text_id, "text")),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def window() -> Iterator[tuple[tk.Tk, FlashCardView]]:
|
||||
# One Tk per module: a second interpreter right after destroying the
|
||||
# first fails to find init.tcl on some Windows Python installs.
|
||||
try:
|
||||
root = tk.Tk()
|
||||
except tk.TclError:
|
||||
pytest.skip("no display available for Tk")
|
||||
view = _build_view(root)
|
||||
root.update_idletasks()
|
||||
yield root, view
|
||||
root.destroy()
|
||||
|
||||
|
||||
def test_window_has_title_background_and_padding(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
root, _view = window
|
||||
assert root.title() == WINDOW_TITLE
|
||||
assert root.cget("bg") == BACKGROUND_COLOR
|
||||
assert int(str(root.cget("padx"))) == WINDOW_PADDING
|
||||
assert int(str(root.cget("pady"))) == WINDOW_PADDING
|
||||
|
||||
|
||||
def test_canvas_has_card_size_and_spans_two_columns(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
_root, view = window
|
||||
assert int(str(view.canvas.cget("width"))) == CARD_WIDTH
|
||||
assert int(str(view.canvas.cget("height"))) == CARD_HEIGHT
|
||||
info = view.canvas.grid_info()
|
||||
assert (int(info["row"]), int(info["column"])) == (0, 0)
|
||||
assert int(info["columnspan"]) == 2
|
||||
|
||||
|
||||
def test_canvas_holds_one_image_and_two_texts(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
_root, view = window
|
||||
types = [view.canvas.type(item) for item in view.canvas.find_all()]
|
||||
assert types.count("image") == 1
|
||||
assert types.count("text") == 2
|
||||
assert len(types) == 3
|
||||
|
||||
|
||||
def test_texts_show_first_card_with_font_and_colour(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
_root, view = window
|
||||
canvas = view.canvas
|
||||
expected = [
|
||||
(view.language_text_id, "Malay", LANGUAGE_FONT),
|
||||
(view.word_text_id, "aku", WORD_FONT),
|
||||
]
|
||||
for item, text, font in expected:
|
||||
assert canvas.itemcget(item, "text") == text
|
||||
assert canvas.itemcget(item, "fill") == FRONT_TEXT_COLOR
|
||||
family, size, style = font
|
||||
assert canvas.itemcget(item, "font").split() == [family, str(size), style]
|
||||
|
||||
|
||||
def test_buttons_sit_in_row_one_with_their_images(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
_root, view = window
|
||||
wrong = view.wrong_button.grid_info()
|
||||
right = view.right_button.grid_info()
|
||||
assert (int(wrong["row"]), int(wrong["column"])) == (1, 0)
|
||||
assert (int(right["row"]), int(right["column"])) == (1, 1)
|
||||
wrong_image = str(view.wrong_button.cget("image"))
|
||||
right_image = str(view.right_button.cget("image"))
|
||||
assert wrong_image
|
||||
assert right_image
|
||||
assert wrong_image != right_image
|
||||
assert str(view.wrong_button.cget("command"))
|
||||
assert str(view.right_button.cget("command"))
|
||||
|
||||
|
||||
def test_each_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()
|
||||
assert _shown(view) == ("Malay", "aku")
|
||||
|
||||
|
||||
def _word(view: FlashCardView) -> str:
|
||||
return _shown(view)[1]
|
||||
|
||||
|
||||
def _image(view: FlashCardView) -> str:
|
||||
return str(view.canvas.itemcget(view.image_id, "image"))
|
||||
|
||||
|
||||
def _fill(view: FlashCardView) -> tuple[str, str]:
|
||||
return (
|
||||
str(view.canvas.itemcget(view.language_text_id, "fill")),
|
||||
str(view.canvas.itemcget(view.word_text_id, "fill")),
|
||||
)
|
||||
|
||||
|
||||
def _reset(root: tk.Tk, view: FlashCardView) -> None:
|
||||
"""Show a fresh front with no pending timer."""
|
||||
view.next_card()
|
||||
if view.flip_timer is not None:
|
||||
root.after_cancel(view.flip_timer)
|
||||
view.flip_timer = None
|
||||
|
||||
|
||||
def test_front_state_uses_front_image_black_text_and_front_language(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
root, view = window
|
||||
_reset(root, view)
|
||||
assert _image(view) == str(view.card_front_image)
|
||||
assert _fill(view) == (FRONT_TEXT_COLOR, FRONT_TEXT_COLOR)
|
||||
assert _shown(view)[0] == "Malay"
|
||||
assert _word(view) in {card.front for card in CARDS}
|
||||
|
||||
|
||||
def test_flip_card_shows_back_image_white_text_and_translation(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
root, view = window
|
||||
_reset(root, view)
|
||||
front = _word(view)
|
||||
view.flip_card()
|
||||
translations = {card.front: card.back for card in CARDS}
|
||||
assert _image(view) == str(view.card_back_image)
|
||||
assert _image(view) != str(view.card_front_image)
|
||||
assert _fill(view) == (BACK_TEXT_COLOR, BACK_TEXT_COLOR)
|
||||
assert _shown(view) == ("Danish", translations[front])
|
||||
assert view.flip_timer is None
|
||||
|
||||
|
||||
def test_next_card_after_flip_shows_front_again(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
) -> None:
|
||||
root, view = window
|
||||
_reset(root, view)
|
||||
view.flip_card()
|
||||
view.right_button.invoke()
|
||||
assert _image(view) == str(view.card_front_image)
|
||||
assert _fill(view) == (FRONT_TEXT_COLOR, FRONT_TEXT_COLOR)
|
||||
assert _shown(view)[0] == "Malay"
|
||||
_reset(root, view)
|
||||
|
||||
|
||||
def test_next_card_cancels_pending_timer_and_schedules_flip(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
root, view = window
|
||||
_reset(root, view)
|
||||
calls: list[tuple[str, object]] = []
|
||||
|
||||
def fake_after(delay: int, callback: Callable[[], None]) -> str:
|
||||
calls.append(("after", (delay, callback)))
|
||||
return "timer-id"
|
||||
|
||||
def fake_cancel(timer_id: str) -> None:
|
||||
calls.append(("cancel", timer_id))
|
||||
|
||||
monkeypatch.setattr(root, "after", fake_after, raising=False)
|
||||
monkeypatch.setattr(root, "after_cancel", fake_cancel, raising=False)
|
||||
view.flip_timer = "old-id"
|
||||
view.next_card()
|
||||
assert calls == [
|
||||
("cancel", "old-id"),
|
||||
("after", (FLIP_DELAY_MS, view.flip_card)),
|
||||
]
|
||||
assert view.flip_timer == "timer-id"
|
||||
view.flip_timer = None
|
||||
|
||||
|
||||
def test_real_timer_flips_card_after_delay(
|
||||
window: tuple[tk.Tk, FlashCardView],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
root, view = window
|
||||
monkeypatch.setattr(flash_card_view, "FLIP_DELAY_MS", 50)
|
||||
view.next_card()
|
||||
assert _image(view) == str(view.card_front_image)
|
||||
deadline = time.monotonic() + 2
|
||||
while view.flip_timer is not None and time.monotonic() < deadline:
|
||||
root.update()
|
||||
time.sleep(0.005)
|
||||
assert view.flip_timer is None
|
||||
assert _image(view) == str(view.card_back_image)
|
||||
assert _shown(view)[0] == "Danish"
|
||||
_reset(root, view)
|
||||
|
||||
Reference in New Issue
Block a user