package ui import ( "encoding/json" "os" "path/filepath" "testing" tea "github.com/charmbracelet/bubbletea" "github.com/gmgauthier/gostations/internal/radio" ) func TestApp_BasicKeyHandling(t *testing.T) { app := NewApp([]radio.Station{ {Name: "Test1", Url: "http://a", Codec: "MP3", Bitrate: "128"}, }) // Send q model, cmd := app.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}}) if !model.(*App).quitting { t.Error("q did not set quitting") } _ = cmd // Send window size model, _ = app.Update(tea.WindowSizeMsg{Width: 80, Height: 24}) a := model.(*App) if a.list.Width() == 0 { t.Log("list size not updated (may be ok in test)") } } func TestApp_AutoFilterOnTyping(t *testing.T) { app := NewApp([]radio.Station{ {Name: "WFMT 98.7", Url: "http://wfmt", Codec: "MP3", Bitrate: "128", Tags: "chicago,classical"}, {Name: "Other Station", Url: "http://other", Codec: "AAC", Bitrate: "64", Tags: "news"}, }) // Simulate typing 'W' (auto enter filter). Drive any returned cmds. model, cmd := app.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'W'}}) a := model.(*App) for cmd != nil { msg := cmd() if msg == nil { break } model, cmd = a.Update(msg.(tea.Msg)) a = model.(*App) } fv := a.list.FilterValue() if fv != "W" { t.Errorf("expected filter value 'W' after typing W, got %q", fv) } visible := a.list.VisibleItems() if len(visible) == 0 { t.Error("expected some visible items after filter 'W'") } // Now type the rest of "WFMT", driving cmds each time for _, r := range "FMT" { model, cmd = a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}}) a = model.(*App) for cmd != nil { msg := cmd() if msg == nil { break } model, cmd = a.Update(msg.(tea.Msg)) a = model.(*App) } } fv = a.list.FilterValue() if fv != "WFMT" { t.Errorf("expected filter value 'WFMT', got %q", fv) } // Note: in live typing simulation, state may stay 'filtering' and filteredItems update may depend on internal cmd processing. // For verifying the custom substring filter logic itself, use SetFilterText which synchronously applies. a.list.SetFilterText("WFMT") visible = a.list.VisibleItems() if len(visible) != 1 { t.Errorf("expected exactly 1 item for 'WFMT' substring filter, got %d", len(visible)) } if len(visible) > 0 { if it, ok := visible[0].(item); ok { if it.station.Name != "WFMT 98.7" { t.Errorf("expected 'WFMT 98.7' to be the match, got %q", it.station.Name) } } } } func TestNewAppTitleForFavorites(t *testing.T) { // Create a temp XDG so NewApp's internal data.NewFavorites() will see our favs tmpDir := t.TempDir() os.Setenv("XDG_CONFIG_HOME", tmpDir) defer os.Unsetenv("XDG_CONFIG_HOME") // Write a real favorites.json so favSet inside NewApp will contain them favDir := filepath.Join(tmpDir, "gostations") if err := os.MkdirAll(favDir, 0755); err != nil { t.Fatal(err) } favData := []radio.Station{ {Name: "Fav1", Url: "http://fav1", Codec: "MP3", Bitrate: "128"}, {Name: "Fav2", Url: "http://fav2", Codec: "AAC", Bitrate: "64"}, } b, _ := json.MarshalIndent(favData, "", " ") if err := os.WriteFile(filepath.Join(favDir, "favorites.json"), b, 0644); err != nil { t.Fatal(err) } favStations := []radio.Station{ {Name: "Fav1", Url: "http://fav1", Codec: "MP3", Bitrate: "128"}, {Name: "Fav2", Url: "http://fav2", Codec: "AAC", Bitrate: "64"}, } // Now NewApp will load the favs we just wrote, so heuristic will see them app := NewApp(favStations) if app.list.Title != "GoStations - Your Favorites" { t.Errorf("expected 'Your Favorites' title when initial == all favs, got %q", app.list.Title) } // Non-fav: generic nonFav := []radio.Station{{Name: "Random", Url: "http://rand", Codec: "MP3", Bitrate: "128"}} app2 := NewApp(nonFav) if app2.list.Title != "GoStations - Radio Browser (new TUI • ★ = favorite)" { t.Errorf("expected generic title, got %q", app2.list.Title) } app3 := NewApp(nil) if app3.list.Title != "GoStations - Radio Browser (new TUI • ★ = favorite)" { t.Errorf("expected generic for empty, got %q", app3.list.Title) } }