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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/config/default.yml +71 -3
  4. data/lib/rubocop/capybara/version.rb +1 -1
  5. data/lib/rubocop/cop/capybara/absolute_x_path_in_element_scope.rb +104 -0
  6. data/lib/rubocop/cop/capybara/always_true_count_query.rb +75 -0
  7. data/lib/rubocop/cop/capybara/ambiguous_click.rb +1 -1
  8. data/lib/rubocop/cop/capybara/assert_style.rb +1 -1
  9. data/lib/rubocop/cop/capybara/chained_find.rb +175 -0
  10. data/lib/rubocop/cop/capybara/find_all_first.rb +13 -41
  11. data/lib/rubocop/cop/capybara/ignored_query_option.rb +242 -0
  12. data/lib/rubocop/cop/capybara/mixin/capybara_help.rb +86 -2
  13. data/lib/rubocop/cop/capybara/mixin/css_attributes_parser.rb +23 -5
  14. data/lib/rubocop/cop/capybara/mixin/css_selector.rb +13 -10
  15. data/lib/rubocop/cop/capybara/mixin/query_methods.rb +120 -0
  16. data/lib/rubocop/cop/capybara/mixin/visibility.rb +53 -0
  17. data/lib/rubocop/cop/capybara/modal_method_without_block.rb +37 -0
  18. data/lib/rubocop/cop/capybara/native_attribute.rb +76 -0
  19. data/lib/rubocop/cop/capybara/redundant_within_find.rb +10 -21
  20. data/lib/rubocop/cop/capybara/rspec/current_path_expectation.rb +13 -16
  21. data/lib/rubocop/cop/capybara/rspec/current_url_expectation.rb +122 -0
  22. data/lib/rubocop/cop/capybara/rspec/find_element_value_eq.rb +173 -0
  23. data/lib/rubocop/cop/capybara/rspec/have_content.rb +1 -1
  24. data/lib/rubocop/cop/capybara/rspec/have_selector.rb +26 -15
  25. data/lib/rubocop/cop/capybara/rspec/match_style.rb +1 -1
  26. data/lib/rubocop/cop/capybara/rspec/negation_matcher.rb +1 -1
  27. data/lib/rubocop/cop/capybara/rspec/negation_matcher_after_visit.rb +2 -2
  28. data/lib/rubocop/cop/capybara/rspec/predicate_matcher.rb +43 -7
  29. data/lib/rubocop/cop/capybara/rspec/specific_matcher.rb +8 -2
  30. data/lib/rubocop/cop/capybara/rspec/visibility_matcher.rb +29 -24
  31. data/lib/rubocop/cop/capybara/rspec.rb +25 -0
  32. data/lib/rubocop/cop/capybara/specific_actions.rb +12 -7
  33. data/lib/rubocop/cop/capybara/specific_finders.rb +149 -28
  34. data/lib/rubocop/cop/capybara/unordered_window_access.rb +40 -0
  35. data/lib/rubocop/cop/capybara/visibility_option.rb +64 -0
  36. data/lib/rubocop/cop/capybara.rb +28 -0
  37. data/lib/rubocop/cop/capybara_cops.rb +4 -16
  38. data/lib/rubocop-capybara.rb +7 -2
  39. 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}?".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)
@@ -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 < ::RuboCop::Cop::Base
63
+ class SpecificFinders < RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
19
64
  extend AutoCorrector
20
65
  include RangeHelp
21
66
 
22
- MSG = 'Prefer `find_by_id` over `find`.'
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 class_options(node)
31
- def_node_search :class_options, <<~PATTERN
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
- next if CssSelector.pseudo_classes(arg).any?
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, "'#{id}'",
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, "'#{id}'")
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, id, classes = [])
74
- add_offense(offense_range(node)) do |corrector|
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, id.delete('\\'))
77
- unless classes.compact.empty?
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 = class_options(node).first)
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
- if node.loc.end
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