package branches import ( "errors" "strings" "testing" tea "github.com/charmbracelet/bubbletea" "submarine/internal/submodule" "submarine/internal/tui/theme" ) func list() []submodule.Branch { return []submodule.Branch{ {Ref: "refs/heads/main", Name: "main", Local: "main", Current: true, SHA: "aaaaaaaaaa"}, {Ref: "refs/heads/dev", Name: "dev", Local: "dev", SHA: "bbbbbbbbbb"}, {Ref: "refs/remotes/origin/main", Name: "origin/main", Local: "main", Remote: true, SHA: "aaaaaaaaaa"}, {Ref: "refs/remotes/origin/feat/search", Name: "origin/feat/search", Local: "feat/search", Remote: true, SHA: "cccccccccc"}, } } func typed(m *Model, s string) { for _, r := range s { m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}}) } } func name(m *Model) string { if b := m.Selected(); b != nil { return b.Name } return "" } func TestSelectionMovesAndClamps(t *testing.T) { m := New(theme.Default(), "src/app") m.SetBranches(list()) if name(m) != "main" { t.Fatalf("first = %q", name(m)) } m.Up() if name(m) != "main" { t.Errorf("up at the top = %q", name(m)) } m.Down() m.Down() if name(m) != "origin/main" { t.Errorf("down twice = %q", name(m)) } m.PageDown() if name(m) != "origin/feat/search" { t.Errorf("page down = %q, want the last", name(m)) } m.Down() if name(m) != "origin/feat/search" { t.Errorf("down at the end = %q", name(m)) } } func TestSearchFiltersAndAllWordsMustMatch(t *testing.T) { m := New(theme.Default(), "src/app") m.SetBranches(list()) typed(m, "ORIGIN") if len(m.shown) != 2 { t.Errorf("origin matches %d, want 2", len(m.shown)) } typed(m, " sea") if len(m.shown) != 1 || name(m) != "origin/feat/search" { t.Errorf("shown = %d, selected %q", len(m.shown), name(m)) } typed(m, "zzz") if m.Selected() != nil { t.Errorf("no match must select nothing, got %q", name(m)) } if !strings.Contains(m.View(100, 30), "no branch matches") { t.Error("view does not say that nothing matches") } } func TestSelectionStaysOnTheSameBranchWhenItStillMatches(t *testing.T) { m := New(theme.Default(), "src/app") m.SetBranches(list()) m.Down() m.Down() // origin/main typed(m, "main") if name(m) != "origin/main" { t.Errorf("selected %q, want origin/main kept", name(m)) } } func TestViewStates(t *testing.T) { m := New(theme.Default(), "src/app") if v := m.View(100, 30); !strings.Contains(v, "reading branches") || !strings.Contains(v, "Switch src/app") { t.Errorf("loading view:\n%s", v) } m.SetErr(errors.New("not initialized; restore it first")) if v := m.View(100, 30); !strings.Contains(v, "not initialized") { t.Errorf("error view:\n%s", v) } m.SetBranches(nil) if v := m.View(100, 30); !strings.Contains(v, "no branches") { t.Errorf("empty view:\n%s", v) } m.SetBranches(list()) v := m.View(100, 30) for _, want := range []string{"main", "origin/", "feat/search", "aaaaaaa", "enter switch"} { if !strings.Contains(v, want) { t.Errorf("view lacks %q:\n%s", want, v) } } } func TestListScrollsInASmallTerminal(t *testing.T) { var many []submodule.Branch for i := 0; i < 30; i++ { n := string(rune('a'+i%26)) + string(rune('a'+i/26)) many = append(many, submodule.Branch{Ref: "refs/heads/" + n, Name: n, Local: n, SHA: "abcdef0123"}) } m := New(theme.Default(), "x") m.SetBranches(many) m.View(80, 14) // fewer rows than branches for i := 0; i < 20; i++ { m.Down() } if m.cursor < m.offset || m.cursor >= m.offset+m.rows { t.Errorf("cursor %d outside the window [%d,%d)", m.cursor, m.offset, m.offset+m.rows) } } func TestClickSelectsTheBranchUnderTheMouse(t *testing.T) { m := New(theme.Default(), "src/app") m.SetBranches(list()) m.View(100, 30) x, y := m.Origin(100, 30) if inside, hit := m.Click(x+3, y+listTop+1, 100, 30); !inside || !hit || name(m) != "dev" { t.Errorf("click on the 2nd row: inside=%v hit=%v selected %q", inside, hit, name(m)) } if inside, hit := m.Click(x+3, y+listTop+3, 100, 30); !inside || !hit || name(m) != "origin/feat/search" { t.Errorf("click on the 4th row: inside=%v hit=%v selected %q", inside, hit, name(m)) } for _, c := range []struct { name string x, y int }{ {"title", x + 3, y + 1}, {"search line", x + 3, y + 2}, {"hint line", x + 3, y + m.h - 2}, {"left border", x, y + listTop}, } { before := name(m) if inside, hit := m.Click(c.x, c.y, 100, 30); !inside || hit || name(m) != before { t.Errorf("click on the %s: inside=%v hit=%v selected %q", c.name, inside, hit, name(m)) } } if inside, hit := m.Click(0, 0, 100, 30); inside || hit { t.Errorf("click outside: inside=%v hit=%v", inside, hit) } } func TestClickBelowTheLastBranchHitsNothing(t *testing.T) { m := New(theme.Default(), "x") m.SetBranches(list()[:1]) // 1 branch, the list is padded to 3 rows m.View(100, 30) x, y := m.Origin(100, 30) if inside, hit := m.Click(x+3, y+listTop+2, 100, 30); !inside || hit { t.Errorf("inside=%v hit=%v on an empty row", inside, hit) } }