# frozen_string_literal: true #require 'em/pure_ruby' require 'appium_lib_core' require_relative 'common/bounds' require_relative 'common/exceptions/strategy_mix_exception' require_relative 'common/helpers' require_relative 'common/locator' require_relative 'common/scroll_actions' require_relative 'common/selenium_element' module TestaAppiumDriver class Driver include Helpers # @return [::Appium::Core::Base::Driver] the ruby_lib_core appium driver attr_accessor :driver # @return [String] iOS or Android attr_reader :device # @return [String] driver automation name (uiautomator2 or xcuitest) attr_reader :automation_name # custom options # - default_find_strategy: default strategy to be used for finding elements. Available strategies :uiautomator or :xpath # - default_scroll_strategy: default strategy to be used for scrolling. Available strategies: :uiautomator(android only), :w3c def initialize(opts = {}) @testa_opts = opts[:testa_appium_driver] || {} core = Appium::Core.for(opts) @driver = core.start_driver @automation_name = @driver.capabilities["automationName"].downcase.to_sym @device = @driver.capabilities.platform_name.downcase.to_sym extend_for(@device, @automation_name) handle_testa_opts invalidate_cache #disable_wait_for_idle #disable_implicit_wait ::Appium::Core::Element.set_driver(self, @driver.capabilities["udid"]) end # invalidates current find_element cache def invalidate_cache @cache = { strategy: nil, selector: nil, element: nil, from_element: nil, time: Time.at(0) } end # Executes the find_element with the resolved locator strategy and selector. Find_element might be skipped if cache is hit. # Cache stores last executed find_element with given selector, strategy and from_element. If given values are the same within # last 5 seconds element is retrieved from cache. # @param [TestaAppiumDriver::Locator, TestaAppiumDriver::Driver] from_element element from which start the search # @param [Boolean] single fetch single or multiple results # @param [Array] strategies_and_selectors array of usable strategies and selectors # @param [Boolean] skip_cache to skip checking and storing cache # @return [Selenium::WebDriver::Element, Array] element is returned if single is true, array otherwise def execute(from_element, single, strategies_and_selectors, skip_cache: false, ignore_implicit_wait: false) # if user wants to wait for element to exist, he can use wait_until_present start_time = Time.now.to_f ss_index = 0 # resolve from_element unique id, so that we can cache it properly from_element_id = from_element.instance_of?(TestaAppiumDriver::Locator) ? from_element.strategies_and_selectors : nil begin begin ss = strategies_and_selectors[ss_index % strategies_and_selectors.count] rescue ZeroDivisionError puts "aa" end ss_index +=1 puts "Executing #{from_element_id ? "from #{from_element.strategy}: #{from_element.strategies_and_selectors} => " : ""}#{ss.keys[0]}: #{ss.values[0]}" if @cache[:selector] != ss.values[0] || # cache miss, selector is different @cache[:time] + 5 <= Time.now || # cache miss, older than 5 seconds @cache[:strategy] != ss.keys[0] || # cache miss, different find strategy @cache[:from_element_id] != from_element_id || # cache miss, search is started from different element skip_cache # cache is skipped if ss.keys[0] == FIND_STRATEGY_IMAGE set_find_by_image_settings(ss.values[0].dup) if single execute_result = from_element.find_element_by_image(ss.values[0][:image]) else execute_result = from_element.find_elements_by_image(ss.values[0][:image]) end restore_set_by_image_settings else if single execute_result = from_element.find_element(ss) else execute_result = from_element.find_elements(ss) end end unless skip_cache @cache[:selector] = ss.values[0] @cache[:strategy] = ss.keys[0] @cache[:time] = Time.now @cache[:from_element_id] = from_element_id @cache[:element] = execute_result end else # this is a cache hit, use the element from cache execute_result = @cache[:element] puts "Using cache from #{@cache[:time].strftime("%H:%M:%S.%L")}, strategy: #{@cache[:strategy]}" end rescue => e #if (start_time + @implicit_wait_ms/1000 < Time.now.to_f && !ignore_implicit_wait) || ss_index < strategies_and_selectors.count if ss_index < strategies_and_selectors.count sleep EXISTS_WAIT if ss_index >= strategies_and_selectors.count retry else raise e end end execute_result end # method missing is used to forward methods to the actual appium driver # after the method is executed, find element cache is invalidated def method_missing(method, *args, &block) r = @driver.send(method, *args, &block) invalidate_cache r end # disables implicit wait def disable_implicit_wait @implicit_wait_ms = @driver.get_timeouts["implicit"].to_i @implicit_wait_ms = @implicit_wait_ms/1000 if @implicit_wait_ms > 100000 @implicit_wait_uiautomator_ms = @driver.get_settings["waitForSelectorTimeout"] @driver.manage.timeouts.implicit_wait = 0 @driver.update_settings({waitForSelectorTimeout: 0}) end # disables wait for idle, only executed for android devices def disable_wait_for_idle if @device == :android @wait_for_idle_timeout = @driver.settings.get["waitForIdleTimeout"] @driver.update_settings({waitForIdleTimeout: 0}) end end def set_find_by_image_settings(settings) settings.delete(:image) @default_find_image_settings = {} old_settings = @driver.get_settings @default_find_image_settings[:imageMatchThreshold] = old_settings["imageMatchThreshold"] @default_find_image_settings[:fixImageFindScreenshotDims] = old_settings["fixImageFindScreenshotDims"] @default_find_image_settings[:fixImageTemplateSize] = old_settings["fixImageTemplateSize"] @default_find_image_settings[:fixImageTemplateScale] = old_settings["fixImageTemplateScale"] @default_find_image_settings[:defaultImageTemplateScale] = old_settings["defaultImageTemplateScale"] @default_find_image_settings[:checkForImageElementStaleness] = old_settings["checkForImageElementStaleness"] @default_find_image_settings[:autoUpdateImageElementPosition] = old_settings["autoUpdateImageElementPosition"] @default_find_image_settings[:imageElementTapStrategy] = old_settings["imageElementTapStrategy"] @default_find_image_settings[:getMatchedImageResult] = old_settings["getMatchedImageResult"] @driver.update_settings(settings) end def restore_set_by_image_settings @driver.update_settings(@default_find_image_settings) if @default_find_image_settings end # @@return [String] current package under test def current_package @driver.current_package end def window_size @driver.window_size end def back @driver.back end def is_keyboard_shown? @driver.is_keyboard_shown end def hide_keyboard @driver.hide_keyboard end def home_key @driver.press_keycode(3) end def tab_key @driver.press_keycode(61) end def dpad_up_key @driver.press_keycode(19) end def dpad_down_key @driver.press_keycode(20) end def dpad_right_key @driver.press_keycode(22) end def dpad_left_key @driver.press_keycode(23) end def enter_key @driver.press_keycode(66) end def press_keycode(code) @driver.press_keycode(code) end def long_press_keycode(code) @driver.long_press_keycode(code) end def click(x, y, double: false) ws = driver.window_size window_width = ws.width.to_i window_height = ws.height.to_i if x.kind_of?(Integer) if x < 0 x = window_width + x end elsif x.kind_of?(Float) && x <= 1.0 && x >= 0 x = window_width*x else raise "x value #{x} not supported. Use integer as pixel or float (0..1) as percentage of screen" end if y.kind_of?(Integer) if y < 0 y = window_height + y end elsif y.kind_of?(Float) && y <= 1.0 && y >= 0 y = window_height*y else raise "y value #{x} not supported. Use integer as pixel or float (0..1) as percentage of screen" end action_builder = @driver.action f1 = action_builder.add_pointer_input(:touch, "finger1") f1.create_pointer_move(duration: 0, x: x, y: y, origin: ::Selenium::WebDriver::Interactions::PointerMove::VIEWPORT) f1.create_pointer_down(:left) f1.create_pointer_up(:left) if double f1.create_pause(0.1) f1.create_pointer_down(:left) f1.create_pointer_up(:left) end @driver.perform_actions [f1] end def double_click(x,y) click(x,y, double: true) end # @return [Array