brew vendor-gems: commit updates.

This commit is contained in:
BrewTestBot 2023-01-16 18:06:57 +00:00
parent b63eb43b84
commit 76822805f2
No known key found for this signature in database
GPG Key ID: 82D7D104050B0F0F
167 changed files with 1316 additions and 874 deletions

View File

@ -105,9 +105,10 @@ $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/ruby-progressbar-1.11.0/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/unicode-display_width-2.4.2/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-1.43.0/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-capybara-2.17.0/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-performance-1.15.2/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-rails-2.17.4/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-rspec-2.17.0/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-rspec-2.18.0/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-sorbet-0.6.11/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/ruby-macho-3.0.0/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/simplecov-html-0.12.3/lib")

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'pathname'
require 'yaml'
require 'rubocop'
require_relative 'rubocop/cop/capybara/mixin/capybara_help'
require_relative 'rubocop/cop/capybara/mixin/css_selector'
require_relative 'rubocop/cop/capybara_cops'
project_root = File.join(__dir__, '..')
RuboCop::ConfigLoader.inject_defaults!(project_root)
obsoletion = File.join(project_root, 'config', 'obsoletion.yml')
RuboCop::ConfigObsoletion.files << obsoletion if File.exist?(obsoletion)
RuboCop::Cop::Style::TrailingCommaInArguments.singleton_class.prepend(
Module.new do
def autocorrect_incompatible_with
super.push(RuboCop::Cop::Capybara::CurrentPathExpectation)
end
end
)

View File

@ -3,13 +3,13 @@
require 'yaml'
module RuboCop
module RSpec
module Capybara
# Builds a YAML config file from two config hashes
class ConfigFormatter
EXTENSION_ROOT_DEPARTMENT = %r{^(RSpec/)}.freeze
SUBDEPARTMENTS = %(RSpec/Capybara RSpec/FactoryBot RSpec/Rails)
AMENDMENTS = %(Metrics/BlockLength)
COP_DOC_BASE_URL = 'https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/'
EXTENSION_ROOT_DEPARTMENT = %r{^(Capybara/)}.freeze
SUBDEPARTMENTS = [].freeze
AMENDMENTS = [].freeze
COP_DOC_BASE_URL = 'https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/'
def initialize(config, descriptions)
@config = config
@ -19,9 +19,9 @@ module RuboCop
def dump
YAML.dump(unified_config)
.gsub(EXTENSION_ROOT_DEPARTMENT, "\n\\1")
.gsub(*AMENDMENTS, "\n\\0")
.gsub(/^(\s+)- /, '\1 - ')
.gsub('"~"', '~')
# .gsub(*AMENDMENTS, "\n\\0")
end
private

View File

@ -0,0 +1,70 @@
# frozen_string_literal: true
module RuboCop
module Capybara
# Extracts cop descriptions from YARD docstrings
class DescriptionExtractor
def initialize(yardocs)
@code_objects = yardocs.map(&CodeObject.public_method(:new))
end
def to_h
code_objects
.select(&:cop?)
.map(&:configuration)
.reduce(:merge)
end
private
attr_reader :code_objects
# Decorator of a YARD code object for working with documented cops
class CodeObject
RUBOCOP_COP_CLASS_NAME = 'RuboCop::Cop::Base'
def initialize(yardoc)
@yardoc = yardoc
end
# Test if the YARD code object documents a concrete cop class
#
# @return [Boolean]
def cop?
cop_subclass? && !abstract?
end
# Configuration for the documented cop that would live in default.yml
#
# @return [Hash]
def configuration
{ cop_name => { 'Description' => description } }
end
private
def cop_name
Object.const_get(documented_constant).cop_name
end
def description
yardoc.docstring.split("\n\n").first.to_s
end
def documented_constant
yardoc.to_s
end
def cop_subclass?
yardoc.superclass.path == RUBOCOP_COP_CLASS_NAME
end
def abstract?
yardoc.tags.any? { |tag| tag.tag_name.eql?('abstract') }
end
attr_reader :yardoc
end
end
end
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
module RuboCop
module Capybara
# Version information for the Capybara RuboCop plugin.
module Version
STRING = '2.17.0'
end
end
end

View File

@ -0,0 +1,123 @@
# frozen_string_literal: true
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/main/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/main/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')
#
# # good
# expect(page).to have_current_path('/callback')
#
# # bad (does not support autocorrection)
# expect(page.current_path).to match(variable)
#
# # 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
end
end
end
end

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Capybara
# Checks for usage of deprecated style methods.
#
# @example when using `assert_style`
# # bad
# page.find(:css, '#first').assert_style(display: 'block')
#
# # good
# page.find(:css, '#first').assert_matches_style(display: 'block')
#
# @example when using `has_style?`
# # bad
# expect(page.find(:css, 'first')
# .has_style?(display: 'block')).to be true
#
# # good
# expect(page.find(:css, 'first')
# .matches_style?(display: 'block')).to be true
#
# @example when using `have_style`
# # bad
# expect(page).to have_style(display: 'block')
#
# # good
# expect(page).to match_style(display: 'block')
#
class MatchStyle < ::RuboCop::Cop::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
end
end
end
end

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
module RuboCop
module Cop
# 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

View File

@ -0,0 +1,144 @@
# frozen_string_literal: true
module RuboCop
module Cop
# Helps parsing css selector.
module CssSelector
COMMON_OPTIONS = %w[
above below left_of right_of near count minimum maximum between text
id class style visible obscured exact exact_text normalize_ws match
wait filter_set focused
].freeze
SPECIFIC_OPTIONS = {
'button' => (
COMMON_OPTIONS + %w[disabled name value title type]
).freeze,
'link' => (
COMMON_OPTIONS + %w[href alt title download]
).freeze,
'table' => (
COMMON_OPTIONS + %w[
caption with_cols cols with_rows rows
]
).freeze,
'select' => (
COMMON_OPTIONS + %w[
disabled name placeholder options enabled_options
disabled_options selected with_selected multiple with_options
]
).freeze,
'field' => (
COMMON_OPTIONS + %w[
checked unchecked disabled valid name placeholder
validation_message readonly with type multiple
]
).freeze
}.freeze
SPECIFIC_PSEUDO_CLASSES = %w[
not() disabled enabled checked unchecked
].freeze
module_function
# @param element [String]
# @param attribute [String]
# @return [Boolean]
# @example
# specific_pesudo_classes?('button', 'name') # => true
# specific_pesudo_classes?('link', 'invalid') # => false
def specific_options?(element, attribute)
SPECIFIC_OPTIONS.fetch(element, []).include?(attribute)
end
# @param pseudo_class [String]
# @return [Boolean]
# @example
# specific_pesudo_classes?('disabled') # => true
# specific_pesudo_classes?('first-of-type') # => false
def specific_pesudo_classes?(pseudo_class)
SPECIFIC_PSEUDO_CLASSES.include?(pseudo_class)
end
# @param selector [String]
# @return [Boolean]
# @example
# id?('#some-id') # => true
# id?('.some-class') # => false
def id?(selector)
selector.start_with?('#')
end
# @param selector [String]
# @return [Boolean]
# @example
# attribute?('[attribute]') # => true
# attribute?('attribute') # => false
def attribute?(selector)
selector.start_with?('[')
end
# @param selector [String]
# @return [Array<String>]
# @example
# attributes('a[foo-bar_baz]') # => {"foo-bar_baz=>true}
# attributes('button[foo][bar]') # => {"foo"=>true, "bar"=>true}
# attributes('table[foo=bar]') # => {"foo"=>"'bar'"}
def attributes(selector)
selector.scan(/\[(.*?)\]/).flatten.to_h do |attr|
key, value = attr.split('=')
[key, normalize_value(value)]
end
end
# @param selector [String]
# @return [Boolean]
# @example
# common_attributes?('a[focused]') # => true
# common_attributes?('button[focused][visible]') # => true
# common_attributes?('table[id=some-id]') # => true
# common_attributes?('h1[invalid]') # => false
def common_attributes?(selector)
attributes(selector).keys.difference(COMMON_OPTIONS).none?
end
# @param selector [String]
# @return [Array<String>]
# @example
# pseudo_classes('button:not([disabled])') # => ['not()']
# pseudo_classes('a:enabled:not([valid])') # => ['enabled', 'not()']
def pseudo_classes(selector)
# Attributes must be excluded or else the colon in the `href`s URL
# will also be picked up as pseudo classes.
# "a:not([href='http://example.com']):enabled" => "a:not():enabled"
ignored_attribute = selector.gsub(/\[.*?\]/, '')
# "a:not():enabled" => ["not()", "enabled"]
ignored_attribute.scan(/:([^:]*)/).flatten
end
# @param selector [String]
# @return [Boolean]
# @example
# multiple_selectors?('a.cls b#id') # => true
# multiple_selectors?('a.cls') # => false
def multiple_selectors?(selector)
selector.match?(/[ >,+~]/)
end
# @param value [String]
# @return [Boolean, String]
# @example
# normalize_value('true') # => true
# normalize_value('false') # => false
# normalize_value(nil) # => false
# normalize_value("foo") # => "'foo'"
def normalize_value(value)
case value
when 'true' then true
when 'false' then false
when nil then true
else "'#{value}'"
end
end
end
end
end

View File

@ -0,0 +1,104 @@
# frozen_string_literal: true
module RuboCop
module Cop
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 < ::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
end
end
end
end

View File

@ -0,0 +1,83 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Capybara
# Checks for there is a more specific actions offered by Capybara.
#
# @example
#
# # bad
# find('a').click
# find('button.cls').click
# find('a', exact_text: 'foo').click
# find('div button').click
#
# # good
# click_link
# click_button(class: 'cls')
# 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
end
end
end
end

View File

@ -0,0 +1,91 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Capybara
# Checks if there is a more specific finder offered by Capybara.
#
# @example
# # bad
# find('#some-id')
# find('[visible][id=some-id]')
#
# # good
# find_by_id('some-id')
# find_by_id('some-id', visible: true)
#
class SpecificFinders < ::RuboCop::Cop::Base
extend AutoCorrector
include RangeHelp
MSG = 'Prefer `find_by` over `find`.'
RESTRICT_ON_SEND = %i[find].freeze
# @!method find_argument(node)
def_node_matcher :find_argument, <<~PATTERN
(send _ :find (str $_) ...)
PATTERN
def on_send(node)
find_argument(node) do |arg|
next if CssSelector.multiple_selectors?(arg)
on_attr(node, arg) if attribute?(arg)
on_id(node, arg) if CssSelector.id?(arg)
end
end
private
def on_attr(node, arg)
return unless (id = CssSelector.attributes(arg)['id'])
register_offense(node, replaced_arguments(arg, id))
end
def on_id(node, arg)
register_offense(node, "'#{arg.to_s.delete('#')}'")
end
def attribute?(arg)
CssSelector.attribute?(arg) &&
CssSelector.common_attributes?(arg)
end
def register_offense(node, arg_replacement)
add_offense(offense_range(node)) do |corrector|
corrector.replace(node.loc.selector, 'find_by_id')
corrector.replace(node.first_argument.loc.expression,
arg_replacement)
end
end
def replaced_arguments(arg, id)
options = to_options(CssSelector.attributes(arg))
options.empty? ? id : "#{id}, #{options}"
end
def to_options(attrs)
attrs.each.map do |key, value|
next if key == 'id'
"#{key}: #{value}"
end.compact.join(', ')
end
def offense_range(node)
range_between(node.loc.selector.begin_pos, end_pos(node))
end
def end_pos(node)
if node.loc.end
node.loc.end.end_pos
else
node.loc.expression.end_pos
end
end
end
end
end
end

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Capybara
# Checks for there is a more specific matcher offered by Capybara.
#
# @example
#
# # bad
# expect(page).to have_selector('button')
# expect(page).to have_no_selector('button.cls')
# expect(page).to have_css('button')
# expect(page).to have_no_css('a.cls', href: 'http://example.com')
# expect(page).to have_css('table.cls')
# expect(page).to have_css('select')
# expect(page).to have_css('input', exact_text: 'foo')
#
# # good
# expect(page).to have_button
# expect(page).to have_no_button(class: 'cls')
# expect(page).to have_button
# expect(page).to have_no_link('foo', class: 'cls', href: 'http://example.com')
# expect(page).to have_table(class: 'cls')
# expect(page).to have_select
# expect(page).to have_field('foo')
#
class SpecificMatcher < ::RuboCop::Cop::Base
MSG = 'Prefer `%<good_matcher>s` over `%<bad_matcher>s`.'
RESTRICT_ON_SEND = %i[have_selector have_no_selector have_css
have_no_css].freeze
SPECIFIC_MATCHER = {
'button' => 'button',
'a' => 'link',
'table' => 'table',
'select' => 'select',
'input' => 'field'
}.freeze
# @!method first_argument(node)
def_node_matcher :first_argument, <<-PATTERN
(send nil? _ (str $_) ... )
PATTERN
def on_send(node)
first_argument(node) do |arg|
next unless (matcher = specific_matcher(arg))
next if CssSelector.multiple_selectors?(arg)
next unless CapybaraHelp.specific_option?(node, arg, matcher)
next unless CapybaraHelp.specific_pseudo_classes?(arg)
add_offense(node, message: message(node, matcher))
end
end
private
def specific_matcher(arg)
splitted_arg = arg[/^\w+/, 0]
SPECIFIC_MATCHER[splitted_arg]
end
def message(node, matcher)
format(MSG,
good_matcher: good_matcher(node, matcher),
bad_matcher: node.method_name)
end
def good_matcher(node, matcher)
node.method_name
.to_s
.gsub(/selector|css/, matcher.to_s)
end
end
end
end
end

View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Capybara
# Checks for boolean visibility in Capybara finders.
#
# Capybara lets you find elements that match a certain visibility using
# the `:visible` option. `:visible` accepts both boolean and symbols as
# values, however using booleans can have unwanted effects. `visible:
# false` does not find just invisible elements, but both visible and
# invisible elements. For expressiveness and clarity, use one of the
# symbol values, `:all`, `:hidden` or `:visible`.
# Read more in
# https://www.rubydoc.info/gems/capybara/Capybara%2FNode%2FFinders:all[the documentation].
#
# @example
# # bad
# expect(page).to have_selector('.foo', visible: false)
# expect(page).to have_css('.foo', visible: true)
# expect(page).to have_link('my link', visible: false)
#
# # good
# expect(page).to have_selector('.foo', visible: :visible)
# expect(page).to have_css('.foo', visible: :all)
# expect(page).to have_link('my link', visible: :hidden)
#
class VisibilityMatcher < ::RuboCop::Cop::Base
MSG_FALSE = 'Use `:all` or `:hidden` instead of `false`.'
MSG_TRUE = 'Use `:visible` instead of `true`.'
CAPYBARA_MATCHER_METHODS = %w[
button
checked_field
css
field
link
select
selector
table
unchecked_field
xpath
].flat_map do |element|
["have_#{element}".to_sym, "have_no_#{element}".to_sym]
end
RESTRICT_ON_SEND = CAPYBARA_MATCHER_METHODS
# @!method visible_true?(node)
def_node_matcher :visible_true?, <<~PATTERN
(send nil? #capybara_matcher? ... (hash <$(pair (sym :visible) true) ...>))
PATTERN
# @!method visible_false?(node)
def_node_matcher :visible_false?, <<~PATTERN
(send nil? #capybara_matcher? ... (hash <$(pair (sym :visible) false) ...>))
PATTERN
def on_send(node)
visible_false?(node) { |arg| add_offense(arg, message: MSG_FALSE) }
visible_true?(node) { |arg| add_offense(arg, message: MSG_TRUE) }
end
private
def capybara_matcher?(method_name)
CAPYBARA_MATCHER_METHODS.include? method_name
end
end
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
require_relative 'capybara/current_path_expectation'
require_relative 'capybara/match_style'
require_relative 'capybara/negation_matcher'
require_relative 'capybara/specific_actions'
require_relative 'capybara/specific_finders'
require_relative 'capybara/specific_matcher'
require_relative 'capybara/visibility_matcher'

View File

@ -1,14 +0,0 @@
#
# Configuration of obsolete/deprecated cops used by `ConfigObsoletion`.
#
# See: https://docs.rubocop.org/rubocop/extensions.html#config-obsoletions
#
# Cop parameters that have been changed
# Can be treated as a warning instead of a failure with `severity: warning`
changed_parameters:
- cops:
- RSpec/VariableName
parameters: IgnoredPatterns
alternative: AllowedPatterns
severity: warning

View File

@ -1,125 +0,0 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
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')
#
# # good
# expect(page).to have_current_path('/callback')
#
# # bad (does not support autocorrection)
# expect(page.current_path).to match(variable)
#
# # 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
end
end
end
end
end

View File

@ -1,60 +0,0 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# Checks for usage of deprecated style methods.
#
# @example when using `assert_style`
# # bad
# page.find(:css, '#first').assert_style(display: 'block')
#
# # good
# page.find(:css, '#first').assert_matches_style(display: 'block')
#
# @example when using `has_style?`
# # bad
# expect(page.find(:css, 'first')
# .has_style?(display: 'block')).to be true
#
# # good
# expect(page.find(:css, 'first')
# .matches_style?(display: 'block')).to be true
#
# @example when using `have_style`
# # bad
# expect(page).to have_style(display: 'block')
#
# # 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
end
end
end
end
end

View File

@ -1,106 +0,0 @@
# 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 < ::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
end
end
end
end
end

View File

@ -1,85 +0,0 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# Checks for there is a more specific actions offered by Capybara.
#
# @example
#
# # bad
# find('a').click
# find('button.cls').click
# find('a', exact_text: 'foo').click
# find('div button').click
#
# # good
# click_link
# click_button(class: 'cls')
# 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
end
end
end
end
end

View File

@ -1,93 +0,0 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# Checks if there is a more specific finder offered by Capybara.
#
# @example
# # bad
# find('#some-id')
# find('[visible][id=some-id]')
#
# # good
# find_by_id('some-id')
# find_by_id('some-id', visible: true)
#
class SpecificFinders < ::RuboCop::Cop::Base
extend AutoCorrector
include RangeHelp
MSG = 'Prefer `find_by` over `find`.'
RESTRICT_ON_SEND = %i[find].freeze
# @!method find_argument(node)
def_node_matcher :find_argument, <<~PATTERN
(send _ :find (str $_) ...)
PATTERN
def on_send(node)
find_argument(node) do |arg|
next if CssSelector.multiple_selectors?(arg)
on_attr(node, arg) if attribute?(arg)
on_id(node, arg) if CssSelector.id?(arg)
end
end
private
def on_attr(node, arg)
return unless (id = CssSelector.attributes(arg)['id'])
register_offense(node, replaced_arguments(arg, id))
end
def on_id(node, arg)
register_offense(node, "'#{arg.to_s.delete('#')}'")
end
def attribute?(arg)
CssSelector.attribute?(arg) &&
CssSelector.common_attributes?(arg)
end
def register_offense(node, arg_replacement)
add_offense(offense_range(node)) do |corrector|
corrector.replace(node.loc.selector, 'find_by_id')
corrector.replace(node.first_argument.loc.expression,
arg_replacement)
end
end
def replaced_arguments(arg, id)
options = to_options(CssSelector.attributes(arg))
options.empty? ? id : "#{id}, #{options}"
end
def to_options(attrs)
attrs.each.map do |key, value|
next if key == 'id'
"#{key}: #{value}"
end.compact.join(', ')
end
def offense_range(node)
range_between(node.loc.selector.begin_pos, end_pos(node))
end
def end_pos(node)
if node.loc.end
node.loc.end.end_pos
else
node.loc.expression.end_pos
end
end
end
end
end
end
end

View File

@ -1,79 +0,0 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# Checks for there is a more specific matcher offered by Capybara.
#
# @example
#
# # bad
# expect(page).to have_selector('button')
# expect(page).to have_no_selector('button.cls')
# expect(page).to have_css('button')
# expect(page).to have_no_css('a.cls', href: 'http://example.com')
# expect(page).to have_css('table.cls')
# expect(page).to have_css('select')
# expect(page).to have_css('input', exact_text: 'foo')
#
# # good
# expect(page).to have_button
# expect(page).to have_no_button(class: 'cls')
# expect(page).to have_button
# expect(page).to have_no_link('foo', class: 'cls', href: 'http://example.com')
# expect(page).to have_table(class: 'cls')
# expect(page).to have_select
# expect(page).to have_field('foo')
#
class SpecificMatcher < ::RuboCop::Cop::Base
MSG = 'Prefer `%<good_matcher>s` over `%<bad_matcher>s`.'
RESTRICT_ON_SEND = %i[have_selector have_no_selector have_css
have_no_css].freeze
SPECIFIC_MATCHER = {
'button' => 'button',
'a' => 'link',
'table' => 'table',
'select' => 'select',
'input' => 'field'
}.freeze
# @!method first_argument(node)
def_node_matcher :first_argument, <<-PATTERN
(send nil? _ (str $_) ... )
PATTERN
def on_send(node)
first_argument(node) do |arg|
next unless (matcher = specific_matcher(arg))
next if CssSelector.multiple_selectors?(arg)
next unless CapybaraHelp.specific_option?(node, arg, matcher)
next unless CapybaraHelp.specific_pseudo_classes?(arg)
add_offense(node, message: message(node, matcher))
end
end
private
def specific_matcher(arg)
splitted_arg = arg[/^\w+/, 0]
SPECIFIC_MATCHER[splitted_arg]
end
def message(node, matcher)
format(MSG,
good_matcher: good_matcher(node, matcher),
bad_matcher: node.method_name)
end
def good_matcher(node, matcher)
node.method_name
.to_s
.gsub(/selector|css/, matcher.to_s)
end
end
end
end
end
end

View File

@ -1,73 +0,0 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# Checks for boolean visibility in Capybara finders.
#
# Capybara lets you find elements that match a certain visibility using
# the `:visible` option. `:visible` accepts both boolean and symbols as
# values, however using booleans can have unwanted effects. `visible:
# false` does not find just invisible elements, but both visible and
# invisible elements. For expressiveness and clarity, use one of the
# symbol values, `:all`, `:hidden` or `:visible`.
# Read more in
# https://www.rubydoc.info/gems/capybara/Capybara%2FNode%2FFinders:all[the documentation].
#
# @example
# # bad
# expect(page).to have_selector('.foo', visible: false)
# expect(page).to have_css('.foo', visible: true)
# expect(page).to have_link('my link', visible: false)
#
# # good
# expect(page).to have_selector('.foo', visible: :visible)
# expect(page).to have_css('.foo', visible: :all)
# expect(page).to have_link('my link', visible: :hidden)
#
class VisibilityMatcher < ::RuboCop::Cop::Base
MSG_FALSE = 'Use `:all` or `:hidden` instead of `false`.'
MSG_TRUE = 'Use `:visible` instead of `true`.'
CAPYBARA_MATCHER_METHODS = %w[
button
checked_field
css
field
link
select
selector
table
unchecked_field
xpath
].flat_map do |element|
["have_#{element}".to_sym, "have_no_#{element}".to_sym]
end
RESTRICT_ON_SEND = CAPYBARA_MATCHER_METHODS
# @!method visible_true?(node)
def_node_matcher :visible_true?, <<~PATTERN
(send nil? #capybara_matcher? ... (hash <$(pair (sym :visible) true) ...>))
PATTERN
# @!method visible_false?(node)
def_node_matcher :visible_false?, <<~PATTERN
(send nil? #capybara_matcher? ... (hash <$(pair (sym :visible) false) ...>))
PATTERN
def on_send(node)
visible_false?(node) { |arg| add_offense(arg, message: MSG_FALSE) }
visible_true?(node) { |arg| add_offense(arg, message: MSG_TRUE) }
end
private
def capybara_matcher?(method_name)
CAPYBARA_MATCHER_METHODS.include? method_name
end
end
end
end
end
end

View File

@ -1,80 +0,0 @@
# 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

View File

@ -1,146 +0,0 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Helps parsing css selector.
module CssSelector
COMMON_OPTIONS = %w[
above below left_of right_of near count minimum maximum between text
id class style visible obscured exact exact_text normalize_ws match
wait filter_set focused
].freeze
SPECIFIC_OPTIONS = {
'button' => (
COMMON_OPTIONS + %w[disabled name value title type]
).freeze,
'link' => (
COMMON_OPTIONS + %w[href alt title download]
).freeze,
'table' => (
COMMON_OPTIONS + %w[
caption with_cols cols with_rows rows
]
).freeze,
'select' => (
COMMON_OPTIONS + %w[
disabled name placeholder options enabled_options
disabled_options selected with_selected multiple with_options
]
).freeze,
'field' => (
COMMON_OPTIONS + %w[
checked unchecked disabled valid name placeholder
validation_message readonly with type multiple
]
).freeze
}.freeze
SPECIFIC_PSEUDO_CLASSES = %w[
not() disabled enabled checked unchecked
].freeze
module_function
# @param element [String]
# @param attribute [String]
# @return [Boolean]
# @example
# specific_pesudo_classes?('button', 'name') # => true
# specific_pesudo_classes?('link', 'invalid') # => false
def specific_options?(element, attribute)
SPECIFIC_OPTIONS.fetch(element, []).include?(attribute)
end
# @param pseudo_class [String]
# @return [Boolean]
# @example
# specific_pesudo_classes?('disabled') # => true
# specific_pesudo_classes?('first-of-type') # => false
def specific_pesudo_classes?(pseudo_class)
SPECIFIC_PSEUDO_CLASSES.include?(pseudo_class)
end
# @param selector [String]
# @return [Boolean]
# @example
# id?('#some-id') # => true
# id?('.some-class') # => false
def id?(selector)
selector.start_with?('#')
end
# @param selector [String]
# @return [Boolean]
# @example
# attribute?('[attribute]') # => true
# attribute?('attribute') # => false
def attribute?(selector)
selector.start_with?('[')
end
# @param selector [String]
# @return [Array<String>]
# @example
# attributes('a[foo-bar_baz]') # => {"foo-bar_baz=>true}
# attributes('button[foo][bar]') # => {"foo"=>true, "bar"=>true}
# attributes('table[foo=bar]') # => {"foo"=>"'bar'"}
def attributes(selector)
selector.scan(/\[(.*?)\]/).flatten.to_h do |attr|
key, value = attr.split('=')
[key, normalize_value(value)]
end
end
# @param selector [String]
# @return [Boolean]
# @example
# common_attributes?('a[focused]') # => true
# common_attributes?('button[focused][visible]') # => true
# common_attributes?('table[id=some-id]') # => true
# common_attributes?('h1[invalid]') # => false
def common_attributes?(selector)
attributes(selector).keys.difference(COMMON_OPTIONS).none?
end
# @param selector [String]
# @return [Array<String>]
# @example
# pseudo_classes('button:not([disabled])') # => ['not()']
# pseudo_classes('a:enabled:not([valid])') # => ['enabled', 'not()']
def pseudo_classes(selector)
# Attributes must be excluded or else the colon in the `href`s URL
# will also be picked up as pseudo classes.
# "a:not([href='http://example.com']):enabled" => "a:not():enabled"
ignored_attribute = selector.gsub(/\[.*?\]/, '')
# "a:not():enabled" => ["not()", "enabled"]
ignored_attribute.scan(/:([^:]*)/).flatten
end
# @param selector [String]
# @return [Boolean]
# @example
# multiple_selectors?('a.cls b#id') # => true
# multiple_selectors?('a.cls') # => false
def multiple_selectors?(selector)
selector.match?(/[ >,+~]/)
end
# @param value [String]
# @return [Boolean, String]
# @example
# normalize_value('true') # => true
# normalize_value('false') # => false
# normalize_value(nil) # => false
# normalize_value("foo") # => "'foo'"
def normalize_value(value)
case value
when 'true' then true
when 'false' then false
when nil then true
else "'#{value}'"
end
end
end
end
end
end

View File

@ -0,0 +1,23 @@
#
# Configuration of obsolete/deprecated cops used by `ConfigObsoletion`.
#
# See: https://docs.rubocop.org/rubocop/extensions.html#config-obsoletions
#
# Cop parameters that have been changed
# Can be treated as a warning instead of a failure with `severity: warning`
changed_parameters:
- cops:
- RSpec/VariableName
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

View File

@ -4,6 +4,7 @@ require 'pathname'
require 'yaml'
require 'rubocop'
require 'rubocop-capybara'
require_relative 'rubocop/rspec'
require_relative 'rubocop/rspec/inject'
@ -17,8 +18,6 @@ require_relative 'rubocop/rspec/language'
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'

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# @!parse
# # 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')
# #
# # # good
# # expect(page).to have_current_path('/callback')
# #
# # # bad (does not support autocorrection)
# # expect(page.current_path).to match(variable)
# #
# # # good
# # expect(page).to have_current_path('/callback')
# #
# class CurrentPathExpectation < ::RuboCop::Cop::Base; end
CurrentPathExpectation =
::RuboCop::Cop::Capybara::CurrentPathExpectation
end
end
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# @!parse
# # Checks for usage of deprecated style methods.
# #
# # @example when using `assert_style`
# # # bad
# # page.find(:css, '#first').assert_style(display: 'block')
# #
# # # good
# # page.find(:css, '#first').assert_matches_style(display: 'block')
# #
# # @example when using `has_style?`
# # # bad
# # expect(page.find(:css, 'first')
# # .has_style?(display: 'block')).to be true
# #
# # # good
# # expect(page.find(:css, 'first')
# # .matches_style?(display: 'block')).to be true
# #
# # @example when using `have_style`
# # # bad
# # expect(page).to have_style(display: 'block')
# #
# # # good
# # expect(page).to match_style(display: 'block')
# #
# class MatchStyle < ::RuboCop::Cop::Base; end
MatchStyle = ::RuboCop::Cop::Capybara::MatchStyle
end
end
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# @!parse
# # 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 < ::RuboCop::Cop::Base; end
NegationMatcher = ::RuboCop::Cop::Capybara::NegationMatcher
end
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# @!parse
# # Checks for there is a more specific actions offered by Capybara.
# #
# # @example
# #
# # # bad
# # find('a').click
# # find('button.cls').click
# # find('a', exact_text: 'foo').click
# # find('div button').click
# #
# # # good
# # click_link
# # click_button(class: 'cls')
# # click_link(exact_text: 'foo')
# # find('div').click_button
# #
# class SpecificActions < ::RuboCop::Cop::Base; end
SpecificActions = ::RuboCop::Cop::Capybara::SpecificActions
end
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# @!parse
# # Checks if there is a more specific finder offered by Capybara.
# #
# # @example
# # # bad
# # find('#some-id')
# # find('[visible][id=some-id]')
# #
# # # good
# # find_by_id('some-id')
# # find_by_id('some-id', visible: true)
# #
# class SpecificFinders < ::RuboCop::Cop::Base; end
SpecificFinders = ::RuboCop::Cop::Capybara::SpecificFinders
end
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# @!parse
# # Checks for there is a more specific matcher offered by Capybara.
# #
# # @example
# #
# # # bad
# # expect(page).to have_selector('button')
# # expect(page).to have_no_selector('button.cls')
# # expect(page).to have_css('button')
# # expect(page).to have_no_css('a.cls', href: 'http://example.com')
# # expect(page).to have_css('table.cls')
# # expect(page).to have_css('select')
# # expect(page).to have_css('input', exact_text: 'foo')
# #
# # # good
# # expect(page).to have_button
# # expect(page).to have_no_button(class: 'cls')
# # expect(page).to have_button
# # expect(page).to have_no_link('foo', class: 'cls', href: 'http://example.com')
# # expect(page).to have_table(class: 'cls')
# # expect(page).to have_select
# # expect(page).to have_field('foo')
# #
# class SpecificMatcher < ::RuboCop::Cop::Base; end
SpecificMatcher = ::RuboCop::Cop::Capybara::SpecificMatcher
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Capybara
# @!parse
# # Checks for boolean visibility in Capybara finders.
# #
# # Capybara lets you find elements that match a certain visibility
# # using the `:visible` option. `:visible` accepts both boolean and
# # symbols as values, however using booleans can have unwanted
# # effects. `visible: false` does not find just invisible elements,
# # but both visible and invisible elements. For expressiveness and
# # clarity, use one of the # symbol values, `:all`, `:hidden` or
# # `:visible`.
# # Read more in
# # https://www.rubydoc.info/gems/capybara/Capybara%2FNode%2FFinders:all[the documentation].
# #
# # @example
# # # bad
# # expect(page).to have_selector('.foo', visible: false)
# # expect(page).to have_css('.foo', visible: true)
# # expect(page).to have_link('my link', visible: false)
# #
# # # good
# # expect(page).to have_selector('.foo', visible: :visible)
# # expect(page).to have_css('.foo', visible: :all)
# # expect(page).to have_link('my link', visible: :hidden)
# #
# class VisibilityMatcher < ::RuboCop::Cop::Base; end
VisibilityMatcher = ::RuboCop::Cop::Capybara::VisibilityMatcher
end
end
end
end

View File

@ -31,7 +31,11 @@ module RuboCop
# @!method context_method(node)
def_node_matcher :context_method, <<-PATTERN
(block (send #rspec? :context $(str #method_name?) ...) ...)
(block
(send #rspec? :context
${(str #method_name?) (dstr (str #method_name?) ...)}
...)
...)
PATTERN
def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler

Some files were not shown because too many files have changed in this diff Show More