package picker import ( "strings" "testing" "time" "github.com/charmbracelet/lipgloss" "gitea.apointless.space/bsncubed/dial/internal/sshconf" "gitea.apointless.space/bsncubed/dial/internal/store" ) func sampleModel() *model { hosts := []sshconf.Host{ {Alias: "web1", HostName: "10.0.0.5", User: "deploy", Port: "2222", ProxyJump: "bastion", Tags: []string{"prod", "riedel"}}, {Alias: "db-primary", HostName: "db.internal", User: "postgres", Port: "22", Tags: []string{"prod"}}, {Alias: "bastion", HostName: "bastion.example.com", User: "admin"}, {Alias: "staging-web", HostName: "10.1.0.9", User: "deploy", Tags: []string{"staging"}}, } data := store.DefaultData() data.Stats["web1"] = &store.HostStat{Count: 9, LastUsed: time.Now()} data.Stats["bastion"] = &store.HostStat{Count: 3, LastUsed: time.Now().Add(-time.Hour)} data.Favorites = []string{"bastion"} settings := store.DefaultSettings() m := newModelForTest(hosts, data, settings) m.width, m.height = 90, 30 m.ensureVisible() return m } // newModelForTest mirrors the setup Run does, without starting a tea program. func newModelForTest(hosts []sshconf.Host, data store.Data, settings store.Settings) *model { res, _ := buildModel(hosts, data, settings) return res } func TestRenderSnapshot(t *testing.T) { m := sampleModel() out := m.View() if !strings.Contains(out, "dial") { t.Error("header missing") } if !strings.Contains(out, "web1") || !strings.Contains(out, "bastion") { t.Error("hosts missing from render") } // bastion is a favourite, so it should sort to the top of a recent-sort list. t.Logf("\n%s", out) } func TestFilterNarrows(t *testing.T) { m := sampleModel() m.query = "staging" m.applyFilter() if len(m.view) != 1 { t.Fatalf("expected 1 match for 'staging', got %d", len(m.view)) } if m.rows[m.view[0]].host.Alias != "staging-web" { t.Errorf("wrong match: %s", m.rows[m.view[0]].host.Alias) } } func TestFavoriteSortsFirst(t *testing.T) { m := sampleModel() if len(m.view) == 0 { t.Fatal("no rows") } if !m.rows[m.view[0]].fav { t.Errorf("favourite should be first, got %s", m.rows[m.view[0]].host.Alias) } } func TestAlphaSort(t *testing.T) { m := sampleModel() m.sortM = store.SortAlpha m.sortAll() m.applyFilter() // Favourite (bastion) still pins first; the rest are alphabetical. got := []string{} for _, vi := range m.view { got = append(got, m.rows[vi].host.Alias) } want := []string{"bastion", "db-primary", "staging-web", "web1"} if strings.Join(got, ",") != strings.Join(want, ",") { t.Errorf("alpha order = %v, want %v", got, want) } } func TestSortCycle(t *testing.T) { s := store.SortRecent seq := []store.SortMode{} for i := 0; i < 3; i++ { s = s.Next() seq = append(seq, s) } want := []store.SortMode{store.SortFrequent, store.SortAlpha, store.SortRecent} for i := range want { if seq[i] != want[i] { t.Errorf("cycle[%d] = %s, want %s", i, seq[i], want[i]) } } } func TestGrouping(t *testing.T) { m := sampleModel() m.grouped = true m.applyFilter() // Expect a Favourites header first, then tag sections, then no untagged // (bastion, the only untagged host, is a favourite). if h, ok := m.headers[0]; !ok || !strings.Contains(h.title, "favourites") { t.Fatalf("first section should be favourites, got %+v", m.headers[0]) } // bastion (fav) appears once, only in the favourites section. count := 0 for _, vi := range m.view { if m.rows[vi].host.Alias == "bastion" { count++ } } if count != 1 { t.Errorf("favourite host should appear exactly once, got %d", count) } // Every non-header host is reachable; view length equals host count (no dupes). if len(m.view) != len(m.rows) { t.Errorf("grouped view should contain each host once: got %d, want %d", len(m.view), len(m.rows)) } // A tag section header should carry the tag as its colour key. foundProd := false for _, h := range m.headers { if h.title == "prod" && h.tag == "prod" { foundProd = true } } if !foundProd { t.Error("expected a 'prod' tag section header") } } func TestNoOverflow(t *testing.T) { for _, grouped := range []bool{false, true} { for _, w := range []int{40, 60, 90, 140} { for _, h := range []int{18, 24, 40} { m := sampleModel() m.grouped = grouped m.applyFilter() m.width, m.height = w, h out := m.View() lines := strings.Split(out, "\n") if len(lines) > h { t.Errorf("grouped=%v %dx%d: %d lines > height", grouped, w, h, len(lines)) } for _, ln := range lines { if lw := lipgloss.Width(ln); lw > w { t.Errorf("grouped=%v %dx%d: line %d cols > width", grouped, w, h, lw) } } } } } }