From f188b797a75ded08428edb44d7d62b4afbbf919f Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Fri, 8 May 2026 13:58:51 +0200 Subject: [PATCH 01/16] cmdio: pin promptui prompt behavior with golden tests Adds libs/cmdio/cmdiotest, an external test package that drives cmdio.Select / SelectOrdered / Secret / RunPrompt through a real pty and snapshots the rendered screen via vt10x. 26 tests cover the prompt matrix as it behaves on origin/main today (still promptui under the hood): basic navigation, scrolling, filter typing and no-match, vim-key fallthrough, Esc/Tab inertness, Ctrl+C with and without filter text, single-item rendering, long descriptions, cursor editing keys (Home/End/Ctrl+W/Ctrl+U), 20-item filter scroll, UTF-8 input, and the empty-list rejection path. Lives on its own branch as a one-shot test capsule: never merged to main, copied into the bubbletea migration branch to verify each widget swap leaves the goldens byte-identical. Once promptui is gone the harness is rewritten on charmbracelet/x/exp/teatest, which lets us drop creack/pty and x/term entirely. Adds three test-only deps with license annotations: - creack/pty (MIT) - hinshun/vt10x (MIT) - golang.org/x/term (BSD-3-Clause) Co-authored-by: Isaac --- NOTICE | 12 + go.mod | 3 + go.sum | 2 + .../cmdiotest/prompt_ctrl_c_baseline_test.go | 59 ++++ .../prompt_cursor_editing_baseline_test.go | 79 ++++++ .../prompt_default_no_edit_baseline_test.go | 62 ++++ .../cmdiotest/prompt_mask_baseline_test.go | 64 +++++ .../cmdiotest/prompt_plain_baseline_test.go | 63 +++++ .../cmdiotest/prompt_utf8_baseline_test.go | 69 +++++ .../prompt_validate_baseline_test.go | 74 +++++ libs/cmdio/cmdiotest/secret_baseline_test.go | 60 ++++ .../cmdiotest/secret_ctrl_c_baseline_test.go | 58 ++++ .../secret_empty_enter_baseline_test.go | 51 ++++ .../cmdiotest/select_baseline_ctrl_c_test.go | 59 ++++ ...select_baseline_ctrl_c_with_filter_test.go | 64 +++++ .../cmdiotest/select_baseline_esc_key_test.go | 77 +++++ ...ect_baseline_filter_cursor_editing_test.go | 85 ++++++ .../select_baseline_filter_cursor_test.go | 72 +++++ .../select_baseline_filter_no_match_test.go | 73 +++++ .../select_baseline_filter_scroll_test.go | 80 ++++++ .../select_baseline_filter_typing_test.go | 75 +++++ .../select_baseline_long_descriptions_test.go | 72 +++++ .../cmdiotest/select_baseline_scroll_test.go | 77 +++++ .../select_baseline_single_item_test.go | 61 ++++ .../cmdiotest/select_baseline_tab_key_test.go | 75 +++++ libs/cmdio/cmdiotest/select_baseline_test.go | 68 +++++ .../select_baseline_vim_keys_test.go | 72 +++++ .../select_baseline_wrap_around_test.go | 70 +++++ .../cmdiotest/select_empty_items_test.go | 27 ++ libs/cmdio/cmdiotest/termtest/termtest.go | 267 ++++++++++++++++++ .../TestPromptBaseline_CtrlC/01-empty.golden | 1 + .../02-after-typing.golden | 1 + .../01-empty.golden | 1 + .../02-typed.golden | 1 + .../03-insert-at-start.golden | 1 + .../04-insert-at-end.golden | 1 + .../05-insert-mid.golden | 1 + .../06-after-delete.golden | 1 + .../07-after-ctrl-w.golden | 1 + .../08-after-ctrl-u.golden | 1 + .../01-default-shown.golden | 1 + .../02-after-one-char.golden | 1 + .../03-after-typing.golden | 1 + .../TestPromptBaseline_Mask/01-empty.golden | 1 + .../02-after-typing.golden | 1 + .../03-after-backspace.golden | 1 + .../TestPromptBaseline_Plain/01-empty.golden | 1 + .../02-after-typing.golden | 1 + .../03-after-edit.golden | 1 + .../TestPromptBaseline_UTF8/01-empty.golden | 1 + .../02-after-typing.golden | 1 + .../03-after-backspace.golden | 1 + .../04-restored.golden | 1 + .../01-empty.golden | 1 + .../02-invalid-typing.golden | 1 + .../03-cleared.golden | 1 + .../04-valid.golden | 1 + .../TestSecretBaseline_CtrlC/01-empty.golden | 1 + .../02-after-typing.golden | 1 + .../01-empty.golden | 1 + .../TestSecretBaseline_Typing/01-empty.golden | 1 + .../02-after-typing.golden | 1 + .../03-after-backspace.golden | 1 + .../01-initial.golden | 5 + .../01-no-results-with-filter.golden | 4 + .../01-initial.golden | 5 + .../02-after-down.golden | 5 + .../01-initial.golden | 5 + .../02-esc-from-initial.golden | 5 + .../03-after-typing-a.golden | 5 + .../04-esc-clears-filter-or-not.golden | 5 + .../01-initial.golden | 7 + .../02-on-gamma.golden | 7 + .../03-after-filter-a.golden | 6 + .../04-after-clear-filter.golden | 7 + .../01-initial.golden | 7 + .../02-after-typing-alp.golden | 3 + .../03-after-insert-mid.golden | 4 + .../04-after-insert-at-start.golden | 4 + .../05-after-insert-at-end.golden | 4 + .../06-after-ctrl-u.golden | 4 + .../07-after-ctrl-w.golden | 4 + .../01-initial.golden | 5 + .../02-after-x.golden | 4 + .../03-after-xy.golden | 4 + .../04-after-xyz.golden | 4 + .../05-after-backspaces.golden | 5 + .../01-initial.golden | 7 + .../02-filtered-top.golden | 7 + .../03-filtered-mid.golden | 7 + .../04-filtered-bottom.golden | 7 + .../01-initial.golden | 7 + .../02-after-a.golden | 6 + .../03-after-al.golden | 3 + .../04-after-alp.golden | 3 + .../01-initial.golden | 5 + .../02-second-active.golden | 5 + .../03-third-active.golden | 5 + .../01-initial.golden | 7 + .../02-bottom.golden | 7 + .../03-past-bottom.golden | 7 + .../01-initial.golden | 3 + .../02-after-down.golden | 3 + .../01-initial.golden | 5 + .../02-after-tab.golden | 4 + .../03-after-second-tab.golden | 4 + .../01-initial.golden | 6 + .../02-after-j.golden | 4 + .../03-after-jk.golden | 4 + .../04-after-backspaces.golden | 6 + .../01-initial.golden | 5 + .../02-up-from-top.golden | 5 + .../03-down-past-bottom.golden | 5 + 113 files changed, 2318 insertions(+) create mode 100644 libs/cmdio/cmdiotest/prompt_ctrl_c_baseline_test.go create mode 100644 libs/cmdio/cmdiotest/prompt_cursor_editing_baseline_test.go create mode 100644 libs/cmdio/cmdiotest/prompt_default_no_edit_baseline_test.go create mode 100644 libs/cmdio/cmdiotest/prompt_mask_baseline_test.go create mode 100644 libs/cmdio/cmdiotest/prompt_plain_baseline_test.go create mode 100644 libs/cmdio/cmdiotest/prompt_utf8_baseline_test.go create mode 100644 libs/cmdio/cmdiotest/prompt_validate_baseline_test.go create mode 100644 libs/cmdio/cmdiotest/secret_baseline_test.go create mode 100644 libs/cmdio/cmdiotest/secret_ctrl_c_baseline_test.go create mode 100644 libs/cmdio/cmdiotest/secret_empty_enter_baseline_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_ctrl_c_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_ctrl_c_with_filter_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_esc_key_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_filter_cursor_editing_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_filter_cursor_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_filter_no_match_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_filter_scroll_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_filter_typing_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_long_descriptions_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_scroll_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_single_item_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_tab_key_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_vim_keys_test.go create mode 100644 libs/cmdio/cmdiotest/select_baseline_wrap_around_test.go create mode 100644 libs/cmdio/cmdiotest/select_empty_items_test.go create mode 100644 libs/cmdio/cmdiotest/termtest/termtest.go create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_CtrlC/01-empty.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_CtrlC/02-after-typing.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_CursorEditing/01-empty.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_CursorEditing/02-typed.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_CursorEditing/03-insert-at-start.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_CursorEditing/04-insert-at-end.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_CursorEditing/05-insert-mid.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_CursorEditing/06-after-delete.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_CursorEditing/07-after-ctrl-w.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_CursorEditing/08-after-ctrl-u.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_DefaultNoEdit/01-default-shown.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_DefaultNoEdit/02-after-one-char.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_DefaultNoEdit/03-after-typing.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Mask/01-empty.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Mask/02-after-typing.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Mask/03-after-backspace.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Plain/01-empty.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Plain/02-after-typing.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Plain/03-after-edit.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_UTF8/01-empty.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_UTF8/02-after-typing.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_UTF8/03-after-backspace.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_UTF8/04-restored.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/02-invalid-typing.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/03-cleared.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/04-valid.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSecretBaseline_CtrlC/01-empty.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSecretBaseline_CtrlC/02-after-typing.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSecretBaseline_EmptyEnter/01-empty.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSecretBaseline_Typing/01-empty.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSecretBaseline_Typing/02-after-typing.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSecretBaseline_Typing/03-after-backspace.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_CtrlC/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_CtrlCWithFilter/01-no-results-with-filter.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_DownEnter/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_DownEnter/02-after-down.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_EscKey/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_EscKey/02-esc-from-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_EscKey/03-after-typing-a.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_EscKey/04-esc-clears-filter-or-not.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterCursor/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterCursor/02-on-gamma.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterCursor/03-after-filter-a.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterCursor/04-after-clear-filter.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterCursorEditing/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterCursorEditing/02-after-typing-alp.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterCursorEditing/03-after-insert-mid.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterCursorEditing/04-after-insert-at-start.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterCursorEditing/05-after-insert-at-end.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterCursorEditing/06-after-ctrl-u.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterCursorEditing/07-after-ctrl-w.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterNoMatch/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterNoMatch/02-after-x.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterNoMatch/03-after-xy.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterNoMatch/04-after-xyz.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterNoMatch/05-after-backspaces.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterScroll/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterScroll/02-filtered-top.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterScroll/03-filtered-mid.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterScroll/04-filtered-bottom.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterTyping/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterTyping/02-after-a.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterTyping/03-after-al.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_FilterTyping/04-after-alp.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_LongDescriptions/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_LongDescriptions/02-second-active.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_LongDescriptions/03-third-active.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_Scroll/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_Scroll/02-bottom.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_Scroll/03-past-bottom.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_SingleItem/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_SingleItem/02-after-down.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_TabKey/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_TabKey/02-after-tab.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_TabKey/03-after-second-tab.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_VimKeys/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_VimKeys/02-after-j.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_VimKeys/03-after-jk.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_VimKeys/04-after-backspaces.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_WrapAround/01-initial.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_WrapAround/02-up-from-top.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestSelectBaseline_WrapAround/03-down-past-bottom.golden 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_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_cursor_editing_baseline_test.go b/libs/cmdio/cmdiotest/prompt_cursor_editing_baseline_test.go new file mode 100644 index 0000000000..8ccce2adfb --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_cursor_editing_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/require" +) + +// TestPromptBaseline_CursorEditing pins how RunPrompt responds to cursor +// movement and line-editing keys: ←/→, Home/End, Delete, Ctrl+W. Promptui +// passes these through to readline; the hand-rolled bubbletea promptModel +// must reproduce the visible result for every snapshot to match. +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.KeyDelete) + tm.Golden("06-after-delete") + + 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_default_no_edit_baseline_test.go b/libs/cmdio/cmdiotest/prompt_default_no_edit_baseline_test.go new file mode 100644 index 0000000000..d872f31f29 --- /dev/null +++ b/libs/cmdio/cmdiotest/prompt_default_no_edit_baseline_test.go @@ -0,0 +1,62 @@ +package cmdiotest_test + +import ( + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/flags" + "github.com/stretchr/testify/require" +) + +// TestPromptBaseline_DefaultNoEdit pins Prompt's behavior when Default is +// set but AllowEdit is left at its zero value (false). The default renders +// as a hint rather than pre-filling the buffer, so the first keystroke +// replaces it instead of concatenating. +func TestPromptBaseline_DefaultNoEdit(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: "Region", + Default: "us-west-2", + }) + resCh <- result{value: v, err: err} + }() + + tm.WaitFor("Region") + tm.Golden("01-default-shown") + + tm.Type("e") + tm.Golden("02-after-one-char") + + tm.Type("u-central-1") + tm.Golden("03-after-typing") + + tm.Type(termtest.KeyEnter) + + res := <-resCh + require.NoError(t, res.err, "raw output: %q", tm.Raw()) + t.Logf("returned: %q", res.value) +} 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..ff7cfa2620 --- /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/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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..96d7501bc0 --- /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/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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..ce7059b4bc --- /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/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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..7224a7bd08 --- /dev/null +++ b/libs/cmdio/cmdiotest/secret_baseline_test.go @@ -0,0 +1,60 @@ +package cmdiotest_test + +import ( + "context" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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 := context.Background() + 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..9883274bda --- /dev/null +++ b/libs/cmdio/cmdiotest/secret_ctrl_c_baseline_test.go @@ -0,0 +1,58 @@ +package cmdiotest_test + +import ( + "context" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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 := context.Background() + 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..b6011c11b7 --- /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/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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_ctrl_c_test.go b/libs/cmdio/cmdiotest/select_baseline_ctrl_c_test.go new file mode 100644 index 0000000000..d63281cba0 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_c_test.go @@ -0,0 +1,59 @@ +package cmdiotest_test + +import ( + "context" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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 := context.Background() + 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..fa3284ddb0 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_ctrl_c_with_filter_test.go @@ -0,0 +1,64 @@ +package cmdiotest_test + +import ( + "context" + "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 := context.Background() + 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_esc_key_test.go b/libs/cmdio/cmdiotest/select_baseline_esc_key_test.go new file mode 100644 index 0000000000..49a92291e5 --- /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/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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..3388aeb522 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_cursor_editing_test.go @@ -0,0 +1,85 @@ +package cmdiotest_test + +import ( + "context" + "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 := context.Background() + 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..99e38727ff --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_cursor_test.go @@ -0,0 +1,72 @@ +package cmdiotest_test + +import ( + "context" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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 := context.Background() + 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..b2007b5e2b --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_no_match_test.go @@ -0,0 +1,73 @@ +package cmdiotest_test + +import ( + "context" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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 := context.Background() + 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..39787d493b --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_scroll_test.go @@ -0,0 +1,80 @@ +package cmdiotest_test + +import ( + "context" + "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 := context.Background() + 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..b195fdef1f --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_filter_typing_test.go @@ -0,0 +1,75 @@ +package cmdiotest_test + +import ( + "context" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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 := context.Background() + 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..2070f50795 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_long_descriptions_test.go @@ -0,0 +1,72 @@ +package cmdiotest_test + +import ( + "context" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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 := context.Background() + 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..68e0c8dc72 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_scroll_test.go @@ -0,0 +1,77 @@ +package cmdiotest_test + +import ( + "context" + "fmt" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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 := context.Background() + 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_single_item_test.go b/libs/cmdio/cmdiotest/select_baseline_single_item_test.go new file mode 100644 index 0000000000..5aa3cfa441 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_single_item_test.go @@ -0,0 +1,61 @@ +package cmdiotest_test + +import ( + "context" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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 := context.Background() + 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_tab_key_test.go b/libs/cmdio/cmdiotest/select_baseline_tab_key_test.go new file mode 100644 index 0000000000..89e2c480d0 --- /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/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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..66f315bc90 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_test.go @@ -0,0 +1,68 @@ +package cmdiotest_test + +import ( + "context" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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 := context.Background() + 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..b38f894680 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_vim_keys_test.go @@ -0,0 +1,72 @@ +package cmdiotest_test + +import ( + "context" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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 := context.Background() + 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_wrap_around_test.go b/libs/cmdio/cmdiotest/select_baseline_wrap_around_test.go new file mode 100644 index 0000000000..34c62473c0 --- /dev/null +++ b/libs/cmdio/cmdiotest/select_baseline_wrap_around_test.go @@ -0,0 +1,70 @@ +package cmdiotest_test + +import ( + "context" + "runtime" + "testing" + + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" + "github.com/databricks/cli/libs/cmdio" + "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 := context.Background() + 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