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,272 @@
1
+ module ::TestaAppiumDriver
2
+ module Helpers
3
+
4
+ # supported selectors
5
+ # id: "com.my.package:id/myId"
6
+ # id: "myId" => will be converted to "com.my.package:id/myId"
7
+ # id: /my/ will find all elements with ids that contain my
8
+ # desc: "element description"
9
+ # desc: /ription/ will find all elements that contains ription
10
+ # class: "android.widget.Button"
11
+ # class: /Button/ will find all elements with classes that contain Button
12
+ # text: "Hello world"
13
+ # text: /ello/ will find all elements with text that contain ello
14
+ # package: "com.my.package"
15
+ # package: /my/ will find all elements with package that contains my
16
+ # long_clickable: true or false
17
+ # checkable: true or false
18
+ # checked: true or false
19
+ # clickable: true or false
20
+ # enabled: true or false
21
+ # focusable: true or false
22
+ # focused: true or false
23
+ # index: child index inside of a parent element, index starts from 0
24
+ # selected: true or false
25
+ # scrollable: true or false
26
+ # @param [Hash] hash selectors for finding elements
27
+ # @param [Boolean] single should the command return first instance or all of matched elements
28
+ # @return [String] hash selectors converted to uiautomator command
29
+ def hash_to_uiautomator(hash, single = true)
30
+ command = "new UiSelector()"
31
+
32
+ id = resolve_id(hash[:id])
33
+ command = "#{ command }.resourceId(\"#{ %(#{ id }) }\")" if id && id.kind_of?(String)
34
+ command = "#{ command }.resourceIdMatches(\".*#{ %(#{ id.source }) }.*\")" if id && id.kind_of?(Regexp)
35
+ command = "#{ command }.description(\"#{ %(#{ hash[:desc] }) }\")" if hash[:desc] && hash[:desc].kind_of?(String)
36
+ command = "#{ command }.descriptionMatches(\".*#{ %(#{ hash[:desc].source }) }.*\")" if hash[:desc] && hash[:desc].kind_of?(Regexp)
37
+ command = "#{ command }.className(\"#{ %(#{ hash[:class] }) }\")" if hash[:class] && hash[:class].kind_of?(String)
38
+ command = "#{ command }.classNameMatches(\".*#{ %(#{ hash[:class].source }) }.*\")" if hash[:class] && hash[:class].kind_of?(Regexp)
39
+ command = "#{ command }.text(\"#{ %(#{ hash[:text] }) }\")" if hash[:text] && hash[:text].kind_of?(String)
40
+ command = "#{ command }.textMatches(\".*#{ %(#{ hash[:text].source }) }.*\")" if hash[:text] && hash[:text].kind_of?(Regexp)
41
+ command = "#{ command }.packageName(\"#{ %(#{ hash[:package] }) }\")" if hash[:package] && hash[:package].kind_of?(String)
42
+ command = "#{ command }.packageNameMatches(\".*#{ %(#{ hash[:package].source }) }.*\")" if hash[:package] && hash[:package].kind_of?(Regexp)
43
+
44
+ command = "#{ command }.longClickable(#{ hash[:long_clickable] })" if hash[:long_clickable]
45
+ command = "#{ command }.checkable(#{ hash[:checkable] })" unless hash[:checkable].nil?
46
+ command = "#{ command }.checked(#{ hash[:checked] })" unless hash[:checked].nil?
47
+ command = "#{ command }.clickable(#{ hash[:clickable] })" unless hash[:clickable].nil?
48
+ command = "#{ command }.enabled(#{ hash[:enabled] })" unless hash[:enabled].nil?
49
+ command = "#{ command }.focusable(#{ hash[:focusable] })" unless hash[:focusable].nil?
50
+ command = "#{ command }.focused(#{ hash[:focused] })" unless hash[:focused].nil?
51
+ command = "#{ command }.index(#{ hash[:index].to_s })" unless hash[:index].nil?
52
+ command = "#{ command }.selected(#{ hash[:selected] })" unless hash[:selected].nil?
53
+ command = "#{ command }.scrollable(#{ hash[:scrollable] })" unless hash[:scrollable].nil?
54
+
55
+ command += ".instance(0)" if single
56
+
57
+ command
58
+ end
59
+
60
+
61
+ # supported selectors
62
+ # id: "com.my.package:id/myId"
63
+ # id: "myId" => will be converted to "com.my.package:id/myId"
64
+ # id: /my/ will find all elements with ids that contain my
65
+ # desc: "element description"
66
+ # desc: /ription/ will find all elements that contains ription
67
+ # class: "android.widget.Button"
68
+ # class: /Button/ will find all elements with classes that contain Button
69
+ # text: "Hello world"
70
+ # text: /ello/ will find all elements with text that contain ello
71
+ # package: "com.my.package"
72
+ # package: /my/ will find all elements with package that contains my
73
+ # long_clickable: true or false
74
+ # checkable: true or false
75
+ # checked: true or false
76
+ # clickable: true or false
77
+ # enabled: true or false
78
+ # focusable: true or false
79
+ # focused: true or false
80
+ # index: child index inside of a parent element, index starts from 0
81
+ # selected: true or false
82
+ # scrollable: true or false
83
+ # @param [Hash] hash selectors for finding elements
84
+ # @param [Boolean] single should the command return first instance or all of matched elements
85
+ # @return [String] hash selectors converted to xpath command
86
+ def hash_to_xpath(device, hash, single = true)
87
+ for_android = device == :android
88
+
89
+
90
+ command = "//"
91
+
92
+
93
+
94
+ if for_android
95
+ id = resolve_id(hash[:id])
96
+ if hash[:class] && hash[:class].kind_of?(String)
97
+ command = "#{ command }#{hash[:class] }"
98
+ elsif hash[:class] && hash[:class].kind_of?(Regexp)
99
+ command = "#{ command}*[contains(@class, \"#{ %(#{hash[:class].source }) }\")]"
100
+ else
101
+ command = "#{command}*"
102
+ end
103
+
104
+ # TODO: with new uiautomator sever you can use matches to look with regex instead of contains
105
+ command = "#{ command }[@resource-id=\"#{ %(#{ id }) }\"]" if id && id.kind_of?(String)
106
+ command = "#{ command }[contains(@resource-id, \"#{ %(#{ id.source }) }\")]" if id && id.kind_of?(Regexp)
107
+ command = "#{ command }[@content-desc=\"#{ %(#{hash[:desc] }) }\"]" if hash[:desc] && hash[:desc].kind_of?(String)
108
+ command = "#{ command }[contains(@content-desc, \"#{ %(#{hash[:desc].source }) }\")]" if hash[:desc] && hash[:desc].kind_of?(Regexp)
109
+ command = "#{ command }[contains(@class, \"#{ %(#{hash[:class].source }) }\")]" if hash[:class] && hash[:class].kind_of?(Regexp)
110
+ command = "#{ command }[@text=\"#{ %(#{hash[:text] }) }\"]" if hash[:text] && hash[:text].kind_of?(String)
111
+ command = "#{ command }[contains(@text, \"#{ %(#{hash[:text].source }) }\")]" if hash[:text] && hash[:text].kind_of?(Regexp)
112
+ command = "#{ command }[@package=\"#{ %(#{hash[:package] }) }\"]" if hash[:package] && hash[:package].kind_of?(String)
113
+ command = "#{ command }[contains=(@package, \"#{ %(#{hash[:package].source }) }\")]" if hash[:package] && hash[:package].kind_of?(Regexp)
114
+ command = "#{ command }[@long-clickable=\"#{ hash[:long_clickable] }\"]" if hash[:long_clickable]
115
+ command = "#{ command }[@checkable=\"#{ hash[:checkable] }\"]" unless hash[:checkable].nil?
116
+ command = "#{ command }[@checked=\"#{ hash[:checked] }\"]" unless hash[:checked].nil?
117
+ command = "#{ command }[@clickable=\"#{ hash[:clickable] }\"]" unless hash[:clickable].nil?
118
+ command = "#{ command }[@enabled=\"#{ hash[:enabled] }\"]" unless hash[:enabled].nil?
119
+ command = "#{ command }[@focusable=\"#{ hash[:focusable] }\"]" unless hash[:focusable].nil?
120
+ command = "#{ command }[@focused=\"#{ hash[:focused] }\"]" unless hash[:focused].nil?
121
+ command = "#{ command }[@index=\"#{ hash[:index] }\"]" unless hash[:index].nil?
122
+ command = "#{ command }[@selected=\"#{ hash[:selected] }\"]" unless hash[:selected].nil?
123
+
124
+ # it seems like you cannot query by scrollable
125
+ # command = "#{ command }[@scrollable=\"#{ hash[:scrollable] }\"]" unless hash[:scrollable].nil?
126
+ else
127
+
128
+ hash[:type] = hash[:class] unless hash[:class].nil?
129
+ if hash[:type] && hash[:type].kind_of?(String)
130
+ command = "#{ command }#{hash[:type] }"
131
+ elsif hash[:type] && hash[:type].kind_of?(Regexp)
132
+ command = "#{ command}*[contains(@type, \"#{ %(#{hash[:type].source }) }\")]"
133
+ else
134
+ command = "#{command}*"
135
+ end
136
+
137
+
138
+ # # ios specific
139
+ hash[:label] = hash[:text] unless hash[:text].nil?
140
+ hash[:name] = hash[:id] unless hash[:id].nil?
141
+
142
+ command = "#{ command }[@enabled=\"#{ hash[:enabled] }\"]" unless hash[:enabled].nil?
143
+ command = "#{ command }[@label=\"#{ %(#{hash[:label] }) }\"]" if hash[:label] && hash[:label].kind_of?(String)
144
+ command = "#{ command }[contains(@label, \"#{ %(#{hash[:label].source }) }\")]" if hash[:label] && hash[:label].kind_of?(Regexp)
145
+ command = "#{ command }[@name=\"#{ %(#{hash[:name] }) }\"]" if hash[:name] && hash[:name].kind_of?(String)
146
+ command = "#{ command }[contains(@name, \"#{ %(#{hash[:name].source }) }\")]" if hash[:name] && hash[:name].kind_of?(Regexp)
147
+ command = "#{ command }[@value=\"#{ %(#{hash[:value] }) }\"]" if hash[:value] && hash[:value].kind_of?(String)
148
+ command = "#{ command }[contains(@value, \"#{ %(#{hash[:value].source }) }\")]" if hash[:value] && hash[:value].kind_of?(Regexp)
149
+ command = "#{ command }[@width=\"#{ hash[:width] }\"]" unless hash[:width].nil?
150
+ command = "#{ command }[@height=\"#{ hash[:height] }\"]" unless hash[:height].nil?
151
+ command = "#{ command }[@visible=\"#{ hash[:visible] }\"]" unless hash[:visible].nil?
152
+ command = "#{ command }[@index=\"#{ hash[:index] }\"]" unless hash[:index].nil?
153
+ end
154
+
155
+
156
+ command += "[1]" if single
157
+
158
+ command
159
+ end
160
+
161
+
162
+ def hash_to_class_chain(hash, single = true)
163
+ command = "**/"
164
+
165
+ hash[:type] = hash[:class] unless hash[:class].nil?
166
+ hash[:label] = hash[:text] unless hash[:text].nil?
167
+ hash[:name] = hash[:id] unless hash[:id].nil?
168
+ if hash[:type] && hash[:type].kind_of?(String)
169
+ command = "#{ command }#{hash[:type] }"
170
+ else
171
+ command = "#{command}*"
172
+ end
173
+
174
+ command = "#{ command }[`enabled == #{ hash[:enabled] }`]" unless hash[:enabled].nil?
175
+ command = "#{ command }[`label == \"#{ %(#{hash[:label] }) }\"`]" if hash[:label] && hash[:label].kind_of?(String)
176
+ command = "#{ command }[`label CONTAINS \"#{ %(#{hash[:label].source }) }\"`]" if hash[:label] && hash[:label].kind_of?(Regexp)
177
+ command = "#{ command }[`name == \"#{ %(#{hash[:name] }) }\"`]" if hash[:name] && hash[:name].kind_of?(String)
178
+ command = "#{ command }[`name CONTAINS \"#{ %(#{hash[:name].source }) }\"`]" if hash[:name] && hash[:name].kind_of?(Regexp)
179
+ command = "#{ command }[`value == \"#{ %(#{hash[:value] }) }\"`]" if hash[:value] && hash[:value].kind_of?(String)
180
+ command = "#{ command }[`value CONTAINS \"#{ %(#{hash[:value].source }) }\"`]" if hash[:value] && hash[:value].kind_of?(Regexp)
181
+ command = "#{ command }[`visible == #{ hash[:visible] }`]" unless hash[:visible].nil?
182
+
183
+ command += "[1]" if single
184
+
185
+ command
186
+ end
187
+
188
+ # check if selectors are for a scrollable element
189
+ # @param [Boolean] single should the command return first instance or all of matched elements
190
+ # @param [Hash] selectors for fetching elements
191
+ # @return [Boolean] true if element has scrollable attribute true or is class one of (RecyclerView, HorizontalScrollView, ScrollView, ListView)
192
+ def is_scrollable_selector?(selectors, single)
193
+ return false unless single
194
+ return true if selectors[:scrollable]
195
+ if selectors[:class] == "androidx.recyclerview.widget.RecyclerView" ||
196
+ selectors[:class] == "android.widget.HorizontalScrollView" ||
197
+ selectors[:class] == "android.widget.ScrollView" ||
198
+ selectors[:class] == "android.widget.ListView"
199
+ return true
200
+ elsif selectors[:type] == "XCUIElementTypeScrollView" ||
201
+ selectors[:type] == "XCUIElementTypeTable"
202
+ return true
203
+ end
204
+ false
205
+ end
206
+
207
+ #noinspection RubyUnnecessaryReturnStatement,RubyUnusedLocalVariable
208
+ # separate selectors from given hash parameters
209
+ # @param [Hash] params
210
+ # @return [Array] first element is params, second are selectors
211
+ def extract_selectors_from_params(params = {})
212
+ selectors = params.select { |key, value| [
213
+ :id,
214
+ :long_clickable,
215
+ :desc,
216
+ :class,
217
+ :text,
218
+ :package,
219
+ :checkable,
220
+ :checked,
221
+ :clickable,
222
+ :enabled,
223
+ :focusable,
224
+ :focused,
225
+ :index,
226
+ :selected,
227
+ :scrollable,
228
+
229
+ # ios specific
230
+ :type,
231
+ :label,
232
+ :x,
233
+ :y,
234
+ :width,
235
+ :height,
236
+ :visible,
237
+ :name,
238
+ :value,
239
+
240
+ :image
241
+ ].include?(key) }
242
+ params = Hash[params.to_a - selectors.to_a]
243
+
244
+ # default params
245
+ params[:single] = true if params[:single].nil?
246
+ params[:scrollable_locator] = nil if params[:scrollable_locator].nil?
247
+ if params[:default_find_strategy].nil?
248
+ params[:default_find_strategy] = DEFAULT_ANDROID_FIND_STRATEGY if @driver.device == :android
249
+ params[:default_find_strategy] = DEFAULT_IOS_FIND_STRATEGY if @driver.device == :ios || @driver.device == :tvos
250
+ end
251
+ if params[:default_scroll_strategy].nil?
252
+ params[:default_scroll_strategy] = DEFAULT_ANDROID_SCROLL_STRATEGY if @driver.device == :android
253
+ params[:default_scroll_strategy] = DEFAULT_IOS_SCROLL_STRATEGY if @driver.device == :ios || @driver.device == :tvos
254
+ end
255
+ return params, selectors
256
+ end
257
+
258
+
259
+ def resolve_id(id)
260
+ if id && id.kind_of?(String) && !id.match?(/.*:id\//)
261
+ # shorthand ids like myId make full ids => my.app.package:id/myId
262
+ if id[0] == "="
263
+ return id[1..-1]
264
+ else
265
+ return "#{@driver.current_package}:id/#{id}"
266
+ end
267
+ else
268
+ id
269
+ end
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,390 @@
1
+ module TestaAppiumDriver
2
+ # noinspection RubyTooManyMethodsInspection
3
+ class Locator
4
+ # performs a long tap on the retrieved element
5
+ # @param [Float] duration in seconds
6
+ def long_tap(duration = LONG_TAP_DURATION)
7
+ action_builder = @driver.action
8
+ b = bounds
9
+ f1 = action_builder.add_pointer_input(:touch, "finger1")
10
+ f1.create_pointer_move(duration: 0, x: b.center.x, y: b.center.y, origin: ::Selenium::WebDriver::Interactions::PointerMove::VIEWPORT)
11
+ f1.create_pointer_down(:left)
12
+ f1.create_pause(duration)
13
+ f1.create_pointer_up(:left)
14
+ puts "long tap execute: {x: #{b.center.x}, y: #{b.center.y}}"
15
+ @driver.perform_actions [f1]
16
+ end
17
+
18
+ # scrolls to the start of the scrollable containers and scrolls to the end,
19
+ # everytime a locator element is found the given block is executed
20
+ # @return [Array<Selenium::WebDriver::Element>]
21
+ def scroll_each(top: nil, bottom: nil, right: nil, left: nil, direction: nil, &block)
22
+ deadzone = _process_deadzone(top, bottom, right, left)
23
+ raise "Each can only be performed on multiple elements locator" if @single
24
+
25
+ deadzone = @scrollable_locator.scroll_deadzone if deadzone.nil? && !@scrollable_locator.nil?
26
+ sa = ScrollActions.new(@scrollable_locator,
27
+ locator: self,
28
+ deadzone: deadzone,
29
+ default_scroll_strategy: @default_scroll_strategy)
30
+ if direction.nil?
31
+ sa.scroll_each(&block)
32
+ else
33
+ sa.send("scroll_each_#{direction}", &block)
34
+ end
35
+ end
36
+
37
+ # scrolls down from the current page view (without prior scrolling to the top) and
38
+ # everytime a locator element is found the given block is executed
39
+ # @return [Array<Selenium::WebDriver::Element>]
40
+ def scroll_each_down(top: nil, bottom: nil, right: nil, left: nil, &block)
41
+ scroll_each(top: top, bottom: bottom, right: right, left: left, direction: :down, &block)
42
+ end
43
+
44
+ # scrolls up from the current page view (without prior scrolling to the bottom) and
45
+ # everytime a locator element is found the given block is executed
46
+ # @return [Array<Selenium::WebDriver::Element>]
47
+ def scroll_each_up(top: nil, bottom: nil, right: nil, left: nil, &block)
48
+ scroll_each(top: top, bottom: bottom, right: right, left: left, direction: :up, &block)
49
+ end
50
+
51
+ # scrolls right from the current page view (without prior scrolling to the left) and
52
+ # everytime a locator element is found the given block is executed
53
+ # @return [Array<Selenium::WebDriver::Element>]
54
+ def scroll_each_right(top: nil, bottom: nil, right: nil, left: nil, &block)
55
+ scroll_each(top: top, bottom: bottom, right: right, left: left, direction: :right, &block)
56
+ end
57
+
58
+ # scrolls left from the current page view (without prior scrolling to the right) and
59
+ # everytime a locator element is found the given block is executed
60
+ # @return [Array<Selenium::WebDriver::Element>]
61
+ def scroll_each_left(top: nil, bottom: nil, right: nil, left: nil, &block)
62
+ scroll_each(top: top, bottom: bottom, right: right, left: left, direction: :left, &block)
63
+ end
64
+
65
+ # Aligns element (by default) on top of the scrollable container, if the element does not exists it will scroll to find it
66
+ # The element is aligned if the the distance from the top/bottom/right/left of the scrollable container is less than [TestaAppiumDriver::SCROLL_ALIGNMENT_THRESHOLD]
67
+ # If the distance is greater than the threshold, it will attempt to realign it up to 2 more times.
68
+ # The retry mechanism allows alignment even for dynamic layouts when elements are hidden/show when scrolling to certain direction
69
+ # @return [TestaAppiumDriver::Locator]
70
+ def align(with = :top, top: nil, bottom: nil, right: nil, left: nil, scroll_to_find: false, max_attempts: 3)
71
+ deadzone = _process_deadzone(top, bottom, right, left)
72
+ deadzone = @scrollable_locator.scroll_deadzone if deadzone.nil? && !@scrollable_locator.nil?
73
+ sa = ScrollActions.new(@scrollable_locator,
74
+ locator: self,
75
+ deadzone: deadzone,
76
+ default_scroll_strategy: @default_scroll_strategy)
77
+ sa.align(with, scroll_to_find, max_attempts)
78
+ self
79
+ end
80
+
81
+ # Aligns element on top of the scrollable container, if the element does not exists it will scroll to find it
82
+ # The element is aligned if the the distance from the top of the scrollable container is less than [TestaAppiumDriver::SCROLL_ALIGNMENT_THRESHOLD]
83
+ # If the distance is greater than the threshold, it will attempt to realign it up to 2 more times.
84
+ # The retry mechanism allows alignment even for dynamic layouts when elements are hidden/show when scrolling to certain direction
85
+ # @return [TestaAppiumDriver::Locator]
86
+ def align_top(top: nil, bottom: nil, right: nil, left: nil, max_attempts: 3)
87
+ align(:top, top: top, bottom: bottom, right: right, left: left, max_attempts: max_attempts)
88
+ end
89
+
90
+ # Aligns element on bottom of the scrollable container, if the element does not exists it will scroll to find it
91
+ # The element is aligned if the the distance from the bottom of the scrollable container is less than [TestaAppiumDriver::SCROLL_ALIGNMENT_THRESHOLD]
92
+ # If the distance is greater than the threshold, it will attempt to realign it up to 2 more times.
93
+ # The retry mechanism allows alignment even for dynamic layouts when elements are hidden/show when scrolling to certain direction
94
+ # @return [TestaAppiumDriver::Locator]
95
+ def align_bottom(top: nil, bottom: nil, right: nil, left: nil, max_attempts: 3)
96
+ align(:bottom, top: top, bottom: bottom, right: right, left: left, max_attempts: max_attempts)
97
+ end
98
+
99
+ # Aligns element on left of the scrollable container, if the element does not exists it will scroll to find it
100
+ # The element is aligned if the the distance from the left of the scrollable container is less than [TestaAppiumDriver::SCROLL_ALIGNMENT_THRESHOLD]
101
+ # If the distance is greater than the threshold, it will attempt to realign it up to 2 more times.
102
+ # The retry mechanism allows alignment even for dynamic layouts when elements are hidden/show when scrolling to certain direction
103
+ # @return [TestaAppiumDriver::Locator]
104
+ def align_left(top: nil, bottom: nil, right: nil, left: nil, max_attempts: 3)
105
+ align(:left, top: top, bottom: bottom, right: right, left: left, max_attempts: max_attempts)
106
+ end
107
+
108
+ # Aligns element on right of the scrollable container, if the element does not exists it will scroll to find it
109
+ # The element is aligned if the the distance from the right of the scrollable container is less than [TestaAppiumDriver::SCROLL_ALIGNMENT_THRESHOLD]
110
+ # If the distance is greater than the threshold, it will attempt to realign it up to 2 more times.
111
+ # The retry mechanism allows alignment even for dynamic layouts when elements are hidden/show when scrolling to certain direction
112
+ # @return [TestaAppiumDriver::Locator]
113
+ def align_right(top: nil, bottom: nil, right: nil, left: nil, max_attempts: 3)
114
+ align(:right, top: top, bottom: bottom, right: right, left: left, max_attempts: max_attempts)
115
+ end
116
+
117
+ # Aligns element (by default) on top of the scrollable container, if the element does not exists it raise an exception
118
+ # The element is aligned if the the distance from the top/bottom/right/left of the scrollable container is less than [TestaAppiumDriver::SCROLL_ALIGNMENT_THRESHOLD]
119
+ # If the distance is greater than the threshold, it will attempt to realign it up to 2 more times.
120
+ # The retry mechanism allows alignment even for dynamic layouts when elements are hidden/show when scrolling to certain direction
121
+ # @return [TestaAppiumDriver::Locator]
122
+ def align!(with = :top, top: nil, bottom: nil, right: nil, left: nil, max_attempts: 3)
123
+ align(with, top: top, bottom: bottom, right: right, left: left, scroll_to_find: true, max_attempts: max_attempts)
124
+ end
125
+
126
+ # Aligns element on top of the scrollable container, if the element does not exists it raise an exception
127
+ # The element is aligned if the the distance from the top of the scrollable container is less than [TestaAppiumDriver::SCROLL_ALIGNMENT_THRESHOLD]
128
+ # If the distance is greater than the threshold, it will attempt to realign it up to 2 more times.
129
+ # The retry mechanism allows alignment even for dynamic layouts when elements are hidden/show when scrolling to certain direction
130
+ # @return [TestaAppiumDriver::Locator]
131
+ def align_top!(top: nil, bottom: nil, right: nil, left: nil, max_attempts: 3)
132
+ align(:top, top: top, bottom: bottom, right: right, left: left, scroll_to_find: true, max_attempts: max_attempts)
133
+ end
134
+
135
+ # Aligns element on bottom of the scrollable container, if the element does not exists it raise an exception
136
+ # The element is aligned if the the distance from the bottom of the scrollable container is less than [TestaAppiumDriver::SCROLL_ALIGNMENT_THRESHOLD]
137
+ # If the distance is greater than the threshold, it will attempt to realign it up to 2 more times.
138
+ # The retry mechanism allows alignment even for dynamic layouts when elements are hidden/show when scrolling to certain direction
139
+ # @return [TestaAppiumDriver::Locator]
140
+ def align_bottom!(top: nil, bottom: nil, right: nil, left: nil, max_attempts: 3)
141
+ align(:bottom, top: top, bottom: bottom, right: right, left: left, scroll_to_find: true, max_attempts: max_attempts)
142
+ end
143
+
144
+ # Aligns element on left of the scrollable container, if the element does not exists it raise an exception
145
+ # The element is aligned if the the distance from the left of the scrollable container is less than [TestaAppiumDriver::SCROLL_ALIGNMENT_THRESHOLD]
146
+ # If the distance is greater than the threshold, it will attempt to realign it up to 2 more times.
147
+ # The retry mechanism allows alignment even for dynamic layouts when elements are hidden/show when scrolling to certain direction
148
+ # @return [TestaAppiumDriver::Locator]
149
+ def align_left!(top: nil, bottom: nil, right: nil, left: nil, max_attempts: 3)
150
+ align(:left, top: top, bottom: bottom, right: right, left: left, scroll_to_find: true, max_attempts: max_attempts)
151
+ end
152
+
153
+ # Aligns element on right of the scrollable container, if the element does not exists it raise an exception
154
+ # The element is aligned if the the distance from the right of the scrollable container is less than [TestaAppiumDriver::SCROLL_ALIGNMENT_THRESHOLD]
155
+ # If the distance is greater than the threshold, it will attempt to realign it up to 2 more times.
156
+ # The retry mechanism allows alignment even for dynamic layouts when elements are hidden/show when scrolling to certain direction
157
+ # @return [TestaAppiumDriver::Locator]
158
+ def align_right!(top: nil, bottom: nil, right: nil, left: nil, max_attempts: 3)
159
+ align(:right, top: top, bottom: bottom, right: right, left: left, scroll_to_find: true, max_attempts: max_attempts)
160
+ end
161
+
162
+ # First scrolls to the beginning of the scrollable container and then scrolls down until element is found or end is reached
163
+ # @return [TestaAppiumDriver::Locator]
164
+ def scroll_to(top: nil, bottom: nil, right: nil, left: nil, max_scrolls: nil, direction: nil)
165
+ if direction
166
+ _scroll_dir_to(_process_deadzone(top, bottom, right, left), max_scrolls, direction)
167
+ else
168
+ _scroll_to(_process_deadzone(top, bottom, right, left), max_scrolls)
169
+ end
170
+ end
171
+
172
+ # Scrolls down until element is found or end is reached
173
+ # @return [TestaAppiumDriver::Locator]
174
+ def scroll_down_to(top: nil, bottom: nil, right: nil, left: nil, max_scrolls: nil)
175
+ _scroll_dir_to(_process_deadzone(top, bottom, right, left), max_scrolls, :down)
176
+ end
177
+
178
+ # Scrolls up until element is found or end is reached
179
+ # @return [TestaAppiumDriver::Locator]
180
+ def scroll_up_to(top: nil, bottom: nil, right: nil, left: nil, max_scrolls: nil)
181
+ _scroll_dir_to(_process_deadzone(top, bottom, right, left), max_scrolls, :up)
182
+ end
183
+
184
+ # Scrolls right until element is found or end is reached
185
+ # @return [TestaAppiumDriver::Locator]
186
+ def scroll_right_to(top: nil, bottom: nil, right: nil, left: nil, max_scrolls: nil)
187
+ _scroll_dir_to(_process_deadzone(top, bottom, right, left), max_scrolls, :right)
188
+ end
189
+
190
+ # Scrolls left until element is found or end is reached
191
+ # @return [TestaAppiumDriver::Locator]
192
+ def scroll_left_to(top: nil, bottom: nil, right: nil, left: nil, max_scrolls: nil)
193
+ _scroll_dir_to(_process_deadzone(top, bottom, right, left), max_scrolls, :left)
194
+ end
195
+
196
+ # Scrolls to the start of the scrollable container (top on vertical container, left on horizontal)
197
+ # @return [TestaAppiumDriver::Locator]
198
+ def scroll_to_start(top: nil, bottom: nil, right: nil, left: nil)
199
+ _scroll_to_start_or_end(:start, _process_deadzone(top, bottom, right, left))
200
+ end
201
+
202
+ # Scrolls to the end of the scrollable container (bottom on vertical container, right on horizontal)
203
+ # @return [TestaAppiumDriver::Locator]
204
+ def scroll_to_end(top: nil, bottom: nil, right: nil, left: nil)
205
+ _scroll_to_start_or_end(:end, _process_deadzone(top, bottom, right, left))
206
+ end
207
+
208
+ # @return [TestaAppiumDriver::Locator]
209
+ def page_down(top: nil, bottom: nil, right: nil, left: nil)
210
+ _page(:down, _process_deadzone(top, bottom, right, left))
211
+ end
212
+
213
+ # @return [TestaAppiumDriver::Locator]
214
+ def page_up(top: nil, bottom: nil, right: nil, left: nil)
215
+ _page(:up, _process_deadzone(top, bottom, right, left))
216
+ end
217
+
218
+ # @return [TestaAppiumDriver::Locator]
219
+ def page_left(top: nil, bottom: nil, right: nil, left: nil)
220
+ _page(:left, _process_deadzone(top, bottom, right, left))
221
+ end
222
+
223
+ # @return [TestaAppiumDriver::Locator]
224
+ def page_right(top: nil, bottom: nil, right: nil, left: nil)
225
+ _page(:right, _process_deadzone(top, bottom, right, left))
226
+ end
227
+
228
+ # @return [TestaAppiumDriver::Locator]
229
+ def fling_down(top: nil, bottom: nil, right: nil, left: nil)
230
+ _fling(:down, _process_deadzone(top, bottom, right, left))
231
+ end
232
+
233
+ # @return [TestaAppiumDriver::Locator]
234
+ def fling_up(top: nil, bottom: nil, right: nil, left: nil)
235
+ _fling(:up, _process_deadzone(top, bottom, right, left))
236
+ end
237
+
238
+ # @return [TestaAppiumDriver::Locator]
239
+ def fling_left(top: nil, bottom: nil, right: nil, left: nil)
240
+ _fling(:left, _process_deadzone(top, bottom, right, left))
241
+ end
242
+
243
+ # @return [TestaAppiumDriver::Locator]
244
+ def fling_right(top: nil, bottom: nil, right: nil, left: nil)
245
+ _fling(:right, _process_deadzone(top, bottom, right, left))
246
+ end
247
+
248
+ def drag_up_by(amount)
249
+ drag_by(amount, direction: :top)
250
+ end
251
+
252
+ def drag_down_by(amount)
253
+ drag_by(amount, direction: :bottom)
254
+ end
255
+
256
+ def drag_left_by(amount)
257
+ drag_by(amount, direction: :left)
258
+ end
259
+
260
+ def drag_right_by(amount)
261
+ drag_by(amount, direction: :right)
262
+ end
263
+
264
+ # @param [TestaAppiumDriver::Locator, Hash, Selenium::WebDriver::Element, String] to
265
+ # noinspection RubyYardParamTypeMatch,RubyScope
266
+ def drag_to(to)
267
+ if !to.kind_of?(::Selenium::WebDriver::Element) && !to.kind_of?(::Appium::Core::Element) && !to.kind_of?(TestaAppiumDriver::Locator) && !to.kind_of?(Hash)
268
+ raise "Parameter not accepted, acceptable instances of [TestaAppiumDriver::Locator, Hash, Selenium::WebDriver::Element]"
269
+ end
270
+
271
+ if to.kind_of?(::Selenium::WebDriver::Element) || to.kind_of?(::Appium::Core::Element)
272
+ bounds = TestaAppiumDriver::Bounds.from_android(to.bounds, @driver)
273
+ x = bounds.center.x
274
+ y = bounds.center.y
275
+ end
276
+ if to.kind_of?(TestaAppiumDriver::Locator)
277
+ bounds = to.bounds
278
+ x = bounds.center.x
279
+ y = bounds.center.y
280
+ end
281
+ if to.kind_of?(Hash)
282
+ raise "Missing x coordinate" if to[:x].nil?
283
+ raise "Missing y coordinate" if to[:y].nil?
284
+
285
+ x = to[:x]
286
+ y = to[:y]
287
+ end
288
+ _drag_to(bounds.center.x, bounds.center.y, x, y)
289
+ end
290
+
291
+ def drag_by(amount, direction: :top)
292
+ b = bounds
293
+ x = b.center.x
294
+ y = b.center.y
295
+ case direction
296
+ when :top
297
+ y -= amount.to_i
298
+ when :bottom
299
+ y += amount.to_i
300
+ when :left
301
+ x -= amount.to_i
302
+ when :right
303
+ x += amount.to_i
304
+ else
305
+ raise "Unknown direction #{direction}"
306
+ end
307
+ _drag_to(b.center.x, b.center.y, x, y)
308
+ end
309
+
310
+ private
311
+ def _process_deadzone(top, bottom, right, left)
312
+ deadzone = nil
313
+ if !top.nil? || !bottom.nil? || !right.nil? || !left.nil?
314
+ deadzone = {}
315
+ deadzone[:top] = top unless top.nil?
316
+ deadzone[:bottom] = bottom unless bottom.nil?
317
+ deadzone[:right] = right unless right.nil?
318
+ deadzone[:left] = left unless left.nil?
319
+ end
320
+ deadzone
321
+ end
322
+
323
+ def _drag_to(x0, y0, x1, y1)
324
+ sa = ScrollActions.new(@scrollable_locator,
325
+ locator: self,
326
+ default_scroll_strategy: @default_scroll_strategy)
327
+ sa.drag_to(x0, y0, x1, y1)
328
+ self
329
+ end
330
+
331
+ def _page(direction, deadzone)
332
+ deadzone = @scrollable_locator.scroll_deadzone if deadzone.nil? && !@scrollable_locator.nil?
333
+ sa = ScrollActions.new(@scrollable_locator,
334
+ locator: self,
335
+ deadzone: deadzone,
336
+ direction: direction.to_sym,
337
+ default_scroll_strategy: @default_scroll_strategy)
338
+ sa.send("page_#{direction}")
339
+ self
340
+ end
341
+
342
+ def _fling(direction, deadzone)
343
+ deadzone = @scrollable_locator.scroll_deadzone if deadzone.nil? && !@scrollable_locator.nil?
344
+ sa = ScrollActions.new(@scrollable_locator,
345
+ locator: self,
346
+ deadzone: deadzone,
347
+ direction: direction.to_sym,
348
+ default_scroll_strategy: @default_scroll_strategy)
349
+ sa.send("fling_#{direction}")
350
+ self
351
+ end
352
+
353
+ def _scroll_to_start_or_end(type, deadzone)
354
+ deadzone = @scrollable_locator.scroll_deadzone if deadzone.nil? && !@scrollable_locator.nil?
355
+ sa = ScrollActions.new(@scrollable_locator,
356
+ locator: self,
357
+ deadzone: deadzone,
358
+ default_scroll_strategy: @default_scroll_strategy)
359
+ if type == :start
360
+ sa.scroll_to_start
361
+ else
362
+ sa.scroll_to_end
363
+ end
364
+ self
365
+ end
366
+
367
+ def _scroll_to(deadzone, max_scrolls)
368
+ deadzone = @scrollable_locator.scroll_deadzone if deadzone.nil? && !@scrollable_locator.nil?
369
+ sa = ScrollActions.new(@scrollable_locator,
370
+ locator: self,
371
+ deadzone: deadzone,
372
+ max_scrolls: max_scrolls,
373
+ default_scroll_strategy: @default_scroll_strategy)
374
+ sa.scroll_to
375
+ self
376
+ end
377
+
378
+ def _scroll_dir_to(deadzone, max_scrolls, direction)
379
+ deadzone = @scrollable_locator.scroll_deadzone if deadzone.nil? && !@scrollable_locator.nil?
380
+ sa = ScrollActions.new(@scrollable_locator,
381
+ locator: self,
382
+ deadzone: deadzone,
383
+ max_scrolls: max_scrolls,
384
+ default_scroll_strategy: @default_scroll_strategy)
385
+
386
+ sa.send("scroll_#{direction}_to")
387
+ self
388
+ end
389
+ end
390
+ end