diff --git a/README.md b/README.md
index 3953cbe..d61205e 100644
--- a/README.md
+++ b/README.md
@@ -29,9 +29,9 @@ Type | Supported | Supported Actions
`Editor` | ✘ |
`Input` | ✓ |
- Answer
- No answer
- Suggestions with navigation (Arrow Up `↑`, Arrow Down `↓`, Tab `⇆`, Esc `⎋`, Enter `⏎`)
- Interrupt (`^C`)
- Ask for help
`Multiline` | ✘ |
-`Multiselect` | ✘ |
+`Multiselect` | ✓ | - Type to filter, delete
- Navigation (Move Up `↑`, Move Down `↓`, Select None `←`, Select All `→`, Tab `⇆`, Enter `⏎`)
- Interrupt (`^C`)
- Ask for help
`Password` | ✓ | - Answer (+ check for `*`)
- No answer
- Interrupt (`^C`)
- Ask for help
-`Select` | ✓ | - Type to filter
- Navigation (Arrow Up `↑`, Arrow Down `↓`, Tab `⇆`, Esc `⎋`, Enter `⏎`)
- Interrupt (`^C`)
- Ask for help
+`Select` | ✓ | - Type to filter, delete
- Navigation (Move Up `↑`, Move Down `↓`, Tab `⇆`, Enter `⏎`)
- Interrupt (`^C`)
- Ask for help
### Expect
diff --git a/answer.go b/answer.go
index 1c4c095..ddf3dfe 100644
--- a/answer.go
+++ b/answer.go
@@ -10,6 +10,11 @@ import (
// ReactionTime is to create a small delay to simulate human reaction.
var ReactionTime = 10 * time.Millisecond
+// WaitForReaction creates a small delay to simulate human reaction.
+func WaitForReaction() <-chan time.Time {
+ return time.After(ReactionTime)
+}
+
// Answer is an expectation for answering a question.
type Answer interface {
Step
@@ -135,10 +140,22 @@ func pressArrowDown() *Action {
return action(terminal.KeyArrowDown, "ARROW DOWN")
}
+func pressArrowLeft() *Action {
+ return action(terminal.KeyArrowLeft, "ARROW LEFT")
+}
+
+func pressArrowRight() *Action {
+ return action(terminal.KeyArrowRight, "ARROW RIGHT")
+}
+
func pressInterrupt() *Action {
return action(terminal.KeyInterrupt, "INTERRUPT")
}
+func pressSpace() *Action {
+ return action(terminal.KeySpace, "SPACE")
+}
+
func pressDelete() *Action {
return action(terminal.KeyDelete, "DELETE")
}
@@ -163,7 +180,7 @@ func (a *HelpAction) Do(c Console) error {
// String represents the answer as a string.
func (a *HelpAction) String() string {
- return fmt.Sprintf("press %q", a.icon)
+ return fmt.Sprintf("press %q and see %q", a.icon, a.help)
}
func pressHelp(help string, options ...string) *HelpAction {
diff --git a/cursor_posix.go b/cursor_posix.go
index 51728cc..a5ea0e8 100644
--- a/cursor_posix.go
+++ b/cursor_posix.go
@@ -2,8 +2,6 @@
package surveyexpect
-import "time"
-
func waitForCursor(c Console) error {
// ANSI escape sequence for DSR - Device Status Report
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
@@ -16,7 +14,7 @@ func waitForCursor(c Console) error {
// After rendering the question, the prompt asks for the cursor's size and location (ESC[6n) and expects to receive
// `ESC[n;mR` in return before reading the answer. If the addStep answers too fast (so the answer will be in between
// `ESC[n;mR` and reading answer), the prompt won't see the answer and hangs indefinitely.
- <-time.After(ReactionTime)
+ <-WaitForReaction()
return err
}
diff --git a/cursor_windosw.go b/cursor_windows.go
similarity index 77%
rename from cursor_windosw.go
rename to cursor_windows.go
index 3d3440a..74a42fb 100644
--- a/cursor_windosw.go
+++ b/cursor_windows.go
@@ -3,7 +3,7 @@
package surveyexpect
func waitForCursor(c Console) error {
- <-time.After(ReactionTime)
+ <-WaitForReaction()
return nil
}
diff --git a/expectation.go b/expectation.go
index de1115e..397a139 100644
--- a/expectation.go
+++ b/expectation.go
@@ -5,9 +5,12 @@ import (
"regexp"
)
-var indicatorRegex = regexp.MustCompile(`^([^ ]\s+)(.*)`)
+var (
+ selectIndicatorRegex = regexp.MustCompile(`^([^ ]\s+)(.*)`)
+ multiselectIndicatorRegex = regexp.MustCompile(`^([^ ]\s+)(\[[x ]].*)`)
+)
-// SelectExpect expects strings from console.
+// SelectExpect expects a select list from console.
type SelectExpect []string
// Do runs the step.
@@ -23,17 +26,62 @@ func (e *SelectExpect) Do(c Console) error {
// String represents the answer as a string.
func (e *SelectExpect) String() string {
- breakdown := make([]map[string]string, 0)
+ var sb stringsBuilder
- var size int
+ sb.WriteLinef("Expect a select list:")
+ writeSelectList(&sb, *e, selectIndicatorRegex)
+
+ return sb.String()
+}
+
+func expectSelect(options ...string) *SelectExpect {
+ e := SelectExpect(options)
+
+ return &e
+}
+// MultiSelectExpect expects a multiselect list from console.
+type MultiSelectExpect []string
+
+// Do runs the step.
+func (e *MultiSelectExpect) Do(c Console) error {
for _, o := range *e {
+ if _, err := c.ExpectString(o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// String represents the answer as a string.
+func (e *MultiSelectExpect) String() string {
+ var sb stringsBuilder
+
+ sb.WriteLinef("Expect a multiselect list:")
+ writeSelectList(&sb, *e, multiselectIndicatorRegex)
+
+ return sb.String()
+}
+
+func expectMultiSelect(options ...string) *MultiSelectExpect {
+ e := MultiSelectExpect(options)
+
+ return &e
+}
+
+func breakdownOptions(options []string, indicator *regexp.Regexp) (breakdown []map[string]string, pad string) {
+ breakdown = make([]map[string]string, 0, len(options))
+
+ var size int
+
+ for _, o := range options {
e := map[string]string{
"prefix": "",
"option": "",
}
- if m := indicatorRegex.FindStringSubmatch(o); m != nil {
+ if m := indicator.FindStringSubmatch(o); m != nil {
e["prefix"] = m[1]
e["option"] = m[2]
l := len(m[1])
@@ -48,12 +96,11 @@ func (e *SelectExpect) String() string {
breakdown = append(breakdown, e)
}
- var (
- sb stringsBuilder
- pad = fmt.Sprintf("%%-%ds", size)
- )
+ return breakdown, fmt.Sprintf("%%-%ds", size)
+}
- sb.WriteLinef("Expect a select list:")
+func writeSelectList(sb *stringsBuilder, options []string, indicator *regexp.Regexp) {
+ breakdown, pad := breakdownOptions(options, indicator)
for i, o := range breakdown {
if i > 0 {
@@ -63,12 +110,4 @@ func (e *SelectExpect) String() string {
sb.Writef(pad, o["prefix"]).
Writef(o["option"])
}
-
- return sb.String()
-}
-
-func expectSelect(options ...string) *SelectExpect {
- e := SelectExpect(options)
-
- return &e
}
diff --git a/multiselect.go b/multiselect.go
new file mode 100644
index 0000000..b2d9051
--- /dev/null
+++ b/multiselect.go
@@ -0,0 +1,154 @@
+package surveyexpect
+
+var _ Prompt = (*MultiSelectPrompt)(nil)
+
+// MultiSelectPrompt is an expectation of survey.Select.
+type MultiSelectPrompt struct {
+ *basePrompt
+
+ message string
+ steps *InlineSteps
+}
+
+func (p *MultiSelectPrompt) append(steps ...Step) *MultiSelectPrompt {
+ p.lock()
+ defer p.unlock()
+
+ p.steps.Append(steps...)
+
+ return p
+}
+
+// ShowHelp asks for help and asserts the help text.
+//
+// Survey.ExpectMultiSelect("Select a language:").
+// ShowHelp("Your preferred language")
+func (p *MultiSelectPrompt) ShowHelp(help string, options ...string) *MultiSelectPrompt {
+ return p.append(pressHelp(help, options...))
+}
+
+// Type sends some text to filter the options.
+//
+// Survey.ExpectMultiSelect("Select a language:").
+// Type("Eng")
+func (p *MultiSelectPrompt) Type(s string) *MultiSelectPrompt {
+ return p.append(typeAnswer(s))
+}
+
+// Tab sends the TAB key the indicated times. Default is 1 when omitted.
+//
+// Survey.ExpectMultiSelect("Select a language:").
+// Type("Eng").
+// Tab()
+func (p *MultiSelectPrompt) Tab(times ...int) *MultiSelectPrompt {
+ return p.append(repeatStep(pressTab(), times...)...)
+}
+
+// Interrupt sends ^C and ends the sequence.
+//
+// Survey.ExpectMultiSelect("Select a language:").
+// Interrupt()
+func (p *MultiSelectPrompt) Interrupt() {
+ p.append(pressInterrupt())
+ p.steps.Close()
+}
+
+// Enter sends the ENTER key and ends the sequence.
+//
+// Survey.ExpectMultiSelect("Select a language:").
+// Type("Eng").
+// Enter()
+func (p *MultiSelectPrompt) Enter() {
+ p.append(pressEnter())
+ p.steps.Close()
+}
+
+// Delete sends the DELETE key the indicated times. Default is 1 when omitted.
+//
+// Survey.ExpectMultiSelect("Select a language:").
+// Type("Eng").
+// Delete(3)
+func (p *MultiSelectPrompt) Delete(times ...int) *MultiSelectPrompt {
+ return p.append(repeatStep(pressDelete(), times...)...)
+}
+
+// MoveUp sends the ARROW UP key the indicated times. Default is 1 when omitted.
+//
+// Survey.ExpectMultiSelect("Select a language:").
+// Type("Eng").
+// MoveUp()
+func (p *MultiSelectPrompt) MoveUp(times ...int) *MultiSelectPrompt {
+ return p.append(repeatStep(pressArrowUp(), times...)...)
+}
+
+// MoveDown sends the ARROW DOWN key the indicated times. Default is 1 when omitted.
+//
+// Survey.ExpectMultiSelect("Select a language:").
+// Type("Eng").
+// MoveDown()
+func (p *MultiSelectPrompt) MoveDown(times ...int) *MultiSelectPrompt {
+ return p.append(repeatStep(pressArrowDown(), times...)...)
+}
+
+// Select selects an option. If the option is selected, it will be deselected.
+//
+// Survey.ExpectMultiSelect("Select a language:").
+// Type("Eng").
+// Select()
+func (p *MultiSelectPrompt) Select() *MultiSelectPrompt {
+ return p.append(pressSpace())
+}
+
+// SelectNone deselects all filtered options.
+//
+// Survey.ExpectMultiSelect("Select a language:").
+// Type("Eng").
+// SelectNone()
+func (p *MultiSelectPrompt) SelectNone() *MultiSelectPrompt {
+ return p.append(pressArrowLeft())
+}
+
+// SelectAll selects all filtered options.
+//
+// Survey.ExpectMultiSelect("Select a language:").
+// Type("Eng").
+// SelectAll()
+func (p *MultiSelectPrompt) SelectAll() *MultiSelectPrompt {
+ return p.append(pressArrowRight())
+}
+
+// ExpectOptions expects a list of options.
+//
+// Survey.ExpectMultiSelect("Select a language:").
+// Type("Eng").
+// ExpectOptions("English")
+func (p *MultiSelectPrompt) ExpectOptions(options ...string) *MultiSelectPrompt {
+ return p.append(expectMultiSelect(options...))
+}
+
+// Do runs the step.
+func (p *MultiSelectPrompt) Do(c Console) error {
+ if _, err := c.ExpectString(p.message); err != nil {
+ return err
+ }
+
+ return p.steps.Do(c)
+}
+
+// String represents the expectation as a string.
+func (p *MultiSelectPrompt) String() string {
+ var sb stringsBuilder
+
+ return sb.WriteLabelLinef("Expect", "MultiSelect Prompt").
+ WriteLabelLinef("Message", "%q", p.message).
+ WriteString(p.steps.String()).
+ String()
+}
+
+func newMultiSelect(parent *Survey, message string) *MultiSelectPrompt {
+ return &MultiSelectPrompt{
+ basePrompt: &basePrompt{parent: parent},
+ message: message,
+ steps: inlineSteps(),
+ }
+}
diff --git a/multiselect_test.go b/multiselect_test.go
new file mode 100644
index 0000000..0a9a16f
--- /dev/null
+++ b/multiselect_test.go
@@ -0,0 +1,271 @@
+package surveyexpect_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/AlecAivazis/survey/v2"
+ "github.com/AlecAivazis/survey/v2/terminal"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/nhatthm/surveyexpect"
+ "github.com/nhatthm/surveyexpect/options"
+)
+
+func TestMultiSelectPrompt(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ scenario string
+ expectSurvey surveyexpect.Expector
+ help string
+ showHelp bool
+ options []string
+ expectedAnswer []string
+ expectedError string
+ }{
+ {
+ scenario: "enter without taking any other actions",
+ expectSurvey: surveyexpect.Expect(func(s *surveyexpect.Survey) {
+ s.ExpectMultiSelect("Select destinations").
+ Enter()
+ }),
+ },
+ {
+ scenario: "with help and ask for it",
+ expectSurvey: surveyexpect.Expect(func(s *surveyexpect.Survey) {
+ s.ExpectMultiSelect("Select destinations [Use arrows to move, space to select, to all, to none, type to filter, ? for more help]").
+ ShowHelp("Your favorite countries").
+ Select().
+ Enter()
+ }),
+ help: "Your favorite countries",
+ showHelp: true,
+ expectedAnswer: []string{"France"},
+ },
+ {
+ scenario: "input is interrupted",
+ expectSurvey: surveyexpect.Expect(func(s *surveyexpect.Survey) {
+ s.ExpectMultiSelect("Select destinations").
+ Interrupt()
+ }),
+ expectedError: "interrupt",
+ },
+ {
+ scenario: "input is invalid",
+ expectSurvey: surveyexpect.Expect(func(s *surveyexpect.Survey) {
+ s.ExpectMultiSelect("Select destinations").
+ Type("\033X")
+ }),
+ expectedError: `Unexpected Escape Sequence: ['\x1b' 'X']`,
+ },
+ {
+ scenario: "navigation",
+ expectSurvey: surveyexpect.Expect(func(s *surveyexpect.Survey) {
+ s.ExpectMultiSelect("Select destinations").
+ Type("United").Delete(2).
+ ExpectOptions(
+ "> [ ] United Kingdom",
+ "[ ] United States",
+ ).
+ SelectAll().
+ Tab(2).MoveDown().MoveUp(4).
+ ExpectOptions(
+ "[ ] Germany",
+ "[ ] Malaysia",
+ "[ ] Singapore",
+ "[ ] Thailand",
+ "[x] United Kingdom",
+ "[x] United States",
+ "> [ ] Vietnam",
+ ).
+ MoveDown().Select().
+ ExpectOptions(
+ "[x] France",
+ "[ ] Germany",
+ "[ ] Malaysia",
+ "[ ] Singapore",
+ "[ ] Thailand",
+ "[x] United Kingdom",
+ "[x] United States",
+ ).
+ SelectNone().
+ Select().Select().
+ MoveUp().
+ ExpectOptions(
+ "[ ] Germany",
+ "[ ] Malaysia",
+ "[ ] Singapore",
+ "[ ] Thailand",
+ "[ ] United Kingdom",
+ "[ ] United States",
+ "> [ ] Vietnam",
+ ).
+ Select().
+ Enter()
+ }),
+ expectedAnswer: []string{"Vietnam"},
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.scenario, func(t *testing.T) {
+ t.Parallel()
+
+ // Prepare the survey.
+ s := tc.expectSurvey(t)
+ p := &survey.MultiSelectTemplateData{
+ MultiSelect: survey.MultiSelect{
+ Message: "Select destinations",
+ Help: tc.help,
+ Options: []string{
+ "France",
+ "Germany",
+ "Malaysia",
+ "Singapore",
+ "Thailand",
+ "United Kingdom",
+ "United States",
+ "Vietnam",
+ },
+ },
+ ShowHelp: tc.showHelp,
+ }
+
+ // Start the survey.
+ s.Start(func(stdio terminal.Stdio) {
+ var answer []string
+ err := survey.AskOne(p, &answer, options.WithStdio(stdio))
+
+ assert.Equal(t, tc.expectedAnswer, answer)
+
+ if tc.expectedError == "" {
+ assert.NoError(t, err)
+ } else {
+ assert.EqualError(t, err, tc.expectedError)
+ }
+ })
+ })
+ }
+}
+
+func TestMultiSelectPrompt_NoHelpButStillExpect(t *testing.T) {
+ t.Parallel()
+
+ testingT := T()
+ s := surveyexpect.Expect(func(s *surveyexpect.Survey) {
+ s.WithTimeout(50 * time.Millisecond)
+
+ s.ExpectMultiSelect("Select destinations").
+ ShowHelp("Your favorite countries").
+ ExpectOptions(
+ "> [ ] option 1",
+ "[ ] option 2",
+ )
+ })(testingT)
+
+ expectedError := `there are remaining expectations that were not met:
+
+Expect : MultiSelect Prompt
+Message: "Select destinations"
+press "?" and see "Your favorite countries"
+Expect a multiselect list:
+> [ ] option 1
+ [ ] option 2`
+
+ p := &survey.MultiSelect{
+ Message: "Select destinations",
+ Options: []string{
+ "option 1",
+ "option 2",
+ },
+ }
+
+ // Start the survey.
+ s.Start(func(stdio terminal.Stdio) {
+ var answer []string
+ err := survey.AskOne(p, &answer, options.WithStdio(stdio))
+
+ assert.Empty(t, answer)
+ assert.NoError(t, err)
+ })
+
+ assert.EqualError(t, s.ExpectationsWereMet(), expectedError)
+
+ t.Log(testingT.LogString())
+}
+
+func TestMultiSelectPrompt_SurveyInterrupted(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ scenario string
+ expectSurvey surveyexpect.Expector
+ expectedError string
+ }{
+ {
+ scenario: "interrupt",
+ expectSurvey: surveyexpect.Expect(func(s *surveyexpect.Survey) {
+ s.ExpectMultiSelect("Select destinations").
+ Interrupt()
+ }),
+ expectedError: "interrupt",
+ },
+ {
+ scenario: "invalid input",
+ expectSurvey: surveyexpect.Expect(func(s *surveyexpect.Survey) {
+ s.ExpectMultiSelect("Select destinations").
+ Type("\033X")
+ }),
+ expectedError: `Unexpected Escape Sequence: ['\x1b' 'X']`,
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.scenario, func(t *testing.T) {
+ t.Parallel()
+
+ testingT := T()
+ s := tc.expectSurvey(testingT)
+
+ questions := []*survey.Question{
+ {
+ Name: "countries",
+ Prompt: &survey.MultiSelect{Message: "Select destinations", Options: []string{"Germany", "Vietnam"}},
+ },
+ {
+ Name: "transports",
+ Prompt: &survey.MultiSelect{Message: "Select transports", Options: []string{"Train", "Bus"}},
+ },
+ }
+
+ expectedResult := map[string]interface{}{
+ "countries": []string{"Vietnam"},
+ "transports": []string{"Bus"},
+ }
+
+ // Start the survey.
+ s.Start(func(stdio terminal.Stdio) {
+ result := map[string]interface{}{
+ "countries": []string{"Vietnam"},
+ "transports": []string{"Bus"},
+ }
+ err := survey.Ask(questions, &result, options.WithStdio(stdio))
+
+ assert.Equal(t, expectedResult, result)
+
+ if tc.expectedError == "" {
+ assert.NoError(t, err)
+ } else {
+ assert.EqualError(t, err, tc.expectedError)
+ }
+ })
+
+ assert.Nil(t, s.ExpectationsWereMet())
+
+ t.Log(testingT.LogString())
+ })
+ }
+}
diff --git a/select_test.go b/select_test.go
index 00a5edc..3d70c7c 100644
--- a/select_test.go
+++ b/select_test.go
@@ -135,10 +135,21 @@ func TestSelectPrompt_NoHelpButStillExpect(t *testing.T) {
s.WithTimeout(50 * time.Millisecond)
s.ExpectSelect("Select a country").
- ShowHelp("Your favorite country")
+ ShowHelp("Your favorite country").
+ ExpectOptions(
+ "> option 1",
+ "option 2",
+ )
})(testingT)
- expectedError := "there are remaining expectations that were not met:\n\nExpect : Select Prompt\nMessage: \"Select a country\"\npress \"?\""
+ expectedError := `there are remaining expectations that were not met:
+
+Expect : Select Prompt
+Message: "Select a country"
+press "?" and see "Your favorite country"
+Expect a select list:
+> option 1
+ option 2`
p := &survey.Select{
Message: "Select a country",
diff --git a/survey.go b/survey.go
index 70af8dc..854c87f 100644
--- a/survey.go
+++ b/survey.go
@@ -76,6 +76,18 @@ func (s *Survey) ExpectInput(message string) *InputPrompt {
return e
}
+// ExpectMultiSelect expects a MultiSelectPrompt.
+//
+// Survey.ExpectMultiSelect("Enter password:").
+// Enter()
+func (s *Survey) ExpectMultiSelect(message string) *MultiSelectPrompt {
+ e := newMultiSelect(s, message)
+
+ s.addStep(e)
+
+ return e
+}
+
// ExpectPassword expects a PasswordPrompt.
//
// Survey.ExpectPassword("Enter password:").