Introduces a test case to verify that errors returned from the MultiSelectWithSearch search function are properly propagated to the caller.
958 lines
29 KiB
Go
958 lines
29 KiB
Go
//go:build !windows
|
|
|
|
package prompter_test
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Netflix/go-expect"
|
|
"github.com/cli/cli/v2/internal/prompter"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/creack/pty"
|
|
"github.com/hinshun/vt10x"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// The following tests are broadly testing the accessible prompter, and NOT asserting
|
|
// on the prompter's complete and exact output strings.
|
|
//
|
|
// These tests generally operate with this logic:
|
|
// - Wait for a particular substring (a portion of the prompt) to appear
|
|
// - Send input
|
|
// - Wait for another substring to appear or for control to return to the test
|
|
// - Assert that the input value was returned from the prompter function
|
|
|
|
// In the future, expanding these tests to assert on the exact prompt strings
|
|
// would help build confidence in `huh` upgrades, but for now these tests
|
|
// are sufficient to ensure that the accessible prompter behaves roughly as expected
|
|
// but doesn't mandate that prompts always look exactly the same.
|
|
func TestAccessiblePrompter(t *testing.T) {
|
|
|
|
beforePasswordSendTimeout := 100 * time.Microsecond
|
|
|
|
t.Run("Select", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Enter a number between 1 and 3:")
|
|
require.NoError(t, err)
|
|
|
|
// Select option 1
|
|
_, err = console.SendLine("1")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, selectValue)
|
|
})
|
|
|
|
t.Run("Select - blank input returns default value", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
dummyDefaultValue := "12345abcdefg"
|
|
options := []string{"1", "2", dummyDefaultValue}
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Enter a number between 1 and 3:")
|
|
require.NoError(t, err)
|
|
|
|
// Just press enter to accept the default
|
|
_, err = console.SendLine("")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
selectValue, err := p.Select("Select a number", dummyDefaultValue, options)
|
|
require.NoError(t, err)
|
|
|
|
expectedIndex := slices.Index(options, dummyDefaultValue)
|
|
assert.Equal(t, expectedIndex, selectValue)
|
|
})
|
|
|
|
t.Run("Select - default value is in prompt and in readable format", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
dummyDefaultValue := "12345abcdefg"
|
|
options := []string{"1", "2", dummyDefaultValue}
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Select a number (default: 12345abcdefg)")
|
|
require.NoError(t, err)
|
|
|
|
// Just press enter to accept the default
|
|
_, err = console.SendLine("")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
selectValue, err := p.Select("Select a number", dummyDefaultValue, options)
|
|
require.NoError(t, err)
|
|
|
|
expectedIndex := slices.Index(options, dummyDefaultValue)
|
|
assert.Equal(t, expectedIndex, selectValue)
|
|
})
|
|
|
|
t.Run("Select - invalid defaults are excluded from prompt", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
dummyDefaultValue := "foo"
|
|
options := []string{"1", "2"}
|
|
|
|
go func() {
|
|
// Wait for prompt to appear without the invalid default value
|
|
_, err := console.ExpectString("Select a number \r\n")
|
|
require.NoError(t, err)
|
|
|
|
// Select option 2
|
|
_, err = console.SendLine("2")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
selectValue, err := p.Select("Select a number", dummyDefaultValue, options)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, selectValue)
|
|
})
|
|
|
|
t.Run("MultiSelect", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Input a number between 0 and 3:")
|
|
require.NoError(t, err)
|
|
|
|
// Select options 1 and 2
|
|
_, err = console.SendLine("1")
|
|
require.NoError(t, err)
|
|
_, err = console.SendLine("2")
|
|
require.NoError(t, err)
|
|
|
|
// This confirms selections
|
|
_, err = console.SendLine("0")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
multiSelectValue, err := p.MultiSelect("Select a number", []string{}, []string{"1", "2", "3"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []int{0, 1}, multiSelectValue)
|
|
})
|
|
|
|
t.Run("MultiSelect - default values are respected by being pre-selected", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Select a number")
|
|
require.NoError(t, err)
|
|
|
|
// Don't select anything because the default should be selected.
|
|
|
|
// This confirms selections
|
|
_, err = console.SendLine("0")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
multiSelectValue, err := p.MultiSelect("Select a number", []string{"2"}, []string{"1", "2", "3"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []int{1}, multiSelectValue)
|
|
})
|
|
|
|
t.Run("MultiSelect - default value is in prompt and in readable format", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
dummyDefaultValues := []string{"foo", "bar"}
|
|
options := []string{"1", "2"}
|
|
options = append(options, dummyDefaultValues...)
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Select a number (defaults: foo, bar)")
|
|
require.NoError(t, err)
|
|
|
|
// Don't select anything because the defaults should be selected.
|
|
|
|
// This confirms selections
|
|
_, err = console.SendLine("0")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
multiSelectValues, err := p.MultiSelect("Select a number", dummyDefaultValues, options)
|
|
require.NoError(t, err)
|
|
var expectedIndices []int
|
|
|
|
// Get the indices of the default values within the options slice
|
|
// as that's what we expect the prompter to return when no selections are made.
|
|
for _, defaultValue := range dummyDefaultValues {
|
|
expectedIndices = append(expectedIndices, slices.Index(options, defaultValue))
|
|
}
|
|
assert.Equal(t, expectedIndices, multiSelectValues)
|
|
})
|
|
|
|
t.Run("MultiSelect - invalid defaults are excluded from prompt", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
dummyDefaultValues := []string{"foo", "bar"}
|
|
options := []string{"1", "2"}
|
|
|
|
go func() {
|
|
// Wait for prompt to appear without the invalid default values
|
|
_, err := console.ExpectString("Select a number \r\n")
|
|
require.NoError(t, err)
|
|
|
|
// Not selecting anything will fail because there are no defaults.
|
|
_, err = console.SendLine("2")
|
|
require.NoError(t, err)
|
|
|
|
// This confirms selections
|
|
_, err = console.SendLine("0")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
multiSelectValues, err := p.MultiSelect("Select a number", dummyDefaultValues, options)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []int{1}, multiSelectValues)
|
|
})
|
|
|
|
t.Run("MultiSelectWithSearch - basic flow", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
persistentOptions := []string{"persistent-option-1"}
|
|
searchFunc := func(input string) prompter.MultiSelectSearchResult {
|
|
var searchResultKeys []string
|
|
var searchResultLabels []string
|
|
|
|
// Initial search with no input
|
|
if input == "" {
|
|
moreResults := 2
|
|
searchResultKeys = []string{"initial-result-1", "initial-result-2"}
|
|
searchResultLabels = []string{"Initial Result Label 1", "Initial Result Label 2"}
|
|
return prompter.MultiSelectSearchResult{
|
|
Keys: searchResultKeys,
|
|
Labels: searchResultLabels,
|
|
MoreResults: moreResults,
|
|
Err: nil,
|
|
}
|
|
}
|
|
|
|
// Subsequent search with input
|
|
moreResults := 0
|
|
searchResultKeys = []string{"search-result-1", "search-result-2"}
|
|
searchResultLabels = []string{"Search Result Label 1", "Search Result Label 2"}
|
|
return prompter.MultiSelectSearchResult{
|
|
Keys: searchResultKeys,
|
|
Labels: searchResultLabels,
|
|
MoreResults: moreResults,
|
|
Err: nil,
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Select an option \r\n")
|
|
require.NoError(t, err)
|
|
|
|
// Select the search option, which will always be the first option
|
|
_, err = console.SendLine("1")
|
|
require.NoError(t, err)
|
|
|
|
// Submit search
|
|
_, err = console.SendLine("0")
|
|
require.NoError(t, err)
|
|
|
|
// Wait for the search prompt to appear
|
|
_, err = console.ExpectString("Search for an option")
|
|
require.NoError(t, err)
|
|
|
|
// Enter some search text to trigger the search
|
|
_, err = console.SendLine("search text")
|
|
require.NoError(t, err)
|
|
|
|
// Wait for the multiselect prompt to re-appear after search
|
|
_, err = console.ExpectString("Select an option \r\n")
|
|
require.NoError(t, err)
|
|
|
|
// Select the first search result
|
|
_, err = console.SendLine("2")
|
|
require.NoError(t, err)
|
|
|
|
// This confirms selections
|
|
_, err = console.SendLine("0")
|
|
require.NoError(t, err)
|
|
}()
|
|
multiSelectValues, err := p.MultiSelectWithSearch("Select an option", "Search for an option", []string{}, persistentOptions, searchFunc)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"search-result-1"}, multiSelectValues)
|
|
})
|
|
|
|
t.Run("MultiSelectWithSearch - defaults are pre-selected", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
initialSearchResultKeys := []string{"initial-result-1"}
|
|
initialSearchResultLabels := []string{"Initial Result Label 1"}
|
|
defaultOptions := initialSearchResultKeys
|
|
searchFunc := func(input string) prompter.MultiSelectSearchResult {
|
|
// Initial search with no input
|
|
if input == "" {
|
|
moreResults := 2
|
|
return prompter.MultiSelectSearchResult{
|
|
Keys: initialSearchResultKeys,
|
|
Labels: initialSearchResultLabels,
|
|
MoreResults: moreResults,
|
|
Err: nil,
|
|
}
|
|
}
|
|
|
|
// No search selected, so this should fail the test.
|
|
t.FailNow()
|
|
return prompter.MultiSelectSearchResult{
|
|
Keys: nil,
|
|
Labels: nil,
|
|
MoreResults: 0,
|
|
Err: nil,
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Select an option (default: Initial Result Label 1) \r\n")
|
|
require.NoError(t, err)
|
|
|
|
// This confirms default selections
|
|
_, err = console.SendLine("0")
|
|
require.NoError(t, err)
|
|
}()
|
|
multiSelectValues, err := p.MultiSelectWithSearch("Select an option", "Search for an option", defaultOptions, initialSearchResultKeys, searchFunc)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, defaultOptions, multiSelectValues)
|
|
})
|
|
|
|
t.Run("MultiSelectWithSearch - selected options persist between searches", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
initialSearchResultKeys := []string{"initial-result-1"}
|
|
initialSearchResultLabels := []string{"Initial Result Label 1"}
|
|
moreResultKeys := []string{"more-result-1"}
|
|
moreResultLabels := []string{"More Result Label 1"}
|
|
|
|
searchFunc := func(input string) prompter.MultiSelectSearchResult {
|
|
// Initial search with no input
|
|
if input == "" {
|
|
moreResults := 2
|
|
return prompter.MultiSelectSearchResult{
|
|
Keys: initialSearchResultKeys,
|
|
Labels: initialSearchResultLabels,
|
|
MoreResults: moreResults,
|
|
Err: nil,
|
|
}
|
|
}
|
|
|
|
// Subsequent search with input "more"
|
|
if input == "more" {
|
|
return prompter.MultiSelectSearchResult{
|
|
Keys: moreResultKeys,
|
|
Labels: moreResultLabels,
|
|
MoreResults: 0,
|
|
Err: nil,
|
|
}
|
|
}
|
|
|
|
// No other searches expected
|
|
t.FailNow()
|
|
return prompter.MultiSelectSearchResult{
|
|
Keys: nil,
|
|
Labels: nil,
|
|
MoreResults: 0,
|
|
Err: nil,
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Select an option \r\n")
|
|
require.NoError(t, err)
|
|
|
|
// Select one of our initial search results
|
|
_, err = console.SendLine("2")
|
|
require.NoError(t, err)
|
|
|
|
// Select to search
|
|
_, err = console.SendLine("1")
|
|
require.NoError(t, err)
|
|
|
|
// Submit the search selection
|
|
_, err = console.SendLine("0")
|
|
require.NoError(t, err)
|
|
|
|
// Wait for the search prompt to appear
|
|
_, err = console.ExpectString("Search for an option")
|
|
require.NoError(t, err)
|
|
|
|
// Enter some search text to trigger the search
|
|
_, err = console.SendLine("more")
|
|
require.NoError(t, err)
|
|
|
|
// Wait for the multiselect prompt to re-appear after search
|
|
_, err = console.ExpectString("Select up to")
|
|
require.NoError(t, err)
|
|
|
|
// Select the new option from the new search results
|
|
_, err = console.SendLine("3")
|
|
require.NoError(t, err)
|
|
|
|
// Submit selections
|
|
_, err = console.SendLine("0")
|
|
require.NoError(t, err)
|
|
}()
|
|
multiSelectValues, err := p.MultiSelectWithSearch("Select an option", "Search for an option", []string{}, []string{}, searchFunc)
|
|
require.NoError(t, err)
|
|
expectedValues := append(initialSearchResultKeys, moreResultKeys...)
|
|
assert.Equal(t, expectedValues, multiSelectValues)
|
|
})
|
|
|
|
t.Run("MultiSelectWithSearch - search error propagates", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
|
|
searchFunc := func(input string) prompter.MultiSelectSearchResult {
|
|
return prompter.MultiSelectSearchResult{
|
|
Err: fmt.Errorf("search error"),
|
|
}
|
|
}
|
|
|
|
_, err := p.MultiSelectWithSearch("Select", "Search", []string{}, []string{}, searchFunc)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "search error")
|
|
})
|
|
|
|
t.Run("Input", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
dummyText := "12345abcdefg"
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Enter some characters")
|
|
require.NoError(t, err)
|
|
|
|
// Enter a number
|
|
_, err = console.SendLine(dummyText)
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
inputValue, err := p.Input("Enter some characters", "")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, dummyText, inputValue)
|
|
})
|
|
|
|
t.Run("Input - blank input returns default value", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
dummyDefaultValue := "12345abcdefg"
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Enter some characters")
|
|
require.NoError(t, err)
|
|
|
|
// Enter nothing
|
|
_, err = console.SendLine("")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
inputValue, err := p.Input("Enter some characters", dummyDefaultValue)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, dummyDefaultValue, inputValue)
|
|
})
|
|
|
|
t.Run("Input - default value is in prompt and in readable format", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
dummyDefaultValue := "12345abcdefg"
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Enter some characters (default: 12345abcdefg)")
|
|
require.NoError(t, err)
|
|
|
|
// Enter nothing
|
|
_, err = console.SendLine("")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
inputValue, err := p.Input("Enter some characters", dummyDefaultValue)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, dummyDefaultValue, inputValue)
|
|
})
|
|
|
|
t.Run("Password", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
dummyPassword := "12345abcdefg"
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Enter password")
|
|
require.NoError(t, err)
|
|
|
|
// Wait to ensure huh has time to set the echo mode
|
|
time.Sleep(beforePasswordSendTimeout)
|
|
|
|
// Enter a number
|
|
_, err = console.SendLine(dummyPassword)
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
passwordValue, err := p.Password("Enter password")
|
|
require.NoError(t, err)
|
|
require.Equal(t, dummyPassword, passwordValue)
|
|
|
|
// Ensure the dummy password is not printed to the screen,
|
|
// asserting that echo mode is disabled.
|
|
//
|
|
// Note that since console.ExpectString returns successful if the
|
|
// expected string matches any part of the stream, we have to use an
|
|
// anchored regexp (i.e., with ^ and $) to make sure the password/token
|
|
// is not printed at all.
|
|
_, err = console.Expect(expect.RegexpPattern("^ \r\n\r\n$"))
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Confirm", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Are you sure")
|
|
require.NoError(t, err)
|
|
|
|
// Confirm
|
|
_, err = console.SendLine("y")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
confirmValue, err := p.Confirm("Are you sure", false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, true, confirmValue)
|
|
})
|
|
|
|
t.Run("Confirm - blank input returns default", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Are you sure")
|
|
require.NoError(t, err)
|
|
|
|
// Enter nothing
|
|
_, err = console.SendLine("")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
confirmValue, err := p.Confirm("Are you sure", false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, false, confirmValue)
|
|
})
|
|
|
|
t.Run("Confirm - default value is in prompt and in readable format", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
defaultValue := true
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Are you sure (default: yes)")
|
|
require.NoError(t, err)
|
|
|
|
// Enter nothing
|
|
_, err = console.SendLine("")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
confirmValue, err := p.Confirm("Are you sure", defaultValue)
|
|
require.NoError(t, err)
|
|
require.Equal(t, defaultValue, confirmValue)
|
|
})
|
|
|
|
t.Run("AuthToken", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
dummyAuthToken := "12345abcdefg"
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Paste your authentication token:")
|
|
require.NoError(t, err)
|
|
|
|
// Wait to ensure huh has time to set the echo mode
|
|
time.Sleep(beforePasswordSendTimeout)
|
|
|
|
// Enter some dummy auth token
|
|
_, err = console.SendLine(dummyAuthToken)
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
authValue, err := p.AuthToken()
|
|
require.NoError(t, err)
|
|
require.Equal(t, dummyAuthToken, authValue)
|
|
|
|
// Ensure the dummy password is not printed to the screen,
|
|
// asserting that echo mode is disabled.
|
|
//
|
|
// Note that since console.ExpectString returns successful if the
|
|
// expected string matches any part of the stream, we have to use an
|
|
// anchored regexp (i.e., with ^ and $) to make sure the password/token
|
|
// is not printed at all.
|
|
_, err = console.Expect(expect.RegexpPattern("^ \r\n\r\n$"))
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("AuthToken - blank input returns error", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
dummyAuthTokenForAfterFailure := "12345abcdefg"
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Paste your authentication token:")
|
|
require.NoError(t, err)
|
|
|
|
// Enter nothing
|
|
_, err = console.SendLine("")
|
|
require.NoError(t, err)
|
|
|
|
// Expect an error message
|
|
_, err = console.ExpectString("token is required")
|
|
require.NoError(t, err)
|
|
|
|
// Wait for the retry prompt
|
|
_, err = console.ExpectString("Paste your authentication token:")
|
|
require.NoError(t, err)
|
|
|
|
// Wait to ensure huh has time to set the echo mode
|
|
time.Sleep(beforePasswordSendTimeout)
|
|
|
|
// Now enter some dummy auth token to return control back to the test
|
|
_, err = console.SendLine(dummyAuthTokenForAfterFailure)
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
authValue, err := p.AuthToken()
|
|
require.NoError(t, err)
|
|
require.Equal(t, dummyAuthTokenForAfterFailure, authValue)
|
|
|
|
// Ensure the dummy password is not printed to the screen,
|
|
// asserting that echo mode is disabled.
|
|
//
|
|
// Note that since console.ExpectString returns successful if the
|
|
// expected string matches any part of the stream, we have to use an
|
|
// anchored regexp (i.e., with ^ and $) to make sure the password/token
|
|
// is not printed at all.
|
|
_, err = console.Expect(expect.RegexpPattern("^ \r\n\r\n$"))
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("ConfirmDeletion", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
|
|
requiredValue := "test"
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue))
|
|
require.NoError(t, err)
|
|
|
|
// Confirm
|
|
_, err = console.SendLine(requiredValue)
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
// An err indicates that the confirmation text sent did not match
|
|
err := p.ConfirmDeletion(requiredValue)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("ConfirmDeletion - bad input", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
requiredValue := "test"
|
|
badInputValue := "garbage"
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue))
|
|
require.NoError(t, err)
|
|
|
|
// Confirm with bad input
|
|
_, err = console.SendLine(badInputValue)
|
|
require.NoError(t, err)
|
|
|
|
// Expect an error message and loop back to the prompt
|
|
_, err = console.ExpectString(fmt.Sprintf("You entered: %q", badInputValue))
|
|
require.NoError(t, err)
|
|
|
|
// Confirm with the correct input to return control back to the test
|
|
_, err = console.SendLine(requiredValue)
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
// An err indicates that the confirmation text sent did not match
|
|
err := p.ConfirmDeletion(requiredValue)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("InputHostname", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
hostname := "example.com"
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Hostname:")
|
|
require.NoError(t, err)
|
|
|
|
// Enter the hostname
|
|
_, err = console.SendLine(hostname)
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
inputValue, err := p.InputHostname()
|
|
require.NoError(t, err)
|
|
require.Equal(t, hostname, inputValue)
|
|
})
|
|
|
|
t.Run("MarkdownEditor - blank allowed with blank input returns blank", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("How to edit?")
|
|
require.NoError(t, err)
|
|
|
|
// Enter 2, to select "skip"
|
|
_, err = console.SendLine("2")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
inputValue, err := p.MarkdownEditor("How to edit?", "", true)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "", inputValue)
|
|
})
|
|
|
|
t.Run("MarkdownEditor - blank disallowed with default value returns default value", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
defaultValue := "12345abcdefg"
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("How to edit?")
|
|
require.NoError(t, err)
|
|
|
|
// Enter number 2 to select "skip". This shouldn't be allowed.
|
|
_, err = console.SendLine("2")
|
|
require.NoError(t, err)
|
|
|
|
// Expect a notice to enter something valid since blank is disallowed.
|
|
_, err = console.ExpectString("Invalid: must be 1")
|
|
require.NoError(t, err)
|
|
|
|
// Send a 1 to select to open the editor. This will immediately exit
|
|
_, err = console.SendLine("1")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
inputValue, err := p.MarkdownEditor("How to edit?", defaultValue, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, defaultValue, inputValue)
|
|
})
|
|
|
|
t.Run("MarkdownEditor - blank disallowed no default value returns error", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestAccessiblePrompter(t, console)
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("How to edit?")
|
|
require.NoError(t, err)
|
|
|
|
// Enter number 2 to select "skip". This shouldn't be allowed.
|
|
_, err = console.SendLine("2")
|
|
require.NoError(t, err)
|
|
|
|
// Expect a notice to enter something valid since blank is disallowed.
|
|
_, err = console.ExpectString("Invalid: must be 1")
|
|
require.NoError(t, err)
|
|
|
|
// Send a 1 to select to open the editor since skip is invalid and
|
|
// we need to return control back to the test.
|
|
_, err = console.SendLine("1")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
inputValue, err := p.MarkdownEditor("How to edit?", "", false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "", inputValue)
|
|
})
|
|
}
|
|
|
|
func TestSurveyPrompter(t *testing.T) {
|
|
// This not a comprehensive test of the survey prompter, but it does
|
|
// demonstrate that the survey prompter is used when the
|
|
// accessible prompter is disabled.
|
|
t.Run("Select uses survey prompter when accessible prompter is disabled", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
p := newTestSurveyPrompter(t, console)
|
|
|
|
go func() {
|
|
// Wait for prompt to appear
|
|
_, err := console.ExpectString("Select a number")
|
|
require.NoError(t, err)
|
|
|
|
// Send a newline to select the first option
|
|
// Note: This would not work with the accessible prompter
|
|
// because it would requires sending a 1 to select the first option.
|
|
// So it proves we are seeing a survey prompter.
|
|
_, err = console.SendLine("")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, selectValue)
|
|
})
|
|
}
|
|
|
|
func newTestVirtualTerminal(t *testing.T) *expect.Console {
|
|
t.Helper()
|
|
|
|
// Create a PTY and hook up a virtual terminal emulator
|
|
ptm, pts, err := pty.Open()
|
|
require.NoError(t, err)
|
|
|
|
term := vt10x.New(vt10x.WithWriter(pts))
|
|
|
|
// Create a console via Expect that allows scripting against the terminal
|
|
consoleOpts := []expect.ConsoleOpt{
|
|
expect.WithStdin(ptm),
|
|
expect.WithStdout(term),
|
|
expect.WithCloser(ptm, pts),
|
|
failOnExpectError(t),
|
|
failOnSendError(t),
|
|
expect.WithDefaultTimeout(time.Second),
|
|
// Use this logger to debug expect based tests by printing the
|
|
// characters being read to stdout.
|
|
// expect.WithLogger(log.New(os.Stdout, "", 0)),
|
|
}
|
|
|
|
console, err := expect.NewConsole(consoleOpts...)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { testCloser(t, console) })
|
|
|
|
return console
|
|
}
|
|
|
|
func newTestVirtualTerminalIOStreams(t *testing.T, console *expect.Console) *iostreams.IOStreams {
|
|
t.Helper()
|
|
io := &iostreams.IOStreams{
|
|
In: console.Tty(),
|
|
Out: console.Tty(),
|
|
ErrOut: console.Tty(),
|
|
}
|
|
io.SetStdinTTY(false)
|
|
io.SetStdoutTTY(false)
|
|
io.SetStderrTTY(false)
|
|
return io
|
|
}
|
|
|
|
// `echo` is chosen as the editor command because it immediately returns
|
|
// a success exit code, returns an empty string, doesn't require any user input,
|
|
// and since this file is only built on Linux, it is near guaranteed to be available.
|
|
var editorCmd = "echo"
|
|
|
|
func newTestAccessiblePrompter(t *testing.T, console *expect.Console) prompter.Prompter {
|
|
t.Helper()
|
|
|
|
io := newTestVirtualTerminalIOStreams(t, console)
|
|
io.SetAccessiblePrompterEnabled(true)
|
|
|
|
return prompter.New(editorCmd, io)
|
|
}
|
|
|
|
func newTestSurveyPrompter(t *testing.T, console *expect.Console) prompter.Prompter {
|
|
t.Helper()
|
|
|
|
io := newTestVirtualTerminalIOStreams(t, console)
|
|
io.SetAccessiblePrompterEnabled(false)
|
|
|
|
return prompter.New(editorCmd, io)
|
|
}
|
|
|
|
// failOnExpectError adds an observer that will fail the test in a standardised way
|
|
// if any expectation on the command output fails, without requiring an explicit
|
|
// assertion.
|
|
//
|
|
// Use WithRelaxedIO to disable this behaviour.
|
|
func failOnExpectError(t *testing.T) expect.ConsoleOpt {
|
|
t.Helper()
|
|
return expect.WithExpectObserver(
|
|
func(matchers []expect.Matcher, buf string, err error) {
|
|
t.Helper()
|
|
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
if len(matchers) == 0 {
|
|
t.Fatalf("Error occurred while matching %q: %s\n", buf, err)
|
|
}
|
|
|
|
var criteria []string
|
|
for _, matcher := range matchers {
|
|
criteria = append(criteria, fmt.Sprintf("%q", matcher.Criteria()))
|
|
}
|
|
t.Fatalf("Failed to find [%s] in %q: %s\n", strings.Join(criteria, ", "), buf, err)
|
|
},
|
|
)
|
|
}
|
|
|
|
// failOnSendError adds an observer that will fail the test in a standardised way
|
|
// if any sending of input fails, without requiring an explicit assertion.
|
|
//
|
|
// Use WithRelaxedIO to disable this behaviour.
|
|
func failOnSendError(t *testing.T) expect.ConsoleOpt {
|
|
t.Helper()
|
|
return expect.WithSendObserver(
|
|
func(msg string, n int, err error) {
|
|
t.Helper()
|
|
|
|
if err != nil {
|
|
t.Fatalf("Failed to send %q: %s\n", msg, err)
|
|
}
|
|
if len(msg) != n {
|
|
t.Fatalf("Only sent %d of %d bytes for %q\n", n, len(msg), msg)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// testCloser is a helper to fail the test if a Closer fails to close.
|
|
func testCloser(t *testing.T, closer io.Closer) {
|
|
t.Helper()
|
|
if err := closer.Close(); err != nil {
|
|
t.Errorf("Close failed: %s", err)
|
|
}
|
|
}
|