spec_selector 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecSelectorUtil
4
+ # The Terminal module contains methods concerned with terminal display
5
+ # function.
6
+ module Terminal
7
+ def clear_frame
8
+ system("printf '\e[H'")
9
+ system("printf '\e[3J'")
10
+ system("printf '\e[0J'")
11
+ end
12
+
13
+ def hide_cursor
14
+ system("printf '\e[?25l'")
15
+ end
16
+
17
+ def reveal_cursor
18
+ system("printf '\e[?25h'")
19
+ end
20
+
21
+ def term_height
22
+ $stdout.winsize[0]
23
+ end
24
+
25
+ def open_alt_buffer
26
+ system('tput smcup')
27
+ end
28
+
29
+ def close_alt_buffer
30
+ system('tput rmcup')
31
+ end
32
+
33
+ def reset_cursor
34
+ system("printf '\e[H'")
35
+ end
36
+
37
+ def term_width
38
+ $stdout.winsize[1]
39
+ end
40
+
41
+ def position_cursor(row, col)
42
+ system("printf '\e[#{row};#{col}H'")
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecSelectorUtil
4
+ # The UI module contains methods used to bind and process user input.
5
+ module UI
6
+ DIRECTION_KEYS = ["\e[A", "\e[B"].freeze
7
+ TREE_NAVIGATION_KEYS = ["\r", "\x7F", "\e"].freeze
8
+ OPTION_KEYS = [
9
+ /t/i, /f/i, /p/i, /q/i, /i/i, /r/i, /m/i, /c/i, /a/i, /v/i
10
+ ].freeze
11
+
12
+ def exit_only
13
+ q_to_exit
14
+ loop { quit if user_input.match?(/q/i) }
15
+ end
16
+
17
+ def selector
18
+ set_selected
19
+ @example_count > 1 ? display_list : display_example
20
+ navigate
21
+ end
22
+
23
+ def set_selected
24
+ @list ||= @active_map[:top_level]
25
+ @selected ||= @list.first
26
+ end
27
+
28
+ def navigate
29
+ @selector_index = @list.index(@selected) || 0
30
+ loop { bind_input }
31
+ end
32
+
33
+ def bind_input
34
+ input = user_input
35
+ direction_keys(input) if DIRECTION_KEYS.include?(input)
36
+ tree_nav_keys(input) if TREE_NAVIGATION_KEYS.include?(input)
37
+ option_keys(input) if OPTION_KEYS.any? { |key| input.match?(key) }
38
+ end
39
+
40
+ def quit
41
+ close_alt_buffer if @instructions
42
+ clear_frame
43
+ delete_filter_data
44
+ reveal_cursor
45
+ exit
46
+ end
47
+
48
+ def top_level_list
49
+ exit_instruction_page if @instructions
50
+ @example_display = false
51
+ @selected = nil
52
+ @list = @active_map[:top_level]
53
+ set_selected
54
+ display_list
55
+ end
56
+
57
+ def select_item
58
+ return if @example_display
59
+
60
+ if example?(@selected)
61
+ display_example
62
+ return
63
+ end
64
+
65
+ @list = @active_map[@selected.metadata[:block]]
66
+ @selected = nil
67
+ set_selected
68
+ display_list
69
+ end
70
+
71
+ def exit_instruction_page_only
72
+ exit_instruction_page
73
+ refresh_display
74
+ end
75
+
76
+ def top_fail
77
+ exit_instruction_page if @instructions
78
+ return if @failed.empty?
79
+
80
+ @selected = @failed.first
81
+ display_example
82
+ end
83
+
84
+ def back
85
+ return if top_level?
86
+
87
+ parent_list
88
+ set_selected
89
+ display_list
90
+ end
91
+
92
+ def parent_list
93
+ if @example_display
94
+ @example_display = false
95
+ @list = @active_map[@selected.example_group.metadata[:block]]
96
+ else
97
+ data = parent_data(@selected.metadata)
98
+ p_data = parent_data(data)
99
+ parent_key = p_data ? p_data[:block] : :top_level
100
+ @list = @active_map[parent_key]
101
+ @selected = @groups[data[:block]]
102
+ end
103
+ end
104
+
105
+ def direction_keys(input)
106
+ exit_instruction_page if @instructions
107
+ dir = input == "\e[A" ? -1 : 1
108
+ @selector_index = (@selector_index + dir) % @list.length
109
+ @selected = @list[@selector_index]
110
+ @example_display ? display_example : display_list
111
+ end
112
+
113
+ def tree_nav_keys(input)
114
+ exit_instruction_page_only if @instructions && input != "\e"
115
+
116
+ case input
117
+ when "\r"
118
+ select_item
119
+ when "\x7F"
120
+ back
121
+ when "\e"
122
+ top_level_list
123
+ end
124
+ end
125
+
126
+ def option_keys(input)
127
+ case input
128
+ when /T/
129
+ top_fail!
130
+ when /t/
131
+ top_fail
132
+ when /p/i
133
+ toggle_passing
134
+ when /f/i
135
+ run_only_fails
136
+ when /q/i
137
+ quit
138
+ when /i/i
139
+ unless @instructions
140
+ view_instructions_page
141
+ return
142
+ end
143
+
144
+ exit_instruction_page_only
145
+ when /r/i
146
+ rerun
147
+ when /^a$/i
148
+ rerun_all
149
+ when /m/i
150
+ return if @instructions
151
+
152
+ @selected.metadata[:include] ? filter_remove : filter_include
153
+ refresh_display
154
+ when /^c$/i
155
+ clear_filter
156
+ when /v/i
157
+ view_inclusion_filter
158
+ end
159
+ end
160
+
161
+ def user_input
162
+ input = $stdin.getch
163
+ return input unless IO.select([$stdin], nil, nil, 0.000001)
164
+
165
+ input << $stdin.read_nonblock(2)
166
+ input
167
+ end
168
+ end
169
+ end
data/license.md ADDED
@@ -0,0 +1,8 @@
1
+ The MIT License (MIT)
2
+ Copyright © 2021 Trevor Almon
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/spec/factories.rb ADDED
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestObjects
4
+ RSN = RSpec::Core::Notifications
5
+
6
+ class Example < RSpec::Core::Example
7
+ attr_accessor :metadata, :execution_result, :description, :example_group
8
+
9
+ def initialize
10
+ end
11
+ end
12
+
13
+ class ExecutionResult < RSpec::Core::Example::ExecutionResult
14
+ attr_accessor :status
15
+ end
16
+
17
+ class ExampleGroup < RSpec::Core::ExampleGroup
18
+ attr_accessor :examples, :metadata, :description
19
+ end
20
+
21
+ class SummaryNotification < RSN::SummaryNotification
22
+ attr_accessor :example_count,
23
+ :duration,
24
+ :load_time,
25
+ :errors_outside_of_example_count
26
+
27
+ :examples
28
+ end
29
+
30
+ class SkippedExampleNotification < RSN::SkippedExampleNotification
31
+ attr_accessor :example
32
+
33
+ def fully_formatted(_)
34
+ "\npending example"
35
+ end
36
+ end
37
+
38
+ class FailedExampleNotification < RSN::FailedExampleNotification
39
+ attr_accessor :example
40
+
41
+ def initialize
42
+ end
43
+
44
+ def fully_formatted(_)
45
+ "\nfailed example"
46
+ end
47
+ end
48
+ end
49
+
50
+ FactoryBot.define do
51
+ factory :execution_result, class: 'TestObjects::ExecutionResult' do
52
+ status { :passed }
53
+ end
54
+
55
+ factory :example, class: 'TestObjects::Example' do
56
+ execution_result { build(:execution_result) }
57
+ description { 'passed' }
58
+ metadata { {} }
59
+ end
60
+
61
+ factory :example_group, class: 'TestObjects::ExampleGroup' do
62
+ examples { [build(:example)] }
63
+ metadata { { block: self } }
64
+ description do
65
+ if examples.all? { |ex| ex.execution_result.status == :passed }
66
+ 'passing example group'
67
+ else
68
+ 'non-passing example group'
69
+ end
70
+ end
71
+ end
72
+
73
+ factory :summary_notification, class: 'TestObjects::SummaryNotification' do
74
+ example_count { 25 }
75
+ duration { 1.5 }
76
+ load_time { 0.5 }
77
+ errors_outside_of_examples_count { 0 }
78
+ end
79
+
80
+ factory :skipped_example_notification,
81
+ class: 'TestObjects::SkippedExampleNotification' do
82
+ example
83
+ end
84
+
85
+ factory :failed_example_notification,
86
+ class: 'TestObjects::FailedExampleNotification' do
87
+ example
88
+ end
89
+ end
data/spec/shared.rb ADDED
@@ -0,0 +1,145 @@
1
+ RSpec.shared_context 'shared' do
2
+ let(:spec_selector) { SpecSelector.new(StringIO.new) }
3
+ let(:output) { spec_selector.ivar(:@output).string }
4
+ let(:fail_result) { build(:execution_result, status: :failed) }
5
+ let(:pending_result) { build(:execution_result, status: :pending) }
6
+ let(:failed_example) { build(:example, execution_result: fail_result, metadata: { description: 'failed_example' }) }
7
+ let(:pending_example) do
8
+ build(:example, execution_result: pending_result, metadata: { description: 'pending_example' })
9
+ end
10
+ let(:passing_example) do
11
+ build(:example, metadata: { description: 'passing_example' }, description: 'passing_example')
12
+ end
13
+ let(:pass_group) do
14
+ build(:example_group, examples: [build(:example), build(:example)], metadata: { description: 'pass_group' })
15
+ end
16
+ let(:fail_group) { build(:example_group, examples: [failed_example, failed_example]) }
17
+ let(:pending_group) do
18
+ build(:example_group, examples: [pending_example, pending_example], metadata: { description: 'pending_group' })
19
+ end
20
+ let(:mixed_result_group) { build(:example_group, examples: [passing_example, failed_example, pending_example]) }
21
+ let(:fail_subgroup) do
22
+ build(
23
+ :example_group,
24
+ metadata: {
25
+ parent_example_group: {}
26
+ },
27
+ examples: [failed_example, failed_example]
28
+ )
29
+ end
30
+
31
+ let(:pending_subgroup) do
32
+ build(
33
+ :example_group,
34
+ metadata: {
35
+ parent_example_group: {}
36
+ },
37
+ examples: [pending_example, pending_example]
38
+ )
39
+ end
40
+
41
+ let(:fail_parent_group) { build(:example_group, examples: [], metadata: { description: 'fail_parent_group' }) }
42
+ let(:pending_parent_group) { build(:example_group, examples: [], metadata: { description: 'pending_parent_group' }) }
43
+ let(:pass_subgroup) do
44
+ build(
45
+ :example_group,
46
+ metadata: { parent_example_group: pass_parent_group.metadata, description: 'pass_subgroup' },
47
+ examples: [passing_example, passing_example]
48
+ )
49
+ end
50
+
51
+ let(:pass_parent_group) { build(:example_group, examples: [], metadata: { description: 'pass_parent_group' }) }
52
+ let(:mixed_list) { [pass_group, fail_group] }
53
+ let(:mixed_map) do
54
+ {
55
+ :top_level => [pass_group, fail_group],
56
+ pass_group.metadata[:block] => pass_group.examples,
57
+ fail_group.metadata[:block] => fail_group.examples
58
+ }
59
+ end
60
+
61
+ let(:pending_map) do
62
+ {
63
+ :top_level => [pending_group],
64
+ pending_group.metadata[:block] => pending_group.examples
65
+ }
66
+ end
67
+
68
+ let(:deep_map) do
69
+ {
70
+ :top_level => [pending_parent_group, pass_parent_group, fail_parent_group],
71
+ pending_parent_group.metadata[:block] => [pending_subgroup],
72
+ pass_parent_group.metadata[:block] => [pass_subgroup],
73
+ fail_parent_group.metadata[:block] => [fail_subgroup],
74
+ pending_subgroup.metadata[:block] => pending_subgroup.examples,
75
+ pass_subgroup.metadata[:block] => pass_subgroup.examples,
76
+ fail_subgroup.metadata[:block] => fail_subgroup.examples
77
+ }
78
+ end
79
+
80
+ let(:all_passing_map) do
81
+ {
82
+ top_level: [pass_group, pass_group],
83
+ pass_group.metadata[:block] => pass_group.examples,
84
+ pass_group.metadata[:block] => pass_group.examples
85
+ }
86
+ end
87
+
88
+ def allow_methods(*methods)
89
+ methods.each do |method|
90
+ allow(spec_selector).to receive(method)
91
+ end
92
+ end
93
+
94
+ def ivars_set(ivar_hash)
95
+ ivar_hash.each do |ivar, value|
96
+ spec_selector.ivar_set(ivar, value)
97
+ end
98
+ end
99
+
100
+ def ivar_set(sym, value)
101
+ spec_selector.ivar_set(sym, value)
102
+ end
103
+
104
+ def ivar(sym)
105
+ spec_selector.ivar(sym)
106
+ end
107
+
108
+ def more_data?(readable)
109
+ IO.select([readable], nil, nil, 0.000001)
110
+ end
111
+
112
+ def summary_settings(example)
113
+ case example
114
+ when failed_example
115
+ notification_type = :failed_example_notification
116
+ summary_list = :@failure_summaries
117
+ ivar = :@failed
118
+ when pending_example
119
+ notification_type = :skipped_example_notification
120
+ summary_list = :@pending_summaries
121
+ ivar = :@pending
122
+ end
123
+
124
+ ivars_set(:@selected => example, ivar => [example])
125
+ notification = build(notification_type, example: example)
126
+ spec_selector.ivar(summary_list)[example] = notification
127
+ end
128
+
129
+ def expect_full_instructions_to_be_displayed
130
+ expect(output).to include('Press I to hide instructions')
131
+ expect(output).to include('Press F to exclude passing examples')
132
+ expect(output).to include('Press ↑ or ↓ to navigate list')
133
+ expect(output).to include('Press [enter] to select')
134
+ expect(output).to include('Press Q to exit')
135
+ end
136
+
137
+ def expect_full_instructions_to_be_hidden
138
+ expect(output).to include('Press I to view instructions')
139
+ expect(output).not_to include('Press I to hide instructions')
140
+ expect(output).not_to include('Press F to exclude passing examples')
141
+ expect(output).not_to include('Press ↑ or ↓ to navigate list')
142
+ expect(output).not_to include('Press [enter] to select')
143
+ expect(output).not_to include('Press Q to exit')
144
+ end
145
+ end