rubocop-capybara 2.23.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 +53 -0
- data/README.md +1 -1
- data/config/default.yml +97 -32
- data/lib/rubocop/capybara/plugin.rb +2 -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 -17
- data/lib/rubocop-capybara.rb +7 -5
- metadata +18 -6
- data/config/obsoletion.yml +0 -18
- data/lib/rubocop/cop/capybara/click_link_or_button_style.rb +0 -87
|
@@ -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
|
|
@@ -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
|