Skip to content

Commit

Permalink
Extract Capybara cops
Browse files Browse the repository at this point in the history
  • Loading branch information
pirj committed Jan 14, 2023
1 parent 5c4b6a4 commit eed7439
Show file tree
Hide file tree
Showing 20 changed files with 27 additions and 1,673 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Fix a false negative for `RSpec/Pending` when using skipped in metadata is multiline string. ([@ydah])
- Fix a false positive for `RSpec/NoExpectationExample` when using skipped in metadata is multiline string. ([@ydah])
- Fix a false positive for `RSpec/ContextMethod` when multi-line context with `#` at the beginning. ([@ydah])
- Extract Capybara cops to a separate repository. ([@pirj])

This comment has been minimized.

Copy link
@AlexWayfer

AlexWayfer Jan 16, 2023

Contributor

Not obvious from the changelog, are they enabled or not (maybe you should require this new gem/repo).


## 2.17.0 (2023-01-13)

Expand Down
9 changes: 9 additions & 0 deletions config/obsoletion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,12 @@ changed_parameters:
parameters: IgnoredPatterns
alternative: AllowedPatterns
severity: warning

renamed:
RSpec/Capybara/CurrentPathExpectation: Capybara/CurrentPathExpectation
RSpec/Capybara/MatchStyle: Capybara/MatchStyle
RSpec/Capybara/NegationMatcher: Capybara/NegationMatcher
RSpec/Capybara/SpecificActions: Capybara/SpecificActions
RSpec/Capybara/SpecificFinders: Capybara/SpecificFinders
RSpec/Capybara/SpecificMatcher: Capybara/SpecificMatcher
RSpec/Capybara/VisibilityMatcher: Capybara/VisibilityMatcher
3 changes: 1 addition & 2 deletions lib/rubocop-rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'yaml'

require 'rubocop'
require 'rubocop-capybara'

require_relative 'rubocop/rspec'
require_relative 'rubocop/rspec/inject'
Expand All @@ -17,8 +18,6 @@

require_relative 'rubocop/rspec/factory_bot/language'

require_relative 'rubocop/cop/rspec/mixin/capybara_help'
require_relative 'rubocop/cop/rspec/mixin/css_selector'
require_relative 'rubocop/cop/rspec/mixin/final_end_location'
require_relative 'rubocop/cop/rspec/mixin/inside_example_group'
require_relative 'rubocop/cop/rspec/mixin/metadata'
Expand Down
92 changes: 3 additions & 89 deletions lib/rubocop/cop/rspec/capybara/current_path_expectation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,95 +29,9 @@ module Capybara
# # good
# expect(page).to have_current_path('/callback')
#
class CurrentPathExpectation < ::RuboCop::Cop::Base
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`'

RESTRICT_ON_SEND = %i[expect].freeze

# @!method expectation_set_on_current_path(node)
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')
# @!method as_is_matcher(node)
def_node_matcher :as_is_matcher, <<-PATTERN
(send
#expectation_set_on_current_path ${:to :to_not :not_to}
${(send nil? :eq ...) (send nil? :match (regexp ...))})
PATTERN

# @!method regexp_str_matcher(node)
def_node_matcher :regexp_str_matcher, <<-PATTERN
(send
#expectation_set_on_current_path ${:to :to_not :not_to}
$(send nil? :match (str $_)))
PATTERN

def self.autocorrect_incompatible_with
[Style::TrailingCommaInArguments]
end

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
# @!parse class CurrentPathExpectation < ::RuboCop::Cop::Base; end
class CurrentPathExpectation <
::RuboCop::Cop::Capybara::CurrentPathExpectation
end
end
end
Expand Down
26 changes: 2 additions & 24 deletions lib/rubocop/cop/rspec/capybara/match_style.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,8 @@ module Capybara
# # good
# expect(page).to match_style(display: 'block')
#
class MatchStyle < Base
extend AutoCorrector

MSG = 'Use `%<good>s` instead of `%<bad>s`.'
RESTRICT_ON_SEND = %i[assert_style has_style? have_style].freeze
PREFERRED_METHOD = {
'assert_style' => 'assert_matches_style',
'has_style?' => 'matches_style?',
'have_style' => 'match_style'
}.freeze

def on_send(node)
method_node = node.loc.selector
add_offense(method_node) do |corrector|
corrector.replace(method_node,
PREFERRED_METHOD[method_node.source])
end
end

private

def message(node)
format(MSG, good: PREFERRED_METHOD[node.source], bad: node.source)
end
# @!parse class MatchStyle < ::RuboCop::Cop::Base; end
class MatchStyle < ::RuboCop::Cop::Capybara::MatchStyle
end
end
end
Expand Down
77 changes: 2 additions & 75 deletions lib/rubocop/cop/rspec/capybara/negation_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,81 +24,8 @@ module Capybara
# expect(page).to have_no_selector
# expect(page).to have_no_css('a')
#
class NegationMatcher < ::RuboCop::Cop::Base
extend AutoCorrector
include ConfigurableEnforcedStyle

MSG = 'Use `expect(...).%<runner>s %<matcher>s`.'
CAPYBARA_MATCHERS = %w[
selector css xpath text title current_path link button
field checked_field unchecked_field select table
sibling ancestor
].freeze
POSITIVE_MATCHERS =
Set.new(CAPYBARA_MATCHERS) { |element| :"have_#{element}" }.freeze
NEGATIVE_MATCHERS =
Set.new(CAPYBARA_MATCHERS) { |element| :"have_no_#{element}" }
.freeze
RESTRICT_ON_SEND = (POSITIVE_MATCHERS + NEGATIVE_MATCHERS).freeze

# @!method not_to?(node)
def_node_matcher :not_to?, <<~PATTERN
(send ... :not_to
(send nil? %POSITIVE_MATCHERS ...))
PATTERN

# @!method have_no?(node)
def_node_matcher :have_no?, <<~PATTERN
(send ... :to
(send nil? %NEGATIVE_MATCHERS ...))
PATTERN

def on_send(node)
return unless offense?(node.parent)

matcher = node.method_name.to_s
add_offense(offense_range(node),
message: message(matcher)) do |corrector|
corrector.replace(node.parent.loc.selector, replaced_runner)
corrector.replace(node.loc.selector,
replaced_matcher(matcher))
end
end

private

def offense?(node)
(style == :have_no && not_to?(node)) ||
(style == :not_to && have_no?(node))
end

def offense_range(node)
node.parent.loc.selector.with(end_pos: node.loc.selector.end_pos)
end

def message(matcher)
format(MSG,
runner: replaced_runner,
matcher: replaced_matcher(matcher))
end

def replaced_runner
case style
when :have_no
'to'
when :not_to
'not_to'
end
end

def replaced_matcher(matcher)
case style
when :have_no
matcher.sub('have_', 'have_no_')
when :not_to
matcher.sub('have_no_', 'have_')
end
end
# @!parse class NegationMatcher < ::RuboCop::Cop::Base; end
class NegationMatcher < ::RuboCop::Cop::Capybara::NegationMatcher
end
end
end
Expand Down
60 changes: 2 additions & 58 deletions lib/rubocop/cop/rspec/capybara/specific_actions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,64 +20,8 @@ module Capybara
# click_link(exact_text: 'foo')
# find('div').click_button
#
class SpecificActions < ::RuboCop::Cop::Base
MSG = "Prefer `%<good_action>s` over `find('%<selector>s').click`."
RESTRICT_ON_SEND = %i[click].freeze
SPECIFIC_ACTION = {
'button' => 'button',
'a' => 'link'
}.freeze

# @!method click_on_selector(node)
def_node_matcher :click_on_selector, <<-PATTERN
(send _ :find (str $_) ...)
PATTERN

def on_send(node)
click_on_selector(node.receiver) do |arg|
next unless supported_selector?(arg)
# Always check the last selector in the case of multiple selectors
# separated by whitespace.
# because the `.click` is executed on the element to
# which the last selector points.
next unless (selector = last_selector(arg))
next unless (action = specific_action(selector))
next unless CapybaraHelp.specific_option?(node.receiver, arg,
action)
next unless CapybaraHelp.specific_pseudo_classes?(arg)

range = offense_range(node, node.receiver)
add_offense(range, message: message(action, selector))
end
end

private

def specific_action(selector)
SPECIFIC_ACTION[last_selector(selector)]
end

def supported_selector?(selector)
!selector.match?(/[>,+~]/)
end

def last_selector(arg)
arg.split.last[/^\w+/, 0]
end

def offense_range(node, receiver)
receiver.loc.selector.with(end_pos: node.loc.expression.end_pos)
end

def message(action, selector)
format(MSG,
good_action: good_action(action),
selector: selector)
end

def good_action(action)
"click_#{action}"
end
# @!parse class SpecificActions < ::RuboCop::Cop::Base; end
class SpecificActions < ::RuboCop::Cop::Capybara::SpecificActions
end
end
end
Expand Down
Loading

0 comments on commit eed7439

Please sign in to comment.