rubocop-capybara 2.23.0 → 3.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +1 -1
- data/config/default.yml +97 -32
- data/lib/rubocop/capybara/plugin.rb +2 -3
- data/lib/rubocop/capybara/version.rb +1 -1
- data/lib/rubocop/cop/capybara/absolute_x_path_in_element_scope.rb +104 -0
- data/lib/rubocop/cop/capybara/always_true_count_query.rb +75 -0
- data/lib/rubocop/cop/capybara/ambiguous_click.rb +1 -1
- data/lib/rubocop/cop/capybara/assert_style.rb +1 -1
- data/lib/rubocop/cop/capybara/chained_find.rb +175 -0
- data/lib/rubocop/cop/capybara/find_all_first.rb +13 -41
- data/lib/rubocop/cop/capybara/ignored_query_option.rb +242 -0
- data/lib/rubocop/cop/capybara/mixin/capybara_help.rb +86 -2
- data/lib/rubocop/cop/capybara/mixin/css_attributes_parser.rb +23 -5
- data/lib/rubocop/cop/capybara/mixin/css_selector.rb +13 -10
- data/lib/rubocop/cop/capybara/mixin/query_methods.rb +120 -0
- data/lib/rubocop/cop/capybara/mixin/visibility.rb +53 -0
- data/lib/rubocop/cop/capybara/modal_method_without_block.rb +37 -0
- data/lib/rubocop/cop/capybara/native_attribute.rb +76 -0
- data/lib/rubocop/cop/capybara/redundant_within_find.rb +10 -21
- data/lib/rubocop/cop/capybara/rspec/current_path_expectation.rb +13 -16
- data/lib/rubocop/cop/capybara/rspec/current_url_expectation.rb +122 -0
- data/lib/rubocop/cop/capybara/rspec/find_element_value_eq.rb +173 -0
- data/lib/rubocop/cop/capybara/rspec/have_content.rb +1 -1
- data/lib/rubocop/cop/capybara/rspec/have_selector.rb +26 -15
- data/lib/rubocop/cop/capybara/rspec/match_style.rb +1 -1
- data/lib/rubocop/cop/capybara/rspec/negation_matcher.rb +1 -1
- data/lib/rubocop/cop/capybara/rspec/negation_matcher_after_visit.rb +2 -2
- data/lib/rubocop/cop/capybara/rspec/predicate_matcher.rb +43 -7
- data/lib/rubocop/cop/capybara/rspec/specific_matcher.rb +8 -2
- data/lib/rubocop/cop/capybara/rspec/visibility_matcher.rb +29 -24
- data/lib/rubocop/cop/capybara/rspec.rb +25 -0
- data/lib/rubocop/cop/capybara/specific_actions.rb +12 -7
- data/lib/rubocop/cop/capybara/specific_finders.rb +149 -28
- data/lib/rubocop/cop/capybara/unordered_window_access.rb +40 -0
- data/lib/rubocop/cop/capybara/visibility_option.rb +64 -0
- data/lib/rubocop/cop/capybara.rb +28 -0
- data/lib/rubocop/cop/capybara_cops.rb +4 -17
- data/lib/rubocop-capybara.rb +7 -5
- metadata +18 -6
- data/config/obsoletion.yml +0 -18
- data/lib/rubocop/cop/capybara/click_link_or_button_style.rb +0 -87
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
module RSpec
|
|
7
|
+
# Checks for expectations on the value of an element found by Capybara.
|
|
8
|
+
#
|
|
9
|
+
# @safety
|
|
10
|
+
# This cop's autocorrection is unsafe because `find('input').value`
|
|
11
|
+
# checks the value of the first matched element, while `have_field`
|
|
12
|
+
# checks that any field on the page has the expected value.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
# expect(find('input').value).to eq('foobar')
|
|
17
|
+
# expect(find(:css, 'input').value).to eq('foobar')
|
|
18
|
+
# expect(find_field('Email').value).to eq('user@example.com')
|
|
19
|
+
#
|
|
20
|
+
# # good
|
|
21
|
+
# expect(page).to have_field(with: 'foobar')
|
|
22
|
+
# expect(page).to have_field('Email', with: 'user@example.com')
|
|
23
|
+
#
|
|
24
|
+
class FindElementValueEq < RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
|
|
25
|
+
extend AutoCorrector
|
|
26
|
+
|
|
27
|
+
MSG = 'Prefer `have_field` over checking `find(...).value`.'
|
|
28
|
+
RESTRICT_ON_SEND = %i[expect].freeze
|
|
29
|
+
FIELD_ELEMENT_SELECTORS = %w[input textarea].freeze
|
|
30
|
+
|
|
31
|
+
# @!method find_element_value_eq(node)
|
|
32
|
+
def_node_matcher :find_element_value_eq, <<~PATTERN
|
|
33
|
+
(send
|
|
34
|
+
(send nil? :expect (send $_ :value))
|
|
35
|
+
${:to :to_not :not_to}
|
|
36
|
+
$(send nil? :eq $_)
|
|
37
|
+
)
|
|
38
|
+
PATTERN
|
|
39
|
+
|
|
40
|
+
def on_send(node)
|
|
41
|
+
find_element_value_eq(node.parent) do |finder, to_symbol,
|
|
42
|
+
matcher, expected|
|
|
43
|
+
register_offense(node, finder, to_symbol, matcher, expected)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
alias on_csend on_send
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def register_offense(node, finder, to_symbol, matcher, expected)
|
|
51
|
+
replacement = replacement_for_finder(finder)
|
|
52
|
+
return unless replacement
|
|
53
|
+
|
|
54
|
+
expect_target, field_locator = replacement
|
|
55
|
+
matcher_replacement =
|
|
56
|
+
replacement_matcher(to_symbol, expected, field_locator)
|
|
57
|
+
|
|
58
|
+
return add_offense(node.loc.selector) unless matcher_replacement
|
|
59
|
+
|
|
60
|
+
add_offense(node.loc.selector) do |corrector|
|
|
61
|
+
autocorrect(corrector, node, matcher, expect_target,
|
|
62
|
+
matcher_replacement)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def replacement_for_finder(node)
|
|
67
|
+
return unless node&.send_type?
|
|
68
|
+
|
|
69
|
+
case node.method_name
|
|
70
|
+
when :find
|
|
71
|
+
replacement_for_find(node)
|
|
72
|
+
when :find_field
|
|
73
|
+
replacement_for_find_field(node)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def replacement_for_find(node)
|
|
78
|
+
if (field_locator = field_selector_locator(node.arguments))
|
|
79
|
+
return [expect_target(node), field_locator]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
return unless field_element_selector?(node.arguments)
|
|
83
|
+
|
|
84
|
+
[expect_target(node), nil]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def replacement_for_find_field(node)
|
|
88
|
+
return unless node.arguments.one?
|
|
89
|
+
|
|
90
|
+
[expect_target(node), node.first_argument]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def field_element_selector?(arguments)
|
|
94
|
+
return field_element_name?(arguments.first) if arguments.one?
|
|
95
|
+
|
|
96
|
+
arguments.length == 2 &&
|
|
97
|
+
arguments.first.sym_type? &&
|
|
98
|
+
arguments.first.value == :css &&
|
|
99
|
+
field_element_name?(arguments.last)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def field_selector_locator(arguments)
|
|
103
|
+
return unless arguments.length == 2
|
|
104
|
+
|
|
105
|
+
selector_type, locator = arguments
|
|
106
|
+
return unless field_selector?(selector_type)
|
|
107
|
+
|
|
108
|
+
locator
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def field_selector?(node)
|
|
112
|
+
node.sym_type? && node.value == :field
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def field_element_name?(node)
|
|
116
|
+
node.str_type? && FIELD_ELEMENT_SELECTORS.include?(node.value)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def expect_target(node)
|
|
120
|
+
node.receiver&.source || 'page'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def autocorrect(corrector, node, matcher, expect_target,
|
|
124
|
+
matcher_replacement)
|
|
125
|
+
corrector.replace(node.first_argument, expect_target)
|
|
126
|
+
corrector.replace(node.parent.loc.selector, 'to')
|
|
127
|
+
corrector.replace(matcher, matcher_replacement)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def replacement_matcher(to_symbol, expected, field_locator)
|
|
131
|
+
expected_value = replacement_value(expected)
|
|
132
|
+
return unless expected_value
|
|
133
|
+
|
|
134
|
+
matcher = to_symbol == :to ? 'have_field' : 'have_no_field'
|
|
135
|
+
args = ["with: #{expected_value}"]
|
|
136
|
+
args.unshift(replacement_locator(field_locator)) if field_locator
|
|
137
|
+
|
|
138
|
+
"#{matcher}(#{args.join(', ')})"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def replacement_locator(locator)
|
|
142
|
+
return locator.source if locator.hash_type? && !locator.braces?
|
|
143
|
+
|
|
144
|
+
replacement_value(locator)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def replacement_value(expected)
|
|
148
|
+
return if unsafe_expected_value?(expected)
|
|
149
|
+
|
|
150
|
+
if expected.hash_type? && !expected.braces?
|
|
151
|
+
return "{#{expected.source}}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if method_call_with_no_parentheses?(expected)
|
|
155
|
+
return "(#{expected.source})"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
expected.source
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def unsafe_expected_value?(node)
|
|
162
|
+
node.splat_type? ||
|
|
163
|
+
(node.hash_type? && node.children.any?(&:kwsplat_type?))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def method_call_with_no_parentheses?(node)
|
|
167
|
+
node.send_type? && node.arguments? && !node.parenthesized?
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -4,20 +4,24 @@ module RuboCop
|
|
|
4
4
|
module Cop
|
|
5
5
|
module Capybara
|
|
6
6
|
module RSpec
|
|
7
|
-
# Use
|
|
7
|
+
# Use CSS or XPath matchers instead of generic selector matchers.
|
|
8
8
|
#
|
|
9
9
|
# @example
|
|
10
10
|
# # bad
|
|
11
11
|
# expect(foo).to have_selector(:css, 'bar')
|
|
12
|
+
# expect(foo).to have_no_selector(:css, 'bar')
|
|
12
13
|
#
|
|
13
14
|
# # good
|
|
14
15
|
# expect(foo).to have_css('bar')
|
|
16
|
+
# expect(foo).to have_no_css('bar')
|
|
15
17
|
#
|
|
16
18
|
# # bad
|
|
17
19
|
# expect(foo).to have_selector(:xpath, 'bar')
|
|
20
|
+
# expect(foo).to have_no_selector(:xpath, 'bar')
|
|
18
21
|
#
|
|
19
22
|
# # good
|
|
20
23
|
# expect(foo).to have_xpath('bar')
|
|
24
|
+
# expect(foo).to have_no_xpath('bar')
|
|
21
25
|
#
|
|
22
26
|
# @example DefaultSelector: css (default)
|
|
23
27
|
# # bad
|
|
@@ -33,12 +37,12 @@ module RuboCop
|
|
|
33
37
|
# # good
|
|
34
38
|
# expect(foo).to have_xpath('bar')
|
|
35
39
|
#
|
|
36
|
-
class HaveSelector <
|
|
40
|
+
class HaveSelector < RuboCop::Cop::Base
|
|
37
41
|
extend AutoCorrector
|
|
38
42
|
include RangeHelp
|
|
39
43
|
|
|
40
|
-
MSG = 'Use `%<good>s` instead of
|
|
41
|
-
RESTRICT_ON_SEND = %i[have_selector].freeze
|
|
44
|
+
MSG = 'Use `%<good>s` instead of `%<bad>s`.'
|
|
45
|
+
RESTRICT_ON_SEND = %i[have_selector have_no_selector].freeze
|
|
42
46
|
SELECTORS = %i[css xpath].freeze
|
|
43
47
|
|
|
44
48
|
def on_send(node)
|
|
@@ -54,33 +58,40 @@ module RuboCop
|
|
|
54
58
|
return unless SELECTORS.include?(type.value)
|
|
55
59
|
return unless (locator = node.arguments[1])
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
replacement = replacement(node, type.value)
|
|
62
|
+
add_offense(node,
|
|
63
|
+
message: message(node, replacement)) do |corrector|
|
|
58
64
|
corrector.remove(deletion_range(type, locator))
|
|
59
|
-
corrector.replace(node.loc.selector,
|
|
65
|
+
corrector.replace(node.loc.selector, replacement)
|
|
60
66
|
end
|
|
61
67
|
end
|
|
62
68
|
|
|
63
|
-
def message_typed(type)
|
|
64
|
-
format(MSG, good: "have_#{type.value}")
|
|
65
|
-
end
|
|
66
|
-
|
|
67
69
|
def deletion_range(first_argument, second_argument)
|
|
68
70
|
range_between(first_argument.source_range.begin_pos,
|
|
69
71
|
second_argument.source_range.begin_pos)
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def on_select_without_type(node)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
return unless (selector = default_selector)
|
|
76
|
+
|
|
77
|
+
replacement = replacement(node, selector)
|
|
78
|
+
add_offense(node,
|
|
79
|
+
message: message(node, replacement)) do |corrector|
|
|
80
|
+
corrector.replace(node.loc.selector, replacement)
|
|
75
81
|
end
|
|
76
82
|
end
|
|
77
83
|
|
|
78
|
-
def
|
|
79
|
-
|
|
84
|
+
def replacement(node, selector)
|
|
85
|
+
node.method_name.to_s.sub('selector', selector.to_s)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def message(node, replacement)
|
|
89
|
+
format(MSG, good: replacement, bad: node.method_name)
|
|
80
90
|
end
|
|
81
91
|
|
|
82
92
|
def default_selector
|
|
83
|
-
cop_config
|
|
93
|
+
selector = cop_config['DefaultSelector'].to_s.to_sym
|
|
94
|
+
selector if SELECTORS.include?(selector)
|
|
84
95
|
end
|
|
85
96
|
end
|
|
86
97
|
end
|
|
@@ -24,7 +24,7 @@ module RuboCop
|
|
|
24
24
|
# expect(page).not_to have_selector 'a'
|
|
25
25
|
# expect(page).not_to have_css('a')
|
|
26
26
|
#
|
|
27
|
-
class NegationMatcher <
|
|
27
|
+
class NegationMatcher < RuboCop::Cop::Base
|
|
28
28
|
extend AutoCorrector
|
|
29
29
|
include ConfigurableEnforcedStyle
|
|
30
30
|
include CapybaraHelp
|
|
@@ -27,7 +27,7 @@ module RuboCop
|
|
|
27
27
|
# expect(page).to have_css('a')
|
|
28
28
|
# expect(page).not_to have_link('bar')
|
|
29
29
|
#
|
|
30
|
-
class NegationMatcherAfterVisit <
|
|
30
|
+
class NegationMatcherAfterVisit < RuboCop::Cop::Base
|
|
31
31
|
include CapybaraHelp
|
|
32
32
|
|
|
33
33
|
MSG = 'Do not use negation matcher immediately after visit.'
|
|
@@ -37,7 +37,7 @@ module RuboCop
|
|
|
37
37
|
def_node_matcher :negation_matcher?, <<~PATTERN
|
|
38
38
|
{
|
|
39
39
|
(send (send nil? :expect _) :to (send nil? %NEGATIVE_MATCHERS ...))
|
|
40
|
-
(send (send nil? :expect _) :not_to (send nil? %POSITIVE_MATCHERS ...))
|
|
40
|
+
(send (send nil? :expect _) {:not_to :to_not} (send nil? %POSITIVE_MATCHERS ...))
|
|
41
41
|
}
|
|
42
42
|
PATTERN
|
|
43
43
|
|
|
@@ -9,7 +9,7 @@ module RuboCop
|
|
|
9
9
|
extend NodePattern::Macros
|
|
10
10
|
|
|
11
11
|
EXPLICIT_MATCHER = %w[css selector style xpath].map do |suffix|
|
|
12
|
-
"matches_#{suffix}?"
|
|
12
|
+
:"matches_#{suffix}?"
|
|
13
13
|
end.freeze
|
|
14
14
|
MSG_INFLECTED = 'Prefer using `%<matcher_name>s` matcher over ' \
|
|
15
15
|
'`%<predicate_name>s`.'
|
|
@@ -19,6 +19,8 @@ module RuboCop
|
|
|
19
19
|
def check_inflected(node)
|
|
20
20
|
predicate_in_actual?(node) do |predicate, to, matcher|
|
|
21
21
|
msg = message_inflected(predicate)
|
|
22
|
+
next add_offense(node, message: msg) if predicate.block_node
|
|
23
|
+
|
|
22
24
|
add_offense(node, message: msg) do |corrector|
|
|
23
25
|
remove_predicate(corrector, predicate)
|
|
24
26
|
corrector.replace(node.loc.selector,
|
|
@@ -93,7 +95,7 @@ module RuboCop
|
|
|
93
95
|
|
|
94
96
|
def true?(to_symbol, matcher)
|
|
95
97
|
result = case matcher.method_name
|
|
96
|
-
when :be, :eq
|
|
98
|
+
when :be, :eq, :eql, :equal
|
|
97
99
|
matcher.first_argument.true_type?
|
|
98
100
|
when :be_truthy, :a_truthy_value
|
|
99
101
|
true
|
|
@@ -118,6 +120,12 @@ module RuboCop
|
|
|
118
120
|
INFLECTED_MATCHER = %w[css selector style xpath].each.map do |suffix|
|
|
119
121
|
"match_#{suffix}"
|
|
120
122
|
end.freeze
|
|
123
|
+
SAFE_RECEIVER_TYPES = Set.new(
|
|
124
|
+
%i[
|
|
125
|
+
array complex const cvar dstr false float gvar hash int ivar
|
|
126
|
+
lvar nil rational regexp self str sym true xstr
|
|
127
|
+
]
|
|
128
|
+
).freeze
|
|
121
129
|
|
|
122
130
|
private
|
|
123
131
|
|
|
@@ -127,8 +135,10 @@ module RuboCop
|
|
|
127
135
|
|
|
128
136
|
def check_explicit(node) # rubocop:disable Metrics/MethodLength
|
|
129
137
|
predicate_matcher?(node) do |actual, matcher|
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
msg = message_explicit(matcher)
|
|
139
|
+
next add_offense(node, message: msg) if matcher.block_node
|
|
140
|
+
|
|
141
|
+
add_offense(node, message: msg) do |corrector|
|
|
132
142
|
corrector_explicit(corrector, node, actual, matcher)
|
|
133
143
|
end
|
|
134
144
|
end
|
|
@@ -169,14 +179,40 @@ module RuboCop
|
|
|
169
179
|
def move_predicate(corrector, actual, matcher)
|
|
170
180
|
predicate = to_predicate_method(matcher.method_name)
|
|
171
181
|
args = args_loc(matcher).source
|
|
172
|
-
|
|
173
|
-
|
|
182
|
+
replacement = ".#{predicate}#{args}"
|
|
183
|
+
|
|
184
|
+
if actual_requires_parentheses?(actual)
|
|
185
|
+
corrector.replace(actual, "(#{actual.source})#{replacement}")
|
|
186
|
+
else
|
|
187
|
+
corrector.insert_after(actual, replacement)
|
|
188
|
+
end
|
|
174
189
|
end
|
|
175
190
|
|
|
176
191
|
def to_predicate_method(matcher)
|
|
177
192
|
"#{matcher.to_s.sub('match_', 'matches_')}?"
|
|
178
193
|
end
|
|
179
194
|
|
|
195
|
+
def actual_requires_parentheses?(actual)
|
|
196
|
+
return false if actual.begin_type?
|
|
197
|
+
|
|
198
|
+
if actual.call_type?
|
|
199
|
+
return actual_send_requires_parentheses?(actual)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
!safe_receiver?(actual)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def actual_send_requires_parentheses?(actual)
|
|
206
|
+
return false if actual.method?(:[])
|
|
207
|
+
|
|
208
|
+
actual.operator_method? ||
|
|
209
|
+
(actual.arguments? && !actual.parenthesized?)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def safe_receiver?(actual)
|
|
213
|
+
SAFE_RECEIVER_TYPES.include?(actual.type)
|
|
214
|
+
end
|
|
215
|
+
|
|
180
216
|
# rubocop:disable Metrics/MethodLength
|
|
181
217
|
def replacement_matcher(node)
|
|
182
218
|
case [cop_config['Strict'], node.method?(:to)]
|
|
@@ -241,7 +277,7 @@ module RuboCop
|
|
|
241
277
|
# # good - the above code is rewritten to it by this cop
|
|
242
278
|
# expect(foo.matches_style?(bar: 'baz')).to be_truthy
|
|
243
279
|
#
|
|
244
|
-
class PredicateMatcher <
|
|
280
|
+
class PredicateMatcher < RuboCop::Cop::Base
|
|
245
281
|
extend AutoCorrector
|
|
246
282
|
include ConfigurableEnforcedStyle
|
|
247
283
|
include InflectedHelper
|
|
@@ -26,7 +26,7 @@ module RuboCop
|
|
|
26
26
|
# expect(page).to have_select
|
|
27
27
|
# expect(page).to have_field(with: 'foo')
|
|
28
28
|
#
|
|
29
|
-
class SpecificMatcher <
|
|
29
|
+
class SpecificMatcher < RuboCop::Cop::Base
|
|
30
30
|
MSG = 'Prefer `%<good_matcher>s` over `%<bad_matcher>s`.'
|
|
31
31
|
RESTRICT_ON_SEND = %i[have_selector have_no_selector have_css
|
|
32
32
|
have_no_css].freeze
|
|
@@ -37,6 +37,7 @@ module RuboCop
|
|
|
37
37
|
'select' => 'select',
|
|
38
38
|
'input' => 'field'
|
|
39
39
|
}.freeze
|
|
40
|
+
EXCLUDED_FIELD_TYPES = %w[hidden image submit].freeze
|
|
40
41
|
|
|
41
42
|
# @!method first_argument(node)
|
|
42
43
|
def_node_matcher :first_argument, <<~PATTERN
|
|
@@ -61,11 +62,16 @@ module RuboCop
|
|
|
61
62
|
private
|
|
62
63
|
|
|
63
64
|
def specific_matcher(arg)
|
|
64
|
-
splitted_arg = arg[
|
|
65
|
+
splitted_arg = arg[/^[\w-]+/, 0]
|
|
65
66
|
SPECIFIC_MATCHER[splitted_arg]
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
def replaceable?(node, arg, matcher)
|
|
70
|
+
if matcher == 'field'
|
|
71
|
+
type = CssSelector.attributes(arg)['type'].to_s.downcase
|
|
72
|
+
return false if EXCLUDED_FIELD_TYPES.include?(type)
|
|
73
|
+
end
|
|
74
|
+
|
|
69
75
|
replaceable_attributes?(arg) &&
|
|
70
76
|
!text_with_regexp?(node) &&
|
|
71
77
|
CapybaraHelp.replaceable_option?(node, arg, matcher) &&
|
|
@@ -4,7 +4,7 @@ module RuboCop
|
|
|
4
4
|
module Cop
|
|
5
5
|
module Capybara
|
|
6
6
|
module RSpec
|
|
7
|
-
# Checks for boolean visibility in Capybara
|
|
7
|
+
# Checks for boolean visibility in Capybara RSpec matchers.
|
|
8
8
|
#
|
|
9
9
|
# Capybara lets you find elements that match a certain visibility using
|
|
10
10
|
# the `:visible` option. `:visible` accepts both boolean and symbols as
|
|
@@ -12,12 +12,19 @@ module RuboCop
|
|
|
12
12
|
# false` does not find just invisible elements, but both visible and
|
|
13
13
|
# invisible elements. For expressiveness and clarity, use one of the
|
|
14
14
|
# symbol values, `:all`, `:hidden` or `:visible`.
|
|
15
|
+
# Only `true` is autocorrected, to `:visible`.
|
|
16
|
+
# Set `DefaultSelector` to match `Capybara.default_selector`, or to
|
|
17
|
+
# `null` if it varies at runtime.
|
|
15
18
|
# Read more at: https://www.rubydoc.info/gems/capybara/Capybara%2FNode%2FFinders:all
|
|
16
19
|
#
|
|
20
|
+
# @safety
|
|
21
|
+
# This cop's autocorrection is unsafe because custom filters on
|
|
22
|
+
# built-in selectors may treat `true` and `:visible` differently.
|
|
23
|
+
#
|
|
17
24
|
# @example
|
|
18
25
|
# # bad
|
|
19
|
-
# expect(page).to have_selector('.foo', visible:
|
|
20
|
-
# expect(page).to have_css('.foo', visible:
|
|
26
|
+
# expect(page).to have_selector('.foo', visible: true)
|
|
27
|
+
# expect(page).to have_css('.foo', visible: false)
|
|
21
28
|
# expect(page).to have_link('my link', visible: false)
|
|
22
29
|
#
|
|
23
30
|
# # good
|
|
@@ -25,25 +32,11 @@ module RuboCop
|
|
|
25
32
|
# expect(page).to have_css('.foo', visible: :all)
|
|
26
33
|
# expect(page).to have_link('my link', visible: :hidden)
|
|
27
34
|
#
|
|
28
|
-
class VisibilityMatcher <
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
CAPYBARA_MATCHER_METHODS = %w[
|
|
32
|
-
button
|
|
33
|
-
checked_field
|
|
34
|
-
css
|
|
35
|
-
field
|
|
36
|
-
link
|
|
37
|
-
select
|
|
38
|
-
selector
|
|
39
|
-
table
|
|
40
|
-
unchecked_field
|
|
41
|
-
xpath
|
|
42
|
-
].flat_map do |element|
|
|
43
|
-
["have_#{element}".to_sym, "have_no_#{element}".to_sym]
|
|
44
|
-
end
|
|
35
|
+
class VisibilityMatcher < RuboCop::Cop::Base
|
|
36
|
+
extend AutoCorrector
|
|
37
|
+
include Visibility
|
|
45
38
|
|
|
46
|
-
RESTRICT_ON_SEND =
|
|
39
|
+
RESTRICT_ON_SEND = CapybaraHelp::VISIBILITY_MATCHER_METHODS
|
|
47
40
|
|
|
48
41
|
# @!method visible_true?(node)
|
|
49
42
|
def_node_matcher :visible_true?, <<~PATTERN
|
|
@@ -56,14 +49,26 @@ module RuboCop
|
|
|
56
49
|
PATTERN
|
|
57
50
|
|
|
58
51
|
def on_send(node)
|
|
59
|
-
visible_false?(node) { |arg|
|
|
60
|
-
visible_true?(node)
|
|
52
|
+
visible_false?(node) { |arg| check_visibility(arg) }
|
|
53
|
+
visible_true?(node) do |arg|
|
|
54
|
+
check_visibility(arg) { autocorrectable?(node, arg) }
|
|
55
|
+
end
|
|
61
56
|
end
|
|
62
57
|
|
|
63
58
|
private
|
|
64
59
|
|
|
65
60
|
def capybara_matcher?(method_name)
|
|
66
|
-
|
|
61
|
+
RESTRICT_ON_SEND.include? method_name
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def autocorrectable?(node, pair)
|
|
65
|
+
if QueryMethods.selector(node.method_name) == :selector
|
|
66
|
+
selector = node.first_argument
|
|
67
|
+
# An unknown argument may be a custom selector name.
|
|
68
|
+
return false unless selector.type?(:hash, :sym, :str, :dstr)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
supported_visibility_query?(node, pair.parent)
|
|
67
72
|
end
|
|
68
73
|
end
|
|
69
74
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
# Cops for the `Capybara/RSpec` department. The department's cops are
|
|
7
|
+
# registered for lazy loading and their files are loaded on demand.
|
|
8
|
+
module RSpec
|
|
9
|
+
extend LazyLoader
|
|
10
|
+
|
|
11
|
+
register_cop :CurrentPathExpectation, "#{__dir__}/rspec/current_path_expectation"
|
|
12
|
+
register_cop :CurrentUrlExpectation, "#{__dir__}/rspec/current_url_expectation"
|
|
13
|
+
register_cop :FindElementValueEq, "#{__dir__}/rspec/find_element_value_eq"
|
|
14
|
+
register_cop :HaveContent, "#{__dir__}/rspec/have_content"
|
|
15
|
+
register_cop :HaveSelector, "#{__dir__}/rspec/have_selector"
|
|
16
|
+
register_cop :MatchStyle, "#{__dir__}/rspec/match_style"
|
|
17
|
+
register_cop :NegationMatcher, "#{__dir__}/rspec/negation_matcher"
|
|
18
|
+
register_cop :NegationMatcherAfterVisit, "#{__dir__}/rspec/negation_matcher_after_visit"
|
|
19
|
+
register_cop :PredicateMatcher, "#{__dir__}/rspec/predicate_matcher"
|
|
20
|
+
register_cop :SpecificMatcher, "#{__dir__}/rspec/specific_matcher"
|
|
21
|
+
register_cop :VisibilityMatcher, "#{__dir__}/rspec/visibility_matcher"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -8,18 +8,18 @@ module RuboCop
|
|
|
8
8
|
# @example
|
|
9
9
|
#
|
|
10
10
|
# # bad
|
|
11
|
-
# find('a').click
|
|
11
|
+
# find('a[href="/home"]').click
|
|
12
12
|
# find('button.cls').click
|
|
13
|
-
# find('a', exact_text: 'foo').click
|
|
13
|
+
# find('a[href="/home"]', exact_text: 'foo').click
|
|
14
14
|
# find('div button').click
|
|
15
15
|
#
|
|
16
16
|
# # good
|
|
17
|
-
# click_link
|
|
17
|
+
# click_link(href: '/home')
|
|
18
18
|
# click_button(class: 'cls')
|
|
19
|
-
# click_link(exact_text: 'foo')
|
|
19
|
+
# click_link(href: '/home', exact_text: 'foo')
|
|
20
20
|
# find('div').click_button
|
|
21
21
|
#
|
|
22
|
-
class SpecificActions <
|
|
22
|
+
class SpecificActions < RuboCop::Cop::Base
|
|
23
23
|
MSG = "Prefer `%<good_action>s` over `find('%<selector>s').click`."
|
|
24
24
|
RESTRICT_ON_SEND = %i[click].freeze
|
|
25
25
|
SPECIFIC_ACTION = {
|
|
@@ -33,6 +33,8 @@ module RuboCop
|
|
|
33
33
|
PATTERN
|
|
34
34
|
|
|
35
35
|
def on_send(node)
|
|
36
|
+
return if node.arguments? || node.block_node
|
|
37
|
+
|
|
36
38
|
click_on_selector(node.receiver) do |arg|
|
|
37
39
|
next unless supported_selector?(arg)
|
|
38
40
|
# Always check the last selector in the case of multiple selectors
|
|
@@ -51,7 +53,7 @@ module RuboCop
|
|
|
51
53
|
private
|
|
52
54
|
|
|
53
55
|
def specific_action(selector)
|
|
54
|
-
SPECIFIC_ACTION[
|
|
56
|
+
SPECIFIC_ACTION[selector]
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
def replaceable?(node, arg, action)
|
|
@@ -71,7 +73,10 @@ module RuboCop
|
|
|
71
73
|
end
|
|
72
74
|
|
|
73
75
|
def last_selector(arg)
|
|
74
|
-
arg.
|
|
76
|
+
without_attributes = arg.gsub(/\[(?:'[^']*'|"[^"]*"|[^\]])*\]/) do |m|
|
|
77
|
+
m.gsub(/\s/, '')
|
|
78
|
+
end
|
|
79
|
+
without_attributes.split.last&.[](/^[\w-]+/, 0)
|
|
75
80
|
end
|
|
76
81
|
|
|
77
82
|
def offense_range(node, receiver)
|