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.



696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 696

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:



174
175
176
177
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 174

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



565
566
567
568
569
570
571
572
573
574
575
576
577
578
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 565

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
104
105
106
107
108
109
110
111
112
113
# 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?

  rect = res['rect']
  o = status_bar_orientation.to_sym
  case o
    when :left
      if ios8?
        rect['center_x'] == 512 and rect['center_y'] == 592
      else
        rect['center_x'] == 592 and rect['center_y'] == 512
      end
    when :right
      if ios8?
        rect['center_x'] == 512 and rect['center_y'] == 592
      else
        rect['center_x'] == 176 and rect['center_y'] == 512
      end
    when :up
      if ios8?
        rect['center_x'] == 384 and rect['center_y'] == 892
      else
        rect['center_x'] == 384 and rect['center_y'] == 132
      end
    when :down
        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:



467
468
469
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 467

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



723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 723

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



811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 811

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



764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 764

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.



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 207

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:



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
350
351
352
353
354
355
356
357
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 294

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_raw 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()")
      else
        uia_type_string_raw(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.



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 363

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.



146
147
148
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 146

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



137
138
139
140
141
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 137

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.



445
446
447
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 445

def tap_keyboard_action_key
  keyboard_enter_char 'Return'
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:



865
866
867
868
869
870
871
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 865

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:



883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 883

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



121
122
123
124
125
126
127
128
# File 'lib/calabash-cucumber/keyboard_helpers.rb', line 121

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:



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

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