fix(prompter): pass io to huh and refactor tests

This commit is contained in:
Kynan Ware 2025-04-09 15:37:48 -06:00
parent 2f5e896535
commit fab0de5583
2 changed files with 84 additions and 142 deletions

View file

@ -5,9 +5,7 @@ package prompter_test
import (
"fmt"
"io"
"os"
"strings"
"sync"
"testing"
"time"
@ -20,54 +18,11 @@ import (
)
func TestAccessiblePrompter(t *testing.T) {
// 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),
}
console, err := expect.NewConsole(consoleOpts...)
require.NoError(t, err)
t.Cleanup(func() { testCloser(t, console) })
// Using OS here because huh currently ignores configured iostreams
// See https://github.com/charmbracelet/huh/issues/612
stdIn := os.Stdin
stdOut := os.Stdout
stdErr := os.Stderr
t.Cleanup(func() {
os.Stdin = stdIn
os.Stdout = stdOut
os.Stderr = stdErr
})
os.Stdin = console.Tty()
os.Stdout = console.Tty()
os.Stderr = console.Tty()
t.Setenv("GH_ACCESSIBLE_PROMPTER", "true")
// Using echo as the editor command here because it will immediately exit
// and return no input.
p := prompter.New("echo", nil, nil, nil)
var wg sync.WaitGroup
t.Run("Select", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("Choose:")
require.NoError(t, err)
@ -80,15 +35,13 @@ func TestAccessiblePrompter(t *testing.T) {
selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"})
require.NoError(t, err)
assert.Equal(t, 0, selectValue)
wg.Wait()
})
t.Run("MultiSelect", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("Select a number")
require.NoError(t, err)
@ -107,16 +60,14 @@ func TestAccessiblePrompter(t *testing.T) {
multiSelectValue, err := p.MultiSelect("Select a number", []string{}, []string{"1", "2", "3"})
require.NoError(t, err)
assert.Equal(t, []int{0, 1}, multiSelectValue)
wg.Wait()
})
t.Run("Input", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
dummyText := "12345abcdefg"
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("Enter some characters")
require.NoError(t, err)
@ -129,16 +80,14 @@ func TestAccessiblePrompter(t *testing.T) {
inputValue, err := p.Input("Enter some characters", "")
require.NoError(t, err)
assert.Equal(t, dummyText, inputValue)
wg.Wait()
})
t.Run("Input - blank input returns default value", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
dummyDefaultValue := "12345abcdefg"
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("Enter some characters")
require.NoError(t, err)
@ -155,16 +104,14 @@ func TestAccessiblePrompter(t *testing.T) {
inputValue, err := p.Input("Enter some characters", dummyDefaultValue)
require.NoError(t, err)
assert.Equal(t, dummyDefaultValue, inputValue)
wg.Wait()
})
t.Run("Password", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
dummyPassword := "12345abcdefg"
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("Enter password")
require.NoError(t, err)
@ -177,15 +124,13 @@ func TestAccessiblePrompter(t *testing.T) {
passwordValue, err := p.Password("Enter password")
require.NoError(t, err)
require.Equal(t, dummyPassword, passwordValue)
wg.Wait()
})
t.Run("Confirm", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("Are you sure")
require.NoError(t, err)
@ -198,11 +143,12 @@ func TestAccessiblePrompter(t *testing.T) {
confirmValue, err := p.Confirm("Are you sure", false)
require.NoError(t, err)
require.Equal(t, true, confirmValue)
wg.Wait()
})
t.Run("Confirm - blank input returns default", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
go func() {
// Wait for prompt to appear
_, err := console.ExpectString("Are you sure")
@ -219,11 +165,11 @@ func TestAccessiblePrompter(t *testing.T) {
})
t.Run("AuthToken", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
dummyAuthToken := "12345abcdefg"
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("Paste your authentication token:")
require.NoError(t, err)
@ -236,16 +182,14 @@ func TestAccessiblePrompter(t *testing.T) {
authValue, err := p.AuthToken()
require.NoError(t, err)
require.Equal(t, dummyAuthToken, authValue)
wg.Wait()
})
t.Run("AuthToken - blank input returns error", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
dummyAuthTokenForAfterFailure := "12345abcdefg"
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("Paste your authentication token:")
require.NoError(t, err)
@ -266,16 +210,14 @@ func TestAccessiblePrompter(t *testing.T) {
authValue, err := p.AuthToken()
require.NoError(t, err)
require.Equal(t, dummyAuthTokenForAfterFailure, authValue)
wg.Wait()
})
t.Run("ConfirmDeletion", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
requiredValue := "test"
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue))
require.NoError(t, err)
@ -288,17 +230,15 @@ func TestAccessiblePrompter(t *testing.T) {
// An err indicates that the confirmation text sent did not match
err := p.ConfirmDeletion(requiredValue)
require.NoError(t, err)
wg.Wait()
})
t.Run("ConfirmDeletion - bad input", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
requiredValue := "test"
badInputValue := "garbage"
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue))
require.NoError(t, err)
@ -319,16 +259,14 @@ func TestAccessiblePrompter(t *testing.T) {
// An err indicates that the confirmation text sent did not match
err := p.ConfirmDeletion(requiredValue)
require.NoError(t, err)
wg.Wait()
})
t.Run("InputHostname", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
hostname := "example.com"
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("Hostname:")
require.NoError(t, err)
@ -341,15 +279,13 @@ func TestAccessiblePrompter(t *testing.T) {
inputValue, err := p.InputHostname()
require.NoError(t, err)
require.Equal(t, hostname, inputValue)
wg.Wait()
})
t.Run("MarkdownEditor - blank allowed with blank input returns blank", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("How to edit?")
require.NoError(t, err)
@ -362,16 +298,14 @@ func TestAccessiblePrompter(t *testing.T) {
inputValue, err := p.MarkdownEditor("How to edit?", "", true)
require.NoError(t, err)
require.Equal(t, "", inputValue)
wg.Wait()
})
t.Run("MarkdownEditor - blank disallowed with default value returns default value", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
defaultValue := "12345abcdefg"
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("How to edit?")
require.NoError(t, err)
@ -392,15 +326,13 @@ func TestAccessiblePrompter(t *testing.T) {
inputValue, err := p.MarkdownEditor("How to edit?", defaultValue, false)
require.NoError(t, err)
require.Equal(t, defaultValue, inputValue)
wg.Wait()
})
t.Run("MarkdownEditor - blank disallowed no default value returns error", func(t *testing.T) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("How to edit?")
require.NoError(t, err)
@ -422,46 +354,18 @@ func TestAccessiblePrompter(t *testing.T) {
inputValue, err := p.MarkdownEditor("How to edit?", "", false)
require.NoError(t, err)
require.Equal(t, "", inputValue)
wg.Wait()
})
}
func TestSurveyPrompter(t *testing.T) {
// 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 * 600),
}
console, err := expect.NewConsole(consoleOpts...)
require.NoError(t, err)
t.Cleanup(func() { testCloser(t, console) })
// Using echo as the editor command here because it will immediately exit
// and return no input.
p := prompter.New("echo", console.Tty(), console.Tty(), console.Tty())
var wg sync.WaitGroup
// 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) {
wg.Add(1)
console := newTestVirtualTerminal(t)
p := newTestSurveyPrompter(t, console)
go func() {
defer wg.Done()
// Wait for prompt to appear
_, err := console.ExpectString("Select a number")
require.NoError(t, err)
@ -477,11 +381,49 @@ func TestSurveyPrompter(t *testing.T) {
selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"})
require.NoError(t, err)
assert.Equal(t, 0, selectValue)
wg.Wait()
})
}
func newTestVirtualTerminal(t testing.TB) *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),
}
console, err := expect.NewConsole(consoleOpts...)
require.NoError(t, err)
t.Cleanup(func() { testCloser(t, console) })
return console
}
func newTestAcessiblePrompter(t testing.TB, console *expect.Console) prompter.Prompter {
t.Helper()
t.Setenv("GH_ACCESSIBLE_PROMPTER", "true")
return prompter.New("echo", console.Tty(), console.Tty(), console.Tty())
}
func newTestSurveyPrompter(t testing.TB, console *expect.Console) prompter.Prompter {
t.Helper()
t.Setenv("GH_ACCESSIBLE_PROMPTER", "false")
return prompter.New("echo", console.Tty(), console.Tty(), console.Tty())
}
// 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.

View file

@ -75,9 +75,9 @@ type accessiblePrompter struct {
func (p *accessiblePrompter) newForm(groups ...*huh.Group) *huh.Form {
return huh.NewForm(groups...).
WithTheme(huh.ThemeBase16()).
WithAccessible(true)
// Commented out because https://github.com/charmbracelet/huh/issues/612
// WithProgramOptions(tea.WithOutput(p.stdout), tea.WithInput(p.stdin))
WithAccessible(true).
WithInput(p.stdin).
WithOutput(p.stdout)
}
func (p *accessiblePrompter) Select(prompt, _ string, options []string) (int, error) {