Files
049-snack-and-lift/tests/unit/test_rules.py
Tirsvad 2ce7e3d8dc Add counters, summary and every Tuesday and Thursday class (steps 5 and 6)
Target class lookup for Tuesday and Thursday 6pm, counters, booking summary
and detailed class list, --first-tuesday-only for the earlier step,
screenshot names for steps 5 and 6, unit and container tests.

Task: MIL-005#1
Task: MIL-005#2
Task: MIL-005#3
Task: MIL-005#4
2026-10-28 17:54:11 +08:00

165 lines
5.3 KiB
Python

"""@file test_rules.py
@brief Day and time parsing, finding the target class, the decision and the messages.
"""
import pytest
from tests.unit.test_models import make_card
from snack_and_lift.domain.errors import PageFormatError, UnknownButtonError
from snack_and_lift.domain.models import Outcome
from snack_and_lift.domain.rules import (
Action,
decide_outcome_action,
find_next_class,
find_target_classes,
format_outcome,
is_target_time,
parse_day_label,
parse_time,
)
@pytest.mark.parametrize(
("label", "expected"),
[
("Today (Tue, Aug 12)", ("Tue", "Tue, Aug 12")),
("Tomorrow (Wed, Aug 13)", ("Wed", "Wed, Aug 13")),
("Thu, Aug 14", ("Thu", "Thu, Aug 14")),
(" Fri, Sep 5 ", ("Fri", "Fri, Sep 5")),
],
)
def test_day_label_is_split(label: str, expected: tuple[str, str]) -> None:
assert parse_day_label(label) == expected
@pytest.mark.parametrize("label", ["", "Tuesday", "Today Tue, Aug 12", "Today (Tue, Aug 12", "x"])
def test_day_label_junk_raises(label: str) -> None:
with pytest.raises(PageFormatError):
parse_day_label(label)
@pytest.mark.parametrize(
("text", "expected"),
[
("6:00 PM", (18, 0)),
("Time: 6:00 PM", (18, 0)),
("12:00 AM", (0, 0)),
("12:30 AM", (0, 30)),
("12:00 PM", (12, 0)),
("11:59 PM", (23, 59)),
("1:05 am", (1, 5)),
],
)
def test_time_is_read(text: str, expected: tuple[int, int]) -> None:
assert parse_time(text) == expected
@pytest.mark.parametrize("text", ["", "noon", "6 PM", "0:00 AM", "13:00 PM", "6:60 PM", "18:00"])
def test_time_junk_raises(text: str) -> None:
with pytest.raises(PageFormatError):
parse_time(text)
def test_target_time_is_six_pm() -> None:
assert is_target_time(make_card(time_text="Time: 6:00 PM"))
assert not is_target_time(make_card(time_text="Time: 6:00 AM"))
assert not is_target_time(make_card(time_text="Time: 6:30 PM"))
def test_next_class_is_found_when_the_schedule_starts_on_a_wednesday() -> None:
cards = [
make_card("Today (Wed, Aug 13)", card_id="1"),
make_card("Thu, Aug 14", card_id="2"),
make_card("Tue, Aug 19", time_text="Time: 7:00 AM", card_id="3"),
make_card("Tue, Aug 19", card_id="4"),
make_card("Tue, Aug 26", card_id="5"),
]
found = find_next_class(cards, "Tue")
assert found is not None
assert found.card_id == "4"
def test_next_class_accepts_a_today_label() -> None:
cards = [make_card("Today (Tue, Aug 12)", card_id="9")]
found = find_next_class(cards, "Tue")
assert found is not None
assert found.card_id == "9"
def test_next_class_skips_completed_classes() -> None:
cards = [
make_card("Today (Tue, Aug 12)", button_text="Completed", card_id="1"),
make_card("Tue, Aug 19", card_id="2"),
]
found = find_next_class(cards, "Tue")
assert found is not None
assert found.card_id == "2"
def test_next_class_is_none_without_a_match() -> None:
assert find_next_class([make_card("Thu, Aug 14")], "Tue") is None
assert find_next_class([], "Tue") is None
@pytest.mark.parametrize(
("button_text", "action"),
[
("Book Class", Action.CLICK_BOOK),
("Join Waitlist", Action.CLICK_WAITLIST),
("Booked", Action.ALREADY_BOOKED),
("Waitlisted", Action.ALREADY_WAITLISTED),
],
)
def test_action_follows_the_button_state(button_text: str, action: Action) -> None:
assert decide_outcome_action(make_card(button_text=button_text)) is action
@pytest.mark.parametrize("button_text", ["Booking...", "Joining...", "Completed", "???"])
def test_action_for_busy_or_unknown_button_raises(button_text: str) -> None:
with pytest.raises(UnknownButtonError):
decide_outcome_action(make_card(button_text=button_text))
@pytest.mark.parametrize(
("outcome", "message"),
[
(Outcome.NEW_BOOKING, "✓ Successfully booked: Spin Class on Tomorrow (Thu, Aug 14)"),
(Outcome.NEW_WAITLIST, "✓ Joined waitlist for: Spin Class on Tomorrow (Thu, Aug 14)"),
(Outcome.ALREADY_BOOKED, "✓ Already booked: Spin Class on Tomorrow (Thu, Aug 14)"),
(Outcome.ALREADY_WAITLISTED, "✓ Already on waitlist: Spin Class on Tomorrow (Thu, Aug 14)"),
],
)
def test_outcome_message(outcome: Outcome, message: str) -> None:
assert format_outcome(outcome, make_card()) == message
def test_find_target_classes_returns_tuesdays_and_thursdays_in_page_order() -> None:
cards = [
make_card("Today (Wed, Aug 13)", card_id="1"),
make_card("Tomorrow (Thu, Aug 14)", card_id="2"),
make_card("Fri, Aug 15", card_id="3"),
make_card("Tue, Aug 19", card_id="4"),
]
assert [card.card_id for card in find_target_classes(cards)] == ["2", "4"]
def test_find_target_classes_ignores_other_times_and_completed_cards() -> None:
cards = [
make_card("Thu, Aug 14", time_text="Time: 7:00 AM", card_id="1"),
make_card("Thu, Aug 14", button_text="Completed", card_id="2"),
make_card("Tue, Aug 19", time_text="Time: 6:00 PM", card_id="3"),
]
assert [card.card_id for card in find_target_classes(cards)] == ["3"]
def test_find_target_classes_is_empty_when_none_match() -> None:
assert find_target_classes([make_card("Wed, Aug 13")]) == []