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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/README.md +1 -1
  4. data/config/default.yml +97 -32
  5. data/lib/rubocop/capybara/plugin.rb +2 -3
  6. data/lib/rubocop/capybara/version.rb +1 -1
  7. data/lib/rubocop/cop/capybara/absolute_x_path_in_element_scope.rb +104 -0
  8. data/lib/rubocop/cop/capybara/always_true_count_query.rb +75 -0
  9. data/lib/rubocop/cop/capybara/ambiguous_click.rb +1 -1
  10. data/lib/rubocop/cop/capybara/assert_style.rb +1 -1
  11. data/lib/rubocop/cop/capybara/chained_find.rb +175 -0
  12. data/lib/rubocop/cop/capybara/find_all_first.rb +13 -41
  13. data/lib/rubocop/cop/capybara/ignored_query_option.rb +242 -0
  14. data/lib/rubocop/cop/capybara/mixin/capybara_help.rb +86 -2
  15. data/lib/rubocop/cop/capybara/mixin/css_attributes_parser.rb +23 -5
  16. data/lib/rubocop/cop/capybara/mixin/css_selector.rb +13 -10
  17. data/lib/rubocop/cop/capybara/mixin/query_methods.rb +120 -0
  18. data/lib/rubocop/cop/capybara/mixin/visibility.rb +53 -0
  19. data/lib/rubocop/cop/capybara/modal_method_without_block.rb +37 -0
  20. data/lib/rubocop/cop/capybara/native_attribute.rb +76 -0
  21. data/lib/rubocop/cop/capybara/redundant_within_find.rb +10 -21
  22. data/lib/rubocop/cop/capybara/rspec/current_path_expectation.rb +13 -16
  23. data/lib/rubocop/cop/capybara/rspec/current_url_expectation.rb +122 -0
  24. data/lib/rubocop/cop/capybara/rspec/find_element_value_eq.rb +173 -0
  25. data/lib/rubocop/cop/capybara/rspec/have_content.rb +1 -1
  26. data/lib/rubocop/cop/capybara/rspec/have_selector.rb +26 -15
  27. data/lib/rubocop/cop/capybara/rspec/match_style.rb +1 -1
  28. data/lib/rubocop/cop/capybara/rspec/negation_matcher.rb +1 -1
  29. data/lib/rubocop/cop/capybara/rspec/negation_matcher_after_visit.rb +2 -2
  30. data/lib/rubocop/cop/capybara/rspec/predicate_matcher.rb +43 -7
  31. data/lib/rubocop/cop/capybara/rspec/specific_matcher.rb +8 -2
  32. data/lib/rubocop/cop/capybara/rspec/visibility_matcher.rb +29 -24
  33. data/lib/rubocop/cop/capybara/rspec.rb +25 -0
  34. data/lib/rubocop/cop/capybara/specific_actions.rb +12 -7
  35. data/lib/rubocop/cop/capybara/specific_finders.rb +149 -28
  36. data/lib/rubocop/cop/capybara/unordered_window_access.rb +40 -0
  37. data/lib/rubocop/cop/capybara/visibility_option.rb +64 -0
  38. data/lib/rubocop/cop/capybara.rb +28 -0
  39. data/lib/rubocop/cop/capybara_cops.rb +4 -17
  40. data/lib/rubocop-capybara.rb +7 -5
  41. metadata +18 -6
  42. data/config/obsoletion.yml +0 -18
  43. 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
@@ -19,7 +19,7 @@ module RuboCop
19
19
  # expect(page).to have_text('capy')
20
20
  # expect(page).to have_no_text('bara')
21
21
  #
22
- class HaveContent < ::RuboCop::Cop::Base
22
+ class HaveContent < RuboCop::Cop::Base
23
23
  extend AutoCorrector
24
24
 
25
25
  MSG = 'Prefer `%<good>s` over `%<bad>s`.'
@@ -4,20 +4,24 @@ module RuboCop
4
4
  module Cop
5
5
  module Capybara
6
6
  module RSpec
7
- # Use `have_css` or `have_xpath` instead of `have_selector`.
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 < ::RuboCop::Cop::Base
40
+ class HaveSelector < RuboCop::Cop::Base
37
41
  extend AutoCorrector
38
42
  include RangeHelp
39
43
 
40
- MSG = 'Use `%<good>s` instead of `have_selector`.'
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
- add_offense(node, message: message_typed(type)) do |corrector|
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, "have_#{type.value}")
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
- add_offense(node, message: message_untyped) do |corrector|
74
- corrector.replace(node.loc.selector, "have_#{default_selector}")
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 message_untyped
79
- format(MSG, good: "have_#{default_selector}")
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.fetch('DefaultSelector', 'css')
93
+ selector = cop_config['DefaultSelector'].to_s.to_sym
94
+ selector if SELECTORS.include?(selector)
84
95
  end
85
96
  end
86
97
  end
@@ -22,7 +22,7 @@ module RuboCop
22
22
  # # good
23
23
  # expect(page).to match_style(display: 'block')
24
24
  #
25
- class MatchStyle < ::RuboCop::Cop::Base
25
+ class MatchStyle < RuboCop::Cop::Base
26
26
  extend AutoCorrector
27
27
 
28
28
  MSG = 'Use `%<good>s` instead of `%<bad>s`.'
@@ -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 < ::RuboCop::Cop::Base
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 < ::RuboCop::Cop::Base
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}?".to_sym
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
- add_offense(node,
131
- message: message_explicit(matcher)) do |corrector|
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
- corrector.insert_after(actual,
173
- ".#{predicate}" + args)
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 < ::RuboCop::Cop::Base
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 < ::RuboCop::Cop::Base
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[/^\w+/, 0]
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 finders.
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: false)
20
- # expect(page).to have_css('.foo', visible: true)
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 < ::RuboCop::Cop::Base
29
- MSG_FALSE = 'Use `:all` or `:hidden` instead of `false`.'
30
- MSG_TRUE = 'Use `:visible` instead of `true`.'
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 = CAPYBARA_MATCHER_METHODS
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| add_offense(arg, message: MSG_FALSE) }
60
- visible_true?(node) { |arg| add_offense(arg, message: MSG_TRUE) }
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
- CAPYBARA_MATCHER_METHODS.include? method_name
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 < ::RuboCop::Cop::Base
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[last_selector(selector)]
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.split.last[/^\w+/, 0]
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)