module Watir module Locators class Element class SelectorBuilder class XPath def initialize(should_use_label_element) @should_use_label_element = should_use_label_element end def build(selectors) adjacent = selectors.delete :adjacent xpath = adjacent ? process_adjacent(adjacent) : './/*' tag_name = selectors.delete(:tag_name).to_s xpath << "[local-name()='#{tag_name}']" unless tag_name.empty? index = selectors.delete(:index) # the remaining entries should be attributes xpath << '[' << attribute_expression(nil, selectors) << ']' unless selectors.empty? xpath << "[#{index + 1}]" if adjacent && index p xpath: xpath, selectors: selectors if $DEBUG [:xpath, xpath] end # @todo Get rid of building def attribute_expression(building, selectors) f = selectors.map do |key, val| if val.is_a?(Array) && key == :class '(' + val.map { |v| build_class_match(v) }.join(' and ') + ')' elsif val.is_a?(Array) '(' + val.map { |v| equal_pair(building, key, v) }.join(' or ') + ')' elsif val.eql? true attribute_presence(key) elsif val.eql? false attribute_absence(key) else equal_pair(building, key, val) end end f.join(' and ') end # @todo Get rid of building def equal_pair(building, key, value) if key == :class if value.strip.include?(' ') dep = "Using the :class locator to locate multiple classes with a String value (i.e. \"#{value}\")" Watir.logger.deprecate dep, "Array (e.g. #{value.split})", ids: [:class_array] end build_class_match(value) elsif key == :label && @should_use_label_element # we assume :label means a corresponding label element, not the attribute text = "normalize-space()=#{XpathSupport.escape value}" "(@id=//label[#{text}]/@for or parent::label[#{text}])" else "#{lhs_for(building, key)}=#{XpathSupport.escape value}" end end # @todo Get rid of building def lhs_for(_building, key) case key when :text, 'text' 'normalize-space()' when String "@#{key}" when :href # TODO: change this behaviour? 'normalize-space(@href)' when :type # type attributes can be upper case - downcase them # https://github.com/watir/watir/issues/72 XpathSupport.downcase('@type') when ::Symbol "@#{key.to_s.tr('_', '-')}" else raise Error::Exception, "Unable to build XPath using #{key}" end end private def process_adjacent(adjacent) xpath = './' xpath << case adjacent when :ancestor 'ancestor::*' when :preceding 'preceding-sibling::*' when :following 'following-sibling::*' when :child 'child::*' end xpath end def build_class_match(value) if value =~ /^!/ klass = XpathSupport.escape " #{value[1..-1]} " "not(contains(concat(' ', @class, ' '), #{klass}))" else klass = XpathSupport.escape " #{value} " "contains(concat(' ', @class, ' '), #{klass})" end end def attribute_presence(attribute) lhs_for(nil, attribute) end def attribute_absence(attribute) "not(#{lhs_for(nil, attribute)})" end end end end end end