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,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
# Checks for query options that have no effect in Capybara.
|
|
7
|
+
#
|
|
8
|
+
# Count options passed to singular finders are not autocorrected because
|
|
9
|
+
# removing them may conceal the author's intent.
|
|
10
|
+
#
|
|
11
|
+
# This cop targets Capybara's built-in selectors. Set `DefaultSelector`
|
|
12
|
+
# to match `Capybara.default_selector`, or to `null` if it varies at
|
|
13
|
+
# runtime. Queries using custom or unknown selectors are skipped.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# # bad
|
|
17
|
+
# find('.row', count: 3)
|
|
18
|
+
# find(:css, '.item', exact: true)
|
|
19
|
+
# find('.item', text: /foo/, exact_text: true)
|
|
20
|
+
# all(:css, '.item', match: :first)
|
|
21
|
+
# expect(page).to have_css('.item', count: 3, minimum: 1)
|
|
22
|
+
#
|
|
23
|
+
# # good
|
|
24
|
+
# all('.row', count: 3)
|
|
25
|
+
# find(:css, '.item')
|
|
26
|
+
# find('.item', text: /foo/)
|
|
27
|
+
# all(:css, '.item')
|
|
28
|
+
# expect(page).to have_css('.item', count: 3)
|
|
29
|
+
#
|
|
30
|
+
# @example DefaultSelector: custom
|
|
31
|
+
# # bad
|
|
32
|
+
# find(:css, '.item', text: /foo/, exact_text: true)
|
|
33
|
+
#
|
|
34
|
+
# # good
|
|
35
|
+
# find('.item', text: /foo/, exact_text: true)
|
|
36
|
+
#
|
|
37
|
+
class IgnoredQueryOption < RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
|
|
38
|
+
extend AutoCorrector
|
|
39
|
+
include RangeHelp
|
|
40
|
+
include CapybaraHelp
|
|
41
|
+
|
|
42
|
+
RESTRICT_ON_SEND = QueryMethods.names(
|
|
43
|
+
:finder, :collection, :selector, :match, :text
|
|
44
|
+
).freeze
|
|
45
|
+
COUNT_OPTIONS = %i[count minimum maximum between].freeze
|
|
46
|
+
MATCH_STRATEGIES = %i[first smart prefer_exact one].freeze
|
|
47
|
+
|
|
48
|
+
FIND_COUNT_MSG =
|
|
49
|
+
'`%<option>s` has no effect when passed to `%<method>s`.'
|
|
50
|
+
EXACT_MSG = '`exact` has no effect on CSS queries.'
|
|
51
|
+
EXACT_TEXT_MSG =
|
|
52
|
+
'`exact_text` has no effect when `text` is a regexp.'
|
|
53
|
+
MATCH_MSG =
|
|
54
|
+
'`match` has no effect when passed to `%<method>s`.'
|
|
55
|
+
SHADOWED_COUNT_MSG =
|
|
56
|
+
'`%<option>s` has no effect when `count` is specified.'
|
|
57
|
+
|
|
58
|
+
# @!method query_options(node)
|
|
59
|
+
def_node_matcher :query_options, <<~PATTERN
|
|
60
|
+
(call #capybara_receiver? _ <$(hash ...) ...>)
|
|
61
|
+
PATTERN
|
|
62
|
+
|
|
63
|
+
# @!method custom_filters?(node)
|
|
64
|
+
def_node_matcher :custom_filters?, <<~PATTERN
|
|
65
|
+
(hash <{(pair (sym {:filter_set :session_options}) _) (pair !sym _) kwsplat} ...>)
|
|
66
|
+
PATTERN
|
|
67
|
+
|
|
68
|
+
# @!method removable_value?(node)
|
|
69
|
+
def_node_matcher :removable_value?, <<~PATTERN
|
|
70
|
+
{basic_literal? (range {int nil?} {int nil?})}
|
|
71
|
+
PATTERN
|
|
72
|
+
|
|
73
|
+
def on_send(node)
|
|
74
|
+
return unless (options = query_options(node))
|
|
75
|
+
return if options.braces?
|
|
76
|
+
return if custom_filters?(options)
|
|
77
|
+
|
|
78
|
+
issues = options.pairs.filter_map do |pair|
|
|
79
|
+
issue_for(node, options, pair)
|
|
80
|
+
end
|
|
81
|
+
return if issues.empty?
|
|
82
|
+
|
|
83
|
+
register_issues(node, options, issues)
|
|
84
|
+
end
|
|
85
|
+
alias on_csend on_send
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def register_issues(node, options, issues)
|
|
90
|
+
correction_pair = issues.find do |pair, _, correctable|
|
|
91
|
+
correctable && removable?(options, pair) && !comments_in?(node)
|
|
92
|
+
end
|
|
93
|
+
issues.each do |pair, message, _|
|
|
94
|
+
add_offense(pair, message: message) do |corrector|
|
|
95
|
+
next unless pair == correction_pair&.first
|
|
96
|
+
|
|
97
|
+
remove_option(corrector, node, options, pair)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def issue_for(node, options, pair)
|
|
103
|
+
option = pair.key.value
|
|
104
|
+
issue = find_count_issue(node, option) ||
|
|
105
|
+
exact_issue(node, option) ||
|
|
106
|
+
exact_text_issue(node, option, pair, options) ||
|
|
107
|
+
match_issue(node, option, pair) ||
|
|
108
|
+
shadowed_count_issue(node, option, options)
|
|
109
|
+
|
|
110
|
+
[pair, *issue] if issue
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def find_count_issue(node, option)
|
|
114
|
+
return unless QueryMethods.type(node.method_name) == :finder
|
|
115
|
+
return unless COUNT_OPTIONS.include?(option)
|
|
116
|
+
return if custom_selector?(node)
|
|
117
|
+
|
|
118
|
+
[
|
|
119
|
+
format(FIND_COUNT_MSG,
|
|
120
|
+
option: option,
|
|
121
|
+
method: node.method_name),
|
|
122
|
+
false
|
|
123
|
+
]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def shadowed_count_issue(node, option, options)
|
|
127
|
+
return if QueryMethods.type(node.method_name) == :match
|
|
128
|
+
return unless count_query_with_known_selector?(node)
|
|
129
|
+
return unless shadowed_count?(option, options)
|
|
130
|
+
|
|
131
|
+
[format(SHADOWED_COUNT_MSG, option: option), true]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def exact_issue(node, option)
|
|
135
|
+
[EXACT_MSG, true] if option == :exact && css_query?(node)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def exact_text_issue(node, option, pair, options)
|
|
139
|
+
return unless option == :exact_text
|
|
140
|
+
return unless ineffective_exact_text?(node, pair, options)
|
|
141
|
+
|
|
142
|
+
[EXACT_TEXT_MSG, true]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def match_issue(node, option, pair)
|
|
146
|
+
return unless option == :match && ineffective_match?(node, pair)
|
|
147
|
+
|
|
148
|
+
[format(MATCH_MSG, method: node.method_name), true]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def css_query?(node)
|
|
152
|
+
selector = QueryMethods.selector(node.method_name)
|
|
153
|
+
selector == :css ||
|
|
154
|
+
(selector == :selector && explicit_selector(node) == :css)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def explicit_selector(node)
|
|
158
|
+
argument = node.first_argument
|
|
159
|
+
argument.value if argument&.sym_type?
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def ineffective_exact_text?(node, pair, options)
|
|
163
|
+
exact_text_supported_call?(node) &&
|
|
164
|
+
effective_option_pair(options, :exact_text) == pair &&
|
|
165
|
+
pair.value.type?(:boolean) &&
|
|
166
|
+
effective_option_pair(options, :text)&.value&.regexp_type?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def exact_text_supported_call?(node)
|
|
170
|
+
QueryMethods.type(node.method_name) != :text &&
|
|
171
|
+
!custom_selector?(node)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def ineffective_match?(node, pair)
|
|
175
|
+
QueryMethods.type(node.method_name) == :collection &&
|
|
176
|
+
QueryMethods.built_in_selector?(explicit_selector(node)) &&
|
|
177
|
+
pair.value.sym_type? &&
|
|
178
|
+
MATCH_STRATEGIES.include?(pair.value.value)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def shadowed_count?(option, options)
|
|
182
|
+
option != :count && COUNT_OPTIONS.include?(option) &&
|
|
183
|
+
count_specified?(options)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def count_query_with_known_selector?(node)
|
|
187
|
+
selector = QueryMethods.selector(node.method_name)
|
|
188
|
+
return true unless selector == :selector
|
|
189
|
+
|
|
190
|
+
QueryMethods.built_in_selector?(explicit_selector(node))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def custom_selector?(node)
|
|
194
|
+
QueryMethods.selector(node.method_name) == :selector &&
|
|
195
|
+
!QueryMethods.built_in_selector?(query_selector(node))
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def query_selector(node)
|
|
199
|
+
selector = explicit_selector(node)
|
|
200
|
+
return selector if selector
|
|
201
|
+
|
|
202
|
+
positional = node.arguments.reject do |arg|
|
|
203
|
+
arg.type?(:hash, :block_pass)
|
|
204
|
+
end
|
|
205
|
+
return if positional.length > 1
|
|
206
|
+
|
|
207
|
+
cop_config['DefaultSelector'].to_s.to_sym
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def count_specified?(options)
|
|
211
|
+
effective_option_pair(options, :count)&.value&.truthy_literal?
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def effective_option_pair(options, name)
|
|
215
|
+
options.pairs.reverse_each.find { |pair| pair.key.value == name }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def comments_in?(node)
|
|
219
|
+
processed_source.comments.any? do |comment|
|
|
220
|
+
comment.loc.line.between?(node.first_line, node.last_line)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def removable?(options, pair)
|
|
225
|
+
return false unless removable_value?(pair.value)
|
|
226
|
+
return false if pair.value.str_type? && pair.value.heredoc?
|
|
227
|
+
|
|
228
|
+
options.pairs.one? { |other| other.key == pair.key }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def remove_option(corrector, node, options, pair)
|
|
232
|
+
first = node.first_argument == options &&
|
|
233
|
+
options.children.first == pair
|
|
234
|
+
side = first ? :right : :left
|
|
235
|
+
range = range_with_surrounding_space(pair.source_range, side: side)
|
|
236
|
+
range = range_with_surrounding_comma(range, side)
|
|
237
|
+
corrector.remove(range_with_surrounding_space(range, side: side))
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -5,11 +5,13 @@ module RuboCop
|
|
|
5
5
|
module Capybara
|
|
6
6
|
# Help methods for capybara.
|
|
7
7
|
# @api private
|
|
8
|
-
module CapybaraHelp
|
|
8
|
+
module CapybaraHelp # rubocop:disable Metrics/ModuleLength
|
|
9
|
+
extend RuboCop::AST::NodePattern::Macros
|
|
10
|
+
|
|
9
11
|
CAPYBARA_MATCHERS = %w[
|
|
10
12
|
selector css xpath text title current_path link button
|
|
11
13
|
field checked_field unchecked_field select table
|
|
12
|
-
sibling ancestor content
|
|
14
|
+
sibling ancestor content element
|
|
13
15
|
].freeze
|
|
14
16
|
POSITIVE_MATCHERS =
|
|
15
17
|
Set.new(CAPYBARA_MATCHERS) { |element| :"have_#{element}" }.freeze
|
|
@@ -45,9 +47,91 @@ module RuboCop
|
|
|
45
47
|
SPECIFIC_PSEUDO_CLASSES = %w[
|
|
46
48
|
not() disabled enabled checked unchecked
|
|
47
49
|
].freeze
|
|
50
|
+
VISIBILITY_MATCHER_METHODS = %w[
|
|
51
|
+
button checked_field css field link select selector table
|
|
52
|
+
unchecked_field xpath
|
|
53
|
+
].flat_map do |element|
|
|
54
|
+
[:"have_#{element}", :"have_no_#{element}"]
|
|
55
|
+
end.freeze
|
|
56
|
+
VISIBILITY_FALSE_MESSAGE = 'Use `:all` or `:hidden` instead of `false`.'
|
|
57
|
+
VISIBILITY_TRUE_MESSAGE = 'Use `:visible` instead of `true`.'
|
|
48
58
|
|
|
49
59
|
module_function
|
|
50
60
|
|
|
61
|
+
# Recognize the Capybara DSL and finder chains, not arbitrary objects
|
|
62
|
+
# with methods such as Enumerable#find or ActiveRecord#first.
|
|
63
|
+
# Recursive references intentionally use the pattern-only matcher.
|
|
64
|
+
# Resolving local variables here would recurse forever for assignments
|
|
65
|
+
# such as `tr = tr.find(...)`.
|
|
66
|
+
# @!method capybara_receiver_pattern?(node)
|
|
67
|
+
def_node_matcher :capybara_receiver_pattern?, <<~PATTERN
|
|
68
|
+
{nil? self
|
|
69
|
+
(call {nil? self} :page)
|
|
70
|
+
(call (const {nil? cbase} :Capybara) :current_session)
|
|
71
|
+
(call #capybara_receiver_pattern?
|
|
72
|
+
{:all :ancestor :find :find_all :find_button :find_by_id
|
|
73
|
+
:find_field :find_link :first :sibling} ...)
|
|
74
|
+
(any_block #capybara_receiver_pattern? ...)
|
|
75
|
+
(begin #capybara_receiver_pattern?) }
|
|
76
|
+
PATTERN
|
|
77
|
+
module_function :capybara_receiver_pattern?
|
|
78
|
+
|
|
79
|
+
# @param node [RuboCop::AST::Node]
|
|
80
|
+
# @return [Boolean]
|
|
81
|
+
def capybara_receiver?(node)
|
|
82
|
+
capybara_receiver_pattern?(node) ||
|
|
83
|
+
local_variable_capybara_receiver?(node)
|
|
84
|
+
end
|
|
85
|
+
module_function :capybara_receiver?
|
|
86
|
+
|
|
87
|
+
# ponytail: Only preceding direct assignments in visible lexical scopes;
|
|
88
|
+
# use data-flow analysis if coverage grows.
|
|
89
|
+
def local_variable_capybara_receiver?(node)
|
|
90
|
+
value = local_variable_value(node)
|
|
91
|
+
value && capybara_receiver_pattern?(value)
|
|
92
|
+
end
|
|
93
|
+
module_function :local_variable_capybara_receiver?
|
|
94
|
+
|
|
95
|
+
def local_variable_value(node)
|
|
96
|
+
return unless node.lvar_type?
|
|
97
|
+
|
|
98
|
+
name = node.children.first
|
|
99
|
+
scopes = node.each_ancestor.take_while do |scope|
|
|
100
|
+
outer_assignment_visible_through?(scope, name)
|
|
101
|
+
end
|
|
102
|
+
assignment = scopes.filter_map do |scope|
|
|
103
|
+
local_variable_assignment(node, scope) if scope.begin_type?
|
|
104
|
+
end.first
|
|
105
|
+
assignment && assignment.children[1]
|
|
106
|
+
end
|
|
107
|
+
module_function :local_variable_value
|
|
108
|
+
|
|
109
|
+
def outer_assignment_visible_through?(scope, name)
|
|
110
|
+
return false if scope.type?(:any_def, :class, :module, :sclass)
|
|
111
|
+
|
|
112
|
+
!scope.block_type? ||
|
|
113
|
+
!shadowed_block_variable?(scope.arguments, name)
|
|
114
|
+
end
|
|
115
|
+
module_function :outer_assignment_visible_through?
|
|
116
|
+
|
|
117
|
+
def shadowed_block_variable?(arguments, name)
|
|
118
|
+
args = arguments.each_node(
|
|
119
|
+
:arg, :optarg, :restarg, :kwarg,
|
|
120
|
+
:kwoptarg, :kwrestarg, :blockarg, :shadowarg
|
|
121
|
+
)
|
|
122
|
+
args.any? { |arg| arg.children.first == name }
|
|
123
|
+
end
|
|
124
|
+
module_function :shadowed_block_variable?
|
|
125
|
+
|
|
126
|
+
def local_variable_assignment(node, scope)
|
|
127
|
+
scope.children.reverse.find do |child|
|
|
128
|
+
child.lvasgn_type? &&
|
|
129
|
+
child.children.first == node.children.first &&
|
|
130
|
+
child.source_range.end_pos <= node.source_range.begin_pos
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
module_function :local_variable_assignment
|
|
134
|
+
|
|
51
135
|
# @param node [RuboCop::AST::SendNode]
|
|
52
136
|
# @param locator [String]
|
|
53
137
|
# @param element [String]
|
|
@@ -6,6 +6,8 @@ module RuboCop
|
|
|
6
6
|
# Css selector parser.
|
|
7
7
|
# @api private
|
|
8
8
|
class CssAttributesParser
|
|
9
|
+
QUOTE_CHARS = ['"', "'"].freeze
|
|
10
|
+
|
|
9
11
|
def initialize(selector)
|
|
10
12
|
@selector = selector
|
|
11
13
|
@state = :initial
|
|
@@ -14,7 +16,7 @@ module RuboCop
|
|
|
14
16
|
@bracket_count = 0
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
# @return [
|
|
19
|
+
# @return [Hash<String, String, Boolean, nil>]
|
|
18
20
|
def parse # rubocop:disable Metrics/MethodLength
|
|
19
21
|
@selector.each_char do |char|
|
|
20
22
|
if char == '['
|
|
@@ -43,7 +45,7 @@ module RuboCop
|
|
|
43
45
|
@bracket_count -= 1
|
|
44
46
|
if @bracket_count.zero?
|
|
45
47
|
@state = :initial
|
|
46
|
-
key, value = @temp.split('=')
|
|
48
|
+
key, value = @temp.split('=', 2)
|
|
47
49
|
@results[key] = normalize_value(value)
|
|
48
50
|
@temp.clear
|
|
49
51
|
else
|
|
@@ -52,20 +54,36 @@ module RuboCop
|
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
# @param value [String]
|
|
55
|
-
# @return [Boolean, String]
|
|
57
|
+
# @return [Boolean, String, nil]
|
|
56
58
|
# @example
|
|
57
59
|
# normalize_value('true') # => true
|
|
58
60
|
# normalize_value('false') # => false
|
|
59
61
|
# normalize_value(nil) # => nil
|
|
60
|
-
# normalize_value("foo") # => "
|
|
62
|
+
# normalize_value("foo") # => "foo"
|
|
63
|
+
# normalize_value("'foo'") # => "foo"
|
|
61
64
|
def normalize_value(value)
|
|
65
|
+
return if value&.match?(/\s+[is]\s*\z/i)
|
|
66
|
+
|
|
62
67
|
case value
|
|
63
68
|
when 'true' then true
|
|
64
69
|
when 'false' then false
|
|
65
70
|
when nil then nil
|
|
66
|
-
else
|
|
71
|
+
else strip_outer_quotes(value)
|
|
67
72
|
end
|
|
68
73
|
end
|
|
74
|
+
|
|
75
|
+
def strip_outer_quotes(value)
|
|
76
|
+
return value unless quoted?(value)
|
|
77
|
+
|
|
78
|
+
value[1...-1]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def quoted?(value)
|
|
82
|
+
return false if value.length < 2
|
|
83
|
+
|
|
84
|
+
quote = value[0]
|
|
85
|
+
QUOTE_CHARS.include?(quote) && value.end_with?(quote)
|
|
86
|
+
end
|
|
69
87
|
end
|
|
70
88
|
end
|
|
71
89
|
end
|
|
@@ -8,6 +8,8 @@ module RuboCop
|
|
|
8
8
|
module CssSelector
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
|
+
ID_PATTERN = /\A#((?:[\w-]|\\[.>,+~])+)(?:\.|\z)/.freeze
|
|
12
|
+
|
|
11
13
|
# @param selector [String]
|
|
12
14
|
# @return [String]
|
|
13
15
|
# @example
|
|
@@ -15,9 +17,7 @@ module RuboCop
|
|
|
15
17
|
# id('.some-cls') # => nil
|
|
16
18
|
# id('#some-id.cls') # => some-id
|
|
17
19
|
def id(selector)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
selector.delete('#').gsub(selector.scan(/[^\\]([>,+~.].*)/).join, '')
|
|
20
|
+
selector[ID_PATTERN, 1]
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# @param selector [String]
|
|
@@ -26,18 +26,21 @@ module RuboCop
|
|
|
26
26
|
# id?('#some-id') # => true
|
|
27
27
|
# id?('.some-cls') # => false
|
|
28
28
|
def id?(selector)
|
|
29
|
-
selector.
|
|
29
|
+
!id(selector).nil?
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# @param selector [String]
|
|
33
|
-
# @return [
|
|
33
|
+
# @return [Hash<String, String, Boolean, nil>]
|
|
34
34
|
# @example
|
|
35
35
|
# classes('#some-id') # => []
|
|
36
36
|
# classes('.some-cls') # => ['some-cls']
|
|
37
37
|
# classes('#some-id.some-cls') # => ['some-cls']
|
|
38
38
|
# classes('#some-id.cls1.cls2') # => ['cls1', 'cls2']
|
|
39
|
+
# classes('.some-cls\.with-dot') # => ['some-cls.with-dot']
|
|
39
40
|
def classes(selector)
|
|
40
|
-
selector.scan(
|
|
41
|
+
selector.scan(/(?<!\\)\.((?:\\.|[\w-])+)/).flatten.map do |class_name|
|
|
42
|
+
class_name.gsub('\\.', '.')
|
|
43
|
+
end
|
|
41
44
|
end
|
|
42
45
|
|
|
43
46
|
# @param selector [String]
|
|
@@ -50,12 +53,12 @@ module RuboCop
|
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
# @param selector [String]
|
|
53
|
-
# @return [
|
|
56
|
+
# @return [Hash<String, String, Boolean, nil>]
|
|
54
57
|
# @example
|
|
55
58
|
# attributes('a[foo-bar_baz]') # => {"foo-bar_baz=>nil}
|
|
56
|
-
# attributes('button[foo][bar=baz]') # => {"foo"=>nil, "bar"=>"
|
|
57
|
-
# attributes('table[foo=bar]') # => {"foo"=>"
|
|
58
|
-
# attributes('[foo="bar[baz][qux]"]') # => {"foo"=>"
|
|
59
|
+
# attributes('button[foo][bar=baz]') # => {"foo"=>nil, "bar"=>"baz"}
|
|
60
|
+
# attributes('table[foo=bar]') # => {"foo"=>"bar"}
|
|
61
|
+
# attributes('[foo="bar[baz][qux]"]') # => {"foo"=>"bar[baz][qux]"}
|
|
59
62
|
def attributes(selector)
|
|
60
63
|
CssAttributesParser.new(selector).parse
|
|
61
64
|
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
# Classification of Capybara's public query methods.
|
|
7
|
+
# @api private
|
|
8
|
+
module QueryMethods
|
|
9
|
+
BUILT_IN_SELECTORS = %i[
|
|
10
|
+
button checkbox css datalist_input datalist_option element field
|
|
11
|
+
fieldset file_field fillable_field frame id label link link_or_button
|
|
12
|
+
option radio_button select table table_row xpath
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
# Each entry contains the query type and its selector. `:selector`
|
|
16
|
+
# means the selector is an argument; nil means it is not applicable.
|
|
17
|
+
METHODS = begin
|
|
18
|
+
methods = {
|
|
19
|
+
find: %i[finder selector],
|
|
20
|
+
ancestor: %i[finder selector],
|
|
21
|
+
sibling: %i[finder selector],
|
|
22
|
+
find_button: %i[finder button],
|
|
23
|
+
find_by_id: %i[finder id],
|
|
24
|
+
find_field: %i[finder field],
|
|
25
|
+
find_link: %i[finder link],
|
|
26
|
+
within: %i[finder selector]
|
|
27
|
+
}
|
|
28
|
+
%i[all find_all first].each do |name|
|
|
29
|
+
methods[name] = %i[collection selector]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
%w[
|
|
33
|
+
ancestor button checked_field css element field link select
|
|
34
|
+
selector sibling table unchecked_field xpath
|
|
35
|
+
].each do |selector|
|
|
36
|
+
kind = case selector
|
|
37
|
+
when 'ancestor', 'sibling' then :selector
|
|
38
|
+
when 'checked_field', 'unchecked_field' then :field
|
|
39
|
+
else selector.to_sym
|
|
40
|
+
end
|
|
41
|
+
prefixes = %w[assert assert_no refute has has_no have have_no]
|
|
42
|
+
prefixes += %w[must_have wont_have] unless selector == 'element'
|
|
43
|
+
prefixes.each do |prefix|
|
|
44
|
+
name = "#{prefix}_#{selector}"
|
|
45
|
+
name += '?' if prefix.start_with?('has')
|
|
46
|
+
methods[name.to_sym] = [:selector, kind]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
%w[css selector xpath].each do |selector|
|
|
51
|
+
%W[
|
|
52
|
+
assert_matches_#{selector} assert_not_matches_#{selector}
|
|
53
|
+
refute_matches_#{selector} matches_#{selector}?
|
|
54
|
+
not_matches_#{selector}? match_#{selector} not_match_#{selector}
|
|
55
|
+
must_match_#{selector} wont_match_#{selector}
|
|
56
|
+
].each { |name| methods[name.to_sym] = [:match, selector.to_sym] }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
%w[text content].each do |kind|
|
|
60
|
+
%W[
|
|
61
|
+
assert_#{kind} assert_no_#{kind} refute_#{kind}
|
|
62
|
+
has_#{kind}? has_no_#{kind}? have_#{kind} have_no_#{kind}
|
|
63
|
+
must_have_#{kind} wont_have_#{kind}
|
|
64
|
+
].each { |name| methods[name.to_sym] = [:text, nil] }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
%i[
|
|
68
|
+
attach_file check choose click_button click_link
|
|
69
|
+
click_link_or_button click_on fill_in select uncheck unselect
|
|
70
|
+
].each { |name| methods[name] = [:action, nil] }
|
|
71
|
+
|
|
72
|
+
%w[assert have must_have].product(%w[all any none]) do |prefix, count|
|
|
73
|
+
methods[:"#{prefix}_#{count}_of_selectors"] = %i[grouped selector]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
methods.transform_values(&:freeze).freeze
|
|
77
|
+
end
|
|
78
|
+
private_constant :METHODS, :BUILT_IN_SELECTORS
|
|
79
|
+
|
|
80
|
+
module_function
|
|
81
|
+
|
|
82
|
+
# @param types [Array<Symbol>]
|
|
83
|
+
# @return [Array<Symbol>]
|
|
84
|
+
# @example
|
|
85
|
+
# names(:collection) # => [:all, :find_all, :first]
|
|
86
|
+
def names(*types)
|
|
87
|
+
METHODS.filter_map { |name, (type, _)| name if types.include?(type) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @param method_name [Symbol]
|
|
91
|
+
# @return [Symbol, nil]
|
|
92
|
+
# @example
|
|
93
|
+
# type(:find) # => :finder
|
|
94
|
+
# type(:unknown) # => nil
|
|
95
|
+
def type(method_name)
|
|
96
|
+
METHODS[method_name]&.first
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @param method_name [Symbol]
|
|
100
|
+
# @return [Symbol, nil]
|
|
101
|
+
# @example
|
|
102
|
+
# selector(:find_button) # => :button
|
|
103
|
+
# selector(:find) # => :selector
|
|
104
|
+
# selector(:have_text) # => nil
|
|
105
|
+
def selector(method_name)
|
|
106
|
+
METHODS[method_name]&.last
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @param selector [Symbol]
|
|
110
|
+
# @return [Boolean]
|
|
111
|
+
# @example
|
|
112
|
+
# built_in_selector?(:css) # => true
|
|
113
|
+
# built_in_selector?(:custom) # => false
|
|
114
|
+
def built_in_selector?(selector)
|
|
115
|
+
BUILT_IN_SELECTORS.include?(selector)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
# Shared visibility query checks and offense handling.
|
|
7
|
+
# @api private
|
|
8
|
+
module Visibility
|
|
9
|
+
extend RuboCop::AST::NodePattern::Macros
|
|
10
|
+
|
|
11
|
+
# @!method custom_filters?(node)
|
|
12
|
+
def_node_matcher :custom_filters?, <<~PATTERN
|
|
13
|
+
(hash <{(pair (sym :filter_set) _) (pair !sym _) kwsplat} ...>)
|
|
14
|
+
PATTERN
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def supported_visibility_query?(node, options)
|
|
19
|
+
return false if options.braces? || custom_filters?(options)
|
|
20
|
+
return true if QueryMethods.selector(node.method_name) != :selector
|
|
21
|
+
|
|
22
|
+
QueryMethods.built_in_selector?(query_selector(node))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def query_selector(node)
|
|
26
|
+
selector = node.first_argument
|
|
27
|
+
return selector.value if selector&.sym_type?
|
|
28
|
+
|
|
29
|
+
positional = node.arguments.reject do |arg|
|
|
30
|
+
arg.type?(:hash, :block_pass)
|
|
31
|
+
end
|
|
32
|
+
return if positional.length > 1 && !selector.str_type?
|
|
33
|
+
|
|
34
|
+
cop_config['DefaultSelector'].to_s.to_sym
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def check_visibility(pair)
|
|
38
|
+
if pair.value.false_type?
|
|
39
|
+
return add_offense(pair, message: CapybaraHelp::VISIBILITY_FALSE_MESSAGE)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
add_offense(
|
|
43
|
+
pair, message: CapybaraHelp::VISIBILITY_TRUE_MESSAGE
|
|
44
|
+
) do |corrector|
|
|
45
|
+
next if block_given? && !yield
|
|
46
|
+
|
|
47
|
+
corrector.replace(pair.value, ':visible')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
# Checks that modal handling methods are called with a block.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# accept_confirm
|
|
11
|
+
# page.accept_prompt(with: 'answer')
|
|
12
|
+
#
|
|
13
|
+
# # good
|
|
14
|
+
# accept_confirm { click_button 'Delete' }
|
|
15
|
+
# page.accept_prompt(with: 'answer', &trigger_modal)
|
|
16
|
+
#
|
|
17
|
+
class ModalMethodWithoutBlock < RuboCop::Cop::Base
|
|
18
|
+
MSG = 'Call `%<method>s` with a block.'
|
|
19
|
+
RESTRICT_ON_SEND = %i[
|
|
20
|
+
accept_alert
|
|
21
|
+
accept_confirm
|
|
22
|
+
accept_prompt
|
|
23
|
+
dismiss_confirm
|
|
24
|
+
dismiss_prompt
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def on_send(node)
|
|
28
|
+
return if node.block_literal? || node.block_argument?
|
|
29
|
+
return if node.last_argument&.forwarded_args_type?
|
|
30
|
+
|
|
31
|
+
add_offense(node, message: format(MSG, method: node.method_name))
|
|
32
|
+
end
|
|
33
|
+
alias on_csend on_send
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|