Skip to content

Commit

Permalink
Add new RSpec/Capybara/NegationMatcher cop
Browse files Browse the repository at this point in the history
Resolve: #378
  • Loading branch information
ydah authored and pirj committed Oct 15, 2022
1 parent d16fb33 commit fcbc84d
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ RSpec/SubjectDeclaration:
Enabled: true
RSpec/VerifiedDoubleReference:
Enabled: true
RSpec/Capybara/NegationMatcher:
Enabled: true
RSpec/Capybara/SpecificFinders:
Enabled: true
RSpec/Capybara/SpecificMatcher:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Add `require_implicit` style to `RSpec/ImplicitSubject`. ([@r7kamura][])
* Fix a false positive for `RSpec/Capybara/SpecificMatcher` when `have_css("a")` without attribute. ([@ydah][])
* Update `RSpec/ExampleWording` cop to raise error for insufficient descriptions. ([@akrox58][])
* Add new `RSpec/Capybara/NegationMatcher` cop. ([@ydah][])

## 2.13.2 (2022-09-23)

Expand Down
10 changes: 10 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,16 @@ RSpec/Capybara/FeatureMethods:
VersionChanged: '2.0'
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Capybara/FeatureMethods

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

RSpec/Capybara/SpecificFinders:
Description: Checks if there is a more specific finder offered by Capybara.
Enabled: pending
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@

* xref:cops_rspec_capybara.adoc#rspeccapybara/currentpathexpectation[RSpec/Capybara/CurrentPathExpectation]
* xref:cops_rspec_capybara.adoc#rspeccapybara/featuremethods[RSpec/Capybara/FeatureMethods]
* xref:cops_rspec_capybara.adoc#rspeccapybara/negationmatcher[RSpec/Capybara/NegationMatcher]
* xref:cops_rspec_capybara.adoc#rspeccapybara/specificfinders[RSpec/Capybara/SpecificFinders]
* xref:cops_rspec_capybara.adoc#rspeccapybara/specificmatcher[RSpec/Capybara/SpecificMatcher]
* xref:cops_rspec_capybara.adoc#rspeccapybara/visibilitymatcher[RSpec/Capybara/VisibilityMatcher]
Expand Down
56 changes: 56 additions & 0 deletions docs/modules/ROOT/pages/cops_rspec_capybara.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,62 @@ end

* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Capybara/FeatureMethods

== RSpec/Capybara/NegationMatcher

|===
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed

| Pending
| Yes
| Yes
| 2.14
| -
|===

Enforces use of `have_no_*` or `not_to` for negated expectations.

=== Examples

==== EnforcedStyle: not_to (default)

[source,ruby]
----
# bad
expect(page).to have_no_selector
expect(page).to have_no_css('a')
# good
expect(page).not_to have_selector
expect(page).not_to have_css('a')
----

==== EnforcedStyle: have_no

[source,ruby]
----
# bad
expect(page).not_to have_selector
expect(page).not_to have_css('a')
# good
expect(page).to have_no_selector
expect(page).to have_no_css('a')
----

=== Configurable attributes

|===
| Name | Default value | Configurable values

| EnforcedStyle
| `not_to`
| `have_no`, `not_to`
|===

=== References

* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Capybara/NegationMatcher

== RSpec/Capybara/SpecificFinders

|===
Expand Down
106 changes: 106 additions & 0 deletions lib/rubocop/cop/rspec/capybara/negation_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# frozen_string_literal: true

module RuboCop
module Cop
module RSpec
module Capybara
# Enforces use of `have_no_*` or `not_to` for negated expectations.
#
# @example EnforcedStyle: not_to (default)
# # bad
# expect(page).to have_no_selector
# expect(page).to have_no_css('a')
#
# # good
# expect(page).not_to have_selector
# expect(page).not_to have_css('a')
#
# @example EnforcedStyle: have_no
# # bad
# expect(page).not_to have_selector
# expect(page).not_to have_css('a')
#
# # good
# expect(page).to have_no_selector
# expect(page).to have_no_css('a')
#
class NegationMatcher < 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
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/rspec_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative 'rspec/capybara/current_path_expectation'
require_relative 'rspec/capybara/feature_methods'
require_relative 'rspec/capybara/negation_matcher'
require_relative 'rspec/capybara/specific_finders'
require_relative 'rspec/capybara/specific_matcher'
require_relative 'rspec/capybara/visibility_matcher'
Expand Down
97 changes: 97 additions & 0 deletions spec/rubocop/cop/rspec/capybara/negation_matcher_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::RSpec::Capybara::NegationMatcher, :config do
let(:cop_config) { { 'EnforcedStyle' => enforced_style } }

context 'with EnforcedStyle `have_no`' do
let(:enforced_style) { 'have_no' }

%i[selector css xpath text title current_path link button
field checked_field unchecked_field select table
sibling ancestor].each do |matcher|
it 'registers an offense when using ' \
'`expect(...).not_to have_#{matcher}`' do
expect_offense(<<~RUBY, matcher: matcher)
expect(page).not_to have_#{matcher}
^^^^^^^^^^^^^{matcher} Use `expect(...).to have_no_#{matcher}`.
expect(page).not_to have_#{matcher}('a')
^^^^^^^^^^^^^{matcher} Use `expect(...).to have_no_#{matcher}`.
RUBY

expect_correction(<<~RUBY)
expect(page).to have_no_#{matcher}
expect(page).to have_no_#{matcher}('a')
RUBY
end

it 'does not register an offense when using ' \
'`expect(...).to have_no_#{matcher}`' do
expect_no_offenses(<<~RUBY)
expect(page).to have_no_#{matcher}
RUBY
end
end

it 'registers an offense when using ' \
'`expect(...).not_to have_text` with heredoc' do
expect_offense(<<~RUBY)
expect(page).not_to have_text(exact_text: <<~TEXT)
^^^^^^^^^^^^^^^^ Use `expect(...).to have_no_text`.
foo
TEXT
RUBY

expect_correction(<<~RUBY)
expect(page).to have_no_text(exact_text: <<~TEXT)
foo
TEXT
RUBY
end
end

context 'with EnforcedStyle `not_to`' do
let(:enforced_style) { 'not_to' }

%i[selector css xpath text title current_path link button
field checked_field unchecked_field select table
sibling ancestor].each do |matcher|
it 'registers an offense when using ' \
'`expect(...).to have_no_#{matcher}`' do
expect_offense(<<~RUBY, matcher: matcher)
expect(page).to have_no_#{matcher}
^^^^^^^^^^^^{matcher} Use `expect(...).not_to have_#{matcher}`.
expect(page).to have_no_#{matcher}('a')
^^^^^^^^^^^^{matcher} Use `expect(...).not_to have_#{matcher}`.
RUBY

expect_correction(<<~RUBY)
expect(page).not_to have_#{matcher}
expect(page).not_to have_#{matcher}('a')
RUBY
end

it 'does not register an offense when using ' \
'`expect(...).not_to have_#{matcher}`' do
expect_no_offenses(<<~RUBY)
expect(page).not_to have_#{matcher}
RUBY
end

it 'registers an offense when using ' \
'`expect(...).to have_no_text` with heredoc' do
expect_offense(<<~RUBY)
expect(page).to have_no_text(exact_text: <<~TEXT)
^^^^^^^^^^^^^^^ Use `expect(...).not_to have_text`.
foo
TEXT
RUBY

expect_correction(<<~RUBY)
expect(page).not_to have_text(exact_text: <<~TEXT)
foo
TEXT
RUBY
end
end
end
end

0 comments on commit fcbc84d

Please sign in to comment.