diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f142cd..e060393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Drop Ruby 2.6 support. ([@ydah]) - Add new `Capybara/RSpec/PredicateMatcher` cop. ([@ydah]) - Fix a false positive for `Capybara/SpecificFinders` when `find` with kind option. ([@ydah]) +- Add new `Capybara/RSpec/HaveSelector` cop. ([@ydah]) ## 2.18.0 (2023-04-21) diff --git a/config/default.yml b/config/default.yml index 98edccb..fe63f85 100644 --- a/config/default.yml +++ b/config/default.yml @@ -61,6 +61,13 @@ Capybara/RSpec: Enabled: true Include: *1 +Capybara/RSpec/HaveSelector: + Description: Use `have_css` or `have_xpath` instead of `have_selector`. + Enabled: pending + DefaultSelector: bar + VersionAdded: "<>" + Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/HaveSelector + Capybara/RSpec/PredicateMatcher: Description: Prefer using predicate matcher over using predicate method directly. Enabled: pending diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index 66bc952..17675c4 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -12,6 +12,7 @@ === Department xref:cops_capybara_rspec.adoc[Capybara/RSpec] +* xref:cops_capybara_rspec.adoc#capybararspec/haveselector[Capybara/RSpec/HaveSelector] * xref:cops_capybara_rspec.adoc#capybararspec/predicatematcher[Capybara/RSpec/PredicateMatcher] // END_COP_LIST diff --git a/docs/modules/ROOT/pages/cops_capybara_rspec.adoc b/docs/modules/ROOT/pages/cops_capybara_rspec.adoc index 4b265b7..28414d3 100644 --- a/docs/modules/ROOT/pages/cops_capybara_rspec.adoc +++ b/docs/modules/ROOT/pages/cops_capybara_rspec.adoc @@ -1,5 +1,72 @@ = Capybara/RSpec +== Capybara/RSpec/HaveSelector + +|=== +| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed + +| Pending +| Yes +| Yes +| <> +| - +|=== + +Use `have_css` or `have_xpath` instead of `have_selector`. + +=== Examples + +[source,ruby] +---- +# bad +expect(foo).to have_selector(:css, 'bar') + +# good +expect(foo).to have_css('bar') + +# bad +expect(foo).to have_selector(:xpath, 'bar') + +# good +expect(foo).to have_xpath('bar') +---- + +==== DefaultSelector: css (default) + +[source,ruby] +---- +# bad +expect(foo).to have_selector('bar') + +# good +expect(foo).to have_css('bar') +---- + +==== DefaultSelector: xpath + +[source,ruby] +---- +# bad +expect(foo).to have_selector('bar') + +# good +expect(foo).to have_xpath('bar') +---- + +=== Configurable attributes + +|=== +| Name | Default value | Configurable values + +| DefaultSelector +| `bar` +| String +|=== + +=== References + +* https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/RSpec/HaveSelector + == Capybara/RSpec/PredicateMatcher |=== diff --git a/lib/rubocop/cop/capybara/rspec/have_selector.rb b/lib/rubocop/cop/capybara/rspec/have_selector.rb new file mode 100644 index 0000000..53a5c4b --- /dev/null +++ b/lib/rubocop/cop/capybara/rspec/have_selector.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Capybara + module RSpec + # Use `have_css` or `have_xpath` instead of `have_selector`. + # + # @example + # # bad + # expect(foo).to have_selector(:css, 'bar') + # + # # good + # expect(foo).to have_css('bar') + # + # # bad + # expect(foo).to have_selector(:xpath, 'bar') + # + # # good + # expect(foo).to have_xpath('bar') + # + # @example DefaultSelector: css (default) + # # bad + # expect(foo).to have_selector('bar') + # + # # good + # expect(foo).to have_css('bar') + # + # @example DefaultSelector: xpath + # # bad + # expect(foo).to have_selector('bar') + # + # # good + # expect(foo).to have_xpath('bar') + # + class HaveSelector < ::RuboCop::Cop::Base + extend AutoCorrector + include RangeHelp + + MSG = 'Use `%s` instead of `have_selector`.' + RESTRICT_ON_SEND = %i[have_selector].freeze + SELECTORS = %i[css xpath].freeze + + def on_send(node) + argument = node.first_argument + on_select_with_type(node, argument) if argument.sym_type? + on_select_without_type(node) if argument.str_type? + end + + private + + def on_select_with_type(node, type) + return unless SELECTORS.include?(type.value) + + add_offense(node, message: message_typed(type)) do |corrector| + corrector.remove(deletion_range(type, node.arguments[1])) + corrector.replace(node.loc.selector, "have_#{type.value}") + end + end + + def message_typed(type) + format(MSG, good: "have_#{type.value}") + end + + def deletion_range(first_argument, second_argument) + range_between(first_argument.source_range.begin_pos, + second_argument.source_range.begin_pos) + end + + def on_select_without_type(node) + add_offense(node, message: message_untyped) do |corrector| + corrector.replace(node.loc.selector, "have_#{default_selector}") + end + end + + def message_untyped + format(MSG, good: "have_#{default_selector}") + end + + def default_selector + cop_config.fetch('DefaultSelector', 'css') + end + end + end + end + end +end diff --git a/lib/rubocop/cop/capybara_cops.rb b/lib/rubocop/cop/capybara_cops.rb index 108b3b9..dc430b1 100644 --- a/lib/rubocop/cop/capybara_cops.rb +++ b/lib/rubocop/cop/capybara_cops.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative 'capybara/rspec/have_selector' require_relative 'capybara/rspec/predicate_matcher' require_relative 'capybara/current_path_expectation' diff --git a/spec/rubocop/cop/capybara/rspec/have_selector_spec.rb b/spec/rubocop/cop/capybara/rspec/have_selector_spec.rb new file mode 100644 index 0000000..4635738 --- /dev/null +++ b/spec/rubocop/cop/capybara/rspec/have_selector_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Capybara::RSpec::HaveSelector, :config do + let(:cop_config) do + { 'DefaultSelector' => default_selector } + end + let(:default_selector) { 'css' } + + it 'registers an offense when using `have_selector`' do + expect_offense(<<~RUBY) + expect(foo).to have_selector('bar') + ^^^^^^^^^^^^^^^^^^^^ Use `have_css` instead of `have_selector`. + RUBY + + expect_correction(<<~RUBY) + expect(foo).to have_css('bar') + RUBY + end + + it 'registers an offense when using `have_selector` with `:css`' do + expect_offense(<<~RUBY) + expect(foo).to have_selector(:css, 'bar') + ^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `have_css` instead of `have_selector`. + RUBY + + expect_correction(<<~RUBY) + expect(foo).to have_css('bar') + RUBY + end + + it 'registers an offense when using `have_selector` with `:xpath`' do + expect_offense(<<~RUBY) + expect(foo).to have_selector(:xpath, 'bar') + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `have_xpath` instead of `have_selector`. + RUBY + + expect_correction(<<~RUBY) + expect(foo).to have_xpath('bar') + RUBY + end + + it 'does not register an offense when using `have_css`' do + expect_no_offenses(<<~RUBY) + expect(foo).to have_css('bar') + RUBY + end + + it 'does not register an offense when using `have_selector` with other sym' do + expect_no_offenses(<<~RUBY) + expect(foo).to have_selector(:foo, 'bar') + RUBY + end + + context 'when DefaultSelector is xpath' do + let(:default_selector) { 'xpath' } + + it 'registers an offense when using `have_selector`' do + expect_offense(<<~RUBY) + expect(foo).to have_selector('bar') + ^^^^^^^^^^^^^^^^^^^^ Use `have_xpath` instead of `have_selector`. + RUBY + + expect_correction(<<~RUBY) + expect(foo).to have_xpath('bar') + RUBY + end + + it 'registers an offense when using `have_selector` with `:xpath`' do + expect_offense(<<~RUBY) + expect(foo).to have_selector(:xpath, 'bar') + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `have_xpath` instead of `have_selector`. + RUBY + + expect_correction(<<~RUBY) + expect(foo).to have_xpath('bar') + RUBY + end + + it 'registers an offense when using `have_selector` with `:css`' do + expect_offense(<<~RUBY) + expect(foo).to have_selector(:css, 'bar') + ^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `have_css` instead of `have_selector`. + RUBY + + expect_correction(<<~RUBY) + expect(foo).to have_css('bar') + RUBY + end + end +end