feat: make scoreboard playoff-aware with banner, bracket, and series drill-down
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>
This commit is contained in:
+102
-1
@@ -1,5 +1,5 @@
|
||||
import app.games
|
||||
from tests.conftest import make_game
|
||||
from tests.conftest import make_game, make_playoff_game
|
||||
from app.games import (
|
||||
_get_man_advantage,
|
||||
calculate_game_importance,
|
||||
@@ -745,6 +745,107 @@ class TestGetComebackBonus:
|
||||
assert result == 70 # 50*1.0 + 20
|
||||
|
||||
|
||||
class TestPlayoffEnrichment:
|
||||
_FULL_STANDINGS = {
|
||||
"league_sequence": 16,
|
||||
"league_l10_sequence": 16,
|
||||
"division_abbrev": "ATL",
|
||||
"conference_abbrev": "E",
|
||||
"games_played": 40,
|
||||
"wildcard_sequence": 16,
|
||||
}
|
||||
|
||||
def test_regular_season_game_has_empty_playoff_fields(self, mocker):
|
||||
mocker.patch(
|
||||
"app.games.get_team_standings", return_value=self._FULL_STANDINGS
|
||||
)
|
||||
result = parse_games({"games": [make_game()]})
|
||||
g = result[0]
|
||||
assert g["Is Playoff"] is False
|
||||
assert g["Pinned"] is False
|
||||
assert g["Playoff OT"] is False
|
||||
assert g["Series Blurb"] == ""
|
||||
assert g["Series Badges"] == []
|
||||
|
||||
def test_playoff_game_gets_series_fields(self, mocker):
|
||||
mocker.patch(
|
||||
"app.games.get_team_standings",
|
||||
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||
)
|
||||
game = make_playoff_game(top_wins=2, bottom_wins=1, round_num=1)
|
||||
result = parse_games({"games": [game]})
|
||||
g = result[0]
|
||||
assert g["Is Playoff"] is True
|
||||
assert g["Pinned"] is False
|
||||
assert "Game 4" in g["Series Blurb"]
|
||||
assert "R1" in g["Series Badges"]
|
||||
|
||||
def test_game7_is_pinned(self, mocker):
|
||||
mocker.patch(
|
||||
"app.games.get_team_standings",
|
||||
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||
)
|
||||
game = make_playoff_game(top_wins=3, bottom_wins=3)
|
||||
result = parse_games({"games": [game]})
|
||||
assert result[0]["Pinned"] is True
|
||||
assert "GAME 7" in result[0]["Series Badges"]
|
||||
|
||||
def test_pinned_game_sorts_first(self, mocker):
|
||||
mocker.patch(
|
||||
"app.games.get_team_standings", return_value=self._FULL_STANDINGS
|
||||
)
|
||||
# Low-priority Game 7 PRE should sort above a high-priority non-playoff LIVE
|
||||
g7_pre = make_playoff_game(
|
||||
top_wins=3,
|
||||
bottom_wins=3,
|
||||
game_state="FUT",
|
||||
period=0,
|
||||
seconds_remaining=1200,
|
||||
start_time_utc="2026-04-20T23:00:00Z",
|
||||
home_name="Kings",
|
||||
away_name="Oilers",
|
||||
)
|
||||
hype_live = make_game(
|
||||
game_state="LIVE",
|
||||
home_name="Rangers",
|
||||
away_name="Devils",
|
||||
home_score=2,
|
||||
away_score=2,
|
||||
period=3,
|
||||
seconds_remaining=60,
|
||||
)
|
||||
result = parse_games({"games": [hype_live, g7_pre]})
|
||||
assert result[0]["Home Team"] == "Kings"
|
||||
assert result[0]["Pinned"] is True
|
||||
|
||||
def test_playoff_ot_flagged(self, mocker):
|
||||
mocker.patch(
|
||||
"app.games.get_team_standings",
|
||||
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||
)
|
||||
game = make_playoff_game(
|
||||
top_wins=1,
|
||||
bottom_wins=1,
|
||||
period=4,
|
||||
seconds_remaining=600,
|
||||
game_state="LIVE",
|
||||
)
|
||||
result = parse_games({"games": [game]})
|
||||
assert result[0]["Playoff OT"] is True
|
||||
assert result[0]["OT Label"] == "OT"
|
||||
|
||||
def test_regular_season_ot_not_flagged_as_playoff_ot(self, mocker):
|
||||
mocker.patch(
|
||||
"app.games.get_team_standings", return_value=self._FULL_STANDINGS
|
||||
)
|
||||
game = make_game(
|
||||
game_state="LIVE", period=4, seconds_remaining=180, game_type=2
|
||||
)
|
||||
result = parse_games({"games": [game]})
|
||||
assert result[0]["Playoff OT"] is False
|
||||
assert result[0]["OT Label"] == ""
|
||||
|
||||
|
||||
class TestCalculateGameImportance:
|
||||
def _standings(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user