calabash 1.2.1 → 1.9.9.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +39 -0
  3. data/LICENSE +204 -21
  4. data/README.md +36 -6
  5. data/VERSIONING.md +16 -0
  6. data/bin/calabash +95 -0
  7. data/lib/calabash.rb +185 -1
  8. data/lib/calabash/android.rb +64 -0
  9. data/lib/calabash/android/adb.rb +277 -0
  10. data/lib/calabash/android/application.rb +110 -0
  11. data/lib/calabash/android/build.rb +12 -0
  12. data/lib/calabash/android/build/application.rb +13 -0
  13. data/lib/calabash/android/build/build_error.rb +11 -0
  14. data/lib/calabash/android/build/builder.rb +119 -0
  15. data/lib/calabash/android/build/java_keystore.rb +177 -0
  16. data/lib/calabash/android/build/resigner.rb +56 -0
  17. data/lib/calabash/android/build/test_server.rb +27 -0
  18. data/lib/calabash/android/console_helpers.rb +44 -0
  19. data/lib/calabash/android/cucumber.rb +3 -0
  20. data/lib/calabash/android/device.rb +965 -0
  21. data/lib/calabash/android/environment.rb +470 -0
  22. data/lib/calabash/android/gestures.rb +369 -0
  23. data/lib/calabash/android/interactions.rb +45 -0
  24. data/lib/calabash/android/lib/.irbrc +55 -0
  25. data/lib/calabash/android/lib/AndroidManifest.xml +51 -0
  26. data/lib/calabash/android/lib/TestServer.apk +0 -0
  27. data/lib/calabash/android/lib/calmd5/arm64-v8a/calmd5 +0 -0
  28. data/lib/calabash/android/lib/calmd5/arm64-v8a/calmd5-pie +0 -0
  29. data/lib/calabash/android/lib/calmd5/armeabi-v7a/calmd5 +0 -0
  30. data/lib/calabash/android/lib/calmd5/armeabi-v7a/calmd5-pie +0 -0
  31. data/lib/calabash/android/lib/calmd5/armeabi/calmd5 +0 -0
  32. data/lib/calabash/android/lib/calmd5/armeabi/calmd5-pie +0 -0
  33. data/lib/calabash/android/lib/calmd5/mips/calmd5 +0 -0
  34. data/lib/calabash/android/lib/calmd5/mips/calmd5-pie +0 -0
  35. data/lib/calabash/android/lib/calmd5/mips64/calmd5 +0 -0
  36. data/lib/calabash/android/lib/calmd5/mips64/calmd5-pie +0 -0
  37. data/lib/calabash/android/lib/calmd5/x86/calmd5 +0 -0
  38. data/lib/calabash/android/lib/calmd5/x86/calmd5-pie +0 -0
  39. data/lib/calabash/android/lib/calmd5/x86_64/calmd5 +0 -0
  40. data/lib/calabash/android/lib/calmd5/x86_64/calmd5-pie +0 -0
  41. data/lib/calabash/android/lib/screenshot_taker.jar +0 -0
  42. data/lib/calabash/android/life_cycle.rb +37 -0
  43. data/lib/calabash/android/orientation.rb +30 -0
  44. data/lib/calabash/android/physical_buttons.rb +39 -0
  45. data/lib/calabash/android/screenshot.rb +9 -0
  46. data/lib/calabash/android/scroll.rb +5 -0
  47. data/lib/calabash/android/server.rb +10 -0
  48. data/lib/calabash/android/text.rb +54 -0
  49. data/lib/calabash/application.rb +74 -0
  50. data/lib/calabash/cli.rb +12 -0
  51. data/lib/calabash/cli/build.rb +33 -0
  52. data/lib/calabash/cli/console.rb +90 -0
  53. data/lib/calabash/cli/generate.rb +110 -0
  54. data/lib/calabash/cli/helpers.rb +130 -0
  55. data/lib/calabash/cli/resign.rb +33 -0
  56. data/lib/calabash/cli/run.rb +99 -0
  57. data/lib/calabash/cli/setup_keystore.rb +39 -0
  58. data/lib/calabash/color.rb +32 -0
  59. data/lib/calabash/console_helpers.rb +90 -0
  60. data/lib/calabash/defaults.rb +56 -0
  61. data/lib/calabash/device.rb +401 -0
  62. data/lib/calabash/environment.rb +75 -0
  63. data/lib/calabash/gestures.rb +384 -0
  64. data/lib/calabash/http.rb +8 -0
  65. data/lib/calabash/http/error.rb +15 -0
  66. data/lib/calabash/http/request.rb +42 -0
  67. data/lib/calabash/http/retriable_client.rb +156 -0
  68. data/lib/calabash/interactions.rb +105 -0
  69. data/lib/calabash/ios.rb +37 -0
  70. data/lib/calabash/ios/application.rb +119 -0
  71. data/lib/calabash/ios/conditions.rb +79 -0
  72. data/lib/calabash/ios/console_helpers.rb +72 -0
  73. data/lib/calabash/ios/device.rb +24 -0
  74. data/lib/calabash/ios/device/device_implementation.rb +779 -0
  75. data/lib/calabash/ios/device/gestures_mixin.rb +167 -0
  76. data/lib/calabash/ios/device/keyboard_mixin.rb +133 -0
  77. data/lib/calabash/ios/device/physical_device_mixin.rb +266 -0
  78. data/lib/calabash/ios/device/rotation_mixin.rb +124 -0
  79. data/lib/calabash/ios/device/routes/backdoor_route_mixin.rb +86 -0
  80. data/lib/calabash/ios/device/routes/condition_route_mixin.rb +62 -0
  81. data/lib/calabash/ios/device/routes/error.rb +8 -0
  82. data/lib/calabash/ios/device/routes/handle_route_mixin.rb +102 -0
  83. data/lib/calabash/ios/device/routes/map_route_mixin.rb +38 -0
  84. data/lib/calabash/ios/device/routes/playback_route_mixin.rb +70 -0
  85. data/lib/calabash/ios/device/routes/response_parser.rb +48 -0
  86. data/lib/calabash/ios/device/routes/uia_route_mixin.rb +238 -0
  87. data/lib/calabash/ios/device/runtime_attributes.rb +184 -0
  88. data/lib/calabash/ios/device/status_bar_mixin.rb +17 -0
  89. data/lib/calabash/ios/device/text_mixin.rb +19 -0
  90. data/lib/calabash/ios/device/uia_keyboard_mixin.rb +188 -0
  91. data/lib/calabash/ios/device/uia_mixin.rb +12 -0
  92. data/lib/calabash/ios/environment.rb +41 -0
  93. data/lib/calabash/ios/interactions.rb +10 -0
  94. data/lib/calabash/ios/lib/.irbrc +55 -0
  95. data/lib/calabash/ios/lib/recordings/rotate_left_home_down_ipad.base64 +2 -0
  96. data/lib/calabash/ios/lib/recordings/rotate_left_home_down_iphone.base64 +2 -0
  97. data/lib/calabash/ios/lib/recordings/rotate_left_home_left_ipad.base64 +2 -0
  98. data/lib/calabash/ios/lib/recordings/rotate_left_home_left_iphone.base64 +2 -0
  99. data/lib/calabash/ios/lib/recordings/rotate_left_home_right_ipad.base64 +2 -0
  100. data/lib/calabash/ios/lib/recordings/rotate_left_home_right_iphone.base64 +2 -0
  101. data/lib/calabash/ios/lib/recordings/rotate_left_home_up_ipad.base64 +2 -0
  102. data/lib/calabash/ios/lib/recordings/rotate_left_home_up_iphone.base64 +2 -0
  103. data/lib/calabash/ios/lib/recordings/rotate_right_home_down_ipad.base64 +2 -0
  104. data/lib/calabash/ios/lib/recordings/rotate_right_home_down_iphone.base64 +2 -0
  105. data/lib/calabash/ios/lib/recordings/rotate_right_home_left_ipad.base64 +2 -0
  106. data/lib/calabash/ios/lib/recordings/rotate_right_home_left_iphone.base64 +2 -0
  107. data/lib/calabash/ios/lib/recordings/rotate_right_home_right_ipad.base64 +2 -0
  108. data/lib/calabash/ios/lib/recordings/rotate_right_home_right_iphone.base64 +2 -0
  109. data/lib/calabash/ios/lib/recordings/rotate_right_home_up_ipad.base64 +2 -0
  110. data/lib/calabash/ios/lib/recordings/rotate_right_home_up_iphone.base64 +2 -0
  111. data/lib/calabash/ios/orientation.rb +117 -0
  112. data/lib/calabash/ios/scroll.rb +504 -0
  113. data/lib/calabash/ios/server.rb +73 -0
  114. data/lib/calabash/ios/text.rb +248 -0
  115. data/lib/calabash/ios/uia.rb +24 -0
  116. data/lib/calabash/lib/skeleton/config/cucumber.yml +6 -0
  117. data/lib/calabash/lib/skeleton/features/sample.feature +5 -0
  118. data/lib/calabash/lib/skeleton/features/step_definitions/calabash_steps.rb +29 -0
  119. data/lib/calabash/lib/skeleton/features/support/env.rb +54 -0
  120. data/lib/calabash/lib/skeleton/features/support/hooks.rb +83 -0
  121. data/lib/calabash/life_cycle.rb +111 -0
  122. data/lib/calabash/location.rb +51 -0
  123. data/lib/calabash/logger.rb +87 -0
  124. data/lib/calabash/orientation.rb +84 -0
  125. data/lib/calabash/page.rb +35 -0
  126. data/lib/calabash/patch.rb +14 -0
  127. data/lib/calabash/patch/array.rb +16 -0
  128. data/lib/calabash/patch/run_loop.rb +90 -0
  129. data/lib/calabash/query.rb +160 -0
  130. data/lib/calabash/query_result.rb +85 -0
  131. data/lib/calabash/screenshot.rb +89 -0
  132. data/lib/calabash/server.rb +16 -0
  133. data/lib/calabash/text.rb +76 -0
  134. data/lib/calabash/utility.rb +58 -0
  135. data/lib/calabash/version.rb +3 -1
  136. data/lib/calabash/wait.rb +474 -0
  137. metadata +462 -24
@@ -0,0 +1,79 @@
1
+ module Calabash
2
+ module IOS
3
+ module Conditions
4
+
5
+ # Waits for all elements to stop animating.
6
+ #
7
+ # @param [Numeric] timeout How long to wait for the animations to stop.
8
+ # @return [nil] when the condition is satisfied
9
+ # @raise [Calabash::Cucumber::WaitHelpers::WaitError] when the timeout is exceeded
10
+ def wait_for_animations(timeout=2)
11
+ message = "Timed out after #{timeout} seconds wait for all views to stop animating."
12
+
13
+ wait_for_condition(CALABASH_CONDITIONS[:none_animating],
14
+ timeout,
15
+ message)
16
+
17
+ end
18
+
19
+ # Waits for all elements matching `query` to stop animating.
20
+ #
21
+ # @param [String] query The view to wait for.
22
+ # @param [Numeric] timeout How long to wait for the animations to stop.
23
+ # @return [nil] When the condition is satisfied.
24
+ # @raise [Calabash::Wait::TimeoutError] When the timeout is exceeded.
25
+ def wait_for_animations_in(query, timeout=2)
26
+
27
+ if query.nil? || query == ''
28
+ raise ArgumentError, 'Query argument must not be nil or the empty string'
29
+ end
30
+
31
+ message = "Timed out after #{timeout} waiting for views matching '#{query}' to stop animating."
32
+
33
+ wait_for_condition(CALABASH_CONDITIONS[:none_animating],
34
+ timeout,
35
+ message,
36
+ query)
37
+ end
38
+
39
+ # Waits for the status-bar network indicator to stop animating
40
+ # (network activity done).
41
+ #
42
+ # param [Numeric] timeout How long to wait for the animations to stop.
43
+ # @return [nil] When the condition is satisfied.
44
+ # @raise [Calabash::Wait::TimeoutError] When the timeout is exceeded.
45
+ def wait_for_no_network_indicator(timeout=15)
46
+ message = "Timed out after #{timeout} waiting for the network indicator to stop animating."
47
+
48
+ wait_for_condition(CALABASH_CONDITIONS[:no_network_indicator],
49
+ timeout,
50
+ message)
51
+ end
52
+
53
+ private
54
+
55
+ CALABASH_CONDITIONS = {:none_animating => 'NONE_ANIMATING',
56
+ :no_network_indicator => 'NO_NETWORK_INDICATOR'}
57
+
58
+ # @!visibility private
59
+ #
60
+ # Waits for condition.
61
+ #
62
+ # @param [String] condition The condition to wait for.
63
+ # @param [Numeric] timeout How long to wait.
64
+ # @param [String] timeout_message The message used when raising an error if
65
+ # the condition is not satisfied.
66
+ # @param [String] query Views matching this query will have the condition
67
+ # applied to them. Will be ignored for some conditions e.g.
68
+ # NO_NETWORK_INDICATOR
69
+ # @return [nil] When the condition is satisfied.
70
+ # @raise [Calabash::Wait::TimeoutError] When the timeout is exceeded.
71
+ def wait_for_condition(condition, timeout, timeout_message, query='*')
72
+ unless Device.default.condition_route(condition, timeout, query)
73
+ raise Calabash::Wait::TimeoutError, timeout_message
74
+ end
75
+ true
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,72 @@
1
+ module Calabash
2
+ module ConsoleHelpers
3
+ def self.render(data, indentation)
4
+ if visible?(data)
5
+ type = data['type']
6
+
7
+ str_type = if data['type'] == 'dom'
8
+ "#{Color.yellow("[")}#{type}:#{Color.yellow("#{data['nodeName']}]")} "
9
+ else
10
+ Color.yellow("[#{type}] ")
11
+ end
12
+
13
+ str_id = data['id'] ? "[id:#{Color.blue(data['id'])}] " : ''
14
+ str_label = data['label'] ? "[label:#{Color.green(data['label'])}] " : ''
15
+ str_text = data['value'] ? "[text:#{Color.magenta(data['value'])}] " : ''
16
+ output("#{str_type}#{str_id}#{str_label}#{str_text}", indentation)
17
+ output("\n", indentation)
18
+ end
19
+ end
20
+
21
+ def self.visible?(data)
22
+ (data['visible'] == 1) || data['children'].map{|child| visible?(child)}.any?
23
+ end
24
+
25
+ # Attach the current Calabash run-loop to a console.
26
+ #
27
+ # @example
28
+ # You have encountered a failing cucumber Scenario.
29
+ # You open the console and want to start investigating the cause of the failure.
30
+ #
31
+ # Use
32
+ #
33
+ # > console_attach
34
+ #
35
+ # to connect to the current run-loop so you can perform gestures.
36
+ #
37
+ # @param [Symbol] uia_strategy Optionally specify the uia strategy, which
38
+ # can be one of :shared_element, :preferences, :host. If you don't
39
+ # know which to choose, don't specify one and calabash will try deduce
40
+ # the correct strategy to use based on the environment variables used
41
+ # when starting the console.
42
+ #
43
+ # @return [Hash] The hash will contain the current device, the path to the
44
+ # current application, and the run-loop strategy.
45
+ #
46
+ # @raise [RuntimeError] If the app is not running.
47
+ def console_attach(uia_strategy=nil)
48
+ Calabash::Application.default = Calabash::IOS::Application.default_from_environment
49
+
50
+ identifier = Calabash::IOS::Device.default_identifier_for_application(Calabash::Application.default)
51
+ server = Calabash::IOS::Server.default
52
+
53
+ device = Calabash::IOS::Device.new(identifier, server)
54
+ Calabash::Device.default = device
55
+
56
+ begin
57
+ Calabash::Device.default.ensure_test_server_ready({:timeout => 4})
58
+ rescue RuntimeError => e
59
+ if e.to_s == 'Calabash server did not respond'
60
+ raise RuntimeError, 'You can only attach to a running Calabash iOS App'
61
+ else
62
+ raise e
63
+ end
64
+ end
65
+
66
+ run_loop_device = device.send(:run_loop_device)
67
+ result = Calabash::Device.default.send(:attach_to_run_loop, run_loop_device, uia_strategy)
68
+ result[:application] = Calabash::Application.default
69
+ result
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,24 @@
1
+ module Calabash
2
+ module IOS
3
+
4
+ require 'calabash/ios/device/runtime_attributes'
5
+ require 'calabash/ios/device/routes/error'
6
+ require 'calabash/ios/device/routes/response_parser'
7
+ require 'calabash/ios/device/routes/handle_route_mixin'
8
+ require 'calabash/ios/device/routes/map_route_mixin'
9
+ require 'calabash/ios/device/routes/uia_route_mixin'
10
+ require 'calabash/ios/device/routes/condition_route_mixin'
11
+ require 'calabash/ios/device/routes/backdoor_route_mixin'
12
+ require 'calabash/ios/device/routes/playback_route_mixin'
13
+ require 'calabash/ios/device/gestures_mixin'
14
+ require 'calabash/ios/device/physical_device_mixin'
15
+ require 'calabash/ios/device/status_bar_mixin'
16
+ require 'calabash/ios/device/rotation_mixin'
17
+ require 'calabash/ios/device/keyboard_mixin'
18
+ require 'calabash/ios/device/uia_keyboard_mixin'
19
+ require 'calabash/ios/device/text_mixin'
20
+ require 'calabash/ios/device/uia_mixin'
21
+ require 'calabash/ios/device/device_implementation'
22
+
23
+ end
24
+ end
@@ -0,0 +1,779 @@
1
+ module Calabash
2
+ module IOS
3
+
4
+ # An iOS Device is an iOS Simulator or physical device.
5
+ class Device < ::Calabash::Device
6
+
7
+ include Calabash::IOS::PhysicalDeviceMixin
8
+ include Calabash::IOS::Routes::ResponseParser
9
+ include Calabash::IOS::Routes::HandleRouteMixin
10
+ include Calabash::IOS::Routes::MapRouteMixin
11
+ include Calabash::IOS::Routes::UIARouteMixin
12
+ include Calabash::IOS::Routes::ConditionRouteMixin
13
+ include Calabash::IOS::Routes::BackdoorRouteMixin
14
+ include Calabash::IOS::Routes::PlaybackRouteMixin
15
+ include Calabash::IOS::StatusBarMixin
16
+ include Calabash::IOS::RotationMixin
17
+ include Calabash::IOS::KeyboardMixin
18
+ include Calabash::IOS::UIAKeyboardMixin
19
+ include Calabash::IOS::TextMixin
20
+ include Calabash::IOS::UIAMixin
21
+
22
+ include Calabash::IOS::GesturesMixin
23
+
24
+ # @todo Should these be public?
25
+ # @todo If public, document!
26
+ attr_reader :run_loop
27
+ attr_reader :uia_strategy
28
+ attr_reader :start_options
29
+
30
+ # Returns the default simulator identifier. The string that is return
31
+ # can be used as an argument to `instruments`.
32
+ #
33
+ # You can set the default simulator identifier by setting the
34
+ # `CAL_DEVICE_ID` environment variable. If this value is not set, then
35
+ # the default simulator identifier will indicate the highest supported
36
+ # iPhone 5s Simulator SDK. For example, when the active Xcode is 6.3,
37
+ # the default value will be "iPhone 5s (8.3 Simulator)".
38
+ #
39
+ # @see Calabash::Environment::DEVICE_IDENTIFIER
40
+ #
41
+ # @return [String] An instruments-ready simulator identifier.
42
+ # @raise [RuntimeError] When `CAL_DEVICE_ID` is set, this method will
43
+ # raise an error if no matching simulator can be found.
44
+ def self.default_simulator_identifier
45
+ identifier = Environment::DEVICE_IDENTIFIER
46
+
47
+ if identifier.nil?
48
+ RunLoop::Core.default_simulator
49
+ else
50
+ run_loop_device = Device.fetch_matching_simulator(identifier)
51
+ if run_loop_device.nil?
52
+ raise "Could not find a simulator with a UDID or name matching '#{identifier}'"
53
+ end
54
+ run_loop_device.instruments_identifier
55
+ end
56
+ end
57
+
58
+ # Returns the default physical device identifier. The string that is
59
+ # return can be used as an argument to `instruments`.
60
+ #
61
+ # You can set the default physical device identifier by setting the
62
+ # `CAL_DEVICE_ID` environment variable. If this value is not set,
63
+ # Calabash will try to detect available devices.
64
+ # * If no devices are available, this method will raise an error.
65
+ # * If more than one device is available, this method will raise an error.
66
+ # * If only one device is available, this method will return the UDID
67
+ # of that device.
68
+ #
69
+ # @see Calabash::Environment::DEVICE_IDENTIFIER
70
+ #
71
+ # @return [String] An instruments-ready device identifier.
72
+ # @raise [RuntimeError] When `CAL_DEVICE_ID` is set, this method will
73
+ # raise an error if no matching physical device can be found.
74
+ # @raise [RuntimeError] When `CAL_DEVICE_ID` is not set and no physical
75
+ # devices are available.
76
+ # @raise [RuntimeError] When `CAL_DEVICE_ID` is not set and more than one
77
+ # physical device is available.
78
+ def self.default_physical_device_identifier
79
+ identifier = Environment::DEVICE_IDENTIFIER
80
+
81
+ if identifier.nil?
82
+ connected_devices = RunLoop::XCTools.new.instruments(:devices)
83
+ if connected_devices.empty?
84
+ raise 'There are no physical devices connected.'
85
+ elsif connected_devices.count > 1
86
+ raise 'There is more than one physical devices connected. Use CAL_DEVICE_ID to indicate which you want to connect to.'
87
+ else
88
+ connected_devices.first.instruments_identifier
89
+ end
90
+ else
91
+ run_loop_device = Device.fetch_matching_physical_device(identifier)
92
+ if run_loop_device.nil?
93
+ raise "Could not find a physical device with a UDID or name matching '#{identifier}'"
94
+ end
95
+ run_loop_device.instruments_identifier
96
+ end
97
+ end
98
+
99
+ # Returns the default identifier for an application. If the application
100
+ # is a simulator bundle (.app), the default simulator identifier is
101
+ # returned. If the application is a device binary (.ipa), the default
102
+ # physical device identifier is returned.
103
+ #
104
+ # @see Calabash::IOS::Device#default_simulator_identifier
105
+ # @see Calabash::IOS::Device#default_physical_device_identifier
106
+ #
107
+ # @return [String] An instruments ready identifier based on whether the
108
+ # application is for a simulator or physical device.
109
+ # @raise [RuntimeError] If the application is not a .app or .ipa.
110
+ def self.default_identifier_for_application(application)
111
+ if application.simulator_bundle?
112
+ default_simulator_identifier
113
+ elsif application.device_binary?
114
+ default_physical_device_identifier
115
+ else
116
+ raise "Invalid application #{application} for iOS platform."
117
+ end
118
+ end
119
+
120
+ # Create a new iOS Device.
121
+ #
122
+ # @param [String] identifier The name or UDID of a simulator or physical
123
+ # device.
124
+ # @param [Calabash::IOS::Server] server A representation of the embedded
125
+ # Calabash server.
126
+ #
127
+ # @return [Calabash::IOS::Device] A representation of an iOS Simulator or
128
+ # physical device.
129
+ # @raise [RuntimeError] If the server points to localhost and the
130
+ # identifier is not for a simulator.
131
+ #
132
+ # @todo My inclination is to defer calling out to simctl or instruments
133
+ # here to find the RunLoop::Device that matches identifier. These are
134
+ # very expensive calls.
135
+ def initialize(identifier, server)
136
+ super
137
+
138
+ Calabash::IOS::Device.expect_compatible_server_endpoint(identifier, server)
139
+ end
140
+
141
+ # @!visibility private
142
+ def test_server_responding?
143
+ begin
144
+ http_client.get(Calabash::HTTP::Request.new('version')).status.to_i == 200
145
+ rescue Calabash::HTTP::Error => _
146
+ false
147
+ end
148
+ end
149
+
150
+ # @!visibility private
151
+ def to_s
152
+ if @run_loop_device
153
+ run_loop_device.to_s
154
+ else
155
+ "#<iOS Device '#{identifier}'>"
156
+ end
157
+ end
158
+
159
+ # @!visibility private
160
+ def inspect
161
+ to_s
162
+ end
163
+
164
+ # The device family of this device.
165
+ #
166
+ # @example
167
+ # # will be one of
168
+ # iPhone
169
+ # iPod
170
+ # iPad
171
+ #
172
+ # @return [String] the device family
173
+ # @raise [RuntimeError] If the app has not been launched.
174
+ def device_family
175
+ # For iOS Simulators, this can be obtained by asking the run_loop_device
176
+ # and analyzing the name of the device. This does not require the app
177
+ # to be launched, but it is expensive (takes many seconds).
178
+
179
+ # For physical devices, this can only be obtained using a third-party
180
+ # tool like ideviceinfo or asking the server.
181
+ expect_runtime_attributes_available(__method__)
182
+ runtime_attributes.device_family
183
+ end
184
+
185
+ # The form factor of the device under test.
186
+ #
187
+ # Will be one of:
188
+ #
189
+ # * ipad
190
+ # * iphone 4in
191
+ # * iphone 3.5in
192
+ # * iphone 6
193
+ # * iphone 6+
194
+ # * unknown # if no information can be found.
195
+ #
196
+ # @note iPod is not on this list for a reason! An iPod has an iPhone
197
+ # form factor. If you need to detect an iPod use `device_family`. Also
198
+ # note that there are no iPod simulators.
199
+ #
200
+ # @return [String] The form factor of the device under test.
201
+ # @raise [RuntimeError] If the app has not been launched.
202
+ def form_factor
203
+ # For iOS Simulators, this can be obtained by asking the run_loop_device
204
+ # and analyzing the name of the device. This does not require the app
205
+ # to be launched, but it is expensive (takes many seconds).
206
+
207
+ # For physical devices, this can only be obtained using a third-party
208
+ # tool like ideviceinfo or asking the server.
209
+ expect_runtime_attributes_available(__method__)
210
+ runtime_attributes.form_factor
211
+ end
212
+
213
+ # @!visibility private
214
+ # The iOS version on the test device.
215
+ #
216
+ # @return [RunLoop::Version] The major.minor.patch[.pre\d] version of the
217
+ # iOS version on the device.
218
+ def ios_version
219
+ # Can be obtain by asking for a device's run_loop_device. This does not
220
+ # require the app to be launched, but it is expensive
221
+ # (takes many seconds). run_loop_device is memoized so the expense
222
+ # is only incurred 1x per device instance.
223
+
224
+ # Can also be obtained by asking the server after the app is launched
225
+ # on the device which would be cheaper.
226
+ run_loop_device.version
227
+ end
228
+
229
+ # Is the app that is running an iPhone-only app emulated on an iPad?
230
+ #
231
+ # @note If the app is running in emulation mode, there will be a 1x or 2x
232
+ # scale button visible on the iPad.
233
+ #
234
+ # @return [Boolean] true if the app running on this devices is an
235
+ # iPhone-only app emulated on an iPad
236
+ # @raise [RuntimeError] If the app has not been launched.
237
+ def iphone_app_emulated_on_ipad?
238
+ # It is possible to find this information on iOS Simulators without
239
+ # launching the app. It is not possible to find this information
240
+ # when targeting a physical device unless a third-party tool is used.
241
+ expect_runtime_attributes_available(__method__)
242
+ runtime_attributes.iphone_app_emulated_on_ipad?
243
+ end
244
+
245
+ # Is this device a physical device?
246
+ # @return [Boolean] Returns true if this device is a physical device.
247
+ def physical_device?
248
+ # Can be obtain by asking for a device's run_loop_device. This does not
249
+ # require the app to be launched, but it is expensive
250
+ # (takes many seconds). run_loop_device is memoized so the expense
251
+ # is only incurred 1x per device instance.
252
+
253
+ # Can also be obtained by asking the server after the app is launched
254
+ # on the device which would be cheaper.
255
+ run_loop_device.physical_device?
256
+ end
257
+
258
+ # Information about the runtime screen dimensions of the app under test.
259
+ #
260
+ # This is a hash of form:
261
+ #
262
+ # ```
263
+ # {
264
+ # :sample => 1,
265
+ # :height => 1334,
266
+ # :width => 750,
267
+ # :scale" => 2
268
+ # }
269
+ # ```
270
+ #
271
+ # @return [Hash] screen dimensions, scale and down/up sampling fraction.
272
+ # @raise [RuntimeError] If the app has not been launched.
273
+ def screen_dimensions
274
+ # This can only be obtained at runtime because of iOS scaling and
275
+ # sampling.
276
+ expect_runtime_attributes_available(__method__)
277
+ runtime_attributes.screen_dimensions
278
+ end
279
+
280
+ # The version of the embedded Calabash server that is running in the
281
+ # app under test on this device.
282
+ #
283
+ # @return [RunLoop::Version] The major.minor.patch[.pre\d] version of the
284
+ # embedded Calabash server
285
+ # @raise [RuntimeError] If the app has not been launched.
286
+ def server_version
287
+ # It is possible to find this information without launching the app but
288
+ # it's probably best to ask the server for this information after the
289
+ # app has launched.
290
+ expect_runtime_attributes_available(__method__)
291
+ runtime_attributes.server_version
292
+ end
293
+
294
+ # Is this device a simulator?
295
+ # @return [Boolean] Returns true if this device is a simulator.
296
+ def simulator?
297
+ # Can be obtain by asking for a device's run_loop_device. This does not
298
+ # require the app to be launched, but it is expensive
299
+ # (takes many seconds). run_loop_device is memoized so the expense
300
+ # is only incurred 1x per device instance.
301
+
302
+ # Can also be obtained by asking the server after the app is launched
303
+ # on the device which would be cheaper.
304
+ run_loop_device.simulator?
305
+ end
306
+
307
+ # @see Calabash::Location#set_location
308
+ def set_location(location)
309
+ if physical_device?
310
+ raise 'Setting the location is not supported on physical devices'
311
+ end
312
+
313
+ location_data =
314
+ {
315
+ 'latitude' => location[:latitude],
316
+ 'longitude' => location[:longitude]
317
+ }
318
+
319
+ uia_serialize_and_call(:setLocation, location_data)
320
+ end
321
+
322
+ private
323
+
324
+ attr_reader :runtime_attributes
325
+
326
+ # @!visibility private
327
+ def _start_app(application, options={})
328
+ if application.simulator_bundle?
329
+ start_app_on_simulator(application, options)
330
+
331
+ elsif application.device_binary?
332
+ start_app_on_physical_device(application, options)
333
+ else
334
+ raise "Invalid application #{application} for iOS platform."
335
+ end
336
+ {
337
+ :device => self,
338
+ :application => application,
339
+ :uia_strategy => uia_strategy
340
+ }
341
+ end
342
+
343
+ # @!visibility private
344
+ def start_app_on_simulator(application, options)
345
+ @run_loop_device ||= Device.fetch_matching_simulator(identifier)
346
+
347
+ if @run_loop_device.nil?
348
+ raise "Could not find a simulator with a UDID or name matching '#{identifier}'"
349
+ end
350
+
351
+ expect_valid_simulator_state_for_starting(application, @run_loop_device)
352
+
353
+ start_app_with_device_and_options(application, @run_loop_device, options)
354
+ wait_for_server_to_start
355
+ end
356
+
357
+ # @todo No unit tests.
358
+ # @!visibility private
359
+ def expect_valid_simulator_state_for_starting(application, run_loop_device)
360
+ bridge = run_loop_bridge(run_loop_device, application)
361
+
362
+ expect_app_installed_on_simulator(bridge)
363
+
364
+ installed_app = Calabash::IOS::Application.new(bridge.fetch_app_dir)
365
+ expect_matching_sha1s(installed_app, application)
366
+ end
367
+
368
+ # @!visibility private
369
+ def start_app_on_physical_device(application, options)
370
+ # @todo Cannot check to see if app is already installed.
371
+ # @todo Cannot check to see if app is different.
372
+
373
+ @run_loop_device ||= Device.fetch_matching_physical_device(identifier)
374
+
375
+ if @run_loop_device.nil?
376
+ raise "Could not find a physical device with a UDID or name matching '#{identifier}'"
377
+ end
378
+
379
+ start_app_with_device_and_options(application, @run_loop_device, options)
380
+ wait_for_server_to_start
381
+ end
382
+
383
+ # @!visibility private
384
+ def start_app_with_device_and_options(application, run_loop_device, user_defined_options)
385
+ start_options = merge_start_options!(application, run_loop_device, user_defined_options)
386
+ @run_loop = RunLoop.run(start_options)
387
+ @uia_strategy = @run_loop[:uia_strategy]
388
+ end
389
+
390
+ # @!visibility private
391
+ def wait_for_server_to_start(options={})
392
+ ensure_test_server_ready(options)
393
+ device_info = fetch_runtime_attributes
394
+ @runtime_attributes = new_device_runtime_info(device_info)
395
+ end
396
+
397
+ # @!visibility private
398
+ def new_device_runtime_info(device_info)
399
+ RuntimeAttributes.new(device_info)
400
+ end
401
+
402
+ # @!visibility private
403
+ def _stop_app
404
+ begin
405
+ if test_server_responding?
406
+ parameters = default_stop_app_parameters
407
+ request = request_factory('exit', parameters)
408
+ http_client.get(request)
409
+ else
410
+ true
411
+ end
412
+ rescue Calabash::HTTP::Error => e
413
+ raise "Could send 'exit' to the app: #{e}"
414
+ ensure
415
+ @runtime_attributes = nil
416
+ end
417
+ end
418
+
419
+ # @!visibility private
420
+ def _screenshot(path)
421
+ request = request_factory('screenshot', {:path => path})
422
+ begin
423
+ screenshot = http_client.get(request)
424
+ File.open(path, 'wb') { |file| file.write screenshot.body }
425
+ rescue Calabash::HTTP::Error => e
426
+ raise "Could not send 'screenshot' to the app: #{e}"
427
+ end
428
+ path
429
+ end
430
+
431
+ # @!visibility private
432
+ def _install_app(application)
433
+ if application.simulator_bundle?
434
+ @run_loop_device ||= Device.fetch_matching_simulator(identifier)
435
+
436
+ if @run_loop_device.nil?
437
+ raise "Could not find a simulator with a UDID or name matching '#{identifier}'"
438
+ end
439
+
440
+ install_app_on_simulator(application, @run_loop_device)
441
+ elsif application.device_binary?
442
+ @run_loop_device ||= Device.fetch_matching_physical_device(identifier)
443
+
444
+ if @run_loop_device.nil?
445
+ raise "Could not find a physical device with a UDID or name matching '#{identifier}'"
446
+ end
447
+ install_app_on_physical_device(application, @run_loop_device.udid)
448
+ else
449
+ raise "Invalid application #{application} for iOS platform."
450
+ end
451
+ end
452
+
453
+ # @!visibility private
454
+ def _ensure_app_installed(application)
455
+ if application.simulator_bundle?
456
+ @run_loop_device ||= Device.fetch_matching_simulator(identifier)
457
+
458
+ if @run_loop_device.nil?
459
+ raise "Could not find a simulator with a UDID or name matching '#{identifier}'"
460
+ end
461
+
462
+ bridge = run_loop_bridge(@run_loop_device, application)
463
+
464
+ if bridge.app_is_installed?
465
+ installed_app = Calabash::IOS::Application.new(bridge.fetch_app_dir)
466
+
467
+ if installed_app.same_sha1_as?(application)
468
+ true
469
+ else
470
+ @logger.log("The sha1 checksum has changed (#{installed_app.sha1} != #{application.sha1}.", :info)
471
+ install_app_on_simulator(application, @run_loop_device, bridge)
472
+ end
473
+ else
474
+ install_app_on_simulator(application, @run_loop_device, bridge)
475
+ end
476
+ elsif application.device_binary?
477
+
478
+ @run_loop_device ||= Device.fetch_matching_physical_device(identifier)
479
+
480
+ if @run_loop_device.nil?
481
+ raise "Could not find a physical device with a UDID or name matching '#{identifier}'"
482
+ end
483
+
484
+ ensure_app_installed_on_physical_device(application, @run_loop_device.udid)
485
+ else
486
+ raise "Invalid application #{application} for iOS platform."
487
+ end
488
+ end
489
+
490
+ # @!visibility private
491
+ def _clear_app_data(application)
492
+ if application.simulator_bundle?
493
+ @run_loop_device ||= Device.fetch_matching_simulator(identifier)
494
+
495
+ if @run_loop_device.nil?
496
+ raise "Could not find a simulator with a UDID or name matching '#{identifier}'"
497
+ end
498
+
499
+ bridge = run_loop_bridge(@run_loop_device, application)
500
+ if bridge.app_is_installed?
501
+ clear_app_data_on_simulator(application, @run_loop_device, bridge)
502
+ else
503
+ true
504
+ end
505
+ elsif application.device_binary?
506
+ @run_loop_device ||= Device.fetch_matching_physical_device(identifier)
507
+
508
+ if @run_loop_device.nil?
509
+ raise "Could not find a physical device with a UDID or name matching '#{identifier}'"
510
+ end
511
+
512
+ clear_app_data_on_physical_device(application, @run_loop_device.udid)
513
+ else
514
+ raise "Invalid application #{application} for iOS platform."
515
+ end
516
+ end
517
+
518
+ # @!visibility private
519
+ def enter_text(text)
520
+ # @todo implement this
521
+ raise 'ni'
522
+ end
523
+
524
+ # @!visibility private
525
+ def clear_app_data_on_simulator(application, run_loop_device, bridge)
526
+ begin
527
+ bridge.reset_app_sandbox
528
+ true
529
+ rescue StandardError => e
530
+ raise "Could not clear app data for #{application.identifier} on #{run_loop_device}: #{e}"
531
+ end
532
+ end
533
+
534
+ # @!visibility private
535
+ def _uninstall_app(application)
536
+ if application.simulator_bundle?
537
+ @run_loop_device ||= Device.fetch_matching_simulator(identifier)
538
+
539
+ if @run_loop_device.nil?
540
+ raise "Could not find a simulator with a UDID or name matching '#{identifier}'"
541
+ end
542
+
543
+ bridge = run_loop_bridge(@run_loop_device, application)
544
+ if bridge.app_is_installed?
545
+ uninstall_app_on_simulator(application, @run_loop_device, bridge)
546
+ else
547
+ true
548
+ end
549
+ elsif application.device_binary?
550
+ @run_loop_device ||= Device.fetch_matching_physical_device(identifier)
551
+
552
+ if @run_loop_device.nil?
553
+ raise "Could not find a physical device with a UDID or name matching '#{identifier}'"
554
+ end
555
+
556
+ uninstall_app_on_physical_device(application, @run_loop_device.udid)
557
+ else
558
+ raise "Invalid application #{application} for iOS platform."
559
+ end
560
+ end
561
+
562
+ # @!visibility private
563
+ def uninstall_app_on_simulator(application, run_loop_device, bridge)
564
+ begin
565
+ bridge.uninstall
566
+ true
567
+ rescue e
568
+ raise "Could not uninstall #{application.identifier} on #{run_loop_device}: #{e}"
569
+ end
570
+ end
571
+
572
+ # @!visibility private
573
+ def default_stop_app_parameters
574
+ {
575
+ :post_resign_active_delay => 0.4,
576
+ :post_will_terminate_delay => 0.4,
577
+ :exit_code => 0
578
+ }
579
+ end
580
+
581
+ # @!visibility private
582
+ def request_factory(route, parameters={})
583
+ Calabash::HTTP::Request.new(route, parameters)
584
+ end
585
+
586
+ # @!visibility private
587
+ # RunLoop::Device is incredibly slow; don't call it more than once.
588
+ def run_loop_device
589
+ @run_loop_device ||= RunLoop::Device.device_with_identifier(identifier)
590
+ end
591
+
592
+ # @!visibility private
593
+ # Do not memoize this. The Bridge initializer does a bunch of work to
594
+ # prepare the environment for simctl actions.
595
+ def run_loop_bridge(run_loop_simulator_device, application)
596
+ RunLoop::Simctl::Bridge.new(run_loop_simulator_device, application.path)
597
+ end
598
+
599
+ # @!visibility private
600
+ def install_app_on_simulator(application, run_loop_device, run_loop_bridge = nil)
601
+ begin
602
+
603
+ if run_loop_bridge.nil?
604
+ bridge = run_loop_bridge(run_loop_device, application)
605
+ else
606
+ bridge = run_loop_bridge
607
+ end
608
+
609
+ bridge.uninstall
610
+ bridge.install
611
+ rescue StandardError => e
612
+ raise "Could not install #{application} on #{run_loop_device}: #{e}"
613
+ end
614
+ end
615
+
616
+ # @!visibility private
617
+ # Expensive!
618
+ def Device.fetch_matching_simulator(udid_or_name)
619
+ sim_control = RunLoop::SimControl.new
620
+ sim_control.simulators.detect do |sim|
621
+ sim.instruments_identifier == udid_or_name ||
622
+ sim.udid == udid_or_name
623
+ end
624
+ end
625
+
626
+ # @!visibility private
627
+ # Very expensive!
628
+ def Device.fetch_matching_physical_device(udid_or_name)
629
+ xctools = RunLoop::XCTools.new
630
+ xctools.instruments(:devices).detect do |device|
631
+ device.name == udid_or_name ||
632
+ device.udid == udid_or_name
633
+ end
634
+ end
635
+
636
+ # @!visibility private
637
+ # @todo Should this take a run_loop_device as an argument, rather than
638
+ # an identifier? Since calls to instruments and simctl are very
639
+ # expensive we want to do as few of them as possible. Maybe the
640
+ # localhost? check should be done outside of this method? If nothing
641
+ # else, the result of Device.fetch_matching_simulator should be captured
642
+ # in @run_loop_device.
643
+ def self.expect_compatible_server_endpoint(identifier, server)
644
+ if server.localhost?
645
+ run_loop_device = Device.fetch_matching_simulator(identifier)
646
+ if run_loop_device.nil?
647
+ Logger.error("The identifier for this device is '#{identifier}'")
648
+ Logger.error('which resolves to a physical device.')
649
+ Logger.error("The server endpoint '#{server.endpoint}' is for an iOS Simulator.")
650
+ Logger.error('Use CAL_ENDPOINT to specify the IP address of your device')
651
+ Logger.error("Ex. $ CAL_ENDPOINT=http://10.0.1.2:37265 CAL_DEVICE_ID=#{identifier} be calabash ...")
652
+ raise "Invalid device endpoint '#{server.endpoint}'"
653
+ end
654
+ end
655
+ end
656
+
657
+ # @!visibility private
658
+ def expect_app_installed_on_simulator(bridge)
659
+ unless bridge.app_is_installed?
660
+ raise 'App is not installed, you need to install it first.'
661
+ end
662
+ true
663
+ end
664
+
665
+ # @!visibility private
666
+ def expect_matching_sha1s(installed_app, new_app)
667
+ unless installed_app.same_sha1_as?(new_app)
668
+ logger.log('The installed application and the one under test are different.', :error)
669
+ logger.log("Installed path: #{installed_app.path}", :error)
670
+ logger.log(" New path: #{new_app.path}", :error)
671
+ logger.log("Installed SHA1: #{installed_app.sha1}", :error)
672
+ logger.log(" New SHA1: #{new_app.sha1}", :error)
673
+ raise 'The installed app is different from the app under test. You must install the new app before starting'
674
+ end
675
+ true
676
+ end
677
+
678
+ # @!visibility private
679
+ def uia_strategy_from_environment(run_loop_device)
680
+ Environment::UIA_STRATEGY || default_uia_strategy(run_loop_device)
681
+ end
682
+
683
+ # @!visibility private
684
+ # @todo Needs a bunch of work; see the argument munging in Calabash 0.x Launcher.
685
+ def merge_start_options!(application, run_loop_device, options_from_user)
686
+ strategy = uia_strategy_from_environment(run_loop_device)
687
+
688
+ default_options =
689
+ {
690
+ :app => application.path,
691
+ :bundle_id => application.identifier,
692
+ :device_target => run_loop_device.instruments_identifier,
693
+ :uia_strategy => strategy
694
+ }
695
+ @start_options = default_options.merge(options_from_user)
696
+ end
697
+
698
+ # @todo Move to run-loop!?!
699
+ # @todo Not tested locally!
700
+ def default_uia_strategy(run_loop_device)
701
+ default = :preferences
702
+ if run_loop_device.physical_device?
703
+ # `setPreferencesValueForKey` on iOS 8 devices is broken in Xcode 6
704
+ #
705
+ # rdar://18296714
706
+ # http://openradar.appspot.com/radar?id=5891145586442240
707
+ # :preferences strategy is broken on iOS 8.0
708
+ if run_loop_device.version >= RunLoop::Version.new('8.0')
709
+ default = :host
710
+ end
711
+ end
712
+ default
713
+ end
714
+
715
+ # @!visibility private
716
+ def fetch_runtime_attributes
717
+ request = request_factory('version')
718
+ body = http_client.get(request).body
719
+ begin
720
+ JSON.parse(body)
721
+ rescue TypeError, JSON::ParserError => _
722
+ raise "Could not parse response '#{body}'; the app has probably crashed"
723
+ end
724
+ end
725
+
726
+ # @!visibility private
727
+ def expect_runtime_attributes_available(method_name)
728
+ if runtime_attributes.nil?
729
+ logger.log("The method '#{method_name}' is not available to IOS::Device until", :info)
730
+ logger.log('the app has been launched with Calabash start_app.', :info)
731
+ raise "The method '#{method_name}' can only be called after the app has been launched"
732
+ end
733
+ true
734
+ end
735
+
736
+ def instruments_pid
737
+ pids = RunLoop::Instruments.new.instruments_pids
738
+ if pids
739
+ pids.first
740
+ else
741
+ nil
742
+ end
743
+ end
744
+
745
+ # Assumes the app is already running and the server can be reached.
746
+ # @todo It might make sense to cache the uia_strategy on the _server_
747
+ # to avoid having to guess.
748
+ def attach_to_run_loop(run_loop_device, uia_strategy)
749
+ if uia_strategy
750
+ strategy = uia_strategy
751
+ else
752
+ strategy = uia_strategy_from_environment(run_loop_device)
753
+ end
754
+
755
+ if strategy == :host
756
+ @run_loop = RunLoop::HostCache.default.read
757
+ @uia_strategy = :host
758
+ else
759
+ pid = instruments_pid
760
+ @run_loop = {}
761
+ @run_loop[:uia_strategy] = strategy
762
+ @run_loop[:pid] = pid
763
+ @uia_strategy = strategy
764
+ end
765
+
766
+ # populate the @runtime_attributes
767
+ wait_for_server_to_start({:timeout => 2})
768
+ {
769
+ :device => self,
770
+ :uia_strategy => strategy
771
+ }
772
+ end
773
+
774
+ def world_module
775
+ Calabash::IOS
776
+ end
777
+ end
778
+ end
779
+ end