lib/calabash-cucumber/keyboard_helpers.rb in calabash-cucumber-0.19.2 vs lib/calabash-cucumber/keyboard_helpers.rb in calabash-cucumber-0.20.0

- old
+ new

@@ -1,109 +1,47 @@ -require 'calabash-cucumber/core' -require 'calabash-cucumber/tests_helpers' -require 'calabash-cucumber/environment_helpers' - module Calabash module Cucumber - # Raised when there is a problem involving a keyboard mode. There are - # three keyboard modes: docked, split, and undocked. - # - # All iPads support these keyboard modes, but the user can disable them - # in Settings.app. - # - # The iPhone 6+ family also supports keyboard modes, but Calabash does - # support keyboard modes on these devices. - class KeyboardModeError < StandardError; ; end - # 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 - # ``` - # - # @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. module KeyboardHelpers - include Calabash::Cucumber::TestsHelpers + # This module is expected to be included in Calabash::Cucumber::Core. + # Core includes necessary methods from: + # + # StatusBarHelpers + # EnvironmentHelpers + # WaitHelpers + # FailureHelpers + # UIA - # @!visibility private - KEYPLANE_NAMES = { - :small_letters => 'small-letters', - :capital_letters => 'capital-letters', - :numbers_and_punctuation => 'numbers-and-punctuation', - :first_alternate => 'first-alternate', - :numbers_and_punctuation_alternate => 'numbers-and-punctuation-alternate' - } + require "calabash-cucumber/map" - # @!visibility private - # noinspection RubyStringKeysInHashInspection - UIA_SUPPORTED_CHARS = { - 'Delete' => '\b', - 'Return' => '\n' - # these are not supported yet and I am pretty sure that they - # cannot be touched by passing an escaped character and instead - # the must be found using UIAutomation calls. -jmoody - #'Dictation' => nil, - #'Shift' => nil, - #'International' => nil, - #'More' => nil, - } - - # @!visibility private - # Returns a query string for detecting a keyboard. - def _qstr_for_keyboard - "view:'UIKBKeyplaneView'" - end - # 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. # # @return [Boolean] if a keyboard is visible and docked. def docked_keyboard_visible? - res = query(_qstr_for_keyboard).first + keyboard = _query_for_keyboard - return false if res.nil? + return false if keyboard.nil? return true if device_family_iphone? - orientation = status_bar_orientation.to_sym - keyboard_height = res['rect']['height'] - keyboard_y = res['rect']['y'] - scale = screen_dimensions[:scale] + keyboard_height = keyboard['rect']['height'] + keyboard_y = keyboard['rect']['y'] + dimensions = screen_dimensions + scale = dimensions[:scale] - if orientation == :left || orientation == :right - screen_height = screen_dimensions[:width]/scale + if landscape? + screen_height = dimensions[:width]/scale else - screen_height = screen_dimensions[:height]/scale + screen_height = dimensions[:height]/scale end screen_height - keyboard_height == keyboard_y end @@ -114,14 +52,14 @@ # @return [Boolean] Returns false if the device is not an iPad; all # keyboards on the iPhone and iPod are docked. def undocked_keyboard_visible? return false if device_family_iphone? - res = query(_qstr_for_keyboard).first - return false if res.nil? + keyboard = _query_for_keyboard + return false if keyboard.nil? - not docked_keyboard_visible? + !docked_keyboard_visible? end # Returns true if a split keyboard is visible. # # A split keyboard is floats in the middle of the view and is split to @@ -129,631 +67,81 @@ # # @return [Boolean] Returns false if the device is not an iPad; all # keyboards on the Phone and iPod are docked and not split. def split_keyboard_visible? return false if device_family_iphone? - query("view:'UIKBKeyView'").count > 0 and - element_does_not_exist(_qstr_for_keyboard) + _query_for_split_keyboard && !_query_for_keyboard end # Returns true if there is a visible keyboard. # # @return [Boolean] Returns true if there is a visible keyboard. def keyboard_visible? - docked_keyboard_visible? or undocked_keyboard_visible? or split_keyboard_visible? + # Order matters! + docked_keyboard_visible? || + undocked_keyboard_visible? || + split_keyboard_visible? end + # @!visibility private + # Raises an error ir the keyboard is not visible. + def expect_keyboard_visible! + if !keyboard_visible? + screenshot_and_raise "Keyboard is not visible" + end + true + end + # Waits for a keyboard to appear and once it does appear waits for # `:post_timeout` seconds. # # @see Calabash::Cucumber::WaitHelpers#wait_for for other options this # method can handle. # - # @param [Hash] opts controls the `wait_for` behavior + # @param [Hash] options controls the `wait_for` behavior # @option opts [String] :timeout_message ('keyboard did not appear') # Controls the message that appears in the error. # @option opts [Number] :post_timeout (0.3) Controls how long to wait # _after_ the keyboard has appeared. # # @raise [Calabash::Cucumber::WaitHelpers::WaitError] if no keyboard appears - 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 + def wait_for_keyboard(options={}) + default_opts = { + :timeout_message => "Keyboard did not appear", + :post_timeout => 0.3 + } + + merged_opts = default_opts.merge(options) + wait_for(merged_opts) do keyboard_visible? end + true end - # @!visibility private - # returns an array of possible ipad keyboard modes - def _ipad_keyboard_modes - [:docked, :undocked, :split] - end - - # Returns the keyboard mode. + # Waits for a keyboard to disappear. # - # @example How to use in a wait_* function. - # wait_for do - # ipad_keyboard_mode({:raise_on_no_visible_keyboard => false}) == :split - # end + # @see Calabash::Cucumber::WaitHelpers#wait_for for other options this + # method can handle. # - # ``` - # 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 - # ``` + # @param [Hash] options controls the `wait_for` behavior + # @option opts [String] :timeout_message ('keyboard did not appear') + # Controls the message that appears in the error. # - # @raise [RuntimeError] if the device under test is not an iPad. - # - # @raise [RuntimeError] if `:raise_on_no_visible_keyboard` is truthy and - # no keyboard is visible. - # @param [Hash] opts controls the runtime behavior. - # @option opts [Boolean] :raise_on_no_visible_keyboard (true) set to false - # if you don't want to raise an error. - # @return [Symbol] Returns one of `{:docked | :undocked | :split | :unknown}` - def ipad_keyboard_mode(opts = {}) - raise 'the keyboard mode does not exist on the iphone or ipod' if device_family_iphone? + # @raise [Calabash::Cucumber::WaitHelpers::WaitError] If keyboard does + # not disappear. + def wait_for_no_keyboard(options={}) + default_opts = { + :timeout_message => "Keyboard is visible", + } - 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 + merged_opts = default_opts.merge(options) + wait_for(merged_opts) do + !keyboard_visible? end + true end - # @!visibility private - # Ensures that there is a keyboard to enter text. - # - # @note - # *IMPORTANT* will always raise an error when the keyboard is split and - # there is no `run_loop`; i.e. UIAutomation is not available. - # - # @param [Hash] opts controls screenshot-ing and error raising conditions - # @option opts [Boolean] :screenshot (true) raise with a screenshot if - # a keyboard cannot be ensured - # @option opts [Boolean] :skip (false) skip any checking (a nop) - used - # when iterating over keyplanes for keys - def _ensure_can_enter_text(opts={}) - default_opts = {:screenshot => true, - :skip => false} - opts = default_opts.merge(opts) - return if opts[:skip] - - screenshot = opts[:screenshot] - if !keyboard_visible? - msg = "No visible keyboard." - if screenshot - screenshot_and_raise msg - else - raise msg - end - end - end - - # Use keyboard to enter a character. - # - # @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. - # - # @see #keyboard_enter_text - # - # @note - # You should prefer to call `keyboard_enter_text`. - # - # @raise [RuntimeError] if there is no visible keyboard - # @raise [RuntimeError] if the keyboard (layout) is not supported - # - # @param [String] chr the character to type - # @param [Hash] opts options to control the behavior of the method - # @option opts [Boolean] :should_screenshot (true) whether or not to - # screenshot on errors - # @option opts [Float] :wait_after_char ('POST_ENTER_KEYBOARD' or 0.05) - # how long to wait after a character is typed. - def keyboard_enter_char(chr, opts={}) - 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 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']) - js_tap_delete = "(function() {"\ - "var deleteElement = uia.keyboard().elements().firstWithName('Delete');"\ - "deleteElement = deleteElement.isValid() ? deleteElement : uia.keyboard().elements().firstWithName('delete');"\ - "deleteElement.tap();"\ - "})();" - uia(js_tap_delete) - else - uia_type_string_raw(code) - end - end - # noinspection RubyStringKeysInHashInspection - res = {'results' => []} - - 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 - - # Uses the keyboard to enter text. - # - # @param [String] text the text to type. - # @raise [RuntimeError] if the text cannot be typed. - def keyboard_enter_text(text) - _ensure_can_enter_text - text_before = _text_from_first_responder() - text_before = text_before.gsub("\n","\\n") if text_before - uia_type_string(text, text_before) - end - - # @!visibility private - # - # Enters text into view identified by a query - # - # @note - # *IMPORTANT* enter_text defaults to calling 'setValue' in UIAutomation - # on the text field. This is fast, but in some cases might result in slightly - # different behaviour than using `keyboard_enter_text`. - # To force use of `keyboard_enter_text` in `enter_text` use - # option :use_keyboard - # - # @param [String] uiquery the element to enter text into - # @param [String] text the text to enter - # @param [Hash] options controls details of text entry - # @option options [Boolean] :use_keyboard (false) use the iOS keyboard - # to enter each character separately - # @option options [Boolean] :wait (true) call wait_for_element_exists with uiquery - # @option options [Hash] :wait_options ({}) if :wait pass this as options to wait_for_element_exists - def enter_text(uiquery, text, options = {}) - default_opts = {:use_keyboard => false, :wait => true, :wait_options => {}} - options = default_opts.merge(options) - wait_for_element_exists(uiquery, options[:wait_options]) if options[:wait] - touch(uiquery, options) - wait_for_keyboard - if options[:use_keyboard] - keyboard_enter_text(text) - else - fast_enter_text(text) - end - end - - # @!visibility private - # - # Enters text into current text input field - # - # @note - # *IMPORTANT* fast_enter_text defaults to calling 'setValue' in UIAutomation - # on the text field. This is fast, but in some cases might result in slightly - # different behaviour than using `keyboard_enter_text`. - # @param [String] text the text to enter - def fast_enter_text(text) - _ensure_can_enter_text - uia_set_responder_value(text) - end - - # Touches the keyboard action key. - # - # The action key depends on the keyboard. Some examples include: - # - # * Return - # * Next - # * Go - # * Join - # * Search - # - # @note - # Not all keyboards have an action key. For example, numeric keyboards - # do not have an action key. - # - # @raise [RuntimeError] if the text cannot be typed. - def tap_keyboard_action_key - keyboard_enter_char 'Return' - end - - # @!visibility private - # Returns the current keyplane. - def _current_keyplane - kp_arr = _do_keyplane( - lambda { query("view:'UIKBKeyplaneView'", 'keyplane', 'componentName') }, - lambda { query("view:'UIKBKeyplaneView'", 'keyplane', 'name') }) - kp_arr.first.downcase - end - - # @!visibility private - # Searches the available keyplanes for chr and if it is found, types it. - # - # This is a recursive function. - # - # @note - # Use the `KEYPLANE_SEARCH_STEP_PAUSE` variable to control how quickly - # the next keyplane is searched. Increase this value if you encounter - # problems with missed keystrokes. - # - # @note - # When running under instruments, this method is not called. - # - # @raise [RuntimeError] if the char cannot be found - def _search_keyplanes_and_enter_char(chr, visited=Set.new) - cur_kp = _current_keyplane - begin - keyboard_enter_char(chr, {:should_screenshot => false}) - return true #found - rescue - pause = (ENV['KEYPLANE_SEARCH_STEP_PAUSE'] || 0.2).to_f - sleep (pause) if pause > 0 - - visited.add(cur_kp) - - #figure out keyplane alternates - props = _do_keyplane( - lambda { query("view:'UIKBKeyplaneView'", 'keyplane', 'properties') }, - lambda { query("view:'UIKBKeyplaneView'", 'keyplane', 'attributes', 'dict') } - ).first - - known = KEYPLANE_NAMES.values - - found = false - keyplane_selection_keys = ['shift', 'more'] - keyplane_selection_keys.each do |key| - sleep (pause) if pause > 0 - plane = props["#{key}-alternate"] - if known.member?(plane) and (not visited.member?(plane)) - keyboard_enter_char(key.capitalize, {:should_screenshot => false}) - found = _search_keyplanes_and_enter_char(chr, visited) - return true if found - #not found => try with other keyplane selection key - keyplane_selection_keys.delete(key) - other_key = keyplane_selection_keys.last - keyboard_enter_char(other_key.capitalize, {:should_screenshot => false}) - found = _search_keyplanes_and_enter_char(chr, visited) - return true if found - end - end - return false - end - end - - # @!visibility private - # Process a keyplane. - # - # @raise [RuntimeError] if there is no visible keyplane - def _do_keyplane(kbtree_proc, keyplane_proc) - desc = query("view:'UIKBKeyplaneView'", 'keyplane') - fail('No keyplane (UIKBKeyplaneView keyplane)') if desc.empty? - fail('Several keyplanes (UIKBKeyplaneView keyplane)') if desc.count > 1 - kp_desc = desc.first - if /^<UIKBTree/.match(kp_desc) - #ios5+ - kbtree_proc.call - elsif /^<UIKBKeyplane/.match(kp_desc) - #ios4 - keyplane_proc.call - end - end - - # @!visibility private - # Returns a query string for finding the iPad 'Hide keyboard' button. - def _query_uia_hide_keyboard_button - "uia.keyboard().buttons()['Hide keyboard']" - end - - # Dismisses a iPad keyboard by touching the 'Hide keyboard' button and waits - # for the keyboard to disappear. - # - # @note - # the dismiss keyboard key does not exist on the iPhone or iPod - # - # @raise [RuntimeError] if the device is not an iPad - def dismiss_ipad_keyboard - screenshot_and_raise "Cannot dismiss keyboard on iPhone" if device_family_iphone? - send_uia_command({:command => "#{_query_uia_hide_keyboard_button}.tap()"}) - - opts = {:timeout_message => 'keyboard did not disappear'} - wait_for(opts) do - not keyboard_visible? - end - end - - # @!visibility private - # Returns the activation point of the iPad keyboard mode key. - # - # The mode key is also known as the 'Hide keyboard' key. - # - # @note - # This is only available when running under instruments. - # - # @raise [RuntimeError] when the device is not an iPad - # @raise [RuntimeError] the app was not launched with instruments - def _point_for_ipad_keyboard_mode_key - raise "The keyboard mode does not exist on the on the iphone" if device_family_iphone? - res = send_uia_command({:command => "#{_query_uia_hide_keyboard_button}.rect()"}) - origin = res['value']['origin'] - {:x => origin['x'], :y => origin['y']} - end - - # @!visibility private - # Touches the bottom 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. - def _touch_bottom_keyboard_mode_row - start_pt = _point_for_ipad_keyboard_mode_key - # there are 10 pt btw the key and the popup and the row is 50 pt - y_offset = 10 + 25 - end_pt = {:x => (start_pt[:x] - 40), :y => (start_pt[:y] - y_offset)} - uia_pan_offset(start_pt, end_pt, {}) - sleep(1.0) - end - - # 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. - def _touch_top_keyboard_mode_row - 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 - - x_offset = 40 - y_offset = 10 + 50 + 25 - end_pt = {:x => (start_pt[:x] - x_offset), :y => (start_pt[:y] - y_offset)} - uia_pan_offset(start_pt, end_pt, {:duration => 1.0}) - end - - # 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`. - # - # @raise [RuntimeError] if there is no visible keyboard - # @raise [RuntimeError] a docked keyboard was not achieved - def ensure_docked_keyboard - wait_for_keyboard - - return if device_family_iphone? - - mode = ipad_keyboard_mode - - return if mode == :docked - - if ios9? - raise KeyboardModeError, - 'Changing keyboard modes is not supported on iOS 9' - else - 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 - 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 - - # 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`. - # - # @raise [RuntimeError] if there is no visible keyboard - # @raise [RuntimeError] an undocked keyboard was not achieved - def ensure_undocked_keyboard - wait_for_keyboard - - return if device_family_iphone? - - mode = ipad_keyboard_mode - - return if mode == :undocked - - if ios9? - raise KeyboardModeError, - 'Changing keyboard modes is not supported on iOS 9' - else - 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 - end - _wait_for_keyboard_in_mode(:undocked) - end - - - # 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`. - # - # @raise [RuntimeError] if there is no visible keyboard - # @raise [RuntimeError] a split keyboard was not achieved - def ensure_split_keyboard - wait_for_keyboard - - return if device_family_iphone? - - mode = ipad_keyboard_mode - - return if mode == :split - - if ios9? - raise KeyboardModeError, - 'Changing keyboard modes is not supported on iOS 9' - else - 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 - end - _wait_for_keyboard_in_mode(:split) - end - - # @!visibility private - def _wait_for_keyboard_in_mode(mode, opts={}) - default_opts = {:post_timeout => 1.0} - opts = default_opts.merge(opts) - begin - wait_for(opts) do - case mode - when :split then - split_keyboard_visible? - when :undocked - undocked_keyboard_visible? - when :docked - docked_keyboard_visible? - else - screenshot_and_raise "expected '#{mode}' to be one of #{_ipad_keyboard_modes}" - end - end - rescue - actual = ipad_keyboard_mode - o = status_bar_orientation - screenshot_and_raise "expected keyboard to be '#{mode}' but found '#{actual}' in orientation '#{o}'" - end - end - - # Used for detecting keyboards that are not normally visible to calabash; - # e.g. the keyboard on the `MFMailComposeViewController` - # - # @note - # IMPORTANT this should only be used when the app does not respond to - # `keyboard_visible?`. - # - # @see #keyboard_visible? - # - # @raise [RuntimeError] if the app was not launched with instruments - def uia_keyboard_visible? - res = uia_query_windows(:keyboard) - not res.eql?(':nil') - end - - # Waits for a keyboard that is not normally visible to calabash; - # e.g. the keyboard on `MFMailComposeViewController`. - # - # @note - # IMPORTANT this should only be used when the app does not respond to - # `keyboard_visible?`. - # - # @see #keyboard_visible? - # - # @raise [RuntimeError] if the app was not launched with instruments - def uia_wait_for_keyboard(opts={}) - 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 - # Waits for a keyboard to appear and returns the localized name of the # `key_code` signifier # # @param [String] key_code Maps to a specific name in some localization def lookup_key_name(key_code) @@ -778,19 +166,49 @@ # # Returns empty string if no textField or textView elements are found to be # the first responder. # # @raise [RuntimeError] if there is no visible keyboard - def _text_from_first_responder - raise 'there must be a visible keyboard' unless keyboard_visible? + def text_from_first_responder + if !keyboard_visible? + screenshot_and_raise "There must be a visible keyboard" + end ['textField', 'textView'].each do |ui_class| - res = query("#{ui_class} isFirstResponder:1", :text) - return res.first unless res.empty? + query = "#{ui_class} isFirstResponder:1" + result = _query_wrapper(query, :text) + if !result.empty? + return result.first + end end - #noinspection RubyUnnecessaryReturnStatement - return '' + "" end + # @visibility private + # TODO Remove in 0.21.0 + alias_method :_text_from_first_responder, :text_from_first_responder + + private + + # @!visibility private + KEYBOARD_QUERY = "view:'UIKBKeyplaneView'" + + # @!visibility private + SPLIT_KEYBOARD_QUERY = "view:'UIKBKeyView'" + + # @!visibility private + def _query_wrapper(query, *args) + Calabash::Cucumber::Map.map(query, :query, *args) + end + + # @!visibility private + def _query_for_keyboard + _query_wrapper(KEYBOARD_QUERY).first + end + + # @!visibility private + def _query_for_split_keyboard + _query_wrapper(SPLIT_KEYBOARD_QUERY).first + end end end end