rubocop-capybara 2.17.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|
[](https://rubygems.org/gems/rubocop-capybara)
|
|
5
5
|

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