testa_appium_driver 0.1.21 → 0.1.24

Sign up to get free protection for your applications and to get access to all the features.
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