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 +4 -4
- data/CHANGELOG.md +15 -2
- data/README.md +1 -1
- data/config/default.yml +3 -1
- data/lib/rubocop/capybara/version.rb +1 -1
- data/lib/rubocop/cop/capybara/current_path_expectation.rb +37 -12
- data/lib/rubocop/cop/capybara/mixin/capybara_help.rb +108 -55
- data/lib/rubocop/cop/capybara/mixin/css_selector.rb +92 -125
- data/lib/rubocop/cop/capybara/negation_matcher.rb +1 -1
- data/lib/rubocop/cop/capybara/specific_actions.rb +14 -4
- data/lib/rubocop/cop/capybara/specific_finders.rb +43 -8
- data/lib/rubocop/cop/capybara/specific_matcher.rb +13 -2
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb7f795cc80f072b94781f2339e781cccf9922ad2303c09111c0e34a4af264f8
|
4
|
+
data.tar.gz: d2ae47b522208177b7c93338973c8a144dfbccca2f817f0797b531f8c89e50b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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:
|
22
|
+
VersionAdded: '2.17'
|
21
23
|
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/MatchStyle
|
22
24
|
|
23
25
|
Capybara/NegationMatcher:
|
@@ -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/
|
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/
|
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
|
54
|
-
def_node_matcher :
|
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
|
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
|
-
|
82
|
+
regexp_node_matcher(node.parent) do |to_sym, matcher_node, regexp|
|
82
83
|
rewrite_expectation(corrector, node, to_sym, matcher_node)
|
83
|
-
|
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
|
-
|
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
|
101
|
+
def convert_regexp_node_to_literal(corrector, matcher_node, regexp_node)
|
101
102
|
str_node = matcher_node.first_argument
|
102
|
-
regexp_expr =
|
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[
|
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
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
locator
|
46
|
-
|
47
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
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.
|
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 `
|
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
|
-
|
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
|
-
|
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
|
-
|
65
|
+
CapybaraHelp.common_attributes?(arg)
|
54
66
|
end
|
55
67
|
|
56
|
-
def register_offense(node,
|
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
|
60
|
-
|
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.
|
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
|
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.
|
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:
|
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.
|
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
|