rubocop-capybara 2.17.0 → 2.17.1

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: 8f842364dafbf74a569f7e4d40eeee32b49bf3b9983d7ada2f376fb919481bdf
4
+ data.tar.gz: 075717405e4165171110bbdd29863873b4b94822baef45cb3946d8055da497bb
5
5
  SHA512:
6
- metadata.gz: 7fab06dac5d6ba1989c2c0bf47cd0a674e067baf7265378b8d4493055a1597087a442b79d88b931eadaa0f4ebd5c78f5d1929a116b12afa0d9c20110506651cd
7
- data.tar.gz: fd8f7cfebf049c450b25e20b76d76fba8ddbb1a50d626644e740e042589cdd2b1d406ea410df1d558439fc2d66a2b37733ce4ebd409f7f83568cd96dea27c61a
6
+ metadata.gz: 70e7fe7215dda9d8ae088bc0df07e50cf3d2f1c0645afe8797d7ce4a03b080a710b52da0403b37ce0ec6dc1a428d999e0342ea53d39d406c68f894059d9d7f6c
7
+ data.tar.gz: 7d17ff840e4f4ac78b079bbc9a567c10b1fbf2419f748b932bd67a2bdfe43e2a825dd06735974daff51dc35f323824803010e4526adedef9dc0812f77a8fe5f7
data/CHANGELOG.md CHANGED
@@ -2,11 +2,17 @@
2
2
 
3
3
  ## Edge (Unreleased)
4
4
 
5
- ## 2.17.0 (TBD)
5
+ ## 2.17.1 (2023-02-13)
6
+
7
+ - Fix an incorrect autocorrect for `Capybara/CurrentPathExpectation`. ([@ydah])
8
+ - Fix a false negative for `Capybara/CurrentPathExpectation` when using `match`. ([@ydah])
9
+ - Fix a false positive and incorrect autocorrect for `Capybara/SpecificActions`, `Capybara/SpecificFinders` and `Capybara/SpecificMatcher`. ([@ydah])
10
+
11
+ ## 2.17.0 (2022-12-29)
6
12
 
7
13
  - Extracted from `rubocop-rspec` into a separate repository for easier use with Minitest/Cucumber. ([@pirj])
8
14
 
9
- ## Previously (see rubocop-rspec's changelist for details)
15
+ ## Previously (see [rubocop-rspec's changelist](https://github.com/rubocop/rubocop-rspec/blob/9558719/CHANGELOG.md) for details)
10
16
 
11
17
  - Fix a false positive for `Capybara/SpecificMatcher` when `have_css("a")` without attribute. ([@ydah])
12
18
  - Add new `Capybara/NegationMatcher` cop. ([@ydah])
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.17.1'
8
8
  end
9
9
  end
10
10
  end
@@ -50,11 +50,11 @@ module RuboCop
50
50
  ${(send nil? :eq ...) (send nil? :match (regexp ...))})
51
51
  PATTERN
52
52
 
53
- # @!method regexp_str_matcher(node)
54
- def_node_matcher :regexp_str_matcher, <<-PATTERN
53
+ # @!method regexp_node_matcher(node)
54
+ def_node_matcher :regexp_node_matcher, <<-PATTERN
55
55
  (send
56
56
  #expectation_set_on_current_path ${:to :to_not :not_to}
57
- $(send nil? :match (str $_)))
57
+ $(send nil? :match ${str dstr xstr}))
58
58
  PATTERN
59
59
 
60
60
  def self.autocorrect_incompatible_with
@@ -78,9 +78,9 @@ module RuboCop
78
78
  rewrite_expectation(corrector, node, to_sym, matcher_node)
79
79
  end
80
80
 
81
- regexp_str_matcher(node.parent) do |to_sym, matcher_node, regexp|
81
+ regexp_node_matcher(node.parent) do |to_sym, matcher_node, regexp|
82
82
  rewrite_expectation(corrector, node, to_sym, matcher_node)
83
- convert_regexp_str_to_literal(corrector, matcher_node, regexp)
83
+ convert_regexp_node_to_literal(corrector, matcher_node, regexp)
84
84
  end
85
85
  end
86
86
 
@@ -97,12 +97,20 @@ module RuboCop
97
97
  add_ignore_query_options(corrector, node)
98
98
  end
99
99
 
100
- def convert_regexp_str_to_literal(corrector, matcher_node, regexp_str)
100
+ def convert_regexp_node_to_literal(corrector, matcher_node, regexp_node)
101
101
  str_node = matcher_node.first_argument
102
- regexp_expr = Regexp.new(regexp_str).inspect
102
+ regexp_expr = regexp_node_to_regexp_expr(regexp_node)
103
103
  corrector.replace(str_node, regexp_expr)
104
104
  end
105
105
 
106
+ def regexp_node_to_regexp_expr(regexp_node)
107
+ if regexp_node.xstr_type?
108
+ "/\#{`#{regexp_node.value.value}`}/"
109
+ else
110
+ Regexp.new(regexp_node.value).inspect
111
+ end
112
+ end
113
+
106
114
  # `have_current_path` with no options will include the querystring
107
115
  # while `page.current_path` does not.
108
116
  # This ensures the option `ignore_query: true` is added
@@ -110,7 +118,9 @@ module RuboCop
110
118
  def add_ignore_query_options(corrector, node)
111
119
  expectation_node = node.parent.last_argument
112
120
  expectation_last_child = expectation_node.children.last
113
- return if %i[regexp str].include?(expectation_last_child.type)
121
+ return if %i[
122
+ regexp str dstr xstr
123
+ ].include?(expectation_last_child.type)
114
124
 
115
125
  corrector.insert_after(
116
126
  expectation_last_child,
@@ -2,76 +2,128 @@
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
+ module CapybaraHelp
8
+ COMMON_OPTIONS = %w[
9
+ id class style
10
+ ].freeze
11
+ SPECIFIC_OPTIONS = {
12
+ 'button' => (
13
+ COMMON_OPTIONS + %w[disabled name value title type]
14
+ ).freeze,
15
+ 'link' => (
16
+ COMMON_OPTIONS + %w[href alt title download]
17
+ ).freeze,
18
+ 'table' => (
19
+ COMMON_OPTIONS + %w[cols rows]
20
+ ).freeze,
21
+ 'select' => (
22
+ COMMON_OPTIONS + %w[
23
+ disabled name placeholder
24
+ selected multiple
25
+ ]
26
+ ).freeze,
27
+ 'field' => (
28
+ COMMON_OPTIONS + %w[
29
+ checked disabled name placeholder
30
+ readonly type multiple
31
+ ]
32
+ ).freeze
33
+ }.freeze
34
+ SPECIFIC_PSEUDO_CLASSES = %w[
35
+ not() disabled enabled checked unchecked
36
+ ].freeze
8
37
 
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)
38
+ module_function
16
39
 
17
- attrs.all? do |attr|
18
- CssSelector.specific_options?(element, attr)
40
+ # @param node [RuboCop::AST::SendNode]
41
+ # @param locator [String]
42
+ # @param element [String]
43
+ # @return [Boolean]
44
+ def replaceable_option?(node, locator, element)
45
+ attrs = CssSelector.attributes(locator).keys
46
+ return false unless replaceable_element?(node, element, attrs)
47
+
48
+ attrs.all? do |attr|
49
+ SPECIFIC_OPTIONS.fetch(element, []).include?(attr)
50
+ end
19
51
  end
20
- end
21
52
 
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)
53
+ # @param selector [String]
54
+ # @return [Boolean]
55
+ # @example
56
+ # common_attributes?('a[focused]') # => true
57
+ # common_attributes?('button[focused][visible]') # => true
58
+ # common_attributes?('table[id=some-id]') # => true
59
+ # common_attributes?('h1[invalid]') # => false
60
+ def common_attributes?(selector)
61
+ CssSelector.attributes(selector).keys.difference(COMMON_OPTIONS).none?
27
62
  end
28
- end
29
63
 
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)
64
+ # @param attrs [Array<String>]
65
+ # @return [Boolean]
66
+ # @example
67
+ # replaceable_attributes?('table[id=some-id]') # => true
68
+ # replaceable_attributes?('a[focused]') # => false
69
+ def replaceable_attributes?(attrs)
70
+ attrs.values.none?(&:nil?)
71
+ end
35
72
 
36
- case pseudo_class
37
- when 'not()' then replaceable_pseudo_class_not?(locator)
38
- else true
73
+ # @param locator [String]
74
+ # @return [Boolean]
75
+ def replaceable_pseudo_classes?(locator)
76
+ CssSelector.pseudo_classes(locator).all? do |pseudo_class|
77
+ replaceable_pseudo_class?(pseudo_class, locator)
78
+ end
39
79
  end
40
- end
41
80
 
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)
81
+ # @param pseudo_class [String]
82
+ # @param locator [String]
83
+ # @return [Boolean]
84
+ def replaceable_pseudo_class?(pseudo_class, locator)
85
+ return false unless SPECIFIC_PSEUDO_CLASSES.include?(pseudo_class)
86
+
87
+ case pseudo_class
88
+ when 'not()' then replaceable_pseudo_class_not?(locator)
89
+ else true
48
90
  end
49
91
  end
50
- end
51
92
 
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
93
+ # @param locator [String]
94
+ # @return [Boolean]
95
+ def replaceable_pseudo_class_not?(locator)
96
+ locator.scan(/not\(.*?\)/).all? do |negation|
97
+ CssSelector.attributes(negation).values.all? do |v|
98
+ v.is_a?(TrueClass) || v.is_a?(FalseClass)
99
+ end
100
+ end
60
101
  end
61
- end
62
102
 
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
103
+ # @param node [RuboCop::AST::SendNode]
104
+ # @param element [String]
105
+ # @param attrs [Array<String>]
106
+ # @return [Boolean]
107
+ def replaceable_element?(node, element, attrs)
108
+ case element
109
+ when 'link' then replaceable_to_link?(node, attrs)
110
+ else true
111
+ end
112
+ end
113
+
114
+ # @param node [RuboCop::AST::SendNode]
115
+ # @param attrs [Array<String>]
116
+ # @return [Boolean]
117
+ def replaceable_to_link?(node, attrs)
118
+ include_option?(node, :href) || attrs.include?('href')
119
+ end
69
120
 
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 }
121
+ # @param node [RuboCop::AST::SendNode]
122
+ # @param option [Symbol]
123
+ # @return [Boolean]
124
+ def include_option?(node, option)
125
+ node.each_descendant(:sym).find { |opt| opt.value == option }
126
+ end
75
127
  end
76
128
  end
77
129
  end
@@ -2,141 +2,107 @@
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
+ module CssSelector
8
+ module_function
40
9
 
41
- module_function
10
+ # @param selector [String]
11
+ # @return [String]
12
+ # @example
13
+ # id('#some-id') # => some-id
14
+ # id('.some-cls') # => nil
15
+ # id('#some-id.cls') # => some-id
16
+ def id(selector)
17
+ return unless id?(selector)
42
18
 
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
19
+ selector.delete('#').gsub(selector.scan(/[^\\]([>,+~.].*)/).join, '')
20
+ end
61
21
 
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
22
+ # @param selector [String]
23
+ # @return [Boolean]
24
+ # @example
25
+ # id?('#some-id') # => true
26
+ # id?('.some-cls') # => false
27
+ def id?(selector)
28
+ selector.start_with?('#')
29
+ end
70
30
 
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
31
+ # @param selector [String]
32
+ # @return [Array<String>]
33
+ # @example
34
+ # classes('#some-id') # => []
35
+ # classes('.some-cls') # => ['some-cls']
36
+ # classes('#some-id.some-cls') # => ['some-cls']
37
+ # classes('#some-id.cls1.cls2') # => ['cls1', 'cls2']
38
+ def classes(selector)
39
+ selector.scan(/\.([\w-]*)/).flatten
40
+ end
79
41
 
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)]
42
+ # @param selector [String]
43
+ # @return [Boolean]
44
+ # @example
45
+ # attribute?('[attribute]') # => true
46
+ # attribute?('attribute') # => false
47
+ def attribute?(selector)
48
+ selector.start_with?('[')
90
49
  end
91
- end
92
50
 
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
51
+ # @param selector [String]
52
+ # @return [Array<String>]
53
+ # @example
54
+ # attributes('a[foo-bar_baz]') # => {"foo-bar_baz=>nil}
55
+ # attributes('button[foo][bar=baz]') # => {"foo"=>nil, "bar"=>"'baz'"}
56
+ # attributes('table[foo=bar]') # => {"foo"=>"'bar'"}
57
+ def attributes(selector)
58
+ # Extract the inner strings of attributes.
59
+ # For example, extract the following:
60
+ # 'button[foo][bar=baz]' => 'foo][bar=baz'
61
+ inside_attributes = selector.scan(/\[(.*)\]/).flatten.join
62
+ inside_attributes.split('][').to_h do |attr|
63
+ key, value = attr.split('=')
64
+ [key, normalize_value(value)]
65
+ end
66
+ end
103
67
 
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
68
+ # @param selector [String]
69
+ # @return [Array<String>]
70
+ # @example
71
+ # pseudo_classes('button:not([disabled])') # => ['not()']
72
+ # pseudo_classes('a:enabled:not([valid])') # => ['enabled', 'not()']
73
+ def pseudo_classes(selector)
74
+ # Attributes must be excluded or else the colon in the `href`s URL
75
+ # will also be picked up as pseudo classes.
76
+ # "a:not([href='http://example.com']):enabled" => "a:not():enabled"
77
+ ignored_attribute = selector.gsub(/\[.*?\]/, '')
78
+ # "a:not():enabled" => ["not()", "enabled"]
79
+ ignored_attribute.scan(/:([^:]*)/).flatten
80
+ end
117
81
 
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
82
+ # @param selector [String]
83
+ # @return [Boolean]
84
+ # @example
85
+ # multiple_selectors?('a.cls b#id') # => true
86
+ # multiple_selectors?('a.cls') # => false
87
+ def multiple_selectors?(selector)
88
+ normalize = selector.gsub(/(\\[>,+~]|\(.*\))/, '')
89
+ normalize.match?(/[ >,+~]/)
90
+ end
126
91
 
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}'"
92
+ # @param value [String]
93
+ # @return [Boolean, String]
94
+ # @example
95
+ # normalize_value('true') # => true
96
+ # normalize_value('false') # => false
97
+ # normalize_value(nil) # => nil
98
+ # normalize_value("foo") # => "'foo'"
99
+ def normalize_value(value)
100
+ case value
101
+ when 'true' then true
102
+ when 'false' then false
103
+ when nil then nil
104
+ else "'#{value.gsub(/"|'/, '')}'"
105
+ end
140
106
  end
141
107
  end
142
108
  end
@@ -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
@@ -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
71
  corrector.replace(node.first_argument.loc.expression,
60
- arg_replacement)
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}"
@@ -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.17.1
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-02-16 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.3
79
79
  signing_key:
80
80
  specification_version: 4
81
81
  summary: Code style checking for Capybara test files