▍ humdrum codex / sportsball
license AGPL-3.0
4.6 KB raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package ui

import (
	"testing"
	"time"

	"github.com/humdrum-tiv/sportsball/internal/model"
)

// tickerGames must exclude the watched game and order league-first then state:
// same-league live, same-league final, other-league live, other-league final.
// Only today's games count; pre-game and other days are dropped.
func TestTickerGamesOrderingAndExclusion(t *testing.T) {
	now := time.Now()
	a := App{
		leagues: model.Leagues,
		detail:  model.Game{ID: "watch", League: model.MLB},
		games: map[model.LeagueID][]model.Game{
			model.MLB: {
				{ID: "watch", League: model.MLB, State: model.StateLive, Start: now},
				{ID: "mlb-live", League: model.MLB, State: model.StateLive, Start: now},
				{ID: "mlb-final", League: model.MLB, State: model.StateFinal, Start: now},
				{ID: "mlb-pre", League: model.MLB, State: model.StatePre, Start: now},
				{ID: "mlb-old", League: model.MLB, State: model.StateFinal, Start: now.AddDate(0, 0, -2)},
			},
			model.NBA: {
				{ID: "nba-live", League: model.NBA, State: model.StateLive, Start: now},
				{ID: "nba-final", League: model.NBA, State: model.StateFinal, Start: now},
			},
		},
	}

	got := a.tickerGames()
	var ids []string
	for _, g := range got {
		ids = append(ids, g.ID)
	}

	want := []string{"mlb-live", "mlb-final", "nba-live", "nba-final"}
	if len(ids) != len(want) {
		t.Fatalf("got %v, want %v", ids, want)
	}
	for i := range want {
		if ids[i] != want[i] {
			t.Fatalf("order mismatch at %d: got %v, want %v", i, ids, want)
		}
	}
}

// A favorited game must lead the ticker even when it's a final and the live
// games belong to the watched league.
func TestTickerGamesFavoritesLead(t *testing.T) {
	now := time.Now()
	fav := model.Team{ID: "22", Abbr: "PHI"}
	a := App{
		leagues: model.Leagues,
		detail:  model.Game{ID: "watch", League: model.MLB},
		favs:    map[string]bool{favKey(model.NBA, fav): true},
		games: map[model.LeagueID][]model.Game{
			model.MLB: {
				{ID: "mlb-live", League: model.MLB, State: model.StateLive, Start: now},
			},
			model.NBA: {
				{ID: "nba-fav-final", League: model.NBA, State: model.StateFinal, Start: now, Home: fav},
			},
		},
	}
	got := a.tickerGames()
	if len(got) == 0 || got[0].ID != "nba-fav-final" {
		t.Fatalf("favorite game should lead, got %v", got)
	}
}

// The previously-viewed game (back-breadcrumb) must lead the ticker, ahead of
// even favorites.
func TestTickerGamesPrevLeads(t *testing.T) {
	now := time.Now()
	fav := model.Team{ID: "22", Abbr: "PHI"}
	a := App{
		leagues:    model.Leagues,
		detail:     model.Game{ID: "watch", League: model.MLB},
		detailPrev: "nba-prev",
		favs:       map[string]bool{favKey(model.MLB, fav): true},
		games: map[model.LeagueID][]model.Game{
			model.MLB: {
				{ID: "mlb-fav-live", League: model.MLB, State: model.StateLive, Start: now, Home: fav},
			},
			model.NBA: {
				{ID: "nba-prev", League: model.NBA, State: model.StateFinal, Start: now},
			},
		},
	}
	got := a.tickerGames()
	if len(got) == 0 || got[0].ID != "nba-prev" {
		t.Fatalf("previous game should lead, got %v", got)
	}
}

// detailView with a populated ticker must render without panicking and include
// a sibling game's abbreviation in the strip.
func TestDetailViewRendersTicker(t *testing.T) {
	now := time.Now()
	a := App{
		width: 120, height: 40,
		leagues:    model.Leagues,
		mode:       viewDetail,
		detail:     model.Game{ID: "watch", League: model.MLB, State: model.StateLive, Start: now, Home: model.Team{Abbr: "PHI"}, Away: model.Team{Abbr: "NYM"}},
		games:      map[model.LeagueID][]model.Game{model.MLB: {{ID: "sib", League: model.MLB, State: model.StateLive, Start: now, Home: model.Team{Abbr: "LAD"}, Away: model.Team{Abbr: "SDP"}}}},
		detailData: map[string]model.GameDetail{},
	}
	out := a.detailView()
	if out == "" {
		t.Fatal("empty detailView output")
	}
	if !contains(out, "LAD") || !contains(out, "SDP") {
		t.Errorf("ticker missing sibling game abbrs in output")
	}
}

func contains(s, sub string) bool {
	for i := 0; i+len(sub) <= len(s); i++ {
		if s[i:i+len(sub)] == sub {
			return true
		}
	}
	return false
}

// tickerWindow keeps the selected index inside the returned [lo,hi) span.
func TestTickerWindowKeepsSelectionVisible(t *testing.T) {
	cases := []struct{ n, sel, fit int }{
		{10, 0, 4}, {10, 9, 4}, {10, 5, 4}, {3, 2, 4},
	}
	for _, c := range cases {
		lo, hi := tickerWindow(c.n, c.sel, c.fit)
		if lo < 0 || hi > c.n || lo > hi {
			t.Fatalf("invalid window [%d,%d) for %+v", lo, hi, c)
		}
		if c.sel < lo || c.sel >= hi {
			t.Errorf("sel %d outside window [%d,%d) for %+v", c.sel, lo, hi, c)
		}
		if c.n >= c.fit && hi-lo != c.fit {
			t.Errorf("window width %d != fit %d for %+v", hi-lo, c.fit, c)
		}
	}
}