fzy
terminal fuzzy finder picker
git clone https://9o.is/git/fzy.git
acceptance_test.rb
(12849B)
1 # coding: utf-8
2 require 'minitest'
3 require 'minitest/autorun'
4 require 'ttytest'
5
6 class FzyTest < Minitest::Test
7 FZY_PATH = File.expand_path('../../../fzy', __FILE__)
8
9 LEFT = "\e[D"
10 RIGHT = "\e[C"
11
12 def test_empty_list
13 @tty = interactive_fzy(input: %w[], before: "placeholder")
14 @tty.assert_cursor_position(y: 1, x: 2)
15 @tty.assert_matches <<~TTY
16 placeholder
17 >
18 TTY
19
20 @tty.send_keys('t')
21 @tty.assert_cursor_position(y: 1, x: 3)
22 @tty.assert_matches <<~TTY
23 placeholder
24 > t
25 TTY
26
27 @tty.send_keys('z')
28 @tty.assert_cursor_position(y: 1, x: 4)
29 @tty.assert_matches <<~TTY
30 placeholder
31 > tz
32 TTY
33
34 @tty.send_keys("\r")
35 @tty.assert_cursor_position(y: 2, x: 0)
36 @tty.assert_matches <<~TTY
37 placeholder
38 tz
39 TTY
40 end
41
42 def test_one_item
43 @tty = interactive_fzy(input: %w[test], before: "placeholder")
44 @tty.assert_matches <<~TTY
45 placeholder
46 >
47 test
48 TTY
49 @tty.assert_cursor_position(y: 1, x: 2)
50
51 @tty.send_keys('t')
52 @tty.assert_cursor_position(y: 1, x: 3)
53 @tty.assert_matches <<~TTY
54 placeholder
55 > t
56 test
57 TTY
58
59 @tty.send_keys('z')
60 @tty.assert_cursor_position(y: 1, x: 4)
61 @tty.assert_matches <<~TTY
62 placeholder
63 > tz
64 TTY
65
66 @tty.send_keys("\r")
67 @tty.assert_cursor_position(y: 2, x: 0)
68 @tty.assert_matches <<~TTY
69 placeholder
70 tz
71 TTY
72 end
73
74 def test_two_items
75 @tty = interactive_fzy(input: %w[test foo], before: "placeholder")
76 @tty.assert_cursor_position(y: 1, x: 2)
77 @tty.assert_matches <<~TTY
78 placeholder
79 >
80 test
81 foo
82 TTY
83
84 @tty.send_keys('t')
85 @tty.assert_cursor_position(y: 1, x: 3)
86 @tty.assert_matches <<~TTY
87 placeholder
88 > t
89 test
90 TTY
91
92 @tty.send_keys('z')
93 @tty.assert_cursor_position(y: 1, x: 4)
94 @tty.assert_matches <<~TTY
95 placeholder
96 > tz
97 TTY
98
99 @tty.send_keys("\r")
100 @tty.assert_matches <<~TTY
101 placeholder
102 tz
103 TTY
104 @tty.assert_cursor_position(y: 2, x: 0)
105 end
106
107 def ctrl(key)
108 ((key.upcase.ord) - ('A'.ord) + 1).chr
109 end
110
111 def test_editing
112 @tty = interactive_fzy(input: %w[test foo], before: "placeholder")
113 @tty.assert_cursor_position(y: 1, x: 2)
114 @tty.assert_matches <<~TTY
115 placeholder
116 >
117 test
118 foo
119 TTY
120
121 @tty.send_keys("foo bar baz")
122 @tty.assert_cursor_position(y: 1, x: 13)
123 @tty.assert_matches <<~TTY
124 placeholder
125 > foo bar baz
126 TTY
127
128 @tty.send_keys(ctrl('H'))
129 @tty.assert_cursor_position(y: 1, x: 12)
130 @tty.assert_matches <<~TTY
131 placeholder
132 > foo bar ba
133 TTY
134
135 @tty.send_keys(ctrl('W'))
136 @tty.assert_cursor_position(y: 1, x: 10)
137 @tty.assert_matches <<~TTY
138 placeholder
139 > foo bar
140 TTY
141
142 @tty.send_keys(ctrl('U'))
143 @tty.assert_cursor_position(y: 1, x: 2)
144 @tty.assert_matches <<~TTY
145 placeholder
146 >
147 test
148 foo
149 TTY
150 end
151
152 def test_ctrl_d
153 @tty = interactive_fzy(input: %w[foo bar])
154 @tty.assert_matches ">\nfoo\nbar"
155
156 @tty.send_keys('foo')
157 @tty.assert_matches "> foo\nfoo"
158
159 @tty.send_keys(ctrl('D'))
160 @tty.assert_matches ''
161 @tty.assert_cursor_position(y: 0, x: 0)
162 end
163
164 def test_ctrl_c
165 @tty = interactive_fzy(input: %w[foo bar])
166 @tty.assert_matches ">\nfoo\nbar"
167
168 @tty.send_keys('foo')
169 @tty.assert_matches "> foo\nfoo"
170
171 @tty.send_keys(ctrl('C'))
172 @tty.assert_matches ''
173 @tty.assert_cursor_position(y: 0, x: 0)
174 end
175
176 def test_down_arrow
177 @tty = interactive_fzy(input: %w[foo bar])
178 @tty.assert_matches ">\nfoo\nbar"
179 @tty.send_keys("\e[A\r")
180 @tty.assert_matches "bar"
181
182 @tty = interactive_fzy(input: %w[foo bar])
183 @tty.assert_matches ">\nfoo\nbar"
184 @tty.send_keys("\eOA\r")
185 @tty.assert_matches "bar"
186 end
187
188 def test_up_arrow
189 @tty = interactive_fzy(input: %w[foo bar])
190 @tty.assert_matches ">\nfoo\nbar"
191 @tty.send_keys("\e[A") # first down
192 @tty.send_keys("\e[B\r") # and back up
193 @tty.assert_matches "foo"
194
195 @tty = interactive_fzy(input: %w[foo bar])
196 @tty.assert_matches ">\nfoo\nbar"
197 @tty.send_keys("\eOA") # first down
198 @tty.send_keys("\e[B\r") # and back up
199 @tty.assert_matches "foo"
200 end
201
202 def test_lines
203 input10 = (1..10).map(&:to_s)
204 input20 = (1..20).map(&:to_s)
205
206 @tty = interactive_fzy(input: input10)
207 @tty.assert_matches ">\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10"
208
209 @tty = interactive_fzy(input: input20)
210 @tty.assert_matches ">\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10"
211
212 @tty = interactive_fzy(input: input10, args: "-l 5")
213 @tty.assert_matches ">\n1\n2\n3\n4\n5"
214
215 @tty = interactive_fzy(input: input10, args: "--lines=5")
216 @tty.assert_matches ">\n1\n2\n3\n4\n5"
217 end
218
219 def test_prompt
220 @tty = interactive_fzy
221 @tty.send_keys("foo")
222 @tty.assert_matches '> foo'
223
224 @tty = interactive_fzy(args: "-p 'C:\\'")
225 @tty.send_keys("foo")
226 @tty.assert_matches 'C:\foo'
227
228 @tty = interactive_fzy(args: "--prompt=\"foo bar \"")
229 @tty.send_keys("baz")
230 @tty.assert_matches "foo bar baz"
231 end
232
233 def test_show_scores
234 expected_score = '( inf)'
235 @tty = interactive_fzy(input: %w[foo bar], args: "-s")
236 @tty.send_keys('foo')
237 @tty.assert_matches "> foo\n#{expected_score} foo"
238
239 @tty = interactive_fzy(input: %w[foo bar], args: "--show-scores")
240 @tty.send_keys('foo')
241 @tty.assert_matches "> foo\n#{expected_score} foo"
242
243 expected_score = '( 0.89)'
244 @tty = interactive_fzy(input: %w[foo bar], args: "-s")
245 @tty.send_keys('f')
246 @tty.assert_matches "> f\n#{expected_score} foo"
247 end
248
249 def test_large_input
250 @tty = TTYtest.new_terminal(%{seq 100000 | #{FZY_PATH} -l 3})
251 @tty.send_keys('34')
252 @tty.assert_matches "> 34\n34\n340\n341"
253
254 @tty.send_keys('5')
255 @tty.assert_matches "> 345\n345\n3450\n3451"
256
257 @tty.send_keys('z')
258 @tty.assert_matches "> 345z"
259 end
260
261 def test_worker_count
262 @tty = interactive_fzy(input: %w[foo bar], args: "-j1")
263 @tty.send_keys('foo')
264 @tty.assert_matches "> foo\nfoo"
265
266 @tty = TTYtest.new_terminal(%{seq 100000 | #{FZY_PATH} -j1 -l3})
267 @tty.send_keys('34')
268 @tty.assert_matches "> 34\n34\n340\n341"
269
270 @tty = TTYtest.new_terminal(%{seq 100000 | #{FZY_PATH} -j200 -l3})
271 @tty.send_keys('34')
272 @tty.assert_matches "> 34\n34\n340\n341"
273 end
274
275 def test_initial_query
276 @tty = interactive_fzy(input: %w[foo bar], args: "-q fo")
277 @tty.assert_matches "> fo\nfoo"
278 @tty.send_keys("o")
279 @tty.assert_matches "> foo\nfoo"
280 @tty.send_keys("o")
281 @tty.assert_matches "> fooo"
282
283 @tty = interactive_fzy(input: %w[foo bar], args: "-q asdf")
284 @tty.assert_matches "> asdf"
285 end
286
287 def test_non_interactive
288 @tty = interactive_fzy(input: %w[foo bar], args: "-e foo", before: "before", after: "after")
289 @tty.assert_matches "before\nfoo\nafter"
290 end
291
292 def test_moving_text_cursor
293 @tty = interactive_fzy(input: %w[foo bar])
294 @tty.send_keys("br")
295 @tty.assert_matches "> br\nbar"
296 @tty.assert_cursor_position(y: 0, x: 4)
297
298 @tty.send_keys(LEFT)
299 @tty.assert_cursor_position(y: 0, x: 3)
300 @tty.assert_matches "> br\nbar"
301 @tty.send_keys("a")
302 @tty.assert_cursor_position(y: 0, x: 4)
303 @tty.assert_matches "> bar\nbar"
304
305 @tty.send_keys(ctrl("A")) # Ctrl-A
306 @tty.assert_cursor_position(y: 0, x: 2)
307 @tty.assert_matches "> bar\nbar"
308 @tty.send_keys("foo")
309 @tty.assert_cursor_position(y: 0, x: 5)
310 @tty.assert_matches "> foobar"
311
312 @tty.send_keys(ctrl("E")) # Ctrl-E
313 @tty.assert_cursor_position(y: 0, x: 8)
314 @tty.assert_matches "> foobar"
315 @tty.send_keys("baz") # Ctrl-E
316 @tty.assert_cursor_position(y: 0, x: 11)
317 @tty.assert_matches "> foobarbaz"
318 end
319
320 # More info;
321 # https://github.com/jhawthorn/fzy/issues/42
322 # https://cirw.in/blog/bracketed-paste
323 def test_bracketed_paste_characters
324 @tty = interactive_fzy(input: %w[foo bar])
325 @tty.assert_matches ">\nfoo\nbar"
326 @tty.send_keys("\e[200~foo\e[201~")
327 @tty.assert_matches "> foo\nfoo"
328 end
329
330 # https://github.com/jhawthorn/fzy/issues/81
331 def test_slow_stdin_fast_user
332 @tty = TTYtest.new_terminal(%{(sleep 0.5; echo aa; echo bc; echo bd) | #{FZY_PATH}})
333
334 # Before input has all come in, but wait for fzy to at least start
335 sleep 0.1
336
337 @tty.send_keys("b\r")
338 @tty.assert_matches "bc"
339 end
340
341 def test_unicode
342 @tty = interactive_fzy(input: %w[English Français 日本語])
343 @tty.assert_matches <<~TTY
344 >
345 English
346 Français
347 日本語
348 TTY
349 @tty.assert_cursor_position(y: 0, x: 2)
350
351 @tty.send_keys("ç")
352 @tty.assert_matches <<~TTY
353 > ç
354 Français
355 TTY
356 @tty.assert_cursor_position(y: 0, x: 3)
357
358 @tty.send_keys("\r")
359 @tty.assert_matches "Français"
360 end
361
362 def test_unicode_backspace
363 @tty = interactive_fzy
364 @tty.send_keys "Français"
365 @tty.assert_matches "> Français"
366 @tty.assert_cursor_position(y: 0, x: 10)
367
368 @tty.send_keys(ctrl('H') * 3)
369 @tty.assert_matches "> Franç"
370 @tty.assert_cursor_position(y: 0, x: 7)
371
372 @tty.send_keys(ctrl('H'))
373 @tty.assert_matches "> Fran"
374 @tty.assert_cursor_position(y: 0, x: 6)
375
376 @tty.send_keys('ce')
377 @tty.assert_matches "> France"
378
379 @tty = interactive_fzy
380 @tty.send_keys "日本語"
381 @tty.assert_matches "> 日本語"
382 @tty.send_keys(ctrl('H'))
383 @tty.assert_matches "> 日本"
384 @tty.send_keys(ctrl('H'))
385 @tty.assert_matches "> 日"
386 @tty.send_keys(ctrl('H'))
387 @tty.assert_matches "> "
388 @tty.assert_cursor_position(y: 0, x: 2)
389 end
390
391 def test_unicode_delete_word
392 @tty = interactive_fzy
393 @tty.send_keys "Je parle Français"
394 @tty.assert_matches "> Je parle Français"
395 @tty.assert_cursor_position(y: 0, x: 19)
396
397 @tty.send_keys(ctrl('W'))
398 @tty.assert_matches "> Je parle"
399 @tty.assert_cursor_position(y: 0, x: 11)
400
401 @tty = interactive_fzy
402 @tty.send_keys "日本語"
403 @tty.assert_matches "> 日本語"
404 @tty.send_keys(ctrl('W'))
405 @tty.assert_matches "> "
406 @tty.assert_cursor_position(y: 0, x: 2)
407 end
408
409 def test_unicode_cursor_movement
410 @tty = interactive_fzy
411 @tty.send_keys "Français"
412 @tty.assert_cursor_position(y: 0, x: 10)
413
414 @tty.send_keys(LEFT*5)
415 @tty.assert_cursor_position(y: 0, x: 5)
416
417 @tty.send_keys(RIGHT*3)
418 @tty.assert_cursor_position(y: 0, x: 8)
419
420 @tty = interactive_fzy
421 @tty.send_keys "日本語"
422 @tty.assert_matches "> 日本語"
423 @tty.assert_cursor_position(y: 0, x: 8)
424 @tty.send_keys(LEFT)
425 @tty.assert_cursor_position(y: 0, x: 6)
426 @tty.send_keys(LEFT)
427 @tty.assert_cursor_position(y: 0, x: 4)
428 @tty.send_keys(LEFT)
429 @tty.assert_cursor_position(y: 0, x: 2)
430 @tty.send_keys(LEFT)
431 @tty.assert_cursor_position(y: 0, x: 2)
432 @tty.send_keys(RIGHT*3)
433 @tty.assert_cursor_position(y: 0, x: 8)
434 @tty.send_keys(RIGHT)
435 @tty.assert_cursor_position(y: 0, x: 8)
436 end
437
438 def test_long_strings
439 ascii = "LongStringOfText" * 6
440 unicode = "LongStringOfText" * 3
441
442 @tty = interactive_fzy(input: [ascii, unicode])
443 @tty.assert_matches <<~TTY
444 >
445 LongStringOfTextLongStringOfTextLongStringOfTextLongStringOfTextLongStringOfText
446 LongStringOfTextLongStringOfTextLongStri
447 TTY
448 end
449
450 def test_show_info
451 @tty = interactive_fzy(input: %w[foo bar baz], args: "-i")
452 @tty.assert_matches ">\n[3/3]\nfoo\nbar\nbaz"
453 @tty.send_keys("ba")
454 @tty.assert_matches "> ba\n[2/3]\nbar\nbaz"
455 @tty.send_keys("q")
456 @tty.assert_matches "> baq\n[0/3]"
457 end
458
459 def test_help
460 @tty = TTYtest.new_terminal(%{#{FZY_PATH} --help})
461 @tty.assert_matches <<TTY
462 Usage: fzy [OPTION]...
463 -l, --lines=LINES Specify how many lines of results to show (default 10)
464 -p, --prompt=PROMPT Input prompt (default '> ')
465 -q, --query=QUERY Use QUERY as the initial search string
466 -e, --show-matches=QUERY Output the sorted matches of QUERY
467 -t, --tty=TTY Specify file to use as TTY device (default /dev/tty)
468 -s, --show-scores Show the scores of each match
469 -0, --read-null Read input delimited by ASCII NUL characters
470 -j, --workers NUM Use NUM workers for searching. (default is # of CPUs)
471 -i, --show-info Show selection info line
472 -h, --help Display this help and exit
473 -v, --version Output version information and exit
474 TTY
475 end
476
477 private
478
479 def interactive_fzy(input: [], before: nil, after: nil, args: "")
480 cmd = []
481 cmd << %{echo "#{before}"} if before
482 cmd << %{printf "#{input.join("\\n")}" | #{FZY_PATH} #{args}}
483 cmd << %{echo "#{after}"} if after
484 cmd = cmd.join("; ")
485 TTYtest.new_terminal(cmd)
486 end
487 end