testa_appium_driver 0.1.0

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.idea/deployment.xml +21 -0
  4. data/.idea/inspectionProfiles/Project_Default.xml +9 -0
  5. data/.idea/misc.xml +6 -0
  6. data/.idea/modules.xml +8 -0
  7. data/.idea/runConfigurations/Android_Test.xml +42 -0
  8. data/.idea/sshConfigs.xml +10 -0
  9. data/.idea/vcs.xml +6 -0
  10. data/.idea/webServers.xml +14 -0
  11. data/.rspec +3 -0
  12. data/.rubocop.yml +13 -0
  13. data/CHANGELOG.md +5 -0
  14. data/CODE_OF_CONDUCT.md +102 -0
  15. data/Gemfile +12 -0
  16. data/LICENSE.txt +21 -0
  17. data/README.md +52 -0
  18. data/Rakefile +12 -0
  19. data/bin/console +17 -0
  20. data/bin/setup +8 -0
  21. data/lib/testa_appium_driver.rb +6 -0
  22. data/lib/testa_appium_driver/android/class_selectors.rb +353 -0
  23. data/lib/testa_appium_driver/android/driver.rb +52 -0
  24. data/lib/testa_appium_driver/android/locator.rb +115 -0
  25. data/lib/testa_appium_driver/android/locator/attributes.rb +116 -0
  26. data/lib/testa_appium_driver/android/scroll_actions/uiautomator_scroll_actions.rb +77 -0
  27. data/lib/testa_appium_driver/android/selenium_element.rb +8 -0
  28. data/lib/testa_appium_driver/common/bounds.rb +150 -0
  29. data/lib/testa_appium_driver/common/constants.rb +33 -0
  30. data/lib/testa_appium_driver/common/exceptions/strategy_mix_exception.rb +12 -0
  31. data/lib/testa_appium_driver/common/helpers.rb +242 -0
  32. data/lib/testa_appium_driver/common/locator.rb +371 -0
  33. data/lib/testa_appium_driver/common/locator/scroll_actions.rb +287 -0
  34. data/lib/testa_appium_driver/common/scroll_actions.rb +253 -0
  35. data/lib/testa_appium_driver/common/scroll_actions/json_wire_scroll_actions.rb +4 -0
  36. data/lib/testa_appium_driver/common/scroll_actions/w3c_scroll_actions.rb +261 -0
  37. data/lib/testa_appium_driver/driver.rb +226 -0
  38. data/lib/testa_appium_driver/ios/driver.rb +35 -0
  39. data/lib/testa_appium_driver/ios/locator.rb +40 -0
  40. data/lib/testa_appium_driver/ios/locator/attributes.rb +79 -0
  41. data/lib/testa_appium_driver/ios/selenium_element.rb +7 -0
  42. data/lib/testa_appium_driver/ios/type_selectors.rb +167 -0
  43. data/lib/testa_appium_driver/version.rb +5 -0
  44. data/testa_appium_driver.gemspec +40 -0
  45. data/testa_appium_driver.iml +79 -0
  46. metadata +147 -0
@@ -0,0 +1,4 @@
1
+ module TestaAppiumDriver
2
+ class ScrollActions
3
+ end
4
+ end
@@ -0,0 +1,261 @@
1
+ module TestaAppiumDriver
2
+ class ScrollActions
3
+
4
+ private
5
+ # @return [Array]
6
+ def w3c_each(skip_scroll_to_start, &block)
7
+ elements = []
8
+ begin
9
+ @driver.disable_wait_for_idle
10
+ @driver.disable_implicit_wait
11
+ default_deadzone!
12
+
13
+ iterations = 0
14
+
15
+
16
+ scroll_to_start unless skip_scroll_to_start
17
+
18
+ until is_end_of_scroll?
19
+ matches = @locator.execute(skip_cache: true)
20
+ matches.each_with_index do |m|
21
+ elements << m
22
+ if block_given? # block is given
23
+ block.call(m) # use call to execute the block
24
+ else # the value of block_argument becomes nil if you didn't give a block
25
+ # block was not given
26
+ end
27
+ end
28
+ iterations += 1
29
+ break if !@max_scrolls.nil? && iterations == @max_scrolls
30
+ end
31
+ rescue => e
32
+ raise e
33
+ ensure
34
+ @driver.enable_implicit_wait
35
+ @driver.enable_wait_for_idle
36
+ end
37
+ elements
38
+ end
39
+
40
+ def w3c_align(with)
41
+ @driver.disable_wait_for_idle
42
+ default_deadzone!
43
+
44
+
45
+
46
+ @locator.scroll_to unless @raise # called with !
47
+
48
+ element = @locator.execute
49
+ @driver.disable_implicit_wait
50
+
51
+ case with
52
+ when :top
53
+ page_down if is_aligned?(with, element)
54
+ when :bottom
55
+ page_up if is_aligned?(with, element)
56
+ when :right
57
+ page_right if is_aligned?(with, element)
58
+ when :left
59
+ page_left if is_aligned?(with, element)
60
+ else
61
+ raise "Unsupported align with option: #{with}"
62
+ end
63
+
64
+ timeout = 0
65
+ until is_aligned?(with, element) || timeout == 3
66
+ w3c_attempt_align(with)
67
+ timeout += 1
68
+ end
69
+
70
+ @driver.enable_implicit_wait
71
+ @driver.enable_wait_for_idle
72
+ end
73
+
74
+
75
+ def w3c_attempt_align(with)
76
+ case with
77
+ when :top
78
+ y0 = @bounds.bottom_right.y - @deadzone[:bottom]
79
+ y1 = y0 - @align_offset
80
+ x0 = @bounds.width / 2
81
+ x1 = x0
82
+ scroll_direction = :down
83
+ when :bottom
84
+ y0 = @bounds.top_left.y + @deadzone[:top]
85
+ y1 = y0 + @align_offset
86
+ x0 = @bounds.width / 2
87
+ x1 = x0
88
+ scroll_direction = :up
89
+ when :left
90
+ x0 = @bounds.bottom_right.x - @deadzone[:right]
91
+ x1 = x0 - @align_offset
92
+ y0 = @bounds.height / 2
93
+ y1 = y0
94
+ scroll_direction = :right
95
+ when :right
96
+ x0 = @bounds.top_left.x + @deadzone[:top]
97
+ x1 = x0 + @align_offset
98
+ y0 = @bounds.height / 2
99
+ y1 = y0
100
+ scroll_direction = :left
101
+ else
102
+ raise "Unsupported align with option: #{with}"
103
+ end
104
+
105
+ x1, y1 = apply_w3c_correction(x1, y1, scroll_direction) if @driver.device == :android
106
+ w3c_action(x0, y0, x1, y1, SCROLL_ACTION_TYPE_SCROLL)
107
+ end
108
+
109
+
110
+ def w3c_scroll_to(direction)
111
+
112
+ rounds = 0
113
+ max_scrolls_reached = false
114
+ end_of_scroll_reached = false
115
+ until @locator.exists? || end_of_scroll_reached
116
+ end_of_scroll_reached = is_end_of_scroll?
117
+ case direction
118
+ when :down
119
+ page_down
120
+ when :right
121
+ page_right
122
+ when :left
123
+ page_left
124
+ when :up
125
+ page_up
126
+ else
127
+ scroll_to_start
128
+ @previous_elements = nil
129
+ if @scrollable.scroll_orientation == :vertical
130
+ direction = :down
131
+ else
132
+ direction = :right
133
+ end
134
+ end
135
+
136
+ rounds += 1
137
+
138
+ max_scrolls_reached = true if rounds == @max_scrolls
139
+ break if rounds == @max_scrolls
140
+ end
141
+ raise Selenium::WebDriver::Error::NoSuchElementError if max_scrolls_reached || end_of_scroll_reached
142
+ end
143
+
144
+ def w3c_scroll_to_start_or_end(type)
145
+ @driver.disable_wait_for_idle
146
+ @driver.disable_implicit_wait
147
+ default_deadzone!
148
+
149
+ @previous_elements = nil
150
+
151
+
152
+ if type == :start
153
+ if @scrollable.scroll_orientation == :vertical
154
+ method = "fling_up"
155
+ else
156
+ method = "fling_left"
157
+ end
158
+ else
159
+ if @scrollable.scroll_orientation == :vertical
160
+ method = "fling_down"
161
+ else
162
+ method = "fling_right"
163
+ end
164
+ end
165
+
166
+ iterations = 0
167
+ until is_end_of_scroll? || iterations >= 3
168
+ self.send(method)
169
+ iterations += 1
170
+ end
171
+
172
+ # reset the flag for end of scroll elements
173
+ @previous_elements = nil
174
+
175
+ @driver.enable_implicit_wait
176
+ @driver.enable_wait_for_idle
177
+ end
178
+
179
+
180
+ def w3c_page_or_fling(type, direction)
181
+ @driver.disable_wait_for_idle
182
+ @driver.disable_implicit_wait
183
+ default_deadzone!
184
+
185
+ if direction == :down || direction == :up
186
+ if direction == :down
187
+ y0 = @bounds.bottom_right.y - @deadzone[:bottom].to_i
188
+ y1 = @bounds.top_left.y + @deadzone[:top].to_i
189
+ else
190
+ y0 = @bounds.top_left.y + @deadzone[:top].to_i
191
+ y1 = @bounds.bottom_right.y - @deadzone[:bottom].to_i
192
+ end
193
+ x0 = @bounds.width / 2
194
+ x1 = x0
195
+ else
196
+ if direction == :right
197
+ x0 = @bounds.bottom_right.x - @deadzone[:right].to_i
198
+ x1 = @bounds.top_left.x + @deadzone[:left].to_i
199
+ else
200
+ x0 = @bounds.top_left.x + @deadzone[:left].to_i
201
+ x1 = @bounds.bottom_right.x - @deadzone[:right].to_i
202
+ end
203
+ y0 = @bounds.height / 2
204
+ y1 = y0
205
+ end
206
+ x1, y1 = apply_w3c_correction(x1, y1, direction) if @driver.device == :android
207
+
208
+
209
+ w3c_action(x0, y0, x1, y1, type)
210
+
211
+ @driver.enable_implicit_wait
212
+ @driver.enable_wait_for_idle
213
+ end
214
+
215
+
216
+ def w3c_action(x0, y0, x1, y1, type)
217
+ if type == SCROLL_ACTION_TYPE_SCROLL
218
+ duration = 1.8
219
+ elsif type == SCROLL_ACTION_TYPE_FLING
220
+ duration = 0.1
221
+ elsif type == SCROLL_ACTION_TYPE_DRAG
222
+ duration = 3.5
223
+ else
224
+ raise "Unknown scroll action type #{type}"
225
+ end
226
+
227
+ action_builder = @driver.action
228
+ f1 = action_builder.add_pointer_input(:touch, "finger1")
229
+ f1.create_pointer_move(duration: 0, x: x0, y: y0, origin: ::Selenium::WebDriver::Interactions::PointerMove::VIEWPORT)
230
+ f1.create_pointer_down(:left)
231
+
232
+ f1.create_pointer_move(duration: duration, x: x1, y: y1, origin: ::Selenium::WebDriver::Interactions::PointerMove::VIEWPORT)
233
+ unless type == SCROLL_ACTION_TYPE_FLING
234
+ # with this move we prevent flinging/overscroll
235
+ f1.create_pointer_move(duration: 0.5, x: x1, y: y1, origin: ::Selenium::WebDriver::Interactions::PointerMove::VIEWPORT)
236
+ end
237
+ f1.create_pointer_up(:left)
238
+ puts "Scroll execute[w3c_action]: #{type}: {x0: #{x0}, y0: #{y0}} => {x1: #{x1}, y1: #{y1}}"
239
+ @driver.perform_actions [f1]
240
+ end
241
+
242
+
243
+ def apply_w3c_correction(x1, y1, direction)
244
+ y1 -= SCROLL_CORRECTION_W3C if direction == :down
245
+ y1 += SCROLL_CORRECTION_W3C if direction == :up
246
+ x1 -= SCROLL_CORRECTION_W3C if direction == :right
247
+ x1 += SCROLL_CORRECTION_W3C if direction == :left
248
+ [x1, y1]
249
+ end
250
+
251
+
252
+
253
+ def drag_to(x, y)
254
+ x0 = @bounds.center.x
255
+ y0 = @bounds.center.y
256
+ w3c_action(x0, y0, x, y, SCROLL_ACTION_TYPE_DRAG)
257
+ end
258
+
259
+ end
260
+
261
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'common/bounds'
4
+ require_relative 'common/exceptions/strategy_mix_exception'
5
+ require_relative 'common/helpers'
6
+ require_relative 'common/locator'
7
+ require_relative 'common/scroll_actions'
8
+
9
+ module TestaAppiumDriver
10
+ class Driver
11
+ attr_accessor :driver
12
+ attr_reader :device
13
+ attr_reader :automation_name
14
+
15
+ # custom options
16
+ # - default_strategy: default strategy to be used for finding elements. Available strategies :uiautomator or :xpath
17
+ def initialize(opts = {})
18
+ @testa_opts = opts[:testa_appium_driver] || {}
19
+
20
+
21
+
22
+ core = Appium::Core.for(opts)
23
+ extend_for(core.device, core.automation_name)
24
+ @device = core.device
25
+ @automation_name = core.automation_name
26
+
27
+ handle_testa_opts
28
+
29
+ @driver = core.start_driver
30
+ invalidate_cache!
31
+
32
+
33
+
34
+ extend_element_with_driver(opts[:caps][:udid])
35
+ end
36
+
37
+
38
+ def invalidate_cache!
39
+ @cache = {
40
+ strategy: nil,
41
+ selector: nil,
42
+ element: nil,
43
+ from_element: nil,
44
+ time: Time.at(0)
45
+ }
46
+ end
47
+
48
+ #noinspection RubyClassVariableUsageInspection
49
+ def extend_element_with_driver(udid)
50
+ Selenium::WebDriver::Element.define_singleton_method(:set_driver) do |driver|
51
+ udid = "unknown" if udid.nil?
52
+ @@drivers ||={}
53
+ @@drivers[udid] = driver
54
+ end
55
+
56
+ Selenium::WebDriver::Element.set_driver(self)
57
+ Selenium::WebDriver::Element.define_method(:get_driver) do
58
+ udid = self.instance_variable_get(:@bridge).instance_variable_get(:@capabilities).instance_variable_get(:@capabilities)["udid"]
59
+ udid = "unknown" if udid.nil?
60
+ @@drivers[udid]
61
+ end
62
+ end
63
+
64
+
65
+ #noinspection RubyScope
66
+ # @param [TestaAppiumDriver::Locator, TestaAppiumDriver::Driver] from_element element from which start the search
67
+ # @param [String] selector resolved string of a [TestaAppiumDriver::Locator] selector xpath for xpath strategy, java UiSelectors for uiautomator
68
+ # @param [Boolean] single fetch single or multiple results
69
+ # @param [Symbol, nil] strategy [TestaAppiumDriver:FIND_STRATEGY_UIAUTOMATOR] or [FIND_STRATEGY_XPATH]
70
+ # @param [Symbol] default_strategy if strategy is not enforced, default can be used
71
+ # @param [Boolean] skip_cache to skip checking and storing cache
72
+ # @return [Selenium::WebDriver::Element, Array] element is returned if single is true, array otherwise
73
+ def execute(from_element, selector, single, strategy, default_strategy, skip_cache = false)
74
+
75
+ # if user wants to wait for element to exist, he can use wait_until_present
76
+ disable_wait_for_idle
77
+
78
+ # if not restricted to a strategy, use the default one
79
+ strategy = default_strategy if strategy.nil?
80
+
81
+ # resolve from_element unique id, so that we can cache it properly
82
+ from_element_id = from_element.kind_of?(TestaAppiumDriver::Locator) ? from_element.selector : nil
83
+
84
+ puts "Executing #{from_element_id ? "from #{from_element.strategy}: #{from_element.selector} => " : ""}#{strategy}: #{selector}"
85
+ begin
86
+ if @cache[:selector] != selector || # cache miss, selector is different
87
+ @cache[:time] + 5 <= Time.now || # cache miss, older than 5 seconds
88
+ @cache[:strategy] != strategy || # cache miss, different find strategy
89
+ @cache[:from_element_id] != from_element_id || # cache miss, search is started from different element
90
+ skip_cache # cache is skipped
91
+
92
+ if strategy == FIND_STRATEGY_UIAUTOMATOR
93
+ if single
94
+ execute_result = from_element.find_element(uiautomator: selector)
95
+ else
96
+ execute_result = from_element.find_elements(uiautomator: selector)
97
+ end
98
+
99
+ elsif strategy == FIND_STRATEGY_XPATH
100
+ if single
101
+ execute_result = from_element.find_element(xpath: selector)
102
+ else
103
+ execute_result = from_element.find_elements(xpath: selector)
104
+ end
105
+ else
106
+ raise "Unknown find_element strategy"
107
+ end
108
+
109
+ unless skip_cache
110
+ @cache[:selector] = selector
111
+ @cache[:strategy] = strategy
112
+ @cache[:time] = Time.now
113
+ @cache[:from_element_id] = from_element_id
114
+ @cache[:element] = execute_result
115
+ end
116
+ else
117
+ # this is a cache hit, use the element from cache
118
+ execute_result = @cache[:element]
119
+ puts "Using cache from #{@cache[:time].strftime("%H:%M:%S.%L")}, strategy: #{@cache[:strategy]}"
120
+ end
121
+ rescue => e
122
+ raise e
123
+ ensure
124
+ enable_wait_for_idle
125
+ end
126
+
127
+ execute_result
128
+ end
129
+
130
+
131
+ # method missing is used to forward methods to the actual appium driver
132
+ def method_missing(method, *args, &block)
133
+ @driver.send(method, *args, &block)
134
+ end
135
+
136
+ def disable_implicit_wait
137
+ @implicit_wait_ms = @driver.get_timeouts["implicit"]
138
+ @driver.manage.timeouts.implicit_wait = 0
139
+ end
140
+
141
+ def enable_implicit_wait
142
+ raise "Implicit wait is not disabled" if @implicit_wait_ms.nil?
143
+ # get_timeouts always returns in milliseconds, but we should set in seconds
144
+ @driver.manage.timeouts.implicit_wait = @implicit_wait_ms / 1000
145
+ end
146
+
147
+ def disable_wait_for_idle
148
+ if @device == :android
149
+ @wait_for_idle_timeout = @driver.settings.get["waitForIdleTimeout"]
150
+ @driver.update_settings({waitForIdleTimeout: 0})
151
+ end
152
+ end
153
+
154
+ def enable_wait_for_idle
155
+ if @device == :android
156
+ raise "Wait for idle is not disabled" if @wait_for_idle_timeout.nil?
157
+ @driver.update_settings({waitForIdleTimeout: @wait_for_idle_timeout})
158
+ end
159
+ end
160
+
161
+ def current_package
162
+ @driver.current_package
163
+ end
164
+
165
+ def window_size(*args)
166
+ @driver.window_size(*args)
167
+ end
168
+
169
+ def back
170
+ @driver.back
171
+ end
172
+
173
+
174
+ def is_keyboard_shown?
175
+ @driver.is_keyboard_shown
176
+ end
177
+
178
+ def hide_keyboard
179
+ @driver.hide_keyboard
180
+ end
181
+
182
+ def press_keycode(code)
183
+ @driver.press_keycode(code)
184
+ end
185
+
186
+ def long_press_keycode(code)
187
+ @driver.long_press_keycode(code)
188
+ end
189
+
190
+
191
+ def first_and_last_leaf(from_element = @driver)
192
+ disable_wait_for_idle
193
+ disable_implicit_wait
194
+ elements = from_element.find_elements(xpath: "//*[not(*)]")
195
+ enable_implicit_wait
196
+ enable_wait_for_idle
197
+ return nil if elements.count == 0
198
+ [elements[0], elements[-1]]
199
+ end
200
+
201
+ private
202
+ def extend_for(device, automation_name)
203
+ case device
204
+ when :android
205
+ case automation_name
206
+ when :uiautomator2
207
+ require_relative 'android/driver'
208
+ else
209
+ raise "Testa appium driver not supported for #{automation_name} automation"
210
+ end
211
+ when :ios, :tvos
212
+ case automation_name
213
+ when :xcuitest
214
+ require_relative 'ios/driver'
215
+ else
216
+ raise "Testa appium driver not supported for #{automation_name} automation"
217
+ end
218
+ else
219
+ raise "Unknown device #{device}, should be either android, ios or tvos"
220
+ end
221
+ end
222
+
223
+
224
+ end
225
+
226
+ end