Archived
b opens a centered popup with a search line and the local and remote branches of the selection. Enter stages the branch tip as the pin of a submodule, restores it (and everything below) to it, and puts HEAD on the branch; a remote branch gets a local tracking branch unless one exists elsewhere. Works on the superproject too. Losses are asked like restore. The backdrop behind the popup is dimmed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
3.5 KiB
Go
130 lines
3.5 KiB
Go
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)
|
|
}
|
|
}
|