test(huh prompter): add table-driven tests for all prompt types
Extract build*Form() methods from each huhPrompter method, separating
form construction from form.Run(). This enables testing the real form
construction code by driving it with direct model updates, adapted
from huh's own test patterns.
Tests cover Input, Select, MultiSelect, Confirm, Password,
MarkdownEditor, and MultiSelectWithSearch including a persistence
test that verifies selections survive across search query changes.
Also fixes a search cache initialization bug where the first
buildOptions("") call would skip the searchFunc due to
cachedSearchQuery defaulting to "".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
f294831e7d
commit
86d876fd34
2 changed files with 602 additions and 48 deletions
|
|
@ -24,7 +24,7 @@ func (p *huhPrompter) newForm(groups ...*huh.Group) *huh.Form {
|
|||
WithOutput(p.stdout)
|
||||
}
|
||||
|
||||
func (p *huhPrompter) Select(prompt, defaultValue string, options []string) (int, error) {
|
||||
func (p *huhPrompter) buildSelectForm(prompt, defaultValue string, options []string) (*huh.Form, *int) {
|
||||
var result int
|
||||
|
||||
if !slices.Contains(options, defaultValue) {
|
||||
|
|
@ -39,19 +39,24 @@ func (p *huhPrompter) Select(prompt, defaultValue string, options []string) (int
|
|||
formOptions[i] = huh.NewOption(o, i)
|
||||
}
|
||||
|
||||
err := p.newForm(
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[int]().
|
||||
Title(prompt).
|
||||
Value(&result).
|
||||
Options(formOptions...),
|
||||
),
|
||||
).Run()
|
||||
|
||||
return result, err
|
||||
)
|
||||
return form, &result
|
||||
}
|
||||
|
||||
func (p *huhPrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) {
|
||||
func (p *huhPrompter) Select(prompt, defaultValue string, options []string) (int, error) {
|
||||
form, result := p.buildSelectForm(prompt, defaultValue, options)
|
||||
err := form.Run()
|
||||
return *result, err
|
||||
}
|
||||
|
||||
func (p *huhPrompter) buildMultiSelectForm(prompt string, defaults []string, options []string) (*huh.Form, *[]int) {
|
||||
var result []int
|
||||
|
||||
defaults = slices.DeleteFunc(defaults, func(s string) bool {
|
||||
|
|
@ -66,7 +71,7 @@ func (p *huhPrompter) MultiSelect(prompt string, defaults []string, options []st
|
|||
formOptions[i] = huh.NewOption(o, i)
|
||||
}
|
||||
|
||||
err := p.newForm(
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewMultiSelect[int]().
|
||||
Title(prompt).
|
||||
|
|
@ -74,12 +79,17 @@ func (p *huhPrompter) MultiSelect(prompt string, defaults []string, options []st
|
|||
Limit(len(options)).
|
||||
Options(formOptions...),
|
||||
),
|
||||
).Run()
|
||||
)
|
||||
return form, &result
|
||||
}
|
||||
|
||||
func (p *huhPrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) {
|
||||
form, result := p.buildMultiSelectForm(prompt, defaults, options)
|
||||
err := form.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
return *result, nil
|
||||
}
|
||||
|
||||
// searchOptionsBinding is used as the OptionsFunc binding for MultiSelectWithSearch.
|
||||
|
|
@ -91,7 +101,7 @@ type searchOptionsBinding struct {
|
|||
Selected *[]string
|
||||
}
|
||||
|
||||
func (p *huhPrompter) MultiSelectWithSearch(prompt, searchPrompt string, defaultValues, persistentValues []string, searchFunc func(string) MultiSelectSearchResult) ([]string, error) {
|
||||
func (p *huhPrompter) buildMultiSelectWithSearchForm(prompt, searchPrompt string, defaultValues, persistentValues []string, searchFunc func(string) MultiSelectSearchResult) (*huh.Form, *[]string) {
|
||||
selectedValues := make([]string, len(defaultValues))
|
||||
copy(selectedValues, defaultValues)
|
||||
|
||||
|
|
@ -103,13 +113,15 @@ func (p *huhPrompter) MultiSelectWithSearch(prompt, searchPrompt string, default
|
|||
// Cache searchFunc results locally keyed by query string.
|
||||
// This avoids redundant calls when the OptionsFunc binding hash changes
|
||||
// due to selection changes (not query changes).
|
||||
searchCacheValid := false
|
||||
var cachedSearchQuery string
|
||||
var cachedSearchResult MultiSelectSearchResult
|
||||
|
||||
buildOptions := func(query string) []huh.Option[string] {
|
||||
if query != cachedSearchQuery || cachedSearchResult.Err != nil {
|
||||
if !searchCacheValid || query != cachedSearchQuery {
|
||||
cachedSearchResult = searchFunc(query)
|
||||
cachedSearchQuery = query
|
||||
searchCacheValid = true
|
||||
}
|
||||
result := cachedSearchResult
|
||||
|
||||
|
|
@ -175,7 +187,7 @@ func (p *huhPrompter) MultiSelectWithSearch(prompt, searchPrompt string, default
|
|||
Selected: &selectedValues,
|
||||
}
|
||||
|
||||
err := p.newForm(
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title(searchPrompt).
|
||||
|
|
@ -189,67 +201,83 @@ func (p *huhPrompter) MultiSelectWithSearch(prompt, searchPrompt string, default
|
|||
Value(&selectedValues).
|
||||
Limit(0),
|
||||
),
|
||||
).Run()
|
||||
)
|
||||
return form, &selectedValues
|
||||
}
|
||||
|
||||
func (p *huhPrompter) MultiSelectWithSearch(prompt, searchPrompt string, defaultValues, persistentValues []string, searchFunc func(string) MultiSelectSearchResult) ([]string, error) {
|
||||
form, result := p.buildMultiSelectWithSearchForm(prompt, searchPrompt, defaultValues, persistentValues, searchFunc)
|
||||
err := form.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return selectedValues, nil
|
||||
return *result, nil
|
||||
}
|
||||
|
||||
func (p *huhPrompter) Input(prompt, defaultValue string) (string, error) {
|
||||
func (p *huhPrompter) buildInputForm(prompt, defaultValue string) (*huh.Form, *string) {
|
||||
result := defaultValue
|
||||
|
||||
err := p.newForm(
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title(prompt).
|
||||
Value(&result),
|
||||
),
|
||||
).Run()
|
||||
|
||||
return result, err
|
||||
)
|
||||
return form, &result
|
||||
}
|
||||
|
||||
func (p *huhPrompter) Password(prompt string) (string, error) {
|
||||
var result string
|
||||
func (p *huhPrompter) Input(prompt, defaultValue string) (string, error) {
|
||||
form, result := p.buildInputForm(prompt, defaultValue)
|
||||
err := form.Run()
|
||||
return *result, err
|
||||
}
|
||||
|
||||
err := p.newForm(
|
||||
func (p *huhPrompter) buildPasswordForm(prompt string) (*huh.Form, *string) {
|
||||
var result string
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
EchoMode(huh.EchoModePassword).
|
||||
Title(prompt).
|
||||
Value(&result),
|
||||
),
|
||||
).Run()
|
||||
)
|
||||
return form, &result
|
||||
}
|
||||
|
||||
func (p *huhPrompter) Password(prompt string) (string, error) {
|
||||
form, result := p.buildPasswordForm(prompt)
|
||||
err := form.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result, nil
|
||||
return *result, nil
|
||||
}
|
||||
|
||||
func (p *huhPrompter) Confirm(prompt string, defaultValue bool) (bool, error) {
|
||||
func (p *huhPrompter) buildConfirmForm(prompt string, defaultValue bool) (*huh.Form, *bool) {
|
||||
result := defaultValue
|
||||
|
||||
err := p.newForm(
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title(prompt).
|
||||
Value(&result),
|
||||
),
|
||||
).Run()
|
||||
)
|
||||
return form, &result
|
||||
}
|
||||
|
||||
func (p *huhPrompter) Confirm(prompt string, defaultValue bool) (bool, error) {
|
||||
form, result := p.buildConfirmForm(prompt, defaultValue)
|
||||
err := form.Run()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result, nil
|
||||
return *result, nil
|
||||
}
|
||||
|
||||
func (p *huhPrompter) AuthToken() (string, error) {
|
||||
func (p *huhPrompter) buildAuthTokenForm() (*huh.Form, *string) {
|
||||
var result string
|
||||
|
||||
err := p.newForm(
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
EchoMode(huh.EchoModePassword).
|
||||
|
|
@ -262,12 +290,17 @@ func (p *huhPrompter) AuthToken() (string, error) {
|
|||
}).
|
||||
Value(&result),
|
||||
),
|
||||
).Run()
|
||||
|
||||
return result, err
|
||||
)
|
||||
return form, &result
|
||||
}
|
||||
|
||||
func (p *huhPrompter) ConfirmDeletion(requiredValue string) error {
|
||||
func (p *huhPrompter) AuthToken() (string, error) {
|
||||
form, result := p.buildAuthTokenForm()
|
||||
err := form.Run()
|
||||
return *result, err
|
||||
}
|
||||
|
||||
func (p *huhPrompter) buildConfirmDeletionForm(requiredValue string) *huh.Form {
|
||||
return p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
|
|
@ -279,28 +312,36 @@ func (p *huhPrompter) ConfirmDeletion(requiredValue string) error {
|
|||
return nil
|
||||
}),
|
||||
),
|
||||
).Run()
|
||||
)
|
||||
}
|
||||
|
||||
func (p *huhPrompter) InputHostname() (string, error) {
|
||||
var result string
|
||||
func (p *huhPrompter) ConfirmDeletion(requiredValue string) error {
|
||||
return p.buildConfirmDeletionForm(requiredValue).Run()
|
||||
}
|
||||
|
||||
err := p.newForm(
|
||||
func (p *huhPrompter) buildInputHostnameForm() (*huh.Form, *string) {
|
||||
var result string
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Hostname:").
|
||||
Validate(ghinstance.HostnameValidator).
|
||||
Value(&result),
|
||||
),
|
||||
).Run()
|
||||
)
|
||||
return form, &result
|
||||
}
|
||||
|
||||
func (p *huhPrompter) InputHostname() (string, error) {
|
||||
form, result := p.buildInputHostnameForm()
|
||||
err := form.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result, nil
|
||||
return *result, nil
|
||||
}
|
||||
|
||||
func (p *huhPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) {
|
||||
func (p *huhPrompter) buildMarkdownEditorForm(prompt string, blankAllowed bool) (*huh.Form, *string) {
|
||||
var result string
|
||||
skipOption := "skip"
|
||||
launchOption := "launch"
|
||||
|
|
@ -311,20 +352,25 @@ func (p *huhPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed b
|
|||
options = append(options, huh.NewOption("Skip", skipOption))
|
||||
}
|
||||
|
||||
err := p.newForm(
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title(prompt).
|
||||
Options(options...).
|
||||
Value(&result),
|
||||
),
|
||||
).Run()
|
||||
)
|
||||
return form, &result
|
||||
}
|
||||
|
||||
func (p *huhPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) {
|
||||
form, result := p.buildMarkdownEditorForm(prompt, blankAllowed)
|
||||
err := form.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result == skipOption {
|
||||
if *result == "skip" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
|
|
|
|||
508
internal/prompter/huh_prompter_test.go
Normal file
508
internal/prompter/huh_prompter_test.go
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
package prompter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/huh/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Test helpers adapted from huh's own test suite (huh_test.go).
|
||||
|
||||
func batchUpdate(m huh.Model, cmd tea.Cmd) huh.Model {
|
||||
if cmd == nil {
|
||||
return m
|
||||
}
|
||||
msg := cmd()
|
||||
m, cmd = m.Update(msg)
|
||||
if cmd == nil {
|
||||
return m
|
||||
}
|
||||
msg = cmd()
|
||||
m, _ = m.Update(msg)
|
||||
return m
|
||||
}
|
||||
|
||||
func codeKeypress(r rune) tea.KeyPressMsg {
|
||||
return tea.KeyPressMsg(tea.Key{Code: r})
|
||||
}
|
||||
|
||||
func keypress(r rune) tea.KeyPressMsg {
|
||||
return tea.KeyPressMsg(tea.Key{
|
||||
Text: string(r),
|
||||
Code: r,
|
||||
ShiftedCode: r,
|
||||
})
|
||||
}
|
||||
|
||||
func typeText(m huh.Model, s string) huh.Model {
|
||||
for _, r := range s {
|
||||
m, _ = m.Update(keypress(r))
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func viewStripped(m huh.Model) string {
|
||||
return ansi.Strip(m.View())
|
||||
}
|
||||
|
||||
func shiftTabKeypress() tea.KeyPressMsg {
|
||||
return tea.KeyPressMsg(tea.Key{Code: tea.KeyTab, Mod: tea.ModShift})
|
||||
}
|
||||
|
||||
func newTestHuhPrompter() *huhPrompter {
|
||||
return &huhPrompter{}
|
||||
}
|
||||
|
||||
// doAllUpdates processes all batched commands from the form, including async
|
||||
// OptionsFunc evaluations. Adapted from huh's own test suite. Uses iterative
|
||||
// rounds with a depth limit to prevent infinite loops from cascading binding updates.
|
||||
func doAllUpdates(f *huh.Form, cmd tea.Cmd) {
|
||||
for range 3 {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
cmds := expandBatch(cmd)
|
||||
var next []tea.Cmd
|
||||
for _, c := range cmds {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
_, result := f.Update(c())
|
||||
if result != nil {
|
||||
next = append(next, result)
|
||||
}
|
||||
}
|
||||
if len(next) == 0 {
|
||||
return
|
||||
}
|
||||
cmd = tea.Batch(next...)
|
||||
}
|
||||
}
|
||||
|
||||
// expandBatch flattens nested tea.BatchMsg into a flat slice of commands.
|
||||
func expandBatch(cmd tea.Cmd) []tea.Cmd {
|
||||
if cmd == nil {
|
||||
return nil
|
||||
}
|
||||
msg := cmd()
|
||||
if batch, ok := msg.(tea.BatchMsg); ok {
|
||||
var all []tea.Cmd
|
||||
for _, sub := range batch {
|
||||
all = append(all, expandBatch(sub)...)
|
||||
}
|
||||
return all
|
||||
}
|
||||
return []tea.Cmd{func() tea.Msg { return msg }}
|
||||
}
|
||||
|
||||
func TestHuhPrompterInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
defaultValue string
|
||||
input string
|
||||
wantResult string
|
||||
}{
|
||||
{
|
||||
name: "basic input",
|
||||
input: "hello",
|
||||
wantResult: "hello",
|
||||
},
|
||||
{
|
||||
name: "default value returned when no input",
|
||||
defaultValue: "default",
|
||||
wantResult: "default",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := newTestHuhPrompter()
|
||||
f, result := p.buildInputForm("Name:", tt.defaultValue)
|
||||
f.Update(f.Init())
|
||||
|
||||
var m huh.Model = f
|
||||
if tt.input != "" {
|
||||
m = typeText(m, tt.input)
|
||||
}
|
||||
batchUpdate(m.Update(codeKeypress(tea.KeyEnter)))
|
||||
|
||||
require.Equal(t, tt.wantResult, *result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHuhPrompterSelect(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
defaultValue string
|
||||
keys []tea.KeyPressMsg // keypresses before Enter
|
||||
wantIndex int
|
||||
}{
|
||||
{
|
||||
name: "selects first option by default",
|
||||
options: []string{"a", "b", "c"},
|
||||
wantIndex: 0,
|
||||
},
|
||||
{
|
||||
name: "respects default value",
|
||||
options: []string{"a", "b", "c"},
|
||||
defaultValue: "b",
|
||||
wantIndex: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid default selects first",
|
||||
options: []string{"a", "b", "c"},
|
||||
defaultValue: "z",
|
||||
wantIndex: 0,
|
||||
},
|
||||
{
|
||||
name: "navigate down one",
|
||||
options: []string{"a", "b", "c"},
|
||||
keys: []tea.KeyPressMsg{keypress('j')},
|
||||
wantIndex: 1,
|
||||
},
|
||||
{
|
||||
name: "navigate down two",
|
||||
options: []string{"a", "b", "c"},
|
||||
keys: []tea.KeyPressMsg{keypress('j'), keypress('j')},
|
||||
wantIndex: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := newTestHuhPrompter()
|
||||
f, result := p.buildSelectForm("Pick:", tt.defaultValue, tt.options)
|
||||
f.Update(f.Init())
|
||||
|
||||
var m huh.Model = f
|
||||
for _, k := range tt.keys {
|
||||
m = batchUpdate(m.Update(k))
|
||||
}
|
||||
batchUpdate(m.Update(codeKeypress(tea.KeyEnter)))
|
||||
|
||||
require.Equal(t, tt.wantIndex, *result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHuhPrompterMultiSelect(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
defaults []string
|
||||
keys []tea.KeyPressMsg
|
||||
wantResult []int
|
||||
}{
|
||||
{
|
||||
name: "no defaults and no toggles returns empty",
|
||||
options: []string{"a", "b", "c"},
|
||||
wantResult: []int{},
|
||||
},
|
||||
{
|
||||
name: "defaults are pre-selected",
|
||||
options: []string{"a", "b", "c"},
|
||||
defaults: []string{"a", "c"},
|
||||
wantResult: []int{0, 2},
|
||||
},
|
||||
{
|
||||
name: "toggle first option",
|
||||
options: []string{"a", "b", "c"},
|
||||
keys: []tea.KeyPressMsg{keypress('x')},
|
||||
wantResult: []int{0},
|
||||
},
|
||||
{
|
||||
name: "toggle multiple options",
|
||||
options: []string{"a", "b", "c"},
|
||||
keys: []tea.KeyPressMsg{
|
||||
keypress('x'), // toggle a
|
||||
keypress('j'), // move to b
|
||||
keypress('j'), // move to c
|
||||
keypress('x'), // toggle c
|
||||
},
|
||||
wantResult: []int{0, 2},
|
||||
},
|
||||
{
|
||||
name: "invalid defaults are excluded",
|
||||
options: []string{"a", "b"},
|
||||
defaults: []string{"z"},
|
||||
wantResult: []int{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := newTestHuhPrompter()
|
||||
f, result := p.buildMultiSelectForm("Pick:", tt.defaults, tt.options)
|
||||
f.Update(f.Init())
|
||||
|
||||
var m huh.Model = f
|
||||
for _, k := range tt.keys {
|
||||
m = batchUpdate(m.Update(k))
|
||||
}
|
||||
batchUpdate(m.Update(codeKeypress(tea.KeyEnter)))
|
||||
|
||||
require.Equal(t, tt.wantResult, *result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHuhPrompterConfirm(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
defaultValue bool
|
||||
keys []tea.KeyPressMsg
|
||||
wantResult bool
|
||||
}{
|
||||
{
|
||||
name: "default false submitted as-is",
|
||||
defaultValue: false,
|
||||
wantResult: false,
|
||||
},
|
||||
{
|
||||
name: "default true submitted as-is",
|
||||
defaultValue: true,
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "toggle from false to true with left arrow",
|
||||
defaultValue: false,
|
||||
keys: []tea.KeyPressMsg{codeKeypress(tea.KeyLeft)},
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "toggle from true to false with right arrow",
|
||||
defaultValue: true,
|
||||
keys: []tea.KeyPressMsg{codeKeypress(tea.KeyRight)},
|
||||
wantResult: false,
|
||||
},
|
||||
{
|
||||
name: "accept with y key",
|
||||
defaultValue: false,
|
||||
keys: []tea.KeyPressMsg{keypress('y')},
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "reject with n key",
|
||||
defaultValue: true,
|
||||
keys: []tea.KeyPressMsg{keypress('n')},
|
||||
wantResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := newTestHuhPrompter()
|
||||
f, result := p.buildConfirmForm("Sure?", tt.defaultValue)
|
||||
f.Update(f.Init())
|
||||
|
||||
var m huh.Model = f
|
||||
for _, k := range tt.keys {
|
||||
m = batchUpdate(m.Update(k))
|
||||
}
|
||||
batchUpdate(m.Update(codeKeypress(tea.KeyEnter)))
|
||||
|
||||
require.Equal(t, tt.wantResult, *result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHuhPrompterPassword(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantResult string
|
||||
}{
|
||||
{
|
||||
name: "basic password",
|
||||
input: "s3cret",
|
||||
wantResult: "s3cret",
|
||||
},
|
||||
{
|
||||
name: "empty password",
|
||||
wantResult: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := newTestHuhPrompter()
|
||||
f, result := p.buildPasswordForm("Password:")
|
||||
f.Update(f.Init())
|
||||
|
||||
var m huh.Model = f
|
||||
if tt.input != "" {
|
||||
m = typeText(m, tt.input)
|
||||
}
|
||||
batchUpdate(m.Update(codeKeypress(tea.KeyEnter)))
|
||||
|
||||
require.Equal(t, tt.wantResult, *result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHuhPrompterMarkdownEditor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
blankAllowed bool
|
||||
keys []tea.KeyPressMsg
|
||||
wantResult string
|
||||
}{
|
||||
{
|
||||
name: "selects launch by default",
|
||||
blankAllowed: true,
|
||||
wantResult: "launch",
|
||||
},
|
||||
{
|
||||
name: "navigate to skip",
|
||||
blankAllowed: true,
|
||||
keys: []tea.KeyPressMsg{keypress('j')},
|
||||
wantResult: "skip",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := newTestHuhPrompter()
|
||||
f, result := p.buildMarkdownEditorForm("Body:", tt.blankAllowed)
|
||||
f.Update(f.Init())
|
||||
|
||||
var m huh.Model = f
|
||||
for _, k := range tt.keys {
|
||||
m = batchUpdate(m.Update(k))
|
||||
}
|
||||
batchUpdate(m.Update(codeKeypress(tea.KeyEnter)))
|
||||
|
||||
require.Equal(t, tt.wantResult, *result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHuhPrompterMultiSelectWithSearch(t *testing.T) {
|
||||
staticSearchFunc := func(query string) MultiSelectSearchResult {
|
||||
if query == "" {
|
||||
return MultiSelectSearchResult{
|
||||
Keys: []string{"result-a", "result-b"},
|
||||
Labels: []string{"Result A", "Result B"},
|
||||
}
|
||||
}
|
||||
return MultiSelectSearchResult{
|
||||
Keys: []string{"search-1", "search-2"},
|
||||
Labels: []string{"Search 1", "Search 2"},
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
defaults []string
|
||||
persistent []string
|
||||
keys []tea.KeyPressMsg
|
||||
wantResult []string
|
||||
}{
|
||||
{
|
||||
name: "defaults are pre-selected and returned on immediate submit",
|
||||
defaults: []string{"result-a"},
|
||||
keys: []tea.KeyPressMsg{
|
||||
// Tab past the search input to the multi-select, then submit.
|
||||
codeKeypress(tea.KeyTab),
|
||||
codeKeypress(tea.KeyEnter),
|
||||
},
|
||||
wantResult: []string{"result-a"},
|
||||
},
|
||||
{
|
||||
name: "toggle an option from search results",
|
||||
keys: []tea.KeyPressMsg{
|
||||
codeKeypress(tea.KeyTab), // advance to multi-select
|
||||
keypress('x'), // toggle first option (result-a)
|
||||
codeKeypress(tea.KeyEnter), // submit
|
||||
},
|
||||
wantResult: []string{"result-a"},
|
||||
},
|
||||
{
|
||||
name: "toggle multiple options",
|
||||
keys: []tea.KeyPressMsg{
|
||||
codeKeypress(tea.KeyTab), // advance to multi-select
|
||||
keypress('x'), // toggle result-a
|
||||
keypress('j'), // move to result-b
|
||||
keypress('x'), // toggle result-b
|
||||
codeKeypress(tea.KeyEnter), // submit
|
||||
},
|
||||
wantResult: []string{"result-a", "result-b"},
|
||||
},
|
||||
{
|
||||
name: "no selection returns empty",
|
||||
keys: []tea.KeyPressMsg{
|
||||
codeKeypress(tea.KeyTab),
|
||||
codeKeypress(tea.KeyEnter),
|
||||
},
|
||||
wantResult: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := newTestHuhPrompter()
|
||||
f, result := p.buildMultiSelectWithSearchForm(
|
||||
"Select", "Search", tt.defaults, tt.persistent, staticSearchFunc,
|
||||
)
|
||||
doAllUpdates(f, f.Init())
|
||||
|
||||
for _, k := range tt.keys {
|
||||
_, cmd := f.Update(k)
|
||||
doAllUpdates(f, cmd)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.wantResult, *result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHuhPrompterMultiSelectWithSearchPersistence(t *testing.T) {
|
||||
callCount := 0
|
||||
staticSearchFunc := func(query string) MultiSelectSearchResult {
|
||||
callCount++
|
||||
if query == "" {
|
||||
return MultiSelectSearchResult{
|
||||
Keys: []string{"result-a", "result-b"},
|
||||
Labels: []string{"Result A", "Result B"},
|
||||
}
|
||||
}
|
||||
return MultiSelectSearchResult{
|
||||
Keys: []string{"search-1", "search-2"},
|
||||
Labels: []string{"Search 1", "Search 2"},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("selections persist after changing search query", func(t *testing.T) {
|
||||
p := newTestHuhPrompter()
|
||||
f, result := p.buildMultiSelectWithSearchForm(
|
||||
"Select", "Search", nil, nil, staticSearchFunc,
|
||||
)
|
||||
doAllUpdates(f, f.Init())
|
||||
|
||||
steps := []tea.KeyPressMsg{
|
||||
// Tab to multi-select, toggle result-a.
|
||||
codeKeypress(tea.KeyTab),
|
||||
keypress('x'),
|
||||
// Shift+Tab back to search input, type "foo".
|
||||
shiftTabKeypress(),
|
||||
keypress('f'), keypress('o'), keypress('o'),
|
||||
// Tab back to multi-select — result-a should still be selected.
|
||||
codeKeypress(tea.KeyTab),
|
||||
// Submit.
|
||||
codeKeypress(tea.KeyEnter),
|
||||
}
|
||||
|
||||
for _, k := range steps {
|
||||
_, cmd := f.Update(k)
|
||||
doAllUpdates(f, cmd)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"result-a"}, *result)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue