testa_appium_driver 0.1.21 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +23 -0
  3. data/.gitignore +17 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +6 -0
  6. data/Gemfile.lock +1 -1
  7. data/bin/console +17 -0
  8. data/bin/setup +8 -0
  9. data/lib/testa_appium_driver/android/class_selectors.rb +438 -0
  10. data/lib/testa_appium_driver/android/driver.rb +71 -0
  11. data/lib/testa_appium_driver/android/locator/attributes.rb +115 -0
  12. data/lib/testa_appium_driver/android/locator.rb +142 -0
  13. data/lib/testa_appium_driver/android/scroll_actions/uiautomator_scroll_actions.rb +62 -0
  14. data/lib/testa_appium_driver/android/selenium_element.rb +12 -0
  15. data/lib/testa_appium_driver/common/bounds.rb +150 -0
  16. data/lib/testa_appium_driver/common/constants.rb +38 -0
  17. data/lib/testa_appium_driver/common/exceptions/strategy_mix_exception.rb +12 -0
  18. data/lib/testa_appium_driver/common/helpers.rb +272 -0
  19. data/lib/testa_appium_driver/common/locator/scroll_actions.rb +390 -0
  20. data/lib/testa_appium_driver/common/locator.rb +640 -0
  21. data/lib/testa_appium_driver/common/scroll_actions/json_wire_scroll_actions.rb +4 -0
  22. data/lib/testa_appium_driver/common/scroll_actions/w3c_scroll_actions.rb +380 -0
  23. data/lib/testa_appium_driver/common/scroll_actions.rb +275 -0
  24. data/lib/testa_appium_driver/common/selenium_element.rb +19 -0
  25. data/lib/testa_appium_driver/driver.rb +338 -0
  26. data/lib/testa_appium_driver/ios/driver.rb +49 -0
  27. data/lib/testa_appium_driver/ios/locator/attributes.rb +89 -0
  28. data/lib/testa_appium_driver/ios/locator.rb +73 -0
  29. data/lib/testa_appium_driver/ios/selenium_element.rb +8 -0
  30. data/lib/testa_appium_driver/ios/type_selectors.rb +201 -0
  31. data/lib/testa_appium_driver.rb +4 -0
  32. data/testa_appium_driver.gemspec +6 -2
  33. metadata +39 -11
  34. data/appium-driver.iml +0 -79
@@ -0,0 +1,338 @@
1
+ # frozen_string_literal: true
2
+
3
+ #require 'em/pure_ruby'
4
+ require 'appium_lib_core'
5
+
6
+ require_relative 'common/bounds'
7
+ require_relative 'common/exceptions/strategy_mix_exception'
8
+ require_relative 'common/helpers'
9
+ require_relative 'common/locator'
10
+ require_relative 'common/scroll_actions'
11
+ require_relative 'common/selenium_element'
12
+
13
+ module ::TestaAppiumDriver
14
+ class Driver
15
+ include Helpers
16
+
17
+ # @return [::Appium::Core::Base::Driver] the ruby_lib_core appium driver
18
+ attr_accessor :driver
19
+
20
+ # @return [String] iOS or Android
21
+ attr_reader :device
22
+
23
+ # @return [String] driver automation name (uiautomator2 or xcuitest)
24
+ attr_reader :automation_name
25
+
26
+ # custom options
27
+ # - default_find_strategy: default strategy to be used for finding elements. Available strategies :uiautomator or :xpath
28
+ # - default_scroll_strategy: default strategy to be used for scrolling. Available strategies: :uiautomator(android only), :w3c
29
+ def initialize(opts = {})
30
+ @testa_opts = opts[:testa_appium_driver] || {}
31
+
32
+ core = Appium::Core.for(opts)
33
+ @driver = core.start_driver
34
+ @automation_name = @driver.capabilities["automationName"].downcase.to_sym
35
+ @device = @driver.capabilities.platform_name.downcase.to_sym
36
+
37
+ extend_for(@device, @automation_name)
38
+
39
+ handle_testa_opts
40
+
41
+ invalidate_cache
42
+
43
+ #disable_wait_for_idle
44
+ #disable_implicit_wait
45
+
46
+ ::Appium::Core::Element.set_driver(self, @driver.capabilities["udid"])
47
+ end
48
+
49
+
50
+ # invalidates current find_element cache
51
+ def invalidate_cache
52
+ @cache = {
53
+ strategy: nil,
54
+ selector: nil,
55
+ element: nil,
56
+ from_element: nil,
57
+ time: Time.at(0)
58
+ }
59
+ end
60
+
61
+
62
+
63
+
64
+ # Executes the find_element with the resolved locator strategy and selector. Find_element might be skipped if cache is hit.
65
+ # Cache stores last executed find_element with given selector, strategy and from_element. If given values are the same within
66
+ # last 5 seconds element is retrieved from cache.
67
+ # @param [TestaAppiumDriver::Locator, TestaAppiumDriver::Driver] from_element element from which start the search
68
+ # @param [Boolean] single fetch single or multiple results
69
+ # @param [Array<Hash>] strategies_and_selectors array of usable strategies and selectors
70
+ # @param [Boolean] skip_cache to skip checking and storing cache
71
+ # @return [Selenium::WebDriver::Element, Array] element is returned if single is true, array otherwise
72
+ def execute(from_element, single, strategies_and_selectors, skip_cache: false, ignore_implicit_wait: false)
73
+
74
+ # if user wants to wait for element to exist, he can use wait_until_present
75
+ start_time = Time.now.to_f
76
+ ss_index = 0
77
+
78
+
79
+
80
+
81
+ # resolve from_element unique id, so that we can cache it properly
82
+ from_element_id = from_element.instance_of?(TestaAppiumDriver::Locator) ? from_element.strategies_and_selectors : nil
83
+
84
+ begin
85
+ begin
86
+ ss = strategies_and_selectors[ss_index % strategies_and_selectors.count]
87
+ rescue ZeroDivisionError
88
+ puts "aa"
89
+ end
90
+ ss_index +=1
91
+
92
+ puts "Executing #{from_element_id ? "from #{from_element.strategy}: #{from_element.strategies_and_selectors} => " : ""}#{ss.keys[0]}: #{ss.values[0]}"
93
+
94
+ if @cache[:selector] != ss.values[0] || # cache miss, selector is different
95
+ @cache[:time] + 5 <= Time.now || # cache miss, older than 5 seconds
96
+ @cache[:strategy] != ss.keys[0] || # cache miss, different find strategy
97
+ @cache[:from_element_id] != from_element_id || # cache miss, search is started from different element
98
+ skip_cache # cache is skipped
99
+
100
+ if ss.keys[0] == FIND_STRATEGY_IMAGE
101
+ set_find_by_image_settings(ss.values[0].dup)
102
+ if single
103
+ execute_result = from_element.find_element_by_image(ss.values[0][:image])
104
+ else
105
+ execute_result = from_element.find_elements_by_image(ss.values[0][:image])
106
+ end
107
+ restore_set_by_image_settings
108
+ else
109
+ if single
110
+ execute_result = from_element.find_element(ss)
111
+ else
112
+ execute_result = from_element.find_elements(ss)
113
+ end
114
+ end
115
+
116
+
117
+
118
+ unless skip_cache
119
+ @cache[:selector] = ss.values[0]
120
+ @cache[:strategy] = ss.keys[0]
121
+ @cache[:time] = Time.now
122
+ @cache[:from_element_id] = from_element_id
123
+ @cache[:element] = execute_result
124
+ end
125
+ else
126
+ # this is a cache hit, use the element from cache
127
+ execute_result = @cache[:element]
128
+ puts "Using cache from #{@cache[:time].strftime("%H:%M:%S.%L")}, strategy: #{@cache[:strategy]}"
129
+ end
130
+ rescue => e
131
+ #if (start_time + @implicit_wait_ms/1000 < Time.now.to_f && !ignore_implicit_wait) || ss_index < strategies_and_selectors.count
132
+ if ss_index < strategies_and_selectors.count
133
+ sleep EXISTS_WAIT if ss_index >= strategies_and_selectors.count
134
+ retry
135
+ else
136
+ raise e
137
+ end
138
+ end
139
+
140
+ execute_result
141
+ end
142
+
143
+ # method missing is used to forward methods to the actual appium driver
144
+ # after the method is executed, find element cache is invalidated
145
+ def method_missing(method, *args, &block)
146
+ r = @driver.send(method, *args, &block)
147
+ invalidate_cache
148
+ r
149
+ end
150
+
151
+ # disables implicit wait
152
+ def disable_implicit_wait
153
+ @implicit_wait_ms = @driver.get_timeouts["implicit"].to_i
154
+ @implicit_wait_ms = @implicit_wait_ms/1000 if @implicit_wait_ms > 100000
155
+ @implicit_wait_uiautomator_ms = @driver.get_settings["waitForSelectorTimeout"]
156
+ @driver.manage.timeouts.implicit_wait = 0
157
+ @driver.update_settings({waitForSelectorTimeout: 0})
158
+ end
159
+
160
+ # disables wait for idle, only executed for android devices
161
+ def disable_wait_for_idle
162
+ if @device == :android
163
+ @wait_for_idle_timeout = @driver.settings.get["waitForIdleTimeout"]
164
+ @driver.update_settings({waitForIdleTimeout: 0})
165
+ end
166
+ end
167
+
168
+
169
+ def set_find_by_image_settings(settings)
170
+ settings.delete(:image)
171
+ @default_find_image_settings = {}
172
+ old_settings = @driver.get_settings
173
+ @default_find_image_settings[:imageMatchThreshold] = old_settings["imageMatchThreshold"]
174
+ @default_find_image_settings[:fixImageFindScreenshotDims] = old_settings["fixImageFindScreenshotDims"]
175
+ @default_find_image_settings[:fixImageTemplateSize] = old_settings["fixImageTemplateSize"]
176
+ @default_find_image_settings[:fixImageTemplateScale] = old_settings["fixImageTemplateScale"]
177
+ @default_find_image_settings[:defaultImageTemplateScale] = old_settings["defaultImageTemplateScale"]
178
+ @default_find_image_settings[:checkForImageElementStaleness] = old_settings["checkForImageElementStaleness"]
179
+ @default_find_image_settings[:autoUpdateImageElementPosition] = old_settings["autoUpdateImageElementPosition"]
180
+ @default_find_image_settings[:imageElementTapStrategy] = old_settings["imageElementTapStrategy"]
181
+ @default_find_image_settings[:getMatchedImageResult] = old_settings["getMatchedImageResult"]
182
+
183
+ @driver.update_settings(settings)
184
+ end
185
+
186
+ def restore_set_by_image_settings
187
+ @driver.update_settings(@default_find_image_settings) if @default_find_image_settings
188
+ end
189
+
190
+
191
+ # @@return [String] current package under test
192
+ def current_package
193
+ @driver.current_package
194
+ end
195
+
196
+
197
+ def window_size
198
+ @driver.window_size
199
+ end
200
+
201
+ def back
202
+ @driver.back
203
+ end
204
+
205
+
206
+ def is_keyboard_shown?
207
+ @driver.is_keyboard_shown
208
+ end
209
+
210
+ def hide_keyboard
211
+ @driver.hide_keyboard
212
+ end
213
+
214
+ def home_key
215
+ @driver.press_keycode(3)
216
+ end
217
+
218
+ def tab_key
219
+ @driver.press_keycode(61)
220
+ end
221
+
222
+ def dpad_up_key
223
+ @driver.press_keycode(19)
224
+ end
225
+
226
+ def dpad_down_key
227
+ @driver.press_keycode(20)
228
+ end
229
+
230
+ def dpad_right_key
231
+ @driver.press_keycode(22)
232
+ end
233
+
234
+ def dpad_left_key
235
+ @driver.press_keycode(23)
236
+ end
237
+
238
+ def enter_key
239
+ @driver.press_keycode(66)
240
+ end
241
+
242
+ def press_keycode(code)
243
+ @driver.press_keycode(code)
244
+ end
245
+
246
+ def long_press_keycode(code)
247
+ @driver.long_press_keycode(code)
248
+ end
249
+
250
+ def click(x, y, double: false)
251
+ ws = driver.window_size
252
+ window_width = ws.width.to_i
253
+ window_height = ws.height.to_i
254
+ if x.kind_of?(Integer)
255
+ if x < 0
256
+ x = window_width + x
257
+ end
258
+ elsif x.kind_of?(Float) && x <= 1.0 && x >= 0
259
+ x = window_width*x
260
+ else
261
+ raise "x value #{x} not supported. Use integer as pixel or float (0..1) as percentage of screen"
262
+ end
263
+
264
+ if y.kind_of?(Integer)
265
+ if y < 0
266
+ y = window_height + y
267
+ end
268
+ elsif y.kind_of?(Float) && y <= 1.0 && y >= 0
269
+ y = window_height*y
270
+ else
271
+ raise "y value #{x} not supported. Use integer as pixel or float (0..1) as percentage of screen"
272
+ end
273
+
274
+
275
+ action_builder = @driver.action
276
+ f1 = action_builder.add_pointer_input(:touch, "finger1")
277
+ f1.create_pointer_move(duration: 0, x: x, y: y, origin: ::Selenium::WebDriver::Interactions::PointerMove::VIEWPORT)
278
+ f1.create_pointer_down(:left)
279
+ f1.create_pointer_up(:left)
280
+ if double
281
+ f1.create_pause(0.1)
282
+ f1.create_pointer_down(:left)
283
+ f1.create_pointer_up(:left)
284
+ end
285
+ @driver.perform_actions [f1]
286
+ end
287
+
288
+ def double_click(x,y)
289
+ click(x,y, double: true)
290
+ end
291
+
292
+
293
+
294
+ # @return [Array<Selenium::WebDriver::Element] array of 2 elements, the first element without children and the last element without children in the current page
295
+ def first_and_last_leaf(from_element = @driver)
296
+ elements = from_element.find_elements(xpath: ".//*[not(*)]")
297
+ return nil if elements.count == 0
298
+ [elements[0], elements[-1]]
299
+ end
300
+
301
+ def first_and_last_child(from_element = @driver)
302
+ elements = from_element.find_elements(xpath: "./*")
303
+ return nil if elements.count == 0
304
+
305
+ [elements[0], elements[-1]]
306
+ end
307
+
308
+ def android?
309
+ device == :android
310
+ end
311
+
312
+ def ios?
313
+ device == :ios
314
+ end
315
+
316
+ private
317
+ def extend_for(device, automation_name)
318
+ case device
319
+ when :android
320
+ case automation_name
321
+ when :uiautomator2, :espresso, :Espresso
322
+ require_relative 'android/driver'
323
+ else
324
+ raise "Testa appium driver not supported for #{automation_name} automation"
325
+ end
326
+ when :ios, :tvos
327
+ case automation_name
328
+ when :xcuitest
329
+ require_relative 'ios/driver'
330
+ else
331
+ raise "Testa appium driver not supported for #{automation_name} automation"
332
+ end
333
+ else
334
+ raise "Unknown device #{device}, should be either android, ios or tvos"
335
+ end
336
+ end
337
+ end
338
+ end
@@ -0,0 +1,49 @@
1
+ require_relative 'type_selectors'
2
+ require_relative 'locator'
3
+ require_relative 'selenium_element'
4
+
5
+ module TestaAppiumDriver
6
+ class Driver
7
+ include TypeSelectors
8
+
9
+
10
+
11
+ # @param params [Hash]
12
+ # @return [TestaAppiumDriver::Locator] first scrollable element
13
+ def scrollable(params = {})
14
+ scroll_view(params)
15
+ end
16
+
17
+ # @param params [Hash]
18
+ # @return [TestaAppiumDriver::Locator] first scrollable element
19
+ def scrollables(params = {})
20
+ scroll_views(params)
21
+ end
22
+
23
+ private
24
+ def handle_testa_opts
25
+ if @testa_opts[:default_find_strategy].nil?
26
+ @default_find_strategy = DEFAULT_IOS_FIND_STRATEGY
27
+ else
28
+ case @testa_opts[:default_find_strategy].to_sym
29
+ when FIND_STRATEGY_XPATH
30
+ @default_find_strategy = @testa_opts[:default_find_strategy].to_sym
31
+ else
32
+ raise "Default find strategy #{@testa_opts[:default_find_strategy]} not supported for iOS"
33
+ end
34
+ end
35
+
36
+
37
+ if @testa_opts[:default_scroll_strategy].nil?
38
+ @default_scroll_strategy = DEFAULT_IOS_SCROLL_STRATEGY
39
+ else
40
+ case @testa_opts[:default_scroll_strategy].to_sym
41
+ when SCROLL_STRATEGY_W3C
42
+ @default_scroll_strategy = @testa_opts[:default_scroll_strategy].to_sym
43
+ else
44
+ raise "Default scroll strategy #{@testa_opts[:default_scroll_strategy]} not supported for iOS"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,89 @@
1
+ module TestaAppiumDriver
2
+ module Attributes
3
+
4
+ #noinspection RubyNilAnalysis
5
+ def testa_attribute(name, *args)
6
+
7
+ if self.instance_of?(::Selenium::WebDriver::Element) || self.instance_of?(::Appium::Core::Element)
8
+ @driver = get_driver
9
+ elements = self
10
+ else
11
+ elements = execute(*args)
12
+ end
13
+
14
+
15
+ if elements.instance_of?(::Selenium::WebDriver::Element) || elements.instance_of?(::Appium::Core::Element)
16
+ r = elements.send(:attribute, name.to_s)
17
+ r = TestaAppiumDriver::Bounds.from_ios(r, @driver) if name.to_s == "rect"
18
+ else
19
+ r = elements.map { |e| e.send(:attribute, name.to_s) }
20
+ r.map! { |b| TestaAppiumDriver::Bounds.from_ios(b, @driver) } if name.to_s == "rect"
21
+ end
22
+ r
23
+ end
24
+
25
+
26
+ def accessibility_container(*args)
27
+ testa_attribute("accessibilityContainer", *args)
28
+ end
29
+
30
+ def accessible?(*args)
31
+ testa_attribute("accessible", *args).to_s == "true"
32
+ end
33
+
34
+
35
+ def class_name(*args)
36
+ testa_attribute("class", *args)
37
+ end
38
+
39
+ def enabled?(*args)
40
+ testa_attribute("enabled", *args).to_s == "true"
41
+ end
42
+
43
+ def frame(*args)
44
+ testa_attribute("frame", *args)
45
+ end
46
+
47
+ def index(*args)
48
+ index = testa_attribute("index", *args)
49
+ index = "1" if index == "true"
50
+ index = "0" if index == "false"
51
+
52
+ index
53
+ end
54
+
55
+ def label(*args)
56
+ testa_attribute("label", *args)
57
+ end
58
+
59
+ def name(*args)
60
+ testa_attribute("name", *args)
61
+ end
62
+
63
+
64
+ def rect(*args)
65
+ testa_attribute("rect", *args)
66
+ end
67
+
68
+ def selected?(*args)
69
+ testa_attribute("selected", *args).to_s == "true"
70
+ end
71
+
72
+ def type(*args)
73
+ testa_attribute("type", *args)
74
+ end
75
+
76
+ def value(*args)
77
+ testa_attribute("value", *args)
78
+ end
79
+
80
+ def visible?(*args)
81
+ testa_attribute("visible", *args).to_s == "true"
82
+ end
83
+
84
+
85
+ alias_method :bounds, :rect
86
+ alias_method :text, :label
87
+ end
88
+
89
+ end
@@ -0,0 +1,73 @@
1
+ require_relative 'locator/attributes'
2
+
3
+ module ::TestaAppiumDriver
4
+ class Locator
5
+ include TypeSelectors
6
+ include Attributes
7
+ attr_accessor :class_chain_selector
8
+
9
+ def init(params, selectors, single)
10
+ if is_scrollable_selector?(selectors, single)
11
+ @scroll_orientation = :vertical
12
+
13
+ if !params[:top].nil? || !params[:bottom].nil? || !params[:right].nil? || !params[:left].nil?
14
+ @scroll_deadzone = {}
15
+ @scroll_deadzone[:top] = params[:top].to_f unless params[:top].nil?
16
+ @scroll_deadzone[:bottom] = params[:bottom].to_f unless params[:bottom].nil?
17
+ @scroll_deadzone[:right] = params[:right].to_f unless params[:right].nil?
18
+ @scroll_deadzone[:left] = params[:left].to_f unless params[:left].nil?
19
+ end
20
+
21
+ params[:scrollable_locator] = self.dup
22
+ end
23
+
24
+ @class_chain_selector = hash_to_class_chain(selectors, single)
25
+
26
+
27
+ @scrollable_locator = params[:scrollable_locator] if params[:scrollable_locator]
28
+ end
29
+
30
+
31
+ # @return [Array] returns 2 elements. The first is the resolved find element strategy and the second is the resolved selector
32
+ def strategies_and_selectors
33
+ ss = []
34
+ if @can_use_id_strategy
35
+ ss.push({"#{FIND_STRATEGY_NAME}": @can_use_id_strategy})
36
+ end
37
+ ss.push({"#{FIND_STRATEGY_CLASS_CHAIN}": @class_chain_selector}) if @strategy.nil? || @strategy == FIND_STRATEGY_CLASS_CHAIN
38
+ ss.push({"#{FIND_STRATEGY_XPATH}": @xpath_selector}) if @strategy.nil? || @strategy == FIND_STRATEGY_XPATH
39
+ ss.push({"#{FIND_STRATEGY_IMAGE}": @image_selector}) if @strategy == FIND_STRATEGY_IMAGE
40
+ ss
41
+ end
42
+
43
+
44
+
45
+ # @return [Locator] new child locator element
46
+ def add_child_selector(params)
47
+ params, selectors = extract_selectors_from_params(params)
48
+ single = params[:single]
49
+ raise "Cannot add child selector to Array" if single && !@single
50
+
51
+ locator = self.dup
52
+ locator.can_use_id_strategy = false
53
+ add_xpath_child_selectors(locator, selectors, single)
54
+ if @strategy.nil? || @strategy == FIND_STRATEGY_CLASS_CHAIN
55
+ add_class_chain_child_selectors(locator, selectors, single)
56
+ end
57
+
58
+ if is_scrollable_selector?(selectors, single)
59
+ locator.scrollable_locator.scroll_orientation = :vertical
60
+ locator.scrollable_locator = locator
61
+ end
62
+
63
+ locator.last_selector_adjacent = false
64
+ locator
65
+ end
66
+
67
+
68
+ def add_class_chain_child_selectors(locator, selectors, single)
69
+ locator.single = false unless single # switching from single result to multiple
70
+ locator.class_chain_selector += "/" + hash_to_class_chain(selectors, single)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,8 @@
1
+ module ::Appium
2
+ module Core
3
+ class Element
4
+ include TestaAppiumDriver::TypeSelectors
5
+ include TestaAppiumDriver::Attributes
6
+ end
7
+ end
8
+ end