class Watir::Locators::Element::SelectorBuilder::XPath

Constants

CAN_NOT_BUILD
LOCATOR

Public Instance Methods

build(selector) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 13
def build(selector)
  @selector = selector
  @valid_attributes = build_valid_attributes

  @built = (@selector.keys & CAN_NOT_BUILD).each_with_object({}) do |key, hash|
    hash[key] = @selector.delete(key)
  end

  index = @selector.delete(:index)
  @adjacent = @selector.delete(:adjacent)
  @scope = @selector.delete(:scope)

  xpath = start_string
  xpath << adjacent_string
  xpath << tag_string
  xpath << class_string
  xpath << text_string
  xpath << additional_string
  xpath << label_element_string
  xpath << attribute_string

  @built[:xpath] = index ? add_index(xpath, index) : xpath
  @built
end

Private Instance Methods

add_index(xpath, index) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 168
def add_index(xpath, index)
  if @adjacent
    "#{xpath}[#{index + 1}]"
  elsif index&.positive? && @built.empty?
    "(#{xpath})[#{index + 1}]"
  elsif index&.negative? && @built.empty?
    last_value = 'last()'
    last_value << (index + 1).to_s if index < -1
    "(#{xpath})[#{last_value}]"
  else
    @built[:index] = index
    xpath
  end
end
add_to_matching(key, regexp, results = nil) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 191
def add_to_matching(key, regexp, results = nil)
  return unless results.nil? || requires_matching?(results, regexp)

  if key == :class
    @built[key] << regexp
  else
    @built[key] = regexp
  end
end
additional_string() click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 163
def additional_string
  # to be used by subclasses as necessary
  ''
end
adjacent_string() click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 87
def adjacent_string
  case @adjacent
  when nil
    ''
  when :ancestor
    'ancestor::*'
  when :preceding
    'preceding-sibling::*'
  when :following
    'following-sibling::*'
  when :child
    'child::*'
  else
    raise LocatorException, "Unable to process adjacent locator with #{@adjacent}"
  end
end
attribute_absence(attribute) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 229
def attribute_absence(attribute)
  lhs = lhs_for(attribute)
  "not(#{lhs})"
end
attribute_presence(attribute) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 225
def attribute_presence(attribute)
  lhs_for(attribute)
end
attribute_string() click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 156
def attribute_string
  attributes = @selector.keys.map { |key|
    process_attribute(key, @selector.delete(key))
  }.flatten.compact
  attributes.empty? ? '' : "[#{attributes.join(' and ')}]"
end
build_valid_attributes() click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 251
def build_valid_attributes
  tag_name = @selector[:tag_name]
  if tag_name.is_a?(String) && Watir.tag_to_class[tag_name.to_sym]
    Watir.tag_to_class[tag_name.to_sym].attribute_list
  else
    Watir::HTMLElement.attribute_list
  end
end
case_insensitive_attribute?(attribute) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 260
def case_insensitive_attribute?(attribute)
  # type attributes can be upper case - downcase them
  # https://github.com/watir/watir/issues/72
  return true if attribute == :type
  return true if Watir::Element::CASE_INSENSITIVE_ATTRIBUTES.include?(attribute) &&
                 @valid_attributes.include?(attribute)

  false
end
class_string() click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 109
def class_string
  class_name = @selector.delete(:class)
  return '' if class_name.nil?

  @built[:class] = []

  predicates = [class_name].flatten.map { |value| process_attribute(:class, value) }.compact

  @built.delete(:class) if @built[:class].empty?

  predicates.empty? ? '' : "[#{predicates.join(' and ')}]"
end
equal_pair(key, value) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 234
def equal_pair(key, value)
  if key == :class
    negate_xpath = value =~ /^!/ && value.slice!(0)
    expression = "contains(concat(' ', @class, ' '), #{XpathSupport.escape " #{value} "})"

    negate_xpath ? "not(#{expression})" : expression
  else
    downcase = case_insensitive_attribute?(key)

    lhs = lhs_for(key, downcase: downcase)
    rhs = XpathSupport.escape(value)
    rhs = XpathSupport.downcase(rhs) if downcase

    "#{lhs}=#{rhs}"
  end
end
label_element_string() click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 136
def label_element_string
  label = @selector.delete :label_element

  return '' if label.nil?

  key = label.is_a?(Regexp) ? :contains_text : :text

  value = process_attribute(key, label)

  @built[:label_element] = @built.delete :contains_text if @built.key?(:contains_text)

  # TODO: This conditional can be removed when we remove this deprecation
  if label.is_a?(Regexp)
    @built[:label_element] = label
    ''
  else
    "[@id=//label[#{value}]/@for or parent::label[#{value}]]"
  end
end
lhs_for(key, downcase: false) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 205
def lhs_for(key, downcase: false)
  case key
  when String
    process_string key
  when :tag_name
    'local-name()'
  when :href
    'normalize-space(@href)'
  when :text
    'normalize-space()'
  when :contains_text
    'normalize-space()'
  when ::Symbol
    lhs = process_string key.to_s.tr('_', '-')
    downcase ? XpathSupport.downcase(lhs) : lhs
  else
    raise LocatorException, "Unable to build XPath using #{key}:#{key.class}"
  end
end
predicate_conversion(key, regexp) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 59
def predicate_conversion(key, regexp)
  downcase = case_insensitive_attribute?(key) || regexp.casefold?

  lhs = lhs_for(key, downcase: downcase)

  results = RegexpDisassembler.new(regexp).substrings

  if results.empty?
    add_to_matching(key, regexp)
    return lhs
  elsif results.size == 1 && starts_with?(results, regexp) && !visible?
    return "starts-with(#{lhs}, '#{results.first}')"
  end

  add_to_matching(key, regexp, results)

  results.map { |rhs|
    rhs = "'#{rhs}'"
    rhs = XpathSupport.downcase(rhs) if downcase
    "contains(#{lhs}, #{rhs})"
  }.flatten.compact.join(' and ')
end
predicate_expression(key, val) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 48
def predicate_expression(key, val)
  case val
  when true
    attribute_presence(key)
  when false
    attribute_absence(key)
  else
    equal_pair(key, val)
  end
end
process_attribute(key, value) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 40
def process_attribute(key, value)
  if value.is_a? Regexp
    predicate_conversion(key, value)
  else
    predicate_expression(key, value)
  end
end
process_string(name) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 270
def process_string(name)
  if name =~ /^[a-zA-Z_:][a-zA-Z0-9_:.\-]*$/
    "@#{name}"
  else
    "(attribute::*[local-name(.) = '#{name}'])"
  end
end
requires_matching?(results, regexp) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 201
def requires_matching?(results, regexp)
  regexp.casefold? ? !results.first.casecmp(regexp.source).zero? : results.first != regexp.source
end
start_string() click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 82
def start_string
  start = @adjacent ? './' : './/*'
  @scope ? "(#{@scope[:xpath]})[1]#{start.tr('.', '')}" : start
end
starts_with?(results, regexp) click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 187
def starts_with?(results, regexp)
  regexp.source[0] == '^' && results.first == regexp.source[1..-1]
end
tag_string() click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 104
def tag_string
  tag_name = @selector.delete(:tag_name)
  tag_name.nil? ? '' : "[#{process_attribute(:tag_name, tag_name)}]"
end
text_string() click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 122
def text_string
  text = @selector.delete :text

  case text
  when nil
    ''
  when Regexp
    @built[:text] = text
    ''
  else
    "[#{predicate_expression(:text, text)}]"
  end
end
visible?() click to toggle source
# File lib/watir/locators/element/selector_builder/xpath.rb, line 183
def visible?
  !(@built.keys & CAN_NOT_BUILD).empty?
end