Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:34:16 +08:00

253 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))
}
})
}
}