Files
NHL-Scoreboard/tests/test_playoff_series.py
T
josh 4f5871d119
CI / Lint (push) Successful in 15s
CI / Test (push) Successful in 16s
CI / Build & Push (push) Successful in 33s
fix: suppress misleading "Game X of 7" on future-date playoff cards
FUT games from the score API carry the current seriesStatus, not the
anticipated state, so all future games in a series computed the same
game number. Skip the summary for FUT games; badges and blurbs still
render.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:31:04 -04:00

333 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
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
def test_pivotal_blurb(self):
game = make_playoff_game(top_wins=2, bottom_wins=2)
assert "pivotal" in series_blurb(game).lower()
def test_leader_trailer_blurb(self):
game = make_playoff_game(top_wins=2, bottom_wins=1)
blurb = series_blurb(game)
assert "lead" in blurb
assert "2\u20111" in blurb
def test_tied_mid_series_blurb(self):
game = make_playoff_game(top_wins=1, bottom_wins=1)
blurb = series_blurb(game)
assert "11" in blurb
def test_final_clincher_falls_through_to_leader_blurb(self):
# Post-game seriesStatus (3-0) would trigger the clincher branch, but
# the FINAL card is already decided — that stake belongs to Game 4.
game = make_playoff_game(
top_wins=3,
bottom_wins=0,
top_abbrev="PHI",
bottom_abbrev="PIT",
top_is_home=True,
game_state="OFF",
)
blurb = series_blurb(game)
assert "close it out" not in blurb
assert "lead" in blurb
assert "30" 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"]
def test_no_stake_badge_on_final(self):
# Post-game seriesStatus shows is_clincher true, but CLINCHER refers to
# the upcoming Game 4, not the completed card.
game = make_playoff_game(top_wins=3, bottom_wins=0, game_state="OFF")
assert series_badges(game) == ["R1"]
class TestSeriesSummary:
def test_opener_summary(self):
game = make_playoff_game(top_wins=0, bottom_wins=0, round_num=1)
assert series_summary(game) == "Game 1 of 7"
def test_leader_summary(self):
game = make_playoff_game(top_wins=2, bottom_wins=1)
assert series_summary(game) == "Game 4 of 7"
def test_tied_mid_series_summary(self):
game = make_playoff_game(top_wins=1, bottom_wins=1)
assert series_summary(game) == "Game 3 of 7"
def test_finished_game_uses_pre_advance_number(self):
# Scoreboard payloads don't carry gameNumber. Once a game goes FINAL,
# seriesStatus already includes this game's result, so the card's game
# number is hi+lo, not hi+lo+1.
game = make_playoff_game(top_wins=1, bottom_wins=0, game_state="FINAL")
assert series_summary(game) == "Game 1 of 7"
def test_finished_game_honors_explicit_game_number(self):
game = make_playoff_game(
top_wins=2, bottom_wins=0, game_state="FINAL", game_number=2
)
assert series_summary(game) == "Game 2 of 7"
def test_fut_game_suppresses_summary(self):
game = make_playoff_game(top_wins=1, bottom_wins=1, game_state="FUT")
assert series_summary(game) == ""
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"
def test_does_not_count_final_games_as_elimination(self):
games = [
make_playoff_game(
top_wins=3, bottom_wins=0, series_letter="A", game_state="OFF"
),
make_playoff_game(
top_wins=3, bottom_wins=1, series_letter="B", game_state="LIVE"
),
]
meta = today_meta(games)
# Only the LIVE card counts; the FINAL card describes a completed game.
assert meta["elimination_count"] == 1
def test_does_not_count_final_game7(self):
games = [
make_playoff_game(
top_wins=3, bottom_wins=3, series_letter="A", game_state="OFF"
)
]
meta = today_meta(games)
assert meta["game7_count"] == 0