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,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ #noinspection ALL
4
+ module TestaAppiumDriver
5
+ FIND_STRATEGY_UIAUTOMATOR = :uiautomator
6
+ FIND_STRATEGY_XPATH = :xpath
7
+
8
+ SCROLL_STRATEGY_UIAUTOMATOR = :uiautomator
9
+ SCROLL_STRATEGY_W3C = :w3c
10
+
11
+
12
+ SCROLL_CORRECTION_W3C = 30
13
+ SCROLL_ALIGNMENT_THRESHOLD = 25
14
+
15
+ SCROLL_ACTION_TYPE_SCROLL = :scroll
16
+ SCROLL_ACTION_TYPE_FLING = :fling
17
+ SCROLL_ACTION_TYPE_DRAG = :drag
18
+
19
+
20
+ DEFAULT_UIAUTOMATOR_MAX_SWIPES = 20
21
+
22
+ DEFAULT_ANDROID_FIND_STRATEGY = FIND_STRATEGY_UIAUTOMATOR
23
+ DEFAULT_ANDROID_SCROLL_STRATEGY = SCROLL_STRATEGY_UIAUTOMATOR
24
+
25
+
26
+ DEFAULT_IOS_FIND_STRATEGY = FIND_STRATEGY_XPATH
27
+ DEFAULT_IOS_SCROLL_STRATEGY = SCROLL_STRATEGY_W3C
28
+
29
+ DEFAULT_W3C_MAX_SCROLLS = 7
30
+
31
+ EXISTS_WAIT = 0.5
32
+ LONG_TAP_DURATION = 1.5
33
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestaAppiumDriver
4
+ class StrategyMixException < Exception
5
+ def initialize(strategy, strategy_reason, mixed_strategy, mixed_reason)
6
+
7
+ # example: parent is only available in xpath strategy and cannot be used with from_element which is only available in uiautomator strategy
8
+ msg = "strategy mix exception: '#{strategy_reason}' is only available in #{strategy} strategy and cannot be used with '#{mixed_reason}' which is only available in #{mixed_strategy} strategy"
9
+ super(msg)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,242 @@
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
+ if hash[:id] && hash[:id].kind_of?(String) && !hash[:id].match?(/.*:id\//)
33
+ # shorthand ids like myId make full ids => my.app.package:id/myId
34
+
35
+ if hash[:id][0] == "="
36
+ id = hash[:id][1..-1]
37
+ else
38
+ id = "#{@driver.current_package}:id/#{hash[:id]}"
39
+ end
40
+ else
41
+ id = hash[:id]
42
+ end
43
+ command = "#{ command }.resourceId(\"#{ %(#{ id }) }\")" if id && id.kind_of?(String)
44
+ command = "#{ command }.resourceIdMatches(\"#{ %(#{ id.source }) }\")" if id && id.kind_of?(Regexp)
45
+ command = "#{ command }.description(\"#{ %(#{ hash[:desc] }) }\")" if hash[:desc] && hash[:desc].kind_of?(String)
46
+ command = "#{ command }.descriptionMatches(\"#{ %(#{ hash[:desc].source }) }\")" if hash[:desc] && hash[:desc].kind_of?(Regexp)
47
+ command = "#{ command }.className(\"#{ %(#{ hash[:class] }) }\")" if hash[:class] && hash[:class].kind_of?(String)
48
+ command = "#{ command }.classNameMatches(\"#{ %(#{ hash[:class].source }) }\")" if hash[:class] && hash[:class].kind_of?(Regexp)
49
+ command = "#{ command }.text(\"#{ %(#{ hash[:text] }) }\")" if hash[:text] && hash[:text].kind_of?(String)
50
+ command = "#{ command }.textMatches(\"#{ %(#{ hash[:text].source }) }\")" if hash[:text] && hash[:text].kind_of?(Regexp)
51
+ command = "#{ command }.packageName(\"#{ %(#{ hash[:package] }) }\")" if hash[:package] && hash[:package].kind_of?(String)
52
+ command = "#{ command }.packageNameMatches(\"#{ %(#{ hash[:package].source }) }\")" if hash[:package] && hash[:package].kind_of?(Regexp)
53
+
54
+ command = "#{ command }.longClickable(#{ hash[:long_clickable] })" if hash[:long_clickable]
55
+ command = "#{ command }.checkable(#{ hash[:checkable] })" unless hash[:checkable].nil?
56
+ command = "#{ command }.checked(#{ hash[:checked] })" unless hash[:checked].nil?
57
+ command = "#{ command }.clickable(#{ hash[:clickable] })" unless hash[:clickable].nil?
58
+ command = "#{ command }.enabled(#{ hash[:enabled] })" unless hash[:enabled].nil?
59
+ command = "#{ command }.focusable(#{ hash[:focusable] })" unless hash[:focusable].nil?
60
+ command = "#{ command }.focused(#{ hash[:focused] })" unless hash[:focused].nil?
61
+ command = "#{ command }.index(#{ hash[:index].to_s })" unless hash[:index].nil?
62
+ command = "#{ command }.selected(#{ hash[:selected] })" unless hash[:selected].nil?
63
+ command = "#{ command }.scrollable(#{ hash[:scrollable] })" unless hash[:scrollable].nil?
64
+
65
+ command += ".instance(0)" if single
66
+
67
+ command
68
+ end
69
+
70
+
71
+ # supported selectors
72
+ # id: "com.my.package:id/myId"
73
+ # id: "myId" => will be converted to "com.my.package:id/myId"
74
+ # id: /my/ will find all elements with ids that contain my
75
+ # desc: "element description"
76
+ # desc: /ription/ will find all elements that contains ription
77
+ # class: "android.widget.Button"
78
+ # class: /Button/ will find all elements with classes that contain Button
79
+ # text: "Hello world"
80
+ # text: /ello/ will find all elements with text that contain ello
81
+ # package: "com.my.package"
82
+ # package: /my/ will find all elements with package that contains my
83
+ # long_clickable: true or false
84
+ # checkable: true or false
85
+ # checked: true or false
86
+ # clickable: true or false
87
+ # enabled: true or false
88
+ # focusable: true or false
89
+ # focused: true or false
90
+ # index: child index inside of a parent element, index starts from 0
91
+ # selected: true or false
92
+ # scrollable: true or false
93
+ # @param [Hash] hash selectors for finding elements
94
+ # @param [Boolean] single should the command return first instance or all of matched elements
95
+ # @return [String] hash selectors converted to xpath command
96
+ def hash_to_xpath(device, hash, single = true)
97
+ for_android = device == :android
98
+
99
+
100
+ command = "//"
101
+
102
+ if hash[:id] && hash[:id].kind_of?(String) && !hash[:id].match?(/.*:id\//)
103
+ # shorthand ids like myId make full ids => my.app.package:id/myId
104
+ if hash[:id][0] == "="
105
+ id = hash[:id][1..-1]
106
+ else
107
+ id = "#{@driver.current_package}:id/#{hash[:id]}"
108
+ end
109
+ else
110
+ id = hash[:id]
111
+ end
112
+
113
+ if for_android
114
+ if hash[:class] && hash[:class].kind_of?(String)
115
+ command = "#{ command }#{hash[:class] }"
116
+ elsif hash[:class] && hash[:class].kind_of?(Regexp)
117
+ command = "#{ command}*[contains(@class, \"#{ %(#{hash[:class].source }) }\")]"
118
+ else
119
+ command = "#{command}*"
120
+ end
121
+
122
+
123
+ command = "#{ command }[@resource-id=\"#{ %(#{ id }) }\"]" if id && id.kind_of?(String)
124
+ command = "#{ command }[contains(@resource-id, \"#{ %(#{ id.source }) }\")]" if id && id.kind_of?(Regexp)
125
+ command = "#{ command }[@content-desc=\"#{ %(#{hash[:desc] }) }\"]" if hash[:desc] && hash[:desc].kind_of?(String)
126
+ command = "#{ command }[contains(@content-desc, \"#{ %(#{hash[:desc].source }) }\")]" if hash[:desc] && hash[:desc].kind_of?(Regexp)
127
+ command = "#{ command }[contains(@class, \"#{ %(#{hash[:class].source }) }\")]" if hash[:class] && hash[:class].kind_of?(Regexp)
128
+ command = "#{ command }[@text=\"#{ %(#{hash[:text] }) }\"]" if hash[:text] && hash[:text].kind_of?(String)
129
+ command = "#{ command }[contains(@text, \"#{ %(#{hash[:text].source }) }\")]" if hash[:text] && hash[:text].kind_of?(Regexp)
130
+ command = "#{ command }[@package=\"#{ %(#{hash[:package] }) }\"]" if hash[:package] && hash[:package].kind_of?(String)
131
+ command = "#{ command }[contains=(@package, \"#{ %(#{hash[:package].source }) }\")]" if hash[:package] && hash[:package].kind_of?(Regexp)
132
+ command = "#{ command }[@long-clickable=\"#{ hash[:long_clickable] }\"]" if hash[:long_clickable]
133
+ command = "#{ command }[@checkable=\"#{ hash[:checkable] }\"]" unless hash[:checkable].nil?
134
+ command = "#{ command }[@checked=\"#{ hash[:checked] }\"]" unless hash[:checked].nil?
135
+ command = "#{ command }[@clickable=\"#{ hash[:clickable] }\"]" unless hash[:clickable].nil?
136
+ command = "#{ command }[@enabled=\"#{ hash[:enabled] }\"]" unless hash[:enabled].nil?
137
+ command = "#{ command }[@focusable=\"#{ hash[:focusable] }\"]" unless hash[:focusable].nil?
138
+ command = "#{ command }[@focused=\"#{ hash[:focused] }\"]" unless hash[:focused].nil?
139
+ command = "#{ command }[@index=\"#{ hash[:index] }\"]" unless hash[:index].nil?
140
+ command = "#{ command }[@selected=\"#{ hash[:selected] }\"]" unless hash[:selected].nil?
141
+ command = "#{ command }[@scrollable=\"#{ hash[:scrollable] }\"]" unless hash[:scrollable].nil?
142
+ else
143
+ if hash[:type] && hash[:type].kind_of?(String)
144
+ command = "#{ command }#{hash[:type] }"
145
+ elsif hash[:type] && hash[:type].kind_of?(Regexp)
146
+ command = "#{ command}*[contains(@type, \"#{ %(#{hash[:type].source }) }\")]"
147
+ else
148
+ command = "#{command}*"
149
+ end
150
+
151
+
152
+ # # ios specific
153
+ hash[:label] = hash[:text] unless hash[:text].nil?
154
+ hash[:name] = hash[:id] unless hash[:id].nil?
155
+
156
+ command = "#{ command }[@enabled=\"#{ hash[:enabled] }\"]" unless hash[:enabled].nil?
157
+ command = "#{ command }[@label=\"#{ %(#{hash[:label] }) }\"]" if hash[:label] && hash[:label].kind_of?(String)
158
+ command = "#{ command }[contains(@label, \"#{ %(#{hash[:label].source }) }\")]" if hash[:label] && hash[:label].kind_of?(Regexp)
159
+ command = "#{ command }[@name=\"#{ %(#{hash[:name] }) }\"]" if hash[:name] && hash[:name].kind_of?(String)
160
+ command = "#{ command }[contains(@name, \"#{ %(#{hash[:name].source }) }\")]" if hash[:name] && hash[:name].kind_of?(Regexp)
161
+ command = "#{ command }[@value=\"#{ %(#{hash[:value] }) }\"]" if hash[:value] && hash[:value].kind_of?(String)
162
+ command = "#{ command }[contains(@value, \"#{ %(#{hash[:value].source }) }\")]" if hash[:value] && hash[:value].kind_of?(Regexp)
163
+ command = "#{ command }[@width=\"#{ hash[:width] }\"]" unless hash[:width].nil?
164
+ command = "#{ command }[@height=\"#{ hash[:height] }\"]" unless hash[:height].nil?
165
+ command = "#{ command }[@visible=\"#{ hash[:visible] }\"]" unless hash[:visible].nil?
166
+ end
167
+
168
+
169
+ command += "[1]" if single
170
+
171
+ command
172
+ end
173
+
174
+ # check if selectors are for a scrollable element
175
+ # @param [Boolean] single should the command return first instance or all of matched elements
176
+ # @param [Hash] selectors for fetching elements
177
+ # @return [Boolean] true if element has scrollable attribute true or is class one of (RecyclerView, HorizontalScrollView, ScrollView, ListView)
178
+ def is_scrollable_selector?(selectors, single)
179
+ return false unless single
180
+ return true if selectors[:scrollable]
181
+ if selectors[:class] == "androidx.recyclerview.widget.RecyclerView" ||
182
+ selectors[:class] == "android.widget.HorizontalScrollView" ||
183
+ selectors[:class] == "android.widget.ScrollView" ||
184
+ selectors[:class] == "android.widget.ListView"
185
+ return true
186
+ elsif selectors[:type] == "XCUIElementTypeScrollView"
187
+ return true
188
+ end
189
+ false
190
+ end
191
+
192
+ #noinspection RubyUnnecessaryReturnStatement,RubyUnusedLocalVariable
193
+ # separate selectors from given hash parameters
194
+ # @param [Hash] params
195
+ # @return [Array] first element is params, second are selectors
196
+ def extract_selectors_from_params(params = {})
197
+ selectors = params.select { |key, value| [
198
+ :id,
199
+ :longClickable,
200
+ :desc,
201
+ :class,
202
+ :text,
203
+ :package,
204
+ :checkable,
205
+ :checked,
206
+ :clickable,
207
+ :enabled,
208
+ :focusable,
209
+ :focused,
210
+ :index,
211
+ :selected,
212
+ :scrollable,
213
+
214
+ # ios specific
215
+ :type,
216
+ :label,
217
+ :x,
218
+ :y,
219
+ :width,
220
+ :height,
221
+ :visible,
222
+ :name,
223
+ :value
224
+ ].include?(key) }
225
+ params = Hash[params.to_a - selectors.to_a]
226
+
227
+ # default params
228
+ params[:single] = true if params[:single].nil?
229
+ params[:scrollable_locator] = nil if params[:scrollable_locator].nil?
230
+ if params[:default_find_strategy].nil?
231
+ params[:default_find_strategy] = DEFAULT_ANDROID_FIND_STRATEGY if @driver.device == :android
232
+ params[:default_find_strategy] = DEFAULT_IOS_FIND_STRATEGY if @driver.device == :ios || @driver.device == :tvos
233
+ end
234
+ if params[:default_scroll_strategy].nil?
235
+ params[:default_scroll_strategy] = DEFAULT_ANDROID_SCROLL_STRATEGY if @driver.device == :android
236
+ params[:default_scroll_strategy] = DEFAULT_IOS_SCROLL_STRATEGY if @driver.device == :ios || @driver.device == :tvos
237
+ end
238
+
239
+ return params, selectors
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,371 @@
1
+ require_relative 'locator/scroll_actions'
2
+
3
+
4
+ module TestaAppiumDriver
5
+ #noinspection RubyTooManyInstanceVariablesInspection,RubyTooManyMethodsInspection
6
+ class Locator
7
+ include Helpers
8
+
9
+ attr_accessor :xpath_selector
10
+ attr_accessor :single
11
+
12
+ attr_accessor :driver
13
+ attr_accessor :strategy
14
+ attr_accessor :strategy_reason
15
+ attr_accessor :last_selector_adjacent
16
+
17
+ attr_accessor :from_element
18
+ attr_accessor :scroll_orientation
19
+ attr_accessor :scroll_deadzone
20
+ attr_accessor :scrollable_locator
21
+
22
+ attr_accessor :default_find_strategy
23
+ attr_accessor :default_scroll_strategy
24
+
25
+
26
+ # locator parameters are:
27
+ # single: true or false
28
+ # scrollable_locator: [TestaAppiumDriver::Locator, nil] for scrolling if needed later
29
+ # default_find_strategy: default strategy if find element strategy is not enforced
30
+ # default_scroll_strategy: default strategy for scrolling if not enforced
31
+ #
32
+ # @param [TestaAppiumDriver::Driver] driver
33
+ # @param [TestaAppiumDriver::Driver, TestaAppiumDriver::Locator, Selenium::WebDriver::Element] from_element from which element to execute the find_element
34
+ # @param [Hash] params selectors and params for locator
35
+ def initialize(driver, from_element, params = {})
36
+ # @type [TestaAppiumDriver::Driver]
37
+ @driver = driver
38
+
39
+ params, selectors = extract_selectors_from_params(params)
40
+
41
+ single = params[:single]
42
+
43
+ @single = single
44
+
45
+ if from_element.instance_of?(Selenium::WebDriver::Element)
46
+ @xpath_selector = "//*" # to select current element
47
+ @xpath_selector += hash_to_xpath(@driver.device, selectors, single)[1..-1]
48
+ else
49
+ @xpath_selector = hash_to_xpath(@driver.device, selectors, single)
50
+ end
51
+
52
+
53
+ @from_element = from_element
54
+ @default_find_strategy = params[:default_find_strategy]
55
+ @default_scroll_strategy = params[:default_scroll_strategy]
56
+
57
+
58
+ @strategy = params[:strategy]
59
+ @strategy_reason = params[:strategy_reason]
60
+
61
+ # @type [Boolean] used to determine if last selector was one of siblings or children. Only in those selectors we can reliably use xpath array [instance] selector
62
+ @last_selector_adjacent = false
63
+
64
+ init(params, selectors, single)
65
+ end
66
+
67
+
68
+ # method missing is used to fetch the element before executing additional commands like click, send_key, count
69
+ def method_missing(method, *args, &block)
70
+ execute.send(method, *args, &block)
71
+ end
72
+
73
+
74
+ # @param [Boolean] skip_cache if true it will skip cache check and store
75
+ # @param [Selenium::WebDriver::Element] force_cache_element, for internal use where we have already the element, and want to execute custom locator methods on it
76
+ # @return [Selenium::WebDriver::Element, Array]
77
+ def execute(skip_cache: false, force_cache_element: nil)
78
+ return force_cache_element unless force_cache_element.nil?
79
+ # if we are looking for current element, then return from_element
80
+ # for example when we have driver.element.elements[1].click
81
+ # elements[2] will be resolved with xpath because we are looking for multiple elements from element
82
+ # and since we are looking for instance 2, [](instance) method will return new "empty locator"
83
+ # we are executing click on that "empty locator" so we have to return the instance 2 of elements for the click
84
+ if @xpath_selector == "//*/*[1]" && @from_element.instance_of?(Selenium::WebDriver::Element)
85
+ return @from_element
86
+ end
87
+ @driver.execute(@from_element, selector, @single, @strategy, @default_find_strategy, skip_cache)
88
+ end
89
+
90
+
91
+ # @param [Integer] timeout in seconds
92
+ # @return [TestaAppiumDriver::Locator]
93
+ def wait_until_exists(timeout = @driver.get_timeouts["implicit"] / 1000)
94
+ start_time = Time.now.to_f
95
+ until exists?
96
+ raise "wait until exists timeout exceeded" if start_time + timeout > Time.now.to_f
97
+ sleep EXISTS_WAIT
98
+ end
99
+ self
100
+ end
101
+
102
+
103
+ # @param [Integer] timeout in seconds
104
+ # @return [TestaAppiumDriver::Locator]
105
+ def wait_while_exists(timeout = @driver.get_timeouts["implicit"] / 1000)
106
+ start_time = Time.now.to_f
107
+ while exists?
108
+ raise "wait until exists timeout exceeded" if start_time + timeout > Time.now.to_f
109
+ sleep EXISTS_WAIT
110
+ end
111
+ self
112
+ end
113
+
114
+
115
+ # all timeouts are disabled before check, and enabled after check
116
+ # @return [boolean] true if it exists in the page regardless if visible or not
117
+ def exists?
118
+ @driver.disable_wait_for_idle
119
+ @driver.disable_implicit_wait
120
+ found = true
121
+ begin
122
+ execute(skip_cache: true)
123
+ rescue StandardError
124
+ found = false
125
+ end
126
+ @driver.enable_implicit_wait
127
+ @driver.enable_wait_for_idle
128
+ found
129
+ end
130
+
131
+
132
+ def [](instance)
133
+ raise "Cannot add index selector to non-Array" if @single
134
+
135
+ if (@strategy.nil? && !@last_selector_adjacent) || @strategy == FIND_STRATEGY_UIAUTOMATOR
136
+ locator = self.dup
137
+ locator.strategy = FIND_STRATEGY_UIAUTOMATOR
138
+ locator.ui_selector = "#{@ui_selector}.instance(#{instance})"
139
+ locator.single = true
140
+ locator
141
+ else
142
+ from_element = self.execute[instance]
143
+ params = {}.merge({single: true, scrollable_locator: @scrollable_locator})
144
+ #params[:strategy] = FIND_STRATEGY_XPATH
145
+ #params[:strategy_reason] = "retrieved instance of a array"
146
+ params[:default_find_strategy] = @default_find_strategy
147
+ params[:default_scroll_strategy] = @default_scroll_strategy
148
+ Locator.new(@driver, from_element, params)
149
+ end
150
+ end
151
+
152
+
153
+ # @param [TestaAppiumDriver::Locator, Selenium::WebDriver::Element, Array] other
154
+ #noinspection RubyNilAnalysis,RubyUnnecessaryReturnStatement
155
+ def ==(other)
156
+ elements = execute
157
+ other = other.execute if other.kind_of?(TestaAppiumDriver::Locator)
158
+
159
+ if elements.kind_of?(Array)
160
+ return false unless other.kind_of?(Array)
161
+ return false if other.count != elements.count
162
+ return (elements - other).empty?
163
+ else
164
+ return false if other.kind_of?(Array)
165
+ return elements == other
166
+ end
167
+ end
168
+
169
+ def as_json
170
+ {
171
+ strategy: @strategy,
172
+ default_strategy: @default_find_strategy,
173
+ single: @single,
174
+ context: @from_element.nil? ? nil : @from_element.to_s,
175
+ uiautomator: defined?(self.ui_selector) ? ui_selector : nil,
176
+ xpath: @xpath_selector,
177
+ scrollable: @scrollable_locator.nil? ? nil : @scrollable_locator.to_s,
178
+ scroll_orientation: @scroll_orientation
179
+ }
180
+ end
181
+
182
+ def to_s
183
+ JSON.dump(as_json)
184
+ end
185
+
186
+ def to_ary
187
+ [self.to_s]
188
+ end
189
+
190
+
191
+ # @return [TestaAppiumDriver::Locator]
192
+ def as_scrollable(orientation: :vertical, top: nil, bottom: nil, right: nil, left: nil)
193
+ @scroll_orientation = orientation
194
+ if !top.nil? || !bottom.nil? || !right.nil? || !left.nil?
195
+ @scroll_deadzone = {}
196
+ @scroll_deadzone[:top] = top.to_f unless top.nil?
197
+ @scroll_deadzone[:bottom] = bottom.to_f unless bottom.nil?
198
+ @scroll_deadzone[:right] = right.to_f unless right.nil?
199
+ @scroll_deadzone[:left] = left.to_f unless left.nil?
200
+ end
201
+ @scrollable_locator = self.dup
202
+ self
203
+ end
204
+
205
+
206
+ def first_and_last_leaf
207
+ @driver.first_and_last_leaf(execute)
208
+ end
209
+
210
+
211
+ def tap
212
+ click
213
+ end
214
+
215
+ def click
216
+ perform_driver_method(:click)
217
+ end
218
+
219
+ def send_key(*args)
220
+ perform_driver_method(:send_keys, *args)
221
+ end
222
+
223
+ def clear
224
+ perform_driver_method(:clear)
225
+ end
226
+
227
+
228
+ # Return parent element
229
+ # @return [TestaAppiumDriver::Locator]
230
+ def parent
231
+ raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "parent") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil?
232
+ raise "Cannot add parent selector to a retrieved instance of a class array" if (@xpath_selector == "//*" || @xpath_selector == "//*[1]") && !@from_element.nil?
233
+
234
+ locator = self.dup
235
+ locator.strategy = FIND_STRATEGY_XPATH
236
+ locator.strategy_reason = "parent"
237
+ locator.xpath_selector += "/.."
238
+ locator
239
+ end
240
+
241
+ # Return all children elements
242
+ # @return [TestaAppiumDriver::Locator]
243
+ def children
244
+ raise "Cannot add children selector to array" unless @single
245
+ raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "children") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil?
246
+
247
+ locator = self.dup
248
+ locator.strategy = FIND_STRATEGY_XPATH
249
+ locator.strategy_reason = "children"
250
+ locator.xpath_selector += "/*"
251
+ locator.single = false
252
+ locator.last_selector_adjacent = true
253
+ locator
254
+ end
255
+
256
+
257
+ # Return first child element
258
+ # @return [TestaAppiumDriver::Locator]
259
+ def child
260
+ raise "Cannot add children selector to array" unless @single
261
+ raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "child") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil?
262
+
263
+ locator = self.dup
264
+ locator.strategy = FIND_STRATEGY_XPATH
265
+ locator.strategy_reason = "child"
266
+ locator.xpath_selector += "/*[1]"
267
+ locator.single = true
268
+ locator
269
+ end
270
+
271
+
272
+ # @return [TestaAppiumDriver::Locator]
273
+ def siblings
274
+ raise "Cannot add siblings selector to array" unless @single
275
+ raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "siblings") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil?
276
+ raise "Cannot add siblings selector to a retrieved instance of a class array" if (@xpath_selector == "//*" || @xpath_selector == "//*[1]") && !@from_element.nil?
277
+
278
+ locator = self.dup
279
+ locator.strategy = FIND_STRATEGY_XPATH
280
+ locator.strategy_reason = "siblings"
281
+ locator.xpath_selector += "/../*[not(@index=\"#{index}\")]"
282
+ locator.single = false
283
+ locator.last_selector_adjacent = true
284
+ locator
285
+ end
286
+
287
+ # @return [TestaAppiumDriver::Locator]
288
+ def preceding_siblings
289
+ raise "Cannot add preceding_siblings selector to array" unless @single
290
+ raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "preceding_siblings") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil?
291
+ raise "Cannot add preceding_siblings selector to a retrieved instance of a class array" if (@xpath_selector == "//*" || @xpath_selector == "//*[1]") && !@from_element.nil?
292
+
293
+ locator = self.dup
294
+ locator.strategy = FIND_STRATEGY_XPATH
295
+ locator.strategy_reason = "preceding_siblings"
296
+ locator.xpath_selector += "/../*[position() < #{index + 1}]" # position() starts from 1
297
+ locator.single = false
298
+ locator.last_selector_adjacent = true
299
+ locator
300
+ end
301
+
302
+ # @return [TestaAppiumDriver::Locator]
303
+ def preceding_sibling
304
+ raise "Cannot add preceding_sibling selector to array" unless @single
305
+ raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "preceding_sibling") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil?
306
+ raise "Cannot add preceding siblings selector to a retrieved instance of a class array" if (@xpath_selector == "//*" || @xpath_selector == "//*[1]") && !@from_element.nil?
307
+
308
+ locator = self.dup
309
+ locator.strategy = FIND_STRATEGY_XPATH
310
+ locator.strategy_reason = "preceding_sibling"
311
+ i = index
312
+ locator.single = true
313
+ return nil if i == 0
314
+ locator.xpath_selector += "/../*[@index=\"#{i - 1}\"]"
315
+ locator.last_selector_adjacent = true
316
+ locator
317
+ end
318
+
319
+
320
+ # @return [TestaAppiumDriver::Locator]
321
+ def following_siblings
322
+ raise "Cannot add following_siblings selector to array" unless @single
323
+ raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "following_siblings") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil?
324
+ raise "Cannot add following_siblings selector to a retrieved instance of a class array" if (@xpath_selector == "//*" || @xpath_selector == "//*[1]") && !@from_element.nil?
325
+
326
+ locator = self.dup
327
+ locator.strategy = FIND_STRATEGY_XPATH
328
+ locator.strategy_reason = "following_siblings"
329
+ locator.xpath_selector += "/../*[position() > #{index + 1}]" # position() starts from 1
330
+ locator.single = false
331
+ locator.last_selector_adjacent = true
332
+ locator
333
+ end
334
+
335
+ # @return [TestaAppiumDriver::Locator]
336
+ def following_sibling
337
+ raise "Cannot add following_sibling selector to array" unless @single
338
+ raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_XPATH, "following_sibling") if @strategy != FIND_STRATEGY_XPATH && !@strategy.nil?
339
+ raise "Cannot add following_sibling selector to a retrieved instance of a class array" if (@xpath_selector == "//*" || @xpath_selector == "//*[1]") && !@from_element.nil?
340
+
341
+ locator = self.dup
342
+ locator.strategy = FIND_STRATEGY_XPATH
343
+ locator.strategy_reason = "following_sibling"
344
+ i = index
345
+ locator.single = true
346
+ return nil if i == 0
347
+ locator.xpath_selector += "/../*[@index=\"#{i + 1}\"]"
348
+ locator.last_selector_adjacent = true
349
+ locator
350
+ end
351
+
352
+
353
+ private
354
+
355
+ #noinspection RubyNilAnalysis
356
+ def perform_driver_method(name, *args)
357
+ elements = execute
358
+ if elements.kind_of?(Array)
359
+ elements.map { |e| e.send(name, *args) }
360
+ else
361
+ elements.send(name, *args)
362
+ end
363
+ end
364
+
365
+ def add_xpath_child_selectors(locator, selectors, single)
366
+ locator.single = false unless single # switching from single result to multiple
367
+ locator.xpath_selector += hash_to_xpath(@driver.device, selectors, single)
368
+ end
369
+ end
370
+
371
+ end