package bonfire import ( "bytes" "context" "errors" "strings" "testing" tea "charm.land/bubbletea/v2" "github.com/charmbracelet/x/ansi" ) // frame is a model's view without escape codes. func frame(m tea.Model) string { return ansi.Strip(m.View().Content) } // TestWidgetMatrixSmoke: -q keeps data and errors but drops decoration, -n // makes prompts return their defaults without reading stdin (Secret with a // terminal stdin fails with a hint), ConfirmProceed never asks off a // terminal, and the ported live models render frames from Update and View. func TestWidgetMatrixSmoke(t *testing.T) { t.Run("quiet", func(t *testing.T) { var stdout, stderr bytes.Buffer c := newConsole(strings.NewReader(""), &stdout, &stderr, func(string) string { return "" }, boolPtr(true), boolPtr(false)) c.setGlobals(Globals{Quiet: true, NoInteraction: true}) c.Info("info") c.Success("success") c.Warning("warning") if err := c.Spinner("spinning", func() error { return nil }); err != nil { t.Fatal(err) } if err := c.Progress(4, func(p Progress) error { for range 4 { p.Advance(1) } Failed(p, "item-2", errors.New("broken")) return nil }); err != nil { t.Fatal(err) } c.Println("data line") c.Table([]string{"key"}, [][]string{{"value"}}) c.Error("boom") out := stdout.String() for _, gone := range []string{"info", "success", "warning", "spinning", "[4/4]"} { if strings.Contains(out, gone) { t.Fatalf("-q printed %q:\n%s", gone, out) } } if !strings.Contains(out, "data line\n") || !strings.Contains(out, "value") { t.Fatalf("-q dropped data:\n%s", out) } if !strings.Contains(stderr.String(), "boom") || !strings.Contains(stderr.String(), "item-2: broken") { t.Fatalf("-q dropped errors: %q", stderr.String()) } }) t.Run("no-interaction", func(t *testing.T) { in := strings.NewReader("typed\n") var buf bytes.Buffer c := newConsole(in, &buf, &buf, func(string) string { return "" }, boolPtr(true), boolPtr(true)) c.setGlobals(Globals{NoInteraction: true}) if got, err := c.Ask("name", "bob"); err != nil || got != "bob" { t.Fatalf("Ask = %q, %v", got, err) } if got, err := c.Confirm("sure?", true); err != nil || !got { t.Fatalf("Confirm = %v, %v", got, err) } if got, err := c.Choice("pick", []string{"a", "b", "c"}, 2); err != nil || got != "c" { t.Fatalf("Choice = %q, %v", got, err) } if in.Len() != len("typed\n") || buf.Len() != 0 { t.Fatalf("-n prompts read stdin or printed: left %d, out %q", in.Len(), buf.String()) } if ok, err := ConfirmProceed(c, "Drop everything?"); err != nil || !ok { t.Fatalf("ConfirmProceed = %v, %v", ok, err) } if Interactive(c) || !GlobalsOf(c).NoInteraction { t.Fatal("console with -n reports interactive") } c.inTTY = true _, err := c.Secret("password") var hint *HintError if !errors.As(err, &hint) || !strings.Contains(hint.Try, "-n") { t.Fatalf("Secret with a terminal stdin under -n: %v", err) } c.inTTY = false if got, err := c.Secret("password"); err != nil || got != "typed" { t.Fatalf("Secret with piped stdin under -n = %q, %v", got, err) } }) t.Run("not interactive", func(t *testing.T) { in := strings.NewReader("y\n") var buf bytes.Buffer out := NewOutput(in, &buf, &buf) if ok, err := ConfirmProceed(out, "Drop everything?"); err != nil || !ok || in.Len() != 2 { t.Fatalf("ConfirmProceed off a terminal = %v, %v (stdin left %d)", ok, err, in.Len()) } if VerbosityOf(out) != 0 || IsQuiet(out) { t.Fatal("zero globals expected") } var lines bytes.Buffer errOut := NewOutput(strings.NewReader(""), &lines, &lines) err := Process(context.Background(), errOut, "Importing", []string{"a", "b"}, func(_ context.Context, item string) error { if item == "b" { return errors.New("bad row") } return nil }) if err == nil || !strings.Contains(err.Error(), "b: bad row") { t.Fatalf("Process err = %v", err) } if got := lines.String(); got != "āœ“ a\nāœ— b: bad row\n" { t.Fatalf("Process lines = %q", got) } }) size := tea.WindowSizeMsg{Width: 80, Height: 24} p := DefaultPalette() t.Run("spinner model", func(t *testing.T) { var m tea.Model = newSpinnerModel(p, "Importing posts") m, _ = m.Update(size) m, _ = m.Update(itemMsg{done: 2, total: 5, item: "post-2"}) if v := frame(m); !strings.Contains(v, "Importing posts 2/5") || !strings.Contains(v, "post-2") { t.Fatalf("spinner frame = %q", v) } m, cmd := m.Update(finishMsg{}) if cmd == nil || !strings.Contains(frame(m), symCheck+" Importing posts") { t.Fatalf("spinner final frame = %q", frame(m)) } }) t.Run("progress model", func(t *testing.T) { var m tea.Model = newProgressModel(p, 10) m, _ = m.Update(size) m, _ = m.Update(itemMsg{done: 4, total: 10, item: "row 4"}) if v := frame(m); !strings.Contains(v, "4/10") || !strings.Contains(v, "row 4") { t.Fatalf("progress frame = %q", v) } }) t.Run("process model", func(t *testing.T) { var m tea.Model = newProcessModel(p, "Pushing", []string{"blog", "shop", "forum"}) m, _ = m.Update(size) m, _ = m.Update(stepMsg{item: "blog", state: itemDone, done: 1}) m, _ = m.Update(stepMsg{item: "shop", state: itemFailed, err: errors.New("rejected"), done: 2}) m, _ = m.Update(stepMsg{item: "forum", state: itemRunning, done: 2}) v := frame(m) for _, want := range []string{symCheck + " blog", symCross + " shop", "rejected", "forum", "Pushing 2/3"} { if !strings.Contains(v, want) { t.Fatalf("process frame missing %q:\n%s", want, v) } } }) t.Run("selector model", func(t *testing.T) { var m tea.Model = newSelectorModel(p, "Pick a plugin", []string{"blog", "shop", "forum"}, 0) m, _ = m.Update(size) if v := frame(m); !strings.Contains(v, "> blog") { t.Fatalf("selector frame = %q", v) } m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyDown}) m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) if v := frame(m); !strings.Contains(v, "Selected:") || !strings.Contains(v, "shop") { t.Fatalf("confirmation frame = %q", v) } m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEscape}) m, _ = m.Update(tea.KeyPressMsg{Code: 'j', Text: "j"}) m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) m, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) sel := m.(selectorModel) if cmd == nil || sel.chosen != 2 || sel.items[sel.chosen] != "forum" { t.Fatalf("selector chose %d (cmd %v)", sel.chosen, cmd != nil) } var c tea.Model = newSelectorModel(p, "Pick", []string{"a"}, 0) c, _ = c.Update(tea.KeyPressMsg{Code: tea.KeyEscape}) if !c.(selectorModel).cancelled { t.Fatal("esc did not cancel") } }) }