calabash 1.2.1 → 1.9.9.pre1

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 (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