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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.idea/deployment.xml +21 -0
- data/.idea/inspectionProfiles/Project_Default.xml +9 -0
- data/.idea/misc.xml +6 -0
- data/.idea/modules.xml +8 -0
- data/.idea/runConfigurations/Android_Test.xml +42 -0
- data/.idea/sshConfigs.xml +10 -0
- data/.idea/vcs.xml +6 -0
- data/.idea/webServers.xml +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +102 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/Rakefile +12 -0
- data/bin/console +17 -0
- data/bin/setup +8 -0
- data/lib/testa_appium_driver.rb +6 -0
- data/lib/testa_appium_driver/android/class_selectors.rb +353 -0
- data/lib/testa_appium_driver/android/driver.rb +52 -0
- data/lib/testa_appium_driver/android/locator.rb +115 -0
- data/lib/testa_appium_driver/android/locator/attributes.rb +116 -0
- data/lib/testa_appium_driver/android/scroll_actions/uiautomator_scroll_actions.rb +77 -0
- data/lib/testa_appium_driver/android/selenium_element.rb +8 -0
- data/lib/testa_appium_driver/common/bounds.rb +150 -0
- data/lib/testa_appium_driver/common/constants.rb +33 -0
- data/lib/testa_appium_driver/common/exceptions/strategy_mix_exception.rb +12 -0
- data/lib/testa_appium_driver/common/helpers.rb +242 -0
- data/lib/testa_appium_driver/common/locator.rb +371 -0
- data/lib/testa_appium_driver/common/locator/scroll_actions.rb +287 -0
- data/lib/testa_appium_driver/common/scroll_actions.rb +253 -0
- data/lib/testa_appium_driver/common/scroll_actions/json_wire_scroll_actions.rb +4 -0
- data/lib/testa_appium_driver/common/scroll_actions/w3c_scroll_actions.rb +261 -0
- data/lib/testa_appium_driver/driver.rb +226 -0
- data/lib/testa_appium_driver/ios/driver.rb +35 -0
- data/lib/testa_appium_driver/ios/locator.rb +40 -0
- data/lib/testa_appium_driver/ios/locator/attributes.rb +79 -0
- data/lib/testa_appium_driver/ios/selenium_element.rb +7 -0
- data/lib/testa_appium_driver/ios/type_selectors.rb +167 -0
- data/lib/testa_appium_driver/version.rb +5 -0
- data/testa_appium_driver.gemspec +40 -0
- data/testa_appium_driver.iml +79 -0
- 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
|