Module: Calabash::Cucumber::KeyboardHelpers

Includes:
Logging, TestsHelpers
Included in:
Operations
Defined in:
lib/calabash-cucumber/keyboard_helpers.rb

Overview

Note:

We have an exhaustive set of keyboard related test.s The API is reasonably stable. We are fighting against known bugs in Apple’s UIAutomation. You should only need to fall back to the examples below in unusual situations.

Collection of methods for interacting with the keyboard.

We’ve gone to great lengths to provide the fastest keyboard entry possible.

If you are having trouble with skipped or are receiving JSON octet errors when typing, you might be able to resolve the problems by slowing down the rate of typing.

Example: Use keyboard_enter_char + :wait_after_char.

“‘ str.each_char do |char|

# defaults to 0.05 seconds
keyboard_enter_char(char, `{wait_after_char:0.5}`)

end “‘

Example: Use keyboard_enter_char + POST_ENTER_KEYBOARD

“‘ $ POST_ENTER_KEYBOARD=0.1 bundle exec cucumber str.each_char do |char|

# defaults to 0.05 seconds
keyboard_enter_char(char)

end “‘

Instance Method Summary collapse

Methods included from Logging

#calabash_info, #calabash_warn

Methods included from TestsHelpers

#check_element_does_not_exist, #check_element_exists, #check_view_with_mark_exists, #classes, #each_cell, #element_does_not_exist, #element_exists, #view_with_mark_exists

Methods included from FailureHelpers

#fail, #screenshot, #screenshot_and_raise, #screenshot_embed

Instance Method Details

#_touch_top_keyboard_mode_rowObject

Touches the top option on the popup dialog that is presented when the the iPad keyboard mode key is touched and held.

The ‘mode` key is also know as the ’Hide keyboard’ key.

The ‘mode` key allows the user to undock, dock, or split the keyboard.



647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 647

def _touch_top_keyboard_mode_row
  mode = ipad_keyboard_mode
  if uia_available?
    start_pt = _point_for_ipad_keyboard_mode_key
    # there are 10 pt btw the key and the popup and each row is 50 pt
    # NB: no amount of offsetting seems to allow touching the top row
    #     when the keyboard is split
    y_offset = 10 + 50 + 25
    end_pt = {:x => (start_pt[:x] - 40), :y => (start_pt[:y] - y_offset)}
    uia_pan_offset(start_pt, end_pt, {:duration => 1.0})
  else
    pan(_query_for_keyboard_mode_key, nil, {})
    touch(_query_for_touch_for_keyboard_mode_option(:top, mode))
    sleep(0.5)
  end
  2.times { sleep(0.5) }
end

#await_keyboardObject

Deprecated.

0.9.163 replaced with ‘wait_for_keyboard`

See Also:



164
165
166
167
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 164

def await_keyboard
  _deprecated('0.9.163', "use 'wait_for_keyboard' instead", :warn)
  wait_for_keyboard
end

#dismiss_ipad_keyboardObject

Note:

the dismiss keyboard key does not exist on the iPhone or iPod

Dismisses a iPad keyboard by touching the ‘Hide keyboard’ button and waits for the keyboard to disappear.

Raises:

  • (RuntimeError)

    if the device is not an iPad



516
517
518
519
520
521
522
523
524
525
526
527
528
529
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 516

def dismiss_ipad_keyboard
  screenshot_and_raise 'cannot dismiss keyboard on iphone' if device_family_iphone?

  if uia_available?
    send_uia_command({:command =>  "#{_query_uia_hide_keyboard_button}.tap()"})
  else
    touch(_query_for_keyboard_mode_key)
  end

  opts = {:timeout_message => 'keyboard did not disappear'}
  wait_for(opts) do
    not keyboard_visible?
  end
end

#docked_keyboard_visible?Boolean

Returns true if a docked keyboard is visible.

A docked keyboard is pinned to the bottom of the view.

Keyboards on the iPhone and iPod are docked.

Returns:

  • (Boolean)

    if a keyboard is visible and docked.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 81

def docked_keyboard_visible?
  res = query(_qstr_for_keyboard).first
  return false if res.nil?

  return true if device_family_iphone?

  # ipad
  rect = res['rect']
  o = status_bar_orientation.to_sym
  case o
    when :left then
      rect['center_x'] == 592 and rect['center_y'] == 512
    when :right then
      rect['center_x'] == 176 and rect['center_y'] == 512
    when :up then
      rect['center_x'] == 384 and rect['center_y'] == 132
    when :down then
      rect['center_x'] == 384 and rect['center_y'] == 892
    else
      false
  end

end

#doneObject

Deprecated.

0.10.0 replaced with ‘tap_keyboard_action_key`

Note:

Not all keyboards have an action key. For example, numeric keyboards do not have an action key.

Touches the keyboard action key.

The action key depends on the keyboard. Some examples include:

  • Return

  • Next

  • Go

  • Join

  • Search

Raises:

  • (RuntimeError)

    if the text cannot be typed.

See Also:



418
419
420
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 418

def done
  tap_keyboard_action_key
end

#ensure_docked_keyboardObject

Ensures that the iPad keyboard is docked.

Docked means the keyboard is pinned to bottom of the view.

If the device is not an iPad, this is behaves like a call to ‘wait_for_keyboard`.

Raises:

  • (RuntimeError)

    if there is no visible keyboard

  • (RuntimeError)

    a docked keyboard was not achieved



674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 674

def ensure_docked_keyboard
  wait_for_keyboard

  return if device_family_iphone?

  mode = ipad_keyboard_mode
  case mode
    when :split then
      _touch_bottom_keyboard_mode_row
    when :undocked then
      _touch_top_keyboard_mode_row
    when :docked then
      # already docked
    else
    screenshot_and_raise "expected '#{mode}' to be one of #{_ipad_keyboard_modes}"
  end

  begin
    wait_for({:post_timeout => 1.0}) do
      docked_keyboard_visible?
    end
  rescue
    mode = ipad_keyboard_mode
    o = status_bar_orientation
    screenshot_and_raise "expected keyboard to be ':docked' but found '#{mode}' in orientation '#{o}'"
  end
end

#ensure_split_keyboardObject

Ensures that the iPad keyboard is split.

Split means the keyboard is floating in the middle of the view and is split into two sections to enable faster thumb typing.

If the device is not an iPad, this is behaves like a call to ‘wait_for_keyboard`.

If the device is not an iPad, this is behaves like a call to ‘wait_for_keyboard`.

Raises:

  • (RuntimeError)

    if there is no visible keyboard

  • (RuntimeError)

    a split keyboard was not achieved



762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 762

def ensure_split_keyboard
  wait_for_keyboard

  return if device_family_iphone?

  mode = ipad_keyboard_mode
  case mode
    when :split then
      # already split
    when :undocked then
      _touch_bottom_keyboard_mode_row
    when :docked then
      _touch_bottom_keyboard_mode_row
    else
      screenshot_and_raise "expected '#{mode}' to be one of #{_ipad_keyboard_modes}"
  end

  _wait_for_keyboard_in_mode(:split)
end

#ensure_undocked_keyboardObject

Ensures that the iPad keyboard is undocked.

Undocked means the keyboard is floating in the middle of the view.

If the device is not an iPad, this is behaves like a call to ‘wait_for_keyboard`.

If the device is not an iPad, this is behaves like a call to ‘wait_for_keyboard`.

Raises:

  • (RuntimeError)

    if there is no visible keyboard

  • (RuntimeError)

    an undocked keyboard was not achieved



715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 715

def ensure_undocked_keyboard
  wait_for_keyboard()

  return if device_family_iphone?

  mode = ipad_keyboard_mode
  case mode
    when :split then
      # keep these condition separate because even though they do the same
      # thing, the else condition is a hack
      if ios5?
        # iOS 5 has no 'Merge' feature in split keyboard, so dock first then
        # undock from docked mode
        _touch_bottom_keyboard_mode_row
        _wait_for_keyboard_in_mode(:docked)
      else
        # in iOS > 5, it seems to be impossible consistently touch the
        # the top keyboard mode popup button, so we punt
        _touch_bottom_keyboard_mode_row
        _wait_for_keyboard_in_mode(:docked)
      end
      _touch_top_keyboard_mode_row
    when :undocked then
      # already undocked
    when :docked then
      _touch_top_keyboard_mode_row
    else
      screenshot_and_raise "expected '#{mode}' to be one of #{_ipad_keyboard_modes}"
  end

  _wait_for_keyboard_in_mode(:undocked)
end

#ipad_keyboard_mode(opts = {}) ⇒ Symbol

Returns the keyboard mode.

“‘

              keyboard is pinned to bottom of the view #=> :docked
        keyboard is floating in the middle of the view #=> :undocked
                        keyboard is floating and split #=> :split
no keyboard and :raise_on_no_visible_keyboard == false #=> :unknown

“‘

Examples:

How to use in a wait_* function.

wait_for do
 ipad_keyboard_mode({:raise_on_no_visible_keyboard => false}) == :split
end

Parameters:

  • opts (Hash) (defaults to: {})

    controls the runtime behavior.

Options Hash (opts):

  • :raise_on_no_visible_keyboard (Boolean) — default: true

    set to false if you don’t want to raise an error.

Returns:

  • (Symbol)

    Returns one of ‘| :undocked | :split | :unknown`

Raises:

  • (RuntimeError)

    if the device under test is not an iPad.

  • (RuntimeError)

    if ‘:raise_on_no_visible_keyboard` is truthy and no keyboard is visible.



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 197

def ipad_keyboard_mode(opts = {})
  raise 'the keyboard mode does not exist on the iphone or ipod' if device_family_iphone?

  default_opts = {:raise_on_no_visible_keyboard => true}
  merged_opts = default_opts.merge(opts)
  if merged_opts[:raise_on_no_visible_keyboard]
    screenshot_and_raise 'there is no visible keyboard' unless keyboard_visible?
    return :docked if docked_keyboard_visible?
    return :undocked if undocked_keyboard_visible?
    :split
  else
    return :docked if docked_keyboard_visible?
    return :undocked if undocked_keyboard_visible?
    return :split if split_keyboard_visible?
    :unknown
  end
end

#keyboard_enter_char(chr, opts = {}) ⇒ Object

Note:

IMPORTANT: Use the ‘POST_ENTER_KEYBOARD` environmental variable to slow down the typing; adds a wait after each character is touched. this can fix problems where the typing is too fast and characters are skipped.

Note:

There are several special ‘characters’, some of which do not appear on all keyboards; e.g. ‘Delete`, `Return`.

Note:

Since 0.9.163, this method accepts a Hash as the second parameter. The previous second parameter was a Boolean that controlled whether or not to screenshot on errors.

Note:

You should prefer to call ‘keyboard_enter_text`.

Use keyboard to enter a character.

Parameters:

  • chr (String)

    the character to type

  • opts (Hash) (defaults to: {})

    options to control the behavior of the method

Options Hash (opts):

  • :should_screenshot (Boolean) — default: true

    whether or not to screenshot on errors

  • :wait_after_char (Float) — default: 'POST_ENTER_KEYBOARD' or 0.05

    how long to wait after a character is typed.

Raises:

  • (RuntimeError)

    if there is no visible keyboard

  • (RuntimeError)

    if the keyboard (layout) is not supported

See Also:



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 284

def keyboard_enter_char(chr, opts={})
  unless opts.is_a?(Hash)
   msg = "you should no longer pass a boolean as the second arg; pass {:should_screenshot => '#{opts}'}  hash instead"
   _deprecated('0.9.163', msg, :warn)
   opts = {:should_screenshot => opts}
  end

  default_opts = {:should_screenshot => true,
                  # introduce a small wait to avoid skipping characters
                  # keep this as short as possible
                  :wait_after_char => (ENV['POST_ENTER_KEYBOARD'] || 0.05).to_f}

  opts = default_opts.merge(opts)

  should_screenshot = opts[:should_screenshot]
  _ensure_can_enter_text({:screenshot => should_screenshot,
                          :skip => (not should_screenshot)})

  if uia_available?
    if chr.length == 1
      uia_type_string chr
    else
      code = UIA_SUPPORTED_CHARS[chr]

      unless code
        raise "typing character '#{chr}' is not yet supported when running with Instruments"
      end

      # on iOS 6, the Delete char code is _not_ \b
      # on iOS 7, the Delete char code is \b on non-numeric keyboards
      #           on numeric keyboards, it is actually a button on the
      #           keyboard and not a key
      if code.eql?(UIA_SUPPORTED_CHARS['Delete'])
        uia("uia.keyboard().elements().firstWithName('Delete').tap()")
      elsif code.eql?(UIA_SUPPORTED_CHARS['Return'])
        tap_keyboard_action_key
      else
        uia_type_string(code, '')
      end
    end
    # noinspection RubyStringKeysInHashInspection
    res = {'results' => []}
  else
    res = http({:method => :post, :path => 'keyboard'},
               {:key => chr, :events => load_playback_data('touch_done')})
    res = JSON.parse(res)
    if res['outcome'] != 'SUCCESS'
      msg = "Keyboard enter failed failed because: #{res['reason']}\n#{res['details']}"
      if should_screenshot
        screenshot_and_raise msg
      else
        raise msg
      end
    end
  end

  if ENV['POST_ENTER_KEYBOARD']
    w = ENV['POST_ENTER_KEYBOARD'].to_f
    if w > 0
      sleep(w)
    end
  end
  pause = opts[:wait_after_char]
  sleep(pause) if pause > 0
  res['results']
end

#keyboard_enter_text(text) ⇒ Object

Uses the keyboard to enter text.

Parameters:

  • text (String)

    the text to type.

Raises:

  • (RuntimeError)

    if the text cannot be typed.



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 355

def keyboard_enter_text(text)
  _ensure_can_enter_text
  if uia_available?
    text_before = _text_from_first_responder()
    text_before = text_before.gsub("\n","\\n") if text_before
    uia_type_string(text, text_before)
  else
    text.each_char do |ch|
      begin
        keyboard_enter_char(ch, {:should_screenshot => false})
      rescue
        _search_keyplanes_and_enter_char(ch)
      end
    end
  end
end

#keyboard_visible?Boolean

Returns true if there is a visible keyboard.

Returns:

  • (Boolean)

    Returns true if there is a visible keyboard.



136
137
138
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 136

def keyboard_visible?
  docked_keyboard_visible? or undocked_keyboard_visible? or split_keyboard_visible?
end

#split_keyboard_visible?Boolean

Returns true if a split keyboard is visible.

A split keyboard is floats in the middle of the view and is split to allow faster thumb typing

keyboards on the Phone and iPod are docked and not split.

Returns:

  • (Boolean)

    Returns false if the device is not an iPad; all



127
128
129
130
131
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 127

def split_keyboard_visible?
  return false if device_family_iphone?
  query("view:'UIKBKeyView'").count > 0 and
        element_does_not_exist(_qstr_for_keyboard)
end

#tap_keyboard_action_keyObject

Note:

Not all keyboards have an action key. For example, numeric keyboards do not have an action key.

Touches the keyboard action key.

The action key depends on the keyboard. Some examples include:

  • Return

  • Next

  • Go

  • Join

  • Search

Raises:

  • (RuntimeError)

    if the text cannot be typed.



387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 387

def tap_keyboard_action_key
  if uia_available?
    run_loop = Calabash::Cucumber::Launcher.launcher.run_loop
    if run_loop[:uia_strategy] == :host
      uia_type_string "\\\\n", '', false
    else
      uia_type_string '\n', '', false
    end
  else
    keyboard_enter_char 'Return'
  end
end

#uia_keyboard_visible?Boolean

Note:

IMPORTANT this should only be used when the app does not respond to ‘keyboard_visible?`.

Used for detecting keyboards that are not normally visible to calabash; e.g. the keyboard on the ‘MFMailComposeViewController`

Returns:

  • (Boolean)

Raises:

  • (RuntimeError)

    if the app was not launched with instruments

See Also:



816
817
818
819
820
821
822
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 816

def uia_keyboard_visible?
  unless uia_available?
    screenshot_and_raise 'only available if there is a run_loop i.e. the app was launched with Instruments'
  end
  res = uia_query_windows(:keyboard)
  not res.eql?(':nil')
end

#uia_wait_for_keyboard(opts = {}) ⇒ Object

Note:

IMPORTANT this should only be used when the app does not respond to ‘keyboard_visible?`.

Waits for a keyboard that is not normally visible to calabash; e.g. the keyboard on ‘MFMailComposeViewController`.

Raises:

  • (RuntimeError)

    if the app was not launched with instruments

See Also:



834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 834

def uia_wait_for_keyboard(opts={})
  unless uia_available?
    screenshot_and_raise 'only available if there is a run_loop i.e. the app was launched with Instruments'
  end
  default_opts = {:timeout => 10,
                  :retry_frequency => 0.1,
                  :post_timeout => 0.5}
  opts = default_opts.merge(opts)
  unless opts[:timeout_message]
    msg = "waited for '#{opts[:timeout]}' for keyboard"
    opts[:timeout_message] = msg
  end

  wait_for(opts) do
    uia_keyboard_visible?
  end
end

#undocked_keyboard_visible?Boolean

Returns true if an undocked keyboard is visible.

A undocked keyboard is floats in the middle of the view.

keyboards on the iPhone and iPod are docked.

Returns:

  • (Boolean)

    Returns false if the device is not an iPad; all



111
112
113
114
115
116
117
118
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 111

def undocked_keyboard_visible?
  return false if device_family_iphone?

  res = query(_qstr_for_keyboard).first
  return false if res.nil?

  not docked_keyboard_visible?
end

#wait_for_keyboard(opts = {}) ⇒ Object

Waits for a keyboard to appear and once it does appear waits for ‘:post_timeout` seconds.

Parameters:

  • opts (Hash) (defaults to: {})

    controls the ‘wait_for` behavior

Options Hash (opts):

  • :timeout_message (String) — default: 'keyboard did not appear'

    Controls the message that appears in the exception.

  • :post_timeout (Number) — default: 0.3

    Controls how long to wait after the keyboard has appeared.

Raises:

See Also:



153
154
155
156
157
158
159
160
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 153

def wait_for_keyboard(opts={})
  default_opts = {:timeout_message => 'keyboard did not appear',
                  :post_timeout => 0.3}
  opts = default_opts.merge(opts)
  wait_for(opts) do
    keyboard_visible?
  end
end