diff --git a/go.mod b/go.mod index b9877f9440a..6d2b02854f9 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/charmbracelet/bubbletea v1.3.10 // MIT github.com/charmbracelet/huh v1.0.0 // MIT github.com/charmbracelet/lipgloss v1.1.0 // MIT + github.com/charmbracelet/x/ansi v0.11.6 // MIT github.com/databricks/databricks-sdk-go v0.126.0 // Apache-2.0 github.com/fatih/color v1.19.0 // MIT github.com/google/jsonschema-go v0.4.2 // MIT @@ -55,7 +56,6 @@ require ( github.com/catppuccin/go v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect diff --git a/libs/apps/prompt/prompt_test.go b/libs/apps/prompt/prompt_test.go index 3400eab9bea..b1dd49972e0 100644 --- a/libs/apps/prompt/prompt_test.go +++ b/libs/apps/prompt/prompt_test.go @@ -3,15 +3,27 @@ package prompt import ( "context" "errors" + "strings" "testing" "time" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" + "github.com/charmbracelet/x/ansi" "github.com/databricks/cli/libs/apps/manifest" "github.com/databricks/cli/libs/cmdio" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// keys creates a tea.KeyMsg for simulating keyboard input in tests. +func keys(runes ...rune) tea.KeyMsg { + return tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: runes, + } +} + func TestValidateProjectName(t *testing.T) { tests := []struct { name string @@ -310,3 +322,99 @@ func TestMaxAppNameLength(t *testing.T) { assert.Len(t, invalidName, 27) assert.Error(t, ValidateProjectName(invalidName)) } + +// initForm runs the form's Init command and feeds the resulting message back +// through Update so the form picks up its initial layout (window size, focus). +// f.Update(f.Init()) silently swallows the cmd because Init returns a tea.Cmd +// (a function), not a tea.Msg, so the form never receives the WindowSizeMsg +// it needs to render. +func initForm(t *testing.T, f *huh.Form) { + t.Helper() + cmd := f.Init() + if cmd != nil { + if msg := cmd(); msg != nil { + f.Update(msg) + } + } + // huh derives layout from a window size; without one the help line + // and titles can be clipped or omitted. + f.Update(tea.WindowSizeMsg{Width: 80, Height: 24}) +} + +// newWarehouseSelect builds a huh.Select identical in shape to the one +// constructed inside promptFromListWithLabel. Production and tests share this +// builder so that future regressions to the production prompt's title or +// description show up in test output. +func newWarehouseSelect(title, description string, options ...string) *huh.Select[string] { + return huh.NewSelect[string](). + Options(huh.NewOptions(options...)...). + Title(title). + Description(description). + Height(8) +} + +// TestSelectTitleVisibleWithoutFiltering verifies that a Select field renders +// its Title on the initial view when Filtering is not activated. This is the +// behavior after the fix: the Title is always visible. +func TestSelectTitleVisibleWithoutFiltering(t *testing.T) { + field := newWarehouseSelect( + "Select SQL Warehouse", + "3 available — press / to filter", + "Warehouse A", "Warehouse B", "Warehouse C", + ) + + f := huh.NewForm(huh.NewGroup(field)) + initForm(t, f) + + view := ansi.Strip(f.View()) + + assert.Contains(t, view, "Select SQL Warehouse", "Title should be visible in initial render") + assert.Contains(t, view, "press / to filter", "Description should be visible") + assert.Contains(t, view, "Warehouse A", "First option should be visible") +} + +// TestSelectSlashKeyActivatesFilter verifies that pressing '/' activates +// filtering even without Filtering(true), and that it filters options. The +// title remains visible after activation, which is the regression this PR +// guards against. +func TestSelectSlashKeyActivatesFilter(t *testing.T) { + field := newWarehouseSelect("Select fruit", "", "Apple", "Apricot", "Banana") + + f := huh.NewForm(huh.NewGroup(field)) + initForm(t, f) + + // Title visible before filtering. + view := ansi.Strip(f.View()) + assert.Contains(t, view, "Select fruit") + assert.Contains(t, view, "Banana") + + // Press '/' to start filtering, then type 'B'. + m, _ := f.Update(keys('/')) + f = m.(*huh.Form) + m, _ = f.Update(keys('B')) + f = m.(*huh.Form) + + view = ansi.Strip(f.View()) + + // Once filter mode is active huh replaces the title with the filter input + // — that is upstream behavior. The fix this PR enforces is that the title + // is visible BEFORE the user presses '/', not after. + assert.Contains(t, view, "Banana", "Banana should match filter 'B'") + assert.NotContains(t, view, "Apple", "Apple should be filtered out") +} + +// TestSelectHelpShowsFilterHint verifies the help text includes a filter hint. +func TestSelectHelpShowsFilterHint(t *testing.T) { + field := newWarehouseSelect("Pick", "", "A", "B") + + f := huh.NewForm(huh.NewGroup(field)) + initForm(t, f) + + view := ansi.Strip(f.View()) + + // huh's default keymap shows "/ filter" in the help line. + assert.True(t, + strings.Contains(view, "/ filter") || strings.Contains(view, "filter"), + "Help text should mention filtering is available via '/'", + ) +}