ebe770fecd
Turn a regular-season-looking Tuesday into a full playoff experience: - Playoff banner with round + day + series + elimination counts, gold/silver Cup theme toggled by body.playoff-mode - Series context on each playoff card: round chip, series score, stake badges (GAME 7, CLINCHER, PIVOTAL), and one-line blurb - Game 7s pin to a new Spotlight section above Live - Playoff OT renders with SUDDEN DEATH badge and pulsing gold border - Client-side OT notifications via bell button in the banner - New /series/<id> drill-down with headline, next-game, and game-by-game history - New /bracket page: 7-column desktop grid, accordion on mobile - Day N banner count auto-anchors on first playoff scoreboard hit - SQLite cache for bracket + per-series schedules, stale-on-failure up to 24h Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
272 lines
9.2 KiB
Python
272 lines
9.2 KiB
Python
import pytest
|
|
|
|
from app.playoff import (
|
|
is_pinned,
|
|
is_playoff_game,
|
|
is_playoff_ot,
|
|
ot_label,
|
|
series_badges,
|
|
series_blurb,
|
|
series_state,
|
|
series_summary,
|
|
today_meta,
|
|
)
|
|
from tests.conftest import make_game, make_playoff_game
|
|
|
|
|
|
class TestSeriesState:
|
|
def test_empty_returns_defaults(self):
|
|
state = series_state({})
|
|
assert state["is_opener"] is True
|
|
assert state["game_number"] == 1
|
|
assert state["round"] == 1
|
|
assert state["leader"] is None
|
|
|
|
@pytest.mark.parametrize(
|
|
"top,bot,expected_game",
|
|
[(0, 0, 1), (1, 0, 2), (1, 1, 3), (2, 1, 4), (2, 2, 5), (3, 2, 6), (3, 3, 7)],
|
|
)
|
|
def test_game_number_computation(self, top, bot, expected_game):
|
|
state = series_state({"round": 1, "topSeedWins": top, "bottomSeedWins": bot})
|
|
assert state["game_number"] == expected_game
|
|
|
|
def test_game7_predicate(self):
|
|
state = series_state({"round": 1, "topSeedWins": 3, "bottomSeedWins": 3})
|
|
assert state["is_game7"] is True
|
|
assert state["is_clincher"] is False
|
|
assert state["is_pivotal"] is False
|
|
|
|
def test_clincher_predicate(self):
|
|
state = series_state({"round": 1, "topSeedWins": 3, "bottomSeedWins": 1})
|
|
assert state["is_clincher"] is True
|
|
assert state["is_elimination"] is True
|
|
assert state["is_game7"] is False
|
|
|
|
def test_pivotal_predicate(self):
|
|
state = series_state({"round": 2, "topSeedWins": 2, "bottomSeedWins": 2})
|
|
assert state["is_pivotal"] is True
|
|
assert state["is_game7"] is False
|
|
assert state["is_clincher"] is False
|
|
|
|
def test_opener_predicate(self):
|
|
state = series_state({"round": 1, "topSeedWins": 0, "bottomSeedWins": 0})
|
|
assert state["is_opener"] is True
|
|
|
|
def test_leader_top(self):
|
|
state = series_state({"round": 1, "topSeedWins": 2, "bottomSeedWins": 1})
|
|
assert state["leader"] == "top"
|
|
assert state["hi"] == 2 and state["lo"] == 1
|
|
|
|
def test_leader_bottom(self):
|
|
state = series_state({"round": 1, "topSeedWins": 1, "bottomSeedWins": 2})
|
|
assert state["leader"] == "bottom"
|
|
|
|
def test_no_leader_when_tied(self):
|
|
state = series_state({"round": 1, "topSeedWins": 1, "bottomSeedWins": 1})
|
|
assert state["leader"] is None
|
|
|
|
|
|
class TestSeriesBlurb:
|
|
def test_opener_blurb(self):
|
|
game = make_playoff_game(top_wins=0, bottom_wins=0)
|
|
assert series_blurb(game) == "Series opener."
|
|
|
|
def test_game7_blurb(self):
|
|
game = make_playoff_game(top_wins=3, bottom_wins=3)
|
|
assert series_blurb(game) == "Win-or-go-home \u2014 Game 7."
|
|
|
|
def test_clincher_blurb_names_leader(self):
|
|
game = make_playoff_game(
|
|
top_wins=3,
|
|
bottom_wins=1,
|
|
top_abbrev="TOR",
|
|
bottom_abbrev="BOS",
|
|
top_is_home=True,
|
|
)
|
|
blurb = series_blurb(game)
|
|
assert "Top Seeds" in blurb
|
|
assert "close it out" in blurb
|
|
assert "Game 5" in blurb
|
|
|
|
def test_pivotal_blurb(self):
|
|
game = make_playoff_game(top_wins=2, bottom_wins=2)
|
|
assert "pivotal" in series_blurb(game).lower()
|
|
assert "Game 5" in series_blurb(game)
|
|
|
|
def test_leader_trailer_blurb(self):
|
|
game = make_playoff_game(top_wins=2, bottom_wins=1)
|
|
blurb = series_blurb(game)
|
|
assert "leads" in blurb
|
|
assert "Game 4" in blurb
|
|
|
|
def test_tied_mid_series_blurb(self):
|
|
game = make_playoff_game(top_wins=1, bottom_wins=1)
|
|
blurb = series_blurb(game)
|
|
assert "1" in blurb
|
|
assert "Game 3" in blurb
|
|
|
|
|
|
class TestSeriesBadges:
|
|
def test_round_1_always_first(self):
|
|
game = make_playoff_game(round_num=1, top_wins=1, bottom_wins=0)
|
|
assert series_badges(game)[0] == "R1"
|
|
|
|
def test_cup_final_label(self):
|
|
game = make_playoff_game(round_num=4, top_wins=0, bottom_wins=0)
|
|
assert series_badges(game)[0] == "CUP FINAL"
|
|
|
|
def test_conf_final_label(self):
|
|
game = make_playoff_game(round_num=3, top_wins=0, bottom_wins=0)
|
|
assert series_badges(game)[0] == "CONF FINAL"
|
|
|
|
def test_game7_badge(self):
|
|
game = make_playoff_game(top_wins=3, bottom_wins=3)
|
|
assert "GAME 7" in series_badges(game)
|
|
|
|
def test_clincher_badge(self):
|
|
game = make_playoff_game(top_wins=3, bottom_wins=1)
|
|
assert "CLINCHER" in series_badges(game)
|
|
|
|
def test_pivotal_badge(self):
|
|
game = make_playoff_game(top_wins=2, bottom_wins=2)
|
|
assert "PIVOTAL" in series_badges(game)
|
|
|
|
def test_opener_has_no_stake_badge(self):
|
|
badges = series_badges(make_playoff_game(top_wins=0, bottom_wins=0))
|
|
assert badges == ["R1"]
|
|
|
|
|
|
class TestSeriesSummary:
|
|
def test_opener_summary(self):
|
|
game = make_playoff_game(top_wins=0, bottom_wins=0, round_num=1)
|
|
assert "Round 1" in series_summary(game)
|
|
|
|
def test_leader_summary(self):
|
|
game = make_playoff_game(top_wins=2, bottom_wins=1)
|
|
assert "leads" in series_summary(game)
|
|
|
|
def test_tied_mid_series_summary(self):
|
|
game = make_playoff_game(top_wins=1, bottom_wins=1)
|
|
assert "tied" in series_summary(game).lower()
|
|
|
|
|
|
class TestIsPinned:
|
|
def test_game7_live_is_pinned(self):
|
|
game = make_playoff_game(top_wins=3, bottom_wins=3, game_state="LIVE")
|
|
assert is_pinned(game) is True
|
|
|
|
def test_game7_pre_is_pinned(self):
|
|
game = make_playoff_game(top_wins=3, bottom_wins=3, game_state="FUT")
|
|
assert is_pinned(game) is True
|
|
|
|
def test_game7_final_not_pinned(self):
|
|
game = make_playoff_game(top_wins=3, bottom_wins=3, game_state="OFF")
|
|
assert is_pinned(game) is False
|
|
|
|
def test_non_game7_not_pinned(self):
|
|
game = make_playoff_game(top_wins=2, bottom_wins=1)
|
|
assert is_pinned(game) is False
|
|
|
|
def test_regular_season_not_pinned(self):
|
|
game = make_game() # game_type=2, no series
|
|
assert is_pinned(game) is False
|
|
|
|
|
|
class TestIsPlayoffOt:
|
|
def test_playoff_period_4_live(self):
|
|
game = make_playoff_game(period=4, game_state="LIVE")
|
|
assert is_playoff_ot(game) is True
|
|
|
|
def test_playoff_period_5_live(self):
|
|
game = make_playoff_game(period=5, game_state="LIVE")
|
|
assert is_playoff_ot(game) is True
|
|
|
|
def test_playoff_period_3_not_ot(self):
|
|
game = make_playoff_game(period=3, game_state="LIVE")
|
|
assert is_playoff_ot(game) is False
|
|
|
|
def test_regular_season_ot_not_playoff_ot(self):
|
|
game = make_game(period=4, game_state="LIVE", game_type=2)
|
|
assert is_playoff_ot(game) is False
|
|
|
|
def test_crit_state_counts_as_live(self):
|
|
game = make_playoff_game(period=4, game_state="CRIT")
|
|
assert is_playoff_ot(game) is True
|
|
|
|
def test_final_state_not_playoff_ot(self):
|
|
game = make_playoff_game(period=4, game_state="OFF")
|
|
assert is_playoff_ot(game) is False
|
|
|
|
|
|
class TestOtLabel:
|
|
def test_period_4_is_ot(self):
|
|
assert ot_label(4) == "OT"
|
|
|
|
def test_period_5_is_2ot(self):
|
|
assert ot_label(5) == "2OT"
|
|
|
|
def test_period_6_is_3ot(self):
|
|
assert ot_label(6) == "3OT"
|
|
|
|
def test_pre_ot_returns_empty(self):
|
|
assert ot_label(3) == ""
|
|
assert ot_label(0) == ""
|
|
|
|
|
|
class TestIsPlayoffGame:
|
|
def test_playoff_raw_shape(self):
|
|
assert is_playoff_game(make_playoff_game()) is True
|
|
|
|
def test_regular_raw_shape(self):
|
|
assert is_playoff_game(make_game(game_type=2)) is False
|
|
|
|
def test_parsed_shape(self):
|
|
# parse_games produces {"Game Type": 3} — is_playoff_game should handle both
|
|
assert is_playoff_game({"Game Type": 3}) is True
|
|
assert is_playoff_game({"Game Type": 2}) is False
|
|
|
|
|
|
class TestTodayMeta:
|
|
def test_no_playoff_games_off_mode(self):
|
|
meta = today_meta([make_game(game_type=2)])
|
|
assert meta["playoff_mode"] is False
|
|
assert meta["round_label"] is None
|
|
|
|
def test_playoff_games_on_mode(self):
|
|
games = [make_playoff_game(series_letter="A"), make_playoff_game(series_letter="B")]
|
|
meta = today_meta(games)
|
|
assert meta["playoff_mode"] is True
|
|
assert meta["series_active"] == 2
|
|
assert meta["round_label"] == "First Round"
|
|
|
|
def test_counts_game7(self):
|
|
games = [
|
|
make_playoff_game(top_wins=3, bottom_wins=3, series_letter="A"),
|
|
make_playoff_game(top_wins=1, bottom_wins=0, series_letter="B"),
|
|
]
|
|
meta = today_meta(games)
|
|
assert meta["game7_count"] == 1
|
|
assert meta["elimination_count"] == 0
|
|
|
|
def test_counts_elimination_games(self):
|
|
games = [
|
|
make_playoff_game(top_wins=3, bottom_wins=1, series_letter="A"),
|
|
make_playoff_game(top_wins=3, bottom_wins=2, series_letter="B"),
|
|
]
|
|
meta = today_meta(games)
|
|
assert meta["elimination_count"] == 2
|
|
assert meta["game7_count"] == 0
|
|
|
|
def test_round_label_reflects_highest_active_round(self):
|
|
games = [
|
|
make_playoff_game(round_num=1, series_letter="A"),
|
|
make_playoff_game(round_num=2, series_letter="I"),
|
|
]
|
|
meta = today_meta(games)
|
|
assert meta["round_label"] == "Second Round"
|
|
|
|
def test_cup_final_label(self):
|
|
games = [make_playoff_game(round_num=4, series_letter="P")]
|
|
meta = today_meta(games)
|
|
assert meta["round_label"] == "Stanley Cup Final"
|