testa_appium_driver 0.1.18 → 0.1.20
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/Gemfile.lock +5 -1
- data/appium-driver.iml +79 -0
- data/testa_appium_driver.gemspec +4 -8
- metadata +11 -40
- data/.gitattributes +0 -23
- data/.gitignore +0 -17
- data/.rspec +0 -3
- data/.rubocop.yml +0 -5
- data/bin/console +0 -17
- data/bin/setup +0 -8
- data/lib/testa_appium_driver/android/class_selectors.rb +0 -438
- data/lib/testa_appium_driver/android/driver.rb +0 -71
- data/lib/testa_appium_driver/android/locator/attributes.rb +0 -118
- data/lib/testa_appium_driver/android/locator.rb +0 -142
- data/lib/testa_appium_driver/android/scroll_actions/uiautomator_scroll_actions.rb +0 -62
- data/lib/testa_appium_driver/android/selenium_element.rb +0 -12
- data/lib/testa_appium_driver/common/bounds.rb +0 -150
- data/lib/testa_appium_driver/common/constants.rb +0 -38
- data/lib/testa_appium_driver/common/exceptions/strategy_mix_exception.rb +0 -12
- data/lib/testa_appium_driver/common/helpers.rb +0 -271
- data/lib/testa_appium_driver/common/locator/scroll_actions.rb +0 -398
- data/lib/testa_appium_driver/common/locator.rb +0 -599
- data/lib/testa_appium_driver/common/scroll_actions/json_wire_scroll_actions.rb +0 -4
- data/lib/testa_appium_driver/common/scroll_actions/w3c_scroll_actions.rb +0 -305
- data/lib/testa_appium_driver/common/scroll_actions.rb +0 -267
- data/lib/testa_appium_driver/common/selenium_element.rb +0 -19
- data/lib/testa_appium_driver/driver.rb +0 -335
- data/lib/testa_appium_driver/ios/driver.rb +0 -49
- data/lib/testa_appium_driver/ios/locator/attributes.rb +0 -85
- data/lib/testa_appium_driver/ios/locator.rb +0 -72
- data/lib/testa_appium_driver/ios/selenium_element.rb +0 -8
- data/lib/testa_appium_driver/ios/type_selectors.rb +0 -188
- data/lib/testa_appium_driver/version.rb +0 -5
- data/lib/testa_appium_driver.rb +0 -6
@@ -1,142 +0,0 @@
|
|
1
|
-
require_relative 'locator/attributes'
|
2
|
-
|
3
|
-
module TestaAppiumDriver
|
4
|
-
#noinspection RubyTooManyInstanceVariablesInspection
|
5
|
-
class Locator
|
6
|
-
attr_accessor :closing_parenthesis
|
7
|
-
include ClassSelectors
|
8
|
-
include Attributes
|
9
|
-
|
10
|
-
def init(params, selectors, single)
|
11
|
-
@closing_parenthesis = 0
|
12
|
-
|
13
|
-
|
14
|
-
@ui_selector = hash_to_uiautomator(selectors, single)
|
15
|
-
|
16
|
-
if is_scrollable_selector?(selectors, single)
|
17
|
-
if selectors[:class] == "android.widget.HorizontalScrollView"
|
18
|
-
@scroll_orientation = :horizontal
|
19
|
-
else
|
20
|
-
@scroll_orientation = :vertical
|
21
|
-
end
|
22
|
-
|
23
|
-
if !params[:top].nil? || !params[:bottom].nil? || !params[:right].nil? || !params[:left].nil?
|
24
|
-
@scroll_deadzone = {}
|
25
|
-
@scroll_deadzone[:top] = params[:top].to_f unless params[:top].nil?
|
26
|
-
@scroll_deadzone[:bottom] = params[:bottom].to_f unless params[:bottom].nil?
|
27
|
-
@scroll_deadzone[:right] = params[:right].to_f unless params[:right].nil?
|
28
|
-
@scroll_deadzone[:left] = params[:left].to_f unless params[:left].nil?
|
29
|
-
end
|
30
|
-
|
31
|
-
params[:scrollable_locator] = self.dup
|
32
|
-
end
|
33
|
-
|
34
|
-
@scrollable_locator = params[:scrollable_locator] if params[:scrollable_locator]
|
35
|
-
end
|
36
|
-
|
37
|
-
|
38
|
-
# resolve selector which will be used for finding element
|
39
|
-
def strategies_and_selectors
|
40
|
-
ss = []
|
41
|
-
if @can_use_id_strategy
|
42
|
-
ss.push({"#{FIND_STRATEGY_ID}": @can_use_id_strategy})
|
43
|
-
end
|
44
|
-
if @strategy.nil? || @strategy == FIND_STRATEGY_UIAUTOMATOR
|
45
|
-
ss.push({"#{FIND_STRATEGY_UIAUTOMATOR}": ui_selector})
|
46
|
-
end
|
47
|
-
|
48
|
-
if @strategy.nil? || @strategy == FIND_STRATEGY_XPATH
|
49
|
-
ss.push({"#{FIND_STRATEGY_XPATH}": @xpath_selector})
|
50
|
-
end
|
51
|
-
|
52
|
-
if @strategy == FIND_STRATEGY_IMAGE
|
53
|
-
ss.push({"#{FIND_STRATEGY_IMAGE}": @image_selector})
|
54
|
-
end
|
55
|
-
ss
|
56
|
-
end
|
57
|
-
|
58
|
-
|
59
|
-
# @param [Boolean] include_semicolon should the semicolon be included at the end
|
60
|
-
# @return ui_selector for uiautomator find strategy
|
61
|
-
def ui_selector(include_semicolon = true)
|
62
|
-
@ui_selector + ")" * @closing_parenthesis + (include_semicolon ? ";" : "");
|
63
|
-
end
|
64
|
-
|
65
|
-
def ui_selector=(value)
|
66
|
-
@ui_selector = value
|
67
|
-
end
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
# @return [TestaAppiumDriver::Locator]
|
73
|
-
def from_parent(selectors = {})
|
74
|
-
raise "Cannot add from_parent selector to array" unless @single
|
75
|
-
raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_UIAUTOMATOR, "from_parent") if @strategy != FIND_STRATEGY_UIAUTOMATOR
|
76
|
-
|
77
|
-
locator = self.dup
|
78
|
-
locator.strategy = FIND_STRATEGY_UIAUTOMATOR
|
79
|
-
locator.strategy_reason = "from_parent"
|
80
|
-
locator.closing_parenthesis += 1
|
81
|
-
locator.ui_selector = "#{locator.ui_selector}.fromParent(#{hash_to_uiautomator(selectors)}"
|
82
|
-
locator
|
83
|
-
end
|
84
|
-
|
85
|
-
|
86
|
-
# @return [Locator] new child locator element
|
87
|
-
def add_child_selector(params)
|
88
|
-
params, selectors = extract_selectors_from_params(params)
|
89
|
-
single = params[:single]
|
90
|
-
raise "Cannot add child selector to Array" if single && !@single
|
91
|
-
|
92
|
-
locator = self.dup
|
93
|
-
locator.can_use_id_strategy = false
|
94
|
-
if (@strategy.nil? && !single) || @strategy == FIND_STRATEGY_XPATH
|
95
|
-
locator.strategy = FIND_STRATEGY_XPATH
|
96
|
-
locator.strategy_reason = "multiple child selector"
|
97
|
-
add_xpath_child_selectors(locator, selectors, single)
|
98
|
-
elsif @strategy == FIND_STRATEGY_UIAUTOMATOR
|
99
|
-
locator = add_uiautomator_child_selector(locator, selectors, single)
|
100
|
-
elsif @strategy == FIND_STRATEGY_IMAGE
|
101
|
-
locator = add_image_child_selector(locator, selectors, single)
|
102
|
-
else
|
103
|
-
# both paths are valid
|
104
|
-
add_xpath_child_selectors(locator, selectors, single)
|
105
|
-
locator = add_uiautomator_child_selector(locator, selectors, single)
|
106
|
-
end
|
107
|
-
|
108
|
-
if is_scrollable_selector?(selectors, single)
|
109
|
-
locator.scrollable_locator = locator
|
110
|
-
if selectors[:class] == "android.widget.HorizontalScrollView"
|
111
|
-
locator.scrollable_locator.scroll_orientation = :horizontal
|
112
|
-
else
|
113
|
-
locator.scrollable_locator.scroll_orientation = :vertical
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
locator.last_selector_adjacent = false
|
118
|
-
locator
|
119
|
-
end
|
120
|
-
|
121
|
-
|
122
|
-
private
|
123
|
-
def add_uiautomator_child_selector(locator, selectors, single)
|
124
|
-
if locator.single && !single
|
125
|
-
# current locator stays single, the child locator looks for multiple
|
126
|
-
params = selectors.merge({single: single, scrollable_locator: locator.scrollable_locator})
|
127
|
-
params[:default_find_strategy] = locator.default_find_strategy
|
128
|
-
params[:default_scroll_strategy] = locator.default_scroll_strategy
|
129
|
-
Locator.new(@driver, self, params)
|
130
|
-
else
|
131
|
-
locator.single = true
|
132
|
-
locator.ui_selector = "#{locator.ui_selector(false)}.childSelector(#{hash_to_uiautomator(selectors, single)})"
|
133
|
-
locator
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
def add_image_child_selector(locator, selectors, single)
|
138
|
-
params = selectors.merge({single: single, scrollable_locator: locator.scrollable_locator})
|
139
|
-
Locator.new(@driver, self, params)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
@@ -1,62 +0,0 @@
|
|
1
|
-
module TestaAppiumDriver
|
2
|
-
#noinspection RubyInstanceMethodNamingConvention
|
3
|
-
class ScrollActions
|
4
|
-
private
|
5
|
-
|
6
|
-
def uiautomator_scroll_to_start_or_end(type)
|
7
|
-
|
8
|
-
scrollable_selector = @scrollable.ui_selector(false)
|
9
|
-
orientation = @scrollable.scroll_orientation == :vertical ? ".setAsVerticalList()" : ".setAsHorizontalList()"
|
10
|
-
scroll_command = type == :start ? ".scrollToBeginning(#{DEFAULT_UIAUTOMATOR_MAX_SWIPES})" : ".scrollToEnd(#{DEFAULT_UIAUTOMATOR_MAX_SWIPES})"
|
11
|
-
cmd = "new UiScrollable(#{scrollable_selector})#{orientation}#{scroll_command};"
|
12
|
-
begin
|
13
|
-
puts "Scroll execute[uiautomator_#{type}]: #{cmd}"
|
14
|
-
@driver.find_element(uiautomator: cmd)
|
15
|
-
rescue
|
16
|
-
# Ignored
|
17
|
-
end
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
|
23
|
-
def uiautomator_scroll_to
|
24
|
-
raise "UiAutomator scroll cannot work with specified direction" unless @direction.nil?
|
25
|
-
|
26
|
-
scrollable_selector = @scrollable.ui_selector(false)
|
27
|
-
element_selector = @locator.ui_selector(false)
|
28
|
-
orientation_command = @scrollable.scroll_orientation == :vertical ? ".setAsVerticalList()" : ".setAsHorizontalList()"
|
29
|
-
cmd = "new UiScrollable(#{scrollable_selector})#{orientation_command}.scrollIntoView(#{element_selector});"
|
30
|
-
begin
|
31
|
-
puts "Scroll execute[uiautomator_scroll_to]: #{cmd}"
|
32
|
-
@driver.find_element(uiautomator: cmd)
|
33
|
-
rescue
|
34
|
-
# Ignored
|
35
|
-
ensure
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
|
40
|
-
def uiautomator_page_or_fling(type, direction)
|
41
|
-
scrollable_selector = @scrollable.ui_selector(false)
|
42
|
-
orientation = direction == :up || direction == :down ? ".setAsVerticalList()" : ".setAsHorizontalList()"
|
43
|
-
if type == SCROLL_ACTION_TYPE_SCROLL
|
44
|
-
direction_command = direction == :down || direction == :right ? ".scrollForward()" : ".scrollBackward()"
|
45
|
-
elsif type == SCROLL_ACTION_TYPE_FLING
|
46
|
-
direction_command = direction == :down || direction == :right ? ".flingForward()" : ".flingBackward()"
|
47
|
-
else
|
48
|
-
raise "Unknown scroll action type #{type}"
|
49
|
-
end
|
50
|
-
cmd = "new UiScrollable(#{scrollable_selector})#{orientation}#{direction_command};"
|
51
|
-
begin
|
52
|
-
puts "Scroll execute[uiautomator_#{type}]: #{cmd}"
|
53
|
-
@driver.find_element(uiautomator: cmd)
|
54
|
-
rescue
|
55
|
-
# Ignored
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
|
60
|
-
end
|
61
|
-
|
62
|
-
end
|
@@ -1,150 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'json'
|
4
|
-
module TestaAppiumDriver
|
5
|
-
class Bounds
|
6
|
-
|
7
|
-
attr_reader :width
|
8
|
-
attr_reader :height
|
9
|
-
attr_reader :offset
|
10
|
-
|
11
|
-
|
12
|
-
# @param top_left [Coordinates]
|
13
|
-
# @param bottom_right [Coordinates]
|
14
|
-
# @param window_width [Integer]
|
15
|
-
# @param window_height [Integer]
|
16
|
-
def initialize(top_left, bottom_right, window_width, window_height)
|
17
|
-
@top_left = top_left
|
18
|
-
@bottom_right = bottom_right
|
19
|
-
@width = bottom_right.x - top_left.x
|
20
|
-
@height = bottom_right.y - top_left.y
|
21
|
-
@offset = Offset.new(self, window_width, window_height)
|
22
|
-
@center = TestaAppiumDriver::Coordinates.new(@top_left.x + @width/2, @top_left.y + @height / 2)
|
23
|
-
end
|
24
|
-
|
25
|
-
def as_json
|
26
|
-
{
|
27
|
-
width: @width,
|
28
|
-
height: @height,
|
29
|
-
top_left: @top_left.as_json,
|
30
|
-
bottom_right: @bottom_right.as_json,
|
31
|
-
offset: @offset.as_json
|
32
|
-
}
|
33
|
-
end
|
34
|
-
|
35
|
-
# @return [TestaAppiumDriver::Offset]
|
36
|
-
def offset
|
37
|
-
@offset
|
38
|
-
end
|
39
|
-
|
40
|
-
# @return [TestaAppiumDriver::Coordinates]
|
41
|
-
def top_left
|
42
|
-
@top_left
|
43
|
-
end
|
44
|
-
# @return [TestaAppiumDriver::Coordinates]
|
45
|
-
def bottom_right
|
46
|
-
@bottom_right
|
47
|
-
end
|
48
|
-
|
49
|
-
# @return [TestaAppiumDriver::Coordinates]
|
50
|
-
def center
|
51
|
-
@center
|
52
|
-
end
|
53
|
-
|
54
|
-
def to_s
|
55
|
-
JSON.dump(as_json)
|
56
|
-
end
|
57
|
-
|
58
|
-
# @param bounds [String] bounds that driver.attribute("bounds") return
|
59
|
-
# @param driver [TestaAppiumDriver::Driver]
|
60
|
-
def self.from_android(bounds, driver)
|
61
|
-
matches = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/)
|
62
|
-
raise "Unexpected bounds: #{bounds}" unless matches
|
63
|
-
|
64
|
-
captures = matches.captures
|
65
|
-
top_left = Coordinates.new(captures[0], captures[1])
|
66
|
-
bottom_right = Coordinates.new(captures[2], captures[3])
|
67
|
-
ws = driver.window_size
|
68
|
-
window_width = ws.width.to_i
|
69
|
-
window_height = ws.height.to_i
|
70
|
-
Bounds.new(top_left, bottom_right, window_width, window_height)
|
71
|
-
end
|
72
|
-
|
73
|
-
def self.from_ios(rect, driver)
|
74
|
-
rect = JSON.parse(rect)
|
75
|
-
top_left = Coordinates.new(rect["x"], rect["y"])
|
76
|
-
bottom_right = Coordinates.new(top_left.x + rect["width"].to_i, top_left.y + rect["height"].to_i)
|
77
|
-
ws = driver.window_size
|
78
|
-
window_width = ws.width.to_i
|
79
|
-
window_height = ws.height.to_i
|
80
|
-
Bounds.new(top_left, bottom_right, window_width, window_height)
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
#noinspection ALL
|
85
|
-
class Coordinates
|
86
|
-
def initialize(x, y)
|
87
|
-
@x = x.to_i
|
88
|
-
@y = y.to_i
|
89
|
-
end
|
90
|
-
|
91
|
-
def as_json
|
92
|
-
{
|
93
|
-
x: @x,
|
94
|
-
y: @y
|
95
|
-
}
|
96
|
-
end
|
97
|
-
|
98
|
-
|
99
|
-
# @return [Integer]
|
100
|
-
def x
|
101
|
-
@x
|
102
|
-
end
|
103
|
-
|
104
|
-
# @return [Integer]
|
105
|
-
def y
|
106
|
-
@y
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
|
111
|
-
class Offset
|
112
|
-
def initialize(bounds, window_width, window_height)
|
113
|
-
@top = bounds.top_left.y
|
114
|
-
@right = window_width - bounds.bottom_right.x
|
115
|
-
@bottom = window_height - bounds.bottom_right.y
|
116
|
-
@left = bounds.top_left.x
|
117
|
-
end
|
118
|
-
|
119
|
-
def as_json
|
120
|
-
{
|
121
|
-
top: @top,
|
122
|
-
right: @right,
|
123
|
-
bottom: @bottom,
|
124
|
-
left: @left
|
125
|
-
}
|
126
|
-
end
|
127
|
-
|
128
|
-
|
129
|
-
# @return [Integer]
|
130
|
-
def top
|
131
|
-
@top
|
132
|
-
end
|
133
|
-
|
134
|
-
# @return [Integer]
|
135
|
-
def right
|
136
|
-
@right
|
137
|
-
end
|
138
|
-
|
139
|
-
# @return [Integer]
|
140
|
-
def bottom
|
141
|
-
@bottom
|
142
|
-
end
|
143
|
-
|
144
|
-
# @return [Integer]
|
145
|
-
def left
|
146
|
-
@left
|
147
|
-
end
|
148
|
-
|
149
|
-
end
|
150
|
-
end
|
@@ -1,38 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
#noinspection ALL
|
4
|
-
module TestaAppiumDriver
|
5
|
-
FIND_STRATEGY_UIAUTOMATOR = :uiautomator
|
6
|
-
FIND_STRATEGY_XPATH = :xpath
|
7
|
-
FIND_STRATEGY_ID = :id
|
8
|
-
FIND_STRATEGY_NAME = :name
|
9
|
-
FIND_STRATEGY_IMAGE = :image
|
10
|
-
FIND_STRATEGY_CLASS_CHAIN = :class_chain
|
11
|
-
|
12
|
-
SCROLL_STRATEGY_UIAUTOMATOR = :uiautomator
|
13
|
-
SCROLL_STRATEGY_W3C = :w3c
|
14
|
-
|
15
|
-
|
16
|
-
SCROLL_CORRECTION_W3C = 30
|
17
|
-
SCROLL_ALIGNMENT_THRESHOLD = 25
|
18
|
-
|
19
|
-
SCROLL_ACTION_TYPE_SCROLL = :scroll
|
20
|
-
SCROLL_ACTION_TYPE_FLING = :fling
|
21
|
-
SCROLL_ACTION_TYPE_DRAG = :drag
|
22
|
-
|
23
|
-
|
24
|
-
DEFAULT_UIAUTOMATOR_MAX_SWIPES = 20
|
25
|
-
|
26
|
-
DEFAULT_ANDROID_FIND_STRATEGY = FIND_STRATEGY_UIAUTOMATOR
|
27
|
-
#DEFAULT_ANDROID_SCROLL_STRATEGY = SCROLL_STRATEGY_UIAUTOMATOR
|
28
|
-
DEFAULT_ANDROID_SCROLL_STRATEGY = SCROLL_STRATEGY_W3C
|
29
|
-
|
30
|
-
|
31
|
-
DEFAULT_IOS_FIND_STRATEGY = FIND_STRATEGY_XPATH
|
32
|
-
DEFAULT_IOS_SCROLL_STRATEGY = SCROLL_STRATEGY_W3C
|
33
|
-
|
34
|
-
DEFAULT_W3C_MAX_SCROLLS = 7
|
35
|
-
|
36
|
-
EXISTS_WAIT = 0.5
|
37
|
-
LONG_TAP_DURATION = 1.5
|
38
|
-
end
|
@@ -1,12 +0,0 @@
|
|
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
|
@@ -1,271 +0,0 @@
|
|
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
|
-
end
|
153
|
-
|
154
|
-
|
155
|
-
command += "[1]" if single
|
156
|
-
|
157
|
-
command
|
158
|
-
end
|
159
|
-
|
160
|
-
|
161
|
-
def hash_to_class_chain(hash, single = true)
|
162
|
-
command = "**/"
|
163
|
-
|
164
|
-
hash[:type] = hash[:class] unless hash[:class].nil?
|
165
|
-
hash[:label] = hash[:text] unless hash[:text].nil?
|
166
|
-
hash[:name] = hash[:id] unless hash[:id].nil?
|
167
|
-
if hash[:type] && hash[:type].kind_of?(String)
|
168
|
-
command = "#{ command }#{hash[:type] }"
|
169
|
-
else
|
170
|
-
command = "#{command}*"
|
171
|
-
end
|
172
|
-
|
173
|
-
command = "#{ command }[`enabled == #{ hash[:enabled] }`]" unless hash[:enabled].nil?
|
174
|
-
command = "#{ command }[`label == \"#{ %(#{hash[:label] }) }\"`]" if hash[:label] && hash[:label].kind_of?(String)
|
175
|
-
command = "#{ command }[`label CONTAINS \"#{ %(#{hash[:label].source }) }\"`]" if hash[:label] && hash[:label].kind_of?(Regexp)
|
176
|
-
command = "#{ command }[`name == \"#{ %(#{hash[:name] }) }\"`]" if hash[:name] && hash[:name].kind_of?(String)
|
177
|
-
command = "#{ command }[`name CONTAINS \"#{ %(#{hash[:name].source }) }\"`]" if hash[:name] && hash[:name].kind_of?(Regexp)
|
178
|
-
command = "#{ command }[`value == \"#{ %(#{hash[:value] }) }\"`]" if hash[:value] && hash[:value].kind_of?(String)
|
179
|
-
command = "#{ command }[`value CONTAINS \"#{ %(#{hash[:value].source }) }\"`]" if hash[:value] && hash[:value].kind_of?(Regexp)
|
180
|
-
command = "#{ command }[`visible == #{ hash[:visible] }`]" unless hash[:visible].nil?
|
181
|
-
|
182
|
-
command += "[1]" if single
|
183
|
-
|
184
|
-
command
|
185
|
-
end
|
186
|
-
|
187
|
-
# check if selectors are for a scrollable element
|
188
|
-
# @param [Boolean] single should the command return first instance or all of matched elements
|
189
|
-
# @param [Hash] selectors for fetching elements
|
190
|
-
# @return [Boolean] true if element has scrollable attribute true or is class one of (RecyclerView, HorizontalScrollView, ScrollView, ListView)
|
191
|
-
def is_scrollable_selector?(selectors, single)
|
192
|
-
return false unless single
|
193
|
-
return true if selectors[:scrollable]
|
194
|
-
if selectors[:class] == "androidx.recyclerview.widget.RecyclerView" ||
|
195
|
-
selectors[:class] == "android.widget.HorizontalScrollView" ||
|
196
|
-
selectors[:class] == "android.widget.ScrollView" ||
|
197
|
-
selectors[:class] == "android.widget.ListView"
|
198
|
-
return true
|
199
|
-
elsif selectors[:type] == "XCUIElementTypeScrollView"
|
200
|
-
return true
|
201
|
-
end
|
202
|
-
false
|
203
|
-
end
|
204
|
-
|
205
|
-
#noinspection RubyUnnecessaryReturnStatement,RubyUnusedLocalVariable
|
206
|
-
# separate selectors from given hash parameters
|
207
|
-
# @param [Hash] params
|
208
|
-
# @return [Array] first element is params, second are selectors
|
209
|
-
def extract_selectors_from_params(params = {})
|
210
|
-
selectors = params.select { |key, value| [
|
211
|
-
:id,
|
212
|
-
:long_clickable,
|
213
|
-
:desc,
|
214
|
-
:class,
|
215
|
-
:text,
|
216
|
-
:package,
|
217
|
-
:checkable,
|
218
|
-
:checked,
|
219
|
-
:clickable,
|
220
|
-
:enabled,
|
221
|
-
:focusable,
|
222
|
-
:focused,
|
223
|
-
:index,
|
224
|
-
:selected,
|
225
|
-
:scrollable,
|
226
|
-
|
227
|
-
# ios specific
|
228
|
-
:type,
|
229
|
-
:label,
|
230
|
-
:x,
|
231
|
-
:y,
|
232
|
-
:width,
|
233
|
-
:height,
|
234
|
-
:visible,
|
235
|
-
:name,
|
236
|
-
:value,
|
237
|
-
|
238
|
-
:image
|
239
|
-
].include?(key) }
|
240
|
-
params = Hash[params.to_a - selectors.to_a]
|
241
|
-
|
242
|
-
# default params
|
243
|
-
params[:single] = true if params[:single].nil?
|
244
|
-
params[:scrollable_locator] = nil if params[:scrollable_locator].nil?
|
245
|
-
if params[:default_find_strategy].nil?
|
246
|
-
params[:default_find_strategy] = DEFAULT_ANDROID_FIND_STRATEGY if @driver.device == :android
|
247
|
-
params[:default_find_strategy] = DEFAULT_IOS_FIND_STRATEGY if @driver.device == :ios || @driver.device == :tvos
|
248
|
-
end
|
249
|
-
if params[:default_scroll_strategy].nil?
|
250
|
-
params[:default_scroll_strategy] = DEFAULT_ANDROID_SCROLL_STRATEGY if @driver.device == :android
|
251
|
-
params[:default_scroll_strategy] = DEFAULT_IOS_SCROLL_STRATEGY if @driver.device == :ios || @driver.device == :tvos
|
252
|
-
end
|
253
|
-
|
254
|
-
return params, selectors
|
255
|
-
end
|
256
|
-
|
257
|
-
|
258
|
-
def resolve_id(id)
|
259
|
-
if id && id.kind_of?(String) && !id.match?(/.*:id\//)
|
260
|
-
# shorthand ids like myId make full ids => my.app.package:id/myId
|
261
|
-
if id[0] == "="
|
262
|
-
return id[1..-1]
|
263
|
-
else
|
264
|
-
return "#{@driver.current_package}:id/#{id}"
|
265
|
-
end
|
266
|
-
else
|
267
|
-
id
|
268
|
-
end
|
269
|
-
end
|
270
|
-
end
|
271
|
-
end
|