Merge pull request #14671 from Homebrew/dependabot/bundler/Library/Homebrew/rubocop-capybara-2.17.1
build(deps): bump rubocop-capybara from 2.17.0 to 2.17.1 in /Library/Homebrew
This commit is contained in:
commit
9b063cc787
@ -142,7 +142,7 @@ GEM
|
|||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.26.0)
|
rubocop-ast (1.26.0)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.2.1.0)
|
||||||
rubocop-capybara (2.17.0)
|
rubocop-capybara (2.17.1)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-performance (1.16.0)
|
rubocop-performance (1.16.0)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
|
|||||||
@ -8,19 +8,74 @@ module RuboCop; end
|
|||||||
module RuboCop::Cop; end
|
module RuboCop::Cop; end
|
||||||
module RuboCop::Cop::Capybara; end
|
module RuboCop::Cop::Capybara; end
|
||||||
|
|
||||||
|
module RuboCop::Cop::Capybara::CapybaraHelp
|
||||||
|
private
|
||||||
|
|
||||||
|
def common_attributes?(selector); end
|
||||||
|
def include_option?(node, option); end
|
||||||
|
def replaceable_attributes?(attrs); end
|
||||||
|
def replaceable_element?(node, element, attrs); end
|
||||||
|
def replaceable_option?(node, locator, element); end
|
||||||
|
def replaceable_pseudo_class?(pseudo_class, locator); end
|
||||||
|
def replaceable_pseudo_class_not?(locator); end
|
||||||
|
def replaceable_pseudo_classes?(locator); end
|
||||||
|
def replaceable_to_link?(node, attrs); end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def common_attributes?(selector); end
|
||||||
|
def include_option?(node, option); end
|
||||||
|
def replaceable_attributes?(attrs); end
|
||||||
|
def replaceable_element?(node, element, attrs); end
|
||||||
|
def replaceable_option?(node, locator, element); end
|
||||||
|
def replaceable_pseudo_class?(pseudo_class, locator); end
|
||||||
|
def replaceable_pseudo_class_not?(locator); end
|
||||||
|
def replaceable_pseudo_classes?(locator); end
|
||||||
|
def replaceable_to_link?(node, attrs); end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RuboCop::Cop::Capybara::CapybaraHelp::COMMON_OPTIONS = T.let(T.unsafe(nil), Array)
|
||||||
|
RuboCop::Cop::Capybara::CapybaraHelp::SPECIFIC_OPTIONS = T.let(T.unsafe(nil), Hash)
|
||||||
|
RuboCop::Cop::Capybara::CapybaraHelp::SPECIFIC_PSEUDO_CLASSES = T.let(T.unsafe(nil), Array)
|
||||||
|
|
||||||
|
module RuboCop::Cop::Capybara::CssSelector
|
||||||
|
private
|
||||||
|
|
||||||
|
def attribute?(selector); end
|
||||||
|
def attributes(selector); end
|
||||||
|
def classes(selector); end
|
||||||
|
def id(selector); end
|
||||||
|
def id?(selector); end
|
||||||
|
def multiple_selectors?(selector); end
|
||||||
|
def normalize_value(value); end
|
||||||
|
def pseudo_classes(selector); end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def attribute?(selector); end
|
||||||
|
def attributes(selector); end
|
||||||
|
def classes(selector); end
|
||||||
|
def id(selector); end
|
||||||
|
def id?(selector); end
|
||||||
|
def multiple_selectors?(selector); end
|
||||||
|
def normalize_value(value); end
|
||||||
|
def pseudo_classes(selector); end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class RuboCop::Cop::Capybara::CurrentPathExpectation < ::RuboCop::Cop::Base
|
class RuboCop::Cop::Capybara::CurrentPathExpectation < ::RuboCop::Cop::Base
|
||||||
extend ::RuboCop::Cop::AutoCorrector
|
extend ::RuboCop::Cop::AutoCorrector
|
||||||
|
|
||||||
def as_is_matcher(param0 = T.unsafe(nil)); end
|
def as_is_matcher(param0 = T.unsafe(nil)); end
|
||||||
def expectation_set_on_current_path(param0 = T.unsafe(nil)); end
|
def expectation_set_on_current_path(param0 = T.unsafe(nil)); end
|
||||||
def on_send(node); end
|
def on_send(node); end
|
||||||
def regexp_str_matcher(param0 = T.unsafe(nil)); end
|
def regexp_node_matcher(param0 = T.unsafe(nil)); end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def add_ignore_query_options(corrector, node); end
|
def add_ignore_query_options(corrector, node); end
|
||||||
def autocorrect(corrector, node); end
|
def autocorrect(corrector, node); end
|
||||||
def convert_regexp_str_to_literal(corrector, matcher_node, regexp_str); end
|
def convert_regexp_node_to_literal(corrector, matcher_node, regexp_node); end
|
||||||
|
def regexp_node_to_regexp_expr(regexp_node); end
|
||||||
def rewrite_expectation(corrector, node, to_symbol, matcher_node); end
|
def rewrite_expectation(corrector, node, to_symbol, matcher_node); end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
@ -78,6 +133,8 @@ class RuboCop::Cop::Capybara::SpecificActions < ::RuboCop::Cop::Base
|
|||||||
def last_selector(arg); end
|
def last_selector(arg); end
|
||||||
def message(action, selector); end
|
def message(action, selector); end
|
||||||
def offense_range(node, receiver); end
|
def offense_range(node, receiver); end
|
||||||
|
def replaceable?(node, arg, action); end
|
||||||
|
def replaceable_attributes?(selector); end
|
||||||
def specific_action(selector); end
|
def specific_action(selector); end
|
||||||
def supported_selector?(selector); end
|
def supported_selector?(selector); end
|
||||||
end
|
end
|
||||||
@ -90,17 +147,21 @@ class RuboCop::Cop::Capybara::SpecificFinders < ::RuboCop::Cop::Base
|
|||||||
include ::RuboCop::Cop::RangeHelp
|
include ::RuboCop::Cop::RangeHelp
|
||||||
extend ::RuboCop::Cop::AutoCorrector
|
extend ::RuboCop::Cop::AutoCorrector
|
||||||
|
|
||||||
|
def class_options(param0); end
|
||||||
def find_argument(param0 = T.unsafe(nil)); end
|
def find_argument(param0 = T.unsafe(nil)); end
|
||||||
def on_send(node); end
|
def on_send(node); end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def append_options(classes, options); end
|
||||||
def attribute?(arg); end
|
def attribute?(arg); end
|
||||||
|
def autocorrect_classes(corrector, node, classes); end
|
||||||
def end_pos(node); end
|
def end_pos(node); end
|
||||||
|
def keyword_argument_class(classes); end
|
||||||
def offense_range(node); end
|
def offense_range(node); end
|
||||||
def on_attr(node, arg); end
|
def on_attr(node, arg); end
|
||||||
def on_id(node, arg); end
|
def on_id(node, arg); end
|
||||||
def register_offense(node, arg_replacement); end
|
def register_offense(node, id, classes = T.unsafe(nil)); end
|
||||||
def replaced_arguments(arg, id); end
|
def replaced_arguments(arg, id); end
|
||||||
def to_options(attrs); end
|
def to_options(attrs); end
|
||||||
end
|
end
|
||||||
@ -116,6 +177,8 @@ class RuboCop::Cop::Capybara::SpecificMatcher < ::RuboCop::Cop::Base
|
|||||||
|
|
||||||
def good_matcher(node, matcher); end
|
def good_matcher(node, matcher); end
|
||||||
def message(node, matcher); end
|
def message(node, matcher); end
|
||||||
|
def replaceable?(node, arg, matcher); end
|
||||||
|
def replaceable_attributes?(selector); end
|
||||||
def specific_matcher(arg); end
|
def specific_matcher(arg); end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -137,58 +200,6 @@ RuboCop::Cop::Capybara::VisibilityMatcher::CAPYBARA_MATCHER_METHODS = T.let(T.un
|
|||||||
RuboCop::Cop::Capybara::VisibilityMatcher::MSG_FALSE = T.let(T.unsafe(nil), String)
|
RuboCop::Cop::Capybara::VisibilityMatcher::MSG_FALSE = T.let(T.unsafe(nil), String)
|
||||||
RuboCop::Cop::Capybara::VisibilityMatcher::MSG_TRUE = T.let(T.unsafe(nil), String)
|
RuboCop::Cop::Capybara::VisibilityMatcher::MSG_TRUE = T.let(T.unsafe(nil), String)
|
||||||
RuboCop::Cop::Capybara::VisibilityMatcher::RESTRICT_ON_SEND = T.let(T.unsafe(nil), Array)
|
RuboCop::Cop::Capybara::VisibilityMatcher::RESTRICT_ON_SEND = T.let(T.unsafe(nil), Array)
|
||||||
|
|
||||||
module RuboCop::Cop::CapybaraHelp
|
|
||||||
private
|
|
||||||
|
|
||||||
def include_option?(node, option); end
|
|
||||||
def replaceable_element?(node, element, attrs); end
|
|
||||||
def replaceable_pseudo_class?(pseudo_class, locator); end
|
|
||||||
def replaceable_pseudo_class_not?(locator); end
|
|
||||||
def replaceable_to_link?(node, attrs); end
|
|
||||||
def specific_option?(node, locator, element); end
|
|
||||||
def specific_pseudo_classes?(locator); end
|
|
||||||
|
|
||||||
class << self
|
|
||||||
def include_option?(node, option); end
|
|
||||||
def replaceable_element?(node, element, attrs); end
|
|
||||||
def replaceable_pseudo_class?(pseudo_class, locator); end
|
|
||||||
def replaceable_pseudo_class_not?(locator); end
|
|
||||||
def replaceable_to_link?(node, attrs); end
|
|
||||||
def specific_option?(node, locator, element); end
|
|
||||||
def specific_pseudo_classes?(locator); end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module RuboCop::Cop::CssSelector
|
|
||||||
private
|
|
||||||
|
|
||||||
def attribute?(selector); end
|
|
||||||
def attributes(selector); end
|
|
||||||
def common_attributes?(selector); end
|
|
||||||
def id?(selector); end
|
|
||||||
def multiple_selectors?(selector); end
|
|
||||||
def normalize_value(value); end
|
|
||||||
def pseudo_classes(selector); end
|
|
||||||
def specific_options?(element, attribute); end
|
|
||||||
def specific_pesudo_classes?(pseudo_class); end
|
|
||||||
|
|
||||||
class << self
|
|
||||||
def attribute?(selector); end
|
|
||||||
def attributes(selector); end
|
|
||||||
def common_attributes?(selector); end
|
|
||||||
def id?(selector); end
|
|
||||||
def multiple_selectors?(selector); end
|
|
||||||
def normalize_value(value); end
|
|
||||||
def pseudo_classes(selector); end
|
|
||||||
def specific_options?(element, attribute); end
|
|
||||||
def specific_pesudo_classes?(pseudo_class); end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
RuboCop::Cop::CssSelector::COMMON_OPTIONS = T.let(T.unsafe(nil), Array)
|
|
||||||
RuboCop::Cop::CssSelector::SPECIFIC_OPTIONS = T.let(T.unsafe(nil), Hash)
|
|
||||||
RuboCop::Cop::CssSelector::SPECIFIC_PSEUDO_CLASSES = T.let(T.unsafe(nil), Array)
|
|
||||||
RuboCop::Cop::IgnoredMethods = RuboCop::Cop::AllowedMethods
|
RuboCop::Cop::IgnoredMethods = RuboCop::Cop::AllowedMethods
|
||||||
RuboCop::Cop::IgnoredPattern = RuboCop::Cop::AllowedPattern
|
RuboCop::Cop::IgnoredPattern = RuboCop::Cop::AllowedPattern
|
||||||
RuboCop::NodePattern = RuboCop::AST::NodePattern
|
RuboCop::NodePattern = RuboCop::AST::NodePattern
|
||||||
@ -104,7 +104,7 @@ $:.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/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/unicode-display_width-2.4.2/lib")
|
||||||
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-1.45.1/lib")
|
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-1.45.1/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-capybara-2.17.1/lib")
|
||||||
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-performance-1.16.0/lib")
|
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-performance-1.16.0/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-rails-2.17.4/lib")
|
||||||
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-rspec-2.18.1/lib")
|
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-rspec-2.18.1/lib")
|
||||||
|
|||||||
@ -1,78 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,144 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -4,7 +4,7 @@ module RuboCop
|
|||||||
module Capybara
|
module Capybara
|
||||||
# Version information for the Capybara RuboCop plugin.
|
# Version information for the Capybara RuboCop plugin.
|
||||||
module Version
|
module Version
|
||||||
STRING = '2.17.0'
|
STRING = '2.17.1'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -50,11 +50,11 @@ module RuboCop
|
|||||||
${(send nil? :eq ...) (send nil? :match (regexp ...))})
|
${(send nil? :eq ...) (send nil? :match (regexp ...))})
|
||||||
PATTERN
|
PATTERN
|
||||||
|
|
||||||
# @!method regexp_str_matcher(node)
|
# @!method regexp_node_matcher(node)
|
||||||
def_node_matcher :regexp_str_matcher, <<-PATTERN
|
def_node_matcher :regexp_node_matcher, <<-PATTERN
|
||||||
(send
|
(send
|
||||||
#expectation_set_on_current_path ${:to :to_not :not_to}
|
#expectation_set_on_current_path ${:to :to_not :not_to}
|
||||||
$(send nil? :match (str $_)))
|
$(send nil? :match ${str dstr xstr}))
|
||||||
PATTERN
|
PATTERN
|
||||||
|
|
||||||
def self.autocorrect_incompatible_with
|
def self.autocorrect_incompatible_with
|
||||||
@ -78,9 +78,9 @@ module RuboCop
|
|||||||
rewrite_expectation(corrector, node, to_sym, matcher_node)
|
rewrite_expectation(corrector, node, to_sym, matcher_node)
|
||||||
end
|
end
|
||||||
|
|
||||||
regexp_str_matcher(node.parent) do |to_sym, matcher_node, regexp|
|
regexp_node_matcher(node.parent) do |to_sym, matcher_node, regexp|
|
||||||
rewrite_expectation(corrector, node, to_sym, matcher_node)
|
rewrite_expectation(corrector, node, to_sym, matcher_node)
|
||||||
convert_regexp_str_to_literal(corrector, matcher_node, regexp)
|
convert_regexp_node_to_literal(corrector, matcher_node, regexp)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -97,12 +97,20 @@ module RuboCop
|
|||||||
add_ignore_query_options(corrector, node)
|
add_ignore_query_options(corrector, node)
|
||||||
end
|
end
|
||||||
|
|
||||||
def convert_regexp_str_to_literal(corrector, matcher_node, regexp_str)
|
def convert_regexp_node_to_literal(corrector, matcher_node, regexp_node)
|
||||||
str_node = matcher_node.first_argument
|
str_node = matcher_node.first_argument
|
||||||
regexp_expr = Regexp.new(regexp_str).inspect
|
regexp_expr = regexp_node_to_regexp_expr(regexp_node)
|
||||||
corrector.replace(str_node, regexp_expr)
|
corrector.replace(str_node, regexp_expr)
|
||||||
end
|
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
|
||||||
|
|
||||||
# `have_current_path` with no options will include the querystring
|
# `have_current_path` with no options will include the querystring
|
||||||
# while `page.current_path` does not.
|
# while `page.current_path` does not.
|
||||||
# This ensures the option `ignore_query: true` is added
|
# This ensures the option `ignore_query: true` is added
|
||||||
@ -110,7 +118,9 @@ module RuboCop
|
|||||||
def add_ignore_query_options(corrector, node)
|
def add_ignore_query_options(corrector, node)
|
||||||
expectation_node = node.parent.last_argument
|
expectation_node = node.parent.last_argument
|
||||||
expectation_last_child = expectation_node.children.last
|
expectation_last_child = expectation_node.children.last
|
||||||
return if %i[regexp str].include?(expectation_last_child.type)
|
return if %i[
|
||||||
|
regexp str dstr xstr
|
||||||
|
].include?(expectation_last_child.type)
|
||||||
|
|
||||||
corrector.insert_after(
|
corrector.insert_after(
|
||||||
expectation_last_child,
|
expectation_last_child,
|
||||||
@ -0,0 +1,130 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module RuboCop
|
||||||
|
module Cop
|
||||||
|
module Capybara
|
||||||
|
# Help methods for capybara.
|
||||||
|
module CapybaraHelp
|
||||||
|
COMMON_OPTIONS = %w[
|
||||||
|
id class style
|
||||||
|
].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[cols rows]
|
||||||
|
).freeze,
|
||||||
|
'select' => (
|
||||||
|
COMMON_OPTIONS + %w[
|
||||||
|
disabled name placeholder
|
||||||
|
selected multiple
|
||||||
|
]
|
||||||
|
).freeze,
|
||||||
|
'field' => (
|
||||||
|
COMMON_OPTIONS + %w[
|
||||||
|
checked disabled name placeholder
|
||||||
|
readonly type multiple
|
||||||
|
]
|
||||||
|
).freeze
|
||||||
|
}.freeze
|
||||||
|
SPECIFIC_PSEUDO_CLASSES = %w[
|
||||||
|
not() disabled enabled checked unchecked
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
# @param node [RuboCop::AST::SendNode]
|
||||||
|
# @param locator [String]
|
||||||
|
# @param element [String]
|
||||||
|
# @return [Boolean]
|
||||||
|
def replaceable_option?(node, locator, element)
|
||||||
|
attrs = CssSelector.attributes(locator).keys
|
||||||
|
return false unless replaceable_element?(node, element, attrs)
|
||||||
|
|
||||||
|
attrs.all? do |attr|
|
||||||
|
SPECIFIC_OPTIONS.fetch(element, []).include?(attr)
|
||||||
|
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)
|
||||||
|
CssSelector.attributes(selector).keys.difference(COMMON_OPTIONS).none?
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param attrs [Array<String>]
|
||||||
|
# @return [Boolean]
|
||||||
|
# @example
|
||||||
|
# replaceable_attributes?('table[id=some-id]') # => true
|
||||||
|
# replaceable_attributes?('a[focused]') # => false
|
||||||
|
def replaceable_attributes?(attrs)
|
||||||
|
attrs.values.none?(&:nil?)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param locator [String]
|
||||||
|
# @return [Boolean]
|
||||||
|
def replaceable_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 SPECIFIC_PSEUDO_CLASSES.include?(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,0 +1,110 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module RuboCop
|
||||||
|
module Cop
|
||||||
|
module Capybara
|
||||||
|
# Helps parsing css selector.
|
||||||
|
module CssSelector
|
||||||
|
module_function
|
||||||
|
|
||||||
|
# @param selector [String]
|
||||||
|
# @return [String]
|
||||||
|
# @example
|
||||||
|
# id('#some-id') # => some-id
|
||||||
|
# id('.some-cls') # => nil
|
||||||
|
# id('#some-id.cls') # => some-id
|
||||||
|
def id(selector)
|
||||||
|
return unless id?(selector)
|
||||||
|
|
||||||
|
selector.delete('#').gsub(selector.scan(/[^\\]([>,+~.].*)/).join, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param selector [String]
|
||||||
|
# @return [Boolean]
|
||||||
|
# @example
|
||||||
|
# id?('#some-id') # => true
|
||||||
|
# id?('.some-cls') # => false
|
||||||
|
def id?(selector)
|
||||||
|
selector.start_with?('#')
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param selector [String]
|
||||||
|
# @return [Array<String>]
|
||||||
|
# @example
|
||||||
|
# classes('#some-id') # => []
|
||||||
|
# classes('.some-cls') # => ['some-cls']
|
||||||
|
# classes('#some-id.some-cls') # => ['some-cls']
|
||||||
|
# classes('#some-id.cls1.cls2') # => ['cls1', 'cls2']
|
||||||
|
def classes(selector)
|
||||||
|
selector.scan(/\.([\w-]*)/).flatten
|
||||||
|
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=>nil}
|
||||||
|
# attributes('button[foo][bar=baz]') # => {"foo"=>nil, "bar"=>"'baz'"}
|
||||||
|
# attributes('table[foo=bar]') # => {"foo"=>"'bar'"}
|
||||||
|
def attributes(selector)
|
||||||
|
# Extract the inner strings of attributes.
|
||||||
|
# For example, extract the following:
|
||||||
|
# 'button[foo][bar=baz]' => 'foo][bar=baz'
|
||||||
|
inside_attributes = selector.scan(/\[(.*)\]/).flatten.join
|
||||||
|
inside_attributes.split('][').to_h do |attr|
|
||||||
|
key, value = attr.split('=')
|
||||||
|
[key, normalize_value(value)]
|
||||||
|
end
|
||||||
|
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)
|
||||||
|
normalize = selector.gsub(/(\\[>,+~]|\(.*\))/, '')
|
||||||
|
normalize.match?(/[ >,+~]/)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param value [String]
|
||||||
|
# @return [Boolean, String]
|
||||||
|
# @example
|
||||||
|
# normalize_value('true') # => true
|
||||||
|
# normalize_value('false') # => false
|
||||||
|
# normalize_value(nil) # => nil
|
||||||
|
# normalize_value("foo") # => "'foo'"
|
||||||
|
def normalize_value(value)
|
||||||
|
case value
|
||||||
|
when 'true' then true
|
||||||
|
when 'false' then false
|
||||||
|
when nil then nil
|
||||||
|
else "'#{value.gsub(/"|'/, '')}'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -41,9 +41,7 @@ module RuboCop
|
|||||||
# which the last selector points.
|
# which the last selector points.
|
||||||
next unless (selector = last_selector(arg))
|
next unless (selector = last_selector(arg))
|
||||||
next unless (action = specific_action(selector))
|
next unless (action = specific_action(selector))
|
||||||
next unless CapybaraHelp.specific_option?(node.receiver, arg,
|
next unless replaceable?(node, arg, action)
|
||||||
action)
|
|
||||||
next unless CapybaraHelp.specific_pseudo_classes?(arg)
|
|
||||||
|
|
||||||
range = offense_range(node, node.receiver)
|
range = offense_range(node, node.receiver)
|
||||||
add_offense(range, message: message(action, selector))
|
add_offense(range, message: message(action, selector))
|
||||||
@ -56,6 +54,18 @@ module RuboCop
|
|||||||
SPECIFIC_ACTION[last_selector(selector)]
|
SPECIFIC_ACTION[last_selector(selector)]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def replaceable?(node, arg, action)
|
||||||
|
replaceable_attributes?(arg) &&
|
||||||
|
CapybaraHelp.replaceable_option?(node.receiver, arg, action) &&
|
||||||
|
CapybaraHelp.replaceable_pseudo_classes?(arg)
|
||||||
|
end
|
||||||
|
|
||||||
|
def replaceable_attributes?(selector)
|
||||||
|
CapybaraHelp.replaceable_attributes?(
|
||||||
|
CssSelector.attributes(selector)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def supported_selector?(selector)
|
def supported_selector?(selector)
|
||||||
!selector.match?(/[>,+~]/)
|
!selector.match?(/[>,+~]/)
|
||||||
end
|
end
|
||||||
@ -27,8 +27,14 @@ module RuboCop
|
|||||||
(send _ :find (str $_) ...)
|
(send _ :find (str $_) ...)
|
||||||
PATTERN
|
PATTERN
|
||||||
|
|
||||||
|
# @!method class_options(node)
|
||||||
|
def_node_search :class_options, <<~PATTERN
|
||||||
|
(pair (sym :class) $_ ...)
|
||||||
|
PATTERN
|
||||||
|
|
||||||
def on_send(node)
|
def on_send(node)
|
||||||
find_argument(node) do |arg|
|
find_argument(node) do |arg|
|
||||||
|
next if CssSelector.pseudo_classes(arg).any?
|
||||||
next if CssSelector.multiple_selectors?(arg)
|
next if CssSelector.multiple_selectors?(arg)
|
||||||
|
|
||||||
on_attr(node, arg) if attribute?(arg)
|
on_attr(node, arg) if attribute?(arg)
|
||||||
@ -39,28 +45,57 @@ module RuboCop
|
|||||||
private
|
private
|
||||||
|
|
||||||
def on_attr(node, arg)
|
def on_attr(node, arg)
|
||||||
return unless (id = CssSelector.attributes(arg)['id'])
|
attrs = CssSelector.attributes(arg)
|
||||||
|
return unless (id = attrs['id'])
|
||||||
|
return if attrs['class']
|
||||||
|
|
||||||
register_offense(node, replaced_arguments(arg, id))
|
register_offense(node, replaced_arguments(arg, id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_id(node, arg)
|
def on_id(node, arg)
|
||||||
register_offense(node, "'#{arg.to_s.delete('#')}'")
|
return if CssSelector.attributes(arg).any?
|
||||||
|
|
||||||
|
id = CssSelector.id(arg)
|
||||||
|
register_offense(node, "'#{id}'",
|
||||||
|
CssSelector.classes(arg.sub("##{id}", '')))
|
||||||
end
|
end
|
||||||
|
|
||||||
def attribute?(arg)
|
def attribute?(arg)
|
||||||
CssSelector.attribute?(arg) &&
|
CssSelector.attribute?(arg) &&
|
||||||
CssSelector.common_attributes?(arg)
|
CapybaraHelp.common_attributes?(arg)
|
||||||
end
|
end
|
||||||
|
|
||||||
def register_offense(node, arg_replacement)
|
def register_offense(node, id, classes = [])
|
||||||
add_offense(offense_range(node)) do |corrector|
|
add_offense(offense_range(node)) do |corrector|
|
||||||
corrector.replace(node.loc.selector, 'find_by_id')
|
corrector.replace(node.loc.selector, 'find_by_id')
|
||||||
corrector.replace(node.first_argument.loc.expression,
|
corrector.replace(node.first_argument.loc.expression,
|
||||||
arg_replacement)
|
id.delete('\\'))
|
||||||
|
unless classes.compact.empty?
|
||||||
|
autocorrect_classes(corrector, node, classes)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def autocorrect_classes(corrector, node, classes)
|
||||||
|
if (options = class_options(node).first)
|
||||||
|
append_options(classes, options)
|
||||||
|
corrector.replace(options, classes.to_s)
|
||||||
|
else
|
||||||
|
corrector.insert_after(node.first_argument,
|
||||||
|
keyword_argument_class(classes))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_options(classes, options)
|
||||||
|
classes << options.value if options.str_type?
|
||||||
|
options.each_value { |v| classes << v.value } if options.array_type?
|
||||||
|
end
|
||||||
|
|
||||||
|
def keyword_argument_class(classes)
|
||||||
|
value = classes.size > 1 ? classes.to_s : "'#{classes.first}'"
|
||||||
|
", class: #{value}"
|
||||||
|
end
|
||||||
|
|
||||||
def replaced_arguments(arg, id)
|
def replaced_arguments(arg, id)
|
||||||
options = to_options(CssSelector.attributes(arg))
|
options = to_options(CssSelector.attributes(arg))
|
||||||
options.empty? ? id : "#{id}, #{options}"
|
options.empty? ? id : "#{id}, #{options}"
|
||||||
@ -46,8 +46,7 @@ module RuboCop
|
|||||||
first_argument(node) do |arg|
|
first_argument(node) do |arg|
|
||||||
next unless (matcher = specific_matcher(arg))
|
next unless (matcher = specific_matcher(arg))
|
||||||
next if CssSelector.multiple_selectors?(arg)
|
next if CssSelector.multiple_selectors?(arg)
|
||||||
next unless CapybaraHelp.specific_option?(node, arg, matcher)
|
next unless replaceable?(node, arg, matcher)
|
||||||
next unless CapybaraHelp.specific_pseudo_classes?(arg)
|
|
||||||
|
|
||||||
add_offense(node, message: message(node, matcher))
|
add_offense(node, message: message(node, matcher))
|
||||||
end
|
end
|
||||||
@ -60,6 +59,18 @@ module RuboCop
|
|||||||
SPECIFIC_MATCHER[splitted_arg]
|
SPECIFIC_MATCHER[splitted_arg]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def replaceable?(node, arg, matcher)
|
||||||
|
replaceable_attributes?(arg) &&
|
||||||
|
CapybaraHelp.replaceable_option?(node, arg, matcher) &&
|
||||||
|
CapybaraHelp.replaceable_pseudo_classes?(arg)
|
||||||
|
end
|
||||||
|
|
||||||
|
def replaceable_attributes?(selector)
|
||||||
|
CapybaraHelp.replaceable_attributes?(
|
||||||
|
CssSelector.attributes(selector)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def message(node, matcher)
|
def message(node, matcher)
|
||||||
format(MSG,
|
format(MSG,
|
||||||
good_matcher: good_matcher(node, matcher),
|
good_matcher: good_matcher(node, matcher),
|
||||||
Loading…
x
Reference in New Issue
Block a user