require_relative 'locator/scroll_actions' module TestaAppiumDriver #noinspection RubyTooManyInstanceVariablesInspection,RubyTooManyMethodsInspection class Locator include Helpers attr_accessor :xpath_selector attr_accessor :single attr_accessor :driver attr_accessor :strategy attr_accessor :strategy_reason # @type [Boolean] used to determine if last selector was one of siblings or children. Only in those selectors we can reliably use xpath array [instance] selector attr_accessor :last_selector_adjacent attr_accessor :can_use_id_strategy attr_accessor :from_element attr_accessor :scroll_orientation attr_accessor :scroll_deadzone attr_accessor :scrollable_locator attr_accessor :default_find_strategy attr_accessor :default_scroll_strategy # locator parameters are: # single: true or false # scrollable_locator: [TestaAppiumDriver::Locator, nil] for scrolling if needed later # default_find_strategy: default strategy if find element strategy is not enforced # default_scroll_strategy: default strategy for scrolling if not enforced # # @param [TestaAppiumDriver::Driver] driver # @param [TestaAppiumDriver::Driver, TestaAppiumDriver::Locator, Selenium::WebDriver::Element] from_element from which element to execute the find_element # @param [Hash] params selectors and params for locator def initialize(driver, from_element, params = {}) # @type [TestaAppiumDriver::Driver] @driver = driver params, selectors = extract_selectors_from_params(params) single = params[:single] @single = single selectors[:id] = selectors[:name] unless selectors[:name].nil? if from_element.instance_of?(Selenium::WebDriver::Element) @xpath_selector = "//*" # to select current element @xpath_selector += hash_to_xpath(@driver.device, selectors, single)[1..-1] else @xpath_selector = hash_to_xpath(@driver.device, selectors, single) end @from_element = from_element @default_find_strategy = params[:default_find_strategy] @default_scroll_strategy = params[:default_scroll_strategy] @can_use_id_strategy = selectors.keys.count == 1 && !selectors[:id].nil? if @can_use_id_strategy if @driver.device == :android @can_use_id_strategy = resolve_id(selectors[:id]) else @can_use_id_strategy = selectors[:id] end end @strategy = params[:strategy] @strategy_reason = params[:strategy_reason] @last_selector_adjacent = false init(params, selectors, single) end # method missing is used to fetch the element before executing additional commands like click, send_key, count def method_missing(method, *args, &block) r = execute.send(method, *args, &block) @driver.invalidate_cache r end # @param [Boolean] skip_cache if true it will skip cache check and store # @param [Selenium::WebDriver::Element] force_cache_element, for internal use where we have already the element, and want to execute custom locator methods on it # @return [Selenium::WebDriver::Element, Array] def execute(skip_cache: false, force_cache_element: nil) return force_cache_element unless force_cache_element.nil? # if we are looking for current element, then return from_element # for example when we have driver.element.elements[1].click # elements[2] will be resolved with xpath because we are looking for multiple elements from element # and since we are looking for instance 2, [](instance) method will return new "empty locator" # we are executing click on that "empty locator" so we have to return the instance 2 of elements for the click if @xpath_selector == "//*/*[1]" && @from_element.instance_of?(Selenium::WebDriver::Element) return @from_element end strategy, selector = strategy_and_selector @driver.execute(@from_element, selector, @single, strategy, @default_find_strategy, skip_cache) end # @param [Integer] timeout in seconds # @return [TestaAppiumDriver::Locator] def wait_until_exists(timeout = nil) timeout = @driver.get_timeouts["implicit"] / 1000 if timeout.nil? start_time = Time.now.to_f until exists? raise "wait until exists timeout exceeded" if start_time + timeout < Time.now.to_f sleep EXISTS_WAIT end self end # @param [Integer] timeout in seconds # @return [TestaAppiumDriver::Locator] def wait_while_exists(timeout = nil) timeout = @driver.get_timeouts["implicit"] / 1000 if timeout.nil? start_time = Time.now.to_f while exists? raise "wait until exists timeout exceeded" if start_time + timeout < Time.now.to_f sleep EXISTS_WAIT end self end # all timeouts are disabled before check, and enabled after check # @return [boolean] true if it exists in the page regardless if visible or not def exists? @driver.disable_wait_for_idle @driver.disable_implicit_wait found = true begin execute(skip_cache: true) rescue StandardError found = false end @driver.enable_implicit_wait @driver.enable_wait_for_idle found end # @return [TestaAppiumDriver::Locator] def first self[0] end # @return [TestaAppiumDriver::Locator] def second self[1] end # @return [TestaAppiumDriver::Locator] def third self[2] end # @return [TestaAppiumDriver::Locator] def last self[-1] end def [](instance) raise "Cannot add index selector to non-Array" if @single if ((@strategy.nil? && !@last_selector_adjacent) || @strategy == FIND_STRATEGY_UIAUTOMATOR) && instance >= 0 locator = self.dup locator.strategy = FIND_STRATEGY_UIAUTOMATOR locator.ui_selector = "#{@ui_selector}.instance(#{instance})" locator.single = true locator.can_use_id_strategy = false locator else from_element = self.execute[instance] params = {}.merge({single: true, scrollable_locator: @scrollable_locator}) #params[:strategy] = FIND_STRATEGY_XPATH #params[:strategy_reason] = "retrieved instance of a array" params[:default_find_strategy] = @default_find_strategy params[:default_scroll_strategy] = @default_scroll_strategy Locator.new(@driver, from_element, params) end end # @param [TestaAppiumDriver::Locator, Selenium::WebDriver::Element, Array] other #noinspection RubyNilAnalysis,RubyUnnecessaryReturnStatement def ==(other) elements = execute other = other.execute if other.kind_of?(TestaAppiumDriver::Locator) if elements.kind_of?(Array) return false unless other.kind_of?(Array) return false if other.count != elements.count return (elements - other).empty? else return false if other.kind_of?(Array) return elements == other end end def as_json { strategy: @strategy, default_strategy: @default_find_strategy, single: @single, context: @from_element.nil? ? nil : @from_element.to_s, uiautomator: defined?(self.ui_selector) ? ui_selector : nil, xpath: @xpath_selector, scrollable: @scrollable_locator.nil? ? nil : @scrollable_locator.to_s, scroll_orientation: @scroll_orientation, resolved: strategy_and_selector } end def to_s JSON.dump(as_json) end def to_ary [self.to_s] end # @return [TestaAppiumDriver::Locator] def as_scrollable(orientation: :vertical, top: nil, bottom: nil, right: nil, left: nil) @scroll_orientation = orientation if !top.nil? || !bottom.nil? || !right.nil? || !left.nil? @scroll_deadzone = {} @scroll_deadzone[:top] = top.to_f unless top.nil? @scroll_deadzone[:bottom] = bottom.to_f unless bottom.nil? @scroll_deadzone[:right] = right.to_f unless right.nil? @scroll_deadzone[:left] = left.to_f unless left.nil? end @scrollable_locator = self.dup self end def first_and_last_leaf @driver.first_and_last_leaf(execute) end def tap click end def click perform_driver_method(:click) end def send_key(*args) perform_driver_method(:send_keys, *args) end def clear perform_driver_method(:clear) end # Return parent element # @return [TestaAppiumDriver::Locator] def parent raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "parent") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil? raise "Cannot add parent selector to a retrieved instance of a class array" if (@xpath_selector == "//*" || @xpath_selector == "//*[1]") && !@from_element.nil? locator = self.dup locator.strategy = FIND_STRATEGY_XPATH locator.strategy_reason = "parent" locator.xpath_selector += "/.." locator.can_use_id_strategy = false locator end # Return all children elements # @return [TestaAppiumDriver::Locator] def children raise "Cannot add children selector to array" unless @single raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "children") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil? locator = self.dup locator.strategy = FIND_STRATEGY_XPATH locator.strategy_reason = "children" locator.xpath_selector += "/*" locator.single = false locator.last_selector_adjacent = true locator.can_use_id_strategy = false locator end # Return first child element # @return [TestaAppiumDriver::Locator] def child raise "Cannot add children selector to array" unless @single raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "child") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil? locator = self.dup locator.strategy = FIND_STRATEGY_XPATH locator.strategy_reason = "child" locator.xpath_selector += "/*[1]" locator.single = true locator.can_use_id_strategy = false locator end # @return [TestaAppiumDriver::Locator] def siblings raise "Cannot add siblings selector to array" unless @single raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "siblings") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil? raise "Cannot add siblings selector to a retrieved instance of a class array" if (@xpath_selector == "//*" || @xpath_selector == "//*[1]") && !@from_element.nil? locator = self.dup locator.strategy = FIND_STRATEGY_XPATH locator.strategy_reason = "siblings" locator.xpath_selector += "/../*[not(@index=\"#{index}\")]" locator.single = false locator.last_selector_adjacent = true locator.can_use_id_strategy = false locator end # @return [TestaAppiumDriver::Locator] def preceding_siblings raise "Cannot add preceding_siblings selector to array" unless @single raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "preceding_siblings") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil? raise "Cannot add preceding_siblings selector to a retrieved instance of a class array" if (@xpath_selector == "//*" || @xpath_selector == "//*[1]") && !@from_element.nil? locator = self.dup locator.strategy = FIND_STRATEGY_XPATH locator.strategy_reason = "preceding_siblings" locator.xpath_selector += "/../*[position() < #{index + 1}]" # position() starts from 1 locator.single = false locator.last_selector_adjacent = true locator.can_use_id_strategy = false locator end # @return [TestaAppiumDriver::Locator] def preceding_sibling raise "Cannot add preceding_sibling selector to array" unless @single raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "preceding_sibling") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil? raise "Cannot add preceding siblings selector to a retrieved instance of a class array" if (@xpath_selector == "//*" || @xpath_selector == "//*[1]") && !@from_element.nil? locator = self.dup locator.strategy = FIND_STRATEGY_XPATH locator.strategy_reason = "preceding_sibling" i = index locator.single = true return nil if i == 0 locator.xpath_selector += "/../*[@index=\"#{i - 1}\"]" locator.last_selector_adjacent = true locator.can_use_id_strategy = false locator end # @return [TestaAppiumDriver::Locator] def following_siblings raise "Cannot add following_siblings selector to array" unless @single raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "following_siblings") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil? raise "Cannot add following_siblings selector to a retrieved instance of a class array" if (@xpath_selector == "//*" || @xpath_selector == "//*[1]") && !@from_element.nil? locator = self.dup locator.strategy = FIND_STRATEGY_XPATH locator.strategy_reason = "following_siblings" locator.xpath_selector += "/../*[position() > #{index + 1}]" # position() starts from 1 locator.single = false locator.last_selector_adjacent = true locator.can_use_id_strategy = false locator end # @return [TestaAppiumDriver::Locator] def following_sibling raise "Cannot add following_sibling selector to array" unless @single raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "following_sibling") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil? raise "Cannot add following_sibling selector to a retrieved instance of a class array" if (@xpath_selector == "//*" || @xpath_selector == "//*[1]") && !@from_element.nil? locator = self.dup locator.strategy = FIND_STRATEGY_XPATH locator.strategy_reason = "following_sibling" i = index locator.single = true return nil if i == 0 locator.xpath_selector += "/../*[@index=\"#{i + 1}\"]" locator.last_selector_adjacent = true locator.can_use_id_strategy = false locator end private #noinspection RubyNilAnalysis def perform_driver_method(name, *args) elements = execute if elements.kind_of?(Array) elements.map { |e| e.send(name, *args) } else elements.send(name, *args) end end def add_xpath_child_selectors(locator, selectors, single) locator.single = false unless single # switching from single result to multiple locator.xpath_selector += hash_to_xpath(@driver.device, selectors, single) end end end