package ui
import (
"bufio"
"bytes"
"context"
"strings"
"testing"
"codey/internal/ask"
)
func askOn(t *testing.T, input string, req ask.Request) (ask.Response, string) {
t.Helper()
var out bytes.Buffer
a := &Asker{In: bufio.NewScanner(strings.NewReader(input)), Out: &out}
resp, err := a.Prompt(context.Background(), req)
if err != nil {
t.Fatalf("Prompt: %v", err)
}
return resp, out.String()
}
func storage() ask.Request {
return ask.Request{Questions: []ask.Question{{
Header: "Storage",
Question: "How should sessions be stored?",
Options: []ask.Option{
{Label: "Postgres", Description: "already running"},
{Label: "SQLite"},
},
}}}
}
func TestLineAskerTakesANumberedAnswer(t *testing.T) {
got, printed := askOn(t, "2\n", storage())
if got.Dismissed || len(got.Answers) != 1 || got.Answers[0].Chosen[0] != "SQLite" {
t.Fatalf("response = %+v", got)
}
for _, want := range []string{"Storage", "How should sessions be stored?", "1.", "Postgres", "already running"} {
if !strings.Contains(printed, want) {
t.Errorf("the prompt never showed %q:\n%s", want, printed)
}
}
}
// Anything that is not a number is the user answering in their own words,
// which every question allows.
func TestLineAskerTakesWordsAsTheAnswer(t *testing.T) {
got, _ := askOn(t, "neither, keep them in memory\n", storage())
if got.Answers[0].Other != "neither, keep them in memory" {
t.Fatalf("Other = %q", got.Answers[0].Other)
}
if len(got.Answers[0].Chosen) != 0 {
t.Error("typed words are not a chosen option")
}
}
func TestLineAskerRefusesANumberThatIsNotAnOption(t *testing.T) {
got, printed := askOn(t, "7\n1\n", storage())
if !strings.Contains(printed, "no option 7") {
t.Errorf("expected the mistake to be named:\n%s", printed)
}
if got.Answers[0].Chosen[0] != "Postgres" {
t.Fatalf("response = %+v, want the answer given second", got)
}
}
func TestLineAskerTakesSeveralWhenTheQuestionAllowsIt(t *testing.T) {
req := ask.Request{Questions: []ask.Question{{
Header: "Scope", Question: "What should it cover?", MultiSelect: true,
Options: []ask.Option{{Label: "the CLI"}, {Label: "the TUI"}, {Label: "the docs"}},
}}}
got, _ := askOn(t, "1,3\n", req)
if want := "the CLI,the docs"; strings.Join(got.Answers[0].Chosen, ",") != want {
t.Fatalf("chosen = %v, want %s", got.Answers[0].Chosen, want)
}
}
// One answer means one. Two numbers is a misunderstanding worth correcting
// rather than a free-text answer to file.
func TestLineAskerRefusesSeveralAnswersToOneQuestion(t *testing.T) {
got, printed := askOn(t, "1,2\n2\n", storage())
if !strings.Contains(printed, "takes one answer") {
t.Errorf("expected a correction:\n%s", printed)
}
if got.Answers[0].Chosen[0] != "SQLite" {
t.Fatalf("response = %+v", got)
}
}
// An empty line is a skip, and a skipped question abandons the request: half
// the answers reported as if they were all of them is worse than none.
func TestLineAskerSkipOnEmptyLine(t *testing.T) {
req := storage()
req.Questions = append(req.Questions, ask.Question{
Header: "Tests", Question: "How far?", Options: []ask.Option{{Label: "unit"}, {Label: "e2e"}},
})
got, _ := askOn(t, "1\n\n", req)
if !got.Dismissed {
t.Fatalf("response = %+v, want a dismissal", got)
}
if len(got.Answers) != 0 {
t.Error("a dismissed request carries no answers")
}
}
// Nobody is there: a piped run reaches EOF, and the tool turns that into an
// instruction to decide alone rather than waiting forever.
func TestLineAskerClosedStdinIsNotAnAnswer(t *testing.T) {
got, _ := askOn(t, "", storage())
if !got.Dismissed {
t.Fatalf("response = %+v, want a dismissal", got)
}
}
func TestLineAskerAsksEveryQuestion(t *testing.T) {
req := storage()
req.Questions = append(req.Questions, ask.Question{
Header: "Tests", Question: "How far should the tests go?", Options: []ask.Option{{Label: "unit"}, {Label: "e2e"}},
})
got, printed := askOn(t, "1\n2\n", req)
if len(got.Answers) != 2 {
t.Fatalf("got %d answers, want one per question", len(got.Answers))
}
if got.Answers[1].Chosen[0] != "e2e" {
t.Errorf("second answer = %v", got.Answers[1].Chosen)
}
if !strings.Contains(printed, "How far should the tests go?") {
t.Error("the second question was never put")
}
}