github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/surveyext/editor_test.go (about) 1 package surveyext 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "os" 8 "strings" 9 "sync" 10 "testing" 11 "time" 12 13 "github.com/AlecAivazis/survey/v2" 14 "github.com/AlecAivazis/survey/v2/terminal" 15 pseudotty "github.com/creack/pty" 16 "github.com/stretchr/testify/assert" 17 ) 18 19 func testLookPath(s string) ([]string, []string, error) { 20 return []string{os.Args[0], "-test.run=TestHelperProcess", "--", s}, []string{"GH_WANT_HELPER_PROCESS=1"}, nil 21 } 22 23 func TestHelperProcess(t *testing.T) { 24 if os.Getenv("GH_WANT_HELPER_PROCESS") != "1" { 25 return 26 } 27 if err := func(args []string) error { 28 switch args[0] { 29 // "vim" appends a message to the file 30 case "vim": 31 f, err := os.OpenFile(args[1], os.O_APPEND|os.O_WRONLY, 0) 32 if err != nil { 33 return err 34 } 35 defer f.Close() 36 _, err = f.WriteString(" - added by vim") 37 return err 38 // "nano" truncates the contents of the file 39 case "nano": 40 f, err := os.OpenFile(args[1], os.O_TRUNC|os.O_WRONLY, 0) 41 if err != nil { 42 return err 43 } 44 return f.Close() 45 default: 46 return fmt.Errorf("unrecognized arguments: %#v\n", args) 47 } 48 }(os.Args[3:]); err != nil { 49 fmt.Fprintln(os.Stderr, err) 50 os.Exit(1) 51 } 52 os.Exit(0) 53 } 54 55 func Test_GhEditor_Prompt_skip(t *testing.T) { 56 pty := newTerminal(t) 57 58 e := &GhEditor{ 59 BlankAllowed: true, 60 EditorCommand: "vim", 61 Editor: &survey.Editor{ 62 Message: "Body", 63 FileName: "*.md", 64 Default: "initial value", 65 HideDefault: true, 66 AppendDefault: true, 67 }, 68 lookPath: func(s string) ([]string, []string, error) { 69 return nil, nil, errors.New("no editor allowed") 70 }, 71 } 72 e.WithStdio(pty.Stdio()) 73 74 // wait until the prompt is rendered and send the Enter key 75 go func() { 76 pty.WaitForOutput("Body") 77 assert.Equal(t, "\x1b[0G\x1b[2K\x1b[0;1;92m? \x1b[0m\x1b[0;1;99mBody \x1b[0m\x1b[0;36m[(e) to launch vim, enter to skip] \x1b[0m", normalizeANSI(pty.Output())) 78 pty.ResetOutput() 79 assert.NoError(t, pty.SendKey('\n')) 80 }() 81 82 res, err := e.Prompt(defaultPromptConfig()) 83 assert.NoError(t, err) 84 assert.Equal(t, "initial value", res) 85 assert.Equal(t, "", normalizeANSI(pty.Output())) 86 } 87 88 func Test_GhEditor_Prompt_editorAppend(t *testing.T) { 89 pty := newTerminal(t) 90 91 e := &GhEditor{ 92 BlankAllowed: true, 93 EditorCommand: "vim", 94 Editor: &survey.Editor{ 95 Message: "Body", 96 FileName: "*.md", 97 Default: "initial value", 98 HideDefault: true, 99 AppendDefault: true, 100 }, 101 lookPath: testLookPath, 102 } 103 e.WithStdio(pty.Stdio()) 104 105 // wait until the prompt is rendered and send the 'e' key 106 go func() { 107 pty.WaitForOutput("Body") 108 assert.Equal(t, "\x1b[0G\x1b[2K\x1b[0;1;92m? \x1b[0m\x1b[0;1;99mBody \x1b[0m\x1b[0;36m[(e) to launch vim, enter to skip] \x1b[0m", normalizeANSI(pty.Output())) 109 pty.ResetOutput() 110 assert.NoError(t, pty.SendKey('e')) 111 }() 112 113 res, err := e.Prompt(defaultPromptConfig()) 114 assert.NoError(t, err) 115 assert.Equal(t, "initial value - added by vim", res) 116 assert.Equal(t, "", normalizeANSI(pty.Output())) 117 } 118 119 func Test_GhEditor_Prompt_editorTruncate(t *testing.T) { 120 pty := newTerminal(t) 121 122 e := &GhEditor{ 123 BlankAllowed: true, 124 EditorCommand: "nano", 125 Editor: &survey.Editor{ 126 Message: "Body", 127 FileName: "*.md", 128 Default: "initial value", 129 HideDefault: true, 130 AppendDefault: true, 131 }, 132 lookPath: testLookPath, 133 } 134 e.WithStdio(pty.Stdio()) 135 136 // wait until the prompt is rendered and send the 'e' key 137 go func() { 138 pty.WaitForOutput("Body") 139 assert.Equal(t, "\x1b[0G\x1b[2K\x1b[0;1;92m? \x1b[0m\x1b[0;1;99mBody \x1b[0m\x1b[0;36m[(e) to launch nano, enter to skip] \x1b[0m", normalizeANSI(pty.Output())) 140 pty.ResetOutput() 141 assert.NoError(t, pty.SendKey('e')) 142 }() 143 144 res, err := e.Prompt(defaultPromptConfig()) 145 assert.NoError(t, err) 146 assert.Equal(t, "", res) 147 assert.Equal(t, "", normalizeANSI(pty.Output())) 148 } 149 150 // survey doesn't expose this 151 func defaultPromptConfig() *survey.PromptConfig { 152 return &survey.PromptConfig{ 153 PageSize: 7, 154 HelpInput: "?", 155 SuggestInput: "tab", 156 Icons: survey.IconSet{ 157 Error: survey.Icon{ 158 Text: "X", 159 Format: "red", 160 }, 161 Help: survey.Icon{ 162 Text: "?", 163 Format: "cyan", 164 }, 165 Question: survey.Icon{ 166 Text: "?", 167 Format: "green+hb", 168 }, 169 MarkedOption: survey.Icon{ 170 Text: "[x]", 171 Format: "green", 172 }, 173 UnmarkedOption: survey.Icon{ 174 Text: "[ ]", 175 Format: "default+hb", 176 }, 177 SelectFocus: survey.Icon{ 178 Text: ">", 179 Format: "cyan+b", 180 }, 181 }, 182 Filter: func(filter string, value string, index int) (include bool) { 183 filter = strings.ToLower(filter) 184 return strings.Contains(strings.ToLower(value), filter) 185 }, 186 KeepFilter: false, 187 } 188 } 189 190 type testTerminal struct { 191 pty *os.File 192 tty *os.File 193 stdout *teeWriter 194 stderr *teeWriter 195 } 196 197 func newTerminal(t *testing.T) *testTerminal { 198 t.Helper() 199 200 pty, tty, err := pseudotty.Open() 201 if errors.Is(err, pseudotty.ErrUnsupported) { 202 t.SkipNow() 203 return nil 204 } 205 if err != nil { 206 t.Fatal(err) 207 } 208 t.Cleanup(func() { 209 pty.Close() 210 tty.Close() 211 }) 212 213 if err := pseudotty.Setsize(tty, &pseudotty.Winsize{Cols: 72, Rows: 30}); err != nil { 214 t.Fatal(err) 215 } 216 217 return &testTerminal{pty: pty, tty: tty} 218 } 219 220 func (t *testTerminal) SendKey(c rune) error { 221 _, err := t.pty.WriteString(string(c)) 222 return err 223 } 224 225 func (t *testTerminal) WaitForOutput(s string) { 226 for { 227 time.Sleep(time.Millisecond) 228 if strings.Contains(t.stdout.String(), s) { 229 return 230 } 231 } 232 } 233 234 func (t *testTerminal) Output() string { 235 return t.stdout.String() 236 } 237 238 func (t *testTerminal) ResetOutput() { 239 t.stdout.Reset() 240 } 241 242 func (t *testTerminal) Stdio() terminal.Stdio { 243 t.stdout = &teeWriter{File: t.tty} 244 t.stderr = t.stdout 245 return terminal.Stdio{ 246 In: t.tty, 247 Out: t.stdout, 248 Err: t.stderr, 249 } 250 } 251 252 // teeWriter is a writer that duplicates all writes to a file into a buffer 253 type teeWriter struct { 254 *os.File 255 buf bytes.Buffer 256 mu sync.Mutex 257 } 258 259 func (f *teeWriter) Write(p []byte) (n int, err error) { 260 f.mu.Lock() 261 defer f.mu.Unlock() 262 _, _ = f.buf.Write(p) 263 return f.File.Write(p) 264 } 265 266 func (f *teeWriter) String() string { 267 f.mu.Lock() 268 s := f.buf.String() 269 f.mu.Unlock() 270 return s 271 } 272 273 func (f *teeWriter) Reset() { 274 f.mu.Lock() 275 f.buf.Reset() 276 f.mu.Unlock() 277 } 278 279 // strips some ANSI escape sequences that we do not want tests to be concerned with 280 func normalizeANSI(t string) string { 281 t = strings.ReplaceAll(t, "\x1b[?25h", "") // strip sequence that shows cursor 282 t = strings.ReplaceAll(t, "\x1b[?25l", "") // strip sequence that hides cursor 283 return t 284 }