-
-
Notifications
You must be signed in to change notification settings - Fork 160
/
driver.rb
613 lines (546 loc) · 19.1 KB
/
driver.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
# Fix uninitialized constant Minitest (NameError)
module Minitest
# Fix superclass mismatch for class Spec
class Runnable
end
class Test < Runnable
end
class Spec < Test
end
end
require 'appium_lib_core'
module Appium
REQUIRED_VERSION_XCUITEST = '1.6.0'.freeze
class Driver
# attr readers are promoted to global scope. To avoid clobbering, they're
# made available via the driver_attributes method
#
# attr_accessor is repeated for each one so YARD documents them properly.
# The amount to sleep in seconds before every webdriver http call.
attr_accessor :global_webdriver_http_sleep
# SauceLab's settings
attr_reader :sauce
# Username for use on Sauce Labs. Set `false` to disable Sauce, even when SAUCE_USERNAME is in ENV.
# same as @sauce.username
attr_reader :sauce_username
# Access Key for use on Sauce Labs. Set `false` to disable Sauce, even when SAUCE_ACCESS_KEY is in ENV.
# same as @sauce.access_key
attr_reader :sauce_access_key
# Override the Sauce Appium endpoint to allow e.g. TestObject tests
# same as @sauce.endpoint
attr_reader :sauce_endpoint
# from Core
# read http://www.rubydoc.info/github/appium/ruby_lib_core/Appium/Core/Driver
attr_reader :caps
attr_reader :custom_url
attr_reader :export_session
attr_reader :export_session_path
attr_reader :default_wait
attr_reader :appium_port
attr_reader :appium_device
attr_reader :automation_name
attr_reader :listener
attr_reader :http_client
attr_reader :appium_wait_timeout
attr_reader :appium_wait_interval
# Appium's server version
attr_reader :appium_server_status
# Boolean debug mode for the Appium Ruby bindings
attr_reader :appium_debug
# Returns the driver
# @return [Driver] the driver
attr_reader :driver
# Instance of Appium::Core::Driver
attr_reader :core
# Creates a new driver. The driver is defined as global scope by default.
# We can avoid defining global driver.
#
# @example
#
# require 'rubygems'
# require 'appium_lib'
#
# # platformName takes a string or a symbol.
# # Start iOS driver with global scope
# opts = {
# caps: {
# platformName: :ios,
# app: '/path/to/MyiOS.app'
# },
# appium_lib: {
# wait_timeout: 30
# }
# }
# Appium::Driver.new(opts, true).start_driver
#
# # Start Android driver with global scope
# opts = {
# caps: {
# platformName: :android,
# app: '/path/to/my.apk'
# },
# appium_lib: {
# wait_timeout: 30,
# wait_interval: 1
# }
# }
# Appium::Driver.new(opts, true).start_driver
#
# # Start iOS driver without global scope
# opts = {
# caps: {
# platformName: :ios,
# app: '/path/to/MyiOS.app'
# },
# appium_lib: {
# wait_timeout: 30
# }
# }
# Appium::Driver.new(opts, false).start_driver
#
# # Start iOS driver without global scope
# opts = {
# caps: {
# platformName: :ios,
# app: '/path/to/MyiOS.app'
# },
# appium_lib: {
# wait_timeout: 30
# },
# global_driver: false
# }
# Appium::Driver.new(opts).start_driver
#
# @param opts [Object] A hash containing various options.
# @param global_driver [Bool] A bool require global driver before initialize.
# @return [Driver]
def initialize(opts = {}, global_driver = nil)
# TODO: set `global_driver = false` by default in the future.
# Capybara can't put `global_driver` as the 2nd argument.
global_driver = opts.delete :global_driver if global_driver.nil?
if global_driver.nil?
warn '[DEPRECATION] Appium::Driver.new(opts) will not generate global driver by default.' \
'If you would like to generate the global driver dy default, ' \
'please initialise driver with Appium::Driver.new(opts, true)'
global_driver = true # if global_driver is nil, then global_driver must be default value.
end
if global_driver
$driver.driver_quit if $driver
end
raise 'opts must be a hash' unless opts.is_a? Hash
@core = Appium::Core.for(self, opts)
opts = Appium.symbolize_keys opts
appium_lib_opts = opts[:appium_lib] || {}
@caps = @core.caps
@custom_url = @core.custom_url
@export_session = @core.export_session
@export_session_path = @core.export_session_path
@default_wait = @core.default_wait
@appium_port = @core.port
@appium_wait_timeout = @core.wait_timeout
@appium_wait_interval = @core.wait_interval
@listener = @core.listener
@appium_device = @core.device
@automation_name = @core.automation_name
# override opts[:app] if sauce labs
set_app_path(opts)
# enable debug patch
@appium_debug = appium_lib_opts.fetch :debug, !!defined?(Pry)
set_sauce_related_values(appium_lib_opts)
# Extend Common methods
extend Appium::Common
extend Appium::Device
# Extend each driver's methods
extend_for(device: @core.device, automation_name: @core.automation_name)
# for command
if @appium_debug
Appium::Logger.debug opts unless opts.empty?
Appium::Logger.debug "Debug is: #{@appium_debug}"
Appium::Logger.debug "Device is: #{@core.device}"
end
# Save global reference to last created Appium driver for top level methods.
$driver = self if global_driver
self # return newly created driver
end
private
# @private
def extend_for(device:, automation_name:)
case device
when :android
case automation_name
when :uiautomator2
::Appium::Android::Uiautomator2::Bridge.for(self)
else # default and UiAutomator
::Appium::Android::Bridge.for(self)
end
when :ios
case automation_name
when :xcuitest
::Appium::Ios::Xcuitest::Bridge.for(self)
else # default and UIAutomation
::Appium::Ios::Bridge.for(self)
end
when :mac
# no Mac specific extentions
Appium::Logger.debug('mac')
when :windows
# no windows specific extentions
Appium::Logger.debug('windows')
else
Appium::Logger.warn('no device matched')
end
end
# @private
def set_app_path(opts)
return unless @core.caps && @core.caps[:app] && !@core.caps[:app].empty?
@core.caps[:app] = self.class.absolute_app_path opts
end
# @private
def set_sauce_related_values(appium_lib_opts)
@sauce = Appium::SauceLabs.new(appium_lib_opts)
@sauce_username = @sauce.username
@sauce_access_key = @sauce.access_key
@sauce_endpoint = @sauce.endpoint
end
public
# Returns a hash of the driver attributes
def driver_attributes
{
caps: @core.caps,
automation_name: @core.automation_name,
custom_url: @core.custom_url,
export_session: @core.export_session,
export_session_path: @core.export_session_path,
default_wait: @core.default_wait,
sauce_username: @sauce.username,
sauce_access_key: @sauce.access_key,
sauce_endpoint: @sauce.endpoint,
port: @core.port,
device: @core.device,
debug: @appium_debug,
listener: @listener,
wait_timeout: @core.wait_timeout,
wait_interval: @core.wait_interval
}
end
def device_is_android?
@core.device == :android
end
def device_is_ios?
@core.device == :ios
end
def device_is_windows?
@core.device == :windows
end
# Return true if automationName is 'uiautomator2'
# @return [Boolean]
def automation_name_is_uiautomator2?
!@core.automation_name.nil? && @core.automation_name == :uiautomator2
end
# Return true if automationName is 'Espresso'
# @return [Boolean]
def automation_name_is_espresso?
!@core.automation_name.nil? && @core.automation_name == :espresso
end
# Return true if automationName is 'XCUITest'
# @return [Boolean]
def automation_name_is_xcuitest?
!@core.automation_name.nil? && @core.automation_name == :xcuitest
end
# Return true if the target Appium server is over REQUIRED_VERSION_XCUITEST.
# If the Appium server is under REQUIRED_VERSION_XCUITEST, then error is raised.
# @return [Boolean]
def check_server_version_xcuitest
if automation_name_is_xcuitest? &&
!@appium_server_status.empty? &&
(@appium_server_status['build']['version'] < REQUIRED_VERSION_XCUITEST)
raise(Appium::Core::Error::NotSupportedAppiumServer,
"XCUITest requires Appium version >= #{REQUIRED_VERSION_XCUITEST}")
end
true
end
# Returns the server's version info
#
# @example
# {
# "build" => {
# "version" => "0.18.1",
# "revision" => "d242ebcfd92046a974347ccc3a28f0e898595198"
# }
# }
#
# @return [Hash]
def appium_server_version
@core.appium_server_version
rescue Selenium::WebDriver::Error::WebDriverError => ex
raise ::Appium::Core::Error::ServerError unless ex.message.include?('content-type=""')
# server (TestObject for instance) does not respond to status call
{}
end
# Return the platform version as an array of integers
# @return [Array<Integer>]
def platform_version
@core.platform_version
end
# Returns the client's version info
#
# @example
#
# {
# "version" => "9.1.1"
# }
#
# @return [Hash]
def appium_client_version
{ version: ::Appium::VERSION }
end
# Converts app_path to an absolute path.
#
# opts is the full options hash (caps and appium_lib). If server_url is set
# then the app path is used as is.
#
# if app isn't set then an error is raised.
#
# @return [String] APP_PATH as an absolute path
def self.absolute_app_path(opts)
raise 'opts must be a hash' unless opts.is_a? Hash
caps = opts[:caps] || {}
appium_lib_opts = opts[:appium_lib] || {}
server_url = appium_lib_opts.fetch :server_url, false
app_path = caps[:app]
raise 'absolute_app_path invoked and app is not set!' if app_path.nil? || app_path.empty?
# may be absolute path to file on remote server.
# if the file is on the remote server then we can't check if it exists
return app_path if server_url
# Sauce storage API. http://saucelabs.com/docs/rest#storage
return app_path if app_path.start_with? 'sauce-storage:'
return app_path if app_path =~ /^http/ # public URL for Sauce
if app_path =~ /^(\/|[a-zA-Z]:)/ # absolute file path
app_path = File.expand_path app_path unless File.exist? app_path
raise "App doesn't exist. #{app_path}" unless File.exist? app_path
return app_path
end
# if it doesn't contain a slash then it's a bundle id
return app_path unless app_path =~ /[\/\\]/
# relative path that must be expanded.
# absolute_app_path is called from load_settings
# and the txt file path is the base of the app path in that case.
app_path = File.expand_path app_path
raise "App doesn't exist #{app_path}" unless File.exist? app_path
app_path
end
# Get the server url
# @return [String] the server url
def server_url
return @core.custom_url if @core.custom_url
return @sauce.server_url if @sauce.sauce_server_url?
"http://127.0.0.1:#{@core.port}/wd/hub"
end
# Restarts the driver
# @return [Driver] the driver
def restart
driver_quit
start_driver
end
# Takes a png screenshot and saves to the target path.
#
# @example
#
# screenshot '/tmp/hi.png'
#
# @param png_save_path [String] the full path to save the png
# @return [nil]
def screenshot(png_save_path)
@driver.save_screenshot png_save_path
nil
end
# Quits the driver
# @return [void]
def driver_quit
@core.quit_driver
end
alias quit_driver driver_quit
# Get the device window's size.
# @return [Selenium::WebDriver::Dimension]
#
# @example
#
# size = @driver.window_size
# size.width #=> Integer
# size.height #=> Integer
#
def window_size
@driver.window_size
end
# Creates a new global driver and quits the old one if it exists.
# You can customise http_client as the following
#
# Read http://www.rubydoc.info/github/appium/ruby_lib_core/Appium/Core/Device to understand more what the driver
# can call instance methods.
#
# @example
#
# require 'rubygems'
# require 'appium_lib'
#
# # platformName takes a string or a symbol.
# # Start iOS driver
# opts = {
# caps: {
# platformName: :ios,
# app: '/path/to/MyiOS.app'
# },
# appium_lib: {
# wait_timeout: 30
# }
# }
# Appium::Driver.new(opts).start_driver
#
# @option http_client_ops [Hash] :http_client Custom HTTP Client
# @option http_client_ops [Hash] :open_timeout Custom open timeout for http client.
# @option http_client_ops [Hash] :read_timeout Custom read timeout for http client.
# @return [Selenium::WebDriver] the new global driver
def start_driver(http_client_ops =
{ http_client: ::Appium::Http::Default.new, open_timeout: 999_999, read_timeout: 999_999 })
@core.quit_driver
# If automationName is set only in server side, then the following automation_name should be nil before
# starting driver.
automation_name = @core.automation_name
@driver = @core.start_driver(server_url: server_url, http_client_ops: http_client_ops)
@http_client = @core.http_client
# if automation_name was nil before start_driver, then re-extend driver specific methods
# to be able to extend correctly.
extend_for(device: @core.device, automation_name: @core.automation_name) if automation_name.nil?
@appium_server_status = appium_server_version
check_server_version_xcuitest
set_implicit_wait(@core.default_wait)
@driver
end
# To ignore error for Espresso Driver
def set_implicit_wait(wait)
@driver.manage.timeouts.implicit_wait = wait
rescue Selenium::WebDriver::Error::UnknownError => e
unless e.message.include?('The operation requested is not yet implemented by Espresso driver')
raise ::Appium::Core::Error::ServerError
end
{}
end
# Set implicit wait to zero.
def no_wait
@driver.manage.timeouts.implicit_wait = 0
end
# Set implicit wait. Default to @core.default_wait.
#
# @example
#
# set_wait 2
# set_wait # @core.default_wait
#
#
# @param timeout [Integer] the timeout in seconds
# @return [void]
def set_wait(timeout = nil)
timeout = @core.default_wait if timeout.nil?
@driver.manage.timeouts.implicit_wait = timeout
end
# Returns existence of element.
#
# Example:
#
# exists { button('sign in') } ? puts('true') : puts('false')
#
# @param [Integer] pre_check The amount in seconds to set the
# wait to before checking existence
# @param [Integer] post_check The amount in seconds to set the
# wait to after checking existence
# @yield The block to call
# @return [Boolean]
def exists(pre_check = 0, post_check = @core.default_wait)
# do not uset set_wait here.
# it will cause problems with other methods reading the default_wait of 0
# which then gets converted to a 1 second wait.
@driver.manage.timeouts.implicit_wait = pre_check
# the element exists unless an error is raised.
exists = true
begin
yield # search for element
rescue
exists = false # error means it's not there
end
# restore wait
@driver.manage.timeouts.implicit_wait = post_check if post_check != pre_check
exists
end
# The same as @driver.execute_script
# @param [String] script The script to execute
# @param [*args] args The args to pass to the script
# @return [Object]
def execute_script(script, *args)
@driver.execute_script script, *args
end
# Calls @driver.find_elements_with_appium
#
# @example
#
# @driver = Appium::Driver.new(opts, false)
# @driver.find_elements :predicate, yyy
#
# If you call `Appium.promote_appium_methods`, you can call `find_elements` directly.
#
# @example
#
# @driver = Appium::Driver.new(opts, false)
# @driver.find_elements :predicate, yyy
#
# If you call `Appium.promote_appium_methods`, you can call `find_elements` directly.
#
# @param [*args] args The args to use
# @return [Array<Element>] Array is empty when no elements are found.
def find_elements(*args)
@driver.find_elements(*args)
end
# Calls @driver.find_element
#
# @example
#
# @driver = Appium::Driver.new(opts, false)
# @driver.find_element :accessibility_id, zzz
#
# If you call `Appium.promote_appium_methods`, you can call `find_element` directly.
#
# @param [*args] args The args to use
# @return [Element]
def find_element(*args)
@driver.find_element(*args)
end
# Calls @driver.set_location
#
# @note This method does not work on real devices.
#
# @param [Hash] opts consisting of:
# @option opts [Float] :latitude the latitude in degrees (required)
# @option opts [Float] :longitude the longitude in degees (required)
# @option opts [Float] :altitude the altitude, defaulting to 75
# @return [Selenium::WebDriver::Location] the location constructed by the selenium webdriver
def set_location(opts = {})
latitude = opts.fetch(:latitude)
longitude = opts.fetch(:longitude)
altitude = opts.fetch(:altitude, 75)
@driver.set_location(latitude, longitude, altitude)
end
# Quit the driver and Pry.
# quit and exit are reserved by Pry.
# @return [void]
def x
driver_quit
exit # exit pry
end
end # class Driver
end # module Appium
# Paging in Pry is annoying :q required to exit.
# With pager disabled, the output is similar to IRB
# Only set if Pry is defined and there is no `.pryrc` files.
if defined?(Pry) && !(File.exist?(Pry::HOME_RC_FILE) || File.exist?(Pry::LOCAL_RC_FILE))
Appium::Logger.debug 'Pry.config.pager = false is set.'
Pry.config.pager = false
end