class Capybara::Queries::SelectorQuery

Constants

SPATIAL_KEYS
VALID_KEYS
VALID_MATCH

Attributes

expression[R]
locator[R]
options[R]
selector[R]

Public Class Methods

new(*args, session_options:, enable_aria_label: session_options.enable_aria_label, enable_aria_role: session_options.enable_aria_role, test_id: session_options.test_id, selector_format: nil, order: nil, **options, &filter_block) click to toggle source
Calls superclass method Capybara::Queries::BaseQuery::new
# File lib/capybara/queries/selector_query.rb, line 15
def initialize(*args,
               session_options:,
               enable_aria_label: session_options.enable_aria_label,
               enable_aria_role: session_options.enable_aria_role,
               test_id: session_options.test_id,
               selector_format: nil,
               order: nil,
               **options,
               &filter_block)
  @resolved_node = nil
  @resolved_count = 0
  @options = options.dup
  @order = order
  @filter_cache = Hash.new { |hsh, key| hsh[key] = {} }

  if @options[:text].is_a?(Regexp) && [true, false].include?(@options[:exact_text])
    Capybara::Helpers.warn(
      "Boolean 'exact_text' option is not supported when 'text' option is a Regexp - ignoring"
    )
  end

  super(@options)
  self.session_options = session_options

  @selector = Selector.new(
    find_selector(args[0].is_a?(Symbol) ? args.shift : args[0]),
    config: {
      enable_aria_label: enable_aria_label,
      enable_aria_role: enable_aria_role,
      test_id: test_id
    },
    format: selector_format
  )

  @locator = args.shift
  @filter_block = filter_block

  raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?

  @expression = selector.call(@locator, **@options)

  warn_exact_usage

  assert_valid_keys
end

Public Instance Methods

applied_description() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 113
def applied_description
  description(true)
end
css() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 154
def css
  filtered_expression(apply_expression_filters(@expression))
end
description(only_applied = false) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 64
def description(only_applied = false) # rubocop:disable Style/OptionalBooleanParameter
  desc = +''
  show_for = show_for_stage(only_applied)

  if show_for[:any]
    desc << 'visible ' if visible == :visible
    desc << 'non-visible ' if visible == :hidden
  end

  desc << "#{label} #{locator.inspect}"

  if show_for[:any]
    desc << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
    desc << " with exact text #{exact_text}" if exact_text.is_a?(String)
  end

  desc << " with id #{options[:id]}" if options[:id]
  desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
  desc << ' that is focused' if options[:focused]
  desc << ' that is not focused' if options[:focused] == false

  desc << case options[:style]
  when String
    " with style attribute #{options[:style].inspect}"
  when Regexp
    " with style attribute matching #{options[:style].inspect}"
  when Hash
    " with styles #{options[:style].inspect}"
  else ''
  end

  %i[above below left_of right_of near].each do |spatial_filter|
    if options[spatial_filter] && show_for[:spatial]
      desc << " #{spatial_filter} #{options[spatial_filter] rescue '<ERROR>'}" # rubocop:disable Style/RescueModifier
    end
  end

  desc << selector.description(node_filters: show_for[:node], **options)

  desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]

  desc << " within #{@resolved_node.inspect}" if describe_within?
  if locator.is_a?(String) && locator.start_with?('#', './/', '//') && !selector.raw_locator?
    desc << "\nNote: It appears you may be passing a CSS selector or XPath expression rather than a locator. " \
            "Please see the documentation for acceptable locator values.\n\n"
  end
  desc
end
exact?() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 137
def exact?
  supports_exact? ? options.fetch(:exact, session_options.exact) : false
end
failure_message() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 178
def failure_message
  +"expected to find #{applied_description}" << count_message
end
label() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 62
def label; selector.label || selector.name; end
match() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 141
def match
  options.fetch(:match, session_options.match)
end
matches_filters?(node, node_filter_errors = []) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 117
def matches_filters?(node, node_filter_errors = [])
  return true if (@resolved_node&.== node) && options[:allow_self]

  matches_locator_filter?(node) &&
    matches_system_filters?(node) &&
    matches_spatial_filters?(node) &&
    matches_node_filters?(node, node_filter_errors) &&
    matches_filter_block?(node)
rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
  false
end
name() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 61
def name; selector.name; end
negative_failure_message() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 182
def negative_failure_message
  +"expected not to find #{applied_description}" << count_message
end
resolve_for(node, exact = nil) click to toggle source

@api private

# File lib/capybara/queries/selector_query.rb, line 159
def resolve_for(node, exact = nil)
  applied_filters.clear
  @filter_cache.clear
  @resolved_node = node
  @resolved_count += 1

  node.synchronize do
    children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
    Capybara::Result.new(ordered_results(children), self)
  end
end
supports_exact?() click to toggle source

@api private

# File lib/capybara/queries/selector_query.rb, line 172
def supports_exact?
  return @expression.respond_to? :to_xpath if @selector.supports_exact?.nil?

  @selector.supports_exact?
end
visible() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 129
def visible
  case (vis = options.fetch(:visible) { default_visibility })
  when true then :visible
  when false then :all
  else vis
  end
end
xpath(exact = nil) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 145
def xpath(exact = nil)
  exact = exact? if exact.nil?
  expr = apply_expression_filters(@expression)
  expr = exact ? expr.to_xpath(:exact) : expr.to_s if expr.respond_to?(:to_xpath)
  expr = filtered_expression(expr)
  expr = "(#{expr})[#{xpath_text_conditions}]" if try_text_match_in_expression?
  expr
end

Private Instance Methods

applied_filters() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 230
def applied_filters
  @applied_filters ||= []
end
apply_expression_filters(expression) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 390
def apply_expression_filters(expression)
  unapplied_options = options.keys - valid_keys
  expression_filters.inject(expression) do |expr, (name, ef)|
    next expr unless apply_filter?(ef)

    if ef.matcher?
      unapplied_options.select(&ef.method(:handles_option?)).inject(expr) do |memo, option_name|
        unapplied_options.delete(option_name)
        ef.apply_filter(memo, option_name, options[option_name], @selector)
      end
    elsif options.key?(name)
      unapplied_options.delete(name)
      ef.apply_filter(expr, name, options[name], @selector)
    elsif ef.default?
      ef.apply_filter(expr, name, ef.default, @selector)
    else
      expr
    end
  end
end
apply_filter?(filter) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 433
def apply_filter?(filter)
  filter.format.nil? || (filter.format == selector_format)
end
assert_valid_keys() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 344
def assert_valid_keys
  unless VALID_MATCH.include?(match)
    raise ArgumentError, "Invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(', ')}"
  end

  unhandled_options = @options.keys.reject do |option_name|
    valid_keys.include?(option_name) ||
      expression_filters.any? { |_name, ef| ef.handles_option? option_name } ||
      node_filters.any? { |_name, nf| nf.handles_option? option_name }
  end

  return if unhandled_options.empty?

  invalid_names = unhandled_options.map(&:inspect).join(', ')
  valid_names = (valid_keys - [:allow_self]).map(&:inspect).join(', ')
  raise ArgumentError, "Invalid option(s) #{invalid_names}, should be one of #{valid_names}"
end
builder(expr) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 613
def builder(expr)
  selector.builder(expr)
end
custom_keys() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 340
def custom_keys
  @custom_keys ||= node_filters.keys + expression_filters.keys
end
default_visibility() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 609
def default_visibility
  @selector.default_visibility(session_options.ignore_hidden_elements, options)
end
describe_within?() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 421
def describe_within?
  @resolved_node && !document?(@resolved_node) && !simple_root?(@resolved_node)
end
document?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 425
def document?(node)
  node.is_a?(::Capybara::Node::Document)
end
exact_text() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 417
def exact_text
  options.fetch(:exact_text, session_options.exact_text)
end
expression_filters() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 325
def expression_filters
  filters = @selector.expression_filters
  filters.merge filter_set(options[:filter_set]).expression_filters if options.key?(:filter_set)
  filters
end
filter_set(name) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 313
def filter_set(name)
  ::Capybara::Selector::FilterSet[name]
end
filtered_expression(expr) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 362
def filtered_expression(expr)
  conditions = {}
  conditions[:id] = options[:id] if use_default_id_filter?
  conditions[:class] = options[:class] if use_default_class_filter?
  conditions[:style] = options[:style] if use_default_style_filter? && !options[:style].is_a?(Hash)
  builder(expr).add_attribute_conditions(**conditions)
end
find_nodes_by_selector_format(node, exact) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 241
def find_nodes_by_selector_format(node, exact)
  hints = {}
  hints[:uses_visibility] = true unless visible == :all
  hints[:texts] = text_fragments unless selector_format == :xpath
  hints[:styles] = options[:style] if use_default_style_filter?
  hints[:position] = true if use_spatial_filter?

  case selector_format
  when :css
    if node.method(:find_css).arity == 1
      node.find_css(css)
    else
      node.find_css(css, **hints)
    end
  when :xpath
    if node.method(:find_xpath).arity == 1
      node.find_xpath(xpath(exact))
    else
      node.find_xpath(xpath(exact), **hints)
    end
  else
    raise ArgumentError, "Unknown format: #{selector_format}"
  end
end
find_selector(locator) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 234
def find_selector(locator)
  case locator
  when Symbol then Selector[locator]
  else Selector.for(locator)
  end || Selector[session_options.default_selector]
end
first_try?() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 220
def first_try?
  @resolved_count == 1
end
matches_class_filter?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 497
def matches_class_filter?(node)
  return true unless use_default_class_filter? && need_to_process_classes?

  if options[:class].is_a? Regexp
    options[:class].match? node[:class]
  else
    classes = (node[:class] || '').split
    options[:class].select { |c| c.is_a? Regexp }.all? do |r|
      classes.any? { |cls| r.match? cls }
    end
  end
end
matches_exact_text_filter?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 556
def matches_exact_text_filter?(node)
  case exact_text
  when String, Regexp
    matches_text_exactly?(node, exact_text)
  else
    true
  end
end
matches_filter_block?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 303
def matches_filter_block?(node)
  return true unless @filter_block

  if node.respond_to?(:session)
    node.session.using_wait_time(0) { @filter_block.call(node) }
  else
    @filter_block.call(node)
  end
end
matches_focused_filter?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 510
def matches_focused_filter?(node)
  return true unless use_default_focused_filter?

  (node == node.session.active_element) == options[:focused]
end
matches_id_filter?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 491
def matches_id_filter?(node)
  return true unless use_default_id_filter? && options[:id].is_a?(Regexp)

  options[:id].match? node[:id]
end
matches_locator_filter?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 437
def matches_locator_filter?(node)
  return true unless @selector.locator_filter && apply_filter?(@selector.locator_filter)

  @selector.locator_filter.matches?(node, @locator, @selector, exact: exact?)
end
matches_node_filters?(node, errors) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 278
def matches_node_filters?(node, errors)
  applied_filters << :node

  unapplied_options = options.keys - valid_keys
  @selector.with_filter_errors(errors) do
    node_filters.all? do |filter_name, filter|
      next true unless apply_filter?(filter)

      if filter.matcher?
        unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
          unapplied_options.delete(option_name)
          filter.matches?(node, option_name, options[option_name], @selector)
        end
      elsif options.key?(filter_name)
        unapplied_options.delete(filter_name)
        filter.matches?(node, filter_name, options[filter_name], @selector)
      elsif filter.default?
        filter.matches?(node, filter_name, filter.default, @selector)
      else
        true
      end
    end
  end
end
matches_spatial_filters?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 455
def matches_spatial_filters?(node)
  applied_filters << :spatial
  return true unless use_spatial_filter?

  node_rect = Rectangle.new(node.initial_cache[:position] || node.rect)

  if options[:above]
    el_rect = rect_cache(options[:above])
    return false unless node_rect.above? el_rect
  end

  if options[:below]
    el_rect = rect_cache(options[:below])
    return false unless node_rect.below? el_rect
  end

  if options[:left_of]
    el_rect = rect_cache(options[:left_of])
    return false unless node_rect.left_of? el_rect
  end

  if options[:right_of]
    el_rect = rect_cache(options[:right_of])
    return false unless node_rect.right_of? el_rect
  end

  if options[:near]
    return false if node == options[:near]

    el_rect = rect_cache(options[:near])
    return false unless node_rect.near? el_rect
  end

  true
end
matches_style?(node, styles) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 536
def matches_style?(node, styles)
  @actual_styles = node.initial_cache[:style] || node.style(*styles.keys)
  styles.all? do |style, value|
    if value.is_a? Regexp
      value.match? @actual_styles[style.to_s]
    else
      @actual_styles[style.to_s] == value
    end
  end
end
matches_style_filter?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 525
def matches_style_filter?(node)
  case options[:style]
  when String, nil
    true
  when Regexp
    options[:style].match? node[:style]
  when Hash
    matches_style?(node, options[:style])
  end
end
matches_system_filters?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 443
def matches_system_filters?(node)
  applied_filters << :system

  matches_visibility_filters?(node) &&
    matches_id_filter?(node) &&
    matches_class_filter?(node) &&
    matches_style_filter?(node) &&
    matches_focused_filter?(node) &&
    matches_text_filter?(node) &&
    matches_exact_text_filter?(node)
end
matches_text_exactly?(node, value) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 590
def matches_text_exactly?(node, value)
  regexp = value.is_a?(Regexp) ? value : /\A#{Regexp.escape(value.to_s)}\z/
  matches_text_regexp(node, regexp).then { |m| m&.pre_match == '' && m&.post_match == '' }
end
matches_text_filter?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 547
def matches_text_filter?(node)
  value = options[:text]
  return true unless value
  return matches_text_exactly?(node, value) if exact_text == true && !value.is_a?(Regexp)

  regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
  matches_text_regexp?(node, regexp)
end
matches_text_regexp(node, regexp) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 599
def matches_text_regexp(node, regexp)
  text_visible = visible
  text_visible = :all if text_visible == :hidden
  node.text(text_visible, normalize_ws: normalize_ws).match(regexp)
end
matches_text_regexp?(node, regexp) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 605
def matches_text_regexp?(node, regexp)
  !matches_text_regexp(node, regexp).nil?
end
matches_visibility_filters?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 565
def matches_visibility_filters?(node)
  obscured = options[:obscured]
  return (visible != :hidden) && (node.initial_cache[:visible] != false) && !node.obscured? if obscured == false

  vis = case visible
  when :visible
    node.initial_cache[:visible] || (node.initial_cache[:visible].nil? && node.visible?)
  when :hidden
    # TODO: check why the 'visbile' cache spelling mistake wasn't caught in a test
    # (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
    (node.initial_cache[:visible] == false) || (node.initial_cache[:visible].nil? && !node.visible?)
  else
    true
  end

  vis && case obscured
         when true
           node.obscured?
         when false
           !node.obscured?
         else
           true
         end
end
matching_text() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 192
def matching_text
  options[:text] || options[:exact_text]
end
need_to_process_classes?() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 516
def need_to_process_classes?
  case options[:class]
  when Regexp then true
  when Array then options[:class].any?(Regexp)
  else
    false
  end
end
node_filters() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 317
def node_filters
  if options.key?(:filter_set)
    filter_set(options[:filter_set])
  else
    @selector
  end.node_filters
end
normalize_ws() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 595
def normalize_ws
  options.fetch(:normalize_ws, session_options.default_normalize_ws)
end
ordered_results(results) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 331
def ordered_results(results)
  case @order
  when :reverse
    results.reverse
  else
    results
  end
end
position_cache(key) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 617
def position_cache(key)
  @filter_cache[key][:position] ||= key.rect
end
rect_cache(key) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 621
def rect_cache(key)
  @filter_cache[key][:rect] ||= Rectangle.new(position_cache(key))
end
selector_format() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 188
def selector_format
  @selector.format
end
show_for_stage(only_applied) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 224
def show_for_stage(only_applied)
  lambda do |stage = :any|
    !only_applied || (stage == :any ? applied_filters.any? : applied_filters.include?(stage))
  end
end
simple_root?(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 429
def simple_root?(node)
  node.is_a?(::Capybara::Node::Simple) && node.path == '/'
end
text_fragments() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 196
def text_fragments
  (text = matching_text).is_a?(String) ? text.split : []
end
to_element(node) click to toggle source
# File lib/capybara/queries/selector_query.rb, line 266
def to_element(node)
  if @resolved_node.is_a?(Capybara::Node::Base)
    Capybara::Node::Element.new(@resolved_node.session, node, @resolved_node, self)
  else
    Capybara::Node::Simple.new(node)
  end
end
try_text_match_in_expression?() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 213
def try_text_match_in_expression?
  first_try? &&
    matching_text &&
    @resolved_node.is_a?(Capybara::Node::Base) &&
    @resolved_node.session&.driver&.wait?
end
use_default_class_filter?() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 374
def use_default_class_filter?
  options.key?(:class) && !custom_keys.include?(:class)
end
use_default_focused_filter?() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 382
def use_default_focused_filter?
  options.key?(:focused) && !custom_keys.include?(:focused)
end
use_default_id_filter?() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 370
def use_default_id_filter?
  options.key?(:id) && !custom_keys.include?(:id)
end
use_default_style_filter?() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 378
def use_default_style_filter?
  options.key?(:style) && !custom_keys.include?(:style)
end
use_spatial_filter?() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 386
def use_spatial_filter?
  options.values_at(*SPATIAL_KEYS).compact.any?
end
valid_keys() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 274
def valid_keys
  (VALID_KEYS + custom_keys).uniq
end
warn_exact_usage() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 411
def warn_exact_usage
  return unless options.key?(:exact) && !supports_exact?

  warn "The :exact option only has an effect on queries using the XPath#is method. Using it with the query \"#{expression}\" has no effect."
end
xpath_text_conditions() click to toggle source
# File lib/capybara/queries/selector_query.rb, line 200
def xpath_text_conditions
  case (text = matching_text)
  when String
    text.split.map { |txt| XPath.contains(txt) }.reduce(&:&)
  when Regexp
    condition = XPath.current
    condition = condition.uppercase if text.casefold?
    Selector::RegexpDisassembler.new(text).alternated_substrings.map do |strs|
      strs.flat_map(&:split).map { |str| condition.contains(str) }.reduce(:&)
    end.reduce(:|)
  end
end