rubocop-capybara 3.0.0 → 3.1.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 +47 -0
- data/config/default.yml +71 -3
- data/lib/rubocop/capybara/version.rb +1 -1
- data/lib/rubocop/cop/capybara/absolute_x_path_in_element_scope.rb +104 -0
- data/lib/rubocop/cop/capybara/always_true_count_query.rb +75 -0
- data/lib/rubocop/cop/capybara/ambiguous_click.rb +1 -1
- data/lib/rubocop/cop/capybara/assert_style.rb +1 -1
- data/lib/rubocop/cop/capybara/chained_find.rb +175 -0
- data/lib/rubocop/cop/capybara/find_all_first.rb +13 -41
- data/lib/rubocop/cop/capybara/ignored_query_option.rb +242 -0
- data/lib/rubocop/cop/capybara/mixin/capybara_help.rb +86 -2
- data/lib/rubocop/cop/capybara/mixin/css_attributes_parser.rb +23 -5
- data/lib/rubocop/cop/capybara/mixin/css_selector.rb +13 -10
- data/lib/rubocop/cop/capybara/mixin/query_methods.rb +120 -0
- data/lib/rubocop/cop/capybara/mixin/visibility.rb +53 -0
- data/lib/rubocop/cop/capybara/modal_method_without_block.rb +37 -0
- data/lib/rubocop/cop/capybara/native_attribute.rb +76 -0
- data/lib/rubocop/cop/capybara/redundant_within_find.rb +10 -21
- data/lib/rubocop/cop/capybara/rspec/current_path_expectation.rb +13 -16
- data/lib/rubocop/cop/capybara/rspec/current_url_expectation.rb +122 -0
- data/lib/rubocop/cop/capybara/rspec/find_element_value_eq.rb +173 -0
- data/lib/rubocop/cop/capybara/rspec/have_content.rb +1 -1
- data/lib/rubocop/cop/capybara/rspec/have_selector.rb +26 -15
- data/lib/rubocop/cop/capybara/rspec/match_style.rb +1 -1
- data/lib/rubocop/cop/capybara/rspec/negation_matcher.rb +1 -1
- data/lib/rubocop/cop/capybara/rspec/negation_matcher_after_visit.rb +2 -2
- data/lib/rubocop/cop/capybara/rspec/predicate_matcher.rb +43 -7
- data/lib/rubocop/cop/capybara/rspec/specific_matcher.rb +8 -2
- data/lib/rubocop/cop/capybara/rspec/visibility_matcher.rb +29 -24
- data/lib/rubocop/cop/capybara/rspec.rb +25 -0
- data/lib/rubocop/cop/capybara/specific_actions.rb +12 -7
- data/lib/rubocop/cop/capybara/specific_finders.rb +149 -28
- data/lib/rubocop/cop/capybara/unordered_window_access.rb +40 -0
- data/lib/rubocop/cop/capybara/visibility_option.rb +64 -0
- data/lib/rubocop/cop/capybara.rb +28 -0
- data/lib/rubocop/cop/capybara_cops.rb +4 -16
- data/lib/rubocop-capybara.rb +7 -2
- metadata +18 -4
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
# Enforces use of `Element#[]` instead of `native.attribute`.
|
|
7
|
+
#
|
|
8
|
+
# Only calls on Capybara finder chains are checked.
|
|
9
|
+
#
|
|
10
|
+
# @safety
|
|
11
|
+
# This cop's autocorrection is unsafe because attribute values may
|
|
12
|
+
# differ between the driver API and Capybara's `Element#[]`.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
# find('.item').native.attribute(:id)
|
|
17
|
+
# page.find('.item').native.attribute('aria-label')
|
|
18
|
+
#
|
|
19
|
+
# # good
|
|
20
|
+
# find('.item')[:id]
|
|
21
|
+
# page.find('.item')['aria-label']
|
|
22
|
+
#
|
|
23
|
+
class NativeAttribute < RuboCop::Cop::Base
|
|
24
|
+
extend AutoCorrector
|
|
25
|
+
include CapybaraHelp
|
|
26
|
+
|
|
27
|
+
MSG = 'Use `Element#[]` instead of driver-specific `native.attribute`.'
|
|
28
|
+
RESTRICT_ON_SEND = %i[attribute].freeze
|
|
29
|
+
|
|
30
|
+
# @!method native_attribute(node)
|
|
31
|
+
def_node_matcher :native_attribute, <<~PATTERN
|
|
32
|
+
(call
|
|
33
|
+
$(call #element_finder? :native)
|
|
34
|
+
:attribute
|
|
35
|
+
$({str sym} _))
|
|
36
|
+
PATTERN
|
|
37
|
+
|
|
38
|
+
# @!method element_finder?(node)
|
|
39
|
+
def_node_matcher :element_finder?, <<~PATTERN
|
|
40
|
+
{#capybara_receiver?
|
|
41
|
+
(call #capybara_receiver?
|
|
42
|
+
{:ancestor :find :find_button :find_by_id
|
|
43
|
+
:find_field :find_link :first :sibling} ...)
|
|
44
|
+
(any_block #element_finder? ...)
|
|
45
|
+
(begin #element_finder?)}
|
|
46
|
+
PATTERN
|
|
47
|
+
|
|
48
|
+
def on_send(node)
|
|
49
|
+
native_attribute(node) do |native, attribute|
|
|
50
|
+
next unless native.receiver
|
|
51
|
+
|
|
52
|
+
add_offense(node) do |corrector|
|
|
53
|
+
next if attribute.str_type? && attribute.heredoc?
|
|
54
|
+
|
|
55
|
+
correct_attribute(corrector, node, native, attribute)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
alias on_csend on_send
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def correct_attribute(corrector, node, native, attribute)
|
|
64
|
+
range = node.source_range.with(begin_pos: native.loc.dot.begin_pos)
|
|
65
|
+
return if processed_source.comments.any? do |comment|
|
|
66
|
+
range.contains?(comment.source_range)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
safe_navigation = native.csend_type? && node.csend_type?
|
|
70
|
+
template = safe_navigation ? '&.[](%s)' : '[%s]'
|
|
71
|
+
corrector.replace(range, format(template, attribute.source))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -22,13 +22,13 @@ module RuboCop
|
|
|
22
22
|
# end
|
|
23
23
|
#
|
|
24
24
|
# # good
|
|
25
|
-
# within '
|
|
25
|
+
# within :id, 'foo' do
|
|
26
26
|
# # ...
|
|
27
27
|
# end
|
|
28
28
|
#
|
|
29
|
-
class RedundantWithinFind <
|
|
30
|
-
include CssSelector
|
|
29
|
+
class RedundantWithinFind < RuboCop::Cop::Base
|
|
31
30
|
extend AutoCorrector
|
|
31
|
+
|
|
32
32
|
MSG = 'Redundant `within %<method>s(...)` call detected.'
|
|
33
33
|
RESTRICT_ON_SEND = %i[within].freeze
|
|
34
34
|
FIND_METHODS = Set.new(%i[find find_by_id]).freeze
|
|
@@ -41,8 +41,10 @@ module RuboCop
|
|
|
41
41
|
|
|
42
42
|
def on_send(node)
|
|
43
43
|
within_find(node) do |find_node|
|
|
44
|
+
replacement = replaced(find_node)
|
|
45
|
+
|
|
44
46
|
add_offense(find_node, message: msg(find_node)) do |corrector|
|
|
45
|
-
corrector.replace(find_node,
|
|
47
|
+
corrector.replace(find_node, replacement) if replacement
|
|
46
48
|
end
|
|
47
49
|
end
|
|
48
50
|
end
|
|
@@ -54,24 +56,11 @@ module RuboCop
|
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
def replaced(node)
|
|
57
|
-
unless node.
|
|
58
|
-
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
if node.first_argument.str_type?
|
|
62
|
-
build_escaped_selector(node.first_argument, node)
|
|
63
|
-
else
|
|
64
|
-
node.arguments.map(&:source).join(', ')
|
|
65
|
-
.sub(/\A(["'])/, '\1#')
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def build_escaped_selector(first_arg, node)
|
|
70
|
-
quote = first_arg.source[0]
|
|
71
|
-
escaped_id = CssSelector.css_escape(first_arg.value, quote)
|
|
72
|
-
rest_args = node.arguments.drop(1).map(&:source)
|
|
59
|
+
return unless node.first_argument
|
|
60
|
+
return if node.arguments.any?(&:block_pass_type?)
|
|
73
61
|
|
|
74
|
-
|
|
62
|
+
args = node.arguments.map(&:source).join(', ')
|
|
63
|
+
node.method?(:find_by_id) ? ":id, #{args}" : args
|
|
75
64
|
end
|
|
76
65
|
end
|
|
77
66
|
end
|
|
@@ -25,9 +25,9 @@ module RuboCop
|
|
|
25
25
|
# expect(page).to have_current_path('/callback', ignore_query: true)
|
|
26
26
|
#
|
|
27
27
|
# # bad (does not support autocorrection when `match` with a variable)
|
|
28
|
-
# expect(
|
|
28
|
+
# expect(current_path).to match(variable)
|
|
29
29
|
#
|
|
30
|
-
class CurrentPathExpectation <
|
|
30
|
+
class CurrentPathExpectation < RuboCop::Cop::Base
|
|
31
31
|
extend AutoCorrector
|
|
32
32
|
include RangeHelp
|
|
33
33
|
|
|
@@ -79,8 +79,11 @@ module RuboCop
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
regexp_node_matcher(node.parent) do |to_sym, matcher_node, regexp|
|
|
82
|
+
regexp_expr = regexp_node_to_regexp_expr(regexp)
|
|
83
|
+
next unless regexp_expr
|
|
84
|
+
|
|
82
85
|
rewrite_expectation(corrector, node, to_sym, matcher_node)
|
|
83
|
-
|
|
86
|
+
corrector.replace(matcher_node.first_argument, regexp_expr)
|
|
84
87
|
end
|
|
85
88
|
end
|
|
86
89
|
|
|
@@ -94,22 +97,19 @@ module RuboCop
|
|
|
94
97
|
end
|
|
95
98
|
corrector.replace(matcher_node.loc.selector, matcher_method)
|
|
96
99
|
add_argument_parentheses(corrector, matcher_node.first_argument)
|
|
97
|
-
add_ignore_query_options(corrector, node
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def convert_regexp_node_to_literal(corrector, matcher_node,
|
|
101
|
-
regexp_node)
|
|
102
|
-
str_node = matcher_node.first_argument
|
|
103
|
-
regexp_expr = regexp_node_to_regexp_expr(regexp_node)
|
|
104
|
-
corrector.replace(str_node, regexp_expr)
|
|
100
|
+
add_ignore_query_options(corrector, node)
|
|
105
101
|
end
|
|
106
102
|
|
|
107
103
|
def regexp_node_to_regexp_expr(regexp_node)
|
|
108
104
|
if regexp_node.xstr_type?
|
|
109
105
|
"/\#{`#{regexp_node.value.value}`}/"
|
|
110
106
|
else
|
|
107
|
+
return if regexp_node.value.match?(/#[{@$]/)
|
|
108
|
+
|
|
111
109
|
Regexp.new(regexp_node.value).inspect
|
|
112
110
|
end
|
|
111
|
+
rescue RegexpError
|
|
112
|
+
nil
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def add_argument_parentheses(corrector, arg_node)
|
|
@@ -129,11 +129,8 @@ module RuboCop
|
|
|
129
129
|
|
|
130
130
|
# `have_current_path` with no options will include the querystring
|
|
131
131
|
# while `page.current_path` does not.
|
|
132
|
-
# This ensures the option `ignore_query: true` is added
|
|
133
|
-
|
|
134
|
-
def add_ignore_query_options(corrector, node, matcher_node)
|
|
135
|
-
return if matcher_node.method?(:match)
|
|
136
|
-
|
|
132
|
+
# This ensures the option `ignore_query: true` is added.
|
|
133
|
+
def add_ignore_query_options(corrector, node)
|
|
137
134
|
expectation_node = node.parent.last_argument
|
|
138
135
|
expectation_last_child = expectation_node.children.last
|
|
139
136
|
corrector.insert_after(expectation_last_child,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
module RSpec
|
|
7
|
+
# Checks expectations on `current_url` that bypass Capybara's waiting.
|
|
8
|
+
#
|
|
9
|
+
# `have_current_path` can match full URLs and waits for navigation.
|
|
10
|
+
# A regexp needs `url: true` so it matches the full URL, not just the
|
|
11
|
+
# path. Autocorrection is unsafe because URL parsing and waiting change
|
|
12
|
+
# comparison behavior.
|
|
13
|
+
# Dynamic `match` patterns and `include`, `start_with`, and `end_with`
|
|
14
|
+
# are reported without autocorrection.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad
|
|
18
|
+
# expect(page.current_url).to eq('https://example.com/login')
|
|
19
|
+
# expect(current_url).to match(%r{/login})
|
|
20
|
+
# expect(current_url).to match('login')
|
|
21
|
+
# expect(page.current_url).to include('/login')
|
|
22
|
+
#
|
|
23
|
+
# # good
|
|
24
|
+
# expect(page).to have_current_path('https://example.com/login')
|
|
25
|
+
# expect(page).to have_current_path(%r{/login}, url: true)
|
|
26
|
+
# expect(page).to have_current_path(/login/, url: true)
|
|
27
|
+
#
|
|
28
|
+
class CurrentUrlExpectation < RuboCop::Cop::Base
|
|
29
|
+
extend AutoCorrector
|
|
30
|
+
|
|
31
|
+
MSG = 'Use `have_current_path` on `page` to wait for the URL.'
|
|
32
|
+
RESTRICT_ON_SEND = %i[expect].freeze
|
|
33
|
+
PATH_MATCHERS = {
|
|
34
|
+
to: 'have_current_path',
|
|
35
|
+
not_to: 'have_no_current_path',
|
|
36
|
+
to_not: 'have_no_current_path'
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# @!method url_actual(node)
|
|
40
|
+
def_node_matcher :url_actual, <<~PATTERN
|
|
41
|
+
(send nil? :expect
|
|
42
|
+
$(send {(send nil? :page) nil?} :current_url))
|
|
43
|
+
PATTERN
|
|
44
|
+
|
|
45
|
+
# @!method supported_matcher?(node)
|
|
46
|
+
def_node_matcher :supported_matcher?, <<~PATTERN
|
|
47
|
+
{(send nil? :eq _)
|
|
48
|
+
(send nil? :match _)
|
|
49
|
+
(send nil? {:include :start_with :end_with} (str _))}
|
|
50
|
+
PATTERN
|
|
51
|
+
|
|
52
|
+
# @!method expectation_runner?(node)
|
|
53
|
+
def_node_matcher :expectation_runner?, <<~PATTERN
|
|
54
|
+
(send (send nil? :expect _) {:to :not_to :to_not} _)
|
|
55
|
+
PATTERN
|
|
56
|
+
|
|
57
|
+
def self.autocorrect_incompatible_with
|
|
58
|
+
[Style::TrailingCommaInArguments]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def on_send(node)
|
|
62
|
+
actual = url_actual(node)
|
|
63
|
+
return unless actual
|
|
64
|
+
|
|
65
|
+
runner = node.parent
|
|
66
|
+
return unless expectation_runner?(runner)
|
|
67
|
+
|
|
68
|
+
matcher = runner.first_argument
|
|
69
|
+
return unless supported_matcher?(matcher)
|
|
70
|
+
|
|
71
|
+
register_offense(node.loc.selector, actual, runner, matcher)
|
|
72
|
+
end
|
|
73
|
+
alias on_csend on_send
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def register_offense(range, actual, runner, matcher)
|
|
78
|
+
regexp = if matcher.method?(:match)
|
|
79
|
+
regexp_source(matcher.first_argument)
|
|
80
|
+
end
|
|
81
|
+
unless correctable_matcher?(matcher, regexp)
|
|
82
|
+
return add_offense(range)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
add_offense(range) do |corrector|
|
|
86
|
+
autocorrect(corrector, actual, runner, matcher, regexp)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def correctable_matcher?(matcher, regexp)
|
|
91
|
+
return true if matcher.method?(:eq)
|
|
92
|
+
return false unless matcher.method?(:match)
|
|
93
|
+
|
|
94
|
+
matcher.first_argument.regexp_type? || regexp
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def regexp_source(argument)
|
|
98
|
+
return unless argument.str_type?
|
|
99
|
+
return if argument.value.match?(/#[{@$]/)
|
|
100
|
+
|
|
101
|
+
Regexp.new(argument.value).inspect
|
|
102
|
+
rescue RegexpError
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def autocorrect(corrector, actual, runner, matcher, regexp)
|
|
107
|
+
corrector.replace(actual, 'page')
|
|
108
|
+
unless runner.method?(:to)
|
|
109
|
+
corrector.replace(runner.loc.selector, 'to')
|
|
110
|
+
end
|
|
111
|
+
corrector.replace(matcher.loc.selector,
|
|
112
|
+
PATH_MATCHERS.fetch(runner.method_name))
|
|
113
|
+
return unless matcher.method?(:match)
|
|
114
|
+
|
|
115
|
+
corrector.replace(matcher.first_argument, regexp) if regexp
|
|
116
|
+
corrector.insert_after(matcher.first_argument, ', url: true')
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
module RSpec
|
|
7
|
+
# Checks for expectations on the value of an element found by Capybara.
|
|
8
|
+
#
|
|
9
|
+
# @safety
|
|
10
|
+
# This cop's autocorrection is unsafe because `find('input').value`
|
|
11
|
+
# checks the value of the first matched element, while `have_field`
|
|
12
|
+
# checks that any field on the page has the expected value.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
# expect(find('input').value).to eq('foobar')
|
|
17
|
+
# expect(find(:css, 'input').value).to eq('foobar')
|
|
18
|
+
# expect(find_field('Email').value).to eq('user@example.com')
|
|
19
|
+
#
|
|
20
|
+
# # good
|
|
21
|
+
# expect(page).to have_field(with: 'foobar')
|
|
22
|
+
# expect(page).to have_field('Email', with: 'user@example.com')
|
|
23
|
+
#
|
|
24
|
+
class FindElementValueEq < RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
|
|
25
|
+
extend AutoCorrector
|
|
26
|
+
|
|
27
|
+
MSG = 'Prefer `have_field` over checking `find(...).value`.'
|
|
28
|
+
RESTRICT_ON_SEND = %i[expect].freeze
|
|
29
|
+
FIELD_ELEMENT_SELECTORS = %w[input textarea].freeze
|
|
30
|
+
|
|
31
|
+
# @!method find_element_value_eq(node)
|
|
32
|
+
def_node_matcher :find_element_value_eq, <<~PATTERN
|
|
33
|
+
(send
|
|
34
|
+
(send nil? :expect (send $_ :value))
|
|
35
|
+
${:to :to_not :not_to}
|
|
36
|
+
$(send nil? :eq $_)
|
|
37
|
+
)
|
|
38
|
+
PATTERN
|
|
39
|
+
|
|
40
|
+
def on_send(node)
|
|
41
|
+
find_element_value_eq(node.parent) do |finder, to_symbol,
|
|
42
|
+
matcher, expected|
|
|
43
|
+
register_offense(node, finder, to_symbol, matcher, expected)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
alias on_csend on_send
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def register_offense(node, finder, to_symbol, matcher, expected)
|
|
51
|
+
replacement = replacement_for_finder(finder)
|
|
52
|
+
return unless replacement
|
|
53
|
+
|
|
54
|
+
expect_target, field_locator = replacement
|
|
55
|
+
matcher_replacement =
|
|
56
|
+
replacement_matcher(to_symbol, expected, field_locator)
|
|
57
|
+
|
|
58
|
+
return add_offense(node.loc.selector) unless matcher_replacement
|
|
59
|
+
|
|
60
|
+
add_offense(node.loc.selector) do |corrector|
|
|
61
|
+
autocorrect(corrector, node, matcher, expect_target,
|
|
62
|
+
matcher_replacement)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def replacement_for_finder(node)
|
|
67
|
+
return unless node&.send_type?
|
|
68
|
+
|
|
69
|
+
case node.method_name
|
|
70
|
+
when :find
|
|
71
|
+
replacement_for_find(node)
|
|
72
|
+
when :find_field
|
|
73
|
+
replacement_for_find_field(node)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def replacement_for_find(node)
|
|
78
|
+
if (field_locator = field_selector_locator(node.arguments))
|
|
79
|
+
return [expect_target(node), field_locator]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
return unless field_element_selector?(node.arguments)
|
|
83
|
+
|
|
84
|
+
[expect_target(node), nil]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def replacement_for_find_field(node)
|
|
88
|
+
return unless node.arguments.one?
|
|
89
|
+
|
|
90
|
+
[expect_target(node), node.first_argument]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def field_element_selector?(arguments)
|
|
94
|
+
return field_element_name?(arguments.first) if arguments.one?
|
|
95
|
+
|
|
96
|
+
arguments.length == 2 &&
|
|
97
|
+
arguments.first.sym_type? &&
|
|
98
|
+
arguments.first.value == :css &&
|
|
99
|
+
field_element_name?(arguments.last)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def field_selector_locator(arguments)
|
|
103
|
+
return unless arguments.length == 2
|
|
104
|
+
|
|
105
|
+
selector_type, locator = arguments
|
|
106
|
+
return unless field_selector?(selector_type)
|
|
107
|
+
|
|
108
|
+
locator
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def field_selector?(node)
|
|
112
|
+
node.sym_type? && node.value == :field
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def field_element_name?(node)
|
|
116
|
+
node.str_type? && FIELD_ELEMENT_SELECTORS.include?(node.value)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def expect_target(node)
|
|
120
|
+
node.receiver&.source || 'page'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def autocorrect(corrector, node, matcher, expect_target,
|
|
124
|
+
matcher_replacement)
|
|
125
|
+
corrector.replace(node.first_argument, expect_target)
|
|
126
|
+
corrector.replace(node.parent.loc.selector, 'to')
|
|
127
|
+
corrector.replace(matcher, matcher_replacement)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def replacement_matcher(to_symbol, expected, field_locator)
|
|
131
|
+
expected_value = replacement_value(expected)
|
|
132
|
+
return unless expected_value
|
|
133
|
+
|
|
134
|
+
matcher = to_symbol == :to ? 'have_field' : 'have_no_field'
|
|
135
|
+
args = ["with: #{expected_value}"]
|
|
136
|
+
args.unshift(replacement_locator(field_locator)) if field_locator
|
|
137
|
+
|
|
138
|
+
"#{matcher}(#{args.join(', ')})"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def replacement_locator(locator)
|
|
142
|
+
return locator.source if locator.hash_type? && !locator.braces?
|
|
143
|
+
|
|
144
|
+
replacement_value(locator)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def replacement_value(expected)
|
|
148
|
+
return if unsafe_expected_value?(expected)
|
|
149
|
+
|
|
150
|
+
if expected.hash_type? && !expected.braces?
|
|
151
|
+
return "{#{expected.source}}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if method_call_with_no_parentheses?(expected)
|
|
155
|
+
return "(#{expected.source})"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
expected.source
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def unsafe_expected_value?(node)
|
|
162
|
+
node.splat_type? ||
|
|
163
|
+
(node.hash_type? && node.children.any?(&:kwsplat_type?))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def method_call_with_no_parentheses?(node)
|
|
167
|
+
node.send_type? && node.arguments? && !node.parenthesized?
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -4,20 +4,24 @@ module RuboCop
|
|
|
4
4
|
module Cop
|
|
5
5
|
module Capybara
|
|
6
6
|
module RSpec
|
|
7
|
-
# Use
|
|
7
|
+
# Use CSS or XPath matchers instead of generic selector matchers.
|
|
8
8
|
#
|
|
9
9
|
# @example
|
|
10
10
|
# # bad
|
|
11
11
|
# expect(foo).to have_selector(:css, 'bar')
|
|
12
|
+
# expect(foo).to have_no_selector(:css, 'bar')
|
|
12
13
|
#
|
|
13
14
|
# # good
|
|
14
15
|
# expect(foo).to have_css('bar')
|
|
16
|
+
# expect(foo).to have_no_css('bar')
|
|
15
17
|
#
|
|
16
18
|
# # bad
|
|
17
19
|
# expect(foo).to have_selector(:xpath, 'bar')
|
|
20
|
+
# expect(foo).to have_no_selector(:xpath, 'bar')
|
|
18
21
|
#
|
|
19
22
|
# # good
|
|
20
23
|
# expect(foo).to have_xpath('bar')
|
|
24
|
+
# expect(foo).to have_no_xpath('bar')
|
|
21
25
|
#
|
|
22
26
|
# @example DefaultSelector: css (default)
|
|
23
27
|
# # bad
|
|
@@ -33,12 +37,12 @@ module RuboCop
|
|
|
33
37
|
# # good
|
|
34
38
|
# expect(foo).to have_xpath('bar')
|
|
35
39
|
#
|
|
36
|
-
class HaveSelector <
|
|
40
|
+
class HaveSelector < RuboCop::Cop::Base
|
|
37
41
|
extend AutoCorrector
|
|
38
42
|
include RangeHelp
|
|
39
43
|
|
|
40
|
-
MSG = 'Use `%<good>s` instead of
|
|
41
|
-
RESTRICT_ON_SEND = %i[have_selector].freeze
|
|
44
|
+
MSG = 'Use `%<good>s` instead of `%<bad>s`.'
|
|
45
|
+
RESTRICT_ON_SEND = %i[have_selector have_no_selector].freeze
|
|
42
46
|
SELECTORS = %i[css xpath].freeze
|
|
43
47
|
|
|
44
48
|
def on_send(node)
|
|
@@ -54,33 +58,40 @@ module RuboCop
|
|
|
54
58
|
return unless SELECTORS.include?(type.value)
|
|
55
59
|
return unless (locator = node.arguments[1])
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
replacement = replacement(node, type.value)
|
|
62
|
+
add_offense(node,
|
|
63
|
+
message: message(node, replacement)) do |corrector|
|
|
58
64
|
corrector.remove(deletion_range(type, locator))
|
|
59
|
-
corrector.replace(node.loc.selector,
|
|
65
|
+
corrector.replace(node.loc.selector, replacement)
|
|
60
66
|
end
|
|
61
67
|
end
|
|
62
68
|
|
|
63
|
-
def message_typed(type)
|
|
64
|
-
format(MSG, good: "have_#{type.value}")
|
|
65
|
-
end
|
|
66
|
-
|
|
67
69
|
def deletion_range(first_argument, second_argument)
|
|
68
70
|
range_between(first_argument.source_range.begin_pos,
|
|
69
71
|
second_argument.source_range.begin_pos)
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def on_select_without_type(node)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
return unless (selector = default_selector)
|
|
76
|
+
|
|
77
|
+
replacement = replacement(node, selector)
|
|
78
|
+
add_offense(node,
|
|
79
|
+
message: message(node, replacement)) do |corrector|
|
|
80
|
+
corrector.replace(node.loc.selector, replacement)
|
|
75
81
|
end
|
|
76
82
|
end
|
|
77
83
|
|
|
78
|
-
def
|
|
79
|
-
|
|
84
|
+
def replacement(node, selector)
|
|
85
|
+
node.method_name.to_s.sub('selector', selector.to_s)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def message(node, replacement)
|
|
89
|
+
format(MSG, good: replacement, bad: node.method_name)
|
|
80
90
|
end
|
|
81
91
|
|
|
82
92
|
def default_selector
|
|
83
|
-
cop_config
|
|
93
|
+
selector = cop_config['DefaultSelector'].to_s.to_sym
|
|
94
|
+
selector if SELECTORS.include?(selector)
|
|
84
95
|
end
|
|
85
96
|
end
|
|
86
97
|
end
|
|
@@ -24,7 +24,7 @@ module RuboCop
|
|
|
24
24
|
# expect(page).not_to have_selector 'a'
|
|
25
25
|
# expect(page).not_to have_css('a')
|
|
26
26
|
#
|
|
27
|
-
class NegationMatcher <
|
|
27
|
+
class NegationMatcher < RuboCop::Cop::Base
|
|
28
28
|
extend AutoCorrector
|
|
29
29
|
include ConfigurableEnforcedStyle
|
|
30
30
|
include CapybaraHelp
|
|
@@ -27,7 +27,7 @@ module RuboCop
|
|
|
27
27
|
# expect(page).to have_css('a')
|
|
28
28
|
# expect(page).not_to have_link('bar')
|
|
29
29
|
#
|
|
30
|
-
class NegationMatcherAfterVisit <
|
|
30
|
+
class NegationMatcherAfterVisit < RuboCop::Cop::Base
|
|
31
31
|
include CapybaraHelp
|
|
32
32
|
|
|
33
33
|
MSG = 'Do not use negation matcher immediately after visit.'
|
|
@@ -37,7 +37,7 @@ module RuboCop
|
|
|
37
37
|
def_node_matcher :negation_matcher?, <<~PATTERN
|
|
38
38
|
{
|
|
39
39
|
(send (send nil? :expect _) :to (send nil? %NEGATIVE_MATCHERS ...))
|
|
40
|
-
(send (send nil? :expect _) :not_to (send nil? %POSITIVE_MATCHERS ...))
|
|
40
|
+
(send (send nil? :expect _) {:not_to :to_not} (send nil? %POSITIVE_MATCHERS ...))
|
|
41
41
|
}
|
|
42
42
|
PATTERN
|
|
43
43
|
|