package main import ( "bytes" "os" "path/filepath" "regexp" "strings" "testing" "time" ) // The tests never touch the network: they parse committed HTML slices. That is // what makes the suite deterministic, and it is why a change in the site's // markup shows up as a failing test with a readable message instead of as // silently different data. func parseFixture(t *testing.T, name string) *Schedule { t.Helper() f, err := os.Open(filepath.Join("testdata", name)) if err != nil { t.Fatal(err) } defer f.Close() s, err := ParseSchedule(20961, f) if err != nil { t.Fatalf("ParseSchedule(%s): %v", name, err) } return s } // TestGolden pins the exact bytes of the rendered output. Same HTML in, same // bytes out - that is the property the whole design exists to provide. func TestGolden(t *testing.T) { for _, tc := range []struct{ fixture, golden string }{ {"schedule_20961.html", "schedule_20961.json"}, {"schedule_legacy.html", "schedule_legacy.json"}, {"schedule_legacy_playoffs.html", "schedule_legacy_playoffs.json"}, } { t.Run(tc.fixture, func(t *testing.T) { f, err := os.Open(filepath.Join("testdata", tc.fixture)) if err != nil { t.Fatal(err) } defer f.Close() s, err := ParseSchedule(20961, f) if err != nil { t.Fatalf("parse: %v", err) } got, err := render("json", s) if err != nil { t.Fatal(err) } want, err := os.ReadFile(filepath.Join("testdata", tc.golden)) if err != nil { t.Fatalf("read golden (regenerate with -update flag): %v", err) } if !bytes.Equal(got, want) { t.Errorf("output drifted from the golden file:\n%s", firstDiff(want, got)) } }) } } func TestModernLayout(t *testing.T) { s := parseFixture(t, "schedule_20961.html") if s.Layout != "modern" { t.Errorf("layout = %q, want modern", s.Layout) } if s.LastUpdate != "2026-09-26 22:41" { t.Errorf("last update = %q", s.LastUpdate) } if len(s.Games) != 10 || s.Played() != 8 { t.Fatalf("games = %d, played = %d; want 10/8", len(s.Games), s.Played()) } // Date headings appear only on the first row of a date; the rest inherit. // A date is printed only on the first row of its group; the rest inherit it. want := []string{"2026-09-19", "2026-09-19", "2026-09-19", "2026-09-19", "2026-09-19", "2026-09-19", "2026-09-22", "2026-09-22", "2027-03-16", "2027-03-16"} for i, g := range s.Games { if g.Date != want[i] { t.Errorf("game %d: date = %s, want %s", i, g.Date, want[i]) } } // Overtime (4 periods) and a shootout (5 periods) keep every period. if g := s.Games[1]; len(g.Periods) != 4 || *g.HomeGoals != 4 { t.Errorf("game 2: %d periods, %d goals", len(g.Periods), *g.HomeGoals) } if g := s.Games[6]; len(g.Periods) != 5 || *g.HomeGoals != 3 { t.Errorf("game 7: %d periods, %d goals", len(g.Periods), *g.HomeGoals) } // An unplayed game has a result of nil, not 0-0, and no periods. if g := s.Games[len(s.Games)-1]; g.Played() || g.HomeGoals != nil || g.AwayGoals != nil || len(g.Periods) != 0 { t.Errorf("last game should be unplayed: %+v", g) } if g := s.Games[0]; g.Home != "Frölunda HC" || g.Away != "Växjö Lakers HC" { t.Errorf("teams not normalised: %q vs %q", g.Home, g.Away) } if g := s.Games[0]; g.EventID != "1109941" || g.Spectators == nil || *g.Spectators != 12044 { t.Errorf("event id / spectators: %+v", g) } } func TestLegacyLayout(t *testing.T) { s := parseFixture(t, "schedule_legacy.html") if s.Layout != "legacy" { t.Errorf("layout = %q, want legacy", s.Layout) } g := s.Games[0] // The legacy cell holds "