curses_menu 0.0.2 → 0.0.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/LICENSE.md +31 -0
- data/README.md +78 -0
- data/lib/curses_menu/version.rb +1 -1
- data/spec/curses_menu_test.rb +121 -0
- data/spec/curses_menu_test/actions_spec.rb +221 -0
- data/spec/curses_menu_test/formatting_spec.rb +397 -0
- data/spec/curses_menu_test/rubocop_spec.rb +31 -0
- data/spec/curses_menu_test/scrolling_spec.rb +95 -0
- data/spec/curses_menu_test/simple_navigation_spec.rb +123 -0
- data/spec/spec_helper.rb +105 -0
- metadata +23 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: db0a039d187afe66e4986489a45193a3ca76bb5e748a14f9cf0e22f08a5b7065
|
4
|
+
data.tar.gz: 6e0fc6819a62ff3e1da4060fa9f9891088624c1098d91cf066b701b6fe7e3cda
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5786992710dfcd47c11d223884c0b4ecef021d28470386c1427e8666e3a62be675ac885f19e69f41d8529a3b9886a48b28b9096079f3b8182255fcf56a2ff6b8
|
7
|
+
data.tar.gz: 6b210504415fe9c5e1d6956eb7024e03b982a5bcf1de034efc96c4ebae47ff0993dddec75e1cec0b52b6bb8a4dd4f6a48c0cfabd1a82d4d917b297cd2fe86f77
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# [v0.0.3](https://github.com/Muriel-Salvan/curses_menu/compare/v0.0.2...v0.0.3) (2021-08-15 16:16:16)
|
2
|
+
|
3
|
+
### Patches
|
4
|
+
|
5
|
+
* [Add the spec direcotry in the gem as it contains files that can be useful to users](https://github.com/Muriel-Salvan/curses_menu/commit/aeb73ea4507029be97dba2d8243fa3e33ac151ba)
|
6
|
+
|
7
|
+
# [v0.0.2](https://github.com/Muriel-Salvan/curses_menu/compare/v0.0.1...v0.0.2) (2021-08-15 15:24:19)
|
8
|
+
|
9
|
+
### Patches
|
10
|
+
|
11
|
+
* [Added colors testing + Proper CI/CD + Ruby 3 compatibility](https://github.com/Muriel-Salvan/curses_menu/commit/035b19fc27a61c9d10c6b5517993dd0f54537eec)
|
12
|
+
* [Add white colors](https://github.com/Muriel-Salvan/curses_menu/commit/5adf552c01f7a254621e53ecd45e04e9c2358bb8)
|
13
|
+
|
14
|
+
# 0.0.1
|
15
|
+
|
16
|
+
* Initial version
|
data/LICENSE.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
The license stated herein is a copy of the BSD License (modified on July 1999).
|
3
|
+
The AUTHOR mentionned below refers to the list of people involved in the
|
4
|
+
creation and modification of any file included in the delivered package.
|
5
|
+
This list is found in the file named AUTHORS.
|
6
|
+
The AUTHORS and LICENSE files have to be included in any release of software
|
7
|
+
embedding source code of this package, or using it as a derivative software.
|
8
|
+
|
9
|
+
Copyright (c) 2019 - 2019 Muriel Salvan (muriel@x-aeon.com)
|
10
|
+
|
11
|
+
Redistribution and use in source and binary forms, with or without
|
12
|
+
modification, are permitted provided that the following conditions are met:
|
13
|
+
|
14
|
+
1. Redistributions of source code must retain the above copyright notice,
|
15
|
+
this list of conditions and the following disclaimer.
|
16
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
17
|
+
this list of conditions and the following disclaimer in the documentation
|
18
|
+
and/or other materials provided with the distribution.
|
19
|
+
3. The name of the author may not be used to endorse or promote products
|
20
|
+
derived from this software without specific prior written permission.
|
21
|
+
|
22
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
23
|
+
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
24
|
+
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
25
|
+
EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
26
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
27
|
+
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
28
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
29
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
30
|
+
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
|
31
|
+
OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# curses_menu
|
2
|
+
|
3
|
+
Ruby API to create terminal-based menus, using [curses][link-curses].
|
4
|
+
|
5
|
+
* Easy navigation using arrows, page up/down, home, end, enter and escape keys.
|
6
|
+
* Several actions per menu item.
|
7
|
+
* Scrolling support.
|
8
|
+
* Extensive formatting options with colors, alignments, decorations...
|
9
|
+
* Easy support for sub-menus.
|
10
|
+
* Automatic key presses for autmating tasks in the menu.
|
11
|
+
* Ruby-like API.
|
12
|
+
|
13
|
+
## Install
|
14
|
+
|
15
|
+
Via gem
|
16
|
+
|
17
|
+
``` bash
|
18
|
+
$ gem install curses_menu
|
19
|
+
```
|
20
|
+
|
21
|
+
Via a Gemfile
|
22
|
+
|
23
|
+
``` ruby
|
24
|
+
$ gem 'curses_menu'
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
``` ruby
|
30
|
+
require 'curses_menu'
|
31
|
+
|
32
|
+
CursesMenu.new 'My awesome new menu!' do |menu|
|
33
|
+
menu.item 'How\'s life?' do
|
34
|
+
puts 'Couldn\'t be easier'
|
35
|
+
:menu_exit
|
36
|
+
end
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
Check the [examples][link-examples] folder for more examples.
|
41
|
+
|
42
|
+
## Change log
|
43
|
+
|
44
|
+
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
|
45
|
+
|
46
|
+
## Testing
|
47
|
+
|
48
|
+
Automated tests are done using rspec.
|
49
|
+
|
50
|
+
Do execute them, first install development dependencies:
|
51
|
+
|
52
|
+
```bash
|
53
|
+
bundle install
|
54
|
+
```
|
55
|
+
|
56
|
+
Then execute rspec
|
57
|
+
|
58
|
+
```bash
|
59
|
+
bundle exec rspec
|
60
|
+
```
|
61
|
+
|
62
|
+
## Contributing
|
63
|
+
|
64
|
+
Any contribution is welcome:
|
65
|
+
* Fork the github project and create pull requests.
|
66
|
+
* Report bugs by creating tickets.
|
67
|
+
* Suggest improvements and new features by creating tickets.
|
68
|
+
|
69
|
+
## Credits
|
70
|
+
|
71
|
+
- [Muriel Salvan][link-author]
|
72
|
+
|
73
|
+
## License
|
74
|
+
|
75
|
+
The BSD License. Please see [License File](LICENSE.md) for more information.
|
76
|
+
|
77
|
+
[link-curses]: https://rubygems.org/gems/curses/versions/1.2.4
|
78
|
+
[link-examples]: ./examples
|
data/lib/curses_menu/version.rb
CHANGED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'curses_menu'
|
2
|
+
|
3
|
+
module CursesMenuTest
|
4
|
+
|
5
|
+
# Monkey-patch the curses_menu_finalize method so that it captures the menu screen before finalizing
|
6
|
+
module CursesMenuPatch
|
7
|
+
|
8
|
+
# Last screenshot taken
|
9
|
+
# Array<String>: List of lines
|
10
|
+
attr_reader :screenshot
|
11
|
+
|
12
|
+
# Finalize the curses menu window
|
13
|
+
def curses_menu_finalize
|
14
|
+
@screenshot = capture_screenshot
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Get a screenshot of the menu
|
21
|
+
#
|
22
|
+
# Result::
|
23
|
+
# * Array<String>: List of lines
|
24
|
+
def capture_screenshot
|
25
|
+
# Curses is initialized
|
26
|
+
window = Curses.stdscr
|
27
|
+
old_x = window.curx
|
28
|
+
old_y = window.cury
|
29
|
+
chars = []
|
30
|
+
window.maxy.times do |idx_y|
|
31
|
+
window.maxx.times do |idx_x|
|
32
|
+
window.setpos idx_y, idx_x
|
33
|
+
chars << window.inch
|
34
|
+
end
|
35
|
+
end
|
36
|
+
window.setpos old_y, old_x
|
37
|
+
# Build the map of colors per color pair acutally registered
|
38
|
+
colors_left_shift = Curses::A_COLOR.to_s(2).match(/^1+(0+)$/)[1].size
|
39
|
+
color_pairs = CursesMenu.constants.select { |const| const.to_s.start_with?('COLORS_') }.map do |const|
|
40
|
+
color_pair = CursesMenu.const_get(const)
|
41
|
+
[
|
42
|
+
# On Windows using Curses.color_pair can result in bugs [BUG] Unnormalized Fixnum value when using/displaying the value.
|
43
|
+
# So for now we depend on the internal algorithm used by color_pair (which is a left shift of the 0 bits of A_COLOR mask)
|
44
|
+
# TODO: Uncomment the following when curses will be fixed on Windows
|
45
|
+
# Curses.color_pair(color_pair),
|
46
|
+
color_pair << colors_left_shift,
|
47
|
+
const
|
48
|
+
]
|
49
|
+
end.to_h
|
50
|
+
chars.
|
51
|
+
map do |chr|
|
52
|
+
{
|
53
|
+
char: (chr & Curses::A_CHARTEXT).chr,
|
54
|
+
color: color_pairs[chr & Curses::A_COLOR] || chr & Curses::A_COLOR,
|
55
|
+
attributes: chr & Curses::A_ATTRIBUTES
|
56
|
+
}
|
57
|
+
end.
|
58
|
+
each_slice(window.maxx).
|
59
|
+
to_a
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
# Helpers for the tests
|
65
|
+
module Helpers
|
66
|
+
|
67
|
+
# Test a given menu, and prepare a screenshot to be analyzed
|
68
|
+
#
|
69
|
+
# Parameters::
|
70
|
+
# * *title* (String): The title [default: 'Menu title']
|
71
|
+
# * *keys* (Array<Object> or nil): Keys to automatically press [default: []]
|
72
|
+
# * *auto_exit* (Boolean): Do we automatically add the escape key to the key presses? [default: true]
|
73
|
+
# * Proc: The code called with the test menu to be populated
|
74
|
+
# * Parameters::
|
75
|
+
# * *menu* (CursesMenu): Curses menu to populate
|
76
|
+
# * *key_presses* (Array<Object>): Keys to possibly give to sub-menus
|
77
|
+
def test_menu(title: 'Menu title', keys: [], auto_exit: true)
|
78
|
+
# TODO: Find a way to not depend on the current terminal screen, and run the tests silently.
|
79
|
+
key_presses = auto_exit ? keys + [CursesMenu::KEY_ESCAPE] : keys
|
80
|
+
menu = CursesMenu.new(title, key_presses: key_presses) do |m|
|
81
|
+
yield m, key_presses
|
82
|
+
end
|
83
|
+
@screenshot = menu.screenshot
|
84
|
+
end
|
85
|
+
|
86
|
+
# Assert that a line of the screenshot starts with a given content
|
87
|
+
#
|
88
|
+
# Parameters::
|
89
|
+
# * *line_idx* (Integer): The line index of the screenshot
|
90
|
+
# * *expectation* (String): The expected line
|
91
|
+
def assert_line(line_idx, expectation)
|
92
|
+
line = @screenshot[line_idx][0..expectation.size].map { |char_info| char_info[:char] }.join
|
93
|
+
# Add an ending space to make sure the line does not continue after what we test
|
94
|
+
expect(line).to eq("#{expectation} "), "Screenshot line #{line_idx} differs:\n \"#{line}\" should be\n \"#{expectation} \""
|
95
|
+
end
|
96
|
+
|
97
|
+
# Assert that a line of the screenshot starts with a given content, using colors information
|
98
|
+
#
|
99
|
+
# Parameters::
|
100
|
+
# * *line_idx* (Integer): The line index of the screenshot
|
101
|
+
# * *expectation* (String): The expected line
|
102
|
+
# * *color* (Symbol): The expected color pair name
|
103
|
+
def assert_colored_line(line_idx, expectation, color)
|
104
|
+
colored_line = @screenshot[line_idx][0..expectation.size - 1].map do |char_info|
|
105
|
+
[char_info[:char], char_info[:color]]
|
106
|
+
end
|
107
|
+
expected_colored_line = expectation.each_char.map do |chr|
|
108
|
+
[chr, color]
|
109
|
+
end
|
110
|
+
expect(colored_line).to eq(expected_colored_line), "Screenshot line #{line_idx} differs:\n \"#{colored_line}\" should be\n \"#{expected_colored_line}\""
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
class CursesMenu
|
118
|
+
|
119
|
+
prepend CursesMenuTest::CursesMenuPatch
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
describe CursesMenu do
|
2
|
+
|
3
|
+
it 'actions the default selection when pressed enter' do
|
4
|
+
actioned = false
|
5
|
+
test_menu(keys: [CursesMenu::KEY_ENTER]) do |menu|
|
6
|
+
menu.item 'Menu item' do
|
7
|
+
actioned = true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
expect(actioned).to eq(true)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'actions the default selection when pressed enter on the correct item' do
|
14
|
+
action = nil
|
15
|
+
test_menu(keys: [Curses::KEY_DOWN, Curses::KEY_DOWN, CursesMenu::KEY_ENTER]) do |menu|
|
16
|
+
menu.item 'Menu item 1' do
|
17
|
+
action = 1
|
18
|
+
end
|
19
|
+
menu.item 'Menu item 2' do
|
20
|
+
action = 2
|
21
|
+
end
|
22
|
+
menu.item 'Menu item 3' do
|
23
|
+
action = 3
|
24
|
+
end
|
25
|
+
menu.item 'Menu item 4' do
|
26
|
+
action = 4
|
27
|
+
end
|
28
|
+
end
|
29
|
+
expect(action).to eq(3)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'actions other actions' do
|
33
|
+
action = nil
|
34
|
+
test_menu(keys: ['a']) do |menu|
|
35
|
+
menu.item 'Menu item', actions: {
|
36
|
+
'a' => {
|
37
|
+
name: 'Action A',
|
38
|
+
execute: proc { action = 'a' }
|
39
|
+
},
|
40
|
+
'b' => {
|
41
|
+
name: 'Action B',
|
42
|
+
execute: proc { action = 'b' }
|
43
|
+
}
|
44
|
+
}
|
45
|
+
end
|
46
|
+
expect(action).to eq('a')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'actions several actions' do
|
50
|
+
actions = []
|
51
|
+
test_menu(keys: %w[a b a]) do |menu|
|
52
|
+
menu.item 'Menu item', actions: {
|
53
|
+
'a' => {
|
54
|
+
name: 'Action A',
|
55
|
+
execute: proc { actions << 'a' }
|
56
|
+
},
|
57
|
+
'b' => {
|
58
|
+
name: 'Action B',
|
59
|
+
execute: proc { actions << 'b' }
|
60
|
+
}
|
61
|
+
}
|
62
|
+
end
|
63
|
+
expect(actions).to eq(%w[a b a])
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'actions several actions including the default one' do
|
67
|
+
actions = []
|
68
|
+
test_menu(keys: ['a', 'b', CursesMenu::KEY_ENTER, 'a']) do |menu|
|
69
|
+
menu.item(
|
70
|
+
'Menu item',
|
71
|
+
actions: {
|
72
|
+
'a' => {
|
73
|
+
name: 'Action A',
|
74
|
+
execute: proc { actions << 'a' }
|
75
|
+
},
|
76
|
+
'b' => {
|
77
|
+
name: 'Action B',
|
78
|
+
execute: proc { actions << 'b' }
|
79
|
+
}
|
80
|
+
}
|
81
|
+
) do
|
82
|
+
actions << 'ENTER'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
expect(actions).to eq(%w[a b ENTER a])
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'actions nothing if action does not exist' do
|
89
|
+
actions = []
|
90
|
+
test_menu(keys: %w[a b c a]) do |menu|
|
91
|
+
menu.item(
|
92
|
+
'Menu item',
|
93
|
+
actions: {
|
94
|
+
'a' => {
|
95
|
+
name: 'Action A',
|
96
|
+
execute: proc { actions << 'a' }
|
97
|
+
},
|
98
|
+
'b' => {
|
99
|
+
name: 'Action B',
|
100
|
+
execute: proc { actions << 'b' }
|
101
|
+
}
|
102
|
+
}
|
103
|
+
) do
|
104
|
+
actions << 'ENTER'
|
105
|
+
end
|
106
|
+
end
|
107
|
+
expect(actions).to eq(%w[a b a])
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'exits when action returns :menu_exit' do
|
111
|
+
quit = false
|
112
|
+
test_menu(keys: [CursesMenu::KEY_ENTER], auto_exit: false) do |menu|
|
113
|
+
menu.item 'Menu item quit' do
|
114
|
+
quit = true
|
115
|
+
:menu_exit
|
116
|
+
end
|
117
|
+
end
|
118
|
+
expect(quit).to eq(true)
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'navigates in sub-menus' do
|
122
|
+
actions = []
|
123
|
+
test_menu(
|
124
|
+
keys: [
|
125
|
+
# Enter sub-menu 1
|
126
|
+
CursesMenu::KEY_ENTER,
|
127
|
+
# Action sub-menu second item
|
128
|
+
Curses::KEY_DOWN,
|
129
|
+
CursesMenu::KEY_ENTER,
|
130
|
+
# Back to first menu
|
131
|
+
CursesMenu::KEY_ESCAPE,
|
132
|
+
# Enter sub-menu 2
|
133
|
+
Curses::KEY_DOWN,
|
134
|
+
CursesMenu::KEY_ENTER,
|
135
|
+
# Action sub-menu item
|
136
|
+
CursesMenu::KEY_ENTER,
|
137
|
+
# Exit sub-menu
|
138
|
+
Curses::KEY_DOWN,
|
139
|
+
CursesMenu::KEY_ENTER
|
140
|
+
]
|
141
|
+
) do |menu, key_presses|
|
142
|
+
menu.item 'Sub-menu 1' do
|
143
|
+
described_class.new('Sub-menu 1 title', key_presses: key_presses) do |sub_menu|
|
144
|
+
sub_menu.item 'Sub-menu item 1'
|
145
|
+
sub_menu.item 'Sub-menu item 2' do
|
146
|
+
actions << 'a'
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
menu.item 'Sub-menu 2' do
|
151
|
+
described_class.new('Sub-menu 2 title', key_presses: key_presses) do |sub_menu|
|
152
|
+
sub_menu.item 'Sub-menu item 1' do
|
153
|
+
actions << 'b'
|
154
|
+
end
|
155
|
+
sub_menu.item 'Sub-menu item 2' do
|
156
|
+
:menu_exit
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
expect(actions).to eq(%w[a b])
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'exits only the sub-menu when action returns :menu_exit in a sub-menu' do
|
165
|
+
actions = []
|
166
|
+
test_menu(keys: [CursesMenu::KEY_ENTER, CursesMenu::KEY_ENTER, Curses::KEY_DOWN, CursesMenu::KEY_ENTER]) do |menu, key_presses|
|
167
|
+
menu.item 'Sub-menu' do
|
168
|
+
described_class.new('Sub-menu title', key_presses: key_presses) do |sub_menu|
|
169
|
+
sub_menu.item 'Sub-menu item quit' do
|
170
|
+
actions << 'a'
|
171
|
+
:menu_exit
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
menu.item 'Menu item 2' do
|
176
|
+
actions << 'b'
|
177
|
+
end
|
178
|
+
end
|
179
|
+
expect(actions).to eq(%w[a b])
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'exits only the sub-menu when Escape key is used' do
|
183
|
+
actions = []
|
184
|
+
test_menu(keys: [CursesMenu::KEY_ENTER, CursesMenu::KEY_ESCAPE, Curses::KEY_DOWN, CursesMenu::KEY_ENTER]) do |menu, key_presses|
|
185
|
+
menu.item 'Sub-menu' do
|
186
|
+
described_class.new('Sub-menu title', key_presses: key_presses) do |sub_menu|
|
187
|
+
sub_menu.item 'Sub-menu item quit' do
|
188
|
+
actions << 'a'
|
189
|
+
:menu_exit
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
menu.item 'Menu item 2' do
|
194
|
+
actions << 'b'
|
195
|
+
end
|
196
|
+
end
|
197
|
+
expect(actions).to eq(%w[b])
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'does not refresh menu items normally' do
|
201
|
+
idx = 0
|
202
|
+
test_menu(keys: [CursesMenu::KEY_ENTER, CursesMenu::KEY_ENTER]) do |menu|
|
203
|
+
menu.item "Menu item #{idx}" do
|
204
|
+
idx += 1
|
205
|
+
end
|
206
|
+
end
|
207
|
+
assert_line 3, 'Menu item 0'
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'refreshes menu items when action returns :menu_refresh' do
|
211
|
+
idx = 0
|
212
|
+
test_menu(keys: [CursesMenu::KEY_ENTER, CursesMenu::KEY_ENTER]) do |menu|
|
213
|
+
menu.item "Menu item #{idx}" do
|
214
|
+
idx += 1
|
215
|
+
:menu_refresh
|
216
|
+
end
|
217
|
+
end
|
218
|
+
assert_line 3, 'Menu item 2'
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
@@ -0,0 +1,397 @@
|
|
1
|
+
describe CursesMenu do
|
2
|
+
|
3
|
+
it 'displays a single string' do
|
4
|
+
test_menu do |menu|
|
5
|
+
menu.item 'Simple string'
|
6
|
+
end
|
7
|
+
assert_line 3, 'Simple string'
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'displays a single string in a CursesRow' do
|
11
|
+
test_menu do |menu|
|
12
|
+
menu.item CursesMenu::CursesRow.new({ cell: { text: 'Simple string' } })
|
13
|
+
end
|
14
|
+
assert_line 3, 'Simple string'
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'displays a different color' do
|
18
|
+
test_menu do |menu|
|
19
|
+
menu.item CursesMenu::CursesRow.new(
|
20
|
+
{
|
21
|
+
cell: {
|
22
|
+
text: 'Selected colored string',
|
23
|
+
color_pair: CursesMenu::COLORS_GREEN
|
24
|
+
}
|
25
|
+
}
|
26
|
+
)
|
27
|
+
menu.item CursesMenu::CursesRow.new(
|
28
|
+
{
|
29
|
+
cell: {
|
30
|
+
text: 'Non-selected colored string',
|
31
|
+
color_pair: CursesMenu::COLORS_GREEN
|
32
|
+
}
|
33
|
+
}
|
34
|
+
)
|
35
|
+
end
|
36
|
+
assert_colored_line 3, 'Selected colored string', :COLORS_MENU_ITEM_SELECTED
|
37
|
+
assert_colored_line 4, 'Non-selected colored string', :COLORS_GREEN
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'adds prefixes' do
|
41
|
+
test_menu do |menu|
|
42
|
+
menu.item CursesMenu::CursesRow.new(
|
43
|
+
{
|
44
|
+
cell: {
|
45
|
+
text: 'Simple string',
|
46
|
+
begin_with: 'PRE'
|
47
|
+
}
|
48
|
+
}
|
49
|
+
)
|
50
|
+
end
|
51
|
+
assert_line 3, 'PRESimple string'
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'adds suffixes' do
|
55
|
+
test_menu do |menu|
|
56
|
+
menu.item CursesMenu::CursesRow.new(
|
57
|
+
{
|
58
|
+
cell: {
|
59
|
+
text: 'Simple string',
|
60
|
+
end_with: 'POST'
|
61
|
+
}
|
62
|
+
}
|
63
|
+
)
|
64
|
+
end
|
65
|
+
assert_line 3, 'Simple stringPOST'
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'limits fixed-size strings that exceed size' do
|
69
|
+
test_menu do |menu|
|
70
|
+
menu.item CursesMenu::CursesRow.new(
|
71
|
+
{
|
72
|
+
cell: {
|
73
|
+
text: 'Simple string',
|
74
|
+
fixed_size: 5
|
75
|
+
}
|
76
|
+
}
|
77
|
+
)
|
78
|
+
end
|
79
|
+
assert_line 3, 'Simpl'
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'pads fixed-size strings that do not exceed size' do
|
83
|
+
test_menu do |menu|
|
84
|
+
menu.item CursesMenu::CursesRow.new(
|
85
|
+
{
|
86
|
+
cell: {
|
87
|
+
text: 'Simple string',
|
88
|
+
fixed_size: 15,
|
89
|
+
pad: '*'
|
90
|
+
}
|
91
|
+
}
|
92
|
+
)
|
93
|
+
end
|
94
|
+
assert_line 3, 'Simple string**'
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'pads fixed-size strings that do not exceed size with multi-chars padding' do
|
98
|
+
test_menu do |menu|
|
99
|
+
menu.item CursesMenu::CursesRow.new(
|
100
|
+
{
|
101
|
+
cell: {
|
102
|
+
text: 'Simple string',
|
103
|
+
fixed_size: 20,
|
104
|
+
pad: '12345'
|
105
|
+
}
|
106
|
+
}
|
107
|
+
)
|
108
|
+
end
|
109
|
+
assert_line 3, 'Simple string1234512'
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'does not pad fixed-size strings that exceed size' do
|
113
|
+
test_menu do |menu|
|
114
|
+
menu.item CursesMenu::CursesRow.new(
|
115
|
+
{
|
116
|
+
cell: {
|
117
|
+
text: 'Simple string',
|
118
|
+
fixed_size: 5,
|
119
|
+
pad: '*'
|
120
|
+
}
|
121
|
+
}
|
122
|
+
)
|
123
|
+
end
|
124
|
+
assert_line 3, 'Simpl'
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'left-justifies fixed-size strings that do not exceed size' do
|
128
|
+
test_menu do |menu|
|
129
|
+
menu.item CursesMenu::CursesRow.new(
|
130
|
+
{
|
131
|
+
cell: {
|
132
|
+
text: 'Simple string',
|
133
|
+
fixed_size: 15,
|
134
|
+
pad: '*',
|
135
|
+
justify: :left
|
136
|
+
}
|
137
|
+
}
|
138
|
+
)
|
139
|
+
end
|
140
|
+
assert_line 3, 'Simple string**'
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'right-justifies fixed-size strings that do not exceed size' do
|
144
|
+
test_menu do |menu|
|
145
|
+
menu.item CursesMenu::CursesRow.new(
|
146
|
+
{
|
147
|
+
cell: {
|
148
|
+
text: 'Simple string',
|
149
|
+
fixed_size: 15,
|
150
|
+
pad: '*',
|
151
|
+
justify: :right
|
152
|
+
}
|
153
|
+
}
|
154
|
+
)
|
155
|
+
end
|
156
|
+
assert_line 3, '**Simple string'
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'never truncates prefixes when size exceeds fixed size' do
|
160
|
+
test_menu do |menu|
|
161
|
+
menu.item CursesMenu::CursesRow.new(
|
162
|
+
{
|
163
|
+
cell: {
|
164
|
+
text: 'Simple string',
|
165
|
+
fixed_size: 15,
|
166
|
+
begin_with: 'PRE'
|
167
|
+
}
|
168
|
+
}
|
169
|
+
)
|
170
|
+
end
|
171
|
+
assert_line 3, 'PRESimple strin'
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'never truncates suffixes when size exceeds fixed size' do
|
175
|
+
test_menu do |menu|
|
176
|
+
menu.item CursesMenu::CursesRow.new(
|
177
|
+
{
|
178
|
+
cell: {
|
179
|
+
text: 'Simple string',
|
180
|
+
fixed_size: 15,
|
181
|
+
end_with: 'POST'
|
182
|
+
}
|
183
|
+
}
|
184
|
+
)
|
185
|
+
end
|
186
|
+
assert_line 3, 'Simple striPOST'
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'never truncates prefixes and suffixes when size exceeds fixed size' do
|
190
|
+
test_menu do |menu|
|
191
|
+
menu.item CursesMenu::CursesRow.new(
|
192
|
+
{
|
193
|
+
cell: {
|
194
|
+
text: 'Simple string',
|
195
|
+
fixed_size: 15,
|
196
|
+
begin_with: 'PRE',
|
197
|
+
end_with: 'POST'
|
198
|
+
}
|
199
|
+
}
|
200
|
+
)
|
201
|
+
end
|
202
|
+
assert_line 3, 'PRESimple sPOST'
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'displays several cells' do
|
206
|
+
test_menu do |menu|
|
207
|
+
menu.item CursesMenu::CursesRow.new(
|
208
|
+
{
|
209
|
+
cell_1: { text: 'Cell 1' },
|
210
|
+
cell_2: { text: 'Cell 2' },
|
211
|
+
cell_3: { text: 'Cell 3' }
|
212
|
+
}
|
213
|
+
)
|
214
|
+
end
|
215
|
+
assert_line 3, 'Cell 1 Cell 2 Cell 3'
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'displays several cells with a different separator' do
|
219
|
+
test_menu do |menu|
|
220
|
+
menu.item CursesMenu::CursesRow.new(
|
221
|
+
{
|
222
|
+
cell_1: { text: 'Cell 1' },
|
223
|
+
cell_2: { text: 'Cell 2' },
|
224
|
+
cell_3: { text: 'Cell 3' }
|
225
|
+
},
|
226
|
+
separator: 'SEP'
|
227
|
+
)
|
228
|
+
end
|
229
|
+
assert_line 3, 'Cell 1SEPCell 2SEPCell 3'
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'does not exceed line when several cells are too long' do
|
233
|
+
nbr_visible_chars = Curses.stdscr.maxx
|
234
|
+
nbr_chars_per_cell = nbr_visible_chars / 3 + 1
|
235
|
+
test_menu do |menu|
|
236
|
+
menu.item CursesMenu::CursesRow.new(
|
237
|
+
{
|
238
|
+
cell_1: { text: '1' * nbr_chars_per_cell },
|
239
|
+
cell_2: { text: '2' * nbr_chars_per_cell },
|
240
|
+
cell_3: { text: '3' * nbr_chars_per_cell },
|
241
|
+
cell_4: { text: '4' * nbr_chars_per_cell }
|
242
|
+
}
|
243
|
+
)
|
244
|
+
menu.item 'Menu item 2'
|
245
|
+
end
|
246
|
+
assert_line 3, "#{'1' * nbr_chars_per_cell} #{'2' * nbr_chars_per_cell} #{'3' * (nbr_visible_chars - 2 * nbr_chars_per_cell - 3)}"
|
247
|
+
assert_line 4, 'Menu item 2'
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'does not exceed line when several cells are too long due to separators' do
|
251
|
+
nbr_visible_chars = Curses.stdscr.maxx
|
252
|
+
nbr_chars_per_cell = nbr_visible_chars / 3 + 1
|
253
|
+
test_menu do |menu|
|
254
|
+
menu.item CursesMenu::CursesRow.new(
|
255
|
+
{
|
256
|
+
cell_1: { text: '1' * nbr_chars_per_cell },
|
257
|
+
cell_3: { text: '3' * nbr_chars_per_cell }
|
258
|
+
},
|
259
|
+
separator: '2' * nbr_chars_per_cell
|
260
|
+
)
|
261
|
+
menu.item 'Menu item 2'
|
262
|
+
end
|
263
|
+
assert_line 3, "#{'1' * nbr_chars_per_cell}#{'2' * nbr_chars_per_cell}#{'3' * (nbr_visible_chars - 2 * nbr_chars_per_cell - 1)}"
|
264
|
+
assert_line 4, 'Menu item 2'
|
265
|
+
end
|
266
|
+
|
267
|
+
it 'displays several cells with different properties' do
|
268
|
+
test_menu do |menu|
|
269
|
+
menu.item CursesMenu::CursesRow.new(
|
270
|
+
{
|
271
|
+
cell_1: {
|
272
|
+
text: 'Cell 1',
|
273
|
+
begin_with: 'PRE'
|
274
|
+
},
|
275
|
+
cell_2: {
|
276
|
+
text: 'Cell 2',
|
277
|
+
color_pair: CursesMenu::COLORS_GREEN,
|
278
|
+
end_with: 'POST'
|
279
|
+
},
|
280
|
+
cell_3: {
|
281
|
+
text: 'Cell 3',
|
282
|
+
fixed_size: 10,
|
283
|
+
pad: '*',
|
284
|
+
justify: :right
|
285
|
+
},
|
286
|
+
cell_4: {
|
287
|
+
text: 'Cell 4',
|
288
|
+
fixed_size: 2
|
289
|
+
},
|
290
|
+
cell_5: {
|
291
|
+
text: 'Cell 5',
|
292
|
+
fixed_size: 10,
|
293
|
+
pad: '='
|
294
|
+
}
|
295
|
+
}
|
296
|
+
)
|
297
|
+
end
|
298
|
+
# TODO: Find a way to test colors
|
299
|
+
assert_line 3, 'PRECell 1 Cell 2POST ****Cell 3 Ce Cell 5===='
|
300
|
+
end
|
301
|
+
|
302
|
+
it 'can reorder cells' do
|
303
|
+
row = CursesMenu::CursesRow.new(
|
304
|
+
{
|
305
|
+
cell_1: { text: 'Cell 1' },
|
306
|
+
cell_2: { text: 'Cell 2' },
|
307
|
+
cell_3: { text: 'Cell 3' }
|
308
|
+
}
|
309
|
+
)
|
310
|
+
row.cells_order(%i[cell_3 cell_2 cell_1])
|
311
|
+
test_menu { |menu| menu.item row }
|
312
|
+
assert_line 3, 'Cell 3 Cell 2 Cell 1'
|
313
|
+
end
|
314
|
+
|
315
|
+
it 'can reorder cells and ignore unknown ones' do
|
316
|
+
row = CursesMenu::CursesRow.new(
|
317
|
+
{
|
318
|
+
cell_1: { text: 'Cell 1' },
|
319
|
+
cell_2: { text: 'Cell 2' },
|
320
|
+
cell_3: { text: 'Cell 3' }
|
321
|
+
}
|
322
|
+
)
|
323
|
+
row.cells_order(%i[cell_4 cell_3 cell_5 cell_2 cell_1])
|
324
|
+
test_menu { |menu| menu.item row }
|
325
|
+
assert_line 3, 'Cell 3 Cell 2 Cell 1'
|
326
|
+
end
|
327
|
+
|
328
|
+
it 'can reorder cells and create unknown ones' do
|
329
|
+
row = CursesMenu::CursesRow.new(
|
330
|
+
{
|
331
|
+
cell_1: { text: 'Cell 1' },
|
332
|
+
cell_2: { text: 'Cell 2' },
|
333
|
+
cell_3: { text: 'Cell 3' }
|
334
|
+
}
|
335
|
+
)
|
336
|
+
row.cells_order(%i[cell_4 cell_3 cell_5 cell_2 cell_1], unknown_cells: 'Cell X')
|
337
|
+
test_menu { |menu| menu.item row }
|
338
|
+
assert_line 3, 'Cell X Cell 3 Cell X Cell 2 Cell 1'
|
339
|
+
end
|
340
|
+
|
341
|
+
it 'can reorder cells and create unknown ones with properties' do
|
342
|
+
row = CursesMenu::CursesRow.new(
|
343
|
+
{
|
344
|
+
cell_1: { text: 'Cell 1' },
|
345
|
+
cell_2: { text: 'Cell 2' },
|
346
|
+
cell_3: { text: 'Cell 3' }
|
347
|
+
}
|
348
|
+
)
|
349
|
+
row.cells_order(
|
350
|
+
%i[cell_4 cell_3 cell_5 cell_2 cell_1],
|
351
|
+
unknown_cells: {
|
352
|
+
text: 'Cell X',
|
353
|
+
begin_with: '{',
|
354
|
+
end_with: '}'
|
355
|
+
}
|
356
|
+
)
|
357
|
+
test_menu { |menu| menu.item row }
|
358
|
+
assert_line 3, '{Cell X} Cell 3 {Cell X} Cell 2 Cell 1'
|
359
|
+
end
|
360
|
+
|
361
|
+
it 'can change cells properties' do
|
362
|
+
row = CursesMenu::CursesRow.new(
|
363
|
+
{
|
364
|
+
cell_1: {
|
365
|
+
text: 'Cell 1',
|
366
|
+
begin_with: 'PRE',
|
367
|
+
end_with: 'POST'
|
368
|
+
},
|
369
|
+
cell_2: {
|
370
|
+
text: 'Cell 2'
|
371
|
+
},
|
372
|
+
cell_3: {
|
373
|
+
text: 'Cell 3',
|
374
|
+
fixed_size: 10,
|
375
|
+
pad: '*'
|
376
|
+
}
|
377
|
+
}
|
378
|
+
)
|
379
|
+
row.change_cells(
|
380
|
+
{
|
381
|
+
cell_1: {
|
382
|
+
begin_with: 'PRE2'
|
383
|
+
},
|
384
|
+
cell_2: {
|
385
|
+
fixed_size: 2
|
386
|
+
},
|
387
|
+
cell_3: {
|
388
|
+
text: 'Cell X',
|
389
|
+
pad: '-='
|
390
|
+
}
|
391
|
+
}
|
392
|
+
)
|
393
|
+
test_menu { |menu| menu.item row }
|
394
|
+
assert_line 3, 'PRE2Cell 1POST Ce Cell X-=-='
|
395
|
+
end
|
396
|
+
|
397
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
describe 'Coding guidelines' do
|
4
|
+
|
5
|
+
it 'makes sure code style follow Rubocop guides' do
|
6
|
+
rubocop_report = JSON.parse(`bundle exec rubocop --format json`)
|
7
|
+
expect(rubocop_report['summary']['offense_count']).to(
|
8
|
+
eq(0),
|
9
|
+
proc do
|
10
|
+
# Format a great error message to help
|
11
|
+
wrong_files = rubocop_report['files'].reject { |file_info| file_info['offenses'].empty? }
|
12
|
+
<<~EO_ERROR
|
13
|
+
#{wrong_files.size} files have Rubocop issues:
|
14
|
+
#{
|
15
|
+
wrong_files.map do |file_info|
|
16
|
+
offenses = file_info['offenses'].map { |offense_info| "L#{offense_info['location']['start_line']}: #{offense_info['cop_name']} - #{offense_info['message']}" }
|
17
|
+
"* #{file_info['path']}:#{
|
18
|
+
if offenses.size == 1
|
19
|
+
" #{offenses.first}"
|
20
|
+
else
|
21
|
+
" #{offenses.size} offenses:\n#{offenses.map { |offense| " - #{offense}" }.join("\n")}"
|
22
|
+
end
|
23
|
+
}"
|
24
|
+
end.join("\n")
|
25
|
+
}
|
26
|
+
EO_ERROR
|
27
|
+
end
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
describe CursesMenu do
|
2
|
+
|
3
|
+
it 'does not span items on more than 1 line' do
|
4
|
+
nbr_visible_chars = Curses.stdscr.maxx
|
5
|
+
test_menu do |menu|
|
6
|
+
menu.item '1' * nbr_visible_chars * 2
|
7
|
+
menu.item 'Menu item 2'
|
8
|
+
end
|
9
|
+
assert_line 3, '1' * (nbr_visible_chars - 1)
|
10
|
+
assert_line 4, 'Menu item 2'
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'scrolls by using right key' do
|
14
|
+
nbr_visible_chars = Curses.stdscr.maxx
|
15
|
+
test_menu(keys: [Curses::KEY_RIGHT, Curses::KEY_RIGHT, Curses::KEY_RIGHT]) do |menu|
|
16
|
+
menu.item "abcde#{'1' * (nbr_visible_chars - 5)}23456789"
|
17
|
+
end
|
18
|
+
assert_line 3, "de#{'1' * (nbr_visible_chars - 5)}23"
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'scrolls by using left key' do
|
22
|
+
nbr_visible_chars = Curses.stdscr.maxx
|
23
|
+
test_menu(keys: [Curses::KEY_RIGHT, Curses::KEY_RIGHT, Curses::KEY_RIGHT, Curses::KEY_LEFT]) do |menu|
|
24
|
+
menu.item "abcde#{'1' * (nbr_visible_chars - 5)}23456789"
|
25
|
+
end
|
26
|
+
assert_line 3, "cde#{'1' * (nbr_visible_chars - 5)}2"
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'scrolls by using down key' do
|
30
|
+
nbr_visible_items = Curses.stdscr.maxy - 5
|
31
|
+
test_menu(keys: [Curses::KEY_NPAGE, Curses::KEY_DOWN, Curses::KEY_DOWN]) do |menu|
|
32
|
+
(nbr_visible_items * 2).times do |idx|
|
33
|
+
menu.item "Menu item #{idx}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
assert_line 3, 'Menu item 2'
|
37
|
+
assert_line(-3, "Menu item #{nbr_visible_items + 1}")
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'scrolls by using up key' do
|
41
|
+
nbr_visible_items = Curses.stdscr.maxy - 5
|
42
|
+
test_menu(keys: [Curses::KEY_END, Curses::KEY_PPAGE, Curses::KEY_UP, Curses::KEY_UP]) do |menu|
|
43
|
+
(nbr_visible_items * 2).times do |idx|
|
44
|
+
menu.item "Menu item #{idx}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
assert_line 3, "Menu item #{nbr_visible_items - 2}"
|
48
|
+
assert_line(-3, "Menu item #{2 * nbr_visible_items - 3}")
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'scrolls by using page down key' do
|
52
|
+
nbr_visible_items = Curses.stdscr.maxy - 5
|
53
|
+
test_menu(keys: [Curses::KEY_NPAGE, Curses::KEY_NPAGE]) do |menu|
|
54
|
+
(nbr_visible_items * 3).times do |idx|
|
55
|
+
menu.item "Menu item #{idx}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
assert_line 3, "Menu item #{nbr_visible_items - 1}"
|
59
|
+
assert_line(-3, "Menu item #{nbr_visible_items * 2 - 2}")
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'scrolls by using page up key' do
|
63
|
+
nbr_visible_items = Curses.stdscr.maxy - 5
|
64
|
+
test_menu(keys: [Curses::KEY_END, Curses::KEY_PPAGE, Curses::KEY_PPAGE]) do |menu|
|
65
|
+
(nbr_visible_items * 3).times do |idx|
|
66
|
+
menu.item "Menu item #{idx}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
assert_line 3, "Menu item #{nbr_visible_items + 2 - 1}"
|
70
|
+
assert_line(-3, "Menu item #{nbr_visible_items * 2}")
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'scrolls by using end key' do
|
74
|
+
nbr_visible_items = Curses.stdscr.maxy - 5
|
75
|
+
test_menu(keys: [Curses::KEY_END]) do |menu|
|
76
|
+
(nbr_visible_items * 2).times do |idx|
|
77
|
+
menu.item "Menu item #{idx}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
assert_line 3, "Menu item #{nbr_visible_items}"
|
81
|
+
assert_line(-3, "Menu item #{nbr_visible_items * 2 - 1}")
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'scrolls by using home key' do
|
85
|
+
nbr_visible_items = Curses.stdscr.maxy - 5
|
86
|
+
test_menu(keys: [Curses::KEY_END, Curses::KEY_HOME]) do |menu|
|
87
|
+
(nbr_visible_items * 3).times do |idx|
|
88
|
+
menu.item "Menu item #{idx}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
assert_line 3, 'Menu item 0'
|
92
|
+
assert_line(-3, "Menu item #{nbr_visible_items - 1}")
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
describe CursesMenu do
|
2
|
+
|
3
|
+
it 'displays a menu with 1 item' do
|
4
|
+
test_menu(title: 'Menu title') do |menu|
|
5
|
+
menu.item 'Menu item'
|
6
|
+
end
|
7
|
+
assert_line 1, '= Menu title'
|
8
|
+
assert_line 3, 'Menu item'
|
9
|
+
assert_line(-1, '= Arrows/Home/End: Navigate | Esc: Exit')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'displays a menu with several items' do
|
13
|
+
test_menu do |menu|
|
14
|
+
menu.item 'Menu item 1'
|
15
|
+
menu.item 'Menu item 2'
|
16
|
+
menu.item 'Menu item 3'
|
17
|
+
end
|
18
|
+
assert_line 3, 'Menu item 1'
|
19
|
+
assert_line 4, 'Menu item 2'
|
20
|
+
assert_line 5, 'Menu item 3'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'displays a menu item with more actions' do
|
24
|
+
test_menu do |menu|
|
25
|
+
menu.item 'Menu item', actions: {
|
26
|
+
'a' => {
|
27
|
+
name: 'First action',
|
28
|
+
execute: proc {}
|
29
|
+
},
|
30
|
+
'b' => {
|
31
|
+
name: 'Second action',
|
32
|
+
execute: proc {}
|
33
|
+
}
|
34
|
+
}
|
35
|
+
end
|
36
|
+
assert_line(-1, '= Arrows/Home/End: Navigate | Esc: Exit | a: First action | b: Second action')
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'navigates by using down key' do
|
40
|
+
test_menu(keys: [Curses::KEY_DOWN, Curses::KEY_DOWN]) do |menu|
|
41
|
+
menu.item 'Menu item 1'
|
42
|
+
menu.item 'Menu item 2'
|
43
|
+
menu.item 'Menu item 3', actions: { 'a' => { name: 'Special action', execute: proc {} } }
|
44
|
+
menu.item 'Menu item 4'
|
45
|
+
end
|
46
|
+
assert_line(-1, '= Arrows/Home/End: Navigate | Esc: Exit | a: Special action')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'navigates by using end key' do
|
50
|
+
test_menu(keys: [Curses::KEY_END]) do |menu|
|
51
|
+
menu.item 'Menu item 1'
|
52
|
+
menu.item 'Menu item 2'
|
53
|
+
menu.item 'Menu item 3'
|
54
|
+
menu.item 'Menu item 4', actions: { 'a' => { name: 'Special action', execute: proc {} } }
|
55
|
+
end
|
56
|
+
assert_line(-1, '= Arrows/Home/End: Navigate | Esc: Exit | a: Special action')
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'navigates by using up key' do
|
60
|
+
test_menu(keys: [Curses::KEY_END, Curses::KEY_UP]) do |menu|
|
61
|
+
menu.item 'Menu item 1'
|
62
|
+
menu.item 'Menu item 2'
|
63
|
+
menu.item 'Menu item 3', actions: { 'a' => { name: 'Special action', execute: proc {} } }
|
64
|
+
menu.item 'Menu item 4'
|
65
|
+
end
|
66
|
+
assert_line(-1, '= Arrows/Home/End: Navigate | Esc: Exit | a: Special action')
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'navigates by using home key' do
|
70
|
+
test_menu(keys: [Curses::KEY_END, Curses::KEY_HOME]) do |menu|
|
71
|
+
menu.item 'Menu item 1', actions: { 'a' => { name: 'Special action', execute: proc {} } }
|
72
|
+
menu.item 'Menu item 2'
|
73
|
+
menu.item 'Menu item 3'
|
74
|
+
menu.item 'Menu item 4'
|
75
|
+
end
|
76
|
+
assert_line(-1, '= Arrows/Home/End: Navigate | Esc: Exit | a: Special action')
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'navigates by using right key' do
|
80
|
+
test_menu(keys: [Curses::KEY_RIGHT, Curses::KEY_RIGHT]) do |menu|
|
81
|
+
menu.item 'Menu item'
|
82
|
+
end
|
83
|
+
assert_line 3, 'nu item'
|
84
|
+
assert_line(-1, 'Arrows/Home/End: Navigate | Esc: Exit')
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'navigates by using left key' do
|
88
|
+
test_menu(keys: [Curses::KEY_RIGHT, Curses::KEY_RIGHT, Curses::KEY_LEFT]) do |menu|
|
89
|
+
menu.item 'Menu item'
|
90
|
+
end
|
91
|
+
assert_line 3, 'enu item'
|
92
|
+
assert_line(-1, ' Arrows/Home/End: Navigate | Esc: Exit')
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'navigates by using page down key' do
|
96
|
+
nbr_visible_items = Curses.stdscr.maxy - 5
|
97
|
+
test_menu(keys: [Curses::KEY_NPAGE]) do |menu|
|
98
|
+
(nbr_visible_items * 2).times do |idx|
|
99
|
+
if idx == nbr_visible_items - 1
|
100
|
+
menu.item "Menu item #{idx}", actions: { 'a' => { name: "Special action #{idx}", execute: proc {} } }
|
101
|
+
else
|
102
|
+
menu.item "Menu item #{idx}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
assert_line(-1, "= Arrows/Home/End: Navigate | Esc: Exit | a: Special action #{nbr_visible_items - 1}")
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'navigates by using page up key' do
|
110
|
+
nbr_visible_items = Curses.stdscr.maxy - 5
|
111
|
+
test_menu(keys: [Curses::KEY_END, Curses::KEY_PPAGE]) do |menu|
|
112
|
+
(nbr_visible_items * 2).times do |idx|
|
113
|
+
if idx == nbr_visible_items
|
114
|
+
menu.item "Menu item #{idx}", actions: { 'a' => { name: "Special action #{idx}", execute: proc {} } }
|
115
|
+
else
|
116
|
+
menu.item "Menu item #{idx}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
assert_line(-1, "= Arrows/Home/End: Navigate | Esc: Exit | a: Special action #{nbr_visible_items}")
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'curses_menu_test'
|
2
|
+
|
3
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
4
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
5
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
6
|
+
# this file to always be loaded, without a need to explicitly require it in any
|
7
|
+
# files.
|
8
|
+
#
|
9
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
10
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
11
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
12
|
+
# individual file that may not need all of that loaded. Instead, consider making
|
13
|
+
# a separate helper file that requires the additional dependencies and performs
|
14
|
+
# the additional setup, and require it from the spec files that actually need
|
15
|
+
# it.
|
16
|
+
#
|
17
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
18
|
+
RSpec.configure do |config|
|
19
|
+
# rspec-expectations config goes here. You can use an alternate
|
20
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
21
|
+
# assertions if you prefer.
|
22
|
+
config.expect_with :rspec do |expectations|
|
23
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
24
|
+
# and `failure_message` of custom matchers include text for helper methods
|
25
|
+
# defined using `chain`, e.g.:
|
26
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
27
|
+
# # => "be bigger than 2 and smaller than 4"
|
28
|
+
# ...rather than:
|
29
|
+
# # => "be bigger than 2"
|
30
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
31
|
+
end
|
32
|
+
|
33
|
+
# Add our own helpers
|
34
|
+
config.include CursesMenuTest::Helpers
|
35
|
+
|
36
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
37
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
38
|
+
config.mock_with :rspec do |mocks|
|
39
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
40
|
+
# a real object. This is generally recommended, and will default to
|
41
|
+
# `true` in RSpec 4.
|
42
|
+
mocks.verify_partial_doubles = true
|
43
|
+
end
|
44
|
+
|
45
|
+
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
|
46
|
+
# have no way to turn it off -- the option exists only for backwards
|
47
|
+
# compatibility in RSpec 3). It causes shared context metadata to be
|
48
|
+
# inherited by the metadata hash of host groups and examples, rather than
|
49
|
+
# triggering implicit auto-inclusion in groups with matching metadata.
|
50
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
51
|
+
|
52
|
+
# The settings below are suggested to provide a good initial experience
|
53
|
+
# with RSpec, but feel free to customize to your heart's content.
|
54
|
+
|
55
|
+
# This allows you to limit a spec run to individual examples or groups
|
56
|
+
# you care about by tagging them with `:focus` metadata. When nothing
|
57
|
+
# is tagged with `:focus`, all examples get run. RSpec also provides
|
58
|
+
# aliases for `it`, `describe`, and `context` that include `:focus`
|
59
|
+
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
|
60
|
+
# config.filter_run_when_matching :focus
|
61
|
+
|
62
|
+
# Allows RSpec to persist some state between runs in order to support
|
63
|
+
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
64
|
+
# you configure your source control system to ignore this file.
|
65
|
+
# config.example_status_persistence_file_path = "spec/examples.txt"
|
66
|
+
|
67
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
68
|
+
# recommended. For more details, see:
|
69
|
+
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
|
70
|
+
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
71
|
+
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
|
72
|
+
# config.disable_monkey_patching!
|
73
|
+
|
74
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
75
|
+
# be too noisy due to issues in dependencies.
|
76
|
+
# config.warnings = true
|
77
|
+
|
78
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
79
|
+
# file, and it's useful to allow more verbose output when running an
|
80
|
+
# individual spec file.
|
81
|
+
# if config.files_to_run.one?
|
82
|
+
# # Use the documentation formatter for detailed output,
|
83
|
+
# # unless a formatter has already been configured
|
84
|
+
# # (e.g. via a command-line flag).
|
85
|
+
# config.default_formatter = "doc"
|
86
|
+
# end
|
87
|
+
|
88
|
+
# Print the 10 slowest examples and example groups at the
|
89
|
+
# end of the spec run, to help surface which specs are running
|
90
|
+
# particularly slow.
|
91
|
+
# config.profile_examples = 10
|
92
|
+
|
93
|
+
# Run specs in random order to surface order dependencies. If you find an
|
94
|
+
# order dependency and want to debug it, you can fix the order by providing
|
95
|
+
# the seed, which is printed after each run.
|
96
|
+
# --seed 1234
|
97
|
+
# config.order = :random
|
98
|
+
|
99
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
100
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
101
|
+
# test failures related to randomization by passing the same `--seed` value
|
102
|
+
# as the one that triggered the failure.
|
103
|
+
# Kernel.srand config.seed
|
104
|
+
|
105
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: curses_menu
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Muriel Salvan
|
@@ -85,8 +85,22 @@ email:
|
|
85
85
|
- muriel@x-aeon.com
|
86
86
|
executables: []
|
87
87
|
extensions: []
|
88
|
-
extra_rdoc_files:
|
88
|
+
extra_rdoc_files:
|
89
|
+
- CHANGELOG.md
|
90
|
+
- README.md
|
91
|
+
- LICENSE.md
|
92
|
+
- examples/hello.rb
|
93
|
+
- examples/sub_menus.rb
|
94
|
+
- examples/automatic_key_presses.rb
|
95
|
+
- examples/refresh.rb
|
96
|
+
- examples/formatting.rb
|
97
|
+
- examples/several_items.rb
|
98
|
+
- examples/actions.rb
|
99
|
+
- examples/scrolling.rb
|
89
100
|
files:
|
101
|
+
- CHANGELOG.md
|
102
|
+
- LICENSE.md
|
103
|
+
- README.md
|
90
104
|
- examples/actions.rb
|
91
105
|
- examples/automatic_key_presses.rb
|
92
106
|
- examples/formatting.rb
|
@@ -98,6 +112,13 @@ files:
|
|
98
112
|
- lib/curses_menu.rb
|
99
113
|
- lib/curses_menu/curses_row.rb
|
100
114
|
- lib/curses_menu/version.rb
|
115
|
+
- spec/curses_menu_test.rb
|
116
|
+
- spec/curses_menu_test/actions_spec.rb
|
117
|
+
- spec/curses_menu_test/formatting_spec.rb
|
118
|
+
- spec/curses_menu_test/rubocop_spec.rb
|
119
|
+
- spec/curses_menu_test/scrolling_spec.rb
|
120
|
+
- spec/curses_menu_test/simple_navigation_spec.rb
|
121
|
+
- spec/spec_helper.rb
|
101
122
|
homepage: https://github.com/Muriel-Salvan/curses_menu
|
102
123
|
licenses:
|
103
124
|
- BSD-3-Clause
|