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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 984aeaf86c3452b30f2eab372f0ec1e1a6dcd3fb3a2efe6e6d60f4e435a9d84d
4
- data.tar.gz: d5336d9300145f0803d3883c195cbcbdcecb0a8de54660d3fcf682166d4e8cd0
3
+ metadata.gz: 5b880f22effca543e3fcbabf13ff06e0055eabeddec7342ec5ff2375a6ceb336
4
+ data.tar.gz: b4937b0aa90a9bb9e095ca0d19418a7ee294bf1bc6cabc84c2001bd8fa46aa92
5
5
  SHA512:
6
- metadata.gz: 2c08f6500d0ce4907ebea18f476a6da57411bd35b3e8c7ebe2be619efd2ef8b03999c7c5724eab4013e7febe656d043e9dd341f2cf1e4dfe8e81b945369d2e57
7
- data.tar.gz: 578287adeb808e1c534134f17dbf2ebf4e7fe91433b90344f0704168deccc98328da9aca3210b4c653960437b788e36f067bee0b4bf7b85d5d926fbeda00b59a
6
+ metadata.gz: d141ff264d07fb784bc62ec4d47f04153b033e599d3297a1f9b063c8398b877e507cdb3e575c3497ff96612b4acea6ad15a82694b28c03913fd1e59b02423c91
7
+ data.tar.gz: 04abfbff88016558648d3cff263ce0b3ef2413dda3a3f381849d26e3a3bbffcd4b7762c76020d8db0892609e2585af7a0413b41fd20712caec161738f3fa852d
data/CHANGELOG.md CHANGED
@@ -2,6 +2,53 @@
2
2
 
3
3
  ## Edge (Unreleased)
4
4
 
5
+ ## 3.1.0 (2026-10-16)
6
+
7
+ - Add new `Capybara/UnorderedWindowAccess` cop. ([@ydah])
8
+ - Add new `Capybara/AbsoluteXPathInElementScope` cop. ([@ydah])
9
+ - Add new `Capybara/AlwaysTrueCountQuery` cop. ([@ydah])
10
+ - Add new `Capybara/RSpec/CurrentUrlExpectation` cop. ([@ydah])
11
+ - Add new `Capybara/ModalMethodWithoutBlock` cop. ([@ydah])
12
+ - Add new `Capybara/NativeAttribute` cop. ([@ydah])
13
+ - Add new `Capybara/VisibilityOption` cop. ([@ydah])
14
+ - Add new `Capybara/ChainedFind` cop. ([@ydah])
15
+ - Add new `Capybara/IgnoredQueryOption` cop. ([@ydah])
16
+ - Add new `Capybara/RSpec/FindElementValueEq` cop. ([@ydah])
17
+ - Recognize `have_element` and `have_no_element` in the RSpec negation cops. ([@ydah])
18
+ - Detect `have_no_selector` in `Capybara/RSpec/HaveSelector`. ([@ydah])
19
+ - Autocorrect `visible: true` in `Capybara/RSpec/VisibilityMatcher` for known selectors. ([@ydah])
20
+ - Add support for using `find` with `:link` and `:field` in `Capybara/SpecificFinders` cop. ([@ydah])
21
+ - Speed up loading rubocop-capybara by lazily loading only the cops needed for a run. This requires RuboCop 1.89.0+. ([@koic])
22
+ - Fix false positives in `Capybara/RSpec/SpecificMatcher` for input types excluded by `have_field`. ([@ydah])
23
+ - Avoid interpolating literal `#{...}`, `#@...`, and `#$...` patterns in `Capybara/RSpec/CurrentPathExpectation` and `Capybara/RSpec/CurrentUrlExpectation` autocorrection. ([@ydah])
24
+ - Fix false positives and negatives for `Capybara/SpecificActions` when CSS attribute values contain spaces. ([@ydah])
25
+ - Honor `DefaultSelector: null` in selector-dependent cops, including `Capybara/RSpec/HaveSelector`. ([@ydah])
26
+ - Fix false positives for custom elements in `Capybara/SpecificActions` and `Capybara/RSpec/SpecificMatcher`. ([@ydah])
27
+ - Avoid unsafe block autocorrection and unnecessary receiver parentheses in `Capybara/RSpec/PredicateMatcher`. ([@ydah])
28
+ - Fix a crash in `Capybara/RSpec/CurrentPathExpectation` for invalid regexp strings. ([@ydah])
29
+ - Fix `Capybara/SpecificFinders` when CSS id values are `true` or `false`. ([@ydah])
30
+ - Fix a crash in `Capybara/SpecificActions` for blank `find` selectors. ([@ydah])
31
+ - Fix reversed autocorrection in `Capybara/RSpec/PredicateMatcher` for `eql` and `equal` boolean matchers. ([@ydah])
32
+ - Avoid errors and unsafe autocorrection in `Capybara/SpecificFinders` with dynamic `class:` options. ([@ydah])
33
+ - Fix incorrect autocorrections and missed offenses for non-CSS locators in `Capybara/SpecificFinders`. ([@ydah])
34
+ - Use the `:id` selector when `Capybara/RedundantWithinFind` autocorrects `find_by_id`, preserving special and dynamic IDs. ([@ydah])
35
+ - Avoid unsafe `find_field` autocorrections for input types and boolean attributes in `Capybara/SpecificFinders`. ([@ydah])
36
+ - Fix false positives and incorrect autocorrections for `Capybara/FindAllFirst` when `find` or `all` uses `match: :first`. ([@ydah])
37
+ - Avoid `Capybara/FindAllFirst` offenses when the result is used with safe navigation or as a condition. ([@ydah])
38
+ - Fix an incorrect autocorrect for `Capybara/RedundantWithinFind` when `find_by_id` uses a dynamic id. ([@ydah])
39
+ - Fix an incorrect autocorrect for `Capybara/RedundantWithinFind` when a finder call has a block argument. ([@ydah])
40
+ - Fix a false positive for `Capybara/RSpec/HaveSelector` when `DefaultSelector` is unsupported. ([@ydah])
41
+ - Fix an incorrect autocorrect for `Capybara/SpecificFinders` when a selector class is empty or contains an escaped dot. ([@ydah])
42
+ - Fix a false positive for `Capybara/SpecificFinders` when an id selector contains unsupported CSS identifier syntax. ([@ydah])
43
+ - Fix a false negative for `Capybara/RSpec/NegationMatcherAfterVisit` when using `to_not`. ([@ydah])
44
+ - Fix an incorrect autocorrect for `Capybara/RSpec/PredicateMatcher` when `EnforcedStyle: explicit` and `expect` actual is a complex expression. ([@ydah])
45
+ - Fix `Capybara/RSpec/CurrentPathExpectation` autocorrection for `match` when the URL has a query string. ([@ydah])
46
+ - Fix a false positive for `Capybara/SpecificActions` when `click` uses arguments or a block. ([@ydah])
47
+ - Fix an incorrect autocorrect for `Capybara/SpecificFinders` when a selector class is used with nested `class:` options. ([@ydah])
48
+ - Fix CSS attribute parsing when attribute values contain quotes. ([@ydah])
49
+ - Fix CSS attribute parsing to return raw values and format Ruby literals in autocorrection. ([@ydah])
50
+ - Fix a false negative for `Capybara/SpecificFinders` when using `find` with input field attributes. ([@ydah])
51
+
5
52
  ## 3.0.0 (2026-06-22)
6
53
 
7
54
  - Enable pending cops by default for the 3.0 release. ([@ydah])
data/config/default.yml CHANGED
@@ -9,6 +9,18 @@ Capybara:
9
9
  - "**/*_steps.rb"
10
10
  - "**/features/step_definitions/**/*"
11
11
 
12
+ Capybara/AbsoluteXPathInElementScope:
13
+ Description: Checks for absolute XPath searches on Capybara elements.
14
+ Enabled: pending
15
+ VersionAdded: '3.1'
16
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/AbsoluteXPathInElementScope
17
+
18
+ Capybara/AlwaysTrueCountQuery:
19
+ Description: Checks for positive queries that succeed when there are no matches.
20
+ Enabled: pending
21
+ VersionAdded: '3.1'
22
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/AlwaysTrueCountQuery
23
+
12
24
  Capybara/AmbiguousClick:
13
25
  Description: Specify the exact target to click on.
14
26
  Enabled: false
@@ -22,6 +34,13 @@ Capybara/AssertStyle:
22
34
  VersionChanged: '3.0'
23
35
  Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/AssertStyle
24
36
 
37
+ Capybara/ChainedFind:
38
+ Description: Avoid chaining `find` methods and combine selectors.
39
+ Enabled: pending
40
+ DefaultSelector: css
41
+ VersionAdded: '3.1'
42
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/ChainedFind
43
+
25
44
  Capybara/FindAllFirst:
26
45
  Description: Enforces use of `first` instead of `all` with `first` or `[0]`.
27
46
  Enabled: true
@@ -30,6 +49,26 @@ Capybara/FindAllFirst:
30
49
  VersionChanged: '3.0'
31
50
  Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/FindAllFirst
32
51
 
52
+ Capybara/IgnoredQueryOption:
53
+ Description: Checks for query options that have no effect in Capybara.
54
+ Enabled: pending
55
+ DefaultSelector: css
56
+ VersionAdded: '3.1'
57
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/IgnoredQueryOption
58
+
59
+ Capybara/ModalMethodWithoutBlock:
60
+ Description: Checks that modal handling methods are called with a block.
61
+ Enabled: pending
62
+ VersionAdded: '3.1'
63
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/ModalMethodWithoutBlock
64
+
65
+ Capybara/NativeAttribute:
66
+ Description: Enforces use of `Element#[]` instead of `native.attribute`.
67
+ Enabled: pending
68
+ SafeAutoCorrect: false
69
+ VersionAdded: '3.1'
70
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/NativeAttribute
71
+
33
72
  Capybara/RedundantWithinFind:
34
73
  Description: Checks for redundant `within find(...)` calls.
35
74
  Enabled: true
@@ -51,6 +90,19 @@ Capybara/SpecificFinders:
51
90
  VersionChanged: '3.0'
52
91
  Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/SpecificFinders
53
92
 
93
+ Capybara/UnorderedWindowAccess:
94
+ Description: Checks positional access to Capybara windows, whose order is undefined.
95
+ Enabled: pending
96
+ VersionAdded: '3.1'
97
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/UnorderedWindowAccess
98
+
99
+ Capybara/VisibilityOption:
100
+ Description: Checks for boolean values passed to Capybara's `visible` option.
101
+ Enabled: pending
102
+ DefaultSelector: css
103
+ VersionAdded: '3.1'
104
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/VisibilityOption
105
+
54
106
  Capybara/RSpec:
55
107
  Enabled: true
56
108
  Include: *1
@@ -62,6 +114,20 @@ Capybara/RSpec/CurrentPathExpectation:
62
114
  VersionChanged: '3.0'
63
115
  Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/CurrentPathExpectation
64
116
 
117
+ Capybara/RSpec/CurrentUrlExpectation:
118
+ Description: Checks expectations on `current_url` that bypass Capybara's waiting.
119
+ Enabled: pending
120
+ SafeAutoCorrect: false
121
+ VersionAdded: '3.1'
122
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/CurrentUrlExpectation
123
+
124
+ Capybara/RSpec/FindElementValueEq:
125
+ Description: Checks for expectations on the value of an element found by Capybara.
126
+ Enabled: pending
127
+ SafeAutoCorrect: false
128
+ VersionAdded: '3.1'
129
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/FindElementValueEq
130
+
65
131
  Capybara/RSpec/HaveContent:
66
132
  Description: Checks for usage of `have_content` and `have_no_content`.
67
133
  Enabled: true
@@ -70,7 +136,7 @@ Capybara/RSpec/HaveContent:
70
136
  Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/HaveContent
71
137
 
72
138
  Capybara/RSpec/HaveSelector:
73
- Description: Use `have_css` or `have_xpath` instead of `have_selector`.
139
+ Description: Use CSS or XPath matchers instead of generic selector matchers.
74
140
  Enabled: true
75
141
  DefaultSelector: css
76
142
  VersionAdded: '2.19'
@@ -123,8 +189,10 @@ Capybara/RSpec/SpecificMatcher:
123
189
  Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/SpecificMatcher
124
190
 
125
191
  Capybara/RSpec/VisibilityMatcher:
126
- Description: Checks for boolean visibility in Capybara finders.
192
+ Description: Checks for boolean visibility in Capybara RSpec matchers.
127
193
  Enabled: true
194
+ SafeAutoCorrect: false
195
+ DefaultSelector: css
128
196
  VersionAdded: '1.39'
129
- VersionChanged: '3.0'
197
+ VersionChanged: "<<next>>"
130
198
  Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/VisibilityMatcher
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Capybara
5
5
  # Version information for the Capybara RuboCop plugin.
6
6
  module Version
7
- STRING = '3.0.0'
7
+ STRING = '3.1.0'
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ # Checks for absolute XPath searches on Capybara elements.
7
+ #
8
+ # An XPath starting with `//` searches from the document root, even when
9
+ # the query is called on an element. Use `.//` to stay within it.
10
+ # Direct RSpec `let` and named `subject` element definitions are
11
+ # recognized.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # page.find('.dialog').find(:xpath, '//button')
16
+ # dialog = page.find('.dialog')
17
+ # dialog.find(:xpath, '//button')
18
+ # RSpec.describe 'dialog' do
19
+ # let(:dialog) { find('.dialog') }
20
+ # it { dialog.find(:xpath, '//button') }
21
+ # end
22
+ #
23
+ # # good
24
+ # page.find('.dialog').find(:xpath, './/button')
25
+ # page.find(:xpath, '//button')
26
+ #
27
+ class AbsoluteXPathInElementScope < RuboCop::Cop::Base
28
+ include CapybaraHelp
29
+
30
+ MSG = 'Use `.//` for an XPath search within an element.'
31
+ RESTRICT_ON_SEND = %i[find all find_all first].freeze
32
+ RSPEC_GROUPS = %i[
33
+ describe context feature shared_examples shared_context
34
+ ].freeze
35
+ RSPEC_HELPERS = %i[let let! subject].freeze
36
+
37
+ # @!method xpath_literal(node)
38
+ def_node_matcher :xpath_literal, '(call _ _ (sym :xpath) $(str _) ...)'
39
+
40
+ # @!method element_finder?(node)
41
+ def_node_matcher :element_finder?, <<~PATTERN
42
+ (call #capybara_receiver? :find ...)
43
+ PATTERN
44
+
45
+ def on_send(node)
46
+ xpath = xpath_literal(node)
47
+ return unless xpath&.value&.start_with?('//')
48
+ return unless element_receiver?(node.receiver)
49
+
50
+ add_offense(xpath)
51
+ end
52
+ alias on_csend on_send
53
+
54
+ private
55
+
56
+ def element_receiver?(node)
57
+ return true if node&.call_type? && element_finder?(node)
58
+ return element_finder?(local_variable_value(node)) if node&.lvar_type?
59
+
60
+ element_finder?(rspec_helper_value(node)) if node&.call_type?
61
+ end
62
+
63
+ def rspec_helper_value(node)
64
+ return unless node.receiver.nil? && node.arguments.empty?
65
+
66
+ node.each_ancestor(:block).each do |group|
67
+ next unless rspec_group?(group)
68
+
69
+ definition = named_helper(group, node.method_name)
70
+ return definition.body if definition
71
+ end
72
+ nil
73
+ end
74
+
75
+ def rspec_group?(block)
76
+ call = block.send_node
77
+ return false unless RSPEC_GROUPS.include?(call.method_name)
78
+
79
+ call.receiver.nil? || (call.receiver.const_type? &&
80
+ call.receiver.const_name == 'RSpec')
81
+ end
82
+
83
+ def named_helper(group, name)
84
+ body = group.body
85
+ statements = body.begin_type? ? body.children : [body]
86
+ statements.reverse_each.find do |statement|
87
+ named_rspec_helper?(statement, name)
88
+ end
89
+ end
90
+
91
+ def named_rspec_helper?(statement, name)
92
+ return false unless statement.block_type?
93
+
94
+ call = statement.send_node
95
+ return false if call.receiver
96
+ return false unless RSPEC_HELPERS.include?(call.method_name)
97
+
98
+ argument = call.first_argument
99
+ argument&.sym_type? && argument.value == name
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ # Checks for positive queries that succeed when there are no matches.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # expect(page).to have_css('.item', minimum: 0)
11
+ # page.has_css?('.item', minimum: 0)
12
+ #
13
+ # # good
14
+ # expect(page).to have_css('.item')
15
+ # expect(page).to have_css('.item', minimum: 0, maximum: 2)
16
+ # all('.item', minimum: 0)
17
+ #
18
+ class AlwaysTrueCountQuery < RuboCop::Cop::Base
19
+ include CapybaraHelp
20
+
21
+ MSG = '`minimum: 0` makes this positive query succeed with no matches.'
22
+ OTHER_COUNT_OPTIONS = %i[count maximum between].freeze
23
+ RESTRICT_ON_SEND = QueryMethods.names(:selector).select do |name|
24
+ name = name.to_s
25
+ name.start_with?('assert_', 'has_', 'have_', 'must_have_') &&
26
+ !name.start_with?('assert_no_', 'has_no_', 'have_no_')
27
+ end.freeze
28
+
29
+ # @!method zero_minimum?(node)
30
+ def_node_matcher :zero_minimum?, '(pair (sym :minimum) (int 0))'
31
+
32
+ def on_send(node)
33
+ return unless capybara_receiver?(node.receiver)
34
+ return unless positive_usage?(node)
35
+
36
+ minimum = always_true_minimum(node.last_argument)
37
+ add_offense(minimum) if minimum
38
+ end
39
+ alias on_csend on_send
40
+
41
+ private
42
+
43
+ def positive_usage?(node)
44
+ return true unless node.method_name.to_s.start_with?('have_')
45
+
46
+ parent = node.parent
47
+ parent&.send_type? && parent.method?(:to) &&
48
+ parent.first_argument == node
49
+ end
50
+
51
+ def always_true_minimum(options)
52
+ return unless known_options?(options)
53
+
54
+ pairs = options.pairs
55
+ return if pairs.any? { |pair| other_count_option?(pair) }
56
+
57
+ minimum = pairs.select { |pair| pair.key.value == :minimum }
58
+ minimum.first if minimum.one? && zero_minimum?(minimum.first)
59
+ end
60
+
61
+ def known_options?(options)
62
+ return false unless options&.hash_type? && !options.braces?
63
+
64
+ options.children.all? do |child|
65
+ child.pair_type? && child.key.sym_type?
66
+ end
67
+ end
68
+
69
+ def other_count_option?(pair)
70
+ OTHER_COUNT_OPTIONS.include?(pair.key.value) && !pair.value.nil_type?
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -17,7 +17,7 @@ module RuboCop
17
17
  # click_link('foo')
18
18
  # click_button('foo')
19
19
  #
20
- class AmbiguousClick < ::RuboCop::Cop::Base
20
+ class AmbiguousClick < RuboCop::Cop::Base
21
21
  MSG = 'Use `click_link` or `click_button` instead of `%<method>s`.'
22
22
  RESTRICT_ON_SEND = %i[click_link_or_button click_on].freeze
23
23
 
@@ -12,7 +12,7 @@ module RuboCop
12
12
  # # good
13
13
  # page.find(:css, '#first').assert_matches_style(display: 'block')
14
14
  #
15
- class AssertStyle < ::RuboCop::Cop::Base
15
+ class AssertStyle < RuboCop::Cop::Base
16
16
  extend AutoCorrector
17
17
 
18
18
  MSG = 'Use `assert_matches_style` instead of `assert_style`.'
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ # Avoid chaining `find` methods and combine selectors.
7
+ #
8
+ # Combining selectors into a single `find` call is more efficient than
9
+ # chaining multiple `find` calls, as it reduces the number of DOM queries.
10
+ # Only recognizable Capybara finder chains with string locators are
11
+ # checked.
12
+ # Set `DefaultSelector` to match `Capybara.default_selector`, or to
13
+ # `null` if it varies at runtime. Autocorrection requires `css` and
14
+ # literal strings.
15
+ #
16
+ # @example
17
+ # # bad
18
+ # page.find('#foo').find('.bar')
19
+ # page.find('#foo').find('.bar').find('.baz')
20
+ #
21
+ # # good
22
+ # page.find('#foo .bar')
23
+ # page.find('#foo .bar .baz')
24
+ # find('#form').find(:button, 'Save')
25
+ #
26
+ class ChainedFind < RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
27
+ extend AutoCorrector
28
+ include CapybaraHelp
29
+
30
+ MSG = 'Avoid chaining `find` methods. ' \
31
+ 'Combine the selectors into a single `find` call.'
32
+ MSG_WITH_COMBINED = 'Use `find(%<combined>s)` instead of ' \
33
+ 'chaining `find` methods.'
34
+ RESTRICT_ON_SEND = %i[find].freeze
35
+
36
+ # @!method single_string_find?(node)
37
+ def_node_matcher :single_string_find?, <<~PATTERN
38
+ (call _ :find (str $_))
39
+ PATTERN
40
+
41
+ def on_send(node)
42
+ return unless chained_find?(node)
43
+
44
+ find_receiver = find_receiver(node.receiver)
45
+ return unless find_receiver
46
+ return unless string_locators?(node, find_receiver)
47
+ return unless capybara_receiver?(find_receiver.receiver)
48
+
49
+ register_offense(node, find_receiver)
50
+ end
51
+ alias on_csend on_send
52
+
53
+ private
54
+
55
+ def chained_find?(node)
56
+ node.method?(:find) && node.receiver
57
+ end
58
+
59
+ def string_locators?(node, receiver)
60
+ node.first_argument&.type?(:str, :dstr) &&
61
+ receiver.first_argument&.type?(:str, :dstr)
62
+ end
63
+
64
+ def find_receiver(receiver)
65
+ return unwrapped_receiver(receiver) if receiver.block_type?
66
+
67
+ receiver if receiver.call_type? && receiver.method?(:find)
68
+ end
69
+
70
+ def unwrapped_receiver(block_node)
71
+ send_node = block_node.send_node
72
+ send_node if send_node.method?(:find)
73
+ end
74
+
75
+ def register_offense(node, find_receiver)
76
+ if safe_to_autocorrect?(node, find_receiver)
77
+ add_correctable_offense(node, find_receiver)
78
+ else
79
+ add_offense(node)
80
+ end
81
+ end
82
+
83
+ def add_correctable_offense(node, find_receiver)
84
+ combined = combined_selector(node, find_receiver)
85
+ message = format(
86
+ MSG_WITH_COMBINED,
87
+ combined: selector_literal(combined)
88
+ )
89
+ return add_offense(node, message: message) if nested_find?(node)
90
+
91
+ add_offense(node, message: message) do |corrector|
92
+ corrector.replace(node, replacement(node, combined))
93
+ end
94
+ end
95
+
96
+ def nested_find?(node)
97
+ node.parent&.call_type? &&
98
+ node.parent.method?(:find) &&
99
+ node.parent.receiver == node
100
+ end
101
+
102
+ def safe_to_autocorrect?(node, receiver)
103
+ cop_config['DefaultSelector'].to_s == 'css' &&
104
+ single_string_argument?(node) &&
105
+ single_string_argument?(receiver) &&
106
+ node.type == receiver.type &&
107
+ safe_selectors?(node)
108
+ end
109
+
110
+ def safe_selectors?(node)
111
+ current = node
112
+ while current&.call_type? && current.method?(:find)
113
+ return false unless single_string_argument?(current)
114
+
115
+ return false if unsafe_selector?(current.first_argument.value)
116
+
117
+ current = current.receiver
118
+ end
119
+
120
+ true
121
+ end
122
+
123
+ def unsafe_selector?(selector)
124
+ selector.match?(/(?<!\\),/) || selector.start_with?('//', './/')
125
+ end
126
+
127
+ def single_string_argument?(node)
128
+ node.arguments.one? &&
129
+ !node.block_node &&
130
+ single_string_find?(node)
131
+ end
132
+
133
+ def combined_selector(node, receiver)
134
+ selectors = collect_selectors(receiver)
135
+ current = node.first_argument.value
136
+
137
+ "#{selectors} #{current}"
138
+ end
139
+
140
+ def collect_selectors(node)
141
+ [].tap do |selectors|
142
+ current = node
143
+ while (selector = single_string_find?(current))
144
+ selectors.unshift(selector)
145
+ current = current.receiver
146
+ break unless current&.call_type? && current.method?(:find)
147
+ end
148
+ end.join(' ')
149
+ end
150
+
151
+ def replacement(node, combined_selector)
152
+ root = chain_root(node)
153
+ receiver_source = root.receiver&.source
154
+ find_call = "find(#{selector_literal(combined_selector)})"
155
+
156
+ [receiver_source, find_call].compact.join(root.loc.dot&.source || '.')
157
+ end
158
+
159
+ def selector_literal(selector)
160
+ return selector.inspect if selector.match?(/['\\]/)
161
+
162
+ "'#{selector}'"
163
+ end
164
+
165
+ def chain_root(node)
166
+ root = node
167
+ while root.receiver&.call_type? && root.receiver.method?(:find)
168
+ root = root.receiver
169
+ end
170
+ root
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -6,29 +6,25 @@ module RuboCop
6
6
  # Enforces use of `first` instead of `all` with `first` or `[0]`.
7
7
  #
8
8
  # @safety
9
- # This cop's autocorrection is unsafe because `all` returns a
10
- # `Capybara::Result` (an enumerable collection), while `first`
11
- # returns a single `Capybara::Node::Element`. Replacing `all`
12
- # with `first` may break code that depends on the return value
13
- # being a collection (e.g. calling `.each` on the result).
9
+ # With default options, `all(...).first` and `all(...)[0]` return
10
+ # `nil` when no element matches, while `first(...)` raises an
11
+ # exception. Autocorrection can break code that depends on `nil`.
14
12
  #
15
13
  # @example
16
14
  #
17
15
  # # bad
18
16
  # all('a').first
19
17
  # all('a')[0]
20
- # find('a', match: :first)
21
- # all('a', match: :first)
22
18
  #
23
19
  # # good
24
20
  # first('a')
25
21
  #
26
- class FindAllFirst < ::RuboCop::Cop::Base
22
+ class FindAllFirst < RuboCop::Cop::Base
27
23
  extend AutoCorrector
28
24
  include RangeHelp
29
25
 
30
26
  MSG = 'Use `first(%<selector>s)`.'
31
- RESTRICT_ON_SEND = %i[all find].freeze
27
+ RESTRICT_ON_SEND = %i[all].freeze
32
28
 
33
29
  # @!method find_all_first?(node)
34
30
  def_node_matcher :find_all_first?, <<~PATTERN
@@ -38,22 +34,10 @@ module RuboCop
38
34
  }
39
35
  PATTERN
40
36
 
41
- # @!method include_match_first?(node)
42
- def_node_matcher :include_match_first?, <<~PATTERN
43
- (send _ {:find :all} _ $(hash <(pair (sym :match) (sym :first)) ...>))
44
- PATTERN
45
-
46
37
  def on_send(node)
47
- on_all_first(node)
48
- on_match_first(node)
49
- end
50
-
51
- private
52
-
53
- def on_all_first(node)
54
38
  return unless (parent = node.parent)
55
39
  return unless find_all_first?(parent)
56
- return if part_of_logical_operator?(parent)
40
+ return if nil_sensitive_use?(parent)
57
41
 
58
42
  range = range_between(node.loc.selector.begin_pos,
59
43
  parent.loc.selector.end_pos)
@@ -64,27 +48,15 @@ module RuboCop
64
48
  end
65
49
  end
66
50
 
67
- def on_match_first(node)
68
- include_match_first?(node) do |hash|
69
- selector = ([node.first_argument.source] + replaced_hash(hash))
70
- .join(', ')
71
- range = range_between(node.loc.selector.begin_pos,
72
- node.source_range.end_pos)
73
- add_offense(range,
74
- message: format(MSG, selector: selector)) do |corrector|
75
- corrector.replace(range, "first(#{selector})")
76
- end
77
- end
78
- end
51
+ private
79
52
 
80
- def replaced_hash(hash)
81
- hash.child_nodes.flat_map(&:source).reject do |arg|
82
- arg == 'match: :first'
83
- end
84
- end
53
+ def nil_sensitive_use?(node)
54
+ return true if node.ancestors.any?(&:operator_keyword?)
85
55
 
86
- def part_of_logical_operator?(node)
87
- node.ancestors.any?(&:operator_keyword?)
56
+ parent = node.parent
57
+ (parent&.csend_type? && parent.receiver == node) ||
58
+ (parent&.type?(:if, :while, :until) &&
59
+ parent.condition == node)
88
60
  end
89
61
  end
90
62
  end