Skip to content

Commit

Permalink
Move the department associated with Capybara::RSpecMatchers to `Cap…
Browse files Browse the repository at this point in the history
…ybara/RSpec/*`
  • Loading branch information
ydah committed Mar 22, 2024
1 parent 6c8deaf commit cea510a
Show file tree
Hide file tree
Showing 16 changed files with 492 additions and 367 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Edge (Unreleased)

- **breaking** Move the department associated with `Capybara::RSpecMatchers` to `Capybara/RSpec/*`. ([@ydah])
- Fix a false negative for `Capybara/NegationMatcher` when using `to_not`. ([@ydah])

## 2.20.0 (2024-01-03)
Expand Down
35 changes: 35 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Capybara/CurrentPathExpectation:
VersionAdded: '1.18'
VersionChanged: '2.0'
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/CurrentPathExpectation
VersionRemoved: '3.0'

Capybara/MatchStyle:
Description: Checks for usage of deprecated style methods.
Expand All @@ -43,6 +44,7 @@ Capybara/NegationMatcher:
- have_no
- not_to
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/NegationMatcher
VersionRemoved: '3.0'

Capybara/RedundantWithinFind:
Description: Checks for redundant `within find(...)` calls.
Expand All @@ -67,25 +69,45 @@ Capybara/SpecificMatcher:
Enabled: pending
VersionAdded: '2.12'
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/SpecificMatcher
VersionRemoved: '3.0'

Capybara/VisibilityMatcher:
Description: Checks for boolean visibility in Capybara finders.
Enabled: true
VersionAdded: '1.39'
VersionChanged: '2.0'
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/VisibilityMatcher
VersionRemoved: '3.0'

Capybara/RSpec:
Enabled: true
Include: *1

Capybara/RSpec/CurrentPathExpectation:
Description: Checks that no expectations are set on Capybara's `current_path`.
Enabled: true
VersionAdded: '1.18'
VersionChanged: '2.0'
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/CurrentPathExpectation

Capybara/RSpec/HaveSelector:
Description: Use `have_css` or `have_xpath` instead of `have_selector`.
Enabled: pending
DefaultSelector: css
VersionAdded: '2.19'
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/HaveSelector

Capybara/RSpec/NegationMatcher:
Description: Enforces use of `have_no_*` or `not_to` for negated expectations.
Enabled: pending
VersionAdded: '2.14'
VersionChanged: '2.20'
EnforcedStyle: have_no
SupportedStyles:
- have_no
- not_to
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/NegationMatcher

Capybara/RSpec/PredicateMatcher:
Description: Prefer using predicate matcher over using predicate method directly.
Enabled: pending
Expand All @@ -97,3 +119,16 @@ Capybara/RSpec/PredicateMatcher:
- explicit
VersionAdded: '2.19'
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/PredicateMatcher

Capybara/RSpec/SpecificMatcher:
Description: Checks for there is a more specific matcher offered by Capybara.
Enabled: pending
VersionAdded: '2.12'
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/SpecificMatcher

Capybara/RSpec/VisibilityMatcher:
Description: Checks for boolean visibility in Capybara finders.
Enabled: true
VersionAdded: '1.39'
VersionChanged: '2.0'
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/VisibilityMatcher
1 change: 1 addition & 0 deletions lib/rubocop-capybara.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

require 'rubocop'

require_relative 'rubocop/capybara/'
require_relative 'rubocop/cop/capybara/mixin/capybara_help'
require_relative 'rubocop/cop/capybara/mixin/css_attributes_parser'
require_relative 'rubocop/cop/capybara/mixin/css_selector'
Expand Down
136 changes: 7 additions & 129 deletions lib/rubocop/cop/capybara/current_path_expectation.rb
Original file line number Diff line number Diff line change
@@ -1,140 +1,18 @@
# frozen_string_literal: true

require_relative './rspec/current_path_expectation'

module RuboCop
module Cop
module Capybara
# Checks that no expectations are set on Capybara's `current_path`.
#
# The
# https://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/RSpecMatchers#have_current_path-instance_method[`have_current_path` matcher]
# should be used on `page` to set expectations on Capybara's
# current path, since it uses
# https://github.com/teamcapybara/capybara/blob/master/README.md#asynchronous-javascript-ajax-and-friends[Capybara's waiting functionality]
# which ensures that preceding actions (like `click_link`) have
# completed.
#
# This cop does not support autocorrection in some cases.
#
# @example
# # bad
# expect(current_path).to eq('/callback')
# expect(page.current_path).to eq('/callback')
# IMPORTANT: This cop is deprecated and will be removed in
# RuboCop Capybara 3.0.
# Please use `Capybara/RSpec/CurrentPathExpectation` instead.
#
# # good
# expect(page).to have_current_path('/callback', ignore_query: true)
#
# # bad (does not support autocorrection when `match` with a variable)
# expect(page).to match(variable)
#
class CurrentPathExpectation < ::RuboCop::Cop::Base
extend AutoCorrector
include RangeHelp

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_node_matcher(node)
def_node_matcher :regexp_node_matcher, <<~PATTERN
(send
#expectation_set_on_current_path ${:to :to_not :not_to}
$(send nil? :match ${str dstr xstr}))
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_node_matcher(node.parent) do |to_sym, matcher_node, regexp|
rewrite_expectation(corrector, node, to_sym, matcher_node)
convert_regexp_node_to_literal(corrector, matcher_node, regexp)
end
end

def rewrite_expectation(corrector, node, to_symbol, matcher_node)
corrector.replace(node.first_argument, '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_argument_parentheses(corrector, matcher_node.first_argument)
add_ignore_query_options(corrector, node, matcher_node)
end

def convert_regexp_node_to_literal(corrector, matcher_node, regexp_node)
str_node = matcher_node.first_argument
regexp_expr = regexp_node_to_regexp_expr(regexp_node)
corrector.replace(str_node, regexp_expr)
end

def regexp_node_to_regexp_expr(regexp_node)
if regexp_node.xstr_type?
"/\#{`#{regexp_node.value.value}`}/"
else
Regexp.new(regexp_node.value).inspect
end
end

def add_argument_parentheses(corrector, arg_node)
return unless method_call_with_no_parentheses?(arg_node)

first_argument_range = range_with_surrounding_space(
arg_node.first_argument.source_range, side: :left
)
corrector.insert_before(first_argument_range, '(')
corrector.insert_after(arg_node.last_argument, ')')
end

def method_call_with_no_parentheses?(arg_node)
arg_node.send_type? && arg_node.arguments? && !arg_node.parenthesized?
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 `match` matcher.
def add_ignore_query_options(corrector, node, matcher_node)
return if matcher_node.method?(:match)

expectation_node = node.parent.last_argument
expectation_last_child = expectation_node.children.last
corrector.insert_after(expectation_last_child, ', ignore_query: true')
end
class CurrentPathExpectation <
RuboCop::Cop::Capybara::RSpec::CurrentPathExpectation
end
end
end
Expand Down
99 changes: 6 additions & 93 deletions lib/rubocop/cop/capybara/negation_matcher.rb
Original file line number Diff line number Diff line change
@@ -1,104 +1,17 @@
# frozen_string_literal: true

require_relative './rspec/negation_matcher'

module RuboCop
module Cop
module Capybara
# Enforces use of `have_no_*` or `not_to` for negated expectations.
#
# @example EnforcedStyle: have_no (default)
# # bad
# expect(page).not_to have_selector 'a'
# expect(page).not_to have_css('a')
#
# # good
# expect(page).to have_no_selector 'a'
# expect(page).to have_no_css('a')
#
# @example EnforcedStyle: not_to
# # bad
# expect(page).to have_no_selector 'a'
# expect(page).to have_no_css('a')
#
# # good
# expect(page).not_to have_selector 'a'
# expect(page).not_to have_css('a')
# IMPORTANT: This cop is deprecated and will be removed in
# RuboCop Capybara 3.0.
# Please use `Capybara/RSpec/NegationMatcher` instead.
#
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 content
].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 :to_not}
(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)

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)
node.arguments? &&
((style == :have_no && not_to?(node.parent)) ||
(style == :not_to && have_no?(node.parent)))
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
class NegationMatcher < RuboCop::Cop::Capybara::RSpec::NegationMatcher
end
end
end
Expand Down
Loading

0 comments on commit cea510a

Please sign in to comment.