appium_lib 0.24.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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)