/
current_path_expectation.rb
110 lines (96 loc) · 4.21 KB
/
current_path_expectation.rb
1
2
3
4
5
6
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# Checks that no expectations are set on Capybara's `current_path`.
#
# The `have_current_path` matcher (https://www.rubydoc.info/github/
# teamcapybara/capybara/master/Capybara/RSpecMatchers#have_current_path-
# instance_method) should be used on `page` to set expectations on
# Capybara's current path, since it uses Capybara's waiting
# functionality (https://github.com/teamcapybara/capybara/blob/master/
# README.md#asynchronous-javascript-ajax-and-friends) which ensures that
# preceding actions (like `click_link`) have completed.
#
# @example
# # bad
# expect(current_path).to eq('/callback')
# expect(page.current_path).to match(/widgets/)
#
# # good
# expect(page).to have_current_path("/callback")
# expect(page).to have_current_path(/widgets/)
#
class CurrentPathExpectation < Cop
extend AutoCorrector
MSG = 'Do not set an RSpec expectation on `current_path` in ' \
'Capybara feature specs - instead, use the ' \
'`have_current_path` matcher on `page`'
def_node_matcher :expectation_set_on_current_path, <<-PATTERN
(send nil? :expect (send {(send nil? :page) nil?} :current_path))
PATTERN
# Supported matchers: eq(...) / match(/regexp/) / match('regexp')
def_node_matcher :as_is_matcher, <<-PATTERN
(send
#expectation_set_on_current_path $#{Runners::ALL.node_pattern_union}
${(send nil? :eq ...) (send nil? :match (regexp ...))})
PATTERN
def_node_matcher :regexp_str_matcher, <<-PATTERN
(send
#expectation_set_on_current_path $#{Runners::ALL.node_pattern_union}
$(send nil? :match (str $_)))
PATTERN
def on_send(node)
expectation_set_on_current_path(node) do
add_offense(node.loc.selector) do |corrector|
next unless node.chained?
autocorrect(corrector, node)
end
end
end
private
def autocorrect(corrector, node)
as_is_matcher(node.parent) do |to_sym, matcher_node|
rewrite_expectation(corrector, node, to_sym, matcher_node)
end
regexp_str_matcher(node.parent) do |to_sym, matcher_node, regexp|
rewrite_expectation(corrector, node, to_sym, matcher_node)
convert_regexp_str_to_literal(corrector, matcher_node, regexp)
end
end
def rewrite_expectation(corrector, node, to_symbol, matcher_node)
current_path_node = node.first_argument
corrector.replace(current_path_node, 'page')
corrector.replace(node.parent.loc.selector, 'to')
matcher_method = if to_symbol == :to
'have_current_path'
else
'have_no_current_path'
end
corrector.replace(matcher_node.loc.selector, matcher_method)
add_ignore_query_options(corrector, node)
end
def convert_regexp_str_to_literal(corrector, matcher_node, regexp_str)
str_node = matcher_node.first_argument
regexp_expr = Regexp.new(regexp_str).inspect
corrector.replace(str_node, regexp_expr)
end
# `have_current_path` with no options will include the querystring
# while `page.current_path` does not.
# This ensures the option `ignore_query: true` is added
# except when the expectation is a regexp or string
def add_ignore_query_options(corrector, node)
expectation_node = node.parent.last_argument
expectation_last_child = expectation_node.children.last
return if %i[regexp str].include?(expectation_last_child.type)
corrector.insert_after(
expectation_last_child,
', ignore_query: true'
)
end
end
end
end
end
end