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>
150 lines
4.4 KiB
Python
150 lines
4.4 KiB
Python
import json
|
|
import sqlite3
|
|
|
|
import pytest
|
|
|
|
|
|
def make_game(
|
|
game_state="LIVE",
|
|
home_name="Maple Leafs",
|
|
away_name="Bruins",
|
|
home_score=2,
|
|
away_score=1,
|
|
period=3,
|
|
seconds_remaining=300,
|
|
in_intermission=False,
|
|
start_time_utc="2024-04-10T23:00:00Z",
|
|
home_record="40-25-10",
|
|
away_record="38-27-09",
|
|
game_type=2,
|
|
situation=None,
|
|
series_status=None,
|
|
home_abbrev="TOR",
|
|
away_abbrev="BOS",
|
|
):
|
|
clock = {
|
|
"timeRemaining": f"{seconds_remaining // 60:02d}:{seconds_remaining % 60:02d}",
|
|
"secondsRemaining": seconds_remaining,
|
|
"running": game_state == "LIVE",
|
|
"inIntermission": in_intermission,
|
|
}
|
|
return {
|
|
"gameState": game_state,
|
|
"startTimeUTC": start_time_utc,
|
|
"periodDescriptor": {"number": period},
|
|
"clock": clock,
|
|
"homeTeam": {
|
|
"name": {"default": home_name},
|
|
"abbrev": home_abbrev,
|
|
"score": home_score,
|
|
"sog": 15,
|
|
"logo": "https://example.com/home.png",
|
|
"record": home_record,
|
|
},
|
|
"awayTeam": {
|
|
"name": {"default": away_name},
|
|
"abbrev": away_abbrev,
|
|
"score": away_score,
|
|
"sog": 12,
|
|
"logo": "https://example.com/away.png",
|
|
"record": away_record,
|
|
},
|
|
"gameOutcome": {"lastPeriodType": "REG"},
|
|
"gameType": game_type,
|
|
**({"situation": situation} if situation is not None else {}),
|
|
**({"seriesStatus": series_status} if series_status is not None else {}),
|
|
}
|
|
|
|
|
|
def make_playoff_game(
|
|
top_wins=0,
|
|
bottom_wins=0,
|
|
round_num=1,
|
|
series_letter="A",
|
|
top_abbrev="TOR",
|
|
bottom_abbrev="BOS",
|
|
top_is_home=True,
|
|
game_state="LIVE",
|
|
**kwargs,
|
|
):
|
|
"""Convenience wrapper around make_game for playoff fixtures.
|
|
|
|
`top_is_home` controls which side of the matchup hosts this game, so tests
|
|
for the blurb copy (leader vs. trailer names) don't have to juggle raw dicts.
|
|
"""
|
|
series_status = {
|
|
"round": round_num,
|
|
"topSeedWins": top_wins,
|
|
"bottomSeedWins": bottom_wins,
|
|
"seriesLetter": series_letter,
|
|
"topSeedTeamAbbrev": top_abbrev,
|
|
"bottomSeedTeamAbbrev": bottom_abbrev,
|
|
}
|
|
if top_is_home:
|
|
home_abbrev, away_abbrev = top_abbrev, bottom_abbrev
|
|
home_name, away_name = "Top Seeds", "Bottom Seeds"
|
|
else:
|
|
home_abbrev, away_abbrev = bottom_abbrev, top_abbrev
|
|
home_name, away_name = "Bottom Seeds", "Top Seeds"
|
|
|
|
return make_game(
|
|
game_state=game_state,
|
|
game_type=3,
|
|
series_status=series_status,
|
|
home_abbrev=kwargs.pop("home_abbrev", home_abbrev),
|
|
away_abbrev=kwargs.pop("away_abbrev", away_abbrev),
|
|
home_name=kwargs.pop("home_name", home_name),
|
|
away_name=kwargs.pop("away_name", away_name),
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
LIVE_GAME = make_game()
|
|
PRE_GAME = make_game(
|
|
game_state="FUT", home_score=0, away_score=0, period=0, seconds_remaining=1200
|
|
)
|
|
FINAL_GAME = make_game(game_state="OFF", period=3, seconds_remaining=0)
|
|
|
|
SAMPLE_SCOREBOARD = {"games": [LIVE_GAME, PRE_GAME, FINAL_GAME]}
|
|
|
|
|
|
@pytest.fixture()
|
|
def sample_scoreboard():
|
|
return SAMPLE_SCOREBOARD
|
|
|
|
|
|
@pytest.fixture()
|
|
def flask_client(tmp_path, monkeypatch):
|
|
data_dir = tmp_path / "data"
|
|
data_dir.mkdir()
|
|
|
|
# Write sample scoreboard JSON
|
|
scoreboard_file = data_dir / "scoreboard_data.json"
|
|
scoreboard_file.write_text(json.dumps(SAMPLE_SCOREBOARD))
|
|
|
|
# Create minimal SQLite DB so get_team_standings doesn't crash
|
|
db_path = data_dir / "nhl_standings.db"
|
|
conn = sqlite3.connect(str(db_path))
|
|
conn.execute(
|
|
"CREATE TABLE standings "
|
|
"(team_common_name TEXT, league_sequence INTEGER, league_l10_sequence INTEGER, "
|
|
"division_abbrev TEXT, conference_abbrev TEXT, games_played INTEGER, wildcard_sequence INTEGER)"
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# Patch module-level path constants so no reloads are needed
|
|
import app.routes as routes
|
|
import app.games as games
|
|
import app.playoff_cache as playoff_cache
|
|
|
|
monkeypatch.setattr(routes, "SCOREBOARD_DATA_FILE", str(scoreboard_file))
|
|
monkeypatch.setattr(games, "DB_PATH", str(db_path))
|
|
monkeypatch.setattr(playoff_cache, "DB_PATH", str(db_path))
|
|
|
|
from app import app as flask_app
|
|
|
|
flask_app.config["TESTING"] = True
|
|
with flask_app.test_client() as client:
|
|
yield client
|