rubocop-capybara 3.0.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 +47 -0
- data/config/default.yml +71 -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 -16
- data/lib/rubocop-capybara.rb +7 -2
- metadata +18 -4
|
@@ -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)
|
|
@@ -3,6 +3,43 @@
|
|
|
3
3
|
module RuboCop
|
|
4
4
|
module Cop
|
|
5
5
|
module Capybara
|
|
6
|
+
# Help methods for SpecificFinders.
|
|
7
|
+
# @api private
|
|
8
|
+
module SpecificFindersHelp
|
|
9
|
+
MESSAGE = 'Prefer `%<good_finder>s` over `find`.'
|
|
10
|
+
FIELD_OPTIONS = CapybaraHelp::SPECIFIC_OPTIONS['field']
|
|
11
|
+
FIELD_SELECTOR_PATTERN = /\Ainput(?:\[.+\])+\z/.freeze
|
|
12
|
+
UNSAFE_FIELD_TYPES = %w[hidden image submit].freeze
|
|
13
|
+
FIELD_STATE_OPTIONS = %w[checked disabled multiple readonly].freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def field_selector?(arg)
|
|
18
|
+
return false unless FIELD_SELECTOR_PATTERN.match?(arg)
|
|
19
|
+
|
|
20
|
+
attrs = CssSelector.attributes(arg)
|
|
21
|
+
type = attrs['type'].to_s.downcase
|
|
22
|
+
return false if UNSAFE_FIELD_TYPES.include?(type)
|
|
23
|
+
return false if (attrs.keys & FIELD_STATE_OPTIONS).any?
|
|
24
|
+
|
|
25
|
+
CapybaraHelp.replaceable_attributes?(attrs) &&
|
|
26
|
+
attrs.keys.all? { |attr| FIELD_OPTIONS.include?(attr) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def options(attrs)
|
|
30
|
+
attrs.filter_map { |key, value| "#{key}: #{yield(value)}" }.join(', ')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def unsupported_selector?(arg)
|
|
34
|
+
CssSelector.pseudo_classes(arg).any? ||
|
|
35
|
+
CssSelector.multiple_selectors?(arg)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def message(finder)
|
|
39
|
+
format(MESSAGE, good_finder: finder)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
6
43
|
# Checks if there is a more specific finder offered by Capybara.
|
|
7
44
|
#
|
|
8
45
|
# @example
|
|
@@ -11,43 +48,90 @@ module RuboCop
|
|
|
11
48
|
# find('[id=some-id]')
|
|
12
49
|
# find(:css, '#some-id')
|
|
13
50
|
# find(:id, 'some-id')
|
|
51
|
+
# find(:link, 'Home')
|
|
52
|
+
# find(:field, 'Name')
|
|
53
|
+
# find('input[placeholder="Email"]')
|
|
54
|
+
# find(:css, 'input[type="checkbox"]')
|
|
14
55
|
#
|
|
15
56
|
# # good
|
|
16
57
|
# find_by_id('some-id')
|
|
58
|
+
# find_link('Home')
|
|
59
|
+
# find_field('Name')
|
|
60
|
+
# find_field(placeholder: 'Email')
|
|
61
|
+
# find_field(type: 'checkbox')
|
|
17
62
|
#
|
|
18
|
-
class SpecificFinders <
|
|
63
|
+
class SpecificFinders < RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
|
|
19
64
|
extend AutoCorrector
|
|
20
65
|
include RangeHelp
|
|
21
66
|
|
|
22
|
-
MSG = 'Prefer `
|
|
67
|
+
MSG = 'Prefer `%<replacement>s` over `find`.'
|
|
23
68
|
RESTRICT_ON_SEND = %i[find].freeze
|
|
24
69
|
|
|
25
70
|
# @!method find_argument(node)
|
|
26
71
|
def_node_matcher :find_argument, <<~PATTERN
|
|
27
|
-
(send _ :find $(sym {:css :id})? (str $_) ...)
|
|
72
|
+
(send _ :find $(sym {:css :id :link :field})? (str $_) ...)
|
|
28
73
|
PATTERN
|
|
29
74
|
|
|
30
|
-
# @!method
|
|
31
|
-
|
|
32
|
-
(pair (sym :class) $_
|
|
75
|
+
# @!method class_option(node)
|
|
76
|
+
def_node_matcher :class_option, <<~PATTERN
|
|
77
|
+
(send _ :find ... (hash <(pair (sym :class) $_) ...>))
|
|
33
78
|
PATTERN
|
|
34
79
|
|
|
35
80
|
def on_send(node)
|
|
36
81
|
find_argument(node) do |sym, arg|
|
|
37
|
-
|
|
38
|
-
next if CssSelector.multiple_selectors?(arg)
|
|
39
|
-
|
|
40
|
-
on_attr(node, sym, arg) if attribute?(arg)
|
|
41
|
-
on_id(node, sym, arg) if CssSelector.id?(arg)
|
|
42
|
-
on_sym_id(node, sym, arg) if sym.first&.value == :id
|
|
82
|
+
handle_find(node, sym, arg)
|
|
43
83
|
end
|
|
44
84
|
end
|
|
45
85
|
|
|
46
86
|
private
|
|
47
87
|
|
|
88
|
+
def selector?(sym)
|
|
89
|
+
%i[link field].include?(sym.first.value)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def id_symbol?(sym)
|
|
93
|
+
sym.first&.value == :id
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def selector_symbol?(sym)
|
|
97
|
+
sym.first && selector?(sym)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def on_sym_selector(node, sym)
|
|
101
|
+
replacement = "find_#{sym.first.value}"
|
|
102
|
+
register_selector_offense(node, replacement)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def register_selector_offense(node, replacement)
|
|
106
|
+
message = format(MSG, replacement: replacement)
|
|
107
|
+
add_offense(offense_range(node), message: message) do |corrector|
|
|
108
|
+
corrector.replace(node.loc.selector, replacement)
|
|
109
|
+
remove_selector_argument(corrector, node)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def remove_selector_argument(corrector, node)
|
|
114
|
+
range = range_between(node.first_argument.source_range.begin_pos,
|
|
115
|
+
node.arguments[1].source_range.begin_pos)
|
|
116
|
+
corrector.remove(range)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def handle_find(node, sym, arg)
|
|
120
|
+
return on_sym_id(node, sym, arg) if id_symbol?(sym)
|
|
121
|
+
return on_sym_selector(node, sym) if selector_symbol?(sym)
|
|
122
|
+
return if SpecificFindersHelp.unsupported_selector?(arg)
|
|
123
|
+
|
|
124
|
+
if SpecificFindersHelp.field_selector?(arg)
|
|
125
|
+
return on_field(node, sym, arg)
|
|
126
|
+
end
|
|
127
|
+
return on_attr(node, sym, arg) if attribute?(arg)
|
|
128
|
+
|
|
129
|
+
on_id(node, sym, arg) if CssSelector.id?(arg)
|
|
130
|
+
end
|
|
131
|
+
|
|
48
132
|
def on_attr(node, sym, arg)
|
|
49
133
|
attrs = CssSelector.attributes(arg)
|
|
50
|
-
return unless (id = attrs['id'])
|
|
134
|
+
return unless (id = attrs['id']&.to_s)
|
|
51
135
|
return if attrs['class']
|
|
52
136
|
|
|
53
137
|
register_offense(node, sym, replaced_arguments(arg, id))
|
|
@@ -57,12 +141,29 @@ module RuboCop
|
|
|
57
141
|
return if CssSelector.attributes(arg).any?
|
|
58
142
|
|
|
59
143
|
id = CssSelector.id(arg)
|
|
60
|
-
register_offense(node, sym,
|
|
144
|
+
register_offense(node, sym, ruby_literal(id.delete('\\')),
|
|
61
145
|
CssSelector.classes(arg.sub("##{id}", '')))
|
|
62
146
|
end
|
|
63
147
|
|
|
64
148
|
def on_sym_id(node, sym, id)
|
|
65
|
-
register_offense(node, sym,
|
|
149
|
+
register_offense(node, sym, ruby_literal(id))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def on_field(node, sym, arg)
|
|
153
|
+
add_offense(
|
|
154
|
+
offense_range(node),
|
|
155
|
+
message: SpecificFindersHelp.message('find_field')
|
|
156
|
+
) do |corrector|
|
|
157
|
+
corrector.replace(node.loc.selector, 'find_field')
|
|
158
|
+
corrector.replace(node.first_argument, field_options(arg))
|
|
159
|
+
corrector.remove(deletion_range(node)) unless sym.empty?
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def field_options(arg)
|
|
164
|
+
SpecificFindersHelp.options(CssSelector.attributes(arg)) do |value|
|
|
165
|
+
ruby_literal(value)
|
|
166
|
+
end
|
|
66
167
|
end
|
|
67
168
|
|
|
68
169
|
def attribute?(arg)
|
|
@@ -70,24 +171,36 @@ module RuboCop
|
|
|
70
171
|
CapybaraHelp.common_attributes?(arg)
|
|
71
172
|
end
|
|
72
173
|
|
|
73
|
-
def register_offense(node, sym,
|
|
74
|
-
|
|
174
|
+
def register_offense(node, sym, replacement, classes = [])
|
|
175
|
+
message = format(MSG, replacement: 'find_by_id')
|
|
176
|
+
add_offense(offense_range(node), message: message) do |corrector|
|
|
177
|
+
next unless autocorrectable_classes?(node, classes)
|
|
178
|
+
|
|
75
179
|
corrector.replace(node.loc.selector, 'find_by_id')
|
|
76
|
-
corrector.replace(node.first_argument,
|
|
77
|
-
|
|
78
|
-
autocorrect_classes(corrector, node, classes)
|
|
79
|
-
end
|
|
180
|
+
corrector.replace(node.first_argument, replacement)
|
|
181
|
+
autocorrect_id_classes(corrector, node, classes)
|
|
80
182
|
corrector.remove(deletion_range(node)) unless sym.empty?
|
|
81
183
|
end
|
|
82
184
|
end
|
|
83
185
|
|
|
186
|
+
def autocorrectable_classes?(node, classes)
|
|
187
|
+
return true if classes.empty? || !(options = class_option(node))
|
|
188
|
+
|
|
189
|
+
options.str_type? ||
|
|
190
|
+
(options.array_type? && options.values.all?(&:str_type?))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def autocorrect_id_classes(corrector, node, classes)
|
|
194
|
+
autocorrect_classes(corrector, node, classes) unless classes.empty?
|
|
195
|
+
end
|
|
196
|
+
|
|
84
197
|
def deletion_range(node)
|
|
85
198
|
range_between(node.first_argument.source_range.end_pos,
|
|
86
199
|
node.arguments[1].source_range.end_pos)
|
|
87
200
|
end
|
|
88
201
|
|
|
89
202
|
def autocorrect_classes(corrector, node, classes)
|
|
90
|
-
if (options =
|
|
203
|
+
if (options = class_option(node))
|
|
91
204
|
append_options(classes, options)
|
|
92
205
|
corrector.replace(options, classes.to_s)
|
|
93
206
|
else
|
|
@@ -108,6 +221,8 @@ module RuboCop
|
|
|
108
221
|
|
|
109
222
|
def replaced_arguments(arg, id)
|
|
110
223
|
options = to_options(CssSelector.attributes(arg))
|
|
224
|
+
id = ruby_literal(id.delete('\\'))
|
|
225
|
+
|
|
111
226
|
options.empty? ? id : "#{id}, #{options}"
|
|
112
227
|
end
|
|
113
228
|
|
|
@@ -115,20 +230,26 @@ module RuboCop
|
|
|
115
230
|
attrs.each.filter_map do |key, value|
|
|
116
231
|
next if key == 'id'
|
|
117
232
|
|
|
118
|
-
"#{key}: #{value}"
|
|
233
|
+
"#{key}: #{ruby_literal(value)}"
|
|
119
234
|
end.join(', ')
|
|
120
235
|
end
|
|
121
236
|
|
|
237
|
+
def ruby_literal(value)
|
|
238
|
+
case value
|
|
239
|
+
when String
|
|
240
|
+
"'#{value.gsub(/['\\]/) { |char| "\\#{char}" }}'"
|
|
241
|
+
when nil then 'nil'
|
|
242
|
+
else
|
|
243
|
+
value.to_s
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
122
247
|
def offense_range(node)
|
|
123
248
|
range_between(node.loc.selector.begin_pos, end_pos(node))
|
|
124
249
|
end
|
|
125
250
|
|
|
126
251
|
def end_pos(node)
|
|
127
|
-
|
|
128
|
-
node.loc.end.end_pos
|
|
129
|
-
else
|
|
130
|
-
node.source_range.end_pos
|
|
131
|
-
end
|
|
252
|
+
node.loc.end ? node.loc.end.end_pos : node.source_range.end_pos
|
|
132
253
|
end
|
|
133
254
|
end
|
|
134
255
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
# Checks positional access to Capybara windows, whose order is undefined.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# popup = windows.last
|
|
11
|
+
# original = page.windows.first
|
|
12
|
+
#
|
|
13
|
+
# # good
|
|
14
|
+
# popup = window_opened_by { click_link 'Open' }
|
|
15
|
+
# original = current_window
|
|
16
|
+
#
|
|
17
|
+
class UnorderedWindowAccess < RuboCop::Cop::Base
|
|
18
|
+
MSG = 'Selecting a window by position relies on undefined order. ' \
|
|
19
|
+
'Use `window_opened_by` or `current_window`.'
|
|
20
|
+
RESTRICT_ON_SEND = %i[first last [] at fetch].freeze
|
|
21
|
+
|
|
22
|
+
# @!method capybara_windows?(node)
|
|
23
|
+
def_node_matcher :capybara_windows?, <<~PATTERN
|
|
24
|
+
(call
|
|
25
|
+
{nil? self
|
|
26
|
+
(call {nil? self} :page)
|
|
27
|
+
(call (const {nil? cbase} :Capybara) :current_session)}
|
|
28
|
+
:windows)
|
|
29
|
+
PATTERN
|
|
30
|
+
|
|
31
|
+
def on_send(node)
|
|
32
|
+
return if node.method?(:[]) && node.first_argument&.type?(:range)
|
|
33
|
+
|
|
34
|
+
add_offense(node) if capybara_windows?(node.receiver)
|
|
35
|
+
end
|
|
36
|
+
alias on_csend on_send
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
# Checks for boolean values passed to Capybara's `visible` option.
|
|
7
|
+
#
|
|
8
|
+
# `visible: false` finds both visible and hidden elements, rather than
|
|
9
|
+
# only hidden elements. Use `:all`, `:hidden` or `:visible` instead.
|
|
10
|
+
# Only `true` is autocorrected, to `:visible`.
|
|
11
|
+
#
|
|
12
|
+
# This cop targets Capybara's built-in selectors. Set `DefaultSelector`
|
|
13
|
+
# to match `Capybara.default_selector`, or to `null` if it varies at
|
|
14
|
+
# runtime. Queries using custom or unknown selectors are skipped.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad
|
|
18
|
+
# find('.menu', visible: true)
|
|
19
|
+
# expect(page).to have_css('.menu', visible: false)
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# find('.menu', visible: :visible)
|
|
23
|
+
# find('.menu', visible: :all)
|
|
24
|
+
# find('.menu', visible: :hidden)
|
|
25
|
+
#
|
|
26
|
+
# @example DefaultSelector: custom
|
|
27
|
+
# # bad
|
|
28
|
+
# find(:css, '.menu', visible: true)
|
|
29
|
+
#
|
|
30
|
+
# # good
|
|
31
|
+
# find('.menu', visible: true)
|
|
32
|
+
#
|
|
33
|
+
class VisibilityOption < RuboCop::Cop::Base
|
|
34
|
+
extend AutoCorrector
|
|
35
|
+
include CapybaraHelp
|
|
36
|
+
include Visibility
|
|
37
|
+
|
|
38
|
+
RESTRICT_ON_SEND = QueryMethods.names(
|
|
39
|
+
:finder, :collection, :selector, :match, :action, :grouped
|
|
40
|
+
).difference(CapybaraHelp::VISIBILITY_MATCHER_METHODS).freeze
|
|
41
|
+
|
|
42
|
+
# @!method visibility_options(node)
|
|
43
|
+
def_node_matcher :visibility_options, <<~PATTERN
|
|
44
|
+
(call #capybara_receiver? _ <$(hash ...) ...>)
|
|
45
|
+
PATTERN
|
|
46
|
+
|
|
47
|
+
# @!method boolean_visibility?(node)
|
|
48
|
+
def_node_matcher :boolean_visibility?, <<~PATTERN
|
|
49
|
+
(pair (sym :visible) boolean)
|
|
50
|
+
PATTERN
|
|
51
|
+
|
|
52
|
+
def on_send(node)
|
|
53
|
+
return unless (options = visibility_options(node))
|
|
54
|
+
return unless supported_visibility_query?(node, options)
|
|
55
|
+
|
|
56
|
+
options.pairs.each do |pair|
|
|
57
|
+
check_visibility(pair) if boolean_visibility?(pair)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
alias on_csend on_send
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|