calabash-cucumber 0.9.163.pre11 → 0.9.163

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ require 'fileutils'
2
+
1
3
  module Calabash
2
4
  module Cucumber
3
5
  module FailureHelpers
@@ -8,12 +10,12 @@ module Calabash
8
10
 
9
11
  @@screenshot_count ||= 0
10
12
  res = http({:method => :get, :path => 'screenshot'})
11
- prefix = prefix || ENV['SCREENSHOT_PATH'] || ""
13
+ prefix = prefix || ENV['SCREENSHOT_PATH'] || ''
12
14
  if name.nil?
13
- name = "screenshot"
15
+ name = 'screenshot'
14
16
  else
15
- if File.extname(name).downcase == ".png"
16
- name = name.split(".png")[0]
17
+ if File.extname(name).downcase == '.png'
18
+ name = name.split('.png')[0]
17
19
  end
18
20
  end
19
21
 
@@ -27,7 +29,8 @@ module Calabash
27
29
 
28
30
  def screenshot_embed(options={:prefix => nil, :name => nil, :label => nil})
29
31
  path = screenshot(options)
30
- embed(path, "image/png", options[:label] || File.basename(path))
32
+ filename = options[:label] || File.basename(path)
33
+ embed(path, 'image/png', filename)
31
34
  end
32
35
 
33
36
  def screenshot_and_raise(msg, options={:prefix => nil, :name => nil, :label => nil})
@@ -35,7 +38,7 @@ module Calabash
35
38
  raise(msg)
36
39
  end
37
40
 
38
- def fail(msg="Error. Check log for details.", options={:prefix => nil, :name => nil, :label => nil})
41
+ def fail(msg='Error. Check log for details.', options={:prefix => nil, :name => nil, :label => nil})
39
42
  screenshot_and_raise(msg, options)
40
43
  end
41
44
 
@@ -2,22 +2,22 @@ require 'calabash-cucumber/launcher'
2
2
  require 'calabash-cucumber/uia'
3
3
  require 'calabash-cucumber/actions/instruments_actions'
4
4
  require 'calabash-cucumber/actions/playback_actions'
5
+ require 'calabash-cucumber/environment_helpers'
6
+
7
+
5
8
 
6
9
  module Calabash
7
10
  module Cucumber
8
11
  module IOS7Operations
9
12
  include Calabash::Cucumber::UIA
13
+ include Calabash::Cucumber::EnvironmentHelpers
10
14
 
11
- def ios7?
12
- launcher = @calabash_launcher || Calabash::Cucumber::Launcher.launcher_if_used
13
- ENV['OS']=='ios7' || (launcher && launcher.device && launcher.device.ios7?)
14
- end
15
-
15
+ # todo deprecate the Calabash::Cucumber::IOS7Operations
16
16
 
17
-
18
- # Deprecated - abstracted into
19
- # actions/instruments_actions.rb - actions that can be performed under instruments
17
+ # <b>DEPRECATED</b>
20
18
  #
19
+ # abstracted into <tt>actions/instruments_actions.rb</tt> - actions that
20
+ # can be performed under instruments
21
21
  def touch_ios7(options)
22
22
  ui_query = options[:query]
23
23
  offset = options[:offset]
@@ -118,9 +118,6 @@ module Calabash
118
118
  [el_to_swipe]
119
119
  end
120
120
  end
121
-
122
-
123
-
124
121
  end
125
122
  end
126
123
  end
@@ -1,12 +1,12 @@
1
1
  require 'calabash-cucumber/core'
2
2
  require 'calabash-cucumber/tests_helpers'
3
3
  require 'calabash-cucumber/playback_helpers'
4
+ require 'calabash-cucumber/environment_helpers'
4
5
 
5
6
  module Calabash
6
7
  module Cucumber
7
8
  module KeyboardHelpers
8
9
  include Calabash::Cucumber::TestsHelpers
9
- include Calabash::Cucumber::PlaybackHelpers
10
10
 
11
11
  KEYPLANE_NAMES = {
12
12
  :small_letters => 'small-letters',
@@ -18,31 +18,228 @@ module Calabash
18
18
 
19
19
 
20
20
  UIA_SUPPORTED_CHARS = {
21
- 'Dictation' => nil,
22
- 'Shift' => nil,
23
- 'Delete' => '\b',
24
- 'International' => nil,
25
- 'More' => nil,
26
- 'Return' => '\n'
21
+ 'Delete' => '\b',
22
+ 'Return' => '\n'
23
+ # these are not supported yet and I am pretty sure that they
24
+ # cannot be touched by passing an escaped character and instead
25
+ # the must be found using UIAutomation calls. -jmoody
26
+ #'Dictation' => nil,
27
+ #'Shift' => nil,
28
+ #'International' => nil,
29
+ #'More' => nil,
27
30
  }
28
- #Possible values
29
- # 'Dictation'
30
- # 'Shift'
31
- # 'Delete'
32
- # 'International'
33
- # 'More'
34
- # 'Return'
35
- def keyboard_enter_char(chr, should_screenshot=true)
36
- #map(nil, :keyboard, load_playback_data("touch_done"), chr)
37
- if uia?
31
+
32
+
33
+ # returns a query string for detecting a keyboard
34
+ def _qstr_for_keyboard
35
+ "view:'UIKBKeyplaneView'"
36
+ end
37
+
38
+ # returns +true+ if a +docked+ keyboard is visible.
39
+ #
40
+ # a +docked+ keyboard is pinned to the bottom of the view.
41
+ #
42
+ # keyboards on the iPhone and iPod are +docked+.
43
+ def docked_keyboard_visible?
44
+ res = query(_qstr_for_keyboard).first
45
+ return false if res.nil?
46
+
47
+ return true if device_family_iphone?
48
+
49
+ # ipad
50
+ rect = res['rect']
51
+ o = status_bar_orientation.to_sym
52
+ case o
53
+ when :left then
54
+ rect['center_x'] == 592 and rect['center_y'] == 512
55
+ when :right then
56
+ rect['center_x'] == 176 and rect['center_y'] == 512
57
+ when :up then
58
+ rect['center_x'] == 384 and rect['center_y'] == 132
59
+ when :down then
60
+ rect['center_x'] == 384 and rect['center_y'] == 892
61
+ else
62
+ false
63
+ end
64
+
65
+ end
66
+
67
+ # returns +true+ if an +undocked+ keyboard is visible.
68
+ #
69
+ # a +undocked+ keyboard is floats in the middle of the view
70
+ #
71
+ # returns +false+ if the device is not an iPad; all keyboards on the
72
+ # iPhone and iPod are +docked+
73
+ def undocked_keyboard_visible?
74
+ return false if device_family_iphone?
75
+
76
+ res = query(_qstr_for_keyboard).first
77
+ return false if res.nil?
78
+
79
+ not docked_keyboard_visible?
80
+ end
81
+
82
+ # returns +true+ if a +split+ keyboard is visible.
83
+ #
84
+ # a +split+ keyboard is floats in the middle of the view and is split to
85
+ # allow faster thumb typing
86
+ #
87
+ # returns +false+ if the device is not an iPad; all keyboards on the
88
+ # iPhone and iPod are +docked+
89
+ def split_keyboard_visible?
90
+ return false if device_family_iphone?
91
+ query("view:'UIKBKeyView'").count > 0 and
92
+ element_does_not_exist(_qstr_for_keyboard)
93
+ end
94
+
95
+ # returns true if there is a visible keyboard
96
+ def keyboard_visible?
97
+ docked_keyboard_visible? or undocked_keyboard_visible? or split_keyboard_visible?
98
+ end
99
+
100
+ # waits for a keyboard to appear and once it does appear waits for 0.3
101
+ # seconds
102
+ #
103
+ # raises an error if no keyboard appears
104
+ def wait_for_keyboard(opts={})
105
+ default_opts = {:timeout_message => 'keyboard did not appear',
106
+ :post_timeout => 0.3}
107
+ opts = default_opts.merge(opts)
108
+ wait_for(opts) do
109
+ keyboard_visible?
110
+ end
111
+ end
112
+
113
+ # <b>DEPRECATED:</b> Use <tt>wait_for_keyboard</tt> instead.
114
+ def await_keyboard
115
+ _deprecated('0.9.163', "use 'wait_for_keyboard' instead", :warn)
116
+ wait_for_keyboard
117
+ end
118
+
119
+ # returns an array of possible ipad keyboard modes
120
+ def _ipad_keyboard_modes
121
+ [:docked, :undocked, :split]
122
+ end
123
+
124
+ # returns the keyboard +mode+
125
+ #
126
+ # keyboard is pinned to bottom of the view #=> :docked
127
+ # keyboard is floating in the middle of the view #=> :undocked
128
+ # keyboard is floating and split #=> :split
129
+ # no keyboard and :raise_on_no_visible_keyboard == +false+ #=> :unknown
130
+ #
131
+ # raises an error if the device is not an iPad
132
+ #
133
+ # raises an error if the <tt>:raise_on_no_visible_keyboard</tt> is +true+
134
+ # (default) and no keyboard is visible
135
+ #
136
+ # set <tt>:raise_on_no_visible_keyboard</tt> to +false+ to use in +wait+
137
+ # functions
138
+ def ipad_keyboard_mode(opts = {})
139
+ raise 'the keyboard mode does not exist on the iphone or ipod' if device_family_iphone?
140
+
141
+ default_opts = {:raise_on_no_visible_keyboard => true}
142
+ opts = default_opts.merge(opts)
143
+ if opts[:raise_on_no_visible_keyboard]
144
+ screenshot_and_raise 'there is no visible keyboard' unless keyboard_visible?
145
+ return :docked if docked_keyboard_visible?
146
+ return :undocked if undocked_keyboard_visible?
147
+ :split
148
+ else
149
+ return :docked if docked_keyboard_visible?
150
+ return :undocked if undocked_keyboard_visible?
151
+ return :split if split_keyboard_visible?
152
+ :unknown
153
+ end
154
+ end
155
+
156
+ # ensures that there is a keyboard to enter text
157
+ #
158
+ # IMPORTANT will always raise an error when the keyboard is split and
159
+ # there is no <tt>run_loop</tt> i.e. +UIAutomation+ is not available
160
+ #
161
+ # the default options are
162
+ # :screenshot +true+ raise with a screenshot
163
+ # :skip +false+ skip any checking (a nop) - used when iterating over
164
+ # keyplanes for keys
165
+ def _ensure_can_enter_text(opts={})
166
+ default_opts = {:screenshot => true,
167
+ :skip => false}
168
+ opts = default_opts.merge(opts)
169
+ return if opts[:skip]
170
+
171
+ screenshot = opts[:screenshot]
172
+ unless keyboard_visible?
173
+ msg = 'no visible keyboard'
174
+ if screenshot
175
+ screenshot_and_raise msg
176
+ else
177
+ raise msg
178
+ end
179
+ end
180
+
181
+ if split_keyboard_visible? and uia_not_available?
182
+ msg = 'cannot type on a split keyboard without launching with Instruments'
183
+ if screenshot
184
+ screenshot_and_raise msg
185
+ else
186
+ raise msg
187
+ end
188
+ end
189
+ end
190
+
191
+ # use keyboard to enter +chr+
192
+ #
193
+ # IMPORTANT: use the <tt>POST_ENTER_KEYBOARD</tt> environmental variable
194
+ # to slow down the typing; adds a wait after each character is touched.
195
+ # this can fix problems where the typing is too fast and characters are
196
+ # skipped.
197
+ #
198
+ # there are several special 'characters', some of which do not appear on all
199
+ # keyboards:
200
+ # * 'Delete'
201
+ # * 'Return'
202
+ #
203
+ # raises error if there is no visible keyboard or the keyboard is not
204
+ # supported
205
+ #
206
+ # use the +should_screenshot+ to control whether or not to raise an error
207
+ # if +chr+ is not found
208
+ def keyboard_enter_char(chr, opts={})
209
+ unless opts.is_a?(Hash)
210
+ msg = "you should no longer pass a boolean as the second arg; pass {:should_screenshot => '#{opts}'} hash instead"
211
+ _deprecated('0.9.163', msg, :warn)
212
+ opts = {:should_screenshot => opts}
213
+ end
214
+
215
+ default_opts = {:should_screenshot => true,
216
+ # introduce a small wait to avoid skipping characters
217
+ # keep this as short as possible
218
+ :wait_after_char => (ENV['POST_ENTER_KEYBOARD'] || 0.05).to_f}
219
+
220
+ opts = default_opts.merge(opts)
221
+
222
+ should_screenshot = opts[:should_screenshot]
223
+ _ensure_can_enter_text({:screenshot => should_screenshot,
224
+ :skip => (not should_screenshot)})
225
+
226
+ if uia_available?
38
227
  if chr.length == 1
39
228
  uia_type_string chr
40
229
  else
41
230
  code = UIA_SUPPORTED_CHARS[chr]
42
- if code
43
- uia_type_string code
231
+
232
+ unless code
233
+ raise "Char #{chr} is not yet supported in when typing with Instruments"
234
+ end
235
+
236
+ # on iOS 6, the char code is _not_ \b
237
+ #
238
+ # as an aside, on iOS 7, the same approach (tap the 'Delete' key) also works
239
+ if ios6? and code.eql?(UIA_SUPPORTED_CHARS['Delete'])
240
+ uia("uia.keyboard().keys().firstWithName('Delete').tap()")
44
241
  else
45
- raise "Char #{chr} is not yet supported in iOS7"
242
+ uia_type_string(code)
46
243
  end
47
244
  end
48
245
  res = {'results' => []}
@@ -66,56 +263,116 @@ module Calabash
66
263
  sleep(w)
67
264
  end
68
265
  end
266
+ pause = opts[:wait_after_char]
267
+ sleep(pause) if pause > 0
69
268
  res['results']
70
269
  end
71
270
 
72
- def done
73
- if uia?
271
+ # uses the keyboard to enter +text+
272
+ #
273
+ # raises an error if the text cannot be entered
274
+ def keyboard_enter_text(text)
275
+ _ensure_can_enter_text
276
+ if uia_available?
277
+ uia_type_string(text)
278
+ else
279
+ text.each_char do |ch|
280
+ begin
281
+ keyboard_enter_char(ch, {:should_screenshot => false})
282
+ rescue
283
+ _search_keyplanes_and_enter_char(ch)
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+
290
+ # touches the keyboard +action+ key
291
+ #
292
+ # the +action+ key depends on the keyboard. some examples include:
293
+ # * Return
294
+ # * Next
295
+ # * Go
296
+ # * Join
297
+ # * Search
298
+ #
299
+ # not all keyboards have an +action+ key
300
+ # raises an error if the key cannot be entered
301
+ def tap_keyboard_action_key
302
+ if uia_available?
74
303
  uia_type_string '\n'
75
304
  else
76
305
  keyboard_enter_char 'Return'
77
306
  end
78
-
79
307
  end
80
308
 
309
+ # touches the keyboard +action+ key
310
+ #
311
+ # the +action+ key depends on the keyboard.
312
+ #
313
+ # some examples include:
314
+ # * Return
315
+ # * Next
316
+ # * Go
317
+ # * Join
318
+ # * Search
319
+ #
320
+ # not all keyboards have an +action+ key
321
+ # raises an error if the key cannot be entered
322
+ def done
323
+ tap_keyboard_action_key
324
+ end
81
325
 
82
- def current_keyplane
326
+ # returns the current keyplane
327
+ def _current_keyplane
83
328
  kp_arr = _do_keyplane(
84
- lambda { query("view:'UIKBKeyplaneView'", "keyplane", "componentName") },
85
- lambda { query("view:'UIKBKeyplaneView'", "keyplane", "name") })
329
+ lambda { query("view:'UIKBKeyplaneView'", 'keyplane', 'componentName') },
330
+ lambda { query("view:'UIKBKeyplaneView'", 'keyplane', 'name') })
86
331
  kp_arr.first.downcase
87
332
  end
88
333
 
89
- def search_keyplanes_and_enter_char(chr, visited=Set.new)
90
- cur_kp = current_keyplane
334
+ # searches the available keyplanes for +chr+ and if it is found, types it
335
+ #
336
+ # this is a recursive function
337
+ #
338
+ # IMPORTANT: use the <tt>KEYPLANE_SEARCH_STEP_PAUSE</tt> variable to
339
+ # control how quickly the next keyplane is searched. increase this value
340
+ # if you encounter problems with missed keystrokes.
341
+ #
342
+ # raises an error if the +chr+ cannot be found
343
+ def _search_keyplanes_and_enter_char(chr, visited=Set.new)
344
+ cur_kp = _current_keyplane
91
345
  begin
92
- keyboard_enter_char(chr, false)
346
+ keyboard_enter_char(chr, {:should_screenshot => false})
93
347
  return true #found
94
348
  rescue
349
+ pause = (ENV['KEYPLANE_SEARCH_STEP_PAUSE'] || 0.2).to_f
350
+ sleep (pause) if pause > 0
351
+
95
352
  visited.add(cur_kp)
96
353
 
97
354
  #figure out keyplane alternates
98
355
  props = _do_keyplane(
99
- lambda { query("view:'UIKBKeyplaneView'", "keyplane", "properties") },
100
- lambda { query("view:'UIKBKeyplaneView'", "keyplane", "attributes", "dict") }
356
+ lambda { query("view:'UIKBKeyplaneView'", 'keyplane', 'properties') },
357
+ lambda { query("view:'UIKBKeyplaneView'", 'keyplane', 'attributes', 'dict') }
101
358
  ).first
102
359
 
103
360
  known = KEYPLANE_NAMES.values
104
361
 
105
362
  found = false
106
- keyplane_selection_keys = ["shift", "more"]
363
+ keyplane_selection_keys = ['shift', 'more']
107
364
  keyplane_selection_keys.each do |key|
365
+ sleep (pause) if pause > 0
108
366
  plane = props["#{key}-alternate"]
109
- if (known.member?(plane) and
110
- not visited.member?(plane))
111
- keyboard_enter_char(key.capitalize, false)
112
- found = search_keyplanes_and_enter_char(chr, visited)
367
+ if known.member?(plane) and (not visited.member?(plane))
368
+ keyboard_enter_char(key.capitalize, {:should_screenshot => false})
369
+ found = _search_keyplanes_and_enter_char(chr, visited)
113
370
  return true if found
114
371
  #not found => try with other keyplane selection key
115
372
  keyplane_selection_keys.delete(key)
116
373
  other_key = keyplane_selection_keys.last
117
- keyboard_enter_char(other_key.capitalize, false)
118
- found = search_keyplanes_and_enter_char(chr, visited)
374
+ keyboard_enter_char(other_key.capitalize, {:should_screenshot => false})
375
+ found = _search_keyplanes_and_enter_char(chr, visited)
119
376
  return true if found
120
377
  end
121
378
  end
@@ -123,48 +380,352 @@ module Calabash
123
380
  end
124
381
  end
125
382
 
126
- def await_keyboard
127
- #deprecated inconsistent wait naming
128
- # use wait_for_keyboard
129
- wait_for_keyboard
383
+ # process a keyplane
384
+ #
385
+ # raises an error if there is not visible keyplane
386
+ def _do_keyplane(kbtree_proc, keyplane_proc)
387
+ desc = query("view:'UIKBKeyplaneView'", 'keyplane')
388
+ fail('No keyplane (UIKBKeyplaneView keyplane)') if desc.empty?
389
+ fail('Several keyplanes (UIKBKeyplaneView keyplane)') if desc.count > 1
390
+ kp_desc = desc.first
391
+ if /^<UIKBTree/.match(kp_desc)
392
+ #ios5+
393
+ kbtree_proc.call
394
+ elsif /^<UIKBKeyplane/.match(kp_desc)
395
+ #ios4
396
+ keyplane_proc.call
397
+ end
130
398
  end
131
399
 
132
- #noinspection RubyLiteralArrayInspection
133
- def wait_for_keyboard
134
- wait_for_elements_exist(["view:'UIKBKeyplaneView'"], :timeout_message => 'No visible keyboard')
135
- sleep(0.5)
400
+ # returns a query string for finding the iPad 'Hide keyboard' button
401
+ def _query_uia_hide_keyboard_button
402
+ "uia.keyboard().buttons()['Hide keyboard']"
136
403
  end
137
404
 
138
- def keyboard_enter_text(text)
139
- wait_for_keyboard if element_does_not_exist("view:'UIKBKeyplaneView'")
405
+ # dismisses a iPad keyboard by touching the 'Hide keyboard' button and waits
406
+ # for the keyboard to disappear
407
+ #
408
+ # raises an error if the device is not an iPad. the dismiss keyboard
409
+ # key does not exist on the iPhone or iPod
410
+ def dismiss_ipad_keyboard
411
+ screenshot_and_raise 'cannot dismiss keyboard on iphone' if device_family_iphone?
140
412
 
141
- if uia?
142
- uia_type_string(text)
413
+ if uia_available?
414
+ send_uia_command({:command => "#{_query_uia_hide_keyboard_button}.tap()"})
143
415
  else
144
- text.each_char do |ch|
145
- begin
146
- keyboard_enter_char(ch, false)
147
- rescue
148
- search_keyplanes_and_enter_char(ch)
149
- end
416
+ touch(_query_for_keyboard_mode_key)
417
+ end
418
+
419
+ opts = {:timeout_message => 'keyboard did not disappear'}
420
+ wait_for(opts) do
421
+ not keyboard_visible?
422
+ end
423
+ end
424
+
425
+ # returns the activation point of the iPad keyboard +mode+ key.
426
+ #
427
+ # the +mode+ key is also known as the <tt>Hide keyboard</tt> key.
428
+ #
429
+ # raises an error when
430
+ # * the device is not an iPad
431
+ # * the app was not launched with instruments i.e. there is no <tt>run_loop</tt>
432
+ def _point_for_ipad_keyboard_mode_key
433
+ raise 'the keyboard mode does not exist on the on the iphone' if device_family_iphone?
434
+ raise 'cannot detect keyboard mode key without launching with instruments' unless uia_available?
435
+ res = send_uia_command({:command => "#{_query_uia_hide_keyboard_button}.rect()"})
436
+ origin = res['value']['origin']
437
+ {:x => origin['x'], :y => origin['y']}
438
+
439
+ # this did not work.
440
+ #size = res['value']['size']
441
+ #{:x => (origin['x'] + (size['width']/2)), :y => (origin['y'] + (size['height']/2))}
442
+ end
443
+
444
+
445
+ # returns a query string for touching one of the options that appears when
446
+ # the iPad +mode+ key is touched and held.
447
+ #
448
+ # the +mode+ key is also know as the <tt>Hide keyboard</tt> key.
449
+ #
450
+ # valid arguments are:
451
+ # top_or_bottom :top | :bottom
452
+ # mode :docked | :undocked | :skipped
453
+ #
454
+ # use <tt>_point_for_keyboard_mode_key</tt> if there is a <tt>run_loop</tt>
455
+ # available
456
+ #
457
+ # raises an error when
458
+ # * the device is not an iPad
459
+ # * the app was launched with Instruments i.e. there is a <tt>run_loop</tt>
460
+ # * it is passed invalid arguments
461
+ def _query_for_touch_for_keyboard_mode_option(top_or_bottom, mode)
462
+ raise 'the keyboard mode does not exist on the iphone' if device_family_iphone?
463
+
464
+ if uia_available?
465
+ raise "UIA is available, use '_point_for_keyboard_mode_key' instead"
466
+ end
467
+
468
+ valid = [:top, :bottom]
469
+ unless valid.include? top_or_bottom
470
+ raise "expected '#{top_or_bottom}' to be one of '#{valid}'"
471
+ end
472
+
473
+ valid = [:split, :undocked, :docked]
474
+ unless valid.include? mode
475
+ raise "expected '#{mode}' to be one of '#{valid}'"
476
+ end
477
+
478
+ hash = {:split => {:top => 'Merge',
479
+ :bottom => 'Dock and Merge'},
480
+ :undocked => {:top => 'Dock',
481
+ :bottom => 'Split'},
482
+ :docked => {:top => 'Undock',
483
+ :bottom => 'Split'}}
484
+ mark = hash[mode][top_or_bottom]
485
+ "label marked:'#{mark}'"
486
+ end
487
+
488
+ # returns a query for touching the iPad keyboard +mode+ key.
489
+ #
490
+ # the +mode+ key is also know as the <tt>Hide keyboard</tt> key.
491
+ #
492
+ # use <tt>_point_for_keyboard_mode_key</tt> if there is a <tt>run_loop</tt>
493
+ # available
494
+ #
495
+ # raises an error when
496
+ # * the device is not an iPad
497
+ # * the app was launched with Instruments i.e. there is a <tt>run_loop</tt>
498
+ def _query_for_keyboard_mode_key
499
+ raise 'cannot detect keyboard mode key on iphone' if device_family_iphone?
500
+ if uia_available?
501
+ raise "UIA is available, use '_point_for_keyboard_mode_key' instead"
502
+ end
503
+ qstr = "view:'UIKBKeyView'"
504
+ idx = query(qstr).count - 1
505
+ "#{qstr} index:#{idx}"
506
+ end
507
+
508
+ # touches the bottom option on the popup dialog that is presented when the
509
+ # the iPad keyboard +mode+ key is touched and held.
510
+ #
511
+ # the +mode+ key is also know as the <tt>Hide keyboard</tt> key.
512
+ #
513
+ # the +mode+ key allows the user to undock, dock, or split the keyboard.
514
+ def _touch_bottom_keyboard_mode_row
515
+ mode = ipad_keyboard_mode
516
+ if uia_available?
517
+ start_pt = _point_for_ipad_keyboard_mode_key
518
+ # there are 10 pt btw the key and the popup and the row is 50 pt
519
+ y_offset = 10 + 25
520
+ end_pt = {:x => (start_pt[:x] - 40), :y => (start_pt[:y] - y_offset)}
521
+ uia_pan_offset(start_pt, end_pt, {})
522
+ else
523
+ pan(_query_for_keyboard_mode_key, nil, {:duration => 1.0})
524
+ touch(_query_for_touch_for_keyboard_mode_option(:bottom, mode))
525
+ sleep(0.5)
526
+ end
527
+ 2.times { sleep(0.5) }
528
+ end
529
+
530
+ # touches the top option on the popup dialog that is presented when the
531
+ # the iPad keyboard +mode+ key is touched and held.
532
+ #
533
+ # the +mode+ key is also know as the <tt>Hide keyboard</tt> key.
534
+ #
535
+ # the +mode+ key allows the user to undock, dock, or split the keyboard.
536
+ def _touch_top_keyboard_mode_row
537
+ mode = ipad_keyboard_mode
538
+ if uia_available?
539
+ start_pt = _point_for_ipad_keyboard_mode_key
540
+ # there are 10 pt btw the key and the popup and each row is 50 pt
541
+ # NB: no amount of offsetting seems to allow touching the top row
542
+ # when the keyboard is split
543
+ y_offset = 10 + 50 + 25
544
+ end_pt = {:x => (start_pt[:x] - 40), :y => (start_pt[:y] - y_offset)}
545
+ uia_pan_offset(start_pt, end_pt, {:duration => 1.0})
546
+ else
547
+ pan(_query_for_keyboard_mode_key, nil, {})
548
+ touch(_query_for_touch_for_keyboard_mode_option(:top, mode))
549
+ sleep(0.5)
550
+ end
551
+ 2.times { sleep(0.5) }
552
+ end
553
+
554
+ # ensures that the iPad keyboard is +docked+
555
+ #
556
+ # +docked+ means the keyboard is pinned to bottom of the view
557
+ #
558
+ # if the device is not an iPad, this is behaves like a call to
559
+ # <tt>wait_for_keyboard</tt>
560
+ #
561
+ # raises an error when
562
+ # * there is no visible keyboard or
563
+ # * the +docked+ keyboard cannot be achieved
564
+ def ensure_docked_keyboard
565
+ wait_for_keyboard
566
+
567
+ return if device_family_iphone?
568
+
569
+ mode = ipad_keyboard_mode
570
+ case mode
571
+ when :split then
572
+ _touch_bottom_keyboard_mode_row
573
+ when :undocked then
574
+ _touch_top_keyboard_mode_row
575
+ when :docked then
576
+ # already docked
577
+ else
578
+ screenshot_and_raise "expected '#{mode}' to be one of #{_ipad_keyboard_modes}"
579
+ end
580
+
581
+ begin
582
+ wait_for({:post_timeout => 1.0}) do
583
+ docked_keyboard_visible?
150
584
  end
585
+ rescue
586
+ mode = ipad_keyboard_mode
587
+ o = status_bar_orientation
588
+ screenshot_and_raise "expected keyboard to be ':docked' but found '#{mode}' in orientation '#{o}'"
151
589
  end
590
+ end
591
+
592
+
593
+ # ensures that the iPad keyboard is +undocked+
594
+ #
595
+ # +undocked+ means the keyboard is floating in the middle of the view
596
+ #
597
+ # if the device is not an iPad, this is behaves like a call to
598
+ # <tt>wait_for_keyboard</tt>
599
+ #
600
+ # raises an error when
601
+ # * there is no visible keyboard or
602
+ # * the an +undocked+ keyboard cannot be achieved
603
+ def ensure_undocked_keyboard
604
+ wait_for_keyboard()
152
605
 
606
+ return if device_family_iphone?
607
+
608
+ mode = ipad_keyboard_mode
609
+ case mode
610
+ when :split then
611
+ # keep these condition separate because even though they do the same
612
+ # thing, the else condition is a hack
613
+ if ios5?
614
+ # iOS 5 has no 'Merge' feature in split keyboard, so dock first then
615
+ # undock from docked mode
616
+ _touch_bottom_keyboard_mode_row
617
+ _wait_for_keyboard_in_mode(:docked)
618
+ else
619
+ # in iOS > 5, it seems to be impossible consistently touch the
620
+ # the top keyboard mode popup button, so we punt
621
+ _touch_bottom_keyboard_mode_row
622
+ _wait_for_keyboard_in_mode(:docked)
623
+ end
624
+ _touch_top_keyboard_mode_row
625
+ when :undocked then
626
+ # already undocked
627
+ when :docked then
628
+ _touch_top_keyboard_mode_row
629
+ else
630
+ screenshot_and_raise "expected '#{mode}' to be one of #{_ipad_keyboard_modes}"
631
+ end
153
632
 
633
+ _wait_for_keyboard_in_mode(:undocked)
154
634
  end
155
635
 
156
636
 
157
- def _do_keyplane(kbtree_proc, keyplane_proc)
158
- desc = query("view:'UIKBKeyplaneView'", "keyplane")
159
- fail("No keyplane (UIKBKeyplaneView keyplane)") if desc.empty?
160
- fail("Several keyplanes (UIKBKeyplaneView keyplane)") if desc.count > 1
161
- kp_desc = desc.first
162
- if /^<UIKBTree/.match(kp_desc)
163
- #ios5+
164
- kbtree_proc.call
165
- elsif /^<UIKBKeyplane/.match(kp_desc)
166
- #ios4
167
- keyplane_proc.call
637
+ # ensures that the iPad keyboard is +split+
638
+ #
639
+ # +split+ means the keyboard is floating in the middle of the view and is
640
+ # split into two sections to enable faster thumb typing.
641
+ #
642
+ # if the device is not an iPad, this is behaves like a call to
643
+ # <tt>wait_for_keyboard</tt>
644
+ #
645
+ # raises an error when
646
+ # * there is no visible keyboard or
647
+ # * the an +undocked+ keyboard cannot be achieved
648
+ def ensure_split_keyboard
649
+ wait_for_keyboard
650
+
651
+ return if device_family_iphone?
652
+
653
+ mode = ipad_keyboard_mode
654
+ case mode
655
+ when :split then
656
+ # already split
657
+ when :undocked then
658
+ _touch_bottom_keyboard_mode_row
659
+ when :docked then
660
+ _touch_bottom_keyboard_mode_row
661
+ else
662
+ screenshot_and_raise "expected '#{mode}' to be one of #{_ipad_keyboard_modes}"
663
+ end
664
+
665
+ _wait_for_keyboard_in_mode(:split)
666
+ end
667
+
668
+ def _wait_for_keyboard_in_mode(mode, opts={})
669
+ default_opts = {:post_timeout => 1.0}
670
+ opts = default_opts.merge(opts)
671
+ begin
672
+ wait_for(opts) do
673
+ case mode
674
+ when :split then
675
+ split_keyboard_visible?
676
+ when :undocked
677
+ undocked_keyboard_visible?
678
+ when :docked
679
+ docked_keyboard_visible?
680
+ else
681
+ screenshot_and_raise "expected '#{mode}' to be one of #{_ipad_keyboard_modes}"
682
+ end
683
+ end
684
+ rescue
685
+ actual = ipad_keyboard_mode
686
+ o = status_bar_orientation
687
+ screenshot_and_raise "expected keyboard to be '#{mode}' but found '#{actual}' in orientation '#{o}'"
688
+ end
689
+ end
690
+
691
+ # used for detecting keyboards that are not normally visible to calabash
692
+ # e.g. the keyboard on +'z'+
693
+ #
694
+ # IMPORTANT this should only be used when the app does not respond to
695
+ # <tt>keyboard_visible?</tt>
696
+ #
697
+ # raises an error if the there is no <tt>run_loop</tt>
698
+ def uia_keyboard_visible?
699
+ unless uia_available?
700
+ screenshot_and_raise 'only available if there is a run_loop i.e. the app was launched with Instruments'
701
+ end
702
+ # TODO refactor keyboard detection to use uia() function conventions (instead of UIATarget...)
703
+ res = uia('UIATarget.localTarget().frontMostApp().keyboard()')['value']
704
+ not res.eql?(':nil')
705
+ end
706
+
707
+ # waits for a keyboard that is not normally visible to calabash
708
+ # e.g. the keyboard on +MFMailComposeViewController+
709
+ #
710
+ # IMPORTANT this should only be used when the app does not respond to
711
+ # <tt>keyboard_visible?</tt>
712
+ #
713
+ # raises an error if the there is no <tt>run_loop</tt>
714
+ def uia_wait_for_keyboard(opts={})
715
+ unless uia_available?
716
+ screenshot_and_raise 'only available if there is a run_loop i.e. the app was launched with Instruments'
717
+ end
718
+ default_opts = {:timeout => 10,
719
+ :retry_frequency => 0.1,
720
+ :post_timeout => 0.5}
721
+ opts = default_opts.merge(opts)
722
+ unless opts[:timeout_message]
723
+ msg = "waited for '#{opts[:timeout]}' for keyboard"
724
+ opts[:timeout_message] = msg
725
+ end
726
+
727
+ wait_for(opts) do
728
+ uia_keyboard_visible?
168
729
  end
169
730
  end
170
731