feat(tui): add interactive repository selection
Introduce a Bubble Tea control deck with eligible repositories preselected, keyboard navigation, responsive paging, and detailed explanations for blocked worktrees. Require confirmation before synchronization, allow refreshing the workspace, and retain each repository's latest hub output without changing the existing CLI and JSON workflows.
This commit is contained in:
+73
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func testTUIRepos() []repo {
|
||||
return []repo{
|
||||
{Path: "/workspace/clean", RelativePath: "clean", Branch: "main", Eligible: true},
|
||||
{Path: "/workspace/dirty", RelativePath: "dirty", Branch: "develop", BlockReason: "dirty worktree (1 changes)", Dirty: []string{" M file.txt"}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIOnlySelectsEligibleRepositories(t *testing.T) {
|
||||
m := newTUIModel("/workspace", testTUIRepos(), options{})
|
||||
if got := len(m.selectedRepos()); got != 1 {
|
||||
t.Fatalf("selected repositories = %d, want 1", got)
|
||||
}
|
||||
|
||||
m.cursor = 1
|
||||
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
m = updated.(tuiModel)
|
||||
if got := len(m.selectedRepos()); got != 1 {
|
||||
t.Fatalf("blocked repository became selectable; selected = %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIRequiresExplicitConfirmation(t *testing.T) {
|
||||
m := newTUIModel("/workspace", testTUIRepos(), options{})
|
||||
updated, cmd := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
||||
m = updated.(tuiModel)
|
||||
if m.mode != modeConfirm || cmd != nil {
|
||||
t.Fatalf("sync should enter confirmation without starting command: mode=%v cmd=%v", m.mode, cmd)
|
||||
}
|
||||
|
||||
updated, cmd = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'n'}})
|
||||
m = updated.(tuiModel)
|
||||
if m.mode != modeReady || cmd != nil || !strings.Contains(m.status, "cancelled") {
|
||||
t.Fatalf("cancel should return to ready state: %+v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIViewExplainsBlockedRepository(t *testing.T) {
|
||||
m := newTUIModel("/workspace", testTUIRepos(), options{})
|
||||
m.width, m.height, m.cursor = 100, 30, 1
|
||||
view := m.View()
|
||||
for _, want := range []string{"STACK SYNC", "dirty worktree", "file.txt", "1 blocked"} {
|
||||
if !strings.Contains(view, want) {
|
||||
t.Fatalf("view missing %q:\n%s", want, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIViewShowsLastHubOutputForFocusedRepository(t *testing.T) {
|
||||
m := newTUIModel("/workspace", testTUIRepos(), options{})
|
||||
m.width, m.height = 100, 30
|
||||
m.lastResult = []syncResult{{Path: "clean", Success: true, Message: "Updated branch main", Duration: "120ms"}}
|
||||
view := m.View()
|
||||
for _, want := range []string{"LAST SYNC", "120ms", "Updated branch main"} {
|
||||
if !strings.Contains(view, want) {
|
||||
t.Fatalf("view missing %q:\n%s", want, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateMiddle(t *testing.T) {
|
||||
if got := truncateMiddle("abcdefghij", 7); got != "abc…hij" {
|
||||
t.Fatalf("truncateMiddle() = %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user