26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
253 lines
7.0 KiB
Go
253 lines
7.0 KiB
Go
package wizard
|
||
|
||
import (
|
||
"regexp"
|
||
"strings"
|
||
"testing"
|
||
|
||
tea "github.com/charmbracelet/bubbletea"
|
||
"github.com/charmbracelet/lipgloss"
|
||
)
|
||
|
||
var ansiRegexp = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||
|
||
func stripANSI(s string) string {
|
||
return ansiRegexp.ReplaceAllString(s, "")
|
||
}
|
||
|
||
func hasLineContainingAll(view string, needles ...string) bool {
|
||
for _, line := range strings.Split(stripANSI(view), "\n") {
|
||
match := true
|
||
for _, needle := range needles {
|
||
if !strings.Contains(line, needle) {
|
||
match = false
|
||
break
|
||
}
|
||
}
|
||
if match {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func lineContaining(view string, needle string) string {
|
||
for _, line := range strings.Split(stripANSI(view), "\n") {
|
||
if strings.Contains(line, needle) {
|
||
return line
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func TestView_RendersSetupTitle(t *testing.T) {
|
||
m := NewModel(baseConfig(&recorder{}))
|
||
m.width = 80
|
||
out := m.View()
|
||
if !strings.Contains(out, "Setup") {
|
||
t.Fatalf("expected box title 'Setup', got:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestView_RendersAllStepLabels(t *testing.T) {
|
||
m := NewModel(baseConfig(&recorder{}))
|
||
m.width = 80
|
||
out := m.View()
|
||
for _, label := range []string{"Branch", "Commit", "Push"} {
|
||
if !strings.Contains(out, label) {
|
||
t.Errorf("expected %q in view, got:\n%s", label, out)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestView_ShowsSkipReason(t *testing.T) {
|
||
cfg := baseConfig(&recorder{})
|
||
cfg.CurrentBranch = "feat/existing"
|
||
cfg.NeedsBranch = false
|
||
m := NewModel(cfg)
|
||
m.width = 80
|
||
out := m.View()
|
||
if !strings.Contains(out, "feat/existing") {
|
||
t.Fatalf("expected skip reason to mention branch, got:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestView_FooterReflectsSideEffects(t *testing.T) {
|
||
r := &recorder{}
|
||
m := NewModel(baseConfig(r))
|
||
m.width = 80
|
||
|
||
// No side-effects yet: footer shows "q quit".
|
||
initial := m.View()
|
||
if !strings.Contains(initial, "quit") {
|
||
t.Fatalf("expected 'quit' in initial footer, got:\n%s", initial)
|
||
}
|
||
|
||
// After creating a branch, footer should mention side-effects stay.
|
||
m = advance(m, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("feat/x")})
|
||
m = advance(m, tea.KeyMsg{Type: tea.KeyEnter})
|
||
m.width = 80
|
||
after := m.View()
|
||
if !strings.Contains(after, "abort") {
|
||
t.Fatalf("expected 'abort' in footer after side-effects, got:\n%s", after)
|
||
}
|
||
if !strings.Contains(after, "feat/x") {
|
||
t.Fatalf("expected branch name in footer, got:\n%s", after)
|
||
}
|
||
}
|
||
|
||
func TestView_ActionBarForConfirm(t *testing.T) {
|
||
cfg := baseConfig(&recorder{})
|
||
cfg.CurrentBranch = "feat/x"
|
||
cfg.NeedsBranch = false
|
||
cfg.IsDirty = false
|
||
m := NewModel(cfg)
|
||
m.width = 80
|
||
out := m.View()
|
||
if !strings.Contains(out, "push") {
|
||
t.Fatalf("expected 'push' in action bar for confirm step, got:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestView_RendersInputInlineWithPlaceholder(t *testing.T) {
|
||
m := NewModel(baseConfig(&recorder{}))
|
||
m.width = 80
|
||
|
||
out := m.View()
|
||
if !hasLineContainingAll(out, "Branch", "›", "blank = let agent suggest") {
|
||
t.Fatalf("expected inline input placeholder on Branch line, got:\n%s", stripANSI(out))
|
||
}
|
||
if strings.Contains(stripANSI(out), "branch name (blank to let the agent suggest):") {
|
||
t.Fatalf("expected stacked branch input label to be removed, got:\n%s", stripANSI(out))
|
||
}
|
||
}
|
||
|
||
func TestView_InlineInputFitsInsideBoxAtMinimumWidth(t *testing.T) {
|
||
m := NewModel(baseConfig(&recorder{}))
|
||
m.width = 60
|
||
m.input.SetValue(strings.Repeat("x", 80))
|
||
|
||
out := m.View()
|
||
branchLine := lineContaining(out, "Branch")
|
||
if branchLine == "" {
|
||
t.Fatalf("expected branch line in view, got:\n%s", stripANSI(out))
|
||
}
|
||
if got := lipgloss.Width(branchLine); got > 60 {
|
||
t.Fatalf("expected inline input line width <= 60, got %d:\n%s", got, stripANSI(out))
|
||
}
|
||
}
|
||
|
||
func TestView_EmitsTerminalTitleEscapeSequence(t *testing.T) {
|
||
m := NewModel(baseConfig(&recorder{}))
|
||
m.width = 80
|
||
out := m.View()
|
||
if !strings.HasPrefix(out, "\x1b]2;") {
|
||
t.Fatalf("expected View output to start with OSC 2 terminal title sequence, got: %q", out[:min(40, len(out))])
|
||
}
|
||
// Title must end with BEL before the rest of the rendered UI.
|
||
end := strings.Index(out, "\x07")
|
||
if end < 0 {
|
||
t.Fatalf("expected BEL terminator in terminal title sequence, got: %q", out)
|
||
}
|
||
}
|
||
|
||
func TestTerminalTitle_BranchInputIncludesCurrentBranch(t *testing.T) {
|
||
m := NewModel(baseConfig(&recorder{}))
|
||
got := m.terminalTitle()
|
||
if !strings.Contains(got, "Branch") {
|
||
t.Fatalf("expected title to mention Branch step, got: %q", got)
|
||
}
|
||
if !strings.Contains(got, "main") {
|
||
t.Fatalf("expected title to mention current branch %q, got: %q", "main", got)
|
||
}
|
||
if !strings.Contains(got, "Setup") {
|
||
t.Fatalf("expected title to include Setup prefix, got: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTerminalTitle_PushConfirmUsesTargetBranch(t *testing.T) {
|
||
cfg := baseConfig(&recorder{})
|
||
cfg.CurrentBranch = "feat/x"
|
||
cfg.NeedsBranch = false
|
||
cfg.IsDirty = false
|
||
m := NewModel(cfg)
|
||
got := m.terminalTitle()
|
||
if !strings.Contains(got, "Push") {
|
||
t.Fatalf("expected title to mention Push step, got: %q", got)
|
||
}
|
||
if !strings.Contains(got, "feat/x") {
|
||
t.Fatalf("expected title to include target branch, got: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTerminalTitle_SuccessShowsComplete(t *testing.T) {
|
||
m := NewModel(baseConfig(&recorder{}))
|
||
m.success = true
|
||
m.pushed = true
|
||
m.targetBranch = "feat/done"
|
||
got := m.terminalTitle()
|
||
if !strings.Contains(got, "complete") {
|
||
t.Fatalf("expected title to indicate completion, got: %q", got)
|
||
}
|
||
if !strings.Contains(got, "feat/done") {
|
||
t.Fatalf("expected title to include target branch on success, got: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTerminalTitle_AbortedShowsAborted(t *testing.T) {
|
||
m := NewModel(baseConfig(&recorder{}))
|
||
m.aborted = true
|
||
got := m.terminalTitle()
|
||
if !strings.Contains(got, "aborted") {
|
||
t.Fatalf("expected title to indicate aborted, got: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTerminalTitle_FailedShowsStepName(t *testing.T) {
|
||
m := NewModel(baseConfig(&recorder{}))
|
||
m.steps[0].status = statFailed
|
||
m.steps[0].errMsg = "boom"
|
||
got := m.terminalTitle()
|
||
if !strings.Contains(got, "Branch") {
|
||
t.Fatalf("expected failed title to mention failing step, got: %q", got)
|
||
}
|
||
if !strings.Contains(got, "✗") {
|
||
t.Fatalf("expected failed title to include ✗ icon, got: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestSetTerminalTitle_FormatsOSCSequence(t *testing.T) {
|
||
got := setTerminalTitle("hello")
|
||
want := "\x1b]2;hello\x07"
|
||
if got != want {
|
||
t.Fatalf("setTerminalTitle: got %q, want %q", got, want)
|
||
}
|
||
}
|
||
|
||
func TestView_RendersTransientStatusInlineWithStep(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
status stepStatus
|
||
result string
|
||
want string
|
||
}{
|
||
{name: "agent", status: statAgent, want: "asking agent for a branch name"},
|
||
{name: "running", status: statRunning, result: "feat/wizard", want: "creating branch feat/wizard"},
|
||
{name: "confirm", status: statConfirm, want: "push main to no-mistakes gate?"},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
m := NewModel(baseConfig(&recorder{}))
|
||
m.width = 120
|
||
m.steps[0].status = tt.status
|
||
m.steps[0].result = tt.result
|
||
|
||
out := m.View()
|
||
if !hasLineContainingAll(out, "Branch", tt.want) {
|
||
t.Fatalf("expected Branch and %q on the same line, got:\n%s", tt.want, stripANSI(out))
|
||
}
|
||
})
|
||
}
|
||
}
|