teamcapybara--capybara/lib/capybara/selector/selector.rb

148 lines
4.1 KiB
Ruby

# frozen_string_literal: true
module Capybara
class Selector < SimpleDelegator
class << self
def all
@definitions ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
end
def [](name)
all.fetch(name.to_sym) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
end
def add(name, **options, &block)
all[name.to_sym] = Definition.new(name.to_sym, **options, &block)
end
def update(name, &block)
self[name].instance_eval(&block)
end
def remove(name)
all.delete(name.to_sym)
end
def for(locator)
all.values.find { |sel| sel.match?(locator) }
end
end
attr_reader :errors
def initialize(definition, config:, format:)
definition = self.class[definition] unless definition.is_a? Definition
super(definition)
@definition = definition
@config = config
@format = format
@errors = []
end
def format
@format || @definition.default_format
end
alias_method :current_format, :format
def enable_aria_label
@config[:enable_aria_label]
end
def test_id
@config[:test_id]
end
def call(locator, **options)
if format
raise ArgumentError, "Selector #{@name} does not support #{format}" unless expressions.key?(format)
instance_exec(locator, **options, &expressions[format])
else
warn 'Selector has no format'
end
ensure
unless locator_valid?(locator)
warn "Locator #{locator.class}:#{locator.inspect} for selector #{name.inspect} must #{locator_description}. This will raise an error in a future version of Capybara."
end
end
def add_error(error_msg)
errors << error_msg
end
def expression_for(name, locator, config: @config, format: current_format, **options)
Selector.new(name, config: config, format: format).call(locator, **options)
end
# @api private
def with_filter_errors(errors)
old_errors = @errors
@errors = errors
yield
ensure
@errors = old_errors
end
# @api private
def builder(expr = nil)
case format
when :css
Capybara::Selector::CSSBuilder
when :xpath
Capybara::Selector::XPathBuilder
else
raise NotImplementedError, "No builder exists for selector of type #{default_format}"
end.new(expr)
end
private
def locator_description
locator_types.group_by { |lt| lt.is_a? Symbol }.map do |symbol, types_or_methods|
if symbol
"respond to #{types_or_methods.join(' or ')}"
else
"be an instance of #{types_or_methods.join(' or ')}"
end
end.join(' or ')
end
def locator_valid?(locator)
return true unless locator && locator_types
locator_types&.any? do |type_or_method|
type_or_method.is_a?(Symbol) ? locator.respond_to?(type_or_method) : type_or_method === locator # rubocop:disable Style/CaseEquality
end
end
def locate_field(xpath, locator, **_options)
return xpath if locator.nil?
locate_xpath = xpath # Need to save original xpath for the label wrap
locator = locator.to_s
attr_matchers = [XPath.attr(:id) == locator,
XPath.attr(:name) == locator,
XPath.attr(:placeholder) == locator,
XPath.attr(:id) == XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for)].reduce(:|)
attr_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
attr_matchers |= XPath.attr(test_id) == locator if test_id
locate_xpath = locate_xpath[attr_matchers]
locate_xpath + XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
end
def find_by_attr(attribute, value)
finder_name = "find_by_#{attribute}_attr"
if respond_to?(finder_name, true)
send(finder_name, value)
else
value ? XPath.attr(attribute) == value : nil
end
end
def find_by_class_attr(classes)
Array(classes).map { |klass| XPath.attr(:class).contains_word(klass) }.reduce(:&)
end
end
end