Skip to content

Commit

Permalink
Add mixin/capybara_help and moved common processes
Browse files Browse the repository at this point in the history
Common process of checking if a method can be replaced by
a more specific method in capybara, which existed in
`Rspec/Capybara/SpecificActions` and `Rspec/Capybara/SpecificMatcher`,
respectively, is now common.
  • Loading branch information
ydah committed Oct 20, 2022
1 parent ed59a61 commit 1fbb19f
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 105 deletions.
1 change: 1 addition & 0 deletions lib/rubocop-rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
require_relative 'rubocop/cop/rspec/mixin/namespace'
require_relative 'rubocop/cop/rspec/mixin/css_selector'
require_relative 'rubocop/cop/rspec/mixin/skip_or_pending'
require_relative 'rubocop/cop/rspec/mixin/capybara_help'

require_relative 'rubocop/rspec/concept'
require_relative 'rubocop/rspec/example_group'
Expand Down
59 changes: 7 additions & 52 deletions lib/rubocop/cop/rspec/capybara/specific_actions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ class SpecificActions < Base
(send _ :find (str $_) ...)
PATTERN

# @!method option?(node)
def_node_search :option?, <<-PATTERN
(pair (sym %) _)
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 specific_action_option?(node.receiver, arg, action)
next unless specific_action_pseudo_classes?(arg)
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))
Expand All @@ -57,51 +57,6 @@ def specific_action(selector)
SPECIFIC_ACTION[last_selector(selector)]
end

def specific_action_option?(node, arg, action)
attrs = CssSelector.attributes(arg).keys
return false unless replaceable_action?(node, action, attrs)

attrs.all? do |attr|
CssSelector.specific_options?(action, attr)
end
end

def specific_action_pseudo_classes?(arg)
CssSelector.pseudo_classes(arg).all? do |pseudo_class|
replaceable_pseudo_class?(pseudo_class, arg)
end
end

def replaceable_pseudo_class?(pseudo_class, arg)
unless CssSelector.specific_pesudo_classes?(pseudo_class)
return false
end

case pseudo_class
when 'not()' then replaceable_pseudo_class_not?(arg)
else true
end
end

def replaceable_pseudo_class_not?(arg)
arg.scan(/not\(.*?\)/).all? do |not_arg|
CssSelector.attributes(not_arg).values.all? do |v|
v.is_a?(TrueClass) || v.is_a?(FalseClass)
end
end
end

def replaceable_action?(node, action, attrs)
case action
when 'link' then replaceable_to_click_link?(node, attrs)
else true
end
end

def replaceable_to_click_link?(node, attrs)
option?(node, :href) || attrs.include?('href')
end

def supported_selector?(selector)
!selector.match?(/[>,+~]/)
end
Expand Down
58 changes: 5 additions & 53 deletions lib/rubocop/cop/rspec/capybara/specific_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ module Capybara
# expect(page).to have_select
# expect(page).to have_field('foo')
#
class SpecificMatcher < Base # rubocop:disable Metrics/ClassLength
class SpecificMatcher < Base
include CapybaraHelp

MSG = 'Prefer `%<good_matcher>s` over `%<bad_matcher>s`.'
RESTRICT_ON_SEND = %i[have_selector have_no_selector have_css
have_no_css].freeze
Expand All @@ -43,17 +45,12 @@ class SpecificMatcher < Base # rubocop:disable Metrics/ClassLength
(send nil? _ (str $_) ... )
PATTERN

# @!method option?(node)
def_node_search :option?, <<-PATTERN
(pair (sym %) _)
PATTERN

def on_send(node)
first_argument(node) do |arg|
next unless (matcher = specific_matcher(arg))
next if CssSelector.multiple_selectors?(arg)
next unless specific_matcher_option?(node, arg, matcher)
next unless specific_matcher_pseudo_classes?(arg)
next unless specific_option?(node, arg, matcher)
next unless specific_pseudo_classes?(arg)

add_offense(node, message: message(node, matcher))
end
Expand All @@ -66,51 +63,6 @@ def specific_matcher(arg)
SPECIFIC_MATCHER[splitted_arg]
end

def specific_matcher_option?(node, arg, matcher)
attrs = CssSelector.attributes(arg).keys
return false unless replaceable_matcher?(node, matcher, attrs)

attrs.all? do |attr|
CssSelector.specific_options?(matcher, attr)
end
end

def specific_matcher_pseudo_classes?(arg)
CssSelector.pseudo_classes(arg).all? do |pseudo_class|
replaceable_pseudo_class?(pseudo_class, arg)
end
end

def replaceable_pseudo_class?(pseudo_class, arg)
unless CssSelector.specific_pesudo_classes?(pseudo_class)
return false
end

case pseudo_class
when 'not()' then replaceable_pseudo_class_not?(arg)
else true
end
end

def replaceable_pseudo_class_not?(arg)
arg.scan(/not\(.*?\)/).all? do |not_arg|
CssSelector.attributes(not_arg).values.all? do |v|
v.is_a?(TrueClass) || v.is_a?(FalseClass)
end
end
end

def replaceable_matcher?(node, matcher, attrs)
case matcher
when 'link' then replaceable_to_have_link?(node, attrs)
else true
end
end

def replaceable_to_have_link?(node, attrs)
option?(node, :href) || attrs.include?('href')
end

def message(node, matcher)
format(MSG,
good_matcher: good_matcher(node, matcher),
Expand Down
80 changes: 80 additions & 0 deletions lib/rubocop/cop/rspec/mixin/capybara_help.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# frozen_string_literal: true

module RuboCop
module Cop
module RSpec
# Help methods for capybara.
module CapybaraHelp
module_function

# @param node [RuboCop::AST::SendNode]
# @param locator [String]
# @param element [String]
# @return [Boolean]
def specific_option?(node, locator, element)
attrs = CssSelector.attributes(locator).keys
return false unless replaceable_element?(node, element, attrs)

attrs.all? do |attr|
CssSelector.specific_options?(element, attr)
end
end

# @param locator [String]
# @return [Boolean]
def specific_pseudo_classes?(locator)
CssSelector.pseudo_classes(locator).all? do |pseudo_class|
replaceable_pseudo_class?(pseudo_class, locator)
end
end

# @param pseudo_class [String]
# @param locator [String]
# @return [Boolean]
def replaceable_pseudo_class?(pseudo_class, locator)
return false unless CssSelector.specific_pesudo_classes?(pseudo_class)

case pseudo_class
when 'not()' then replaceable_pseudo_class_not?(locator)
else true
end
end

# @param locator [String]
# @return [Boolean]
def replaceable_pseudo_class_not?(locator)
locator.scan(/not\(.*?\)/).all? do |negation|
CssSelector.attributes(negation).values.all? do |v|
v.is_a?(TrueClass) || v.is_a?(FalseClass)
end
end
end

# @param node [RuboCop::AST::SendNode]
# @param element [String]
# @param attrs [Array<String>]
# @return [Boolean]
def replaceable_element?(node, element, attrs)
case element
when 'link' then replaceable_to_link?(node, attrs)
else true
end
end

# @param node [RuboCop::AST::SendNode]
# @param attrs [Array<String>]
# @return [Boolean]
def replaceable_to_link?(node, attrs)
include_option?(node, :href) || attrs.include?('href')
end

# @param node [RuboCop::AST::SendNode]
# @param option [Symbol]
# @return [Boolean]
def include_option?(node, option)
node.each_descendant(:sym).find { |opt| opt.value == option }
end
end
end
end
end

0 comments on commit 1fbb19f

Please sign in to comment.