diff --git a/NOTICE b/NOTICE index 2c90b58f2d..7215ba7d8d 100644 --- a/NOTICE +++ b/NOTICE @@ -111,6 +111,10 @@ golang.org/x/sys - https://github.com/golang/sys Copyright 2009 The Go Authors. License - https://github.com/golang/sys/blob/master/LICENSE +golang.org/x/term - https://github.com/golang/term +Copyright 2009 The Go Authors. +License - https://github.com/golang/term/blob/master/LICENSE + golang.org/x/text - https://github.com/golang/text Copyright 2009 The Go Authors. License - https://github.com/golang/text/blob/master/LICENSE @@ -171,3 +175,11 @@ License - https://github.com/yaml/go-yaml/blob/v3/LICENSE zalando/go-keyring - https://github.com/zalando/go-keyring Copyright (c) 2016 Zalando SE License - https://github.com/zalando/go-keyring/blob/master/LICENSE + +creack/pty - https://github.com/creack/pty +Copyright (c) 2019 Christopher Koch +License - https://github.com/creack/pty/blob/master/LICENSE + +hinshun/vt10x - https://github.com/hinshun/vt10x +Copyright (c) 2017 ActiveState Software Inc. +License - https://github.com/hinshun/vt10x/blob/master/LICENSE diff --git a/go.mod b/go.mod index 29248e31c7..a42d5ad1e1 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/creack/pty v1.1.24 // MIT github.com/databricks/databricks-sdk-go v0.128.0 // Apache-2.0 github.com/google/jsonschema-go v0.4.3 // MIT github.com/google/uuid v1.6.0 // BSD-3-Clause @@ -21,6 +22,7 @@ require ( github.com/hashicorp/terraform-exec v0.25.0 // MPL-2.0 github.com/hashicorp/terraform-json v0.27.2 // MPL-2.0 github.com/hexops/gotextdiff v1.0.3 // BSD-3-Clause + github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 // MIT github.com/jackc/pgx/v5 v5.9.2 // MIT github.com/manifoldco/promptui v0.9.0 // BSD-3-Clause github.com/mattn/go-isatty v0.0.21 // MIT @@ -39,6 +41,7 @@ require ( golang.org/x/oauth2 v0.36.0 // BSD-3-Clause golang.org/x/sync v0.20.0 // BSD-3-Clause golang.org/x/sys v0.43.0 // BSD-3-Clause + golang.org/x/term v0.42.0 // BSD-3-Clause golang.org/x/text v0.36.0 // BSD-3-Clause gopkg.in/ini.v1 v1.67.1 // Apache-2.0 ) diff --git a/go.sum b/go.sum index b9b3843e66..5b6cadfd5d 100644 --- a/go.sum +++ b/go.sum @@ -142,6 +142,8 @@ github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoK github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= +github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= diff --git a/libs/cmdio/cmdiotest/prompt_alt_key_noop_baseline_test.go b/libs/cmdio/cmdiotest/prompt_alt_key_noop_baseline_test.go new file mode 100644 index 0000000000..f6c6d4f06f --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_alt_key_noop_baseline_test.go @@ -0,0 +1,79 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_AltKeyNoop pins that Alt-prefixed keys are silent +// no-ops in [cmdio.RunPrompt]. Specifically, Alt+f (the readline binding +// for "move forward by word") must neither move the cursor nor insert a +// literal 'f' into the buffer. +// +// Why: chzyer/readline does process Alt+f — it calls o.buf.MoveToNextWord +// and fires the listener with key=MetaForward — but promptui's +// Cursor.Listen has no case for MetaForward and falls to a default branch +// that only does anything in erase-default mode. The listener wrapper +// then returns (nil, 0, true), which makes readline overwrite its buffer +// with empty. Net effect on the user-visible state (promptui's own +// `cur`): nothing changes. +// +// The same shape applies to Alt+b, Alt+d, Alt+Backspace and any other +// modified key promptui doesn't handle. Pinning Alt+f covers the class. +func TestPromptBaseline_AltKeyNoop(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Workspace name", + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Workspace name") + + // Type "hello" and move cursor two places left so it sits mid-word. + // If Alt+f moved the cursor (or inserted), goldens 01 and 02 would + // diverge. + tm.Type("hello") + tm.Type(termtest.KeyLeft) + tm.Type(termtest.KeyLeft) + tm.Golden("01-cursor-mid") + + tm.Type("\x1bf") + tm.Golden("02-after-alt-f") + + tm.Type(termtest.KeyEnter) + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + // Final guard: the returned value must be exactly "hello". A literal + // 'f' insertion would surface here even if the goldens above somehow + // missed it. + assert.Equal(t, "hello", res.value) +} diff --git a/libs/cmdio/cmdiotest/prompt_ctrl_c_baseline_test.go b/libs/cmdio/cmdiotest/prompt_ctrl_c_baseline_test.go new file mode 100644 index 0000000000..69c7b1e241 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_ctrl_c_baseline_test.go @@ -0,0 +1,59 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_CtrlC pins Ctrl+C cancellation for RunPrompt. Mirrors +// the equivalent Secret test: error is returned, value is empty, snapshot +// captures any "^C" that the terminal echoed. +func TestPromptBaseline_CtrlC(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Workspace name", + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Workspace name") + tm.Golden("01-empty") + + tm.Type("partial input") + tm.Golden("02-after-typing") + + tm.Type(termtest.KeyCtrlC) + + res := <-resCh + require.Error(t, res.err) + assert.Empty(t, res.value) + t.Logf("error: %v", res.err) +} diff --git a/libs/cmdio/cmdiotest/prompt_ctrl_fb_baseline_test.go b/libs/cmdio/cmdiotest/prompt_ctrl_fb_baseline_test.go new file mode 100644 index 0000000000..2807dba372 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_ctrl_fb_baseline_test.go @@ -0,0 +1,68 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_CtrlFCtrlB pins that Ctrl+F and Ctrl+B move the cursor +// one character forward and backward in [cmdio.RunPrompt], the same as the +// right and left arrow keys. +// +// chzyer/readline maps Ctrl+F to CharForward and Ctrl+B to CharBackward — +// the same runes the arrow keys decode to — and promptui's Cursor.Listen +// dispatches both via its KeyForward / KeyBackward cases. So the emacs- +// style bindings are de-facto aliases for the arrow keys; this test pins +// that equivalence. +func TestPromptBaseline_CtrlFCtrlB(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Workspace name", + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Workspace name") + tm.Type("hello") + tm.Golden("01-cursor-end") + + tm.Type(termtest.KeyCtrlB) + tm.Type(termtest.KeyCtrlB) + tm.Golden("02-after-ctrl-b-twice") + + tm.Type(termtest.KeyCtrlF) + tm.Golden("03-after-ctrl-f") + + tm.Type(termtest.KeyEnter) + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "hello", res.value) +} diff --git a/libs/cmdio/cmdiotest/prompt_ctrl_h_baseline_test.go b/libs/cmdio/cmdiotest/prompt_ctrl_h_baseline_test.go new file mode 100644 index 0000000000..64150162aa --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_ctrl_h_baseline_test.go @@ -0,0 +1,64 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_CtrlH pins that Ctrl+H deletes the character to the +// left of the cursor in [cmdio.RunPrompt] — the same as the Backspace key. +// +// Ctrl+H sends BS (0x08) and Backspace sends DEL (0x7f). chzyer/readline +// maps both to CharBackspace, and promptui's Cursor.Listen handles them +// uniformly via its KeyBackspace case. So the control-character form is +// a de-facto alias for the Backspace key; this test pins that equivalence +// so a future hand-rolled prompt implementation can't silently drop it. +func TestPromptBaseline_CtrlH(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Workspace name", + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Workspace name") + tm.Type("hello") + tm.Golden("01-typed-hello") + + tm.Type(termtest.KeyCtrlH) + tm.Type(termtest.KeyCtrlH) + tm.Golden("02-after-ctrl-h-twice") + + tm.Type(termtest.KeyEnter) + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "hel", res.value) +} diff --git a/libs/cmdio/cmdiotest/prompt_ctrl_j_baseline_test.go b/libs/cmdio/cmdiotest/prompt_ctrl_j_baseline_test.go new file mode 100644 index 0000000000..6c324a9c85 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_ctrl_j_baseline_test.go @@ -0,0 +1,59 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_CtrlJ pins that Ctrl+J submits the prompt in +// [cmdio.RunPrompt] — the same as the Enter (Return) key. +// +// Enter sends CR (0x0d) and Ctrl+J sends LF (0x0a). chzyer/readline maps +// both to CharEnter and ends the read loop, so promptui returns the +// current buffer either way. A future hand-rolled prompt that only +// reacts to CR would silently swallow Ctrl+J; this test pins the parity. +func TestPromptBaseline_CtrlJ(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Workspace name", + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Workspace name") + tm.Type("hello") + tm.Golden("01-typed-hello") + + tm.Type(termtest.KeyCtrlJ) + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "hello", res.value) +} diff --git a/libs/cmdio/cmdiotest/prompt_cursor_editing_baseline_test.go b/libs/cmdio/cmdiotest/prompt_cursor_editing_baseline_test.go new file mode 100644 index 0000000000..8ba30d1b76 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_cursor_editing_baseline_test.go @@ -0,0 +1,83 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_CursorEditing pins how RunPrompt responds to cursor +// movement and line-editing keys: ←/→, Home/End, Backspace, Ctrl+W, Ctrl+U. +// Promptui's Cursor.Listen handles Backspace; Ctrl+W and Ctrl+U have no case +// there, so they're no-ops in this prompt — the goldens after them are +// intentionally identical to the post-Backspace one. The Delete key (\x1b[3~) +// is *not* covered here because it conflates with Ctrl+D in chzyer/readline +// and exits the prompt; that behavior is pinned separately by +// TestPromptBaseline_DeleteKeyExits. +func TestPromptBaseline_CursorEditing(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Workspace name", + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Workspace name") + tm.Golden("01-empty") + + tm.Type("hello world") + tm.Golden("02-typed") + + tm.Type(termtest.KeyHome) + tm.Type("X") + tm.Golden("03-insert-at-start") + + tm.Type(termtest.KeyEnd) + tm.Type("!") + tm.Golden("04-insert-at-end") + + tm.Type(termtest.KeyLeft) + tm.Type(termtest.KeyLeft) + tm.Type("Y") + tm.Golden("05-insert-mid") + + tm.Type(termtest.KeyBackspace) + tm.Golden("06-after-backspace") + + tm.Type(termtest.KeyCtrlW) + tm.Golden("07-after-ctrl-w") + + tm.Type(termtest.KeyCtrlU) + tm.Golden("08-after-ctrl-u") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + t.Logf("returned: %q (err=%v)", res.value, res.err) +} diff --git a/libs/cmdio/cmdiotest/prompt_delete_key_exits_baseline_test.go b/libs/cmdio/cmdiotest/prompt_delete_key_exits_baseline_test.go new file mode 100644 index 0000000000..53c304cedf --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_delete_key_exits_baseline_test.go @@ -0,0 +1,71 @@ +package cmdiotest_test + +import ( + "errors" + "io" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_DeleteKeyExits pins a surprising behavior of +// [cmdio.RunPrompt]: pressing the Delete key (\x1b[3~) exits the prompt with +// io.EOF, just like Ctrl+D would on an empty line. +// +// Why: chzyer/readline maps both Delete and Ctrl+D to the same internal rune +// (CharDelete = 4). Its CharDelete handler treats a non-empty buffer as +// "forward-delete" and an empty buffer as EOF. Promptui's listener resets +// readline's buffer to empty after every keystroke (cur is the source of +// truth, not o.buf), so from readline's perspective the buffer is always +// empty when Delete arrives — every Delete press takes the EOF path. +// +// This test pins the current behavior so that any change (e.g. a promptui or +// readline upgrade that splits the two keys) is intentional. +func TestPromptBaseline_DeleteKeyExits(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io2 := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io2) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Workspace name", + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Workspace name") + + // Type some content first to prove the buffer is non-empty from the user's + // perspective. This is what makes the behavior surprising: the prompt + // still exits even though the user has typed input. + tm.Type("hello") + tm.Type(termtest.KeyDelete) + + res := <-resCh + assert.Empty(t, res.value, "Delete-as-EOF discards typed input") + assert.Truef(t, errors.Is(res.err, io.EOF) || res.err.Error() == "^D", + "Delete should exit with EOF; got err=%v (raw output: %q)", res.err, tm.Raw()) +} diff --git a/libs/cmdio/cmdiotest/prompt_hide_entered_baseline_test.go b/libs/cmdio/cmdiotest/prompt_hide_entered_baseline_test.go new file mode 100644 index 0000000000..e2a150f5c5 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_hide_entered_baseline_test.go @@ -0,0 +1,102 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_HideEnteredFalse pins the default post-Enter rendering +// of [cmdio.RunPrompt]: with HideEntered=false (the default), the entered +// value is shown alongside the label after the prompt closes. +func TestPromptBaseline_HideEnteredFalse(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Workspace name", + HideEntered: false, + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Workspace name") + tm.Type("hello") + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "hello", res.value, "snapshot:\n%s", tm.Snapshot()) + + tm.Golden("01-after-enter") +} + +// TestPromptBaseline_HideEnteredTrue pins that HideEntered=true clears the +// prompt frame after the user submits, leaving no trace of the entered value +// on screen. This is the path used by [cmdio.Secret]. +func TestPromptBaseline_HideEnteredTrue(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Workspace name", + HideEntered: true, + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Workspace name") + tm.Type("hello") + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "hello", res.value, "snapshot:\n%s", tm.Snapshot()) + + tm.Golden("01-after-enter") +} diff --git a/libs/cmdio/cmdiotest/prompt_mask_baseline_test.go b/libs/cmdio/cmdiotest/prompt_mask_baseline_test.go new file mode 100644 index 0000000000..491879d1b3 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_mask_baseline_test.go @@ -0,0 +1,64 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_Mask pins runprompts behavior +// when configured with Mask='*'. This is the shape used by `databricks +// configure` for personal access token entry (cmd/configure/configure.go:46). +func TestPromptBaseline_Mask(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Personal access token", + Mask: '*', + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Personal access token") + tm.Golden("01-empty") + + tm.Type("dapi-secret") + tm.Golden("02-after-typing") + + tm.Type(termtest.KeyBackspace) + tm.Type(termtest.KeyBackspace) + tm.Type(termtest.KeyBackspace) + tm.Golden("03-after-backspace") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "dapi-sec", res.value, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/prompt_plain_baseline_test.go b/libs/cmdio/cmdiotest/prompt_plain_baseline_test.go new file mode 100644 index 0000000000..1b6201795a --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_plain_baseline_test.go @@ -0,0 +1,63 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_Plain pins prompts behavior +// when configured with only a Label (no Validate, no Mask, no Default). This +// is the most common shape used across cmd/auth and cmd/configure. +func TestPromptBaseline_Plain(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Workspace name", + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Workspace name") + tm.Golden("01-empty") + + tm.Type("hello") + tm.Golden("02-after-typing") + + tm.Type(termtest.KeyBackspace) + tm.Type(termtest.KeyBackspace) + tm.Type("p there") + tm.Golden("03-after-edit") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "help there", res.value, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/prompt_utf8_baseline_test.go b/libs/cmdio/cmdiotest/prompt_utf8_baseline_test.go new file mode 100644 index 0000000000..c171202de4 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_utf8_baseline_test.go @@ -0,0 +1,69 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_UTF8 pins multi-byte rune handling: typing "café" +// (4 runes, 5 bytes) renders as 4 cells, one Backspace deletes one rune +// not one byte, and the returned value preserves the original code points. +// +// Promptui delegates to readline which is rune-aware. A bubbletea +// reimplementation that counts bytes will silently corrupt non-ASCII input +// even though ASCII tests still pass — exactly the kind of regression a +// migration baseline is meant to catch. +func TestPromptBaseline_UTF8(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Name", + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Name") + tm.Golden("01-empty") + + tm.Type("café") + tm.Golden("02-after-typing") + + tm.Type(termtest.KeyBackspace) + tm.Golden("03-after-backspace") + + tm.Type("é") + tm.Golden("04-restored") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "café", res.value, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go new file mode 100644 index 0000000000..1a9f5c2e1f --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go @@ -0,0 +1,74 @@ +package cmdiotest_test + +import ( + "errors" + "runtime" + "strings" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_Validate pins Prompt's behavior when a Validate +// callback is configured: validation re-runs on every keystroke, the +// indicator glyph reflects the result, and Enter is blocked while invalid. +func TestPromptBaseline_Validate(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + Label: "Workspace host", + Validate: func(s string) error { + if !strings.Contains(s, "://") { + return errors.New("must contain ://") + } + return nil + }, + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Workspace host") + tm.Golden("01-empty") + + tm.Type("abc") + tm.Golden("02-invalid-typing") + + tm.Type(termtest.KeyBackspace) + tm.Type(termtest.KeyBackspace) + tm.Type(termtest.KeyBackspace) + tm.Golden("03-cleared") + + tm.Type("https://example.com") + tm.Golden("04-valid") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "https://example.com", res.value, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/secret_baseline_test.go b/libs/cmdio/cmdiotest/secret_baseline_test.go new file mode 100644 index 0000000000..c3e1da28d1 --- /dev/null +++ b/libs/cmdio/cmdiotest/secret_baseline_test.go @@ -0,0 +1,59 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSecretBaseline_Typing pins secrets behavior: +// each typed character should render as the configured mask ('*'), backspace +// should erase one mask char, and Enter should return the typed value. +func TestSecretBaseline_Typing(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.Secret(ctx, "Enter password") + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Enter password") + tm.Golden("01-empty") + + tm.Type("hunter2") + tm.Golden("02-after-typing") + + tm.Type(termtest.KeyBackspace) + tm.Golden("03-after-backspace") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "hunter", res.value, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/secret_ctrl_c_baseline_test.go b/libs/cmdio/cmdiotest/secret_ctrl_c_baseline_test.go new file mode 100644 index 0000000000..770605998b --- /dev/null +++ b/libs/cmdio/cmdiotest/secret_ctrl_c_baseline_test.go @@ -0,0 +1,57 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSecretBaseline_CtrlC pins Secret's behavior when the user cancels +// with Ctrl+C after typing a few characters. +func TestSecretBaseline_CtrlC(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.Secret(ctx, "Personal access token") + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Personal access token") + tm.Golden("01-empty") + + tm.Type("abc") + tm.Golden("02-after-typing") + + tm.Type(termtest.KeyCtrlC) + + res := <-resCh + require.Error(t, res.err) + t.Logf("error: %v", res.err) + t.Logf("value: %q", res.value) + assert.Empty(t, res.value, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/secret_empty_enter_baseline_test.go b/libs/cmdio/cmdiotest/secret_empty_enter_baseline_test.go new file mode 100644 index 0000000000..cfdc9d5030 --- /dev/null +++ b/libs/cmdio/cmdiotest/secret_empty_enter_baseline_test.go @@ -0,0 +1,51 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestSecretBaseline_EmptyEnter pins Secret's behavior when the user +// presses Enter immediately without typing anything. +func TestSecretBaseline_EmptyEnter(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + value string + err error + } + resCh := make(chan result, 1) + go func() { + v, err := cmdio.Secret(ctx, "Personal access token") + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Personal access token") + tm.Golden("01-empty") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + t.Logf("value: %q", res.value) + t.Logf("error: %v", res.err) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_arrow_page_nav_test.go b/libs/cmdio/cmdiotest/select_baseline_arrow_page_nav_test.go new file mode 100644 index 0000000000..5cc22c16f0 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_arrow_page_nav_test.go @@ -0,0 +1,72 @@ +package cmdiotest_test + +import ( + "fmt" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_ArrowPageNav pins that the right and left arrow +// keys page through the selection list — the same as Ctrl+F / Ctrl+B +// (covered by TestSelectBaseline_CtrlFCtrlB). Promptui maps both pairs +// to KeyForward / KeyBackward, which the select widget treats as +// page-down / page-up rather than item-by-item movement. +func TestSelectBaseline_ArrowPageNav(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + items := make([]cmdio.Tuple, 0, 12) + for i := 1; i <= 12; i++ { + items = append(items, cmdio.Tuple{ + Name: fmt.Sprintf("item-%02d", i), + Id: fmt.Sprintf("id%02d", i), + }) + } + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, items, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("item-01") + tm.Golden("01-initial") + + tm.Type(termtest.KeyRight) + tm.Golden("02-after-right") + + tm.Type(termtest.KeyRight) + tm.Golden("03-after-right-twice") + + tm.Type(termtest.KeyLeft) + tm.Golden("04-after-left") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_ctrl_c_test.go b/libs/cmdio/cmdiotest/select_baseline_ctrl_c_test.go new file mode 100644 index 0000000000..5df663e408 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_c_test.go @@ -0,0 +1,58 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_CtrlC pins the current promptui-driven Select behavior +// when the user cancels the prompt with Ctrl+C without making a selection. +// Captured as a migration baseline for the upcoming bubbletea replacement. +func TestSelectBaseline_CtrlC(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type(termtest.KeyCtrlC) + + res := <-resCh + require.Error(t, res.err) + t.Logf("error: %v", res.err) + t.Logf("id: %q", res.id) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_ctrl_c_with_filter_test.go b/libs/cmdio/cmdiotest/select_baseline_ctrl_c_with_filter_test.go new file mode 100644 index 0000000000..0768c2f779 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_c_with_filter_test.go @@ -0,0 +1,63 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_CtrlCWithFilter pins the cancel path when the search +// filter is non-empty. Readline interprets Ctrl+C globally as interrupt; a +// naive replacement could rebind it to "clear input" first and only cancel on +// the second press. The error sentinel and an empty returned id must match +// the no-filter case. +func TestSelectBaseline_CtrlCWithFilter(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + + tm.Type("xyz") + tm.Golden("01-no-results-with-filter") + + tm.Type(termtest.KeyCtrlC) + + res := <-resCh + require.Error(t, res.err) + assert.Empty(t, res.id) + t.Logf("error: %v", res.err) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_ctrl_f_b_test.go b/libs/cmdio/cmdiotest/select_baseline_ctrl_f_b_test.go new file mode 100644 index 0000000000..37c1dedc03 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_f_b_test.go @@ -0,0 +1,76 @@ +package cmdiotest_test + +import ( + "fmt" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_CtrlFCtrlB pins that Ctrl+F and Ctrl+B page through +// the selection list — distinct from Ctrl+N / Ctrl+P which move by one +// item. Promptui maps these to KeyForward / KeyBackward (the same runes +// the right and left arrow keys decode to), and the select widget treats +// them as page-down / page-up. +// +// The list has 12 items against promptui's default visible window of 5, +// so a single Ctrl+F should advance the highlighted item by roughly a +// page rather than a single row, and Ctrl+B should walk it back. +func TestSelectBaseline_CtrlFCtrlB(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + items := make([]cmdio.Tuple, 0, 12) + for i := 1; i <= 12; i++ { + items = append(items, cmdio.Tuple{ + Name: fmt.Sprintf("item-%02d", i), + Id: fmt.Sprintf("id%02d", i), + }) + } + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, items, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("item-01") + tm.Golden("01-initial") + + tm.Type(termtest.KeyCtrlF) + tm.Golden("02-after-ctrl-f") + + tm.Type(termtest.KeyCtrlF) + tm.Golden("03-after-ctrl-f-twice") + + tm.Type(termtest.KeyCtrlB) + tm.Golden("04-after-ctrl-b") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_ctrl_h_test.go b/libs/cmdio/cmdiotest/select_baseline_ctrl_h_test.go new file mode 100644 index 0000000000..559bf59b22 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_h_test.go @@ -0,0 +1,67 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_CtrlH pins that Ctrl+H deletes the last character +// from the search filter in [cmdio.Select] — the same as the Backspace +// key. Ctrl+H sends BS (0x08) and Backspace sends DEL (0x7f); promptui's +// readline maps both to CharBackspace inside the search buffer, so this +// test pins the equivalence for the filter editor. +func TestSelectBaseline_CtrlH(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type("alp") + tm.Golden("02-after-typing-alp") + + tm.Type(termtest.KeyCtrlH) + tm.Type(termtest.KeyCtrlH) + tm.Golden("03-after-ctrl-h-twice") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "a", res.id, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_ctrl_j_test.go b/libs/cmdio/cmdiotest/select_baseline_ctrl_j_test.go new file mode 100644 index 0000000000..cba0c8ab1c --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_j_test.go @@ -0,0 +1,70 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_CtrlJ pins that Ctrl+J submits the Select prompt +// cleanly. Ctrl+J sends LF (0x0a) and Enter sends CR (0x0d); chzyer/readline +// maps both to CharEnter, so Ctrl+J ends the read loop the same way Enter +// does. The test does not assert which item is returned: promptui's +// listener has a bug where Ctrl+J resets the highlight to the first item +// before returning (Enter from the same state correctly returns "b" — +// pinned by TestSelectBaseline_DownEnter), and a future implementation +// is free to fix that. We only require that submission succeeds and that +// the returned id is one of the valid items. +func TestSelectBaseline_CtrlJ(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type(termtest.KeyDown) + tm.Golden("02-after-down") + + tm.Type(termtest.KeyCtrlJ) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + // promptui today returns "a" here (the first item) instead of the + // highlighted "b"; a future implementation may return "b". Accept any + // valid id so the test pins submission, not the parity miss. + assert.Contains(t, []string{"a", "b", "g"}, res.id, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_ctrl_n_p_test.go b/libs/cmdio/cmdiotest/select_baseline_ctrl_n_p_test.go new file mode 100644 index 0000000000..43663c3a64 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_n_p_test.go @@ -0,0 +1,67 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_CtrlNCtrlP pins that Ctrl+N and Ctrl+P move the +// selection down and up by one item — the same as the down and up arrow +// keys. Promptui exposes these as KeyNext (= readline.CharNext) and +// KeyPrev (= readline.CharPrev), the same runes the arrow keys decode +// to in chzyer/readline; this test pins that equivalence. +func TestSelectBaseline_CtrlNCtrlP(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "c"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type(termtest.KeyCtrlN) + tm.Type(termtest.KeyCtrlN) + tm.Golden("02-after-ctrl-n-twice") + + tm.Type(termtest.KeyCtrlP) + tm.Golden("03-after-ctrl-p") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "b", res.id, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_default_templates_test.go b/libs/cmdio/cmdiotest/select_baseline_default_templates_test.go new file mode 100644 index 0000000000..1432f75e62 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_default_templates_test.go @@ -0,0 +1,77 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_DefaultTemplates pins the rendering of +// [cmdio.RunSelect] when no Label / Active / Inactive / Selected +// template is provided — promptui falls back to its built-in defaults, +// which print {{.}} (i.e. Go's default formatting for the item struct +// rather than any specific field). +// +// This mirrors the `databricks selftest tui run-select` plain mode +// (cmd/selftest/tui/select.go: runSelectPlain) and exists so future +// changes to the defaults — or accidental loss of a custom template at +// a call site — produce a visible diff. +func TestSelectBaseline_DefaultTemplates(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + // Same data shape as cmd/selftest/tui/fixtures.go buildItems(5). + items := []cmdio.Tuple{ + {Name: "unity-catalog", Id: "id-01"}, + {Name: "delta-lake", Id: "id-02"}, + {Name: "delta-sharing", Id: "id-03"}, + {Name: "photon", Id: "id-04"}, + {Name: "mlflow", Id: "id-05"}, + } + + type result struct { + idx int + err error + } + resCh := make(chan result, 1) + go func() { + idx, err := cmdio.RunSelect(ctx, cmdio.SelectOptions{ + Label: "Pick an item", + Items: items, + }) + resCh <- result{idx: idx, err: err} + }() + + tm.WaitFor("Pick an item") + tm.Golden("01-initial") + + tm.Type(termtest.KeyDown) + tm.Golden("02-after-down") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, 1, res.idx, "snapshot:\n%s", tm.Snapshot()) + + tm.Golden("03-after-enter") +} diff --git a/libs/cmdio/cmdiotest/select_baseline_esc_key_test.go b/libs/cmdio/cmdiotest/select_baseline_esc_key_test.go new file mode 100644 index 0000000000..1ace31f315 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_esc_key_test.go @@ -0,0 +1,77 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + "time" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_EscKey pins the current promptui-driven Select behavior +// when the user presses Esc at various states: the initial prompt, and after +// typing into the search filter. cmdio.Select uses StartInSearchMode: true, +// so the filter is active from the start. +// +// This test exists so the upcoming bubbletea replacement can be checked +// against a known-good baseline. Skipped on Windows because pty support there +// requires ConPTY plumbing creack/pty does not provide. +func TestSelectBaseline_EscKey(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type(termtest.KeyEsc) + tm.Golden("02-esc-from-initial") + + tm.Type("a") + tm.Golden("03-after-typing-a") + + tm.Type(termtest.KeyEsc) + tm.Golden("04-esc-clears-filter-or-not") + + select { + case res := <-resCh: + t.Logf("prompt returned after Esc: id=%q err=%v", res.id, res.err) + t.Logf("snapshot:\n%s", tm.Snapshot()) + case <-time.After(200 * time.Millisecond): + tm.Type(termtest.KeyEnter) + res := <-resCh + t.Logf("prompt finalized with Enter: id=%q err=%v", res.id, res.err) + t.Logf("snapshot:\n%s", tm.Snapshot()) + } +} diff --git a/libs/cmdio/cmdiotest/select_baseline_filter_cursor_editing_test.go b/libs/cmdio/cmdiotest/select_baseline_filter_cursor_editing_test.go new file mode 100644 index 0000000000..d124b2678c --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_cursor_editing_test.go @@ -0,0 +1,84 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_FilterCursorEditing pins how the search filter responds +// to cursor-editing keys: ←/→, Home/End, Delete, Ctrl+W. Promptui's readline +// supports all of these inside the search buffer; whether a hand-rolled +// bubbletea filter does is the whole point of the baseline. +func TestSelectBaseline_FilterCursorEditing(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + {Name: "delta", Id: "d"}, + {Name: "epsilon", Id: "e"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type("alp") + tm.Golden("02-after-typing-alp") + + tm.Type(termtest.KeyLeft) + tm.Type(termtest.KeyLeft) + tm.Type("X") + tm.Golden("03-after-insert-mid") + + tm.Type(termtest.KeyHome) + tm.Type("Y") + tm.Golden("04-after-insert-at-start") + + tm.Type(termtest.KeyEnd) + tm.Type("Z") + tm.Golden("05-after-insert-at-end") + + tm.Type(termtest.KeyCtrlU) + tm.Golden("06-after-ctrl-u") + + tm.Type("alpha") + tm.Type(termtest.KeyCtrlW) + tm.Golden("07-after-ctrl-w") + + tm.Type(termtest.KeyCtrlC) + + res := <-resCh + require.Error(t, res.err) + t.Logf("error: %v", res.err) + t.Logf("id: %q", res.id) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_filter_cursor_test.go b/libs/cmdio/cmdiotest/select_baseline_filter_cursor_test.go new file mode 100644 index 0000000000..7be11f7f14 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_cursor_test.go @@ -0,0 +1,71 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_FilterCursor pins the current promptui-driven Select +// behavior when the user has navigated to a non-first item, then types a +// filter query. This documents how the cursor moves into and out of filter +// mode so the upcoming bubbletea replacement can be checked against a known +// baseline. +func TestSelectBaseline_FilterCursor(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + {Name: "delta", Id: "d"}, + {Name: "epsilon", Id: "e"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type(termtest.KeyDown) + tm.Type(termtest.KeyDown) + tm.Golden("02-on-gamma") + + tm.Type("a") + tm.Golden("03-after-filter-a") + + tm.Type(termtest.KeyBackspace) + tm.Golden("04-after-clear-filter") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + t.Logf("selected: %s", res.id) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_filter_no_match_test.go b/libs/cmdio/cmdiotest/select_baseline_filter_no_match_test.go new file mode 100644 index 0000000000..91f8c41857 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_no_match_test.go @@ -0,0 +1,72 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_FilterNoMatch pins promptui's behavior when the user +// types a filter query that matches none of the items, then backspaces it +// out and hits Enter. Captured as a baseline for the bubbletea replacement. +func TestSelectBaseline_FilterNoMatch(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type("x") + tm.Golden("02-after-x") + + tm.Type("y") + tm.Golden("03-after-xy") + + tm.Type("z") + tm.Golden("04-after-xyz") + + tm.Type(termtest.KeyBackspace) + tm.Type(termtest.KeyBackspace) + tm.Type(termtest.KeyBackspace) + tm.Golden("05-after-backspaces") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "a", res.id, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_filter_scroll_test.go b/libs/cmdio/cmdiotest/select_baseline_filter_scroll_test.go new file mode 100644 index 0000000000..b2b6ed2166 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_scroll_test.go @@ -0,0 +1,79 @@ +package cmdiotest_test + +import ( + "fmt" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_FilterScroll pins viewport behavior when a filter +// narrows a long list to a count still larger than the viewport. Combines +// FilterTyping (substring search) with Scroll (12+ items) — neither test +// alone exercises the recompute-then-scroll path. +// +// 20 items named item-01 .. item-20; the filter "item-1" matches item-01 +// plus item-10..item-19 = 11 items, more than the 5-row viewport. +func TestSelectBaseline_FilterScroll(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + items := make([]cmdio.Tuple, 0, 20) + for i := 1; i <= 20; i++ { + items = append(items, cmdio.Tuple{ + Name: fmt.Sprintf("item-%02d", i), + Id: fmt.Sprintf("id%02d", i), + }) + } + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, items, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("item-01") + tm.Golden("01-initial") + + tm.Type("item-1") + tm.Golden("02-filtered-top") + + for range 5 { + tm.Type(termtest.KeyDown) + } + tm.Golden("03-filtered-mid") + + for range 10 { + tm.Type(termtest.KeyDown) + } + tm.Golden("04-filtered-bottom") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + t.Logf("selected: %s", res.id) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_filter_typing_test.go b/libs/cmdio/cmdiotest/select_baseline_filter_typing_test.go new file mode 100644 index 0000000000..ff4e249301 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_typing_test.go @@ -0,0 +1,74 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_FilterTyping pins the current promptui-driven Select +// behavior when the user types letters that filter the list. cmdio.Select +// uses StartInSearchMode: true with a case-insensitive substring searcher on +// Name, so each keystroke immediately narrows the visible options. +// +// This test exists so the upcoming bubbletea replacement can be checked +// against a known-good baseline. Skipped on Windows because pty support there +// requires ConPTY plumbing creack/pty does not provide. +func TestSelectBaseline_FilterTyping(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + {Name: "delta", Id: "d"}, + {Name: "epsilon", Id: "e"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type("a") + tm.Golden("02-after-a") + + tm.Type("l") + tm.Golden("03-after-al") + + tm.Type("p") + tm.Golden("04-after-alp") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "a", res.id, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_long_descriptions_test.go b/libs/cmdio/cmdiotest/select_baseline_long_descriptions_test.go new file mode 100644 index 0000000000..e5f59702e1 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_long_descriptions_test.go @@ -0,0 +1,71 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_LongDescriptions pins the current promptui-driven Select +// behavior when item Ids are long enough to potentially overflow the terminal +// width. The active row uses the Tuple template +// "{{.Name | bold}} ({{.Id|faint}})", so the long Id is only rendered on the +// active line; non-active rows show only the Name. +// +// This is a migration baseline: the bubbletea replacement should produce the +// same visible output, captured in goldens via vt10x. +func TestSelectBaseline_LongDescriptions(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + items := []cmdio.Tuple{ + {Name: "short", Id: "this-is-a-very-long-resource-identifier-that-exceeds-typical-width-1234567890"}, + {Name: "medium-length-name", Id: "another-extremely-long-id-string-with-lots-of-content-aaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {Name: "x", Id: "yet-another-long-identifier-with-quite-a-bit-of-text-bbbbbbbbbbbbbbbbbbbbbbbbbbb"}, + } + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, items, "Pick a resource") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick a resource") + tm.WaitFor("short") + tm.Golden("01-initial") + + tm.Type(termtest.KeyDown) + tm.Golden("02-second-active") + + tm.Type(termtest.KeyDown) + tm.Golden("03-third-active") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, items[2].Id, res.id, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_scroll_test.go b/libs/cmdio/cmdiotest/select_baseline_scroll_test.go new file mode 100644 index 0000000000..b6f94d5f50 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_scroll_test.go @@ -0,0 +1,76 @@ +package cmdiotest_test + +import ( + "fmt" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_Scroll pins the current promptui scrolling behavior for a +// list larger than promptui's default visible window. It feeds enough KeyDown +// presses to reach the last item and then keeps pressing past it, so the +// goldens capture both the bottom-of-list state and the past-bottom state. +// +// This baseline lets the upcoming bubbletea reimplementation be diffed against +// the exact rendering promptui produces today. Skipped on Windows because the +// pty harness is unix-only. +func TestSelectBaseline_Scroll(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + items := make([]cmdio.Tuple, 0, 12) + for i := 1; i <= 12; i++ { + items = append(items, cmdio.Tuple{ + Name: fmt.Sprintf("item-%02d", i), + Id: fmt.Sprintf("id%02d", i), + }) + } + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, items, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("item-01") + tm.Golden("01-initial") + + for range 11 { + tm.Type(termtest.KeyDown) + } + tm.Golden("02-bottom") + + for range 5 { + tm.Type(termtest.KeyDown) + } + tm.Golden("03-past-bottom") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_selected_template_test.go b/libs/cmdio/cmdiotest/select_baseline_selected_template_test.go new file mode 100644 index 0000000000..5ddd527261 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_selected_template_test.go @@ -0,0 +1,83 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_SelectedTemplate pins the post-Enter rendering of +// [cmdio.RunSelect] when a non-empty Selected template is provided. +// +// cmdio.Select / cmdio.SelectOrdered set HideSelected:true, so the Selected +// branch is only reachable via RunSelect. Real callers that hit it: +// cmd/auth/profile_picker.go, libs/databrickscfg/profile/select.go, +// libs/databrickscfg/cfgpickers/clusters.go. Without this test, breaking the +// post-submit render or the Selected template behavior goes undetected. +func TestSelectBaseline_SelectedTemplate(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type item struct { + Name string + Id string + } + items := []item{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "c"}, + } + + type result struct { + idx int + err error + } + resCh := make(chan result, 1) + go func() { + idx, err := cmdio.RunSelect(ctx, cmdio.SelectOptions{ + Label: "Pick one", + Items: items, + Active: `> {{ .Name }} ({{ .Id }})`, + Inactive: ` {{ .Name }} ({{ .Id }})`, + Selected: `Chose: {{ .Name }} ({{ .Id }})`, + }) + resCh <- result{idx: idx, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type(termtest.KeyDown) + tm.Golden("02-after-down") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, 1, res.idx, "snapshot:\n%s", tm.Snapshot()) + + // Pin the rendered Selected template. This is the only test that asserts + // the post-Enter frame; if promptui ever stops rendering Selected, or the + // trailing newline / cursor handling changes, this golden catches it. + tm.Golden("03-after-enter") +} diff --git a/libs/cmdio/cmdiotest/select_baseline_single_item_test.go b/libs/cmdio/cmdiotest/select_baseline_single_item_test.go new file mode 100644 index 0000000000..600f7ac84c --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_single_item_test.go @@ -0,0 +1,60 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_SingleItem pins the current promptui-driven Select +// behavior when the input list contains exactly one entry. It is a migration +// baseline for the bubbletea replacement: we want to know whether promptui +// renders a prompt at all, what KeyDown does, and what id Enter returns. +func TestSelectBaseline_SingleItem(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "only", Id: "o"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("only") + tm.Golden("01-initial") + + tm.Type(termtest.KeyDown) + tm.Golden("02-after-down") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "o", res.id, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_slash_enters_search_test.go b/libs/cmdio/cmdiotest/select_baseline_slash_enters_search_test.go new file mode 100644 index 0000000000..fdf42bd286 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_slash_enters_search_test.go @@ -0,0 +1,85 @@ +package cmdiotest_test + +import ( + "runtime" + "strings" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_SlashEntersSearch pins that pressing "/" toggles a +// non-search-mode select prompt into search mode. The existing filter +// tests all use cmdio.SelectOrdered (which sets StartInSearchMode=true) +// so the toggle path is never exercised. Real callers that depend on it: +// cmd/auth/resolve.go and cmd/auth/profile_picker.go set +// StartInSearchMode based on len(items) > 5, so for small lists the +// only way to filter is to press "/". +func TestSelectBaseline_SlashEntersSearch(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type item struct { + Name string + Id string + } + items := []item{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "c"}, + } + + type result struct { + idx int + err error + } + resCh := make(chan result, 1) + go func() { + idx, err := cmdio.RunSelect(ctx, cmdio.SelectOptions{ + Label: "Pick one", + Items: items, + Searcher: func(input string, idx int) bool { + return strings.Contains(strings.ToLower(items[idx].Name), strings.ToLower(input)) + }, + Active: `> {{ .Name }} ({{ .Id }})`, + Inactive: ` {{ .Name }} ({{ .Id }})`, + }) + resCh <- result{idx: idx, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial-no-search") + + // Slash toggles into search mode: a "Search:" line appears and + // subsequent characters become the filter query. + tm.Type("/") + tm.Golden("02-after-slash") + + tm.Type("b") + tm.Golden("03-filtering-b") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, 1, res.idx, "expected to land on beta; snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_tab_key_test.go b/libs/cmdio/cmdiotest/select_baseline_tab_key_test.go new file mode 100644 index 0000000000..7d3526893a --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_tab_key_test.go @@ -0,0 +1,75 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + "time" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" +) + +// TestSelectBaseline_TabKey pins the current promptui-driven Select behavior +// when the user presses Tab. Tab is a common navigation key but its handling +// in promptui's search-mode Select is unclear, so this test records the +// observed behavior as a migration baseline for the bubbletea replacement. +func TestSelectBaseline_TabKey(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + if !cmdio.IsPromptSupported(ctx) { + t.Fatal("prompt support must be detected on the pty") + } + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type(termtest.KeyTab) + tm.Golden("02-after-tab") + + tm.Type(termtest.KeyTab) + tm.Golden("03-after-second-tab") + + tm.Type(termtest.KeyEnter) + + // Enter may not terminate the prompt: in search mode with no matching + // items, promptui's outer loop keeps calling Readline. Wait briefly, and + // if no result arrives, send Ctrl+C so the goroutine can exit cleanly. + // The diagnostic is what we're after — any error or selection is recorded. + select { + case res := <-resCh: + t.Logf("returned id=%q err=%v", res.id, res.err) + case <-time.After(500 * time.Millisecond): + tm.Type(termtest.KeyCtrlC) + res := <-resCh + t.Logf("Enter did not terminate; after Ctrl+C: id=%q err=%v", res.id, res.err) + } +} diff --git a/libs/cmdio/cmdiotest/select_baseline_test.go b/libs/cmdio/cmdiotest/select_baseline_test.go new file mode 100644 index 0000000000..a86500f30f --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_test.go @@ -0,0 +1,67 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_DownEnter pins Select's rendering end-to-end: a real +// pty is fed arrow-key bytes, vt10x captures the rendered screen, and we +// assert on the chosen item plus a snapshot of the prompt and visible +// options. +// Skipped on Windows because pty support there requires ConPTY plumbing +// creack/pty does not provide. +func TestSelectBaseline_DownEnter(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + // cmdio.NewIO builds capabilities from the passed in/out/err. Using the + // pty slave for all three lets isatty detection succeed and routes prompt + // rendering (which goes to stderr) into the emulator. + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "c"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type(termtest.KeyDown) + tm.Golden("02-after-down") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + assert.Equal(t, "b", res.id, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_vim_keys_test.go b/libs/cmdio/cmdiotest/select_baseline_vim_keys_test.go new file mode 100644 index 0000000000..5d94741ce9 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_vim_keys_test.go @@ -0,0 +1,71 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_VimKeys pins how the current promptui-driven Select +// reacts to vim-style 'j' and 'k' keys. promptui's Select is configured with +// StartInSearchMode: true, so letters likely flow into the filter rather than +// acting as navigation. This baseline captures whatever it does today so the +// upcoming bubbletea replacement can decide deliberately whether to preserve, +// drop, or change that behavior. +func TestSelectBaseline_VimKeys(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + {Name: "delta", Id: "d"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type("j") + tm.Golden("02-after-j") + + tm.Type("k") + tm.Golden("03-after-jk") + + tm.Type(termtest.KeyBackspace) + tm.Type(termtest.KeyBackspace) + tm.Golden("04-after-backspaces") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + t.Logf("Enter selected id=%q (snapshot:\n%s\n)", res.id, tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_vim_nav_outside_search_test.go b/libs/cmdio/cmdiotest/select_baseline_vim_nav_outside_search_test.go new file mode 100644 index 0000000000..733fca6c11 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_vim_nav_outside_search_test.go @@ -0,0 +1,95 @@ +package cmdiotest_test + +import ( + "fmt" + "runtime" + "strings" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_VimNavOutsideSearch pins promptui's vim-style +// navigation when the prompt opens outside search mode. With +// StartInSearchMode=false, j/k move the highlighted item by one and +// h/l page through the list; with search mode enabled (the default in +// cmdio.SelectOrdered) those letters would flow into the filter +// instead. Real callers that hit this branch: +// cmd/auth/resolve.go and cmd/auth/profile_picker.go both set +// StartInSearchMode based on len(items) > 5, so small lists open +// outside search mode. +func TestSelectBaseline_VimNavOutsideSearch(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type item struct { + Name string + Id string + } + items := make([]item, 0, 12) + for i := 1; i <= 12; i++ { + items = append(items, item{ + Name: fmt.Sprintf("item-%02d", i), + Id: fmt.Sprintf("id%02d", i), + }) + } + + type result struct { + idx int + err error + } + resCh := make(chan result, 1) + go func() { + idx, err := cmdio.RunSelect(ctx, cmdio.SelectOptions{ + Label: "Pick one", + Items: items, + // StartInSearchMode defaults to false; setting a Searcher + // makes the / key toggle search mode but does not auto-enter. + Searcher: func(input string, idx int) bool { + return strings.Contains(strings.ToLower(items[idx].Name), strings.ToLower(input)) + }, + Active: `> {{ .Name }} ({{ .Id }})`, + Inactive: ` {{ .Name }} ({{ .Id }})`, + }) + resCh <- result{idx: idx, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("item-01") + tm.Golden("01-initial") + + tm.Type("j") + tm.Type("j") + tm.Golden("02-after-jj") + + tm.Type("k") + tm.Golden("03-after-k") + + tm.Type("l") + tm.Golden("04-after-l-pagedown") + + tm.Type("h") + tm.Golden("05-after-h-pageup") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_wrap_around_test.go b/libs/cmdio/cmdiotest/select_baseline_wrap_around_test.go new file mode 100644 index 0000000000..21eddf4d94 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_wrap_around_test.go @@ -0,0 +1,69 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_WrapAround pins promptui's behavior at the list edges: +// pressing Up on the first item and Down past the last item. This baseline +// lets the bubbletea replacement be checked against the current behavior. +func TestSelectBaseline_WrapAround(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pty-based prompt tests are unix-only") + } + + tm := termtest.New(t) + defer tm.Close() + + pts := tm.Pty() + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm-256color") + + ctx := t.Context() + io := cmdio.NewIO(ctx, flags.OutputText, pts, pts, pts, "", "") + ctx = cmdio.InContext(ctx, io) + + require.True(t, cmdio.IsPromptSupported(ctx), "prompt support must be detected on the pty") + + type result struct { + id string + err error + } + resCh := make(chan result, 1) + go func() { + id, err := cmdio.SelectOrdered(ctx, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + resCh <- result{id: id, err: err} + }() + + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + // Up from the top: does promptui wrap to the last item, pin to alpha, + // or do nothing visible? + tm.Type(termtest.KeyUp) + tm.Golden("02-up-from-top") + + // Five Downs: with three items, this overshoots the bottom by two, + // exposing whether Down wraps, pins, or beeps past the last item. + for range 5 { + tm.Type(termtest.KeyDown) + } + tm.Golden("03-down-past-bottom") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + t.Logf("selected: %s", res.id) +} diff --git a/libs/cmdio/cmdiotest/select_empty_items_test.go b/libs/cmdio/cmdiotest/select_empty_items_test.go new file mode 100644 index 0000000000..483a53390d --- /dev/null +++ b/libs/cmdio/cmdiotest/select_empty_items_test.go @@ -0,0 +1,27 @@ +package cmdiotest_test + +import ( + "bytes" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelect_EmptyItems pins the early-error path: cmdio.Select rejects an +// empty item list before drawing any prompt UI. No pty needed; the function +// returns immediately with a "expected to have