calabash-cucumber 0.19.2 → 0.20.0

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