appium_lib_core 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/.rubocop.yml +20 -0
- data/.travis.yml +16 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +202 -0
- data/README.md +5 -0
- data/Rakefile +62 -0
- data/Thorfile +7 -0
- data/appium_lib_core.gemspec +35 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/appium_lib_core.rb +41 -0
- data/lib/appium_lib_core/android.rb +5 -0
- data/lib/appium_lib_core/android/device.rb +174 -0
- data/lib/appium_lib_core/android/espresso/bridge.rb +16 -0
- data/lib/appium_lib_core/android/search_context.rb +18 -0
- data/lib/appium_lib_core/android/touch.rb +17 -0
- data/lib/appium_lib_core/android/uiautomator1/bridge.rb +16 -0
- data/lib/appium_lib_core/android/uiautomator2/bridge.rb +16 -0
- data/lib/appium_lib_core/android_espresso.rb +5 -0
- data/lib/appium_lib_core/android_uiautomator2.rb +5 -0
- data/lib/appium_lib_core/common.rb +6 -0
- data/lib/appium_lib_core/common/base.rb +8 -0
- data/lib/appium_lib_core/common/base/bridge.rb +47 -0
- data/lib/appium_lib_core/common/base/capabilities.rb +16 -0
- data/lib/appium_lib_core/common/base/command.rb +10 -0
- data/lib/appium_lib_core/common/base/driver.rb +51 -0
- data/lib/appium_lib_core/common/base/http_default.rb +13 -0
- data/lib/appium_lib_core/common/base/search_context.rb +89 -0
- data/lib/appium_lib_core/common/base/wait.rb +56 -0
- data/lib/appium_lib_core/common/command.rb +77 -0
- data/lib/appium_lib_core/common/device.rb +567 -0
- data/lib/appium_lib_core/common/error.rb +18 -0
- data/lib/appium_lib_core/common/log.rb +30 -0
- data/lib/appium_lib_core/common/logger.rb +30 -0
- data/lib/appium_lib_core/device/multi_touch.rb +48 -0
- data/lib/appium_lib_core/device/touch_actions.rb +191 -0
- data/lib/appium_lib_core/driver.rb +459 -0
- data/lib/appium_lib_core/ios.rb +7 -0
- data/lib/appium_lib_core/ios/device.rb +54 -0
- data/lib/appium_lib_core/ios/search_context.rb +27 -0
- data/lib/appium_lib_core/ios/touch.rb +17 -0
- data/lib/appium_lib_core/ios/uiautomation/bridge.rb +18 -0
- data/lib/appium_lib_core/ios/uiautomation/patch.rb +20 -0
- data/lib/appium_lib_core/ios/xcuitest/bridge.rb +18 -0
- data/lib/appium_lib_core/ios/xcuitest/device.rb +66 -0
- data/lib/appium_lib_core/ios/xcuitest/search_context.rb +40 -0
- data/lib/appium_lib_core/ios_xcuitest.rb +8 -0
- data/lib/appium_lib_core/patch.rb +78 -0
- data/lib/appium_lib_core/version.rb +6 -0
- data/release_notes.md +0 -0
- metadata +262 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
module Appium
|
2
|
+
module Core
|
3
|
+
module Error
|
4
|
+
class CoreError < StandardError; end
|
5
|
+
|
6
|
+
# Capability related errors
|
7
|
+
class NoCapabilityError < CoreError; end
|
8
|
+
class CapabilityStructureError < CoreError; end
|
9
|
+
|
10
|
+
# Appium related errors
|
11
|
+
class NotSupportedAppiumServer < CoreError; end
|
12
|
+
class NoSuchElementError < CoreError; end
|
13
|
+
|
14
|
+
# Server side error
|
15
|
+
class ServerError; end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Appium
|
2
|
+
module Core
|
3
|
+
class Logs
|
4
|
+
def initialize(bridge)
|
5
|
+
@bridge = bridge
|
6
|
+
end
|
7
|
+
|
8
|
+
# @param [String|Hash] type You can get particular type's logs.
|
9
|
+
# @return [[Selenium::WebDriver::LogEntry]] A list of logs data.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# Appium::Core::Logs.new(driver).get("syslog") #=> [[Selenium::WebDriver::LogEntry]]
|
13
|
+
# Appium::Core::Logs.new(driver).get(:syslog) #=> [[Selenium::WebDriver::LogEntry]]
|
14
|
+
#
|
15
|
+
def get(type)
|
16
|
+
@bridge.get type
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get a list of available log types
|
20
|
+
#
|
21
|
+
# @return [[Hash]] A list of available log types.
|
22
|
+
# @example
|
23
|
+
# Appium::Core::Logs.new(driver).available_types #=> [:syslog, :crashlog, :performance]
|
24
|
+
#
|
25
|
+
def available_types
|
26
|
+
@bridge.available_types
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end # module Core
|
30
|
+
end # module Appium
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Appium
|
5
|
+
module Logger
|
6
|
+
#
|
7
|
+
# @example Use logger manually
|
8
|
+
# Appium::Logger.debug('This is info message')
|
9
|
+
# Appium::Logger.warn('This is warning message')
|
10
|
+
#
|
11
|
+
class << self
|
12
|
+
extend Forwardable
|
13
|
+
def_delegators :logger, :fatal, :error, :warn, :info, :debug, :level, :level=, :formatter, :formatter=
|
14
|
+
|
15
|
+
attr_writer :logger
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def logger
|
20
|
+
@logger ||= begin
|
21
|
+
logger = ::Logger.new($stdout)
|
22
|
+
logger.progname = 'ruby_lib'
|
23
|
+
logger.level = ::Logger::WARN
|
24
|
+
logger.formatter = proc { |_severity, _datetime, _progname, msg| "#{msg}\n" }
|
25
|
+
logger
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end # class << self
|
29
|
+
end # module Logger
|
30
|
+
end # module Appium
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Appium
|
2
|
+
module Core
|
3
|
+
# MultiTouch actions allow for multiple touches to happen at the same time,
|
4
|
+
# for instance, to simulate multiple finger swipes.
|
5
|
+
#
|
6
|
+
# Create a series of touch actions by themselves (without a `prepare()`), then
|
7
|
+
# add to a new MultiTouch action. When ready, call `prepare()` and all
|
8
|
+
# actions will be executed simultaneously.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# action_1 = TouchAction.new.press(x: 45, y: 100).wait(5).release
|
13
|
+
# action_2 = TouchAction.new.tap(element: el, x: 50, y:5, count: 3)
|
14
|
+
#
|
15
|
+
# multi_touch_action = MultiTouch.new
|
16
|
+
# multi_touch_action.add action_1
|
17
|
+
# multi_touch_action.add action_2
|
18
|
+
# multi_touch_action.perform
|
19
|
+
#
|
20
|
+
# # with an arbitrary driver
|
21
|
+
# driver = Appium::Driver.new(opts, false).start_driver
|
22
|
+
# multi_touch_action = MultiTouch.new(driver)
|
23
|
+
# multi_touch_action.add action_1
|
24
|
+
# multi_touch_action.add action_2
|
25
|
+
# multi_touch_action.perform
|
26
|
+
#
|
27
|
+
class MultiTouch
|
28
|
+
attr_reader :driver
|
29
|
+
|
30
|
+
def initialize(driver)
|
31
|
+
@actions = []
|
32
|
+
@driver = driver
|
33
|
+
end
|
34
|
+
|
35
|
+
# Add a touch_action to be performed
|
36
|
+
# @param chain (TouchAction) The action to add to the chain
|
37
|
+
def add(chain)
|
38
|
+
@actions << chain.actions
|
39
|
+
end
|
40
|
+
|
41
|
+
# Ask Appium to perform the actions
|
42
|
+
def perform
|
43
|
+
@driver.multi_touch @actions
|
44
|
+
@actions.clear
|
45
|
+
end
|
46
|
+
end # class MultiTouch
|
47
|
+
end # module Core
|
48
|
+
end # module Appium
|
@@ -0,0 +1,191 @@
|
|
1
|
+
module Appium
|
2
|
+
# Perform a series of gestures, one after another. Gestures are chained
|
3
|
+
# together and only performed when `perform()` is called. Default is conducted by global driver.
|
4
|
+
#
|
5
|
+
# Each method returns the object itself, so calls can be chained.
|
6
|
+
#
|
7
|
+
# ```ruby
|
8
|
+
# action = TouchAction(@driver).new.press(x: 45, y: 100).wait(5).release
|
9
|
+
# action.perform
|
10
|
+
# action = TouchAction.new.swipe(....)
|
11
|
+
# action.perform
|
12
|
+
# ```
|
13
|
+
module Core
|
14
|
+
class TouchAction
|
15
|
+
ACTIONS = [:move_to, :long_press, :double_tap, :two_finger_tap, :press, :release, :tap, :wait, :perform].freeze
|
16
|
+
COMPLEX_ACTIONS = [:swipe].freeze
|
17
|
+
|
18
|
+
attr_reader :actions, :driver
|
19
|
+
|
20
|
+
def initialize(driver)
|
21
|
+
@actions = []
|
22
|
+
@driver = driver
|
23
|
+
end
|
24
|
+
|
25
|
+
# Move to the given co-ordinates.
|
26
|
+
#
|
27
|
+
# `move_to`'s `x` and `y` have two case. One is working as coordinate, the other is working as offset.
|
28
|
+
#
|
29
|
+
# @option opts [integer] :x x co-ordinate to move to if element isn't set. Works as an offset if x is set with Element.
|
30
|
+
# @option opts [integer] :y y co-ordinate to move to if element isn't set. Works as an offset if y is set with Element.
|
31
|
+
# @option opts [WebDriver::Element] Element to scope this move within.
|
32
|
+
def move_to(opts)
|
33
|
+
opts = args_with_ele_ref(opts)
|
34
|
+
chain_method(:moveTo, opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Press down for a specific duration.
|
38
|
+
# Alternatively, you can use `press(...).wait(...).release()` instead of `long_press` if duration doesn't work well.
|
39
|
+
# https://github.com/appium/ruby_lib/issues/231#issuecomment-269895512
|
40
|
+
# e.g. Appium::TouchAction.new.press(x: 280, y: 530).wait(2000).release.perform
|
41
|
+
#
|
42
|
+
# @option element [WebDriver::Element] the element to press.
|
43
|
+
# @option x [integer] x co-ordinate to press on.
|
44
|
+
# @option y [integer] y co-ordinate to press on.
|
45
|
+
# @option duration [integer] Number of milliseconds to press.
|
46
|
+
def long_press(opts)
|
47
|
+
args = opts.select { |k, _v| [:element, :x, :y, :duration].include? k }
|
48
|
+
args = args_with_ele_ref(args)
|
49
|
+
chain_method(:longPress, args) # longPress is what the appium server expects
|
50
|
+
end
|
51
|
+
|
52
|
+
# Press a finger onto the screen. Finger will stay down until you call
|
53
|
+
# `release`.
|
54
|
+
#
|
55
|
+
# @option opts [WebDriver::Element] :element (Optional) Element to press within.
|
56
|
+
# @option opts [integer] :x x co-ordinate to press on
|
57
|
+
# @option opts [integer] :y y co-ordinate to press on
|
58
|
+
def press(opts)
|
59
|
+
args = opts.select { |k, _v| [:element, :x, :y].include? k }
|
60
|
+
args = args_with_ele_ref(args)
|
61
|
+
chain_method(:press, args)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Remove a finger from the screen.
|
65
|
+
#
|
66
|
+
# @option opts [WebDriver::Element] :element (Optional) Element to release from.
|
67
|
+
# @option opts [integer] :x x co-ordinate to release from
|
68
|
+
# @option opts [integer] :y y co-ordinate to release from
|
69
|
+
def release(opts = nil)
|
70
|
+
args = args_with_ele_ref(opts) if opts
|
71
|
+
chain_method(:release, args)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Touch a point on the screen.
|
75
|
+
# Alternatively, you can use `press(...).release.perform` instead of `tap(...).perform`.
|
76
|
+
#
|
77
|
+
# @option opts [WebDriver::Element] :element (Optional) Element to restrict scope too.
|
78
|
+
# @option opts [integer] :x x co-ordinate to tap
|
79
|
+
# @option opts [integer] :y y co-ordinate to tap
|
80
|
+
# @option opts [integer] :fingers how many fingers to tap with (Default 1)
|
81
|
+
def tap(opts)
|
82
|
+
opts[:count] = opts.delete(:fingers) if opts[:fingers]
|
83
|
+
opts[:count] ||= 1
|
84
|
+
args = args_with_ele_ref opts
|
85
|
+
chain_method(:tap, args)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Double tap an element on the screen
|
89
|
+
#
|
90
|
+
# @option opts [WebDriver::Element] :element (Optional) Element to restrict scope too.
|
91
|
+
# @option opts [integer] :x x co-ordinate to tap
|
92
|
+
# @option opts [integer] :y y co-ordinate to tap
|
93
|
+
|
94
|
+
def double_tap(opts)
|
95
|
+
args = opts.select { |k, _v| [:element, :x, :y].include? k }
|
96
|
+
args = args_with_ele_ref(args)
|
97
|
+
chain_method(:doubleTap, args) # doubleTap is what the appium server expects
|
98
|
+
end
|
99
|
+
|
100
|
+
# Two finger tap an element on the screen
|
101
|
+
#
|
102
|
+
# @option opts [WebDriver::Element] :element (Optional) Element to restrict scope too.
|
103
|
+
# @option opts [integer] :x x co-ordinate to tap
|
104
|
+
# @option opts [integer] :y y co-ordinate to tap
|
105
|
+
def two_finger_tap(opts)
|
106
|
+
args = opts.select { |k, _v| [:element, :x, :y].include? k }
|
107
|
+
args = args_with_ele_ref(args)
|
108
|
+
chain_method(:twoFingerTap, args) # twoFingerTap is what the appium server expects
|
109
|
+
end
|
110
|
+
|
111
|
+
# Pause for a number of milliseconds before the next action
|
112
|
+
# @param milliseconds [integer] Number of milliseconds to pause for
|
113
|
+
def wait(milliseconds)
|
114
|
+
args = { ms: milliseconds }
|
115
|
+
chain_method(:wait, args)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Convenience method to peform a swipe.
|
119
|
+
#
|
120
|
+
# Note that iOS 7 simulators have broken swipe.
|
121
|
+
#
|
122
|
+
# For iOS: Use `offset_x` and `offset_y` to define the end point.
|
123
|
+
#
|
124
|
+
# For Android: Use `end_x` and `end_y` to define the end point.
|
125
|
+
#
|
126
|
+
# If you'd like more details, please read tests and its log samples in
|
127
|
+
# `ios_tests/lib/ios/specs/device/touch_actions.rb` and `ios_tests/lib/ios/specs/device/touch_actions.rb`
|
128
|
+
#
|
129
|
+
# @option opts [int] :start_x Where to start swiping, on the x axis. Default 0.
|
130
|
+
# @option opts [int] :start_y Where to start swiping, on the y axis. Default 0.
|
131
|
+
# @option opts [int] :offset_x Offset, on the x axis. Default 0.
|
132
|
+
# @option opts [int] :offset_y Offset, on the y axis. Default 0.
|
133
|
+
# @option opts [int] :duration How long the actual swipe takes to complete in milliseconds. Default 200.
|
134
|
+
def swipe(opts, ele = nil)
|
135
|
+
start_x = opts.fetch :start_x, 0
|
136
|
+
start_y = opts.fetch :start_y, 0
|
137
|
+
offset_x = opts.fetch :offset_x, 0
|
138
|
+
offset_y = opts.fetch :offset_y, 0
|
139
|
+
duration = opts.fetch :duration, 200
|
140
|
+
|
141
|
+
coordinates = swipe_coordinates(start_x: start_x, start_y: start_y, offset_x: offset_x, offset_y: offset_y)
|
142
|
+
|
143
|
+
if ele # pinch/zoom for XCUITest
|
144
|
+
press x: start_x, y: start_y, element: ele
|
145
|
+
move_to x: coordinates[:offset_x], y: coordinates[:offset_y], element: ele
|
146
|
+
else
|
147
|
+
press x: start_x, y: start_y
|
148
|
+
wait(duration) if duration
|
149
|
+
move_to x: coordinates[:offset_x], y: coordinates[:offset_y]
|
150
|
+
end
|
151
|
+
release
|
152
|
+
|
153
|
+
self
|
154
|
+
end
|
155
|
+
|
156
|
+
# Ask the driver to perform all actions in this action chain.
|
157
|
+
def perform
|
158
|
+
@driver.touch_actions @actions
|
159
|
+
@actions.clear
|
160
|
+
self
|
161
|
+
end
|
162
|
+
|
163
|
+
# Does nothing, currently.
|
164
|
+
def cancel
|
165
|
+
@actions << { action: cancel }
|
166
|
+
@driver.touch_actions @actions
|
167
|
+
self
|
168
|
+
end
|
169
|
+
|
170
|
+
# Visible for testing
|
171
|
+
# @private
|
172
|
+
def swipe_coordinates(start_x: 0, start_y: 0, offset_x: 0, offset_y: 0)
|
173
|
+
Appium::Logger.info "start_x: #{start_x}, start_y: #{start_y}, offset_x: #{offset_x}, offset_y: #{offset_y}"
|
174
|
+
{ offset_x: offset_x, offset_y: offset_y }
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def chain_method(method, args = nil)
|
180
|
+
action = args ? { action: method, options: args } : { action: method }
|
181
|
+
@actions << action
|
182
|
+
self
|
183
|
+
end
|
184
|
+
|
185
|
+
def args_with_ele_ref(args)
|
186
|
+
args[:element] = args[:element].ref if args.key? :element
|
187
|
+
args
|
188
|
+
end
|
189
|
+
end # class TouchAction
|
190
|
+
end # module Core
|
191
|
+
end # module Appium
|
@@ -0,0 +1,459 @@
|
|
1
|
+
module Appium
|
2
|
+
module Core
|
3
|
+
class Driver
|
4
|
+
# Selenium webdriver capabilities
|
5
|
+
# @return [Core::Base::Capabilities]
|
6
|
+
attr_reader :caps
|
7
|
+
|
8
|
+
# Return http client called in start_driver()
|
9
|
+
# @return [Appium::Core::Base::Http::Default] the http client
|
10
|
+
attr_reader :http_client
|
11
|
+
|
12
|
+
# Device type to request from the appium server
|
13
|
+
# @return [Symbol] :android and :ios, for example
|
14
|
+
attr_reader :device
|
15
|
+
|
16
|
+
# Automation name sent to appium server or received from server
|
17
|
+
# If automation_name is nil, it is not set both client side and server side.
|
18
|
+
# @return [Hash]
|
19
|
+
attr_reader :automation_name
|
20
|
+
|
21
|
+
# Custom URL for the selenium server. If set this attribute, ruby_lib try to handshake to the custom url.
|
22
|
+
# @return [String]
|
23
|
+
attr_reader :custom_url
|
24
|
+
|
25
|
+
# Export session id to textfile in /tmp for 3rd party tools
|
26
|
+
# @return [Boolean]
|
27
|
+
attr_reader :export_session
|
28
|
+
# @return [String] By default, session id is exported in '/tmp/appium_lib_session'
|
29
|
+
attr_reader :export_session_path
|
30
|
+
|
31
|
+
# Default wait time for elements to appear
|
32
|
+
# Returns the default client side wait.
|
33
|
+
# This value is independent of what the server is using
|
34
|
+
# Provide Appium::Drive like { appium_lib: { wait: 20 } }
|
35
|
+
# @return [Integer]
|
36
|
+
attr_reader :default_wait
|
37
|
+
|
38
|
+
# Appium's server port
|
39
|
+
# Provide Appium::Drive like { appium_lib: { port: 8080 } }
|
40
|
+
# @return [Integer]
|
41
|
+
attr_reader :port
|
42
|
+
|
43
|
+
# Return a time wait timeout
|
44
|
+
# Wait time for ::Appium::Core::Base::Wait, wait and wait_true
|
45
|
+
# Provide Appium::Drive like { appium_lib: { wait_timeout: 20 } }
|
46
|
+
# @return [Integer]
|
47
|
+
attr_reader :wait_timeout
|
48
|
+
|
49
|
+
# Return a time wait timeout
|
50
|
+
# Wait interval time for ::Appium::Core::Base::Wait, wait and wait_true
|
51
|
+
# Provide Appium::Drive like { appium_lib: { wait_interval: 20 } }
|
52
|
+
# @return [Integer]
|
53
|
+
attr_reader :wait_interval
|
54
|
+
|
55
|
+
# instance of AbstractEventListener for logging support
|
56
|
+
attr_reader :listener
|
57
|
+
|
58
|
+
# @return [Appium::Core::Base::Driver]
|
59
|
+
attr_reader :driver
|
60
|
+
|
61
|
+
# Creates a new global driver and extend particular methods to `target`
|
62
|
+
# @param [Class] target Extend particular methods to this target.
|
63
|
+
# @param [Hash] opts A options include capabilities for the Appium Server and for the client.
|
64
|
+
# @return [Driver]
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
#
|
68
|
+
# require 'rubygems'
|
69
|
+
# require 'appium_lib'
|
70
|
+
#
|
71
|
+
# # Start iOS driver
|
72
|
+
# opts = {
|
73
|
+
# caps: {
|
74
|
+
# platformName: :ios,
|
75
|
+
# app: '/path/to/MyiOS.app'
|
76
|
+
# },
|
77
|
+
# appium_lib: {
|
78
|
+
# server_url: "http://custom-host:8080/wd/hub.com",
|
79
|
+
# export_session: false,
|
80
|
+
# port: 8080,
|
81
|
+
# wait: 0,
|
82
|
+
# wait_timeout: 20,
|
83
|
+
# wait_interval: 0.3,
|
84
|
+
# listener: nil,
|
85
|
+
# }
|
86
|
+
# }
|
87
|
+
# @core_driver = Appium::Core.for(self, opts) # create a core driver with `opts` and extend methods into `self`
|
88
|
+
# @core_driver.start_driver(server_url: server_url, http_client_ops: http_client_ops) # start driver
|
89
|
+
#
|
90
|
+
def self.for(target, opts = {})
|
91
|
+
new(target, opts)
|
92
|
+
end
|
93
|
+
|
94
|
+
# @private
|
95
|
+
def initialize(target, opts = {})
|
96
|
+
opts = Appium.symbolize_keys opts
|
97
|
+
validate_keys(opts)
|
98
|
+
|
99
|
+
@caps = get_caps(opts)
|
100
|
+
|
101
|
+
set_appium_lib_specific_values(get_appium_lib_opts(opts))
|
102
|
+
set_app_path
|
103
|
+
set_appium_device
|
104
|
+
set_automation_name
|
105
|
+
|
106
|
+
extend_for(device: @device, automation_name: @automation_name, target: target)
|
107
|
+
|
108
|
+
self
|
109
|
+
end
|
110
|
+
|
111
|
+
# Creates a new global driver and quits the old one if it exists.
|
112
|
+
# You can customise http_client as the following
|
113
|
+
#
|
114
|
+
# @param [String] server_url Custom server url to send to requests. Default is "http://127.0.0.1:4723/wd/hub".
|
115
|
+
# @option http_client_ops [Hash] :http_client Custom HTTP Client
|
116
|
+
# @option http_client_ops [Hash] :open_timeout Custom open timeout for http client.
|
117
|
+
# @option http_client_ops [Hash] :read_timeout Custom read timeout for http client.
|
118
|
+
# @return [Selenium::WebDriver] the new global driver
|
119
|
+
#
|
120
|
+
# @example
|
121
|
+
#
|
122
|
+
# require 'rubygems'
|
123
|
+
# require 'appium_lib'
|
124
|
+
#
|
125
|
+
# # platformName takes a string or a symbol.
|
126
|
+
#
|
127
|
+
# # Start iOS driver
|
128
|
+
# opts = {
|
129
|
+
# caps: {
|
130
|
+
# platformName: :ios,
|
131
|
+
# app: '/path/to/MyiOS.app'
|
132
|
+
# },
|
133
|
+
# appium_lib: {
|
134
|
+
# server_url: "http://custom-host:8080/wd/hub.com",
|
135
|
+
# export_session: false,
|
136
|
+
# port: 8080,
|
137
|
+
# wait: 0,
|
138
|
+
# wait_timeout: 20,
|
139
|
+
# wait_interval: 0.3,
|
140
|
+
# listener: nil,
|
141
|
+
# }
|
142
|
+
# }
|
143
|
+
# @core = Appium::Driver.new(opts)
|
144
|
+
# @driver = @core.start_driver
|
145
|
+
#
|
146
|
+
def start_driver(server_url: nil,
|
147
|
+
http_client_ops: { http_client: nil, open_timeout: 999_999, read_timeout: 999_999 })
|
148
|
+
server_url = server_url ? server_url : "http://127.0.0.1:#{@port}/wd/hub"
|
149
|
+
|
150
|
+
# open_timeout and read_timeout are explicit wait.
|
151
|
+
open_timeout = http_client_ops.delete(:open_timeout)
|
152
|
+
read_timeout = http_client_ops.delete(:read_timeout)
|
153
|
+
|
154
|
+
http_client = http_client_ops.delete(:http_client)
|
155
|
+
@http_client ||= http_client ? http_client : Appium::Core::Base::Http::Default.new
|
156
|
+
|
157
|
+
@http_client.open_timeout = open_timeout if open_timeout
|
158
|
+
@http_client.read_timeout = read_timeout if read_timeout
|
159
|
+
|
160
|
+
begin
|
161
|
+
# included https://github.com/SeleniumHQ/selenium/blob/43f8b3f66e7e01124eff6a5805269ee441f65707/rb/lib/selenium/webdriver/remote/driver.rb#L29
|
162
|
+
@driver = ::Appium::Core::Base::Driver.new(http_client: @http_client,
|
163
|
+
desired_capabilities: @caps,
|
164
|
+
url: server_url,
|
165
|
+
listener: @listener)
|
166
|
+
|
167
|
+
# export session
|
168
|
+
write_session_id(@driver.session_id, @export_session_path) if @export_session
|
169
|
+
rescue Errno::ECONNREFUSED
|
170
|
+
raise "ERROR: Unable to connect to Appium. Is the server running on #{server_url}?"
|
171
|
+
end
|
172
|
+
|
173
|
+
# If "automationName" is set only server side, this method set "automationName" attribute into @automation_name.
|
174
|
+
# Since @automation_name is set only client side before start_driver is called.
|
175
|
+
set_automation_name_if_nil
|
176
|
+
|
177
|
+
set_implicit_wait_by_default(@default_wait)
|
178
|
+
|
179
|
+
@driver
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
# Ignore setting default wait if the target driver has no implementation
|
185
|
+
def set_implicit_wait_by_default(wait)
|
186
|
+
@driver.manage.timeouts.implicit_wait = wait
|
187
|
+
rescue Selenium::WebDriver::Error::UnknownError => e
|
188
|
+
unless e.message.include?('The operation requested is not yet implemented')
|
189
|
+
raise e.message, ::Appium::Core::Error::ServerError
|
190
|
+
end
|
191
|
+
|
192
|
+
Appium::Logger.debug(e.message)
|
193
|
+
{}
|
194
|
+
end
|
195
|
+
|
196
|
+
public
|
197
|
+
|
198
|
+
# Quits the driver
|
199
|
+
# @return [void]
|
200
|
+
#
|
201
|
+
# @example
|
202
|
+
#
|
203
|
+
# @core.quit_driver
|
204
|
+
#
|
205
|
+
def quit_driver
|
206
|
+
@driver.quit
|
207
|
+
rescue
|
208
|
+
nil
|
209
|
+
end
|
210
|
+
|
211
|
+
# Returns the server's version info
|
212
|
+
# @return [Hash]
|
213
|
+
#
|
214
|
+
# @example
|
215
|
+
#
|
216
|
+
# @core.appium_server_version
|
217
|
+
# {
|
218
|
+
# "build" => {
|
219
|
+
# "version" => "0.18.1",
|
220
|
+
# "revision" => "d242ebcfd92046a974347ccc3a28f0e898595198"
|
221
|
+
# }
|
222
|
+
# }
|
223
|
+
#
|
224
|
+
# Returns blank hash for Selenium Grid since `remote_status` gets 500 error
|
225
|
+
#
|
226
|
+
# @example
|
227
|
+
#
|
228
|
+
# @core.appium_server_version #=> {}
|
229
|
+
#
|
230
|
+
def appium_server_version
|
231
|
+
@driver.remote_status
|
232
|
+
rescue Selenium::WebDriver::Error::ServerError => e
|
233
|
+
raise ::Appium::Core::Error::ServerError unless e.message.include?('status code 500')
|
234
|
+
# driver.remote_status returns 500 error for using selenium grid
|
235
|
+
{}
|
236
|
+
end
|
237
|
+
|
238
|
+
# Return the platform version as an array of integers
|
239
|
+
# @return [Array<Integer>]
|
240
|
+
#
|
241
|
+
# @example
|
242
|
+
#
|
243
|
+
# @core.platform_version #=> [10.1.1]
|
244
|
+
#
|
245
|
+
def platform_version
|
246
|
+
p_version = @driver.capabilities['platformVersion']
|
247
|
+
p_version.split('.').map(&:to_i)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Takes a png screenshot and saves to the target path.
|
251
|
+
#
|
252
|
+
# @param png_save_path [String] the full path to save the png
|
253
|
+
# @return [File]
|
254
|
+
#
|
255
|
+
# @example
|
256
|
+
#
|
257
|
+
# @core.screenshot '/tmp/hi.png' #=> nil
|
258
|
+
# # same as `@driver.save_screenshot png_save_path`
|
259
|
+
#
|
260
|
+
def screenshot(png_save_path)
|
261
|
+
warn '[DEPRECATION] screenshot will be removed. Please use driver.save_screenshot instead.'
|
262
|
+
@driver.save_screenshot png_save_path
|
263
|
+
end
|
264
|
+
|
265
|
+
# Check every interval seconds to see if yield returns a truthy value.
|
266
|
+
# Note this isn't a strict boolean true, any truthy value is accepted.
|
267
|
+
# false and nil are considered failures.
|
268
|
+
# Give up after timeout seconds.
|
269
|
+
#
|
270
|
+
# Wait code from the selenium Ruby gem
|
271
|
+
# https://github.com/SeleniumHQ/selenium/blob/cf501dda3f0ed12233de51ce8170c0e8090f0c20/rb/lib/selenium/webdriver/common/wait.rb
|
272
|
+
#
|
273
|
+
# If only a number is provided then it's treated as the timeout value.
|
274
|
+
#
|
275
|
+
# @param [Hash] opts Options
|
276
|
+
# @option opts [Numeric] :timeout Seconds to wait before timing out. Set default by `appium_wait_timeout` (30).
|
277
|
+
# @option opts [Numeric] :interval Seconds to sleep between polls. Set default by `appium_wait_interval` (0.5).
|
278
|
+
# @option opts [String] :message Exception message if timed out.
|
279
|
+
# @option opts [Array, Exception] :ignore Exceptions to ignore while polling (default: Exception)
|
280
|
+
#
|
281
|
+
# @example
|
282
|
+
#
|
283
|
+
# @core.wait_true { @driver.find_element :accessibility_id, 'something' }
|
284
|
+
#
|
285
|
+
def wait_true(opts = {})
|
286
|
+
opts = process_wait_opts(opts).merge(return_if_true: true)
|
287
|
+
|
288
|
+
opts[:timeout] ||= @wait_timeout
|
289
|
+
opts[:interval] ||= @wait_interval
|
290
|
+
|
291
|
+
wait = ::Appium::Core::Base::Wait.new opts
|
292
|
+
wait.until { yield }
|
293
|
+
end
|
294
|
+
|
295
|
+
# Check every interval seconds to see if yield doesn't raise an exception.
|
296
|
+
# Give up after timeout seconds.
|
297
|
+
#
|
298
|
+
# Wait code from the selenium Ruby gem
|
299
|
+
# https://github.com/SeleniumHQ/selenium/blob/cf501dda3f0ed12233de51ce8170c0e8090f0c20/rb/lib/selenium/webdriver/common/wait.rb
|
300
|
+
#
|
301
|
+
# If only a number is provided then it's treated as the timeout value.
|
302
|
+
#
|
303
|
+
# @param [Hash] opts Options
|
304
|
+
# @option opts [Numeric] :timeout Seconds to wait before timing out. Set default by `appium_wait_timeout` (30).
|
305
|
+
# @option opts [Numeric] :interval Seconds to sleep between polls. Set default by `appium_wait_interval` (0.5).
|
306
|
+
# @option opts [String] :message Exception message if timed out.
|
307
|
+
# @option opts [Array, Exception] :ignore Exceptions to ignore while polling (default: Exception)
|
308
|
+
#
|
309
|
+
# @example
|
310
|
+
#
|
311
|
+
# @core.wait { @driver.find_element :accessibility_id, 'something' }
|
312
|
+
#
|
313
|
+
def wait(opts = {})
|
314
|
+
opts = process_wait_opts(opts).merge(return_if_true: false)
|
315
|
+
|
316
|
+
opts[:timeout] ||= @wait_timeout
|
317
|
+
opts[:interval] ||= @wait_interval
|
318
|
+
|
319
|
+
wait = ::Appium::Core::Base::Wait.new opts
|
320
|
+
wait.until { yield }
|
321
|
+
end
|
322
|
+
|
323
|
+
private
|
324
|
+
|
325
|
+
# @private
|
326
|
+
def process_wait_opts(opts)
|
327
|
+
opts = { timeout: opts } if opts.is_a?(Numeric)
|
328
|
+
raise 'opts must be a hash' unless opts.is_a? Hash
|
329
|
+
opts
|
330
|
+
end
|
331
|
+
|
332
|
+
# @private
|
333
|
+
def extend_for(device:, automation_name:, target:)
|
334
|
+
target.extend Appium::Core
|
335
|
+
target.extend Appium::Core::Device
|
336
|
+
|
337
|
+
case device
|
338
|
+
when :android
|
339
|
+
case automation_name
|
340
|
+
when :espresso
|
341
|
+
::Appium::Core::Android::Espresso::Bridge.for(self)
|
342
|
+
when :uiautomator2
|
343
|
+
::Appium::Core::Android::Uiautomator2::Bridge.for(self)
|
344
|
+
else # default and UiAutomator
|
345
|
+
::Appium::Core::Android::Uiautomator1::Bridge.for(self)
|
346
|
+
end
|
347
|
+
when :ios
|
348
|
+
case automation_name
|
349
|
+
when :xcuitest
|
350
|
+
::Appium::Core::Ios::Xcuitest::Bridge.for(self)
|
351
|
+
else # default and UIAutomation
|
352
|
+
::Appium::Core::Ios::Uiautomation::Bridge.for(self)
|
353
|
+
end
|
354
|
+
when :mac
|
355
|
+
# no Mac specific extentions
|
356
|
+
Appium::Logger.debug('mac')
|
357
|
+
when :windows
|
358
|
+
# no windows specific extentions
|
359
|
+
Appium::Logger.debug('windows')
|
360
|
+
else
|
361
|
+
Appium::Logger.warn('no device matched')
|
362
|
+
end
|
363
|
+
|
364
|
+
target
|
365
|
+
end
|
366
|
+
|
367
|
+
# @private
|
368
|
+
def validate_keys(opts)
|
369
|
+
flatten_ops = flatten_hash_keys(opts)
|
370
|
+
|
371
|
+
raise Error::NoCapabilityError unless opts.member?(:caps)
|
372
|
+
if !opts.member?(:appium_lib) && flatten_ops.member?(:appium_lib)
|
373
|
+
raise Error::CapabilityStructureError, 'Please check the value of appium_lib in the capability'
|
374
|
+
end
|
375
|
+
|
376
|
+
true
|
377
|
+
end
|
378
|
+
|
379
|
+
# @private
|
380
|
+
def flatten_hash_keys(hash, flatten_keys_result = [])
|
381
|
+
hash.each do |key, value|
|
382
|
+
flatten_keys_result << key
|
383
|
+
flatten_hash_keys(value, flatten_keys_result) if value.is_a?(Hash)
|
384
|
+
end
|
385
|
+
|
386
|
+
flatten_keys_result
|
387
|
+
end
|
388
|
+
|
389
|
+
# @private
|
390
|
+
def get_caps(opts)
|
391
|
+
Core::Base::Capabilities.create_capabilities(opts[:caps] || {})
|
392
|
+
end
|
393
|
+
|
394
|
+
# @private
|
395
|
+
def get_appium_lib_opts(opts)
|
396
|
+
opts[:appium_lib] || {}
|
397
|
+
end
|
398
|
+
|
399
|
+
# @private
|
400
|
+
# Path to the .apk, .app or .app.zip.
|
401
|
+
# The path can be local or remote for Sauce.
|
402
|
+
def set_app_path
|
403
|
+
return unless @caps && @caps[:app] && !@caps[:app].empty?
|
404
|
+
@caps[:app] = File.expand_path(@caps[:app])
|
405
|
+
end
|
406
|
+
|
407
|
+
# @private
|
408
|
+
def set_appium_lib_specific_values(appium_lib_opts)
|
409
|
+
@custom_url = appium_lib_opts.fetch :server_url, false
|
410
|
+
@default_wait = appium_lib_opts.fetch :wait, 0
|
411
|
+
|
412
|
+
# bump current session id into a particular file
|
413
|
+
@export_session = appium_lib_opts.fetch :export_session, false
|
414
|
+
@export_session_path = appium_lib_opts.fetch :export_session_path, '/tmp/appium_lib_session'
|
415
|
+
|
416
|
+
@port = appium_lib_opts.fetch :port, 4723
|
417
|
+
|
418
|
+
# timeout and interval used in ::Appium::Comm.wait/wait_true
|
419
|
+
@wait_timeout = appium_lib_opts.fetch :wait_timeout, 30
|
420
|
+
@wait_interval = appium_lib_opts.fetch :wait_interval, 0.5
|
421
|
+
|
422
|
+
# to pass it in Selenium.new.
|
423
|
+
# `listener = opts.delete(:listener)` is called in Selenium::Driver.new
|
424
|
+
@listener = appium_lib_opts.fetch :listener, nil
|
425
|
+
end
|
426
|
+
|
427
|
+
# @private
|
428
|
+
def set_appium_device
|
429
|
+
# https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile
|
430
|
+
@device = @caps[:platformName]
|
431
|
+
return @device unless @device
|
432
|
+
|
433
|
+
@device = @device.is_a?(Symbol) ? @device : @device.downcase.strip.intern
|
434
|
+
end
|
435
|
+
|
436
|
+
# @private
|
437
|
+
def set_automation_name
|
438
|
+
@automation_name = @caps[:automationName] if @caps[:automationName]
|
439
|
+
@automation_name = if @automation_name
|
440
|
+
@automation_name.is_a?(Symbol) ? @automation_name : @automation_name.downcase.strip.intern
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# @private
|
445
|
+
def set_automation_name_if_nil
|
446
|
+
return unless @automation_name.nil?
|
447
|
+
@automation_name = @driver.capabilities['automationName']
|
448
|
+
end
|
449
|
+
|
450
|
+
# @private
|
451
|
+
def write_session_id(session_id, export_path = '/tmp/appium_lib_session')
|
452
|
+
File.open(export_path, 'w') { |f| f.puts session_id }
|
453
|
+
rescue IOError => e
|
454
|
+
::Appium::Logger.warn e
|
455
|
+
nil
|
456
|
+
end
|
457
|
+
end # class Driver
|
458
|
+
end # module Core
|
459
|
+
end # module Appium
|