rubocop-rspec 2.17.1 → 2.18.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/config/obsoletion.yml +9 -0
- data/lib/rubocop/cop/rspec/capybara/current_path_expectation.rb +29 -115
- data/lib/rubocop/cop/rspec/capybara/match_style.rb +28 -50
- data/lib/rubocop/cop/rspec/capybara/negation_matcher.rb +23 -96
- data/lib/rubocop/cop/rspec/capybara/specific_actions.rb +19 -75
- data/lib/rubocop/cop/rspec/capybara/specific_finders.rb +14 -83
- data/lib/rubocop/cop/rspec/capybara/specific_matcher.rb +25 -69
- data/lib/rubocop/cop/rspec/capybara/visibility_matcher.rb +26 -63
- data/lib/rubocop/rspec/config_formatter.rb +10 -0
- data/lib/rubocop/rspec/version.rb +1 -1
- data/lib/rubocop-rspec.rb +1 -2
- metadata +16 -4
- data/lib/rubocop/cop/rspec/mixin/capybara_help.rb +0 -80
- data/lib/rubocop/cop/rspec/mixin/css_selector.rb +0 -146
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b2ef643f2d262910bba4db8e0e6a722b96d0cb6424d04d4261326155176f321
|
4
|
+
data.tar.gz: 29ae9b634075bd3dc11d44969fb00a7542aad3302f3a1aa360a2637e3875a3bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b14f91843dcbc6cd8abfca82a8f21244991aacb79d09a92e975c1eb6d36e554adc44bd1d8b14d2d014fed60b6383f1e102f5142c4a771532be1b60db47c01df1
|
7
|
+
data.tar.gz: 0626b43df05da5b5f386d18be195d2aaa14347a7f70cd99d2d1731e9f9107536c8a0076897b78d9f2f4e69cdeb2a79bc0caaab488a6bee822ea08add9b083fe9
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,14 @@
|
|
2
2
|
|
3
3
|
## Master (Unreleased)
|
4
4
|
|
5
|
+
## 2.18.1 (2023-01-19)
|
6
|
+
|
7
|
+
- Add `rubocop-capybara` version constraint to prevent sudden cop enabling when it hits 3.0. ([@pirj])
|
8
|
+
|
9
|
+
## 2.18.0 (2023-01-16)
|
10
|
+
|
11
|
+
- Extract Capybara cops to a separate repository, [`rubocop-capybara`](https://github.com/rubocop/rubocop-capybara). The `rubocop-capybara` repository is a dependency of `rubocop-rspec` and the Capybara cops are aliased (`RSpec/Capybara/Foo` == `Capybara/Foo`) until v3.0 is released, so the change will be invisible to users until then. ([@pirj])
|
12
|
+
|
5
13
|
## 2.17.1 (2023-01-16)
|
6
14
|
|
7
15
|
- Fix a false negative for `RSpec/Pending` when using skipped in metadata is multiline string. ([@ydah])
|
data/config/obsoletion.yml
CHANGED
@@ -12,3 +12,12 @@ changed_parameters:
|
|
12
12
|
parameters: IgnoredPatterns
|
13
13
|
alternative: AllowedPatterns
|
14
14
|
severity: warning
|
15
|
+
|
16
|
+
renamed:
|
17
|
+
RSpec/Capybara/CurrentPathExpectation: Capybara/CurrentPathExpectation
|
18
|
+
RSpec/Capybara/MatchStyle: Capybara/MatchStyle
|
19
|
+
RSpec/Capybara/NegationMatcher: Capybara/NegationMatcher
|
20
|
+
RSpec/Capybara/SpecificActions: Capybara/SpecificActions
|
21
|
+
RSpec/Capybara/SpecificFinders: Capybara/SpecificFinders
|
22
|
+
RSpec/Capybara/SpecificMatcher: Capybara/SpecificMatcher
|
23
|
+
RSpec/Capybara/VisibilityMatcher: Capybara/VisibilityMatcher
|
@@ -4,121 +4,35 @@ module RuboCop
|
|
4
4
|
module Cop
|
5
5
|
module RSpec
|
6
6
|
module Capybara
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# #
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# #
|
24
|
-
#
|
25
|
-
#
|
26
|
-
# #
|
27
|
-
#
|
28
|
-
#
|
29
|
-
# #
|
30
|
-
#
|
31
|
-
#
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
'Capybara feature specs - instead, use the ' \
|
37
|
-
'`have_current_path` matcher on `page`'
|
38
|
-
|
39
|
-
RESTRICT_ON_SEND = %i[expect].freeze
|
40
|
-
|
41
|
-
# @!method expectation_set_on_current_path(node)
|
42
|
-
def_node_matcher :expectation_set_on_current_path, <<-PATTERN
|
43
|
-
(send nil? :expect (send {(send nil? :page) nil?} :current_path))
|
44
|
-
PATTERN
|
45
|
-
|
46
|
-
# Supported matchers: eq(...) / match(/regexp/) / match('regexp')
|
47
|
-
# @!method as_is_matcher(node)
|
48
|
-
def_node_matcher :as_is_matcher, <<-PATTERN
|
49
|
-
(send
|
50
|
-
#expectation_set_on_current_path ${:to :to_not :not_to}
|
51
|
-
${(send nil? :eq ...) (send nil? :match (regexp ...))})
|
52
|
-
PATTERN
|
53
|
-
|
54
|
-
# @!method regexp_str_matcher(node)
|
55
|
-
def_node_matcher :regexp_str_matcher, <<-PATTERN
|
56
|
-
(send
|
57
|
-
#expectation_set_on_current_path ${:to :to_not :not_to}
|
58
|
-
$(send nil? :match (str $_)))
|
59
|
-
PATTERN
|
60
|
-
|
61
|
-
def self.autocorrect_incompatible_with
|
62
|
-
[Style::TrailingCommaInArguments]
|
63
|
-
end
|
64
|
-
|
65
|
-
def on_send(node)
|
66
|
-
expectation_set_on_current_path(node) do
|
67
|
-
add_offense(node.loc.selector) do |corrector|
|
68
|
-
next unless node.chained?
|
69
|
-
|
70
|
-
autocorrect(corrector, node)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
private
|
76
|
-
|
77
|
-
def autocorrect(corrector, node)
|
78
|
-
as_is_matcher(node.parent) do |to_sym, matcher_node|
|
79
|
-
rewrite_expectation(corrector, node, to_sym, matcher_node)
|
80
|
-
end
|
81
|
-
|
82
|
-
regexp_str_matcher(node.parent) do |to_sym, matcher_node, regexp|
|
83
|
-
rewrite_expectation(corrector, node, to_sym, matcher_node)
|
84
|
-
convert_regexp_str_to_literal(corrector, matcher_node, regexp)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def rewrite_expectation(corrector, node, to_symbol, matcher_node)
|
89
|
-
current_path_node = node.first_argument
|
90
|
-
corrector.replace(current_path_node, 'page')
|
91
|
-
corrector.replace(node.parent.loc.selector, 'to')
|
92
|
-
matcher_method = if to_symbol == :to
|
93
|
-
'have_current_path'
|
94
|
-
else
|
95
|
-
'have_no_current_path'
|
96
|
-
end
|
97
|
-
corrector.replace(matcher_node.loc.selector, matcher_method)
|
98
|
-
add_ignore_query_options(corrector, node)
|
99
|
-
end
|
100
|
-
|
101
|
-
def convert_regexp_str_to_literal(corrector, matcher_node, regexp_str)
|
102
|
-
str_node = matcher_node.first_argument
|
103
|
-
regexp_expr = Regexp.new(regexp_str).inspect
|
104
|
-
corrector.replace(str_node, regexp_expr)
|
105
|
-
end
|
106
|
-
|
107
|
-
# `have_current_path` with no options will include the querystring
|
108
|
-
# while `page.current_path` does not.
|
109
|
-
# This ensures the option `ignore_query: true` is added
|
110
|
-
# except when the expectation is a regexp or string
|
111
|
-
def add_ignore_query_options(corrector, node)
|
112
|
-
expectation_node = node.parent.last_argument
|
113
|
-
expectation_last_child = expectation_node.children.last
|
114
|
-
return if %i[regexp str].include?(expectation_last_child.type)
|
115
|
-
|
116
|
-
corrector.insert_after(
|
117
|
-
expectation_last_child,
|
118
|
-
', ignore_query: true'
|
119
|
-
)
|
120
|
-
end
|
121
|
-
end
|
7
|
+
# @!parse
|
8
|
+
# # Checks that no expectations are set on Capybara's `current_path`.
|
9
|
+
# #
|
10
|
+
# # The
|
11
|
+
# # https://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/RSpecMatchers#have_current_path-instance_method[`have_current_path` matcher]
|
12
|
+
# # should be used on `page` to set expectations on Capybara's
|
13
|
+
# # current path, since it uses
|
14
|
+
# # https://github.com/teamcapybara/capybara/blob/master/README.md#asynchronous-javascript-ajax-and-friends[Capybara's waiting functionality]
|
15
|
+
# # which ensures that preceding actions (like `click_link`) have
|
16
|
+
# # completed.
|
17
|
+
# #
|
18
|
+
# # This cop does not support autocorrection in some cases.
|
19
|
+
# #
|
20
|
+
# # @example
|
21
|
+
# # # bad
|
22
|
+
# # expect(current_path).to eq('/callback')
|
23
|
+
# #
|
24
|
+
# # # good
|
25
|
+
# # expect(page).to have_current_path('/callback')
|
26
|
+
# #
|
27
|
+
# # # bad (does not support autocorrection)
|
28
|
+
# # expect(page.current_path).to match(variable)
|
29
|
+
# #
|
30
|
+
# # # good
|
31
|
+
# # expect(page).to have_current_path('/callback')
|
32
|
+
# #
|
33
|
+
# class CurrentPathExpectation < ::RuboCop::Cop::Base; end
|
34
|
+
CurrentPathExpectation =
|
35
|
+
::RuboCop::Cop::Capybara::CurrentPathExpectation
|
122
36
|
end
|
123
37
|
end
|
124
38
|
end
|
@@ -4,56 +4,34 @@ module RuboCop
|
|
4
4
|
module Cop
|
5
5
|
module RSpec
|
6
6
|
module Capybara
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# #
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# #
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
# #
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# #
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
# #
|
27
|
-
#
|
28
|
-
#
|
29
|
-
# #
|
30
|
-
#
|
31
|
-
#
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
MSG = 'Use `%<good>s` instead of `%<bad>s`.'
|
36
|
-
RESTRICT_ON_SEND = %i[assert_style has_style? have_style].freeze
|
37
|
-
PREFERRED_METHOD = {
|
38
|
-
'assert_style' => 'assert_matches_style',
|
39
|
-
'has_style?' => 'matches_style?',
|
40
|
-
'have_style' => 'match_style'
|
41
|
-
}.freeze
|
42
|
-
|
43
|
-
def on_send(node)
|
44
|
-
method_node = node.loc.selector
|
45
|
-
add_offense(method_node) do |corrector|
|
46
|
-
corrector.replace(method_node,
|
47
|
-
PREFERRED_METHOD[method_node.source])
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
def message(node)
|
54
|
-
format(MSG, good: PREFERRED_METHOD[node.source], bad: node.source)
|
55
|
-
end
|
56
|
-
end
|
7
|
+
# @!parse
|
8
|
+
# # Checks for usage of deprecated style methods.
|
9
|
+
# #
|
10
|
+
# # @example when using `assert_style`
|
11
|
+
# # # bad
|
12
|
+
# # page.find(:css, '#first').assert_style(display: 'block')
|
13
|
+
# #
|
14
|
+
# # # good
|
15
|
+
# # page.find(:css, '#first').assert_matches_style(display: 'block')
|
16
|
+
# #
|
17
|
+
# # @example when using `has_style?`
|
18
|
+
# # # bad
|
19
|
+
# # expect(page.find(:css, 'first')
|
20
|
+
# # .has_style?(display: 'block')).to be true
|
21
|
+
# #
|
22
|
+
# # # good
|
23
|
+
# # expect(page.find(:css, 'first')
|
24
|
+
# # .matches_style?(display: 'block')).to be true
|
25
|
+
# #
|
26
|
+
# # @example when using `have_style`
|
27
|
+
# # # bad
|
28
|
+
# # expect(page).to have_style(display: 'block')
|
29
|
+
# #
|
30
|
+
# # # good
|
31
|
+
# # expect(page).to match_style(display: 'block')
|
32
|
+
# #
|
33
|
+
# class MatchStyle < ::RuboCop::Cop::Base; end
|
34
|
+
MatchStyle = ::RuboCop::Cop::Capybara::MatchStyle
|
57
35
|
end
|
58
36
|
end
|
59
37
|
end
|
@@ -4,102 +4,29 @@ module RuboCop
|
|
4
4
|
module Cop
|
5
5
|
module RSpec
|
6
6
|
module Capybara
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# #
|
11
|
-
#
|
12
|
-
# expect(page).to
|
13
|
-
#
|
14
|
-
# #
|
15
|
-
#
|
16
|
-
# expect(page).not_to
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# #
|
20
|
-
#
|
21
|
-
# expect(page).not_to
|
22
|
-
#
|
23
|
-
# #
|
24
|
-
#
|
25
|
-
# expect(page).to
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
MSG = 'Use `expect(...).%<runner>s %<matcher>s`.'
|
32
|
-
CAPYBARA_MATCHERS = %w[
|
33
|
-
selector css xpath text title current_path link button
|
34
|
-
field checked_field unchecked_field select table
|
35
|
-
sibling ancestor
|
36
|
-
].freeze
|
37
|
-
POSITIVE_MATCHERS =
|
38
|
-
Set.new(CAPYBARA_MATCHERS) { |element| :"have_#{element}" }.freeze
|
39
|
-
NEGATIVE_MATCHERS =
|
40
|
-
Set.new(CAPYBARA_MATCHERS) { |element| :"have_no_#{element}" }
|
41
|
-
.freeze
|
42
|
-
RESTRICT_ON_SEND = (POSITIVE_MATCHERS + NEGATIVE_MATCHERS).freeze
|
43
|
-
|
44
|
-
# @!method not_to?(node)
|
45
|
-
def_node_matcher :not_to?, <<~PATTERN
|
46
|
-
(send ... :not_to
|
47
|
-
(send nil? %POSITIVE_MATCHERS ...))
|
48
|
-
PATTERN
|
49
|
-
|
50
|
-
# @!method have_no?(node)
|
51
|
-
def_node_matcher :have_no?, <<~PATTERN
|
52
|
-
(send ... :to
|
53
|
-
(send nil? %NEGATIVE_MATCHERS ...))
|
54
|
-
PATTERN
|
55
|
-
|
56
|
-
def on_send(node)
|
57
|
-
return unless offense?(node.parent)
|
58
|
-
|
59
|
-
matcher = node.method_name.to_s
|
60
|
-
add_offense(offense_range(node),
|
61
|
-
message: message(matcher)) do |corrector|
|
62
|
-
corrector.replace(node.parent.loc.selector, replaced_runner)
|
63
|
-
corrector.replace(node.loc.selector,
|
64
|
-
replaced_matcher(matcher))
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
private
|
69
|
-
|
70
|
-
def offense?(node)
|
71
|
-
(style == :have_no && not_to?(node)) ||
|
72
|
-
(style == :not_to && have_no?(node))
|
73
|
-
end
|
74
|
-
|
75
|
-
def offense_range(node)
|
76
|
-
node.parent.loc.selector.with(end_pos: node.loc.selector.end_pos)
|
77
|
-
end
|
78
|
-
|
79
|
-
def message(matcher)
|
80
|
-
format(MSG,
|
81
|
-
runner: replaced_runner,
|
82
|
-
matcher: replaced_matcher(matcher))
|
83
|
-
end
|
84
|
-
|
85
|
-
def replaced_runner
|
86
|
-
case style
|
87
|
-
when :have_no
|
88
|
-
'to'
|
89
|
-
when :not_to
|
90
|
-
'not_to'
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def replaced_matcher(matcher)
|
95
|
-
case style
|
96
|
-
when :have_no
|
97
|
-
matcher.sub('have_', 'have_no_')
|
98
|
-
when :not_to
|
99
|
-
matcher.sub('have_no_', 'have_')
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
7
|
+
# @!parse
|
8
|
+
# # Enforces use of `have_no_*` or `not_to` for negated expectations.
|
9
|
+
# #
|
10
|
+
# # @example EnforcedStyle: not_to (default)
|
11
|
+
# # # bad
|
12
|
+
# # expect(page).to have_no_selector
|
13
|
+
# # expect(page).to have_no_css('a')
|
14
|
+
# #
|
15
|
+
# # # good
|
16
|
+
# # expect(page).not_to have_selector
|
17
|
+
# # expect(page).not_to have_css('a')
|
18
|
+
# #
|
19
|
+
# # @example EnforcedStyle: have_no
|
20
|
+
# # # bad
|
21
|
+
# # expect(page).not_to have_selector
|
22
|
+
# # expect(page).not_to have_css('a')
|
23
|
+
# #
|
24
|
+
# # # good
|
25
|
+
# # expect(page).to have_no_selector
|
26
|
+
# # expect(page).to have_no_css('a')
|
27
|
+
# #
|
28
|
+
# class NegationMatcher < ::RuboCop::Cop::Base; end
|
29
|
+
NegationMatcher = ::RuboCop::Cop::Capybara::NegationMatcher
|
103
30
|
end
|
104
31
|
end
|
105
32
|
end
|
@@ -4,81 +4,25 @@ module RuboCop
|
|
4
4
|
module Cop
|
5
5
|
module RSpec
|
6
6
|
module Capybara
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
# #
|
12
|
-
#
|
13
|
-
# find('
|
14
|
-
# find('
|
15
|
-
# find('
|
16
|
-
#
|
17
|
-
# #
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
SPECIFIC_ACTION = {
|
27
|
-
'button' => 'button',
|
28
|
-
'a' => 'link'
|
29
|
-
}.freeze
|
30
|
-
|
31
|
-
# @!method click_on_selector(node)
|
32
|
-
def_node_matcher :click_on_selector, <<-PATTERN
|
33
|
-
(send _ :find (str $_) ...)
|
34
|
-
PATTERN
|
35
|
-
|
36
|
-
def on_send(node)
|
37
|
-
click_on_selector(node.receiver) do |arg|
|
38
|
-
next unless supported_selector?(arg)
|
39
|
-
# Always check the last selector in the case of multiple selectors
|
40
|
-
# separated by whitespace.
|
41
|
-
# because the `.click` is executed on the element to
|
42
|
-
# which the last selector points.
|
43
|
-
next unless (selector = last_selector(arg))
|
44
|
-
next unless (action = specific_action(selector))
|
45
|
-
next unless CapybaraHelp.specific_option?(node.receiver, arg,
|
46
|
-
action)
|
47
|
-
next unless CapybaraHelp.specific_pseudo_classes?(arg)
|
48
|
-
|
49
|
-
range = offense_range(node, node.receiver)
|
50
|
-
add_offense(range, message: message(action, selector))
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
private
|
55
|
-
|
56
|
-
def specific_action(selector)
|
57
|
-
SPECIFIC_ACTION[last_selector(selector)]
|
58
|
-
end
|
59
|
-
|
60
|
-
def supported_selector?(selector)
|
61
|
-
!selector.match?(/[>,+~]/)
|
62
|
-
end
|
63
|
-
|
64
|
-
def last_selector(arg)
|
65
|
-
arg.split.last[/^\w+/, 0]
|
66
|
-
end
|
67
|
-
|
68
|
-
def offense_range(node, receiver)
|
69
|
-
receiver.loc.selector.with(end_pos: node.loc.expression.end_pos)
|
70
|
-
end
|
71
|
-
|
72
|
-
def message(action, selector)
|
73
|
-
format(MSG,
|
74
|
-
good_action: good_action(action),
|
75
|
-
selector: selector)
|
76
|
-
end
|
77
|
-
|
78
|
-
def good_action(action)
|
79
|
-
"click_#{action}"
|
80
|
-
end
|
81
|
-
end
|
7
|
+
# @!parse
|
8
|
+
# # Checks for there is a more specific actions offered by Capybara.
|
9
|
+
# #
|
10
|
+
# # @example
|
11
|
+
# #
|
12
|
+
# # # bad
|
13
|
+
# # find('a').click
|
14
|
+
# # find('button.cls').click
|
15
|
+
# # find('a', exact_text: 'foo').click
|
16
|
+
# # find('div button').click
|
17
|
+
# #
|
18
|
+
# # # good
|
19
|
+
# # click_link
|
20
|
+
# # click_button(class: 'cls')
|
21
|
+
# # click_link(exact_text: 'foo')
|
22
|
+
# # find('div').click_button
|
23
|
+
# #
|
24
|
+
# class SpecificActions < ::RuboCop::Cop::Base; end
|
25
|
+
SpecificActions = ::RuboCop::Cop::Capybara::SpecificActions
|
82
26
|
end
|
83
27
|
end
|
84
28
|
end
|
@@ -4,89 +4,20 @@ module RuboCop
|
|
4
4
|
module Cop
|
5
5
|
module RSpec
|
6
6
|
module Capybara
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# #
|
11
|
-
#
|
12
|
-
# find('
|
13
|
-
#
|
14
|
-
# #
|
15
|
-
#
|
16
|
-
# find_by_id('some-id'
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
include RangeHelp
|
22
|
-
|
23
|
-
MSG = 'Prefer `find_by` over `find`.'
|
24
|
-
RESTRICT_ON_SEND = %i[find].freeze
|
25
|
-
|
26
|
-
# @!method find_argument(node)
|
27
|
-
def_node_matcher :find_argument, <<~PATTERN
|
28
|
-
(send _ :find (str $_) ...)
|
29
|
-
PATTERN
|
30
|
-
|
31
|
-
def on_send(node)
|
32
|
-
find_argument(node) do |arg|
|
33
|
-
next if CssSelector.multiple_selectors?(arg)
|
34
|
-
|
35
|
-
on_attr(node, arg) if attribute?(arg)
|
36
|
-
on_id(node, arg) if CssSelector.id?(arg)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def on_attr(node, arg)
|
43
|
-
return unless (id = CssSelector.attributes(arg)['id'])
|
44
|
-
|
45
|
-
register_offense(node, replaced_arguments(arg, id))
|
46
|
-
end
|
47
|
-
|
48
|
-
def on_id(node, arg)
|
49
|
-
register_offense(node, "'#{arg.to_s.delete('#')}'")
|
50
|
-
end
|
51
|
-
|
52
|
-
def attribute?(arg)
|
53
|
-
CssSelector.attribute?(arg) &&
|
54
|
-
CssSelector.common_attributes?(arg)
|
55
|
-
end
|
56
|
-
|
57
|
-
def register_offense(node, arg_replacement)
|
58
|
-
add_offense(offense_range(node)) do |corrector|
|
59
|
-
corrector.replace(node.loc.selector, 'find_by_id')
|
60
|
-
corrector.replace(node.first_argument.loc.expression,
|
61
|
-
arg_replacement)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def replaced_arguments(arg, id)
|
66
|
-
options = to_options(CssSelector.attributes(arg))
|
67
|
-
options.empty? ? id : "#{id}, #{options}"
|
68
|
-
end
|
69
|
-
|
70
|
-
def to_options(attrs)
|
71
|
-
attrs.each.map do |key, value|
|
72
|
-
next if key == 'id'
|
73
|
-
|
74
|
-
"#{key}: #{value}"
|
75
|
-
end.compact.join(', ')
|
76
|
-
end
|
77
|
-
|
78
|
-
def offense_range(node)
|
79
|
-
range_between(node.loc.selector.begin_pos, end_pos(node))
|
80
|
-
end
|
81
|
-
|
82
|
-
def end_pos(node)
|
83
|
-
if node.loc.end
|
84
|
-
node.loc.end.end_pos
|
85
|
-
else
|
86
|
-
node.loc.expression.end_pos
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
7
|
+
# @!parse
|
8
|
+
# # Checks if there is a more specific finder offered by Capybara.
|
9
|
+
# #
|
10
|
+
# # @example
|
11
|
+
# # # bad
|
12
|
+
# # find('#some-id')
|
13
|
+
# # find('[visible][id=some-id]')
|
14
|
+
# #
|
15
|
+
# # # good
|
16
|
+
# # find_by_id('some-id')
|
17
|
+
# # find_by_id('some-id', visible: true)
|
18
|
+
# #
|
19
|
+
# class SpecificFinders < ::RuboCop::Cop::Base; end
|
20
|
+
SpecificFinders = ::RuboCop::Cop::Capybara::SpecificFinders
|
90
21
|
end
|
91
22
|
end
|
92
23
|
end
|
@@ -4,75 +4,31 @@ module RuboCop
|
|
4
4
|
module Cop
|
5
5
|
module RSpec
|
6
6
|
module Capybara
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
# #
|
12
|
-
#
|
13
|
-
# expect(page).to
|
14
|
-
# expect(page).to
|
15
|
-
# expect(page).to
|
16
|
-
# expect(page).to
|
17
|
-
# expect(page).to have_css('
|
18
|
-
# expect(page).to have_css('
|
19
|
-
#
|
20
|
-
# #
|
21
|
-
#
|
22
|
-
# expect(page).to
|
23
|
-
# expect(page).to
|
24
|
-
# expect(page).to
|
25
|
-
# expect(page).to
|
26
|
-
# expect(page).to
|
27
|
-
# expect(page).to
|
28
|
-
#
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
have_no_css].freeze
|
33
|
-
SPECIFIC_MATCHER = {
|
34
|
-
'button' => 'button',
|
35
|
-
'a' => 'link',
|
36
|
-
'table' => 'table',
|
37
|
-
'select' => 'select',
|
38
|
-
'input' => 'field'
|
39
|
-
}.freeze
|
40
|
-
|
41
|
-
# @!method first_argument(node)
|
42
|
-
def_node_matcher :first_argument, <<-PATTERN
|
43
|
-
(send nil? _ (str $_) ... )
|
44
|
-
PATTERN
|
45
|
-
|
46
|
-
def on_send(node)
|
47
|
-
first_argument(node) do |arg|
|
48
|
-
next unless (matcher = specific_matcher(arg))
|
49
|
-
next if CssSelector.multiple_selectors?(arg)
|
50
|
-
next unless CapybaraHelp.specific_option?(node, arg, matcher)
|
51
|
-
next unless CapybaraHelp.specific_pseudo_classes?(arg)
|
52
|
-
|
53
|
-
add_offense(node, message: message(node, matcher))
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
private
|
58
|
-
|
59
|
-
def specific_matcher(arg)
|
60
|
-
splitted_arg = arg[/^\w+/, 0]
|
61
|
-
SPECIFIC_MATCHER[splitted_arg]
|
62
|
-
end
|
63
|
-
|
64
|
-
def message(node, matcher)
|
65
|
-
format(MSG,
|
66
|
-
good_matcher: good_matcher(node, matcher),
|
67
|
-
bad_matcher: node.method_name)
|
68
|
-
end
|
69
|
-
|
70
|
-
def good_matcher(node, matcher)
|
71
|
-
node.method_name
|
72
|
-
.to_s
|
73
|
-
.gsub(/selector|css/, matcher.to_s)
|
74
|
-
end
|
75
|
-
end
|
7
|
+
# @!parse
|
8
|
+
# # Checks for there is a more specific matcher offered by Capybara.
|
9
|
+
# #
|
10
|
+
# # @example
|
11
|
+
# #
|
12
|
+
# # # bad
|
13
|
+
# # expect(page).to have_selector('button')
|
14
|
+
# # expect(page).to have_no_selector('button.cls')
|
15
|
+
# # expect(page).to have_css('button')
|
16
|
+
# # expect(page).to have_no_css('a.cls', href: 'http://example.com')
|
17
|
+
# # expect(page).to have_css('table.cls')
|
18
|
+
# # expect(page).to have_css('select')
|
19
|
+
# # expect(page).to have_css('input', exact_text: 'foo')
|
20
|
+
# #
|
21
|
+
# # # good
|
22
|
+
# # expect(page).to have_button
|
23
|
+
# # expect(page).to have_no_button(class: 'cls')
|
24
|
+
# # expect(page).to have_button
|
25
|
+
# # expect(page).to have_no_link('foo', class: 'cls', href: 'http://example.com')
|
26
|
+
# # expect(page).to have_table(class: 'cls')
|
27
|
+
# # expect(page).to have_select
|
28
|
+
# # expect(page).to have_field('foo')
|
29
|
+
# #
|
30
|
+
# class SpecificMatcher < ::RuboCop::Cop::Base; end
|
31
|
+
SpecificMatcher = ::RuboCop::Cop::Capybara::SpecificMatcher
|
76
32
|
end
|
77
33
|
end
|
78
34
|
end
|
@@ -4,69 +4,32 @@ module RuboCop
|
|
4
4
|
module Cop
|
5
5
|
module RSpec
|
6
6
|
module Capybara
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# #
|
20
|
-
#
|
21
|
-
#
|
22
|
-
# expect(page).to
|
23
|
-
#
|
24
|
-
# #
|
25
|
-
#
|
26
|
-
#
|
27
|
-
# expect(page).to
|
28
|
-
#
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
button
|
34
|
-
checked_field
|
35
|
-
css
|
36
|
-
field
|
37
|
-
link
|
38
|
-
select
|
39
|
-
selector
|
40
|
-
table
|
41
|
-
unchecked_field
|
42
|
-
xpath
|
43
|
-
].flat_map do |element|
|
44
|
-
["have_#{element}".to_sym, "have_no_#{element}".to_sym]
|
45
|
-
end
|
46
|
-
|
47
|
-
RESTRICT_ON_SEND = CAPYBARA_MATCHER_METHODS
|
48
|
-
|
49
|
-
# @!method visible_true?(node)
|
50
|
-
def_node_matcher :visible_true?, <<~PATTERN
|
51
|
-
(send nil? #capybara_matcher? ... (hash <$(pair (sym :visible) true) ...>))
|
52
|
-
PATTERN
|
53
|
-
|
54
|
-
# @!method visible_false?(node)
|
55
|
-
def_node_matcher :visible_false?, <<~PATTERN
|
56
|
-
(send nil? #capybara_matcher? ... (hash <$(pair (sym :visible) false) ...>))
|
57
|
-
PATTERN
|
58
|
-
|
59
|
-
def on_send(node)
|
60
|
-
visible_false?(node) { |arg| add_offense(arg, message: MSG_FALSE) }
|
61
|
-
visible_true?(node) { |arg| add_offense(arg, message: MSG_TRUE) }
|
62
|
-
end
|
63
|
-
|
64
|
-
private
|
65
|
-
|
66
|
-
def capybara_matcher?(method_name)
|
67
|
-
CAPYBARA_MATCHER_METHODS.include? method_name
|
68
|
-
end
|
69
|
-
end
|
7
|
+
# @!parse
|
8
|
+
# # Checks for boolean visibility in Capybara finders.
|
9
|
+
# #
|
10
|
+
# # Capybara lets you find elements that match a certain visibility
|
11
|
+
# # using the `:visible` option. `:visible` accepts both boolean and
|
12
|
+
# # symbols as values, however using booleans can have unwanted
|
13
|
+
# # effects. `visible: false` does not find just invisible elements,
|
14
|
+
# # but both visible and invisible elements. For expressiveness and
|
15
|
+
# # clarity, use one of the # symbol values, `:all`, `:hidden` or
|
16
|
+
# # `:visible`.
|
17
|
+
# # Read more in
|
18
|
+
# # https://www.rubydoc.info/gems/capybara/Capybara%2FNode%2FFinders:all[the documentation].
|
19
|
+
# #
|
20
|
+
# # @example
|
21
|
+
# # # bad
|
22
|
+
# # expect(page).to have_selector('.foo', visible: false)
|
23
|
+
# # expect(page).to have_css('.foo', visible: true)
|
24
|
+
# # expect(page).to have_link('my link', visible: false)
|
25
|
+
# #
|
26
|
+
# # # good
|
27
|
+
# # expect(page).to have_selector('.foo', visible: :visible)
|
28
|
+
# # expect(page).to have_css('.foo', visible: :all)
|
29
|
+
# # expect(page).to have_link('my link', visible: :hidden)
|
30
|
+
# #
|
31
|
+
# class VisibilityMatcher < ::RuboCop::Cop::Base; end
|
32
|
+
VisibilityMatcher = ::RuboCop::Cop::Capybara::VisibilityMatcher
|
70
33
|
end
|
71
34
|
end
|
72
35
|
end
|
@@ -8,6 +8,15 @@ module RuboCop
|
|
8
8
|
class ConfigFormatter
|
9
9
|
EXTENSION_ROOT_DEPARTMENT = %r{^(RSpec/)}.freeze
|
10
10
|
SUBDEPARTMENTS = %(RSpec/Capybara RSpec/FactoryBot RSpec/Rails)
|
11
|
+
EXTRACTED_COPS = %(
|
12
|
+
RSpec/Capybara/CurrentPathExpectation
|
13
|
+
RSpec/Capybara/MatchStyle
|
14
|
+
RSpec/Capybara/NegationMatcher
|
15
|
+
RSpec/Capybara/SpecificActions
|
16
|
+
RSpec/Capybara/SpecificFinders
|
17
|
+
RSpec/Capybara/SpecificMatcher
|
18
|
+
RSpec/Capybara/VisibilityMatcher
|
19
|
+
)
|
11
20
|
AMENDMENTS = %(Metrics/BlockLength)
|
12
21
|
COP_DOC_BASE_URL = 'https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/'
|
13
22
|
|
@@ -29,6 +38,7 @@ module RuboCop
|
|
29
38
|
def unified_config
|
30
39
|
cops.each_with_object(config.dup) do |cop, unified|
|
31
40
|
next if SUBDEPARTMENTS.include?(cop) || AMENDMENTS.include?(cop)
|
41
|
+
next if EXTRACTED_COPS.include?(cop)
|
32
42
|
|
33
43
|
replace_nil(unified[cop])
|
34
44
|
unified[cop].merge!(descriptions.fetch(cop))
|
data/lib/rubocop-rspec.rb
CHANGED
@@ -4,6 +4,7 @@ require 'pathname'
|
|
4
4
|
require 'yaml'
|
5
5
|
|
6
6
|
require 'rubocop'
|
7
|
+
require 'rubocop-capybara'
|
7
8
|
|
8
9
|
require_relative 'rubocop/rspec'
|
9
10
|
require_relative 'rubocop/rspec/inject'
|
@@ -17,8 +18,6 @@ require_relative 'rubocop/rspec/language'
|
|
17
18
|
|
18
19
|
require_relative 'rubocop/rspec/factory_bot/language'
|
19
20
|
|
20
|
-
require_relative 'rubocop/cop/rspec/mixin/capybara_help'
|
21
|
-
require_relative 'rubocop/cop/rspec/mixin/css_selector'
|
22
21
|
require_relative 'rubocop/cop/rspec/mixin/final_end_location'
|
23
22
|
require_relative 'rubocop/cop/rspec/mixin/inside_example_group'
|
24
23
|
require_relative 'rubocop/cop/rspec/mixin/metadata'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubocop-rspec
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.18.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Backus
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2023-01-
|
13
|
+
date: 2023-01-19 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rubocop
|
@@ -26,6 +26,20 @@ dependencies:
|
|
26
26
|
- - "~>"
|
27
27
|
- !ruby/object:Gem::Version
|
28
28
|
version: '1.33'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: rubocop-capybara
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - "~>"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '2.17'
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '2.17'
|
29
43
|
description: |2
|
30
44
|
Code style checking for RSpec files.
|
31
45
|
A plugin for the RuboCop code style enforcing & linting tool.
|
@@ -116,9 +130,7 @@ files:
|
|
116
130
|
- lib/rubocop/cop/rspec/message_expectation.rb
|
117
131
|
- lib/rubocop/cop/rspec/message_spies.rb
|
118
132
|
- lib/rubocop/cop/rspec/missing_example_group_argument.rb
|
119
|
-
- lib/rubocop/cop/rspec/mixin/capybara_help.rb
|
120
133
|
- lib/rubocop/cop/rspec/mixin/comments_help.rb
|
121
|
-
- lib/rubocop/cop/rspec/mixin/css_selector.rb
|
122
134
|
- lib/rubocop/cop/rspec/mixin/empty_line_separation.rb
|
123
135
|
- lib/rubocop/cop/rspec/mixin/final_end_location.rb
|
124
136
|
- lib/rubocop/cop/rspec/mixin/inside_example_group.rb
|
@@ -1,80 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module RuboCop
|
4
|
-
module Cop
|
5
|
-
module RSpec
|
6
|
-
# Help methods for capybara.
|
7
|
-
module CapybaraHelp
|
8
|
-
module_function
|
9
|
-
|
10
|
-
# @param node [RuboCop::AST::SendNode]
|
11
|
-
# @param locator [String]
|
12
|
-
# @param element [String]
|
13
|
-
# @return [Boolean]
|
14
|
-
def specific_option?(node, locator, element)
|
15
|
-
attrs = CssSelector.attributes(locator).keys
|
16
|
-
return false unless replaceable_element?(node, element, attrs)
|
17
|
-
|
18
|
-
attrs.all? do |attr|
|
19
|
-
CssSelector.specific_options?(element, attr)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
# @param locator [String]
|
24
|
-
# @return [Boolean]
|
25
|
-
def specific_pseudo_classes?(locator)
|
26
|
-
CssSelector.pseudo_classes(locator).all? do |pseudo_class|
|
27
|
-
replaceable_pseudo_class?(pseudo_class, locator)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
# @param pseudo_class [String]
|
32
|
-
# @param locator [String]
|
33
|
-
# @return [Boolean]
|
34
|
-
def replaceable_pseudo_class?(pseudo_class, locator)
|
35
|
-
return false unless CssSelector.specific_pesudo_classes?(pseudo_class)
|
36
|
-
|
37
|
-
case pseudo_class
|
38
|
-
when 'not()' then replaceable_pseudo_class_not?(locator)
|
39
|
-
else true
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
# @param locator [String]
|
44
|
-
# @return [Boolean]
|
45
|
-
def replaceable_pseudo_class_not?(locator)
|
46
|
-
locator.scan(/not\(.*?\)/).all? do |negation|
|
47
|
-
CssSelector.attributes(negation).values.all? do |v|
|
48
|
-
v.is_a?(TrueClass) || v.is_a?(FalseClass)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
# @param node [RuboCop::AST::SendNode]
|
54
|
-
# @param element [String]
|
55
|
-
# @param attrs [Array<String>]
|
56
|
-
# @return [Boolean]
|
57
|
-
def replaceable_element?(node, element, attrs)
|
58
|
-
case element
|
59
|
-
when 'link' then replaceable_to_link?(node, attrs)
|
60
|
-
else true
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# @param node [RuboCop::AST::SendNode]
|
65
|
-
# @param attrs [Array<String>]
|
66
|
-
# @return [Boolean]
|
67
|
-
def replaceable_to_link?(node, attrs)
|
68
|
-
include_option?(node, :href) || attrs.include?('href')
|
69
|
-
end
|
70
|
-
|
71
|
-
# @param node [RuboCop::AST::SendNode]
|
72
|
-
# @param option [Symbol]
|
73
|
-
# @return [Boolean]
|
74
|
-
def include_option?(node, option)
|
75
|
-
node.each_descendant(:sym).find { |opt| opt.value == option }
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
@@ -1,146 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module RuboCop
|
4
|
-
module Cop
|
5
|
-
module RSpec
|
6
|
-
# Helps parsing css selector.
|
7
|
-
module CssSelector
|
8
|
-
COMMON_OPTIONS = %w[
|
9
|
-
above below left_of right_of near count minimum maximum between text
|
10
|
-
id class style visible obscured exact exact_text normalize_ws match
|
11
|
-
wait filter_set focused
|
12
|
-
].freeze
|
13
|
-
SPECIFIC_OPTIONS = {
|
14
|
-
'button' => (
|
15
|
-
COMMON_OPTIONS + %w[disabled name value title type]
|
16
|
-
).freeze,
|
17
|
-
'link' => (
|
18
|
-
COMMON_OPTIONS + %w[href alt title download]
|
19
|
-
).freeze,
|
20
|
-
'table' => (
|
21
|
-
COMMON_OPTIONS + %w[
|
22
|
-
caption with_cols cols with_rows rows
|
23
|
-
]
|
24
|
-
).freeze,
|
25
|
-
'select' => (
|
26
|
-
COMMON_OPTIONS + %w[
|
27
|
-
disabled name placeholder options enabled_options
|
28
|
-
disabled_options selected with_selected multiple with_options
|
29
|
-
]
|
30
|
-
).freeze,
|
31
|
-
'field' => (
|
32
|
-
COMMON_OPTIONS + %w[
|
33
|
-
checked unchecked disabled valid name placeholder
|
34
|
-
validation_message readonly with type multiple
|
35
|
-
]
|
36
|
-
).freeze
|
37
|
-
}.freeze
|
38
|
-
SPECIFIC_PSEUDO_CLASSES = %w[
|
39
|
-
not() disabled enabled checked unchecked
|
40
|
-
].freeze
|
41
|
-
|
42
|
-
module_function
|
43
|
-
|
44
|
-
# @param element [String]
|
45
|
-
# @param attribute [String]
|
46
|
-
# @return [Boolean]
|
47
|
-
# @example
|
48
|
-
# specific_pesudo_classes?('button', 'name') # => true
|
49
|
-
# specific_pesudo_classes?('link', 'invalid') # => false
|
50
|
-
def specific_options?(element, attribute)
|
51
|
-
SPECIFIC_OPTIONS.fetch(element, []).include?(attribute)
|
52
|
-
end
|
53
|
-
|
54
|
-
# @param pseudo_class [String]
|
55
|
-
# @return [Boolean]
|
56
|
-
# @example
|
57
|
-
# specific_pesudo_classes?('disabled') # => true
|
58
|
-
# specific_pesudo_classes?('first-of-type') # => false
|
59
|
-
def specific_pesudo_classes?(pseudo_class)
|
60
|
-
SPECIFIC_PSEUDO_CLASSES.include?(pseudo_class)
|
61
|
-
end
|
62
|
-
|
63
|
-
# @param selector [String]
|
64
|
-
# @return [Boolean]
|
65
|
-
# @example
|
66
|
-
# id?('#some-id') # => true
|
67
|
-
# id?('.some-class') # => false
|
68
|
-
def id?(selector)
|
69
|
-
selector.start_with?('#')
|
70
|
-
end
|
71
|
-
|
72
|
-
# @param selector [String]
|
73
|
-
# @return [Boolean]
|
74
|
-
# @example
|
75
|
-
# attribute?('[attribute]') # => true
|
76
|
-
# attribute?('attribute') # => false
|
77
|
-
def attribute?(selector)
|
78
|
-
selector.start_with?('[')
|
79
|
-
end
|
80
|
-
|
81
|
-
# @param selector [String]
|
82
|
-
# @return [Array<String>]
|
83
|
-
# @example
|
84
|
-
# attributes('a[foo-bar_baz]') # => {"foo-bar_baz=>true}
|
85
|
-
# attributes('button[foo][bar]') # => {"foo"=>true, "bar"=>true}
|
86
|
-
# attributes('table[foo=bar]') # => {"foo"=>"'bar'"}
|
87
|
-
def attributes(selector)
|
88
|
-
selector.scan(/\[(.*?)\]/).flatten.to_h do |attr|
|
89
|
-
key, value = attr.split('=')
|
90
|
-
[key, normalize_value(value)]
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
# @param selector [String]
|
95
|
-
# @return [Boolean]
|
96
|
-
# @example
|
97
|
-
# common_attributes?('a[focused]') # => true
|
98
|
-
# common_attributes?('button[focused][visible]') # => true
|
99
|
-
# common_attributes?('table[id=some-id]') # => true
|
100
|
-
# common_attributes?('h1[invalid]') # => false
|
101
|
-
def common_attributes?(selector)
|
102
|
-
attributes(selector).keys.difference(COMMON_OPTIONS).none?
|
103
|
-
end
|
104
|
-
|
105
|
-
# @param selector [String]
|
106
|
-
# @return [Array<String>]
|
107
|
-
# @example
|
108
|
-
# pseudo_classes('button:not([disabled])') # => ['not()']
|
109
|
-
# pseudo_classes('a:enabled:not([valid])') # => ['enabled', 'not()']
|
110
|
-
def pseudo_classes(selector)
|
111
|
-
# Attributes must be excluded or else the colon in the `href`s URL
|
112
|
-
# will also be picked up as pseudo classes.
|
113
|
-
# "a:not([href='http://example.com']):enabled" => "a:not():enabled"
|
114
|
-
ignored_attribute = selector.gsub(/\[.*?\]/, '')
|
115
|
-
# "a:not():enabled" => ["not()", "enabled"]
|
116
|
-
ignored_attribute.scan(/:([^:]*)/).flatten
|
117
|
-
end
|
118
|
-
|
119
|
-
# @param selector [String]
|
120
|
-
# @return [Boolean]
|
121
|
-
# @example
|
122
|
-
# multiple_selectors?('a.cls b#id') # => true
|
123
|
-
# multiple_selectors?('a.cls') # => false
|
124
|
-
def multiple_selectors?(selector)
|
125
|
-
selector.match?(/[ >,+~]/)
|
126
|
-
end
|
127
|
-
|
128
|
-
# @param value [String]
|
129
|
-
# @return [Boolean, String]
|
130
|
-
# @example
|
131
|
-
# normalize_value('true') # => true
|
132
|
-
# normalize_value('false') # => false
|
133
|
-
# normalize_value(nil) # => false
|
134
|
-
# normalize_value("foo") # => "'foo'"
|
135
|
-
def normalize_value(value)
|
136
|
-
case value
|
137
|
-
when 'true' then true
|
138
|
-
when 'false' then false
|
139
|
-
when nil then true
|
140
|
-
else "'#{value}'"
|
141
|
-
end
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|
146
|
-
end
|