rubocop-capybara 2.17.0 → 2.18.0

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: 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