//go:build linux || darwin package prompter_test import ( "fmt" "io" "os" "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" "golang.org/x/sys/unix" ) // 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) { 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") 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("Enter 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") 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") 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") 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)") 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") 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 until huh has disabled echo mode on the TTY require.NoError(t, waitForEchoDisabled(console.Tty(), 5*time.Second)) // 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(`^(\x1b\[[\d;]*m)* \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 until huh has disabled echo mode on the TTY require.NoError(t, waitForEchoDisabled(console.Tty(), 5*time.Second)) // 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(`^(\x1b\[[\d;]*m)* \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 until huh has disabled echo mode on the TTY require.NoError(t, waitForEchoDisabled(console.Tty(), 5*time.Second)) // 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(`^(\x1b\[[\d;]*m)* \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) } } // waitForEchoDisabled polls the TTY until echo mode is disabled or the // timeout is reached. This is used in password and auth token tests to // ensure that huh has configured the terminal before we send input. func waitForEchoDisabled(tty *os.File, timeout time.Duration) error { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { termios, err := unix.IoctlGetTermios(int(tty.Fd()), ioctlGetTermios) if err != nil { return fmt.Errorf("getting terminal attributes: %w", err) } if termios.Lflag&unix.ECHO == 0 { return nil } time.Sleep(time.Millisecond) } return fmt.Errorf("timed out waiting for echo mode to be disabled") }