rubocop-capybara 2.17.0 → 2.18.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: a9046dc7fd875441eeb556b848c7fc36099c2ab694b12a6cf5834ff191937a16
4
- data.tar.gz: 354393ab09a0849d1c4c7398116827627c5b385e377f5337a135d3d5ff338b08
3
+ metadata.gz: bb7f795cc80f072b94781f2339e781cccf9922ad2303c09111c0e34a4af264f8
4
+ data.tar.gz: d2ae47b522208177b7c93338973c8a144dfbccca2f817f0797b531f8c89e50b9
5
5
  SHA512:
6
- metadata.gz: 7fab06dac5d6ba1989c2c0bf47cd0a674e067baf7265378b8d4493055a1597087a442b79d88b931eadaa0f4ebd5c78f5d1929a116b12afa0d9c20110506651cd
7
- data.tar.gz: fd8f7cfebf049c450b25e20b76d76fba8ddbb1a50d626644e740e042589cdd2b1d406ea410df1d558439fc2d66a2b37733ce4ebd409f7f83568cd96dea27c61a
6
+ metadata.gz: 36b58f29500840c7dc34c2c2ed55856ff08cdd7ca41822209a9388204948903be89cf13c3065da7cfed3e8332f3433b3293087c8de1cf1266f093fadef2fa69d
7
+ data.tar.gz: 5b961abac7ae8dd46708ec9649718a23596bde9e45bcc48f7d9e818c2a116d5fda3de0653cf3aa8e56d11ed11ebcb94e24ef49ee0653e739a26d43d2383c2152
data/CHANGELOG.md CHANGED
@@ -2,11 +2,23 @@
2
2
 
3
3
  ## Edge (Unreleased)
4
4
 
5
- ## 2.17.0 (TBD)
5
+ ## 2.18.0 (2023-04-21)
6
+
7
+ - Fix an offense message for `Capybara/SpecificFinders`. ([@ydah])
8
+ - Expand `Capybara/NegationMatcher` to support `have_content` ([@OskarsEzerins])
9
+ - Fix an incorrect autocorrect for `Capybara/CurrentPathExpectation` when matcher's argument is a method with a argument and no parentheses. ([@ydah])
10
+
11
+ ## 2.17.1 (2023-02-13)
12
+
13
+ - Fix an incorrect autocorrect for `Capybara/CurrentPathExpectation`. ([@ydah])
14
+ - Fix a false negative for `Capybara/CurrentPathExpectation` when using `match`. ([@ydah])
15
+ - Fix a false positive and incorrect autocorrect for `Capybara/SpecificActions`, `Capybara/SpecificFinders` and `Capybara/SpecificMatcher`. ([@ydah])
16
+
17
+ ## 2.17.0 (2022-12-29)
6
18
 
7
19
  - Extracted from `rubocop-rspec` into a separate repository for easier use with Minitest/Cucumber. ([@pirj])
8
20
 
9
- ## Previously (see rubocop-rspec's changelist for details)
21
+ ## Previously (see [rubocop-rspec's changelist](https://github.com/rubocop/rubocop-rspec/blob/9558719/CHANGELOG.md) for details)
10
22
 
11
23
  - Fix a false positive for `Capybara/SpecificMatcher` when `have_css("a")` without attribute. ([@ydah])
12
24
  - Add new `Capybara/NegationMatcher` cop. ([@ydah])
@@ -41,6 +53,7 @@
41
53
  [@bquorning]: https://github.com/bquorning
42
54
  [@darhazer]: https://github.com/Darhazer
43
55
  [@onumis]: https://github.com/onumis
56
+ [@oskarsezerins]: https://github.com/OskarsEzerins
44
57
  [@pirj]: https://github.com/pirj
45
58
  [@rspeicher]: https://github.com/rspeicher
46
59
  [@timrogers]: https://github.com/timrogers
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Gem Version](https://badge.fury.io/rb/rubocop-capybara.svg)](https://rubygems.org/gems/rubocop-capybara)
5
5
  ![CI](https://github.com/rubocop/rubocop-capybara/workflows/CI/badge.svg)
6
6
 
7
- Capybara-specific analysis for your projects, as an extension to
7
+ [Capybara](https://teamcapybara.github.io/capybara)-specific analysis for your projects, as an extension to
8
8
  [RuboCop](https://github.com/rubocop/rubocop).
9
9
 
10
10
  ## Installation
data/config/default.yml CHANGED
@@ -6,6 +6,8 @@ Capybara:
6
6
  - "**/*_spec.rb"
7
7
  - "**/spec/**/*"
8
8
  - "**/test/**/*"
9
+ - "**/*_steps.rb"
10
+ - "**/features/step_definitions/**/*"
9
11
 
10
12
  Capybara/CurrentPathExpectation:
11
13
  Description: Checks that no expectations are set on Capybara's `current_path`.
@@ -17,7 +19,7 @@ Capybara/CurrentPathExpectation:
17
19
  Capybara/MatchStyle:
18
20
  Description: Checks for usage of deprecated style methods.
19
21
  Enabled: pending
20
- VersionAdded: "<<next>>"
22
+ VersionAdded: '2.17'
21
23
  Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/MatchStyle
22
24
 
23
25
  Capybara/NegationMatcher:
@@ -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.17.0'
7
+ STRING = '2.18.0'
8
8
  end
9
9
  end
10
10
  end
@@ -6,10 +6,10 @@ module RuboCop
6
6
  # Checks that no expectations are set on Capybara's `current_path`.
7
7
  #
8
8
  # The
9
- # https://www.rubydoc.info/github/teamcapybara/capybara/main/Capybara/RSpecMatchers#have_current_path-instance_method[`have_current_path` matcher]
9
+ # https://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/RSpecMatchers#have_current_path-instance_method[`have_current_path` matcher]
10
10
  # should be used on `page` to set expectations on Capybara's
11
11
  # current path, since it uses
12
- # https://github.com/teamcapybara/capybara/blob/main/README.md#asynchronous-javascript-ajax-and-friends[Capybara's waiting functionality]
12
+ # https://github.com/teamcapybara/capybara/blob/master/README.md#asynchronous-javascript-ajax-and-friends[Capybara's waiting functionality]
13
13
  # which ensures that preceding actions (like `click_link`) have
14
14
  # completed.
15
15
  #
@@ -30,6 +30,7 @@ module RuboCop
30
30
  #
31
31
  class CurrentPathExpectation < ::RuboCop::Cop::Base
32
32
  extend AutoCorrector
33
+ include RangeHelp
33
34
 
34
35
  MSG = 'Do not set an RSpec expectation on `current_path` in ' \
35
36
  'Capybara feature specs - instead, use the ' \
@@ -50,11 +51,11 @@ module RuboCop
50
51
  ${(send nil? :eq ...) (send nil? :match (regexp ...))})
51
52
  PATTERN
52
53
 
53
- # @!method regexp_str_matcher(node)
54
- def_node_matcher :regexp_str_matcher, <<-PATTERN
54
+ # @!method regexp_node_matcher(node)
55
+ def_node_matcher :regexp_node_matcher, <<-PATTERN
55
56
  (send
56
57
  #expectation_set_on_current_path ${:to :to_not :not_to}
57
- $(send nil? :match (str $_)))
58
+ $(send nil? :match ${str dstr xstr}))
58
59
  PATTERN
59
60
 
60
61
  def self.autocorrect_incompatible_with
@@ -78,15 +79,14 @@ module RuboCop
78
79
  rewrite_expectation(corrector, node, to_sym, matcher_node)
79
80
  end
80
81
 
81
- regexp_str_matcher(node.parent) do |to_sym, matcher_node, regexp|
82
+ regexp_node_matcher(node.parent) do |to_sym, matcher_node, regexp|
82
83
  rewrite_expectation(corrector, node, to_sym, matcher_node)
83
- convert_regexp_str_to_literal(corrector, matcher_node, regexp)
84
+ convert_regexp_node_to_literal(corrector, matcher_node, regexp)
84
85
  end
85
86
  end
86
87
 
87
88
  def rewrite_expectation(corrector, node, to_symbol, matcher_node)
88
- current_path_node = node.first_argument
89
- corrector.replace(current_path_node, 'page')
89
+ corrector.replace(node.first_argument, 'page')
90
90
  corrector.replace(node.parent.loc.selector, 'to')
91
91
  matcher_method = if to_symbol == :to
92
92
  'have_current_path'
@@ -94,15 +94,38 @@ module RuboCop
94
94
  'have_no_current_path'
95
95
  end
96
96
  corrector.replace(matcher_node.loc.selector, matcher_method)
97
+ add_argument_parentheses(corrector, matcher_node.first_argument)
97
98
  add_ignore_query_options(corrector, node)
98
99
  end
99
100
 
100
- def convert_regexp_str_to_literal(corrector, matcher_node, regexp_str)
101
+ def convert_regexp_node_to_literal(corrector, matcher_node, regexp_node)
101
102
  str_node = matcher_node.first_argument
102
- regexp_expr = Regexp.new(regexp_str).inspect
103
+ regexp_expr = regexp_node_to_regexp_expr(regexp_node)
103
104
  corrector.replace(str_node, regexp_expr)
104
105
  end
105
106
 
107
+ def regexp_node_to_regexp_expr(regexp_node)
108
+ if regexp_node.xstr_type?
109
+ "/\#{`#{regexp_node.value.value}`}/"
110
+ else
111
+ Regexp.new(regexp_node.value).inspect
112
+ end
113
+ end
114
+
115
+ def add_argument_parentheses(corrector, arg_node)
116
+ return unless method_call_with_no_parentheses?(arg_node)
117
+
118
+ first_argument_range = range_with_surrounding_space(
119
+ arg_node.first_argument.source_range, side: :left
120
+ )
121
+ corrector.insert_before(first_argument_range, '(')
122
+ corrector.insert_after(arg_node.last_argument, ')')
123
+ end
124
+
125
+ def method_call_with_no_parentheses?(arg_node)
126
+ arg_node.send_type? && arg_node.arguments? && !arg_node.parenthesized?
127
+ end
128
+
106
129
  # `have_current_path` with no options will include the querystring
107
130
  # while `page.current_path` does not.
108
131
  # This ensures the option `ignore_query: true` is added
@@ -110,7 +133,9 @@ module RuboCop
110
133
  def add_ignore_query_options(corrector, node)
111
134
  expectation_node = node.parent.last_argument
112
135
  expectation_last_child = expectation_node.children.last
113
- return if %i[regexp str].include?(expectation_last_child.type)
136
+ return if %i[
137
+ regexp str dstr xstr
138
+ ].include?(expectation_last_child.type)
114
139
 
115
140
  corrector.insert_after(
116
141
  expectation_last_child,
@@ -2,76 +2,129 @@
2
2
 
3
3
  module RuboCop
4
4
  module Cop
5
- # Help methods for capybara.
6
- module CapybaraHelp
7
- module_function
5
+ module Capybara
6
+ # Help methods for capybara.
7
+ # @api private
8
+ module CapybaraHelp
9
+ COMMON_OPTIONS = %w[
10
+ id class style
11
+ ].freeze
12
+ SPECIFIC_OPTIONS = {
13
+ 'button' => (
14
+ COMMON_OPTIONS + %w[disabled name value title type]
15
+ ).freeze,
16
+ 'link' => (
17
+ COMMON_OPTIONS + %w[href alt title download]
18
+ ).freeze,
19
+ 'table' => (
20
+ COMMON_OPTIONS + %w[cols rows]
21
+ ).freeze,
22
+ 'select' => (
23
+ COMMON_OPTIONS + %w[
24
+ disabled name placeholder
25
+ selected multiple
26
+ ]
27
+ ).freeze,
28
+ 'field' => (
29
+ COMMON_OPTIONS + %w[
30
+ checked disabled name placeholder
31
+ readonly type multiple
32
+ ]
33
+ ).freeze
34
+ }.freeze
35
+ SPECIFIC_PSEUDO_CLASSES = %w[
36
+ not() disabled enabled checked unchecked
37
+ ].freeze
8
38
 
9
- # @param node [RuboCop::AST::SendNode]
10
- # @param locator [String]
11
- # @param element [String]
12
- # @return [Boolean]
13
- def specific_option?(node, locator, element)
14
- attrs = CssSelector.attributes(locator).keys
15
- return false unless replaceable_element?(node, element, attrs)
39
+ module_function
16
40
 
17
- attrs.all? do |attr|
18
- CssSelector.specific_options?(element, attr)
41
+ # @param node [RuboCop::AST::SendNode]
42
+ # @param locator [String]
43
+ # @param element [String]
44
+ # @return [Boolean]
45
+ def replaceable_option?(node, locator, element)
46
+ attrs = CssSelector.attributes(locator).keys
47
+ return false unless replaceable_element?(node, element, attrs)
48
+
49
+ attrs.all? do |attr|
50
+ SPECIFIC_OPTIONS.fetch(element, []).include?(attr)
51
+ end
19
52
  end
20
- end
21
53
 
22
- # @param locator [String]
23
- # @return [Boolean]
24
- def specific_pseudo_classes?(locator)
25
- CssSelector.pseudo_classes(locator).all? do |pseudo_class|
26
- replaceable_pseudo_class?(pseudo_class, locator)
54
+ # @param selector [String]
55
+ # @return [Boolean]
56
+ # @example
57
+ # common_attributes?('a[focused]') # => true
58
+ # common_attributes?('button[focused][visible]') # => true
59
+ # common_attributes?('table[id=some-id]') # => true
60
+ # common_attributes?('h1[invalid]') # => false
61
+ def common_attributes?(selector)
62
+ CssSelector.attributes(selector).keys.difference(COMMON_OPTIONS).none?
27
63
  end
28
- end
29
64
 
30
- # @param pseudo_class [String]
31
- # @param locator [String]
32
- # @return [Boolean]
33
- def replaceable_pseudo_class?(pseudo_class, locator)
34
- return false unless CssSelector.specific_pesudo_classes?(pseudo_class)
65
+ # @param attrs [Array<String>]
66
+ # @return [Boolean]
67
+ # @example
68
+ # replaceable_attributes?('table[id=some-id]') # => true
69
+ # replaceable_attributes?('a[focused]') # => false
70
+ def replaceable_attributes?(attrs)
71
+ attrs.values.none?(&:nil?)
72
+ end
35
73
 
36
- case pseudo_class
37
- when 'not()' then replaceable_pseudo_class_not?(locator)
38
- else true
74
+ # @param locator [String]
75
+ # @return [Boolean]
76
+ def replaceable_pseudo_classes?(locator)
77
+ CssSelector.pseudo_classes(locator).all? do |pseudo_class|
78
+ replaceable_pseudo_class?(pseudo_class, locator)
79
+ end
39
80
  end
40
- end
41
81
 
42
- # @param locator [String]
43
- # @return [Boolean]
44
- def replaceable_pseudo_class_not?(locator)
45
- locator.scan(/not\(.*?\)/).all? do |negation|
46
- CssSelector.attributes(negation).values.all? do |v|
47
- v.is_a?(TrueClass) || v.is_a?(FalseClass)
82
+ # @param pseudo_class [String]
83
+ # @param locator [String]
84
+ # @return [Boolean]
85
+ def replaceable_pseudo_class?(pseudo_class, locator)
86
+ return false unless SPECIFIC_PSEUDO_CLASSES.include?(pseudo_class)
87
+
88
+ case pseudo_class
89
+ when 'not()' then replaceable_pseudo_class_not?(locator)
90
+ else true
48
91
  end
49
92
  end
50
- end
51
93
 
52
- # @param node [RuboCop::AST::SendNode]
53
- # @param element [String]
54
- # @param attrs [Array<String>]
55
- # @return [Boolean]
56
- def replaceable_element?(node, element, attrs)
57
- case element
58
- when 'link' then replaceable_to_link?(node, attrs)
59
- else true
94
+ # @param locator [String]
95
+ # @return [Boolean]
96
+ def replaceable_pseudo_class_not?(locator)
97
+ locator.scan(/not\(.*?\)/).all? do |negation|
98
+ CssSelector.attributes(negation).values.all? do |v|
99
+ v.is_a?(TrueClass) || v.is_a?(FalseClass)
100
+ end
101
+ end
60
102
  end
61
- end
62
103
 
63
- # @param node [RuboCop::AST::SendNode]
64
- # @param attrs [Array<String>]
65
- # @return [Boolean]
66
- def replaceable_to_link?(node, attrs)
67
- include_option?(node, :href) || attrs.include?('href')
68
- end
104
+ # @param node [RuboCop::AST::SendNode]
105
+ # @param element [String]
106
+ # @param attrs [Array<String>]
107
+ # @return [Boolean]
108
+ def replaceable_element?(node, element, attrs)
109
+ case element
110
+ when 'link' then replaceable_to_link?(node, attrs)
111
+ else true
112
+ end
113
+ end
114
+
115
+ # @param node [RuboCop::AST::SendNode]
116
+ # @param attrs [Array<String>]
117
+ # @return [Boolean]
118
+ def replaceable_to_link?(node, attrs)
119
+ include_option?(node, :href) || attrs.include?('href')
120
+ end
69
121
 
70
- # @param node [RuboCop::AST::SendNode]
71
- # @param option [Symbol]
72
- # @return [Boolean]
73
- def include_option?(node, option)
74
- node.each_descendant(:sym).find { |opt| opt.value == option }
122
+ # @param node [RuboCop::AST::SendNode]
123
+ # @param option [Symbol]
124
+ # @return [Boolean]
125
+ def include_option?(node, option)
126
+ node.each_descendant(:sym).find { |opt| opt.value == option }
127
+ end
75
128
  end
76
129
  end
77
130
  end
@@ -2,141 +2,108 @@
2
2
 
3
3
  module RuboCop
4
4
  module Cop
5
- # Helps parsing css selector.
6
- module CssSelector
7
- COMMON_OPTIONS = %w[
8
- above below left_of right_of near count minimum maximum between text
9
- id class style visible obscured exact exact_text normalize_ws match
10
- wait filter_set focused
11
- ].freeze
12
- SPECIFIC_OPTIONS = {
13
- 'button' => (
14
- COMMON_OPTIONS + %w[disabled name value title type]
15
- ).freeze,
16
- 'link' => (
17
- COMMON_OPTIONS + %w[href alt title download]
18
- ).freeze,
19
- 'table' => (
20
- COMMON_OPTIONS + %w[
21
- caption with_cols cols with_rows rows
22
- ]
23
- ).freeze,
24
- 'select' => (
25
- COMMON_OPTIONS + %w[
26
- disabled name placeholder options enabled_options
27
- disabled_options selected with_selected multiple with_options
28
- ]
29
- ).freeze,
30
- 'field' => (
31
- COMMON_OPTIONS + %w[
32
- checked unchecked disabled valid name placeholder
33
- validation_message readonly with type multiple
34
- ]
35
- ).freeze
36
- }.freeze
37
- SPECIFIC_PSEUDO_CLASSES = %w[
38
- not() disabled enabled checked unchecked
39
- ].freeze
5
+ module Capybara
6
+ # Helps parsing css selector.
7
+ # @api private
8
+ module CssSelector
9
+ module_function
40
10
 
41
- module_function
11
+ # @param selector [String]
12
+ # @return [String]
13
+ # @example
14
+ # id('#some-id') # => some-id
15
+ # id('.some-cls') # => nil
16
+ # id('#some-id.cls') # => some-id
17
+ def id(selector)
18
+ return unless id?(selector)
42
19
 
43
- # @param element [String]
44
- # @param attribute [String]
45
- # @return [Boolean]
46
- # @example
47
- # specific_pesudo_classes?('button', 'name') # => true
48
- # specific_pesudo_classes?('link', 'invalid') # => false
49
- def specific_options?(element, attribute)
50
- SPECIFIC_OPTIONS.fetch(element, []).include?(attribute)
51
- end
52
-
53
- # @param pseudo_class [String]
54
- # @return [Boolean]
55
- # @example
56
- # specific_pesudo_classes?('disabled') # => true
57
- # specific_pesudo_classes?('first-of-type') # => false
58
- def specific_pesudo_classes?(pseudo_class)
59
- SPECIFIC_PSEUDO_CLASSES.include?(pseudo_class)
60
- end
20
+ selector.delete('#').gsub(selector.scan(/[^\\]([>,+~.].*)/).join, '')
21
+ end
61
22
 
62
- # @param selector [String]
63
- # @return [Boolean]
64
- # @example
65
- # id?('#some-id') # => true
66
- # id?('.some-class') # => false
67
- def id?(selector)
68
- selector.start_with?('#')
69
- end
23
+ # @param selector [String]
24
+ # @return [Boolean]
25
+ # @example
26
+ # id?('#some-id') # => true
27
+ # id?('.some-cls') # => false
28
+ def id?(selector)
29
+ selector.start_with?('#')
30
+ end
70
31
 
71
- # @param selector [String]
72
- # @return [Boolean]
73
- # @example
74
- # attribute?('[attribute]') # => true
75
- # attribute?('attribute') # => false
76
- def attribute?(selector)
77
- selector.start_with?('[')
78
- end
32
+ # @param selector [String]
33
+ # @return [Array<String>]
34
+ # @example
35
+ # classes('#some-id') # => []
36
+ # classes('.some-cls') # => ['some-cls']
37
+ # classes('#some-id.some-cls') # => ['some-cls']
38
+ # classes('#some-id.cls1.cls2') # => ['cls1', 'cls2']
39
+ def classes(selector)
40
+ selector.scan(/\.([\w-]*)/).flatten
41
+ end
79
42
 
80
- # @param selector [String]
81
- # @return [Array<String>]
82
- # @example
83
- # attributes('a[foo-bar_baz]') # => {"foo-bar_baz=>true}
84
- # attributes('button[foo][bar]') # => {"foo"=>true, "bar"=>true}
85
- # attributes('table[foo=bar]') # => {"foo"=>"'bar'"}
86
- def attributes(selector)
87
- selector.scan(/\[(.*?)\]/).flatten.to_h do |attr|
88
- key, value = attr.split('=')
89
- [key, normalize_value(value)]
43
+ # @param selector [String]
44
+ # @return [Boolean]
45
+ # @example
46
+ # attribute?('[attribute]') # => true
47
+ # attribute?('attribute') # => false
48
+ def attribute?(selector)
49
+ selector.start_with?('[')
90
50
  end
91
- end
92
51
 
93
- # @param selector [String]
94
- # @return [Boolean]
95
- # @example
96
- # common_attributes?('a[focused]') # => true
97
- # common_attributes?('button[focused][visible]') # => true
98
- # common_attributes?('table[id=some-id]') # => true
99
- # common_attributes?('h1[invalid]') # => false
100
- def common_attributes?(selector)
101
- attributes(selector).keys.difference(COMMON_OPTIONS).none?
102
- end
52
+ # @param selector [String]
53
+ # @return [Array<String>]
54
+ # @example
55
+ # attributes('a[foo-bar_baz]') # => {"foo-bar_baz=>nil}
56
+ # attributes('button[foo][bar=baz]') # => {"foo"=>nil, "bar"=>"'baz'"}
57
+ # attributes('table[foo=bar]') # => {"foo"=>"'bar'"}
58
+ 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
67
+ end
103
68
 
104
- # @param selector [String]
105
- # @return [Array<String>]
106
- # @example
107
- # pseudo_classes('button:not([disabled])') # => ['not()']
108
- # pseudo_classes('a:enabled:not([valid])') # => ['enabled', 'not()']
109
- def pseudo_classes(selector)
110
- # Attributes must be excluded or else the colon in the `href`s URL
111
- # will also be picked up as pseudo classes.
112
- # "a:not([href='http://example.com']):enabled" => "a:not():enabled"
113
- ignored_attribute = selector.gsub(/\[.*?\]/, '')
114
- # "a:not():enabled" => ["not()", "enabled"]
115
- ignored_attribute.scan(/:([^:]*)/).flatten
116
- end
69
+ # @param selector [String]
70
+ # @return [Array<String>]
71
+ # @example
72
+ # pseudo_classes('button:not([disabled])') # => ['not()']
73
+ # pseudo_classes('a:enabled:not([valid])') # => ['enabled', 'not()']
74
+ def pseudo_classes(selector)
75
+ # Attributes must be excluded or else the colon in the `href`s URL
76
+ # will also be picked up as pseudo classes.
77
+ # "a:not([href='http://example.com']):enabled" => "a:not():enabled"
78
+ ignored_attribute = selector.gsub(/\[.*?\]/, '')
79
+ # "a:not():enabled" => ["not()", "enabled"]
80
+ ignored_attribute.scan(/:([^:]*)/).flatten
81
+ end
117
82
 
118
- # @param selector [String]
119
- # @return [Boolean]
120
- # @example
121
- # multiple_selectors?('a.cls b#id') # => true
122
- # multiple_selectors?('a.cls') # => false
123
- def multiple_selectors?(selector)
124
- selector.match?(/[ >,+~]/)
125
- end
83
+ # @param selector [String]
84
+ # @return [Boolean]
85
+ # @example
86
+ # multiple_selectors?('a.cls b#id') # => true
87
+ # multiple_selectors?('a.cls') # => false
88
+ def multiple_selectors?(selector)
89
+ normalize = selector.gsub(/(\\[>,+~]|\(.*\))/, '')
90
+ normalize.match?(/[ >,+~]/)
91
+ end
126
92
 
127
- # @param value [String]
128
- # @return [Boolean, String]
129
- # @example
130
- # normalize_value('true') # => true
131
- # normalize_value('false') # => false
132
- # normalize_value(nil) # => false
133
- # normalize_value("foo") # => "'foo'"
134
- def normalize_value(value)
135
- case value
136
- when 'true' then true
137
- when 'false' then false
138
- when nil then true
139
- else "'#{value}'"
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
140
107
  end
141
108
  end
142
109
  end
@@ -31,7 +31,7 @@ module RuboCop
31
31
  CAPYBARA_MATCHERS = %w[
32
32
  selector css xpath text title current_path link button
33
33
  field checked_field unchecked_field select table
34
- sibling ancestor
34
+ sibling ancestor content
35
35
  ].freeze
36
36
  POSITIVE_MATCHERS =
37
37
  Set.new(CAPYBARA_MATCHERS) { |element| :"have_#{element}" }.freeze
@@ -41,9 +41,7 @@ module RuboCop
41
41
  # which the last selector points.
42
42
  next unless (selector = last_selector(arg))
43
43
  next unless (action = specific_action(selector))
44
- next unless CapybaraHelp.specific_option?(node.receiver, arg,
45
- action)
46
- next unless CapybaraHelp.specific_pseudo_classes?(arg)
44
+ next unless replaceable?(node, arg, action)
47
45
 
48
46
  range = offense_range(node, node.receiver)
49
47
  add_offense(range, message: message(action, selector))
@@ -56,6 +54,18 @@ module RuboCop
56
54
  SPECIFIC_ACTION[last_selector(selector)]
57
55
  end
58
56
 
57
+ def replaceable?(node, arg, action)
58
+ replaceable_attributes?(arg) &&
59
+ CapybaraHelp.replaceable_option?(node.receiver, arg, action) &&
60
+ CapybaraHelp.replaceable_pseudo_classes?(arg)
61
+ end
62
+
63
+ def replaceable_attributes?(selector)
64
+ CapybaraHelp.replaceable_attributes?(
65
+ CssSelector.attributes(selector)
66
+ )
67
+ end
68
+
59
69
  def supported_selector?(selector)
60
70
  !selector.match?(/[>,+~]/)
61
71
  end
@@ -65,7 +75,7 @@ module RuboCop
65
75
  end
66
76
 
67
77
  def offense_range(node, receiver)
68
- receiver.loc.selector.with(end_pos: node.loc.expression.end_pos)
78
+ receiver.loc.selector.with(end_pos: node.source_range.end_pos)
69
79
  end
70
80
 
71
81
  def message(action, selector)
@@ -19,7 +19,7 @@ module RuboCop
19
19
 
20
20
  include RangeHelp
21
21
 
22
- MSG = 'Prefer `find_by` over `find`.'
22
+ MSG = 'Prefer `find_by_id` over `find`.'
23
23
  RESTRICT_ON_SEND = %i[find].freeze
24
24
 
25
25
  # @!method find_argument(node)
@@ -27,8 +27,14 @@ module RuboCop
27
27
  (send _ :find (str $_) ...)
28
28
  PATTERN
29
29
 
30
+ # @!method class_options(node)
31
+ def_node_search :class_options, <<~PATTERN
32
+ (pair (sym :class) $_ ...)
33
+ PATTERN
34
+
30
35
  def on_send(node)
31
36
  find_argument(node) do |arg|
37
+ next if CssSelector.pseudo_classes(arg).any?
32
38
  next if CssSelector.multiple_selectors?(arg)
33
39
 
34
40
  on_attr(node, arg) if attribute?(arg)
@@ -39,28 +45,57 @@ module RuboCop
39
45
  private
40
46
 
41
47
  def on_attr(node, arg)
42
- return unless (id = CssSelector.attributes(arg)['id'])
48
+ attrs = CssSelector.attributes(arg)
49
+ return unless (id = attrs['id'])
50
+ return if attrs['class']
43
51
 
44
52
  register_offense(node, replaced_arguments(arg, id))
45
53
  end
46
54
 
47
55
  def on_id(node, arg)
48
- register_offense(node, "'#{arg.to_s.delete('#')}'")
56
+ return if CssSelector.attributes(arg).any?
57
+
58
+ id = CssSelector.id(arg)
59
+ register_offense(node, "'#{id}'",
60
+ CssSelector.classes(arg.sub("##{id}", '')))
49
61
  end
50
62
 
51
63
  def attribute?(arg)
52
64
  CssSelector.attribute?(arg) &&
53
- CssSelector.common_attributes?(arg)
65
+ CapybaraHelp.common_attributes?(arg)
54
66
  end
55
67
 
56
- def register_offense(node, arg_replacement)
68
+ def register_offense(node, id, classes = [])
57
69
  add_offense(offense_range(node)) do |corrector|
58
70
  corrector.replace(node.loc.selector, 'find_by_id')
59
- corrector.replace(node.first_argument.loc.expression,
60
- arg_replacement)
71
+ corrector.replace(node.first_argument,
72
+ id.delete('\\'))
73
+ unless classes.compact.empty?
74
+ autocorrect_classes(corrector, node, classes)
75
+ end
61
76
  end
62
77
  end
63
78
 
79
+ def autocorrect_classes(corrector, node, classes)
80
+ if (options = class_options(node).first)
81
+ append_options(classes, options)
82
+ corrector.replace(options, classes.to_s)
83
+ else
84
+ corrector.insert_after(node.first_argument,
85
+ keyword_argument_class(classes))
86
+ end
87
+ end
88
+
89
+ def append_options(classes, options)
90
+ classes << options.value if options.str_type?
91
+ options.each_value { |v| classes << v.value } if options.array_type?
92
+ end
93
+
94
+ def keyword_argument_class(classes)
95
+ value = classes.size > 1 ? classes.to_s : "'#{classes.first}'"
96
+ ", class: #{value}"
97
+ end
98
+
64
99
  def replaced_arguments(arg, id)
65
100
  options = to_options(CssSelector.attributes(arg))
66
101
  options.empty? ? id : "#{id}, #{options}"
@@ -82,7 +117,7 @@ module RuboCop
82
117
  if node.loc.end
83
118
  node.loc.end.end_pos
84
119
  else
85
- node.loc.expression.end_pos
120
+ node.source_range.end_pos
86
121
  end
87
122
  end
88
123
  end
@@ -46,8 +46,7 @@ module RuboCop
46
46
  first_argument(node) do |arg|
47
47
  next unless (matcher = specific_matcher(arg))
48
48
  next if CssSelector.multiple_selectors?(arg)
49
- next unless CapybaraHelp.specific_option?(node, arg, matcher)
50
- next unless CapybaraHelp.specific_pseudo_classes?(arg)
49
+ next unless replaceable?(node, arg, matcher)
51
50
 
52
51
  add_offense(node, message: message(node, matcher))
53
52
  end
@@ -60,6 +59,18 @@ module RuboCop
60
59
  SPECIFIC_MATCHER[splitted_arg]
61
60
  end
62
61
 
62
+ def replaceable?(node, arg, matcher)
63
+ replaceable_attributes?(arg) &&
64
+ CapybaraHelp.replaceable_option?(node, arg, matcher) &&
65
+ CapybaraHelp.replaceable_pseudo_classes?(arg)
66
+ end
67
+
68
+ def replaceable_attributes?(selector)
69
+ CapybaraHelp.replaceable_attributes?(
70
+ CssSelector.attributes(selector)
71
+ )
72
+ end
73
+
63
74
  def message(node, matcher)
64
75
  format(MSG,
65
76
  good_matcher: good_matcher(node, matcher),
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.17.0
4
+ version: 2.18.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: 2022-12-29 00:00:00.000000000 Z
11
+ date: 2023-04-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -75,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
75
  - !ruby/object:Gem::Version
76
76
  version: '0'
77
77
  requirements: []
78
- rubygems_version: 3.2.33
78
+ rubygems_version: 3.4.5
79
79
  signing_key:
80
80
  specification_version: 4
81
81
  summary: Code style checking for Capybara test files