testa_appium_driver 0.1.21 → 0.1.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) 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/bin/console +17 -0
  7. data/bin/setup +8 -0
  8. data/lib/testa_appium_driver/android/class_selectors.rb +438 -0
  9. data/lib/testa_appium_driver/android/driver.rb +71 -0
  10. data/lib/testa_appium_driver/android/locator/attributes.rb +115 -0
  11. data/lib/testa_appium_driver/android/locator.rb +142 -0
  12. data/lib/testa_appium_driver/android/scroll_actions/uiautomator_scroll_actions.rb +62 -0
  13. data/lib/testa_appium_driver/android/selenium_element.rb +12 -0
  14. data/lib/testa_appium_driver/common/bounds.rb +150 -0
  15. data/lib/testa_appium_driver/common/constants.rb +38 -0
  16. data/lib/testa_appium_driver/common/exceptions/strategy_mix_exception.rb +12 -0
  17. data/lib/testa_appium_driver/common/helpers.rb +272 -0
  18. data/lib/testa_appium_driver/common/locator/scroll_actions.rb +390 -0
  19. data/lib/testa_appium_driver/common/locator.rb +640 -0
  20. data/lib/testa_appium_driver/common/scroll_actions/json_wire_scroll_actions.rb +4 -0
  21. data/lib/testa_appium_driver/common/scroll_actions/w3c_scroll_actions.rb +371 -0
  22. data/lib/testa_appium_driver/common/scroll_actions.rb +271 -0
  23. data/lib/testa_appium_driver/common/selenium_element.rb +19 -0
  24. data/lib/testa_appium_driver/driver.rb +338 -0
  25. data/lib/testa_appium_driver/ios/driver.rb +49 -0
  26. data/lib/testa_appium_driver/ios/locator/attributes.rb +89 -0
  27. data/lib/testa_appium_driver/ios/locator.rb +73 -0
  28. data/lib/testa_appium_driver/ios/selenium_element.rb +8 -0
  29. data/lib/testa_appium_driver/ios/type_selectors.rb +201 -0
  30. data/lib/testa_appium_driver.rb +4 -0
  31. data/testa_appium_driver.gemspec +6 -2
  32. metadata +39 -11
  33. 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