diff --git a/NOTICE b/NOTICE index 3ff1cc6243..01f9df9824 100644 --- a/NOTICE +++ b/NOTICE @@ -139,6 +139,10 @@ charmbracelet/lipgloss - https://github.com/charmbracelet/lipgloss Copyright (c) 2021-2025 Charmbracelet, Inc License - https://github.com/charmbracelet/lipgloss/blob/master/LICENSE +charmbracelet/x/ansi - https://github.com/charmbracelet/x +Copyright (c) 2023-2025 Charmbracelet, Inc +License - https://github.com/charmbracelet/x/blob/main/ansi/LICENSE + Masterminds/semver - https://github.com/Masterminds/semver Copyright (C) 2014-2019, Matt Butcher and Matt Farina License - https://github.com/Masterminds/semver/blob/master/LICENSE.txt diff --git a/go.mod b/go.mod index ef9122ae14..22e9093b71 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.132.0 // Apache-2.0 github.com/google/jsonschema-go v0.4.3 // MIT github.com/google/uuid v1.6.0 // BSD-3-Clause @@ -53,7 +54,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/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..005f815605 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_alt_key_noop_baseline_test.go @@ -0,0 +1,43 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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. The same shape applies to Alt+b, Alt+d, +// Alt+Backspace, and any other modified key the prompt model doesn't +// handle; pinning Alt+f covers the class. +func TestPromptBaseline_AltKeyNoop(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace name", + }) + 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) + v, err := tm.Result() + require.NoError(t, 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", v) +} 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..a818d1ba93 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_ctrl_c_baseline_test.go @@ -0,0 +1,32 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace name", + }) + tm.WaitFor("Workspace name") + tm.Golden("01-empty") + + tm.Type("partial input") + tm.Golden("02-after-typing") + + tm.Type(termtest.KeyCtrlC) + + v, err := tm.Result() + require.Error(t, err) + assert.EqualError(t, err, "^C") + assert.Empty(t, v) +} 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..90f1b35579 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_ctrl_fb_baseline_test.go @@ -0,0 +1,36 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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. The emacs-style bindings are de-facto aliases +// for the arrow keys; this test pins that equivalence. +func TestPromptBaseline_CtrlFCtrlB(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace name", + }) + 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) + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "hello", v) +} 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..eee56de461 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_ctrl_h_baseline_test.go @@ -0,0 +1,35 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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); the prompt model +// handles both as backspace, making the control-character form a de-facto +// alias. This test pins that equivalence so a future change can't silently +// drop it. +func TestPromptBaseline_CtrlH(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace name", + }) + 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) + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "hel", v) +} 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..3961f4bd5b --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_ctrl_j_baseline_test.go @@ -0,0 +1,30 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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); the prompt model treats both as +// submit. A future change that only reacts to CR would silently swallow +// Ctrl+J; this test pins the parity. +func TestPromptBaseline_CtrlJ(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace name", + }) + tm.WaitFor("Workspace name") + tm.Type("hello") + tm.Golden("01-typed-hello") + + tm.Type(termtest.KeyCtrlJ) + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "hello", v) +} 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..7f6bbc5abf --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_cursor_editing_baseline_test.go @@ -0,0 +1,59 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "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. +// The prompt model handles ←/→ and Backspace; Home/End/Ctrl+W/Ctrl+U are +// no-ops, so the goldens after them are intentionally identical to the +// post-Backspace one. The Delete key (\x1b[3~) is *not* covered here +// because it exits the prompt with EOF; that behavior is pinned separately +// by TestPromptBaseline_DeleteKeyExits. +func TestPromptBaseline_CursorEditing(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace name", + }) + 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) + + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + // The goldens above show the visible buffer is "hello worldX!" when + // Enter fires; that's what the prompt returns. + assert.Equal(t, "hello worldX!", v, "snapshot:\n%s", tm.Snapshot()) +} 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..be9334d9d3 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_delete_key_exits_baseline_test.go @@ -0,0 +1,36 @@ +package cmdiotest_test + +import ( + "io" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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 — and discards any input +// the user had already typed. The prompt model collapses both keys into the +// same EOF path; see prompt.go for the rationale. Pinning the behavior here +// makes sure a future change that splits the two keys is intentional. +func TestPromptBaseline_DeleteKeyExits(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace name", + }) + 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) + + v, err := tm.Result() + require.Error(t, err, "raw output: %q", tm.Raw()) + assert.ErrorIs(t, err, io.EOF) + assert.Empty(t, v, "Delete-as-EOF discards typed input") +} 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..ab555a2d5f --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_hide_entered_baseline_test.go @@ -0,0 +1,50 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace name", + HideEntered: false, + }) + tm.WaitFor("Workspace name") + tm.Type("hello") + tm.Type(termtest.KeyEnter) + + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "hello", v, "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) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace name", + HideEntered: true, + }) + tm.WaitFor("Workspace name") + tm.Type("hello") + tm.Type(termtest.KeyEnter) + + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "hello", v, "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..3db492592b --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_mask_baseline_test.go @@ -0,0 +1,37 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Personal access token", + Mask: '*', + }) + 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) + + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "dapi-sec", v, "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..2798d7d5a5 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_plain_baseline_test.go @@ -0,0 +1,36 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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). This is the most common shape used across +// cmd/auth and cmd/configure. +func TestPromptBaseline_Plain(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace name", + }) + 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) + + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "help there", v, "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..184ebd5cb5 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_utf8_baseline_test.go @@ -0,0 +1,50 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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. +// An implementation that counts bytes instead of runes would silently +// corrupt non-ASCII input even with ASCII tests passing. +func TestPromptBaseline_UTF8(t *testing.T) { + // On Windows, bubbletea wraps non-console input in + // github.com/mattn/go-localereader (see key_windows.go), which decodes + // each incoming byte ≥0x80 as the system ANSI code page (CP1252 on + // English Windows). Our pipe-based harness feeds raw UTF-8, so the + // c3 a9 bytes for "é" get re-read as Latin-1 Ã + © and never reach + // the prompt model as a single rune. The model itself handles UTF-8 + // correctly in production (where bytes come from the real console). + if runtime.GOOS == "windows" { + t.Skip("bubbletea localereader mangles UTF-8 over non-console input on Windows") + } + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Name", + }) + 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) + + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "café", v, "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..5f703a5865 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go @@ -0,0 +1,47 @@ +package cmdiotest_test + +import ( + "errors" + "strings" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace host", + Validate: func(s string) error { + if !strings.Contains(s, "://") { + return errors.New("must contain ://") + } + return nil + }, + }) + 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) + + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "https://example.com", v, "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..003f04c6d3 --- /dev/null +++ b/libs/cmdio/cmdiotest/secret_baseline_test.go @@ -0,0 +1,31 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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) { + t.Parallel() + tm := termtest.NewSecret(t, "Enter password") + 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) + + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "hunter", v, "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..d547df0d77 --- /dev/null +++ b/libs/cmdio/cmdiotest/secret_ctrl_c_baseline_test.go @@ -0,0 +1,28 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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) { + t.Parallel() + tm := termtest.NewSecret(t, "Personal access token") + tm.WaitFor("Personal access token") + tm.Golden("01-empty") + + tm.Type("abc") + tm.Golden("02-after-typing") + + tm.Type(termtest.KeyCtrlC) + + v, err := tm.Result() + require.Error(t, err) + assert.EqualError(t, err, "^C") + assert.Empty(t, v, "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..0e4eeeda4b --- /dev/null +++ b/libs/cmdio/cmdiotest/secret_empty_enter_baseline_test.go @@ -0,0 +1,24 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "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) { + t.Parallel() + tm := termtest.NewSecret(t, "Personal access token") + tm.WaitFor("Personal access token") + tm.Golden("01-empty") + + tm.Type(termtest.KeyEnter) + + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Empty(t, v) +} 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..4328bb6984 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_arrow_page_nav_test.go @@ -0,0 +1,46 @@ +package cmdiotest_test + +import ( + "fmt" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "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). The select model treats +// both pairs as page-down / page-up rather than item-by-item movement. +func TestSelectBaseline_ArrowPageNav(t *testing.T) { + t.Parallel() + 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), + }) + } + + tm := termtest.NewSelectOrdered(t, items, "Pick one") + 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) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "id03", id, "snapshot:\n%s", tm.Snapshot()) +} 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..a9fb6fc9e6 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_c_test.go @@ -0,0 +1,31 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_CtrlC pins Select's behavior when the user cancels +// the prompt with Ctrl+C without making a selection. +func TestSelectBaseline_CtrlC(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type(termtest.KeyCtrlC) + + id, err := tm.Result() + require.Error(t, err) + assert.EqualError(t, err, "^C") + assert.Empty(t, 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..a1b141aa25 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_c_with_filter_test.go @@ -0,0 +1,36 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + + tm.Type("xyz") + tm.Golden("01-no-results-with-filter") + + tm.Type(termtest.KeyCtrlC) + + id, err := tm.Result() + require.Error(t, err) + assert.EqualError(t, err, "^C") + assert.Empty(t, id) +} 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..da17f76428 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_f_b_test.go @@ -0,0 +1,47 @@ +package cmdiotest_test + +import ( + "fmt" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "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. The list has 12 items against the default 5-row viewport, 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) { + t.Parallel() + 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), + }) + } + + tm := termtest.NewSelectOrdered(t, items, "Pick one") + 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) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "id03", id, "snapshot:\n%s", tm.Snapshot()) +} 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..024827c790 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_h_test.go @@ -0,0 +1,40 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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); the select +// model treats both as backspace inside the search buffer, and this test +// pins that equivalence for the filter editor. +func TestSelectBaseline_CtrlH(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + 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) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "a", 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..c4ae0533a4 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_j_test.go @@ -0,0 +1,37 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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); the bubbletea +// model treats both as submit, so Ctrl+J ends the prompt the same way Enter +// does. After one KeyDown the highlight is on "b" and that's what gets +// returned — pin the exact value so a future change can't silently return a +// different index while still rendering the same screen. +func TestSelectBaseline_CtrlJ(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type(termtest.KeyDown) + tm.Golden("02-after-down") + + tm.Type(termtest.KeyCtrlJ) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "b", 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..caa869928f --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_n_p_test.go @@ -0,0 +1,38 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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. This test pins that equivalence. +func TestSelectBaseline_CtrlNCtrlP(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "c"}, + }, "Pick one") + 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) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "b", 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..2d2ca0e803 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_default_templates_test.go @@ -0,0 +1,50 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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 — the model falls back to its built-in defaults, +// which print {{.}} (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) { + t.Parallel() + // 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"}, + } + + tm := termtest.NewSelect(t, cmdio.SelectOptions{ + Label: "Pick an item", + Items: items, + }) + tm.WaitFor("Pick an item") + tm.Golden("01-initial") + + tm.Type(termtest.KeyDown) + tm.Golden("02-after-down") + + tm.Type(termtest.KeyEnter) + + idx, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, 1, 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..7504a8ffd9 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_esc_key_test.go @@ -0,0 +1,44 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_EscKey pins Select's 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. +func TestSelectBaseline_EscKey(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + 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") + + // Esc is inert in this Select model: it neither finalizes the prompt + // nor clears the filter. So the filter is still "a" here, which matches + // only "alpha"; Enter submits that match. + tm.Type(termtest.KeyEnter) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "a", id, "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..2c644b14e8 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_cursor_editing_test.go @@ -0,0 +1,57 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_FilterCursorEditing pins how the search filter responds +// to cursor-editing keys: ←/→, Home/End, Delete, Ctrl+W. The goldens capture +// which keys actually edit the filter buffer in the current model. +func TestSelectBaseline_FilterCursorEditing(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + {Name: "delta", Id: "d"}, + {Name: "epsilon", Id: "e"}, + }, "Pick one") + 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) + + id, err := tm.Result() + require.Error(t, err) + assert.EqualError(t, err, "^C") + assert.Empty(t, 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..2bc59c3e17 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_cursor_test.go @@ -0,0 +1,43 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_FilterCursor pins Select's behavior when the user has +// navigated to a non-first item, then types a filter query. Documents how +// the cursor moves into and out of filter mode. +func TestSelectBaseline_FilterCursor(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + {Name: "delta", Id: "d"}, + {Name: "epsilon", Id: "e"}, + }, "Pick one") + 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) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "a", id, "snapshot:\n%s", tm.Snapshot()) +} 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..f1942a2d7a --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_no_match_test.go @@ -0,0 +1,45 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_FilterNoMatch pins Select's behavior when the user +// types a filter query that matches none of the items, then backspaces it +// out and hits Enter. +func TestSelectBaseline_FilterNoMatch(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + 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) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "a", 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..ce5710ff7b --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_scroll_test.go @@ -0,0 +1,53 @@ +package cmdiotest_test + +import ( + "fmt" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "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) { + t.Parallel() + 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), + }) + } + + tm := termtest.NewSelectOrdered(t, items, "Pick one") + 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) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "id19", id, "snapshot:\n%s", tm.Snapshot()) +} 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..76cbeb9c67 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_typing_test.go @@ -0,0 +1,43 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_FilterTyping pins Select's 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. +func TestSelectBaseline_FilterTyping(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + {Name: "delta", Id: "d"}, + {Name: "epsilon", Id: "e"}, + }, "Pick one") + 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) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "a", 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..4689f78a59 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_long_descriptions_test.go @@ -0,0 +1,40 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_LongDescriptions pins Select's 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. +func TestSelectBaseline_LongDescriptions(t *testing.T) { + t.Parallel() + 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"}, + } + + tm := termtest.NewSelectOrdered(t, items, "Pick a resource") + 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) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, items[2].Id, 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..4c75cdfa76 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_scroll_test.go @@ -0,0 +1,47 @@ +package cmdiotest_test + +import ( + "fmt" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_Scroll pins Select's scrolling behavior for a list +// larger than the 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. +func TestSelectBaseline_Scroll(t *testing.T) { + t.Parallel() + 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), + }) + } + + tm := termtest.NewSelectOrdered(t, items, "Pick one") + 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) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "id12", id, "snapshot:\n%s", tm.Snapshot()) +} 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..21970a3e8a --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_selected_template_test.go @@ -0,0 +1,56 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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) { + t.Parallel() + type item struct { + Name string + Id string + } + items := []item{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "c"}, + } + + tm := termtest.NewSelect(t, cmdio.SelectOptions{ + Label: "Pick one", + Items: items, + Active: `> {{ .Name }} ({{ .Id }})`, + Inactive: ` {{ .Name }} ({{ .Id }})`, + Selected: `Chose: {{ .Name }} ({{ .Id }})`, + }) + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type(termtest.KeyDown) + tm.Golden("02-after-down") + + tm.Type(termtest.KeyEnter) + + idx, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, 1, idx, "snapshot:\n%s", tm.Snapshot()) + + // Pin the rendered Selected template. This is the only test that asserts + // the post-Enter frame; if the Selected template stops rendering, 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..51c6517121 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_single_item_test.go @@ -0,0 +1,32 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_SingleItem pins Select's behavior when the input list +// contains exactly one entry: whether a prompt renders, what KeyDown does +// (no-op), and what id Enter returns. +func TestSelectBaseline_SingleItem(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "only", Id: "o"}, + }, "Pick one") + tm.WaitFor("Pick one") + tm.WaitFor("only") + tm.Golden("01-initial") + + tm.Type(termtest.KeyDown) + tm.Golden("02-after-down") + + tm.Type(termtest.KeyEnter) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "o", 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..f4b07015a0 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_slash_enters_search_test.go @@ -0,0 +1,58 @@ +package cmdiotest_test + +import ( + "strings" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "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) { + t.Parallel() + type item struct { + Name string + Id string + } + items := []item{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "c"}, + } + + tm := termtest.NewSelect(t, 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 }})`, + }) + 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) + + idx, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, 1, 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..9176fe5ffb --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_tab_key_test.go @@ -0,0 +1,43 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_TabKey pins Select's behavior when the user presses +// Tab. Tab is a common navigation key but in search-mode Select it gets +// typed into the filter; this test records that, plus how Enter behaves +// after the filter has no matches. +func TestSelectBaseline_TabKey(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + 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") + + // Enter does not terminate the prompt: in search mode with no matching + // items (the two Tab keystrokes typed two tab characters into the + // filter), the model treats Enter as inert. Ctrl+C cancels cleanly. + tm.Type(termtest.KeyEnter) + tm.Type(termtest.KeyCtrlC) + + id, err := tm.Result() + require.Error(t, err) + assert.EqualError(t, err, "^C") + assert.Empty(t, id) +} diff --git a/libs/cmdio/cmdiotest/select_baseline_test.go b/libs/cmdio/cmdiotest/select_baseline_test.go new file mode 100644 index 0000000000..ef09bd52ae --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_test.go @@ -0,0 +1,34 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_DownEnter pins Select's rendering end-to-end: the +// termtest emulator captures the rendered screen and we assert on the chosen +// item plus a snapshot of the prompt and visible options. +func TestSelectBaseline_DownEnter(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "c"}, + }, "Pick one") + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + tm.Type(termtest.KeyDown) + tm.Golden("02-after-down") + + tm.Type(termtest.KeyEnter) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "b", 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..18d2939235 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_vim_keys_test.go @@ -0,0 +1,42 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_VimKeys pins how Select reacts to vim-style 'j' and 'k' +// keys. cmdio.SelectOrdered runs with StartInSearchMode: true, so letters +// flow into the filter rather than acting as navigation. +func TestSelectBaseline_VimKeys(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + {Name: "delta", Id: "d"}, + }, "Pick one") + 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) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "a", id, "snapshot:\n%s", 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..c9a5f16568 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_vim_nav_outside_search_test.go @@ -0,0 +1,69 @@ +package cmdiotest_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_VimNavOutsideSearch pins Select'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) { + t.Parallel() + 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), + }) + } + + tm := termtest.NewSelect(t, 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 }})`, + }) + 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) + + idx, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, 0, idx, "snapshot:\n%s", tm.Snapshot()) +} 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..b5e771f33a --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_wrap_around_test.go @@ -0,0 +1,42 @@ +package cmdiotest_test + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSelectBaseline_WrapAround pins Select's behavior at the list edges: +// pressing Up on the first item and Down past the last item. +func TestSelectBaseline_WrapAround(t *testing.T) { + t.Parallel() + tm := termtest.NewSelectOrdered(t, []cmdio.Tuple{ + {Name: "alpha", Id: "a"}, + {Name: "beta", Id: "b"}, + {Name: "gamma", Id: "g"}, + }, "Pick one") + tm.WaitFor("Pick one") + tm.WaitFor("alpha") + tm.Golden("01-initial") + + // Up from the top: does the model 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) + + id, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Equal(t, "g", id, "snapshot:\n%s", tm.Snapshot()) +} diff --git a/libs/cmdio/cmdiotest/select_no_prompt_support_test.go b/libs/cmdio/cmdiotest/select_no_prompt_support_test.go new file mode 100644 index 0000000000..0f7e18bf80 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_no_prompt_support_test.go @@ -0,0 +1,29 @@ +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_NoPromptSupport pins the early-error path: when the cmdIO can't +// prompt (non-TTY streams, no DATABRICKS_OUTPUT_FORMAT override), +// cmdio.SelectOrdered returns "expected to have