appium_lib 0.24.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +17 -8
  3. data/android_tests/Gemfile +1 -0
  4. data/android_tests/LICENSE-2.0.txt +202 -0
  5. data/android_tests/Rakefile +61 -0
  6. data/android_tests/api.apk +0 -0
  7. data/android_tests/appium.txt +3 -0
  8. data/android_tests/flaky.txt +1 -0
  9. data/android_tests/lib/android/specs/android/dynamic.rb +5 -0
  10. data/android_tests/lib/android/specs/android/element/alert.rb +41 -0
  11. data/android_tests/lib/android/specs/android/element/button.rb +55 -0
  12. data/android_tests/lib/android/specs/android/element/generic.rb +48 -0
  13. data/android_tests/lib/android/specs/android/element/text.rb +39 -0
  14. data/android_tests/lib/android/specs/android/element/textfield.rb +60 -0
  15. data/android_tests/lib/android/specs/android/helper.rb +80 -0
  16. data/android_tests/lib/android/specs/android/patch.rb +14 -0
  17. data/android_tests/lib/android/specs/common/device.rb +117 -0
  18. data/android_tests/lib/android/specs/common/element/window.rb +9 -0
  19. data/android_tests/lib/android/specs/common/helper.rb +112 -0
  20. data/android_tests/lib/android/specs/common/patch.rb +69 -0
  21. data/android_tests/lib/android/specs/common/version.rb +9 -0
  22. data/android_tests/lib/android/specs/driver.rb +174 -0
  23. data/android_tests/lib/format.rb +49 -0
  24. data/android_tests/lib/run.rb +72 -0
  25. data/android_tests/readme.md +27 -0
  26. data/appium_lib.gemspec +8 -5
  27. data/docs/android_docs.md +1052 -716
  28. data/docs/ios_docs.md +657 -834
  29. data/docs_gen/make_docs.rb +1 -3
  30. data/ios_tests/Gemfile +1 -0
  31. data/ios_tests/LICENSE-2.0.txt +202 -0
  32. data/ios_tests/Rakefile +47 -0
  33. data/ios_tests/UICatalog.app.zip +0 -0
  34. data/ios_tests/UICatalog.app/12-6AM.png +0 -0
  35. data/ios_tests/UICatalog.app/12-6PM.png +0 -0
  36. data/ios_tests/UICatalog.app/6-12AM.png +0 -0
  37. data/ios_tests/UICatalog.app/6-12PM.png +0 -0
  38. data/ios_tests/UICatalog.app/Default-568h@2x.png +0 -0
  39. data/ios_tests/UICatalog.app/Default@2x.png +0 -0
  40. data/ios_tests/UICatalog.app/Info.plist +0 -0
  41. data/ios_tests/UICatalog.app/PkgInfo +1 -0
  42. data/ios_tests/UICatalog.app/UIButton_custom.png +0 -0
  43. data/ios_tests/UICatalog.app/UICatalog +0 -0
  44. data/ios_tests/UICatalog.app/blueButton.png +0 -0
  45. data/ios_tests/UICatalog.app/bookmarkImage.png +0 -0
  46. data/ios_tests/UICatalog.app/bookmarkImageHighlighted.png +0 -0
  47. data/ios_tests/UICatalog.app/divider.png +0 -0
  48. data/ios_tests/UICatalog.app/en.lproj/AlertsViewController.nib +0 -0
  49. data/ios_tests/UICatalog.app/en.lproj/ButtonsViewController.nib +0 -0
  50. data/ios_tests/UICatalog.app/en.lproj/ControlsViewController.nib +0 -0
  51. data/ios_tests/UICatalog.app/en.lproj/ImagesViewController.nib +0 -0
  52. data/ios_tests/UICatalog.app/en.lproj/Localizable.strings +0 -0
  53. data/ios_tests/UICatalog.app/en.lproj/MainWindow.nib +0 -0
  54. data/ios_tests/UICatalog.app/en.lproj/PickerViewController.nib +0 -0
  55. data/ios_tests/UICatalog.app/en.lproj/SearchBarController.nib +0 -0
  56. data/ios_tests/UICatalog.app/en.lproj/SegmentViewController.nib +0 -0
  57. data/ios_tests/UICatalog.app/en.lproj/TextFieldController.nib +0 -0
  58. data/ios_tests/UICatalog.app/en.lproj/TextViewController.nib +0 -0
  59. data/ios_tests/UICatalog.app/en.lproj/ToolbarViewController.nib +0 -0
  60. data/ios_tests/UICatalog.app/en.lproj/TransitionViewController.nib +0 -0
  61. data/ios_tests/UICatalog.app/en.lproj/WebViewController.nib +0 -0
  62. data/ios_tests/UICatalog.app/orangeslide.png +0 -0
  63. data/ios_tests/UICatalog.app/scene1.jpg +0 -0
  64. data/ios_tests/UICatalog.app/scene2.jpg +0 -0
  65. data/ios_tests/UICatalog.app/scene3.jpg +0 -0
  66. data/ios_tests/UICatalog.app/scene4.jpg +0 -0
  67. data/ios_tests/UICatalog.app/scene5.jpg +0 -0
  68. data/ios_tests/UICatalog.app/searchBarBackground.png +0 -0
  69. data/ios_tests/UICatalog.app/segment_check.png +0 -0
  70. data/ios_tests/UICatalog.app/segment_search.png +0 -0
  71. data/ios_tests/UICatalog.app/segment_tools.png +0 -0
  72. data/ios_tests/UICatalog.app/segmentedBackground.png +0 -0
  73. data/ios_tests/UICatalog.app/slider_ball.png +0 -0
  74. data/ios_tests/UICatalog.app/toolbarBackground.png +0 -0
  75. data/ios_tests/UICatalog.app/whiteButton.png +0 -0
  76. data/ios_tests/UICatalog.app/yellowslide.png +0 -0
  77. data/ios_tests/appium.txt +3 -0
  78. data/ios_tests/flaky.txt +1 -0
  79. data/ios_tests/lib/format.rb +25 -0
  80. data/ios_tests/lib/ios/specs/common/element/window.rb +15 -0
  81. data/ios_tests/lib/ios/specs/common/helper.rb +204 -0
  82. data/ios_tests/lib/ios/specs/common/patch.rb +50 -0
  83. data/ios_tests/lib/ios/specs/common/version.rb +17 -0
  84. data/ios_tests/lib/ios/specs/device/device.rb +82 -0
  85. data/ios_tests/lib/ios/specs/device/multi_touch.rb +12 -0
  86. data/ios_tests/lib/ios/specs/device/touch_actions.rb +15 -0
  87. data/ios_tests/lib/ios/specs/driver.rb +203 -0
  88. data/ios_tests/lib/ios/specs/ios/element/alert.rb +48 -0
  89. data/ios_tests/lib/ios/specs/ios/element/button.rb +58 -0
  90. data/ios_tests/lib/ios/specs/ios/element/generic.rb +35 -0
  91. data/ios_tests/lib/ios/specs/ios/element/text.rb +54 -0
  92. data/ios_tests/lib/ios/specs/ios/element/textfield.rb +123 -0
  93. data/ios_tests/lib/ios/specs/ios/helper.rb +27 -0
  94. data/ios_tests/lib/ios/specs/ios/patch.rb +30 -0
  95. data/ios_tests/lib/run.rb +106 -0
  96. data/ios_tests/readme.md +30 -0
  97. data/ios_tests/upload/sauce_storage.rb +64 -0
  98. data/ios_tests/upload/upload.rb +6 -0
  99. data/lib/appium_lib.rb +4 -14
  100. data/lib/appium_lib/android/dynamic.rb +30 -32
  101. data/lib/appium_lib/android/element/alert.rb +34 -33
  102. data/lib/appium_lib/android/element/button.rb +91 -0
  103. data/lib/appium_lib/android/element/generic.rb +51 -146
  104. data/lib/appium_lib/android/element/text.rb +54 -0
  105. data/lib/appium_lib/android/element/textfield.rb +46 -41
  106. data/lib/appium_lib/android/helper.rb +248 -417
  107. data/lib/appium_lib/android/mobile_methods.rb +17 -0
  108. data/lib/appium_lib/android/patch.rb +9 -8
  109. data/lib/appium_lib/awesome_print/ostruct.rb +33 -0
  110. data/lib/appium_lib/common/element/window.rb +9 -8
  111. data/lib/appium_lib/common/helper.rb +182 -243
  112. data/lib/appium_lib/common/patch.rb +65 -79
  113. data/lib/appium_lib/common/version.rb +2 -3
  114. data/lib/appium_lib/device/device.rb +339 -0
  115. data/lib/appium_lib/device/multi_touch.rb +94 -0
  116. data/lib/appium_lib/device/touch_actions.rb +142 -0
  117. data/lib/appium_lib/driver.rb +217 -306
  118. data/lib/appium_lib/ios/element/alert.rb +16 -92
  119. data/lib/appium_lib/ios/element/button.rb +55 -0
  120. data/lib/appium_lib/ios/element/generic.rb +27 -160
  121. data/lib/appium_lib/ios/element/text.rb +54 -0
  122. data/lib/appium_lib/ios/element/textfield.rb +78 -65
  123. data/lib/appium_lib/ios/helper.rb +300 -190
  124. data/lib/appium_lib/ios/mobile_methods.rb +17 -0
  125. data/lib/appium_lib/ios/patch.rb +55 -41
  126. data/lib/appium_lib/logger.rb +13 -0
  127. data/lib/appium_lib/rails/duplicable.rb +116 -0
  128. data/readme.md +6 -1
  129. data/release_notes.md +118 -0
  130. metadata +170 -12
  131. data/lib/appium_lib/common/element/button.rb +0 -83
  132. data/lib/appium_lib/common/element/text.rb +0 -61
@@ -0,0 +1,94 @@
1
+ module Appium
2
+
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
+ # ```ruby
11
+ # action_1 = TouchAction.new.press(x: 45, y: 100).wait(5).release
12
+ # action_2 = TouchAction.new.tap(element: el, x: 50, y:5, count: 3).release
13
+ #
14
+ # multi_touch_action = MultiTouch.new
15
+ # multi_touch_action.add action_1
16
+ # multi_touch_action.add action_2
17
+ # multi_touch_action.perform
18
+ class MultiTouch
19
+ class << self
20
+
21
+ # Convenience method for pinching the screen.
22
+ # Places two fingers at the edges of the screen and brings them together.
23
+ # @param percentage (int) The percent size by which to shrink the screen when pinched.
24
+ # @param auto_perform (boolean) Whether to perform the action immediately (default true)
25
+ #
26
+ # ```ruby
27
+ # action = pinch 75 #=> Pinch the screen from the top right and bottom left corners
28
+ # action.perform #=> to 25% of its size.
29
+ # ```
30
+ def pinch(percentage=25, auto_perform=true)
31
+ raise ArgumentError("Can't pinch to greater than screen size.") if percentage > 100
32
+
33
+ p = Float(percentage) / 100
34
+ i = 1 - p
35
+
36
+ top = TouchAction.new
37
+ top.swipe start_x: 1.0, start_y: 0.0, end_x: i, end_y: i, duration: 1
38
+
39
+ bottom = TouchAction.new
40
+ bottom.swipe(start_x: 0.0, start_y: 1.0, end_x: p, end_y: p, duration: 1)
41
+
42
+ pinch = MultiTouch.new
43
+ pinch.add top
44
+ pinch.add bottom
45
+ return pinch unless auto_perform
46
+ pinch.perform
47
+ end
48
+
49
+ # Convenience method for zooming the screen.
50
+ # Places two fingers at the edges of the screen and brings them together.
51
+ # @param percentage (int) The percent size by which to shrink the screen when pinched.
52
+ # @param auto_perform (boolean) Whether to perform the action immediately (default true)
53
+ #
54
+ # ```ruby
55
+ # action = zoom 200 #=> Zoom in the screen from the center until it doubles in size.
56
+ # action.perform
57
+ # ```
58
+ def zoom(percentage=200, auto_perform=true)
59
+ raise ArgumentError("Can't zoom to smaller then screen size.") if percentage < 100
60
+
61
+ p = 100 / Float(percentage)
62
+ i = 1 - p
63
+
64
+ top = TouchAction.new
65
+ top.swipe start_x: i, start_y: i, end_x: 1, end_y: 1, duration: 1
66
+
67
+ bottom = TouchAction.new
68
+ bottom.swipe start_x: p, start_y: p, end_x: 1, end_y: 1, duration: 1
69
+
70
+ zoom = MultiTouch.new
71
+ zoom.add top
72
+ zoom.add bottom
73
+ return zoom unless auto_perform
74
+ zoom.perform
75
+ end
76
+ end
77
+
78
+ # Create a new multi-action
79
+ def initialize
80
+ @actions = []
81
+ end
82
+
83
+ # Add a touch_action to be performed
84
+ # @param chain (TouchAction) The action to add to the chain
85
+ def add(chain)
86
+ @actions << chain.actions
87
+ end
88
+
89
+ # Ask Appium to perform the actions
90
+ def perform
91
+ $driver.multi_touch @actions
92
+ end
93
+ end # class MultiTouch
94
+ end # module Appium
@@ -0,0 +1,142 @@
1
+ module Appium
2
+
3
+ # Perform a series of gestures, one after another. Gestures are chained
4
+ # together and only performed when `perform()` is called.
5
+ #
6
+ # Each method returns the object itself, so calls can be chained.
7
+ #
8
+ # ```ruby
9
+ # action = TouchAction.new.press(x: 45, y: 100).wait(5).release
10
+ # action.perform
11
+ class TouchAction
12
+ ACTIONS = [:move_to, :press_for_duration, :press, :release, :tap, :wait, :perform]
13
+ COMPLEX_ACTIONS = [:swipe]
14
+
15
+ class << self
16
+ COMPLEX_ACTIONS.each do |action|
17
+ define_method(action) do |opts|
18
+ auto_perform = opts.delete(:auto_perform) {|k| true}
19
+ ta = TouchAction.new
20
+ ta.send(action, opts)
21
+ return ta unless auto_perform
22
+ ta.perform
23
+ end
24
+ end
25
+ end
26
+
27
+ attr_reader :actions
28
+
29
+ def initialize
30
+ @actions = []
31
+ end
32
+
33
+ # Move to the given co-ordinates.
34
+ # @option opts [integer] :x x co-ordinate to move to.
35
+ # @option opts [integer] :y y co-ordinate to move to.
36
+ # @option opts [WebDriver::Element] Element to scope this move within.
37
+ def move_to(opts)
38
+ opts = args_with_ele_ref(opts)
39
+ chain_method(:moveTo, opts)
40
+ end
41
+
42
+ # Press down for a specific duration.
43
+ # @param element [WebDriver::Element] the element to press.
44
+ # @param x [integer] x co-ordinate to press on.
45
+ # @param y [integer] y co-ordinate to press on.
46
+ # @param duration [integer] Number of seconds to press.
47
+ def press_for_duration(element, x, y, duration)
48
+ @actions << {element: element.ref, x: x, y: y, duration: duration}
49
+ chain_method(:longPress, args)
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
+ #
76
+ # @option opts [WebDriver::Element] :element (Optional) Element to restrict scope too.
77
+ # @option opts [integer] :x x co-ordinate to tap
78
+ # @option opts [integer] :y y co-ordinate to tap
79
+ # @option opts [integer] :fingers how many fingers to tap with (Default 1)
80
+ def tap(opts)
81
+ opts[:count] = opts.delete(:fingers) if opts[:fingers]
82
+ opts_with_defaults = {count: 1}.merge opts
83
+ chain_method(:tap, opts_with_defaults)
84
+ end
85
+
86
+ # Pause for a number of seconds before the next action
87
+ # @param seconds [integer] Number of seconds to pause for
88
+ def wait(seconds)
89
+ args = {ms: seconds}
90
+ chain_method(:wait, args)
91
+ end
92
+
93
+ # Convenience method to peform a swipe.
94
+ # @option opts [int] :start_x Where to start swiping, on the x axis. Default 0.
95
+ # @option opts [int] :start_y Where to start swiping, on the y axis. Default 0.
96
+ # @option opts [int] :end_x Where to end swiping, on the x axis. Default 0.
97
+ # @option opts [int] :end_y Where to end swiping, on the y axis. Default 0.
98
+ # @option opts [int] :duration How long the actual swipe takes to complete.
99
+ def swipe(opts)
100
+ start_x = opts.fetch :start_x, 0
101
+ start_y = opts.fetch :start_y, 0
102
+ end_x = opts.fetch :end_x, 0
103
+ end_y = opts.fetch :end_y, 0
104
+ duration = opts[:duration]
105
+
106
+ self.press x: start_x, y: start_y
107
+ self.wait(duration) if duration
108
+ self.move_to x: end_x, y: end_y
109
+ self.release
110
+ self
111
+ end
112
+
113
+ # Ask the driver to perform all actions in this action chain.
114
+ def perform
115
+ $driver.touch_actions @actions
116
+ self
117
+ end
118
+
119
+ # Does nothing, currently.
120
+ def cancel
121
+ @actions << {action: cancel}
122
+ $driver.touch_actions @actions
123
+ self
124
+ end
125
+
126
+ private
127
+
128
+ def chain_method(method, args=nil)
129
+ if args
130
+ @actions << {action: method, options: args}
131
+ else
132
+ @actions << {action: method}
133
+ end
134
+ self
135
+ end
136
+
137
+ def args_with_ele_ref(args)
138
+ args[:element] = args[:element].ref if args.has_key? :element
139
+ args
140
+ end
141
+ end # class TouchAction
142
+ end # module Appium
@@ -1,146 +1,152 @@
1
- # encoding: utf-8
2
- =begin
3
- Based on simple_test.rb
4
- https://github.com/appium/appium/blob/82995f47408530c80c3376f4e07a1f649d96ba22/sample-code/examples/ruby/simple_test.rb
5
- https://github.com/appium/appium/blob/c58eeb66f2d6fa3b9a89d188a2e657cca7cb300f/LICENSE
6
- =end
7
-
8
1
  require 'rubygems'
9
2
  require 'ap'
3
+ require 'selenium-webdriver'
4
+ require 'nokogiri'
5
+
6
+ # patch ap
7
+ require_relative 'awesome_print/ostruct'
8
+
9
+ # common
10
+ require_relative 'common/helper'
11
+ require_relative 'common/patch'
12
+ require_relative 'common/version'
13
+ require_relative 'common/element/window'
14
+
15
+ # ios
16
+ require_relative 'ios/helper'
17
+ require_relative 'ios/patch'
18
+
19
+ require_relative 'ios/element/alert'
20
+ require_relative 'ios/element/button'
21
+ require_relative 'ios/element/generic'
22
+ require_relative 'ios/element/textfield'
23
+ require_relative 'ios/element/text'
24
+ require_relative 'ios/mobile_methods'
25
+
26
+ # android
27
+ require_relative 'android/dynamic'
28
+ require_relative 'android/helper'
29
+ require_relative 'android/patch'
30
+ require_relative 'android/element/alert'
31
+ require_relative 'android/element/button'
32
+ require_relative 'android/element/generic'
33
+ require_relative 'android/element/textfield'
34
+ require_relative 'android/element/text'
35
+ require_relative 'android/mobile_methods'
36
+
37
+ # device methods
38
+ require_relative 'device/device'
39
+ require_relative 'device/touch_actions'
40
+ require_relative 'device/multi_touch'
10
41
 
11
- # Support OpenStruct in Awesome Print
12
- # /awesome_print/lib/awesome_print/formatter.rb
13
- # upstream issue: https://github.com/michaeldv/awesome_print/pull/36
14
- class AwesomePrint::Formatter
15
- remove_const :CORE if defined?(CORE)
16
- CORE = [ :array, :hash, :class, :file, :dir, :bigdecimal, :rational, :struct, :openstruct, :method, :unboundmethod ]
17
-
18
- def awesome_openstruct target
19
- awesome_hash target.marshal_dump
42
+ # Fix uninitialized constant Minitest (NameError)
43
+ module Minitest
44
+ # Fix superclass mismatch for class Spec
45
+ class Runnable
46
+ end
47
+ class Test < Runnable
48
+ end
49
+ class Spec < Test
20
50
  end
21
51
  end
22
52
 
23
- # Load appium.txt (toml format) into system ENV
24
- # the basedir of this file + appium.txt is what's used
25
- # @param opts [Hash] file: '/path/to/appium.txt', verbose: true
26
- # @return [Array<String>] the require files. nil if require doesn't exist
27
- def load_appium_txt opts
28
- raise 'opts must be a hash' unless opts.kind_of? Hash
29
- opts.each_pair { |k,v| opts[k.to_s.downcase.strip.intern] = v }
30
- opts = {} if opts.nil?
31
- file = opts.fetch :file, nil
32
- raise 'Must pass file' unless file
33
- verbose = opts.fetch :verbose, false
34
- # Check for env vars in .txt
35
- parent_dir = File.dirname file
36
- toml = File.expand_path File.join parent_dir, 'appium.txt'
37
- puts "appium.txt path: #{toml}" if verbose
38
- # @private
39
- def update data, *args
40
- args.each do |name|
41
- var = data[name]
42
- ENV[name] = var if var
43
- end
44
- end
53
+ module Appium
54
+ # Load appium.txt (toml format)
55
+ # the basedir of this file + appium.txt is what's used
56
+ #
57
+ # ```
58
+ # [caps]
59
+ # app = "path/to/app"
60
+ #
61
+ # [appium_lib]
62
+ # port = 8080
63
+ # ```
64
+ #
65
+ # :app is expanded
66
+ # :require is expanded
67
+ # all keys are converted to symbols
68
+ #
69
+ # @param opts [Hash] file: '/path/to/appium.txt', verbose: true
70
+ # @return [hash] the symbolized hash with updated :app and :require keys
71
+ def self.load_appium_txt opts={}
72
+ raise 'opts must be a hash' unless opts.kind_of? Hash
73
+ raise 'opts must not be empty' if opts.empty?
74
+
75
+ file = opts[:file]
76
+ raise 'Must pass file' unless file
77
+ verbose = opts.fetch :verbose, false
78
+
79
+ parent_dir = File.dirname file
80
+ toml = File.expand_path File.join parent_dir, 'appium.txt'
81
+ puts "appium.txt path: #{toml}" if verbose
45
82
 
46
- toml_exists = File.exists? toml
47
- puts "Exists? #{toml_exists}" if verbose
48
- data = nil
83
+ toml_exists = File.exists? toml
84
+ puts "Exists? #{toml_exists}" if verbose
49
85
 
50
- if toml_exists
86
+ raise "toml doesn't exist #{toml}" unless toml_exists
51
87
  require 'toml'
52
88
  puts "Loading #{toml}" if verbose
53
89
 
54
- # bash requires A="OK"
55
- # toml requires A = "OK"
56
- #
57
- # A="OK" => A = "OK"
58
90
  data = File.read toml
59
-
60
- data = data.split("\n").map do |line|
61
- line.sub /([^\s])\=/, "\\1 = "
62
- end.join "\n"
63
-
64
91
  data = TOML::Parser.new(data).parsed
92
+ # TOML creates string keys. must symbolize
93
+ data = Appium::symbolize_keys data
65
94
  ap data unless data.empty? if verbose
66
95
 
67
- update data, 'APP_PATH', 'APP_APK', 'APP_PACKAGE',
68
- 'APP_ACTIVITY', 'APP_WAIT_ACTIVITY',
69
- 'DEVICE'
70
-
71
- # ensure app path is resolved correctly from the context of the .txt file
72
- ENV['APP_PATH'] = Appium::Driver.absolute_app_path ENV['APP_PATH']
73
- end
74
-
75
- # return list of require files as an array
76
- # nil if require doesn't exist
77
- if data && data['require']
78
- r = data['require']
79
- r = r.kind_of?(Array) ? r : [ r ]
80
- # ensure files are absolute
81
- r.map! do |file|
82
- file = file.include?(File::Separator) ? file :
83
- File.join(parent_dir, file)
84
- file = File.expand_path file
85
-
86
- File.exists?(file) ? file : nil
96
+ if data && data[:caps] && data[:caps][:app]
97
+ data[:caps][:app] = Appium::Driver.absolute_app_path data[:caps][:app]
87
98
  end
88
- r.compact! # remove nils
89
-
90
- files = []
91
99
 
92
- # now expand dirs
93
- r.each do |item|
94
- unless File.directory? item
95
- # save file
96
- files << item
97
- next # only look inside folders
100
+ # return list of require files as an array
101
+ # nil if require doesn't exist
102
+ if data && data[:appium_lib] && data[:appium_lib][:require]
103
+ r = data[:appium_lib][:require]
104
+ r = r.kind_of?(Array) ? r : [r]
105
+ # ensure files are absolute
106
+ r.map! do |file|
107
+ file = File.exists?(file) ? file :
108
+ File.join(parent_dir, file)
109
+ file = File.expand_path file
110
+
111
+ File.exists?(file) ? file : nil
98
112
  end
99
- Dir.glob(File.join(item, '**/*.rb')) do |file|
100
- # do not add folders to the file list
101
- files << File.expand_path(file) unless File.directory? file
113
+ r.compact! # remove nils
114
+
115
+ files = []
116
+
117
+ # now expand dirs
118
+ r.each do |item|
119
+ unless File.directory? item
120
+ # save file
121
+ files << item
122
+ next # only look inside folders
123
+ end
124
+ Dir.glob(File.expand_path(File.join(item, '**', '*.rb'))) do |file|
125
+ # do not add folders to the file list
126
+ files << File.expand_path(file) unless File.directory? file
127
+ end
102
128
  end
129
+
130
+ # Must not sort files. File order is specified in appium.txt
131
+ data[:appium_lib][:require] = files
103
132
  end
104
133
 
105
- files
134
+ data
106
135
  end
107
- end
108
-
109
- # Fix uninitialized constant Minitest (NameError)
110
- module Minitest
111
- # Fix superclass mismatch for class Spec
112
- class Runnable; end
113
- class Test < Runnable; end
114
- class Spec < Test; end
115
- end
116
136
 
117
- module Appium
118
- add_to_path __FILE__
119
-
120
- require 'selenium-webdriver'
121
-
122
- # common
123
- require_relative 'common/helper'
124
- require_relative 'common/patch'
125
- require_relative 'common/version'
126
- require_relative 'common/element/button'
127
- require_relative 'common/element/text'
128
- require_relative 'common/element/window'
129
-
130
- # ios
131
- require_relative 'ios/helper'
132
- require_relative 'ios/patch'
133
- require_relative 'ios/element/alert'
134
- require_relative 'ios/element/generic'
135
- require_relative 'ios/element/textfield'
136
-
137
- # android
138
- require_relative 'android/dynamic'
139
- require_relative 'android/helper'
140
- require_relative 'android/patch'
141
- require_relative 'android/element/alert'
142
- require_relative 'android/element/generic'
143
- require_relative 'android/element/textfield'
137
+ # convert all keys (including nested) to symbols
138
+ #
139
+ # based on deep_symbolize_keys & deep_transform_keys from rails
140
+ # https://github.com/rails/docrails/blob/a3b1105ada3da64acfa3843b164b14b734456a50/activesupport/lib/active_support/core_ext/hash/keys.rb#L84
141
+ def self.symbolize_keys hash
142
+ raise 'symbolize_keys requires a hash' unless hash.is_a? Hash
143
+ result = {}
144
+ hash.each do |key, value|
145
+ key = key.to_sym rescue key
146
+ result[key] = value.is_a?(Hash) ? symbolize_keys(value) : value
147
+ end
148
+ result
149
+ end
144
150
 
145
151
  def self.promote_singleton_appium_methods main_module
146
152
  raise 'Driver is nil' if $driver.nil?
@@ -171,7 +177,6 @@ module Appium
171
177
  # ```ruby
172
178
  # Appium.promote_appium_methods Object
173
179
  # ```
174
-
175
180
  def self.promote_appium_methods class_array
176
181
  raise 'Driver is nil' if $driver.nil?
177
182
  # Wrap single class into an array
@@ -185,9 +190,9 @@ module Appium
185
190
  # Prefer existing method.
186
191
  # super will invoke method missing on driver
187
192
  super(*args, &block)
188
- # minitest also defines a name method,
189
- # so rescue argument error
190
- # and call the name method on $driver
193
+ # minitest also defines a name method,
194
+ # so rescue argument error
195
+ # and call the name method on $driver
191
196
  rescue NoMethodError, ArgumentError
192
197
  $driver.send m, *args, &block if $driver.respond_to?(m)
193
198
  end
@@ -201,37 +206,26 @@ module Appium
201
206
  class Driver
202
207
  @@loaded = false
203
208
 
204
- attr_reader :default_wait, :app_path, :app_name, :device,
205
- :app_package, :app_activity, :app_wait_activity,
206
- :sauce_username, :sauce_access_key, :port, :debug,
207
- :export_session, :device_cap, :compress_xml, :custom_url
209
+ # attr readers are promoted to global scope. To avoid clobbering, they're
210
+ # made available via the driver_attributes method
208
211
 
209
212
  # The amount to sleep in seconds before every webdriver http call.
210
213
  attr_accessor :global_webdriver_http_sleep
211
- # Creates a new driver.
212
- # :device is :android, :ios, or :selendroid
214
+
215
+ # Creates a new driver
213
216
  #
214
217
  # ```ruby
215
- # # Options include:
216
- # :app_path, :app_name, :app_package, :app_activity,
217
- # :app_wait_activity, :sauce_username, :sauce_access_key,
218
- # :port, :os, :debug
219
- #
220
218
  # require 'rubygems'
221
219
  # require 'appium_lib'
222
220
  #
221
+ # # platformName takes a string or a symbol.
222
+ #
223
223
  # # Start iOS driver
224
- # app = { device: :ios, app_path: '/path/to/MyiOS.app'}
225
- # Appium::Driver.new(app).start_driver
224
+ # opts = { caps: { platformName: :ios, app: '/path/to/MyiOS.app' } }
225
+ # Appium::Driver.new(opts).start_driver
226
226
  #
227
227
  # # Start Android driver
228
- # apk = { device: :android
229
- # app_path: '/path/to/the.apk',
230
- # app_package: 'com.example.pkg',
231
- # app_activity: 'act.Start',
232
- # app_wait_activity: 'act.Start'
233
- # }
234
- #
228
+ # opts = { caps: { platformName: :android, app: '/path/to/my.apk' } }
235
229
  # Appium::Driver.new(apk).start_driver
236
230
  # ```
237
231
  #
@@ -240,75 +234,38 @@ module Appium
240
234
  def initialize opts={}
241
235
  # quit last driver
242
236
  $driver.driver_quit if $driver
243
- opts = {} if opts.nil?
244
- tmp_opts = {}
245
-
246
- # convert to downcased symbols
247
- opts.each_pair { |k,v| tmp_opts[k.to_s.downcase.strip.intern] = v }
248
- opts = tmp_opts
249
-
250
- @raw_capabilities = opts.fetch(:raw, {})
237
+ raise 'opts must be a hash' unless opts.kind_of? Hash
251
238
 
252
- @custom_url = opts.fetch :server_url, false
239
+ opts = Appium::symbolize_keys opts
253
240
 
254
- @compress_xml = opts[:compress_xml] ? true : false
241
+ # default to {} to prevent nil.fetch and other nil errors
242
+ @caps = opts[:caps] || {}
243
+ appium_lib_opts = opts[:appium_lib] || {}
255
244
 
256
- @export_session = opts.fetch :export_session, false
257
-
258
- @default_wait = opts.fetch :wait, 30
259
- @last_waits = [@default_wait]
245
+ # appium_lib specific values
246
+ @custom_url = appium_lib_opts.fetch :server_url, false
247
+ @export_session = appium_lib_opts.fetch :export_session, false
248
+ @default_wait = appium_lib_opts.fetch :wait, 30
249
+ @last_waits = [@default_wait]
250
+ @sauce_username = appium_lib_opts.fetch :sauce_username, ENV['SAUCE_USERNAME']
251
+ @sauce_access_key = appium_lib_opts.fetch :sauce_access_key, ENV['SAUCE_ACCESS_KEY']
252
+ @port = appium_lib_opts.fetch :port, 4723
260
253
 
261
254
  # Path to the .apk, .app or .app.zip.
262
255
  # The path can be local or remote for Sauce.
263
- @app_path = opts.fetch :app_path, ENV['APP_PATH']
264
- raise 'APP_PATH must be set.' if @app_path.nil?
265
-
266
- # The name to use for the test run on Sauce.
267
- @app_name = opts.fetch :app_name, ENV['APP_NAME']
268
-
269
- # Android app package
270
- @app_package = opts.fetch :app_package, ENV['APP_PACKAGE']
271
-
272
- # Android app starting activity.
273
- @app_activity = opts.fetch :app_activity, ENV['APP_ACTIVITY']
274
-
275
- # Android app waiting activity
276
- @app_wait_activity = opts.fetch :app_wait_activity, ENV['APP_WAIT_ACTIVITY']
277
-
278
- @android_coverage = opts.fetch :android_coverage, ENV['ANDROID_COVERAGE']
279
- # init to empty hash because it's merged later as an optional desired cap.
280
- @android_coverage = @android_coverage ? { androidCoverage: @android_coverage} : {}
281
-
282
- # Sauce Username
283
- @sauce_username = opts.fetch :sauce_username, ENV['SAUCE_USERNAME']
284
-
285
- # Sauce Key
286
- @sauce_access_key = opts.fetch :sauce_access_key, ENV['SAUCE_ACCESS_KEY']
287
-
288
- @port = opts.fetch :port, ENV['PORT'] || 4723
289
-
290
- # 'iPhone Simulator'
291
- # 'iPad Simulator'
292
- # 'Android'
293
- # 'Selendroid'
294
- #
295
- # :ios, :android, :selendroid
296
- @device = opts.fetch :device, ENV['DEVICE']
297
- raise 'Device must be set' unless @device
298
-
299
- @device_type = opts.fetch :device_type, 'tablet'
300
- @device_orientation = opts.fetch :device_orientation, 'portrait'
301
-
302
- @full_reset = opts.fetch :full_reset, true
303
- @no_reset = opts.fetch :no_reset, false
256
+ unless !@caps || @caps[:app].nil? || @caps[:app].empty?
257
+ @caps[:app] = self.class.absolute_app_path @caps[:app]
258
+ end
304
259
 
305
- # no_reset/full_reset are mutually exclusive
306
- @no_reset = false if @full_reset
307
- @full_reset = false if @no_reset
260
+ # https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile
261
+ @device = @caps[:platformName]
262
+ @device = @device.is_a?(Symbol) ? @device : @device.downcase.strip.intern if @device
263
+ raise "platformName must be set. Not found in options: #{opts}" unless @device
264
+ raise 'platformName must be Android or iOS' unless [:android, :ios].include?(@device)
308
265
 
309
266
  # load common methods
310
267
  extend Appium::Common
311
- if @device.downcase == 'android'
268
+ if device_is_android?
312
269
  # load Android specific methods
313
270
  extend Appium::Android
314
271
  else
@@ -321,7 +278,7 @@ module Appium
321
278
 
322
279
  # enable debug patch
323
280
  # !!'constant' == true
324
- @debug = opts.fetch :debug, !!defined?(Pry)
281
+ @debug = appium_lib_opts.fetch :debug, !!defined?(Pry)
325
282
  puts "Debug is: #{@debug}"
326
283
  if @debug
327
284
  ap opts unless opts.empty?
@@ -329,6 +286,7 @@ module Appium
329
286
  patch_webdriver_bridge
330
287
  end
331
288
 
289
+
332
290
  # Save global reference to last created Appium driver for top level methods.
333
291
  $driver = self
334
292
 
@@ -336,78 +294,58 @@ module Appium
336
294
  # Subsequent drivers do not trigger promotion.
337
295
  unless @@loaded
338
296
  @@loaded = true
297
+ # load device methods exactly once
298
+ extend Appium::Device
299
+
339
300
  # Promote only on Minitest::Spec (minitest 5) by default
340
301
  Appium.promote_appium_methods ::Minitest::Spec
341
302
  end
342
303
 
343
304
  self # return newly created driver
344
- end # def initialize
345
-
346
- # Returns the status payload
347
- #
348
- # ```ruby
349
- # {"status"=>0,
350
- # "value"=>
351
- # {"build"=>
352
- # {"version"=>"0.8.2",
353
- # "revision"=>"f2a2bc3782e4b0370d97a097d7e04913cf008995"}},
354
- # "sessionId"=>"8f4b34a7-a9a9-4ac5-b125-36258143446a"}
355
- # ```
356
- #
357
- # Discover the Appium rev running on the server.
358
- #
359
- # `status["value"]["build"]["revision"]`
360
- # `f2a2bc3782e4b0370d97a097d7e04913cf008995`
361
- #
362
- # @return [JSON]
363
- def status
364
- driver.status.payload
365
- end
366
-
367
- # Returns the server's version string
368
- # @return [String]
369
- def server_version
370
- status['value']['build']['version']
371
305
  end
372
306
 
373
- # @private
374
- # WebDriver capabilities. Must be valid for Sauce to work.
375
- # https://github.com/jlipps/appium/blob/master/app/android.js
376
- def android_capabilities
377
- {
378
- compressXml: @compress_xml,
379
- platform: 'Linux',
380
- platformName: @device,
381
- fullReset: @full_reset,
382
- noReset: @no_reset,
383
- :'device-type' => @device_type,
384
- :'device-orientation' => @device_orientation,
385
- name: @app_name || 'Ruby Console Android Appium',
386
- :'app-package' => @app_package,
387
- :'app-activity' => @app_activity,
388
- :'app-wait-activity' => @app_wait_activity || @app_activity,
389
- }.merge(@android_coverage).merge(@raw_capabilities)
307
+ # Returns a hash of the driver attributes
308
+ def driver_attributes
309
+ attributes = { caps: @caps,
310
+ custom_url: @custom_url,
311
+ export_session: @export_session,
312
+ default_wait: @default_wait,
313
+ last_waits: @last_waits,
314
+ sauce_username: @sauce_username,
315
+ sauce_access_key: @sauce_access_key,
316
+ port: @port,
317
+ device: @device,
318
+ debug: @debug,
319
+ }
320
+
321
+ # Return duplicates so attributes are immutable
322
+ attributes.each do |key, value|
323
+ attributes[key] = value.duplicable? ? value.dup : value
324
+ end
325
+ attributes
390
326
  end
391
327
 
392
- # @private
393
- # WebDriver capabilities. Must be valid for Sauce to work.
394
- def ios_capabilities
395
- {
396
- platform: 'OS X 10.9',
397
- platformName: @device,
398
- name: @app_name || 'Ruby Console iOS Appium',
399
- :'device-orientation' => @device_orientation
400
- }.merge(@raw_capabilities)
328
+ def device_is_android?
329
+ @device == :android
401
330
  end
402
331
 
403
- # @private
404
- def capabilities
405
- caps = @device.downcase === 'android' ? android_capabilities : ios_capabilities
406
- caps[:app] = self.class.absolute_app_path(@app_path) unless @app_path.nil? || @app_path.empty?
407
- caps
332
+ # Returns the server's version info
333
+ #
334
+ # ```ruby
335
+ # {
336
+ # "build" => {
337
+ # "version" => "0.18.1",
338
+ # "revision" => "d242ebcfd92046a974347ccc3a28f0e898595198"
339
+ # }
340
+ # }
341
+ # ```
342
+ #
343
+ # @return [Hash]
344
+ def appium_server_version
345
+ driver.remote_status
408
346
  end
409
347
 
410
- # Converts environment variable APP_PATH to an absolute path.
348
+ # Converts app_path to an absolute path.
411
349
  # @return [String] APP_PATH as an absolute path
412
350
  def self.absolute_app_path app_path
413
351
  raise 'APP_PATH not set!' if app_path.nil? || app_path.empty?
@@ -431,7 +369,7 @@ module Appium
431
369
  app_path
432
370
  end
433
371
 
434
- # Get the server url for sauce or local based on env vars.
372
+ # Get the server url
435
373
  # @return [String] the server url
436
374
  def server_url
437
375
  return @custom_url if @custom_url
@@ -469,40 +407,32 @@ module Appium
469
407
  # Quits the driver
470
408
  # @return [void]
471
409
  def driver_quit
472
- # rescue NoSuchDriverError
473
- begin; @driver.quit unless @driver.nil?; rescue; end
410
+ # rescue NoSuchDriverError or nil driver
411
+ @driver.quit rescue nil
474
412
  end
475
413
 
476
414
  # Creates a new global driver and quits the old one if it exists.
477
415
  #
478
416
  # @return [Selenium::WebDriver] the new global driver
479
417
  def start_driver
480
- @client = @client || Selenium::WebDriver::Remote::Http::Default.new
418
+ @client = @client || Selenium::WebDriver::Remote::Http::Default.new
481
419
  @client.timeout = 999999
482
420
 
483
421
  begin
484
- @driver = Selenium::WebDriver.for :remote, http_client: @client, desired_capabilities: capabilities, url: server_url
485
- # Load touch methods. Required for Selendroid.
422
+ @driver = Selenium::WebDriver.for :remote, http_client: @client, desired_capabilities: @caps, url: server_url
423
+ # Load touch methods.
486
424
  @driver.extend Selenium::WebDriver::DriverExtensions::HasTouchScreen
487
425
 
488
426
  # export session
489
427
  if @export_session
490
- begin
491
- File.open('/tmp/appium_lib_session', 'w') do |f|
492
- f.puts @driver.session_id
493
- end
494
- rescue
495
- end
428
+ File.open('/tmp/appium_lib_session', 'w') do |f|
429
+ f.puts @driver.session_id
430
+ end rescue nil
496
431
  end
497
432
  rescue Errno::ECONNREFUSED
498
433
  raise 'ERROR: Unable to connect to Appium. Is the server running?'
499
434
  end
500
435
 
501
- # Set timeout to a large number so that Appium doesn't quit
502
- # when no commands are entered after 60 seconds.
503
- # broken on selendroid: https://github.com/appium/appium/issues/513
504
- mobile :setCommandTimeout, timeout: 9999 unless @device == 'Selendroid'
505
-
506
436
  # Set implicit wait by default unless we're using Pry.
507
437
  @driver.manage.timeouts.implicit_wait = @default_wait unless defined? Pry
508
438
 
@@ -511,8 +441,8 @@ module Appium
511
441
 
512
442
  # Set implicit wait and default_wait to zero.
513
443
  def no_wait
514
- @last_waits = [@default_wait, 0]
515
- @default_wait = 0
444
+ @last_waits = [@default_wait, 0]
445
+ @default_wait = 0
516
446
  @driver.manage.timeouts.implicit_wait = 0
517
447
  end
518
448
 
@@ -537,7 +467,7 @@ module Appium
537
467
  else
538
468
  @default_wait = timeout
539
469
  # puts "last waits before: #{@last_waits}"
540
- @last_waits = [@last_waits.last, @default_wait]
470
+ @last_waits = [@last_waits.last, @default_wait]
541
471
  # puts "last waits after: #{@last_waits}"
542
472
  end
543
473
 
@@ -569,7 +499,7 @@ module Appium
569
499
  # which then gets converted to a 1 second wait.
570
500
  @driver.manage.timeouts.implicit_wait = pre_check
571
501
  # the element exists unless an error is raised.
572
- exists = true
502
+ exists = true
573
503
 
574
504
  begin
575
505
  search_block.call # search for element
@@ -591,25 +521,6 @@ module Appium
591
521
  @driver.execute_script script, *args
592
522
  end
593
523
 
594
- # Helper method for mobile gestures
595
- #
596
- # https://github.com/appium/appium/wiki/Automating-mobile-gestures
597
- #
598
- # driver.execute_script 'mobile: swipe', endX: 100, endY: 100, duration: 0.01
599
- #
600
- # becomes
601
- #
602
- # mobile :swipe, endX: 100, endY: 100, duration: 0.01
603
- # @param method [String, Symbol] the method to execute
604
- # @param args [*args] the args to pass to the method
605
- # @return [Object]
606
- def mobile method, *args
607
- raise 'Method must not be nil' if method.nil?
608
- raise 'Method must have .to_s' unless method.respond_to? :to_s
609
-
610
- @driver.execute_script "mobile: #{method.to_s}", *args
611
- end
612
-
613
524
  # Calls @driver.find_elements
614
525
  #
615
526
  # @param args [*args] the args to use
@@ -633,10 +544,10 @@ module Appium
633
544
  driver_quit
634
545
  exit # exit pry
635
546
  end
636
- end # end class Driver
637
- end # end module Appium
547
+ end # class Driver
548
+ end # module Appium
638
549
 
639
550
  # Paging in Pry is annoying :q required to exit.
640
551
  # With pager disabled, the output is similar to IRB
641
552
  # Only set if Pry is defined.
642
- Pry.config.pager = false if defined?(Pry)
553
+ Pry.config.pager = false if defined?(Pry)