tty-prompt 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -1
  3. data/CHANGELOG.md +30 -0
  4. data/README.md +39 -9
  5. data/examples/echo.rb +5 -1
  6. data/examples/inputs.rb +10 -0
  7. data/examples/mask.rb +6 -2
  8. data/examples/multi_select.rb +1 -1
  9. data/examples/multi_select_paged.rb +9 -0
  10. data/examples/select.rb +5 -5
  11. data/examples/slider.rb +1 -1
  12. data/lib/tty-prompt.rb +2 -36
  13. data/lib/tty/prompt.rb +49 -8
  14. data/lib/tty/prompt/choices.rb +2 -0
  15. data/lib/tty/prompt/confirm_question.rb +6 -1
  16. data/lib/tty/prompt/converter_dsl.rb +9 -6
  17. data/lib/tty/prompt/converter_registry.rb +27 -19
  18. data/lib/tty/prompt/converters.rb +16 -22
  19. data/lib/tty/prompt/enum_list.rb +8 -4
  20. data/lib/tty/prompt/enum_paginator.rb +2 -0
  21. data/lib/tty/prompt/evaluator.rb +1 -1
  22. data/lib/tty/prompt/expander.rb +1 -1
  23. data/lib/tty/prompt/list.rb +21 -11
  24. data/lib/tty/prompt/mask_question.rb +15 -6
  25. data/lib/tty/prompt/multi_list.rb +12 -10
  26. data/lib/tty/prompt/question.rb +38 -36
  27. data/lib/tty/prompt/question/modifier.rb +2 -0
  28. data/lib/tty/prompt/question/validation.rb +5 -4
  29. data/lib/tty/prompt/reader.rb +104 -58
  30. data/lib/tty/prompt/reader/codes.rb +103 -63
  31. data/lib/tty/prompt/reader/console.rb +57 -0
  32. data/lib/tty/prompt/reader/key_event.rb +51 -88
  33. data/lib/tty/prompt/reader/mode.rb +5 -5
  34. data/lib/tty/prompt/reader/win_api.rb +29 -0
  35. data/lib/tty/prompt/reader/win_console.rb +49 -0
  36. data/lib/tty/prompt/slider.rb +10 -6
  37. data/lib/tty/prompt/suggestion.rb +1 -1
  38. data/lib/tty/prompt/symbols.rb +52 -10
  39. data/lib/tty/prompt/version.rb +1 -1
  40. data/lib/tty/{prompt/test.rb → test_prompt.rb} +2 -1
  41. data/spec/unit/ask_spec.rb +8 -16
  42. data/spec/unit/converters/convert_bool_spec.rb +1 -2
  43. data/spec/unit/converters/on_error_spec.rb +9 -0
  44. data/spec/unit/enum_paginator_spec.rb +16 -0
  45. data/spec/unit/enum_select_spec.rb +69 -25
  46. data/spec/unit/expand_spec.rb +14 -14
  47. data/spec/unit/mask_spec.rb +66 -29
  48. data/spec/unit/multi_select_spec.rb +120 -74
  49. data/spec/unit/new_spec.rb +5 -3
  50. data/spec/unit/paginator_spec.rb +16 -0
  51. data/spec/unit/question/default_spec.rb +2 -4
  52. data/spec/unit/question/echo_spec.rb +2 -3
  53. data/spec/unit/question/in_spec.rb +9 -14
  54. data/spec/unit/question/modifier/letter_case_spec.rb +32 -11
  55. data/spec/unit/question/modifier/whitespace_spec.rb +41 -15
  56. data/spec/unit/question/required_spec.rb +9 -13
  57. data/spec/unit/question/validate_spec.rb +7 -10
  58. data/spec/unit/reader/key_event_spec.rb +36 -50
  59. data/spec/unit/reader/publish_keypress_event_spec.rb +5 -3
  60. data/spec/unit/reader/read_keypress_spec.rb +8 -7
  61. data/spec/unit/reader/read_line_spec.rb +9 -9
  62. data/spec/unit/reader/read_multiline_spec.rb +8 -7
  63. data/spec/unit/select_spec.rb +85 -25
  64. data/spec/unit/slider_spec.rb +43 -16
  65. data/spec/unit/yes_no_spec.rb +14 -28
  66. data/tasks/console.rake +1 -0
  67. data/tty-prompt.gemspec +2 -2
  68. metadata +14 -7
@@ -12,8 +12,7 @@ RSpec.describe TTY::Prompt::Question, '#default' do
12
12
  expect(answer).to eq(name)
13
13
  expect(prompt.output.string).to eq([
14
14
  "What is your name? \e[90m(Anonymous)\e[0m ",
15
- "\e[1000D\e[K\e[1A",
16
- "\e[1000D\e[K",
15
+ "\e[1A\e[2K\e[1G",
17
16
  "What is your name? \e[32mAnonymous\e[0m\n"
18
17
  ].join)
19
18
  end
@@ -24,8 +23,7 @@ RSpec.describe TTY::Prompt::Question, '#default' do
24
23
  expect(answer).to eq(name)
25
24
  expect(prompt.output.string).to eq([
26
25
  "What is your name? \e[90m(Anonymous)\e[0m ",
27
- "\e[1000D\e[K\e[1A",
28
- "\e[1000D\e[K",
26
+ "\e[1A\e[2K\e[1G",
29
27
  "What is your name? \e[32mAnonymous\e[0m\n"
30
28
  ].join)
31
29
  end
@@ -11,8 +11,7 @@ RSpec.describe TTY::Prompt::Question, '#echo' do
11
11
  expect(answer).to eql("password")
12
12
  expect(prompt.output.string).to eq([
13
13
  "What is your password? ",
14
- "\e[1000D\e[K\e[1A",
15
- "\e[1000D\e[K",
14
+ "\e[1A\e[2K\e[1G",
16
15
  "What is your password? \e[32mpassword\e[0m\n"
17
16
  ].join)
18
17
  end
@@ -24,7 +23,7 @@ RSpec.describe TTY::Prompt::Question, '#echo' do
24
23
  expect(answer).to eql("password")
25
24
  expect(prompt.output.string).to eq([
26
25
  "What is your password? ",
27
- "\e[1000D\e[K",
26
+ "\e[2K\e[1G",
28
27
  "What is your password? \n"
29
28
  ].join)
30
29
  end
@@ -24,8 +24,7 @@ RSpec.describe TTY::Prompt::Question, '#in' do
24
24
  expect(answer).to eq('8')
25
25
  expect(prompt.output.string).to eq([
26
26
  "How do you like it on scale 1-10? ",
27
- "\e[1000D\e[K\e[1A",
28
- "\e[1000D\e[K",
27
+ "\e[1A\e[2K\e[1G",
29
28
  "How do you like it on scale 1-10? \e[32m8\e[0m\n",
30
29
  ].join)
31
30
  end
@@ -41,8 +40,7 @@ RSpec.describe TTY::Prompt::Question, '#in' do
41
40
  expect(answer).to eq('8.1')
42
41
  expect(prompt.output.string).to eq([
43
42
  "How do you like it on scale 1-10? ",
44
- "\e[1000D\e[K\e[1A",
45
- "\e[1000D\e[K",
43
+ "\e[1A\e[2K\e[1G",
46
44
  "How do you like it on scale 1-10? \e[32m8.1\e[0m\n",
47
45
  ].join)
48
46
  end
@@ -58,8 +56,7 @@ RSpec.describe TTY::Prompt::Question, '#in' do
58
56
  expect(answer).to eq('E')
59
57
  expect(prompt.output.string).to eq([
60
58
  "Your favourite vitamin? (A-K) ",
61
- "\e[1000D\e[K\e[1A",
62
- "\e[1000D\e[K",
59
+ "\e[1A\e[2K\e[1G",
63
60
  "Your favourite vitamin? (A-K) \e[32mE\e[0m\n"
64
61
  ].join)
65
62
  end
@@ -73,12 +70,11 @@ RSpec.describe TTY::Prompt::Question, '#in' do
73
70
  expect(answer).to eq('2')
74
71
  expect(prompt.output.string).to eq([
75
72
  "How spicy on scale? (1-5) ",
76
- "\e[1000D\e[K",
77
73
  "\e[31m>>\e[0m Value A must be within the range 1..5\e[1A",
78
- "\e[1000D\e[K",
74
+ "\e[2K\e[1G",
79
75
  "How spicy on scale? (1-5) ",
80
- "\e[1000D\e[K\e[1A",
81
- "\e[1000D\e[K",
76
+ "\e[2K\e[1G",
77
+ "\e[1A\e[2K\e[1G",
82
78
  "How spicy on scale? (1-5) \e[32m2\e[0m\n"
83
79
  ].join)
84
80
  end
@@ -95,12 +91,11 @@ RSpec.describe TTY::Prompt::Question, '#in' do
95
91
  expect(answer).to eq('2')
96
92
  expect(prompt.output.string).to eq([
97
93
  "How spicy on scale? (1-5) ",
98
- "\e[1000D\e[K",
99
94
  "\e[31m>>\e[0m Ohh dear what is this A doing in 1..5?\e[1A",
100
- "\e[1000D\e[K",
95
+ "\e[2K\e[1G",
101
96
  "How spicy on scale? (1-5) ",
102
- "\e[1000D\e[K\e[1A",
103
- "\e[1000D\e[K",
97
+ "\e[2K\e[1G",
98
+ "\e[1A\e[2K\e[1G",
104
99
  "How spicy on scale? (1-5) \e[32m2\e[0m\n"
105
100
  ].join)
106
101
  end
@@ -1,20 +1,41 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  RSpec.describe TTY::Prompt::Question::Modifier, '#letter_case' do
4
- let(:string) { 'text to modify' }
4
+ context "string" do
5
+ let(:string) { 'text to modify' }
5
6
 
6
- it "changes to uppercase" do
7
- modified = described_class.letter_case(:up, string)
8
- expect(modified).to eq('TEXT TO MODIFY')
9
- end
7
+ it "changes to uppercase" do
8
+ modified = described_class.letter_case(:up, string)
9
+ expect(modified).to eq('TEXT TO MODIFY')
10
+ end
11
+
12
+ it "changes to lower case" do
13
+ modified = described_class.letter_case(:down, string)
14
+ expect(modified).to eq('text to modify')
15
+ end
10
16
 
11
- it "changes to lower case" do
12
- modified = described_class.letter_case(:down, string)
13
- expect(modified).to eq('text to modify')
17
+ it "capitalizes text" do
18
+ modified = described_class.letter_case(:capitalize, string)
19
+ expect(modified).to eq('Text to modify')
20
+ end
14
21
  end
15
22
 
16
- it "capitalizes text" do
17
- modified = described_class.letter_case(:capitalize, string)
18
- expect(modified).to eq('Text to modify')
23
+ context "nil (empty user input)" do
24
+ let(:string) { nil }
25
+
26
+ example "up returns nil" do
27
+ modified = described_class.letter_case(:up, string)
28
+ expect(modified).to be_nil
29
+ end
30
+
31
+ example "down returns nil" do
32
+ modified = described_class.letter_case(:down, string)
33
+ expect(modified).to be_nil
34
+ end
35
+
36
+ example "capitalize returns nil" do
37
+ modified = described_class.letter_case(:capitalize, string)
38
+ expect(modified).to be_nil
39
+ end
19
40
  end
20
41
  end
@@ -1,25 +1,51 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  RSpec.describe TTY::Prompt::Question::Modifier, '#whitespace' do
4
- let(:string) { " text\t \n to\t modify\r\n" }
4
+ context "string with whitespaces" do
5
+ let(:string) { " text\t \n to\t modify\r\n" }
5
6
 
6
- it "trims whitespace" do
7
- modified = described_class.whitespace(:trim, string)
8
- expect(modified).to eq("text\t \n to\t modify")
9
- end
7
+ it "trims whitespace" do
8
+ modified = described_class.whitespace(:trim, string)
9
+ expect(modified).to eq("text\t \n to\t modify")
10
+ end
10
11
 
11
- it "chomps whitespace" do
12
- modified = described_class.whitespace(:chomp, string)
13
- expect(modified).to eq(" text\t \n to\t modify")
14
- end
12
+ it "chomps whitespace" do
13
+ modified = described_class.whitespace(:chomp, string)
14
+ expect(modified).to eq(" text\t \n to\t modify")
15
+ end
15
16
 
16
- it "collapses text" do
17
- modified = described_class.whitespace(:collapse, string)
18
- expect(modified).to eq(" text to modify ")
17
+ it "collapses text" do
18
+ modified = described_class.whitespace(:collapse, string)
19
+ expect(modified).to eq(" text to modify ")
20
+ end
21
+
22
+ it "removes whitespace" do
23
+ modified = described_class.whitespace(:remove, string)
24
+ expect(modified).to eq("texttomodify")
25
+ end
19
26
  end
20
27
 
21
- it "removes whitespace" do
22
- modified = described_class.whitespace(:remove, string)
23
- expect(modified).to eq("texttomodify")
28
+ context "nil (empty user input)" do
29
+ let(:string) { nil }
30
+
31
+ example "trim returns nil" do
32
+ modified = described_class.whitespace(:trim, string)
33
+ expect(modified).to be_nil
34
+ end
35
+
36
+ example "chomp returns nil" do
37
+ modified = described_class.whitespace(:chomp, string)
38
+ expect(modified).to be_nil
39
+ end
40
+
41
+ example "collapse returns nil" do
42
+ modified = described_class.whitespace(:collapse, string)
43
+ expect(modified).to be_nil
44
+ end
45
+
46
+ example "remove returns nil" do
47
+ modified = described_class.whitespace(:remove, string)
48
+ expect(modified).to be_nil
49
+ end
24
50
  end
25
51
  end
@@ -10,8 +10,7 @@ RSpec.describe TTY::Prompt::Question, '#required' do
10
10
  prompt.ask('What is your name?') { |q| q.required(true) }
11
11
  expect(prompt.output.string).to eq([
12
12
  "What is your name? ",
13
- "\e[1000D\e[K\e[1A",
14
- "\e[1000D\e[K",
13
+ "\e[1A\e[2K\e[1G",
15
14
  "What is your name? \e[32mPiotr\e[0m\n"
16
15
  ].join)
17
16
  end
@@ -22,12 +21,11 @@ RSpec.describe TTY::Prompt::Question, '#required' do
22
21
  prompt.ask('What is your name?', required: true)
23
22
  expect(prompt.output.string).to eq([
24
23
  "What is your name? ",
25
- "\e[1000D\e[K",
26
24
  "\e[31m>>\e[0m Value must be provided\e[1A",
27
- "\e[1000D\e[K",
25
+ "\e[2K\e[1G",
28
26
  "What is your name? ",
29
- "\e[1000D\e[K\e[1A",
30
- "\e[1000D\e[K",
27
+ "\e[2K\e[1G",
28
+ "\e[1A\e[2K\e[1G",
31
29
  "What is your name? \e[32mPiotr\e[0m\n"
32
30
  ].join)
33
31
  end
@@ -50,16 +48,14 @@ RSpec.describe TTY::Prompt::Question, '#required' do
50
48
  end
51
49
  expect(prompt.output.string).to eq([
52
50
  "File name? ",
53
- "\e[1000D\e[K",
54
- "\e[31m>>\e[0m File name must not be empty!",
55
- "\e[1A\e[1000D\e[K",
51
+ "\e[31m>>\e[0m File name must not be empty!",
52
+ "\e[1A\e[2K\e[1G",
56
53
  "File name? ",
57
- "\e[1000D\e[K",
58
54
  "\e[31m>>\e[0m File already exists!",
59
- "\e[1A\e[1000D\e[K",
55
+ "\e[1A\e[2K\e[1G",
60
56
  "File name? ",
61
- "\e[1000D\e[K",
62
- "\e[1A\e[1000D\e[K",
57
+ "\e[2K\e[1G",
58
+ "\e[1A\e[2K\e[1G",
63
59
  "File name? \e[32mtest\e[0m\n",
64
60
  ].join)
65
61
  expect(answer).to eq('test')
@@ -15,8 +15,7 @@ RSpec.describe TTY::Prompt::Question, '#validate' do
15
15
  expect(answer).to eq('piotr.murach')
16
16
  expect(prompt.output.string).to eq([
17
17
  "What is your username? ",
18
- "\e[1000D\e[K\e[1A",
19
- "\e[1000D\e[K",
18
+ "\e[1A\e[2K\e[1G",
20
19
  "What is your username? \e[32mpiotr.murach\e[0m\n"
21
20
  ].join)
22
21
  end
@@ -54,12 +53,11 @@ RSpec.describe TTY::Prompt::Question, '#validate' do
54
53
  expect(answer).to eq('piotr@example.com')
55
54
  expect(prompt.output.string).to eq([
56
55
  "What is your email? ",
57
- "\e[1000D\e[K",
58
56
  "\e[31m>>\e[0m Your answer is invalid (must match :email)\e[1A",
59
- "\e[1000D\e[K",
57
+ "\e[2K\e[1G",
60
58
  "What is your email? ",
61
- "\e[1000D\e[K\e[1A",
62
- "\e[1000D\e[K",
59
+ "\e[2K\e[1G",
60
+ "\e[1A\e[2K\e[1G",
63
61
  "What is your email? \e[32mpiotr@example.com\e[0m\n"
64
62
  ].join)
65
63
  end
@@ -76,12 +74,11 @@ RSpec.describe TTY::Prompt::Question, '#validate' do
76
74
  expect(answer).to eq('piotr@example.com')
77
75
  expect(prompt.output.string).to eq([
78
76
  "What is your email? ",
79
- "\e[1000D\e[K",
80
77
  "\e[31m>>\e[0m Not an email!\e[1A",
81
- "\e[1000D\e[K",
78
+ "\e[2K\e[1G",
82
79
  "What is your email? ",
83
- "\e[1000D\e[K\e[1A",
84
- "\e[1000D\e[K",
80
+ "\e[2K\e[1G",
81
+ "\e[1A\e[2K\e[1G",
85
82
  "What is your email? \e[32mpiotr@example.com\e[0m\n"
86
83
  ].join)
87
84
  end
@@ -1,51 +1,56 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'shellwords'
4
+ require 'tty/prompt/reader/codes'
4
5
 
5
6
  RSpec.describe TTY::Prompt::Reader::KeyEvent, '#from' do
6
- it "parses ctrl+h" do
7
- event = described_class.from("\b")
8
- expect(event.key.name).to eq(:backspace)
9
- expect(event.value).to eq("\b")
10
- end
7
+ let(:keys) { TTY::Prompt::Reader::Codes.keys }
11
8
 
12
9
  it "parses backspace" do
13
- event = described_class.from("\e\x7f")
10
+ event = described_class.from(keys, "\x7f")
14
11
  expect(event.key.name).to eq(:backspace)
15
- expect(event.key.meta).to eq(true)
16
- expect(event.value).to eq("\e\x7f")
12
+ expect(event.value).to eq("\x7f")
17
13
  end
18
14
 
19
15
  it "parses lowercase char" do
20
- event = described_class.from('a')
21
- expect(event.key.name).to eq('a')
16
+ event = described_class.from(keys, 'a')
17
+ expect(event.key.name).to eq(:alpha)
22
18
  expect(event.value).to eq('a')
23
19
  end
24
20
 
25
21
  it "parses uppercase char" do
26
- event = described_class.from('A')
27
- expect(event.key.name).to eq('a')
22
+ event = described_class.from(keys, 'A')
23
+ expect(event.key.name).to eq(:alpha)
28
24
  expect(event.value).to eq('A')
29
25
  end
30
26
 
27
+ it "parses ctrl-a to ctrl-z inputs" do
28
+ (1..26).zip('a'..'z').each do |code, char|
29
+ next if ['i', 'j', 'm'].include?(char)
30
+ event = described_class.from(keys, code.chr)
31
+ expect(event.key.name).to eq(:"ctrl_#{char}")
32
+ expect(event.value).to eq(code.chr)
33
+ end
34
+ end
35
+
31
36
  # F1-F12 keys
32
37
  {
33
- f1: ["\eOP", "\e[11~", "\e[[A"],
34
- f2: ["\eOQ", "\e[12~", "\e[[B"],
35
- f3: ["\eOR", "\e[13~", "\e[[C"],
36
- f4: ["\eOS", "\e[14~", "\e[[D"],
37
- f5: [ "\e[15~", "\e[[E"],
38
- f6: [ "\e[17~" ],
39
- f7: [ "\e[18~" ],
40
- f8: [ "\e[19~" ],
41
- f9: [ "\e[20~" ],
42
- f10: [ "\e[21~" ],
43
- f11: [ "\e[23~" ],
44
- f12: [ "\e[24~" ]
38
+ f1: ["\eOP", "\e[11~"],
39
+ f2: ["\eOQ", "\e[12~"],
40
+ f3: ["\eOR", "\e[13~"],
41
+ f4: ["\eOS", "\e[14~"],
42
+ f5: [ "\e[15~"],
43
+ f6: [ "\e[17~"],
44
+ f7: [ "\e[18~"],
45
+ f8: [ "\e[19~"],
46
+ f9: [ "\e[20~"],
47
+ f10: [ "\e[21~"],
48
+ f11: [ "\e[23~"],
49
+ f12: [ "\e[24~"]
45
50
  }.each do |name, codes|
46
51
  codes.each do |code|
47
52
  it "parses #{Shellwords.escape(code)} as #{name} key" do
48
- event = described_class.from(code)
53
+ event = described_class.from(keys, code)
49
54
  expect(event.key.name).to eq(name)
50
55
  expect(event.key.meta).to eq(false)
51
56
  expect(event.key.ctrl).to eq(false)
@@ -55,19 +60,18 @@ RSpec.describe TTY::Prompt::Reader::KeyEvent, '#from' do
55
60
  end
56
61
 
57
62
  # arrow keys & page navigation
58
- #
59
63
  {
60
- up: ["\e[A", "\eOA"],
61
- down: ["\e[B", "\eOB"],
62
- right: ["\e[C", "\eOC"],
63
- left: ["\e[D", "\eOD"],
64
- clear: ["\e[E", "\eOE"],
64
+ up: ["\e[A"],
65
+ down: ["\e[B"],
66
+ right: ["\e[C"],
67
+ left: ["\e[D"],
68
+ clear: ["\e[E"],
65
69
  end: ["\e[F"],
66
70
  home: ["\e[H"]
67
71
  }.each do |name, codes|
68
72
  codes.each do |code|
69
73
  it "parses #{Shellwords.escape(code)} as #{name} key" do
70
- event = described_class.from(code)
74
+ event = described_class.from(keys, code)
71
75
  expect(event.key.name).to eq(name)
72
76
  expect(event.key.meta).to eq(false)
73
77
  expect(event.key.ctrl).to eq(false)
@@ -75,22 +79,4 @@ RSpec.describe TTY::Prompt::Reader::KeyEvent, '#from' do
75
79
  end
76
80
  end
77
81
  end
78
-
79
- {
80
- up: ["\e[a"],
81
- down: ["\e[b"],
82
- right: ["\e[c"],
83
- left: ["\e[d"],
84
- clear: ["\e[e"],
85
- }.each do |name, codes|
86
- codes.each do |code|
87
- it "parses #{Shellwords.escape(code)} as SHIFT + #{name} key" do
88
- event = described_class.from(code)
89
- expect(event.key.name).to eq(name)
90
- expect(event.key.meta).to eq(false)
91
- expect(event.key.ctrl).to eq(false)
92
- expect(event.key.shift).to eq(true)
93
- end
94
- end
95
- end
96
82
  end
@@ -3,7 +3,9 @@
3
3
  RSpec.describe TTY::Prompt::Reader, '#publish_keypress_event' do
4
4
  let(:input) { StringIO.new }
5
5
  let(:out) { StringIO.new }
6
- let(:reader) { described_class.new(input, out) }
6
+ let(:env) { { "TTY_TEST" => true } }
7
+
8
+ let(:reader) { described_class.new(input, out, env: env) }
7
9
 
8
10
  it "publishes :keypress events" do
9
11
  input << "abc\n"
@@ -11,8 +13,8 @@ RSpec.describe TTY::Prompt::Reader, '#publish_keypress_event' do
11
13
  chars = []
12
14
  reader.on(:keypress) { |event| chars << event.value }
13
15
  answer = reader.read_line
14
- expect(chars).to eq(%w(a b c))
15
- expect(answer).to eq("abc")
16
+ expect(chars).to eq(%W(a b c \n))
17
+ expect(answer).to eq("abc\n")
16
18
  end
17
19
 
18
20
  it "publishes :keyup for read_keypress" do