calabash-cucumber 0.19.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/dylibs/libCalabashDyn.dylib +0 -0
  3. data/dylibs/libCalabashDynSim.dylib +0 -0
  4. data/lib/calabash-cucumber.rb +9 -2
  5. data/lib/calabash-cucumber/abstract.rb +23 -0
  6. data/lib/calabash-cucumber/automator/automator.rb +158 -0
  7. data/lib/calabash-cucumber/automator/coordinates.rb +401 -0
  8. data/lib/calabash-cucumber/automator/device_agent.rb +424 -0
  9. data/lib/calabash-cucumber/automator/instruments.rb +441 -0
  10. data/lib/calabash-cucumber/connection_helpers.rb +1 -0
  11. data/lib/calabash-cucumber/core.rb +632 -138
  12. data/lib/calabash-cucumber/device_agent.rb +346 -0
  13. data/lib/calabash-cucumber/dot_dir.rb +1 -0
  14. data/lib/calabash-cucumber/environment.rb +1 -0
  15. data/lib/calabash-cucumber/environment_helpers.rb +4 -3
  16. data/lib/calabash-cucumber/http/http.rb +6 -4
  17. data/lib/calabash-cucumber/keyboard_helpers.rb +97 -679
  18. data/lib/calabash-cucumber/launcher.rb +107 -31
  19. data/lib/calabash-cucumber/log_tailer.rb +46 -0
  20. data/lib/calabash-cucumber/map.rb +7 -1
  21. data/lib/calabash-cucumber/rotation_helpers.rb +47 -139
  22. data/lib/calabash-cucumber/status_bar_helpers.rb +51 -20
  23. data/lib/calabash-cucumber/store/preferences.rb +3 -0
  24. data/lib/calabash-cucumber/uia.rb +333 -2
  25. data/lib/calabash-cucumber/usage_tracker.rb +2 -0
  26. data/lib/calabash-cucumber/version.rb +2 -2
  27. data/lib/calabash-cucumber/wait_helpers.rb +2 -0
  28. data/lib/calabash/formatters/html.rb +6 -1
  29. data/lib/frank-calabash.rb +10 -4
  30. data/scripts/.irbrc +3 -0
  31. data/staticlib/calabash.framework.zip +0 -0
  32. data/staticlib/libFrankCalabash.a +0 -0
  33. metadata +11 -6
  34. data/lib/calabash-cucumber/actions/instruments_actions.rb +0 -155
@@ -0,0 +1,424 @@
1
+ # @!visibility private
2
+ module Calabash
3
+ module Cucumber
4
+ module Automator
5
+
6
+ require "calabash-cucumber/automator/automator"
7
+
8
+ # @!visibility private
9
+ class DeviceAgent < Calabash::Cucumber::Automator::Automator
10
+
11
+ require "run_loop"
12
+ require "calabash-cucumber/map"
13
+
14
+ require "calabash-cucumber/query_helpers"
15
+ include Calabash::Cucumber::QueryHelpers
16
+
17
+ require "calabash-cucumber/status_bar_helpers"
18
+ include Calabash::Cucumber::StatusBarHelpers
19
+
20
+ require "calabash-cucumber/rotation_helpers"
21
+ include Calabash::Cucumber::RotationHelpers
22
+
23
+ require "calabash-cucumber/environment_helpers"
24
+ include Calabash::Cucumber::EnvironmentHelpers
25
+
26
+ require "calabash-cucumber/automator/coordinates"
27
+
28
+ # @!visibility private
29
+ def self.expect_valid_args(args)
30
+ if args.nil?
31
+ raise ArgumentError, "Expected args to be a non-nil Array"
32
+ end
33
+
34
+ if !args.is_a?(Array)
35
+ raise ArgumentError, %Q[Expected args to be an Array, found:
36
+
37
+ args = #{args}
38
+
39
+ ]
40
+ end
41
+
42
+ if args.count != 1
43
+ raise(ArgumentError,
44
+ %Q[Expected args to be an Array with one element, found:
45
+
46
+ args = #{args}
47
+
48
+ ])
49
+ end
50
+
51
+ if !args[0].is_a?(RunLoop::DeviceAgent::Client)
52
+ raise(ArgumentError, %Q[
53
+ Expected first element of args to be a RunLoop::DeviceAgent::Client instance, found:
54
+ args[0] = #{args[0]}])
55
+ end
56
+
57
+ true
58
+ end
59
+
60
+ attr_reader :client
61
+
62
+ # @!visibility private
63
+ def initialize(*args)
64
+ DeviceAgent.expect_valid_args(args)
65
+ @client = args[0]
66
+ end
67
+
68
+ # @!visibility private
69
+ def name
70
+ :device_agent
71
+ end
72
+
73
+ # @!visibility private
74
+ def stop
75
+ client.send(:shutdown)
76
+ end
77
+
78
+ # @!visibility private
79
+ def running?
80
+ client.send(:running?)
81
+ end
82
+
83
+ # @!visibility private
84
+ def session_delete
85
+ client.send(:session_delete)
86
+ end
87
+
88
+ # @!visibility private
89
+ def touch(options)
90
+ hash = query_for_coordinates(options)
91
+
92
+ client.perform_coordinate_gesture("touch",
93
+ hash[:coordinates][:x],
94
+ hash[:coordinates][:y])
95
+ [hash[:view]]
96
+ end
97
+
98
+ # @!visibility private
99
+ def double_tap(options)
100
+ hash = query_for_coordinates(options)
101
+ client.perform_coordinate_gesture("double_tap",
102
+ hash[:coordinates][:x],
103
+ hash[:coordinates][:y])
104
+ [hash[:view]]
105
+ end
106
+
107
+ # @!visibility private
108
+ def two_finger_tap(options)
109
+ hash = query_for_coordinates(options)
110
+ client.perform_coordinate_gesture("two_finger_tap",
111
+ hash[:coordinates][:x],
112
+ hash[:coordinates][:y])
113
+ [hash[:view]]
114
+ end
115
+
116
+ # @!visibility private
117
+ def touch_hold(options)
118
+ hash = query_for_coordinates(options)
119
+
120
+ duration = options[:duration] || 3
121
+ client.perform_coordinate_gesture("touch",
122
+ hash[:coordinates][:x],
123
+ hash[:coordinates][:y],
124
+ {:duration => duration})
125
+ [hash[:view]]
126
+ end
127
+
128
+ # @!visibility private
129
+ def swipe(options)
130
+ dupped_options = options.dup
131
+
132
+ if dupped_options[:query].nil?
133
+ dupped_options[:query] = "*"
134
+ end
135
+
136
+ hash = query_for_coordinates(dupped_options)
137
+ from_point = hash[:coordinates]
138
+ element = hash[:view]
139
+
140
+ # DeviceAgent does not understand the :force. Does anyone?
141
+ force = dupped_options[:force]
142
+ case force
143
+ when :strong
144
+ duration = 0.2
145
+ when :normal
146
+ duration = 0.4
147
+ when :light
148
+ duration = 0.7
149
+ else
150
+ # Caller is responsible for validating the :force option.
151
+ duration = 0.5
152
+ end
153
+
154
+ gesture_options = {
155
+ :duration => duration
156
+ }
157
+
158
+ direction = dupped_options[:direction]
159
+ to_point = Coordinates.end_point_for_swipe(direction, element, force)
160
+ client.pan_between_coordinates(from_point, to_point, gesture_options)
161
+ [hash[:view]]
162
+ end
163
+
164
+ # @!visibility private
165
+ def pan(from_query, to_query, options)
166
+ dupped_options = options.dup
167
+
168
+ dupped_options[:query] = from_query
169
+ from_hash = query_for_coordinates(dupped_options)
170
+ from_point = from_hash[:coordinates]
171
+
172
+ dupped_options[:query] = to_query
173
+ to_hash = query_for_coordinates(dupped_options)
174
+ to_point = to_hash[:coordinates]
175
+
176
+ gesture_options = {
177
+ :duration => dupped_options[:duration]
178
+ }
179
+
180
+ client.pan_between_coordinates(from_point, to_point,
181
+ gesture_options)
182
+
183
+ [from_hash[:view], to_hash[:view]]
184
+ end
185
+
186
+ # @!visibility private
187
+ def pan_coordinates(from_point, to_point, options)
188
+
189
+ gesture_options = {
190
+ :duration => options[:duration]
191
+ }
192
+
193
+ client.pan_between_coordinates(from_point, to_point,
194
+ gesture_options)
195
+ [first_element_for_query("*")]
196
+ end
197
+
198
+ # @!visibility private
199
+ def flick(options)
200
+ gesture_options = {
201
+ duration: 0.2
202
+ }
203
+
204
+ delta = options[:delta]
205
+
206
+ # The UIA deltas are too small.
207
+ scaled_delta = {
208
+ :x => delta[:x] * 2.0,
209
+ :y => delta[:y] * 2.0
210
+ }
211
+
212
+ hash = query_for_coordinates(options)
213
+ view = hash[:view]
214
+
215
+ start_point = point_from(view)
216
+ end_point = point_from(view, {:offset => scaled_delta})
217
+
218
+ client.pan_between_coordinates(start_point,
219
+ end_point,
220
+ gesture_options)
221
+ [view]
222
+ end
223
+
224
+ # @!visibility private
225
+ def enter_text_with_keyboard(string, options={})
226
+ client.enter_text(string)
227
+ end
228
+
229
+ # @!visibility private
230
+ def enter_char_with_keyboard(char)
231
+ client.enter_text(char)
232
+ end
233
+
234
+ # @!visibility private
235
+ def char_for_keyboard_action(action_key)
236
+ SPECIAL_ACTION_CHARS[action_key]
237
+ end
238
+
239
+ # @!visibility private
240
+ def tap_keyboard_action_key
241
+ mark = mark_for_return_key_of_first_responder
242
+ if mark
243
+ begin
244
+ # The underlying query for coordinates always expects results.
245
+ value = client.touch({marked: mark})
246
+ return value
247
+ rescue RuntimeError => _
248
+ RunLoop.log_debug("Cannot find mark '#{mark}' with query; will send a newline")
249
+ end
250
+ else
251
+ RunLoop.log_debug("Cannot find keyboard return key type; sending a newline")
252
+ end
253
+
254
+ code = char_for_keyboard_action("Return")
255
+ client.enter_text(code)
256
+ end
257
+
258
+ # @!visibility private
259
+ def tap_keyboard_delete_key
260
+ client.touch({marked: "delete"})
261
+ end
262
+
263
+ # @!visibility private
264
+ def fast_enter_text(text)
265
+ client.enter_text(text)
266
+ end
267
+
268
+ # @!visibility private
269
+ #
270
+ # Stable across different keyboard languages.
271
+ def dismiss_ipad_keyboard
272
+ client.touch({marked: "Hide keyboard"})
273
+ end
274
+
275
+ # @!visibility private
276
+ def rotate(direction)
277
+ # Caller is responsible for normalizing and verifying direction.
278
+ current_orientation = status_bar_orientation.to_sym
279
+ key = orientation_key(direction, current_orientation)
280
+ position = orientation_for_key(key)
281
+ rotate_home_button_to(position)
282
+ end
283
+
284
+ # @!visibility private
285
+ def rotate_home_button_to(position)
286
+ # Caller is responsible for normalizing and verifying position.
287
+ client.rotate_home_button_to(position)
288
+ status_bar_orientation.to_sym
289
+ end
290
+
291
+ private
292
+
293
+ # @!visibility private
294
+ #
295
+ # Calls #point_from which applies any :offset supplied in the options.
296
+ def query_for_coordinates(options)
297
+ uiquery = options[:query]
298
+
299
+ if uiquery.nil?
300
+ offset = options[:offset]
301
+
302
+ if offset && offset[:x] && offset[:y]
303
+ {
304
+ :coordinates => offset,
305
+ :view => offset
306
+ }
307
+ else
308
+ raise ArgumentError, %Q[
309
+ If query is nil, there must be a valid offset in the options.
310
+
311
+ Expected: options[:offset] = {:x => NUMERIC, :y => NUMERIC}
312
+ Actual: options[:offset] = #{offset ? offset : "nil"}
313
+
314
+ ]
315
+ end
316
+ else
317
+
318
+ first_element = first_element_for_query(uiquery)
319
+
320
+ if first_element.nil?
321
+ msg = %Q[
322
+ Could not find any views with query:
323
+
324
+ #{uiquery}
325
+
326
+ Make sure your query returns at least one view.
327
+
328
+ ]
329
+ Calabash::Cucumber::Map.new.screenshot_and_raise(msg)
330
+ else
331
+ {
332
+ :coordinates => point_from(first_element, options),
333
+ :view => first_element
334
+ }
335
+ end
336
+ end
337
+ end
338
+
339
+ # @!visibility private
340
+ def first_element_for_query(uiquery)
341
+
342
+ if uiquery.nil?
343
+ raise ArgumentError, "Query cannot be nil"
344
+ end
345
+
346
+ # Will raise if response "outcome" is not SUCCESS
347
+ results = Calabash::Cucumber::Map.raw_map(uiquery, :query)["results"]
348
+
349
+ if results.empty?
350
+ nil
351
+ else
352
+ results[0]
353
+ end
354
+ end
355
+
356
+ # @!visibility private
357
+ #
358
+ # Don't change the double quotes.
359
+ SPECIAL_ACTION_CHARS = {
360
+ "Delete" => "\b",
361
+ "Return" => "\n"
362
+ }.freeze
363
+
364
+ # @!visibility private
365
+ #
366
+ # Keys are from the UIReturnKeyType enum.
367
+ #
368
+ # The values are localization independent identifiers - these are
369
+ # stable across localizations and keyboard languages. The exception is
370
+ # Continue which is not stable.
371
+ RETURN_KEY_TYPE = {
372
+ 0 => "Return",
373
+ 1 => "Go",
374
+ 2 => "Google",
375
+ # Needs special physical device vs simulator handling.
376
+ 3 => "Join",
377
+ 4 => "Next",
378
+ 5 => "Route",
379
+ 6 => "Search",
380
+ 7 => "Send",
381
+ 8 => "Yahoo",
382
+ 9 => "Done",
383
+ 10 => "Emergency call",
384
+ # https://xamarin.atlassian.net/browse/TCFW-344
385
+ # Localized!!! Apple bug.
386
+ 11 => "Continue"
387
+ }.freeze
388
+
389
+ # @!visibility private
390
+ def mark_for_return_key_type(number)
391
+ # https://xamarin.atlassian.net/browse/TCFW-361
392
+ value = RETURN_KEY_TYPE[number]
393
+ if value == "Join" && !simulator?
394
+ "Join:"
395
+ else
396
+ value
397
+ end
398
+ end
399
+
400
+ # @!visibility private
401
+ def return_key_type_of_first_responder
402
+
403
+ ['textField', 'textView'].each do |ui_class|
404
+ query = "#{ui_class} isFirstResponder:1"
405
+ raw = Calabash::Cucumber::Map.raw_map(query, :query, :returnKeyType)
406
+ results = raw["results"]
407
+ if !results.empty?
408
+ return results.first
409
+ end
410
+ end
411
+
412
+ RunLoop.log_debug("Cannot find keyboard first responder to ask for its returnKeyType")
413
+ nil
414
+ end
415
+
416
+ # @!visibility private
417
+ def mark_for_return_key_of_first_responder
418
+ number = return_key_type_of_first_responder
419
+ mark_for_return_key_type(number)
420
+ end
421
+ end
422
+ end
423
+ end
424
+ end
@@ -0,0 +1,441 @@
1
+ module Calabash
2
+ module Cucumber
3
+ # @!visibility private
4
+ module Automator
5
+
6
+ require "calabash-cucumber/automator/automator"
7
+
8
+ # @!visibility private
9
+ class Instruments < Calabash::Cucumber::Automator::Automator
10
+
11
+ require "calabash-cucumber/uia"
12
+ include Calabash::Cucumber::UIA
13
+
14
+ require "calabash-cucumber/connection_helpers"
15
+ include Calabash::Cucumber::ConnectionHelpers
16
+
17
+ require "calabash-cucumber/query_helpers"
18
+ include Calabash::Cucumber::QueryHelpers
19
+
20
+ require "calabash-cucumber/status_bar_helpers"
21
+ include Calabash::Cucumber::StatusBarHelpers
22
+
23
+ require "calabash-cucumber/rotation_helpers"
24
+ include Calabash::Cucumber::RotationHelpers
25
+
26
+ require "calabash-cucumber/map"
27
+
28
+ # @!visibility private
29
+ UIA_STRATEGIES = [:preferences, :host, :shared_element]
30
+
31
+ attr_reader :run_loop
32
+
33
+ # @!visibility private
34
+ def self.expect_valid_init_args(args)
35
+ if args.nil?
36
+ raise(ArgumentError, "Expected non-nil argument for initializer")
37
+ end
38
+
39
+ if !args.is_a?(Array)
40
+ raise(ArgumentError, "Expected an array argument for initializer")
41
+ end
42
+
43
+ run_loop = args[0]
44
+
45
+ if run_loop.nil?
46
+ raise(ArgumentError,
47
+ %Q[Expected first element of args to be non-nil:
48
+
49
+ args = #{args}
50
+
51
+ ])
52
+ end
53
+
54
+ if args.count != 1
55
+ raise(ArgumentError,
56
+ %Q[Expected args to have exactly one element but found:
57
+
58
+ args = #{args}
59
+ ])
60
+ end
61
+
62
+ self.expect_valid_run_loop(run_loop)
63
+ end
64
+
65
+ # @!visibility private
66
+ def self.expect_valid_run_loop(run_loop)
67
+ if run_loop.nil?
68
+ raise(ArgumentError, "Expected run_loop arg to be non-nil")
69
+ end
70
+
71
+ if !run_loop.is_a?(Hash)
72
+ raise(ArgumentError, %Q[
73
+ Expected run_loop arg to be a hash, but found:
74
+
75
+ run_loop = #{run_loop} is_a => #{run_loop.class}
76
+
77
+ ])
78
+ end
79
+
80
+ automator = run_loop[:automator]
81
+ if automator && automator != :instruments
82
+ raise(ArgumentError, %Q[
83
+ Invalid :@automator. Expected :instruments but found:
84
+
85
+ #{automator}
86
+
87
+ in
88
+
89
+ #{run_loop}
90
+
91
+ ])
92
+ end
93
+
94
+ [:pid, :udid, :index, :log_file, :uia_strategy].each do |key|
95
+ if !run_loop[key]
96
+ raise(ArgumentError, %Q[
97
+ Expected run_loop to have a truthy value for :#{key} but found:
98
+
99
+ #{run_loop}
100
+
101
+ ])
102
+ end
103
+ end
104
+
105
+ strategy = run_loop[:uia_strategy]
106
+ if !self.valid_uia_strategy?(strategy)
107
+ raise(ArgumentError, %Q[
108
+ Expected '#{strategy}' to be one of these supported strategies:
109
+
110
+ #{UIA_STRATEGIES}
111
+
112
+ ])
113
+ end
114
+ true
115
+ end
116
+
117
+ # @!visibility private
118
+ def self.valid_uia_strategy?(strategy)
119
+ UIA_STRATEGIES.include?(strategy)
120
+ end
121
+
122
+ # @!visibility private
123
+ def initialize(*args)
124
+ Instruments.expect_valid_init_args(args)
125
+ @run_loop = args[0]
126
+ end
127
+
128
+ # @!visibility private
129
+ def name
130
+ :instruments
131
+ end
132
+
133
+ # @!visibility private
134
+ def stop
135
+ RunLoop.stop(run_loop)
136
+ end
137
+
138
+ # @!visibility private
139
+ def touch(options)
140
+ query_action(options, :uia_tap_offset)
141
+ end
142
+
143
+ # @!visibility private
144
+ def double_tap(options)
145
+ query_action(options, :uia_double_tap_offset)
146
+ end
147
+
148
+ # @!visibility private
149
+ def two_finger_tap(options)
150
+ query_action(options, :uia_two_finger_tap_offset)
151
+ end
152
+
153
+ # @!visibility private
154
+ def flick(options)
155
+ query_action(options) do |offset|
156
+ delta = {:offset => options[:delta] || {}}
157
+ uia_flick_offset(offset, point_from(offset, delta))
158
+ end
159
+ end
160
+
161
+ # @!visibility private
162
+ def touch_hold(options)
163
+ query_action(options) do |offset|
164
+ duration = options[:duration] || 3
165
+ uia_touch_hold_offset(duration, offset)
166
+ end
167
+ end
168
+
169
+ # @!visibility private
170
+ def swipe(options)
171
+ query_action(options, :uia_swipe_offset, options)
172
+ end
173
+
174
+ # @!visibility private
175
+ def pan(from, to, options={})
176
+ query_action(:query => from) do |from_offset|
177
+ query_action(:query => to) do |to_offset|
178
+ uia_pan_offset(from_offset, to_offset, options)
179
+ end
180
+ end
181
+ end
182
+
183
+ # @!visibility private
184
+ def pan_coordinates(from, to, options={})
185
+ uia_pan_offset(from, to, options)
186
+ [find_and_normalize("*")]
187
+ end
188
+
189
+ # @!visibility private
190
+ def pinch(in_out, options)
191
+ query_action(options) do |offset|
192
+ options[:duration] = options[:duration] || 0.5
193
+ uia_pinch_offset(in_out, offset, options)
194
+ end
195
+ end
196
+
197
+ # @!visibility private
198
+ def send_app_to_background(secs)
199
+ uia_send_app_to_background(secs)
200
+ end
201
+
202
+ # @!visibility private
203
+ def enter_text_with_keyboard(string, existing_text="")
204
+ uia_type_string(string, existing_text)
205
+ end
206
+
207
+ # @!visibility private
208
+ def fast_enter_text(text)
209
+ uia_set_responder_value(text)
210
+ end
211
+
212
+ # @!visibility private
213
+ # It is the caller's responsibility to ensure the keyboard is visible.
214
+ def enter_char_with_keyboard(char)
215
+ uia("uia.keyboard().typeString('#{char}')")
216
+ end
217
+
218
+ # @!visibility private
219
+ def char_for_keyboard_action(action_key)
220
+ SPECIAL_ACTION_CHARS[action_key]
221
+ end
222
+
223
+ # @!visibility private
224
+ # TODO Implement this in JavaScript?
225
+ # See the device_agent implementation of tap_keyboard_action_key and
226
+ # the tap_keyboard_delete_key of this class.
227
+ def tap_keyboard_action_key
228
+ code = char_for_keyboard_action("Return")
229
+ enter_char_with_keyboard(code)
230
+ end
231
+
232
+ # @!visibility private
233
+ def tap_keyboard_delete_key
234
+ js_tap_delete = %Q[(function() {
235
+ var deleteElement = uia.keyboard().elements().firstWithName('Delete');
236
+ if (deleteElement.isValid()) {
237
+ deleteElement.tap();
238
+ } else {
239
+ uia.keyboard().elements().firstWithName('delete').tap();
240
+ }
241
+ })();].gsub!($-0, "")
242
+
243
+ uia(js_tap_delete)
244
+ end
245
+
246
+ # @!visibility private
247
+ def dismiss_ipad_keyboard
248
+ js = %Q[#{query_uia_hide_keyboard_button}.tap()]
249
+ uia(js)
250
+ end
251
+
252
+ # @!visibility private
253
+ def rotate(direction)
254
+ current_orientation = status_bar_orientation.to_sym
255
+ result = rotate_with_uia(direction, current_orientation)
256
+ recalibrate_after_rotation
257
+ ap result if RunLoop::Environment.debug?
258
+ status_bar_orientation.to_sym
259
+ end
260
+
261
+ # @!visibility private
262
+ def rotate_home_button_to(position)
263
+ rotate_to_uia_orientation(position)
264
+ recalibrate_after_rotation
265
+ status_bar_orientation.to_sym
266
+ end
267
+
268
+ private
269
+
270
+ # @!visibility private
271
+ #
272
+ # Calls #point_from which applies any :offset supplied in the options.
273
+ def query_for_coordinates(options)
274
+ ui_query = options[:query]
275
+
276
+ first_element, orientation = first_element_for_query(ui_query)
277
+
278
+ if first_element.nil?
279
+ msg = %Q[
280
+ Could not find any views with query:
281
+
282
+ #{ui_query}
283
+
284
+ Try adjusting your query to return at least one view.
285
+
286
+ ]
287
+ Calabash::Cucumber::Map.new.screenshot_and_raise(msg)
288
+ else
289
+
290
+ normalize_rect_for_orientation!(orientation, first_element)
291
+
292
+ {
293
+ :coordinates => point_from(first_element, options),
294
+ :view => first_element
295
+ }
296
+ end
297
+ end
298
+
299
+ # @!visibility private
300
+ def first_element_for_query(ui_query)
301
+ # Will raise if response "outcome" is not SUCCESS
302
+ raw = Calabash::Cucumber::Map.raw_map(ui_query, :query)
303
+ results = raw["results"]
304
+ orientation = raw["status_bar_orientation"]
305
+
306
+ if results.empty?
307
+ return nil, nil
308
+ else
309
+ return results[0], orientation
310
+ end
311
+ end
312
+
313
+ # @!visibility private
314
+ # Data interface
315
+ # options[:query] or options[:offset]
316
+ def query_action(options, action=nil, *args, &block)
317
+ ui_query = options[:query]
318
+ offset = options[:offset]
319
+ if ui_query
320
+ res = find_and_normalize(ui_query)
321
+ return res if res.empty?
322
+ el = res.first
323
+ final_offset = point_from(el, options)
324
+ if block_given?
325
+ yield final_offset
326
+ else
327
+ self.send(action, final_offset, *args)
328
+ end
329
+ [el]
330
+ else
331
+ ##implies offset
332
+ if block_given?
333
+ yield offset
334
+ else
335
+ self.send(action, offset, *args)
336
+ end
337
+ end
338
+ end
339
+
340
+ # @!visibility private
341
+ def find_and_normalize(ui_query)
342
+ raw_result = Calabash::Cucumber::Map.raw_map(ui_query, :query)
343
+ orientation = raw_result["status_bar_orientation"]
344
+ res = raw_result["results"]
345
+
346
+ return res if res.empty?
347
+
348
+ first_res = res.first
349
+ normalize_rect_for_orientation!(orientation, first_res["rect"]) if first_res["rect"]
350
+
351
+ res
352
+ end
353
+
354
+ # @!visibility private
355
+ def normalize_rect_for_orientation!(orientation, rect)
356
+ orientation = orientation.to_sym
357
+ launcher = Calabash::Cucumber::Launcher.launcher
358
+ device = launcher.device
359
+
360
+ # Coordinate translations for orientation is handled in the server for iOS 8+
361
+ if device.ios_major_version.to_i >= 8
362
+ return
363
+ end
364
+
365
+ # We cannot use Device#screen_dimensions here because on iPads the height
366
+ # and width are the opposite of what we expect.
367
+ # @todo Move all coordinate/orientation translation into the server.
368
+ if device.ipad?
369
+ screen_size = { :width => 768, :height => 1024 }
370
+ elsif device.iphone_4in?
371
+ screen_size = { :width => 320, :height => 568 }
372
+ else
373
+ screen_size = { :width => 320, :height => 480 }
374
+ end
375
+
376
+ case orientation
377
+ when :right
378
+ cx = rect["center_x"]
379
+ rect["center_x"] = rect["center_y"]
380
+ rect["center_y"] = screen_size[:width] - cx
381
+ when :left
382
+ cx = rect["center_x"]
383
+ rect["center_x"] = screen_size[:height] - rect["center_y"]
384
+ rect["center_y"] = cx
385
+ when :up
386
+ cy = rect["center_y"]
387
+ cx = rect["center_x"]
388
+ rect["center_y"] = screen_size[:height] - cy
389
+ rect["center_x"] = screen_size[:width] - cx
390
+ else
391
+ # no-op by design.
392
+ end
393
+ end
394
+
395
+ # @!visibility private
396
+ def recalibrate_after_rotation
397
+ uia_query :window
398
+ end
399
+
400
+ # @!visibility private
401
+ def rotate_to_uia_orientation(orientation)
402
+ case orientation
403
+ when :down then key = :portrait
404
+ when :up then key = :upside_down
405
+ when :left then key = :landscape_right
406
+ when :right then key = :landscape_left
407
+ else
408
+ raise ArgumentError,
409
+ "Expected '#{orientation}' to be :left, :right, :up, or :down"
410
+ end
411
+ value = orientation_for_key(key)
412
+ cmd = "UIATarget.localTarget().setDeviceOrientation(#{value})"
413
+ uia(cmd)
414
+ end
415
+
416
+ # @!visibility private
417
+ def rotate_with_uia(direction, current_orientation)
418
+ key = orientation_key(direction, current_orientation)
419
+ value = orientation_for_key(key)
420
+ cmd = "UIATarget.localTarget().setDeviceOrientation(#{value})"
421
+ uia(cmd)
422
+ end
423
+
424
+ # @!visibility private
425
+ # Returns a query string for finding the iPad 'Hide keyboard' button.
426
+ def query_uia_hide_keyboard_button
427
+ "uia.keyboard().buttons()['Hide keyboard']"
428
+ end
429
+
430
+ # @!visibility private
431
+ #
432
+ # Don't change the single single quotes.
433
+ SPECIAL_ACTION_CHARS = {
434
+ "Delete" => '\b',
435
+ "Return" => '\n'
436
+ }
437
+ end
438
+ end
439
+ end
440
+ end
441
+