rubocop-capybara 2.18.0 → 2.20.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb7f795cc80f072b94781f2339e781cccf9922ad2303c09111c0e34a4af264f8
4
- data.tar.gz: d2ae47b522208177b7c93338973c8a144dfbccca2f817f0797b531f8c89e50b9
3
+ metadata.gz: 963a68452b58c47db1d1d9e7f9201db0a98a8b70bb8089473b406911fa5cc33e
4
+ data.tar.gz: 58e60dd5ff6097695a1029b9e37414209f7c26d2da0d16cbc6b808c8cd56efd8
5
5
  SHA512:
6
- metadata.gz: 36b58f29500840c7dc34c2c2ed55856ff08cdd7ca41822209a9388204948903be89cf13c3065da7cfed3e8332f3433b3293087c8de1cf1266f093fadef2fa69d
7
- data.tar.gz: 5b961abac7ae8dd46708ec9649718a23596bde9e45bcc48f7d9e818c2a116d5fda3de0653cf3aa8e56d11ed11ebcb94e24ef49ee0653e739a26d43d2383c2152
6
+ metadata.gz: cec93e7919aeefbb1f2b2c13f1f0caeb7627c5556f85fe219f107b6520237cccd792ad4713905dfb47151f0bcb82e04d4bd0034a7a747b741e607ea9d4de5e54
7
+ data.tar.gz: 3e359ec71065b5daf64cdb080d8ddfad4f6144778ad7fb89d932aa083353547663a1316efe7f9e9fd3cb7bca6a6f012812ea7f18e00c76a8af22316f24325f7c
data/CHANGELOG.md CHANGED
@@ -2,10 +2,28 @@
2
2
 
3
3
  ## Edge (Unreleased)
4
4
 
5
+ ## 2.20.0 (2024-01-03)
6
+
7
+ - Change to default `EnforcedStyle: link_or_button` for `Capybara/ClickLinkOrButtonStyle` cop. ([@ydah])
8
+ - Fix a false negative for `RSpec/HaveSelector` when first argument is dstr node. ([@ydah])
9
+ - Add new `Capybara/RedundantWithinFind` cop. ([@ydah])
10
+ - Fix an invalid attributes parse when name with multiple `[]` for `Capybara/SpecificFinders` and `Capybara/SpecificActions` and `Capybara/SpecificMatcher`. ([@ydah])
11
+ - Change to default `EnforcedStyle: have_no` for `Capybara/NegationMatcher` cop. ([@ydah])
12
+ - Fix a false positive for `Capybara/SpecificMatcher` when `text:` or `exact_text:` with regexp. ([@ydah])
13
+
14
+ ## 2.19.0 (2023-09-20)
15
+
16
+ - Add new `Capybara/RSpec/PredicateMatcher` cop. ([@ydah])
17
+ - Add new `Capybara/RSpec/HaveSelector` cop. ([@ydah])
18
+ - Add new `Capybara/ClickLinkOrButtonStyle` cop. ([@ydah])
19
+ - Fix a false positive for `Capybara/SpecificFinders` when `find` with kind option. ([@ydah])
20
+ - Fix an incorrect autocorrect for `Capybara/CurrentPathExpectation`. ([@ydah])
21
+ - Drop Ruby 2.6 support. ([@ydah])
22
+
5
23
  ## 2.18.0 (2023-04-21)
6
24
 
7
25
  - Fix an offense message for `Capybara/SpecificFinders`. ([@ydah])
8
- - Expand `Capybara/NegationMatcher` to support `have_content` ([@OskarsEzerins])
26
+ - Expand `Capybara/NegationMatcher` to support `have_content`. ([@OskarsEzerins])
9
27
  - Fix an incorrect autocorrect for `Capybara/CurrentPathExpectation` when matcher's argument is a method with a argument and no parentheses. ([@ydah])
10
28
 
11
29
  ## 2.17.1 (2023-02-13)
data/README.md CHANGED
@@ -17,7 +17,7 @@ gem install rubocop-capybara
17
17
 
18
18
  or if you use bundler put this in your `Gemfile`
19
19
 
20
- ```
20
+ ```ruby
21
21
  gem 'rubocop-capybara', require: false
22
22
  ```
23
23
 
data/config/default.yml CHANGED
@@ -2,13 +2,24 @@
2
2
  Capybara:
3
3
  Enabled: true
4
4
  DocumentationBaseURL: https://docs.rubocop.org/rubocop-capybara
5
- Include:
5
+ Include: &1
6
6
  - "**/*_spec.rb"
7
7
  - "**/spec/**/*"
8
8
  - "**/test/**/*"
9
9
  - "**/*_steps.rb"
10
10
  - "**/features/step_definitions/**/*"
11
11
 
12
+ Capybara/ClickLinkOrButtonStyle:
13
+ Description: Checks for methods of button or link clicks.
14
+ Enabled: pending
15
+ VersionAdded: '2.19'
16
+ VersionChanged: '2.20'
17
+ EnforcedStyle: link_or_button
18
+ SupportedStyles:
19
+ - link_or_button
20
+ - strict
21
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/ClickLinkOrButtonStyle
22
+
12
23
  Capybara/CurrentPathExpectation:
13
24
  Description: Checks that no expectations are set on Capybara's `current_path`.
14
25
  Enabled: true
@@ -26,12 +37,19 @@ Capybara/NegationMatcher:
26
37
  Description: Enforces use of `have_no_*` or `not_to` for negated expectations.
27
38
  Enabled: pending
28
39
  VersionAdded: '2.14'
29
- EnforcedStyle: not_to
40
+ VersionChanged: '2.20'
41
+ EnforcedStyle: have_no
30
42
  SupportedStyles:
31
43
  - have_no
32
44
  - not_to
33
45
  Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/NegationMatcher
34
46
 
47
+ Capybara/RedundantWithinFind:
48
+ Description: Checks for redundant `within find(...)` calls.
49
+ Enabled: pending
50
+ VersionAdded: '2.20'
51
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RedundantWithinFind
52
+
35
53
  Capybara/SpecificActions:
36
54
  Description: Checks for there is a more specific actions offered by Capybara.
37
55
  Enabled: pending
@@ -56,3 +74,26 @@ Capybara/VisibilityMatcher:
56
74
  VersionAdded: '1.39'
57
75
  VersionChanged: '2.0'
58
76
  Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/VisibilityMatcher
77
+
78
+ Capybara/RSpec:
79
+ Enabled: true
80
+ Include: *1
81
+
82
+ Capybara/RSpec/HaveSelector:
83
+ Description: Use `have_css` or `have_xpath` instead of `have_selector`.
84
+ Enabled: pending
85
+ DefaultSelector: css
86
+ VersionAdded: '2.19'
87
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/HaveSelector
88
+
89
+ Capybara/RSpec/PredicateMatcher:
90
+ Description: Prefer using predicate matcher over using predicate method directly.
91
+ Enabled: pending
92
+ Strict: true
93
+ EnforcedStyle: inflected
94
+ AllowedExplicitMatchers: []
95
+ SupportedStyles:
96
+ - inflected
97
+ - explicit
98
+ VersionAdded: '2.19'
99
+ Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/PredicateMatcher
@@ -7,7 +7,7 @@ module RuboCop
7
7
  # Builds a YAML config file from two config hashes
8
8
  class ConfigFormatter
9
9
  EXTENSION_ROOT_DEPARTMENT = %r{^(Capybara/)}.freeze
10
- SUBDEPARTMENTS = [].freeze
10
+ SUBDEPARTMENTS = %(Capybara/RSpec)
11
11
  AMENDMENTS = [].freeze
12
12
  COP_DOC_BASE_URL = 'https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/'
13
13
 
@@ -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 = '2.18.0'
7
+ STRING = '2.20.0'
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ # Checks for methods of button or link clicks.
7
+ #
8
+ # By default, prefer to use `click_link_or_button` or `click_on`.
9
+ # These methods offer a weaker coupling between the test and HTML,
10
+ # allowing for a more faithful reflection of how the user behaves.
11
+ #
12
+ # You can set `EnforcedStyle: strict` to prefer the use of
13
+ # `click_link` and `click_button`, but this is a deprecated setting.
14
+ #
15
+ # @example EnforcedStyle: link_or_button (default)
16
+ # # bad
17
+ # click_link('foo')
18
+ # click_button('foo')
19
+ #
20
+ # # good
21
+ # click_link_or_button('foo')
22
+ # click_on('foo')
23
+ #
24
+ # @example EnforcedStyle: strict
25
+ # # bad
26
+ # click_link_or_button('foo')
27
+ # click_on('foo')
28
+ #
29
+ # # good
30
+ # click_link('foo')
31
+ # click_button('foo')
32
+ #
33
+ class ClickLinkOrButtonStyle < ::RuboCop::Cop::Base
34
+ include ConfigurableEnforcedStyle
35
+
36
+ MSG_STRICT =
37
+ 'Use `click_link` or `click_button` instead of `%<method>s`.'
38
+ MSG_CLICK_LINK_OR_BUTTON =
39
+ 'Use `click_link_or_button` or `click_on` instead of `%<method>s`.'
40
+ STRICT_METHODS = %i[click_link click_button].freeze
41
+ CLICK_LINK_OR_BUTTON = %i[click_link_or_button click_on].freeze
42
+ RESTRICT_ON_SEND = (STRICT_METHODS + CLICK_LINK_OR_BUTTON).freeze
43
+
44
+ def on_send(node)
45
+ return unless offense?(node)
46
+
47
+ add_offense(node, message: offense_message(node))
48
+ end
49
+
50
+ private
51
+
52
+ def offense?(node)
53
+ (style == :strict && !strict_method?(node)) ||
54
+ (style == :link_or_button && !link_or_button_method?(node))
55
+ end
56
+
57
+ def offense_message(node)
58
+ if style == :strict
59
+ format(MSG_STRICT, method: node.method_name)
60
+ elsif style == :link_or_button
61
+ format(MSG_CLICK_LINK_OR_BUTTON, method: node.method_name)
62
+ end
63
+ end
64
+
65
+ def strict_method?(node)
66
+ STRICT_METHODS.include?(node.method_name)
67
+ end
68
+
69
+ def link_or_button_method?(node)
70
+ CLICK_LINK_OR_BUTTON.include?(node.method_name)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -18,15 +18,13 @@ module RuboCop
18
18
  # @example
19
19
  # # bad
20
20
  # expect(current_path).to eq('/callback')
21
+ # expect(page.current_path).to eq('/callback')
21
22
  #
22
23
  # # good
23
- # expect(page).to have_current_path('/callback')
24
+ # expect(page).to have_current_path('/callback', ignore_query: true)
24
25
  #
25
- # # bad (does not support autocorrection)
26
- # expect(page.current_path).to match(variable)
27
- #
28
- # # good
29
- # expect(page).to have_current_path('/callback')
26
+ # # bad (does not support autocorrection when `match` with a variable)
27
+ # expect(page).to match(variable)
30
28
  #
31
29
  class CurrentPathExpectation < ::RuboCop::Cop::Base
32
30
  extend AutoCorrector
@@ -95,7 +93,7 @@ module RuboCop
95
93
  end
96
94
  corrector.replace(matcher_node.loc.selector, matcher_method)
97
95
  add_argument_parentheses(corrector, matcher_node.first_argument)
98
- add_ignore_query_options(corrector, node)
96
+ add_ignore_query_options(corrector, node, matcher_node)
99
97
  end
100
98
 
101
99
  def convert_regexp_node_to_literal(corrector, matcher_node, regexp_node)
@@ -129,18 +127,13 @@ module RuboCop
129
127
  # `have_current_path` with no options will include the querystring
130
128
  # while `page.current_path` does not.
131
129
  # This ensures the option `ignore_query: true` is added
132
- # except when the expectation is a regexp or string
133
- def add_ignore_query_options(corrector, node)
130
+ # except when `match` matcher.
131
+ def add_ignore_query_options(corrector, node, matcher_node)
132
+ return if matcher_node.method?(:match)
133
+
134
134
  expectation_node = node.parent.last_argument
135
135
  expectation_last_child = expectation_node.children.last
136
- return if %i[
137
- regexp str dstr xstr
138
- ].include?(expectation_last_child.type)
139
-
140
- corrector.insert_after(
141
- expectation_last_child,
142
- ', ignore_query: true'
143
- )
136
+ corrector.insert_after(expectation_last_child, ', ignore_query: true')
144
137
  end
145
138
  end
146
139
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ # Css selector parser.
7
+ # @api private
8
+ class CssAttributesParser
9
+ def initialize(selector)
10
+ @selector = selector
11
+ @state = :initial
12
+ @temp = ''
13
+ @results = {}
14
+ @bracket_count = 0
15
+ end
16
+
17
+ # @return [Array<String>]
18
+ def parse # rubocop:disable Metrics/MethodLength
19
+ @selector.chars do |char|
20
+ if char == '['
21
+ on_bracket_start
22
+ elsif char == ']'
23
+ on_bracket_end
24
+ elsif @state == :inside_attr
25
+ @temp += char
26
+ end
27
+ end
28
+ @results
29
+ end
30
+
31
+ private
32
+
33
+ def on_bracket_start
34
+ @bracket_count += 1
35
+ if @state == :initial
36
+ @state = :inside_attr
37
+ else
38
+ @temp += '['
39
+ end
40
+ end
41
+
42
+ def on_bracket_end
43
+ @bracket_count -= 1
44
+ if @bracket_count.zero?
45
+ @state = :initial
46
+ key, value = @temp.split('=')
47
+ @results[key] = normalize_value(value)
48
+ @temp.clear
49
+ else
50
+ @temp += ']'
51
+ end
52
+ end
53
+
54
+ # @param value [String]
55
+ # @return [Boolean, String]
56
+ # @example
57
+ # normalize_value('true') # => true
58
+ # normalize_value('false') # => false
59
+ # normalize_value(nil) # => nil
60
+ # normalize_value("foo") # => "'foo'"
61
+ def normalize_value(value)
62
+ case value
63
+ when 'true' then true
64
+ when 'false' then false
65
+ when nil then nil
66
+ else "'#{value.gsub(/"|'/, '')}'"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -55,15 +55,9 @@ module RuboCop
55
55
  # attributes('a[foo-bar_baz]') # => {"foo-bar_baz=>nil}
56
56
  # attributes('button[foo][bar=baz]') # => {"foo"=>nil, "bar"=>"'baz'"}
57
57
  # attributes('table[foo=bar]') # => {"foo"=>"'bar'"}
58
+ # attributes('[foo="bar[baz][qux]"]') # => {"foo"=>"'bar[baz][qux]'"}
58
59
  def attributes(selector)
59
- # Extract the inner strings of attributes.
60
- # For example, extract the following:
61
- # 'button[foo][bar=baz]' => 'foo][bar=baz'
62
- inside_attributes = selector.scan(/\[(.*)\]/).flatten.join
63
- inside_attributes.split('][').to_h do |attr|
64
- key, value = attr.split('=')
65
- [key, normalize_value(value)]
66
- end
60
+ CssAttributesParser.new(selector).parse
67
61
  end
68
62
 
69
63
  # @param selector [String]
@@ -89,22 +83,6 @@ module RuboCop
89
83
  normalize = selector.gsub(/(\\[>,+~]|\(.*\))/, '')
90
84
  normalize.match?(/[ >,+~]/)
91
85
  end
92
-
93
- # @param value [String]
94
- # @return [Boolean, String]
95
- # @example
96
- # normalize_value('true') # => true
97
- # normalize_value('false') # => false
98
- # normalize_value(nil) # => nil
99
- # normalize_value("foo") # => "'foo'"
100
- def normalize_value(value)
101
- case value
102
- when 'true' then true
103
- when 'false' then false
104
- when nil then nil
105
- else "'#{value.gsub(/"|'/, '')}'"
106
- end
107
- end
108
86
  end
109
87
  end
110
88
  end
@@ -5,16 +5,7 @@ module RuboCop
5
5
  module Capybara
6
6
  # Enforces use of `have_no_*` or `not_to` for negated expectations.
7
7
  #
8
- # @example EnforcedStyle: not_to (default)
9
- # # bad
10
- # expect(page).to have_no_selector
11
- # expect(page).to have_no_css('a')
12
- #
13
- # # good
14
- # expect(page).not_to have_selector
15
- # expect(page).not_to have_css('a')
16
- #
17
- # @example EnforcedStyle: have_no
8
+ # @example EnforcedStyle: have_no (default)
18
9
  # # bad
19
10
  # expect(page).not_to have_selector
20
11
  # expect(page).not_to have_css('a')
@@ -23,6 +14,15 @@ module RuboCop
23
14
  # expect(page).to have_no_selector
24
15
  # expect(page).to have_no_css('a')
25
16
  #
17
+ # @example EnforcedStyle: not_to
18
+ # # bad
19
+ # expect(page).to have_no_selector
20
+ # expect(page).to have_no_css('a')
21
+ #
22
+ # # good
23
+ # expect(page).not_to have_selector
24
+ # expect(page).not_to have_css('a')
25
+ #
26
26
  class NegationMatcher < ::RuboCop::Cop::Base
27
27
  extend AutoCorrector
28
28
  include ConfigurableEnforcedStyle
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ # Checks for redundant `within find(...)` calls.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # within find('foo.bar') do
11
+ # # ...
12
+ # end
13
+ #
14
+ # # good
15
+ # within 'foo.bar' do
16
+ # # ...
17
+ # end
18
+ #
19
+ # # bad
20
+ # within find_by_id('foo') do
21
+ # # ...
22
+ # end
23
+ #
24
+ # # good
25
+ # within '#foo' do
26
+ # # ...
27
+ # end
28
+ #
29
+ class RedundantWithinFind < ::RuboCop::Cop::Base
30
+ extend AutoCorrector
31
+ MSG = 'Redundant `within %<method>s(...)` call detected.'
32
+ RESTRICT_ON_SEND = %i[within].freeze
33
+ FIND_METHODS = Set.new(%i[find find_by_id]).freeze
34
+
35
+ # @!method within_find(node)
36
+ def_node_matcher :within_find, <<~PATTERN
37
+ (send nil? :within
38
+ $(send nil? %FIND_METHODS ...))
39
+ PATTERN
40
+
41
+ def on_send(node)
42
+ within_find(node) do |find_node|
43
+ add_offense(find_node, message: msg(find_node)) do |corrector|
44
+ corrector.replace(find_node, replaced(find_node))
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def msg(node)
52
+ format(MSG, method: node.method_name)
53
+ end
54
+
55
+ def replaced(node)
56
+ replaced = node.arguments.map(&:source).join(', ')
57
+ if node.method?(:find_by_id)
58
+ replaced.sub(/\A(["'])/, '\1#')
59
+ else
60
+ replaced
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ module RSpec
7
+ # Use `have_css` or `have_xpath` instead of `have_selector`.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # expect(foo).to have_selector(:css, 'bar')
12
+ #
13
+ # # good
14
+ # expect(foo).to have_css('bar')
15
+ #
16
+ # # bad
17
+ # expect(foo).to have_selector(:xpath, 'bar')
18
+ #
19
+ # # good
20
+ # expect(foo).to have_xpath('bar')
21
+ #
22
+ # @example DefaultSelector: css (default)
23
+ # # bad
24
+ # expect(foo).to have_selector('bar')
25
+ #
26
+ # # good
27
+ # expect(foo).to have_css('bar')
28
+ #
29
+ # @example DefaultSelector: xpath
30
+ # # bad
31
+ # expect(foo).to have_selector('bar')
32
+ #
33
+ # # good
34
+ # expect(foo).to have_xpath('bar')
35
+ #
36
+ class HaveSelector < ::RuboCop::Cop::Base
37
+ extend AutoCorrector
38
+ include RangeHelp
39
+
40
+ MSG = 'Use `%<good>s` instead of `have_selector`.'
41
+ RESTRICT_ON_SEND = %i[have_selector].freeze
42
+ SELECTORS = %i[css xpath].freeze
43
+
44
+ def on_send(node)
45
+ argument = node.first_argument
46
+ on_select_with_type(node, argument) if argument.sym_type?
47
+ on_select_without_type(node) if %i[str dstr].include?(argument.type)
48
+ end
49
+
50
+ private
51
+
52
+ def on_select_with_type(node, type)
53
+ return unless SELECTORS.include?(type.value)
54
+
55
+ add_offense(node, message: message_typed(type)) do |corrector|
56
+ corrector.remove(deletion_range(type, node.arguments[1]))
57
+ corrector.replace(node.loc.selector, "have_#{type.value}")
58
+ end
59
+ end
60
+
61
+ def message_typed(type)
62
+ format(MSG, good: "have_#{type.value}")
63
+ end
64
+
65
+ def deletion_range(first_argument, second_argument)
66
+ range_between(first_argument.source_range.begin_pos,
67
+ second_argument.source_range.begin_pos)
68
+ end
69
+
70
+ def on_select_without_type(node)
71
+ add_offense(node, message: message_untyped) do |corrector|
72
+ corrector.replace(node.loc.selector, "have_#{default_selector}")
73
+ end
74
+ end
75
+
76
+ def message_untyped
77
+ format(MSG, good: "have_#{default_selector}")
78
+ end
79
+
80
+ def default_selector
81
+ cop_config.fetch('DefaultSelector', 'css')
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ module RSpec
7
+ # A helper for `inflected` style
8
+ module InflectedHelper
9
+ extend NodePattern::Macros
10
+
11
+ EXPLICIT_MATCHER = %w[css selector style xpath].map do |suffix|
12
+ "matches_#{suffix}?".to_sym
13
+ end.freeze
14
+ MSG_INFLECTED = 'Prefer using `%<matcher_name>s` matcher over ' \
15
+ '`%<predicate_name>s`.'
16
+
17
+ private
18
+
19
+ def check_inflected(node)
20
+ predicate_in_actual?(node) do |predicate, to, matcher|
21
+ msg = message_inflected(predicate)
22
+ add_offense(node, message: msg) do |corrector|
23
+ remove_predicate(corrector, predicate)
24
+ corrector.replace(node.loc.selector,
25
+ true?(to, matcher) ? 'to' : 'not_to')
26
+ rewrite_matcher(corrector, predicate, matcher)
27
+ end
28
+ end
29
+ end
30
+
31
+ # @!method predicate_in_actual?(node)
32
+ def_node_matcher :predicate_in_actual?, <<~PATTERN
33
+ (send
34
+ (send nil? :expect
35
+ {
36
+ (block $(send !nil? #predicate? ...) ...)
37
+ $(send !nil? #predicate? ...)
38
+ }
39
+ )
40
+ ${:to :to_not :not_to}
41
+ $#boolean_matcher?
42
+ )
43
+ PATTERN
44
+
45
+ # @!method be_bool?(node)
46
+ def_node_matcher :be_bool?, <<~PATTERN
47
+ (send nil? {:be :eq :eql :equal} {true false})
48
+ PATTERN
49
+
50
+ # @!method be_boolthy?(node)
51
+ def_node_matcher :be_boolthy?, <<~PATTERN
52
+ (send nil? {:be_truthy :be_falsey :be_falsy :a_truthy_value :a_falsey_value :a_falsy_value})
53
+ PATTERN
54
+
55
+ def boolean_matcher?(node)
56
+ if cop_config['Strict']
57
+ be_boolthy?(node)
58
+ else
59
+ be_bool?(node) || be_boolthy?(node)
60
+ end
61
+ end
62
+
63
+ def predicate?(sym)
64
+ EXPLICIT_MATCHER.include?(sym)
65
+ end
66
+
67
+ def message_inflected(predicate)
68
+ format(MSG_INFLECTED,
69
+ predicate_name: predicate.method_name,
70
+ matcher_name: to_predicate_matcher(predicate.method_name))
71
+ end
72
+
73
+ def to_predicate_matcher(name)
74
+ name.to_s.sub('matches_', 'match_')[0..-2]
75
+ end
76
+
77
+ def remove_predicate(corrector, predicate)
78
+ range = predicate.loc.dot.with(
79
+ end_pos: predicate.source_range.end_pos
80
+ )
81
+
82
+ corrector.remove(range)
83
+ end
84
+
85
+ def rewrite_matcher(corrector, predicate, matcher)
86
+ args = args_loc(predicate).source
87
+
88
+ corrector.replace(
89
+ matcher,
90
+ to_predicate_matcher(predicate.method_name) + args
91
+ )
92
+ end
93
+
94
+ def true?(to_symbol, matcher)
95
+ result = case matcher.method_name
96
+ when :be, :eq
97
+ matcher.first_argument.true_type?
98
+ when :be_truthy, :a_truthy_value
99
+ true
100
+ when :be_falsey, :be_falsy, :a_falsey_value, :a_falsy_value
101
+ false
102
+ end
103
+ to_symbol == :to ? result : !result
104
+ end
105
+ end
106
+
107
+ # A helper for `explicit` style
108
+ module ExplicitHelper
109
+ extend NodePattern::Macros
110
+
111
+ MSG_EXPLICIT = 'Prefer using `%<predicate_name>s` over ' \
112
+ '`%<matcher_name>s` matcher.'
113
+ BUILT_IN_MATCHERS = %w[
114
+ be_truthy be_falsey be_falsy
115
+ have_attributes have_received
116
+ be_between be_within
117
+ ].freeze
118
+ INFLECTED_MATCHER = %w[css selector style xpath].each.map do |suffix|
119
+ "match_#{suffix}"
120
+ end.freeze
121
+
122
+ private
123
+
124
+ def allowed_explicit_matchers
125
+ cop_config.fetch('AllowedExplicitMatchers', []) + BUILT_IN_MATCHERS
126
+ end
127
+
128
+ def check_explicit(node) # rubocop:disable Metrics/MethodLength
129
+ predicate_matcher?(node) do |actual, matcher|
130
+ add_offense(node,
131
+ message: message_explicit(matcher)) do |corrector|
132
+ corrector_explicit(corrector, node, actual, matcher)
133
+ end
134
+ end
135
+ end
136
+
137
+ # @!method predicate_matcher?(node)
138
+ def_node_matcher :predicate_matcher?, <<~PATTERN
139
+ (send
140
+ (send nil? :expect $!nil?)
141
+ {:to :to_not :not_to}
142
+ {
143
+ $(send nil? #predicate_matcher_name? ...)
144
+ (block $(send nil? #predicate_matcher_name? ...) ...)
145
+ }
146
+ )
147
+ PATTERN
148
+
149
+ def predicate_matcher_name?(name)
150
+ name = name.to_s
151
+ return false if allowed_explicit_matchers.include?(name)
152
+
153
+ INFLECTED_MATCHER.include?(name)
154
+ end
155
+
156
+ def message_explicit(matcher)
157
+ format(MSG_EXPLICIT,
158
+ predicate_name: to_predicate_method(matcher.method_name),
159
+ matcher_name: matcher.method_name)
160
+ end
161
+
162
+ def corrector_explicit(corrector, to_node, actual, matcher)
163
+ replacement_matcher = replacement_matcher(to_node)
164
+ corrector.replace(matcher, replacement_matcher)
165
+ move_predicate(corrector, actual, matcher)
166
+ corrector.replace(to_node.loc.selector, 'to')
167
+ end
168
+
169
+ def move_predicate(corrector, actual, matcher)
170
+ predicate = to_predicate_method(matcher.method_name)
171
+ args = args_loc(matcher).source
172
+ corrector.insert_after(actual,
173
+ ".#{predicate}" + args)
174
+ end
175
+
176
+ def to_predicate_method(matcher)
177
+ "#{matcher.to_s.sub('match_', 'matches_')}?"
178
+ end
179
+
180
+ def replacement_matcher(node)
181
+ case [cop_config['Strict'], node.method?(:to)]
182
+ when [true, true]
183
+ 'be(true)'
184
+ when [true, false]
185
+ 'be(false)'
186
+ when [false, true]
187
+ 'be_truthy'
188
+ when [false, false]
189
+ 'be_falsey'
190
+ end
191
+ end
192
+ end
193
+
194
+ # Prefer using predicate matcher over using predicate method directly.
195
+ #
196
+ # Capybara defines magic matchers for predicate methods.
197
+ # This cop recommends to use the predicate matcher instead of using
198
+ # predicate method directly.
199
+ #
200
+ # @example Strict: true, EnforcedStyle: inflected (default)
201
+ # # bad
202
+ # expect(foo.matches_css?(bar: 'baz')).to be_truthy
203
+ # expect(foo.matches_selector?(bar: 'baz')).to be_truthy
204
+ # expect(foo.matches_style?(bar: 'baz')).to be_truthy
205
+ # expect(foo.matches_xpath?(bar: 'baz')).to be_truthy
206
+ #
207
+ # # good
208
+ # expect(foo).to match_css(bar: 'baz')
209
+ # expect(foo).to match_selector(bar: 'baz')
210
+ # expect(foo).to match_style(bar: 'baz')
211
+ # expect(foo).to match_xpath(bar: 'baz')
212
+ #
213
+ # # also good - It checks "true" strictly.
214
+ # expect(foo.matches_style?(bar: 'baz')).to be(true)
215
+ #
216
+ # @example Strict: false, EnforcedStyle: inflected
217
+ # # bad
218
+ # expect(foo.matches_style?(bar: 'baz')).to be_truthy
219
+ # expect(foo.matches_style?(bar: 'baz')).to be(true)
220
+ #
221
+ # # good
222
+ # expect(foo).to match_style(bar: 'baz')
223
+ #
224
+ # @example Strict: true, EnforcedStyle: explicit
225
+ # # bad
226
+ # expect(foo).to match_style(bar: 'baz')
227
+ #
228
+ # # good - the above code is rewritten to it by this cop
229
+ # expect(foo.matches_style?(bar: 'baz')).to be(true)
230
+ #
231
+ # @example Strict: false, EnforcedStyle: explicit
232
+ # # bad
233
+ # expect(foo).to match_style(bar: 'baz')
234
+ #
235
+ # # good - the above code is rewritten to it by this cop
236
+ # expect(foo.matches_style?(bar: 'baz')).to be_truthy
237
+ #
238
+ class PredicateMatcher < ::RuboCop::Cop::Base
239
+ extend AutoCorrector
240
+ include ConfigurableEnforcedStyle
241
+ include InflectedHelper
242
+ include ExplicitHelper
243
+
244
+ RESTRICT_ON_SEND = %i[to to_not not_to].freeze
245
+
246
+ def on_send(node)
247
+ if style == :inflected
248
+ check_inflected(node)
249
+ elsif style == :explicit
250
+ check_explicit(node)
251
+ end
252
+ end
253
+
254
+ private
255
+
256
+ # returns args location with whitespace
257
+ # @example
258
+ # foo 1, 2
259
+ # ^^^^^
260
+ def args_loc(send_node)
261
+ send_node.loc.selector.end.with(
262
+ end_pos: send_node.source_range.end_pos
263
+ )
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
@@ -8,15 +8,14 @@ module RuboCop
8
8
  # @example
9
9
  # # bad
10
10
  # find('#some-id')
11
- # find('[visible][id=some-id]')
11
+ # find('[id=some-id]')
12
+ # find(:css, '#some-id')
12
13
  #
13
14
  # # good
14
15
  # find_by_id('some-id')
15
- # find_by_id('some-id', visible: true)
16
16
  #
17
17
  class SpecificFinders < ::RuboCop::Cop::Base
18
18
  extend AutoCorrector
19
-
20
19
  include RangeHelp
21
20
 
22
21
  MSG = 'Prefer `find_by_id` over `find`.'
@@ -24,7 +23,7 @@ module RuboCop
24
23
 
25
24
  # @!method find_argument(node)
26
25
  def_node_matcher :find_argument, <<~PATTERN
27
- (send _ :find (str $_) ...)
26
+ (send _ :find $(sym :css)? (str $_) ...)
28
27
  PATTERN
29
28
 
30
29
  # @!method class_options(node)
@@ -33,30 +32,30 @@ module RuboCop
33
32
  PATTERN
34
33
 
35
34
  def on_send(node)
36
- find_argument(node) do |arg|
35
+ find_argument(node) do |sym, arg|
37
36
  next if CssSelector.pseudo_classes(arg).any?
38
37
  next if CssSelector.multiple_selectors?(arg)
39
38
 
40
- on_attr(node, arg) if attribute?(arg)
41
- on_id(node, arg) if CssSelector.id?(arg)
39
+ on_attr(node, sym, arg) if attribute?(arg)
40
+ on_id(node, sym, arg) if CssSelector.id?(arg)
42
41
  end
43
42
  end
44
43
 
45
44
  private
46
45
 
47
- def on_attr(node, arg)
46
+ def on_attr(node, sym, arg)
48
47
  attrs = CssSelector.attributes(arg)
49
48
  return unless (id = attrs['id'])
50
49
  return if attrs['class']
51
50
 
52
- register_offense(node, replaced_arguments(arg, id))
51
+ register_offense(node, sym, replaced_arguments(arg, id))
53
52
  end
54
53
 
55
- def on_id(node, arg)
54
+ def on_id(node, sym, arg)
56
55
  return if CssSelector.attributes(arg).any?
57
56
 
58
57
  id = CssSelector.id(arg)
59
- register_offense(node, "'#{id}'",
58
+ register_offense(node, sym, "'#{id}'",
60
59
  CssSelector.classes(arg.sub("##{id}", '')))
61
60
  end
62
61
 
@@ -65,17 +64,22 @@ module RuboCop
65
64
  CapybaraHelp.common_attributes?(arg)
66
65
  end
67
66
 
68
- def register_offense(node, id, classes = [])
67
+ def register_offense(node, sym, id, classes = [])
69
68
  add_offense(offense_range(node)) do |corrector|
70
69
  corrector.replace(node.loc.selector, 'find_by_id')
71
- corrector.replace(node.first_argument,
72
- id.delete('\\'))
70
+ corrector.replace(node.first_argument, id.delete('\\'))
73
71
  unless classes.compact.empty?
74
72
  autocorrect_classes(corrector, node, classes)
75
73
  end
74
+ corrector.remove(deletion_range(node)) unless sym.empty?
76
75
  end
77
76
  end
78
77
 
78
+ def deletion_range(node)
79
+ range_between(node.first_argument.source_range.end_pos,
80
+ node.arguments[1].source_range.end_pos)
81
+ end
82
+
79
83
  def autocorrect_classes(corrector, node, classes)
80
84
  if (options = class_options(node).first)
81
85
  append_options(classes, options)
@@ -42,6 +42,11 @@ module RuboCop
42
42
  (send nil? _ (str $_) ... )
43
43
  PATTERN
44
44
 
45
+ # @!method text_with_regexp?(node)
46
+ def_node_search :text_with_regexp?, <<-PATTERN
47
+ (pair (sym {:text :exact_text}) (regexp ...))
48
+ PATTERN
49
+
45
50
  def on_send(node)
46
51
  first_argument(node) do |arg|
47
52
  next unless (matcher = specific_matcher(arg))
@@ -61,6 +66,7 @@ module RuboCop
61
66
 
62
67
  def replaceable?(node, arg, matcher)
63
68
  replaceable_attributes?(arg) &&
69
+ !text_with_regexp?(node) &&
64
70
  CapybaraHelp.replaceable_option?(node, arg, matcher) &&
65
71
  CapybaraHelp.replaceable_pseudo_classes?(arg)
66
72
  end
@@ -1,8 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'capybara/rspec/have_selector'
4
+ require_relative 'capybara/rspec/predicate_matcher'
5
+
6
+ require_relative 'capybara/click_link_or_button_style'
3
7
  require_relative 'capybara/current_path_expectation'
4
8
  require_relative 'capybara/match_style'
5
9
  require_relative 'capybara/negation_matcher'
10
+ require_relative 'capybara/redundant_within_find'
6
11
  require_relative 'capybara/specific_actions'
7
12
  require_relative 'capybara/specific_finders'
8
13
  require_relative 'capybara/specific_matcher'
@@ -6,6 +6,7 @@ require 'yaml'
6
6
  require 'rubocop'
7
7
 
8
8
  require_relative 'rubocop/cop/capybara/mixin/capybara_help'
9
+ require_relative 'rubocop/cop/capybara/mixin/css_attributes_parser'
9
10
  require_relative 'rubocop/cop/capybara/mixin/css_selector'
10
11
 
11
12
  require_relative 'rubocop/cop/capybara_cops'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-capybara
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.18.0
4
+ version: 2.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-21 00:00:00.000000000 Z
11
+ date: 2024-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -43,11 +43,16 @@ files:
43
43
  - lib/rubocop/capybara/config_formatter.rb
44
44
  - lib/rubocop/capybara/description_extractor.rb
45
45
  - lib/rubocop/capybara/version.rb
46
+ - lib/rubocop/cop/capybara/click_link_or_button_style.rb
46
47
  - lib/rubocop/cop/capybara/current_path_expectation.rb
47
48
  - lib/rubocop/cop/capybara/match_style.rb
48
49
  - lib/rubocop/cop/capybara/mixin/capybara_help.rb
50
+ - lib/rubocop/cop/capybara/mixin/css_attributes_parser.rb
49
51
  - lib/rubocop/cop/capybara/mixin/css_selector.rb
50
52
  - lib/rubocop/cop/capybara/negation_matcher.rb
53
+ - lib/rubocop/cop/capybara/redundant_within_find.rb
54
+ - lib/rubocop/cop/capybara/rspec/have_selector.rb
55
+ - lib/rubocop/cop/capybara/rspec/predicate_matcher.rb
51
56
  - lib/rubocop/cop/capybara/specific_actions.rb
52
57
  - lib/rubocop/cop/capybara/specific_finders.rb
53
58
  - lib/rubocop/cop/capybara/specific_matcher.rb
@@ -68,14 +73,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
68
73
  requirements:
69
74
  - - ">="
70
75
  - !ruby/object:Gem::Version
71
- version: 2.6.0
76
+ version: 2.7.0
72
77
  required_rubygems_version: !ruby/object:Gem::Requirement
73
78
  requirements:
74
79
  - - ">="
75
80
  - !ruby/object:Gem::Version
76
81
  version: '0'
77
82
  requirements: []
78
- rubygems_version: 3.4.5
83
+ rubygems_version: 3.4.22
79
84
  signing_key:
80
85
  specification_version: 4
81
86
  summary: Code style checking for Capybara test files