lib/watir/locators/element/locator.rb in watir-6.15.1 vs lib/watir/locators/element/locator.rb in watir-6.16.0

- old
+ new

@@ -2,206 +2,63 @@ module Locators class Element class Locator include Exception - attr_reader :selector_builder - attr_reader :element_validator + attr_reader :element_matcher, :driver_scope - W3C_FINDERS = %i[ - css - link - link_text - partial_link_text - tag_name - xpath - ].freeze - - def initialize(query_scope, selector, selector_builder, element_validator) - @query_scope = query_scope # either element or browser - @selector = selector - @selector_builder = selector_builder - @element_validator = element_validator + def initialize(element_matcher) + @query_scope = element_matcher.query_scope + @selector = element_matcher.selector + @element_matcher = element_matcher end - def locate - using_selenium(:first) || using_watir(:first) - rescue Selenium::WebDriver::Error::NoSuchElementError, Selenium::WebDriver::Error::StaleElementReferenceError + def locate(built) + @built = built.dup + @driver_scope = (@built.delete(:scope) || @query_scope.browser).wd + matching_elements(@built, :first) + rescue Selenium::WebDriver::Error::NoSuchElementError nil end - def locate_all - return [@selector[:element]] if @selector.key?(:element) + def locate_all(built) + @built = built.dup + @driver_scope = (@built.delete(:scope) || @query_scope.browser).wd + raise ArgumentError, "can't locate all elements by :index" if built.key?(:index) - using_selenium(:all) || using_watir(:all) + [matching_elements(@built, :all)].flatten end private - def using_selenium(filter = :first) - selector = @selector.dup - tag = selector[:tag_name].is_a?(::Symbol) ? selector.delete(:tag_name).to_s : selector.delete(:tag_name) - return if selector.size > 1 + def matching_elements(built, filter) + return locate_element(*built.to_a.flatten) if built.size == 1 && filter == :first - how = selector.keys.first || :tag_name - what = selector.values.first || tag + wd_locator_key = (Watir::Locators::W3C_FINDERS & built.keys).first + wd_locator = built.select { |k, _v| wd_locator_key == k } + match_values = built.reject { |k, _v| wd_locator_key == k } - return unless wd_supported?(how, what, tag) - - filter == :all ? locate_elements(how, what) : locate_element(how, what) - end - - def using_watir(filter = :first) - selector = @selector.dup - raise ArgumentError, "can't locate all elements by :index" if selector.key?(:index) && filter == :all - - @driver_scope ||= @query_scope.wd - - built = selector_builder.build(selector) - - validate_built_selector(built) - - wd_locator = built.select { |key, _value| %i[css xpath link_text partial_link_text].include? key } - values_to_match = built.reject { |key, _value| %i[css xpath link_text partial_link_text].include? key } - - if filter == :all || values_to_match.any? - locate_matching_elements(wd_locator, values_to_match, filter) - else - locate_element(wd_locator.keys.first, wd_locator.values.first, @driver_scope) - end - end - - def validate_built_selector(built) - return unless built.nil? || built.empty? - - msg = "#{selector_builder.class} was unable to build selector from #{@selector.inspect}" - raise LocatorException, msg - end - - def fetch_value(element, how) - case how - when :text - element.text - when :visible - element.displayed? - when :visible_text - element.text - when :tag_name - element.tag_name.downcase - when :href - element.attribute('href')&.strip - else - how = how.to_s.tr('_', '-') if how.is_a?(::Symbol) - element.attribute(how) - end - end - - def matching_labels(elements, values_to_match, scope) - label_key = values_to_match.key?(:label_element) ? :label_element : :visible_label_element - label_value = values_to_match.delete(:label_element) || values_to_match.delete(:visible_label_element) - locator_key = label_key.to_s.gsub('label', 'text').gsub('_element', '').to_sym - - Watir::LabelCollection.new(scope, tag_name: 'label').map { |label| - next unless matches_values?(label.wd, locator_key => label_value) - - input = label.for.empty? ? label.input : Watir::Input.new(scope, id: label.for) - input.wd if elements.include?(input.wd) - }.compact - end - - def matching_elements(elements, values_to_match, filter: :first) - if filter == :first - idx = element_index(elements, values_to_match) - counter = 0 - - # Lazy evaluation to avoid fetching values for elements that will be discarded - matches = elements.lazy.select do |el| - counter += 1 - matches_values?(el, values_to_match) - end - msg = "iterated through #{counter} elements to locate #{@selector.inspect}" - matches.take(idx + 1).to_a[idx].tap { Watir.logger.debug msg } - else - Watir.logger.debug "Iterated through #{elements.size} elements to locate all #{@selector.inspect}" - elements.select { |el| matches_values?(el, values_to_match) } - end - end - - def element_index(elements, values_to_match) - idx = values_to_match.delete(:index) || 0 - return idx unless idx.negative? - - elements.reverse! - idx.abs - 1 - end - - def matches_values?(element, values_to_match) - matches = values_to_match.all? do |how, what| - if how == :tag_name && what.is_a?(String) - element_validator.validate(element, what) - else - val = fetch_value(element, how) - what == val || val =~ /#{what}/ - end - end - - text_regexp_deprecation(element, values_to_match, matches) if values_to_match[:text] - - matches - end - - def text_regexp_deprecation(element, selector, matches) - new_element = Watir::Element.new(@query_scope, element: element) - text_content = new_element.execute_js(:getTextContent, element).strip - text_content_matches = text_content =~ /#{selector[:text]}/ - return if matches == !!text_content_matches - - key = @selector.key?(:text) ? 'text' : 'label' - selector_text = selector[:text].inspect - dep = "Using :#{key} locator with RegExp #{selector_text} to match an element that includes hidden text" - Watir.logger.deprecate(dep, ":visible_#{key}", ids: [:text_regexp]) - end - - def locate_element(how, what, scope = @query_scope.wd) - scope.find_element(how, what) - end - - def locate_elements(how, what, scope = @query_scope.wd) - scope.find_elements(how, what) - end - - def locate_matching_elements(selector, values_to_match, filter) + # TODO: Wrap this to continue trying until default timeout retries = 0 begin - elements = locate_elements(selector.keys.first, selector.values.first, @driver_scope) || [] - if values_to_match.key?(:label_element) || values_to_match.key?(:visible_label_element) - elements = matching_labels(elements, values_to_match, @query_scope) - end - matching_elements(elements, values_to_match, filter: filter) + elements = locate_elements(*wd_locator.to_a.flatten) + + element_matcher.match(elements, match_values, filter) rescue Selenium::WebDriver::Error::StaleElementReferenceError retries += 1 sleep 0.5 retry unless retries > 2 target = filter == :all ? 'element collection' : 'element' raise LocatorException, "Unable to locate #{target} from #{@selector} due to changing page" end end - def wd_supported?(how, what, tag) - return false unless W3C_FINDERS.include? how - return false unless what.is_a?(String) + def locate_element(how, what, scope = driver_scope) + scope.find_element(how, what) + end - if %i[partial_link_text link_text link].include?(how) - Watir.logger.deprecate(":#{how} locator", ':visible_text', ids: [:link_text]) - return true if [:a, :link, nil].include?(tag) - - raise LocatorException, "Can not use #{how} locator to find a #{what} element" - elsif how == :tag_name - return true - else - return false unless tag.nil? - end - true + def locate_elements(how, what, scope = driver_scope) + scope.find_elements(how, what) end end end end end