rubocop-capybara 2.17.0 → 2.17.1

Sign up to get free protection for your applications and to get access to all the features.
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