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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b880f22effca543e3fcbabf13ff06e0055eabeddec7342ec5ff2375a6ceb336
|
|
4
|
+
data.tar.gz: b4937b0aa90a9bb9e095ca0d19418a7ee294bf1bc6cabc84c2001bd8fa46aa92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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:
|
|
197
|
+
VersionChanged: "<<next>>"
|
|
130
198
|
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/VisibilityMatcher
|
|
@@ -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 <
|
|
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 <
|
|
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
|
-
#
|
|
10
|
-
# `
|
|
11
|
-
#
|
|
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 <
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
81
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|