calabash-cucumber 0.19.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/dylibs/libCalabashDyn.dylib +0 -0
  3. data/dylibs/libCalabashDynSim.dylib +0 -0
  4. data/lib/calabash-cucumber.rb +9 -2
  5. data/lib/calabash-cucumber/abstract.rb +23 -0
  6. data/lib/calabash-cucumber/automator/automator.rb +158 -0
  7. data/lib/calabash-cucumber/automator/coordinates.rb +401 -0
  8. data/lib/calabash-cucumber/automator/device_agent.rb +424 -0
  9. data/lib/calabash-cucumber/automator/instruments.rb +441 -0
  10. data/lib/calabash-cucumber/connection_helpers.rb +1 -0
  11. data/lib/calabash-cucumber/core.rb +632 -138
  12. data/lib/calabash-cucumber/device_agent.rb +346 -0
  13. data/lib/calabash-cucumber/dot_dir.rb +1 -0
  14. data/lib/calabash-cucumber/environment.rb +1 -0
  15. data/lib/calabash-cucumber/environment_helpers.rb +4 -3
  16. data/lib/calabash-cucumber/http/http.rb +6 -4
  17. data/lib/calabash-cucumber/keyboard_helpers.rb +97 -679
  18. data/lib/calabash-cucumber/launcher.rb +107 -31
  19. data/lib/calabash-cucumber/log_tailer.rb +46 -0
  20. data/lib/calabash-cucumber/map.rb +7 -1
  21. data/lib/calabash-cucumber/rotation_helpers.rb +47 -139
  22. data/lib/calabash-cucumber/status_bar_helpers.rb +51 -20
  23. data/lib/calabash-cucumber/store/preferences.rb +3 -0
  24. data/lib/calabash-cucumber/uia.rb +333 -2
  25. data/lib/calabash-cucumber/usage_tracker.rb +2 -0
  26. data/lib/calabash-cucumber/version.rb +2 -2
  27. data/lib/calabash-cucumber/wait_helpers.rb +2 -0
  28. data/lib/calabash/formatters/html.rb +6 -1
  29. data/lib/frank-calabash.rb +10 -4
  30. data/scripts/.irbrc +3 -0
  31. data/staticlib/calabash.framework.zip +0 -0
  32. data/staticlib/libFrankCalabash.a +0 -0
  33. metadata +11 -6
  34. data/lib/calabash-cucumber/actions/instruments_actions.rb +0 -155
@@ -0,0 +1,346 @@
1
+ module Calabash
2
+ module Cucumber
3
+
4
+ # An interface to the DeviceAgent Query and Gesture API.
5
+ #
6
+ # Unlike Calabash or UIA gestures, all DeviceAgent gestures wait for the
7
+ # uiquery to match a view. This behavior match the Calabash 2.0 and
8
+ # Calabash 0.x Android behavior.
9
+ #
10
+ # This API is work in progress. There are several methods that are
11
+ # experimental and several methods that will probably removed soon.
12
+ #
13
+ # Wherever possible use Core#query and the gestures defined in Core.
14
+ #
15
+ # TODO Screenshots
16
+ class DeviceAgent < BasicObject
17
+
18
+ # @!visibility private
19
+ #
20
+ # @param [RunLoop::DeviceAgent::Client] client The DeviceAgent client.
21
+ # @param [Cucumber::World] world The Cucumber World.
22
+ def initialize(client, world)
23
+ @client = client
24
+ @world = world
25
+ end
26
+
27
+ # @!visibility private
28
+ #
29
+ # @example
30
+ # query({id: "login", :type "Button"})
31
+ #
32
+ # query({marked: "login"})
33
+ #
34
+ # query({marked: "login", type: "TextField"})
35
+ #
36
+ # query({type: "Button", index: 2})
37
+ #
38
+ # query({text: "Log in"})
39
+ #
40
+ # query({id: "hidden button", :all => true})
41
+ #
42
+ # # Escaping single quote is not necessary, but supported.
43
+ # query({text: "Karl's problem"})
44
+ # query({text: "Karl\'s problem"})
45
+ #
46
+ # # Escaping double quote is not necessary, but supported.
47
+ # query({text: "\"To know is not enough.\""})
48
+ # query({text: %Q["To know is not enough."]})
49
+ #
50
+ # Querying for text with newlines is not supported yet.
51
+ #
52
+ # The query language supports the following keys:
53
+ # * :marked - accessibilityIdentifier, accessibilityLabel, text, and value
54
+ # * :id - accessibilityIdentifier
55
+ # * :type - an XCUIElementType shorthand, e.g. XCUIElementTypeButton =>
56
+ # Button. See the link below for available types. Note, however that
57
+ # some XCUIElementTypes are not available on iOS.
58
+ # * :index - Applied after all other specifiers.
59
+ # * :all - Filter the result by visibility. Defaults to false. See the
60
+ # discussion below about visibility.
61
+ #
62
+ # ### Visibility
63
+ #
64
+ # The rules for visibility are:
65
+ #
66
+ # 1. If any part of the view is visible, the visible.
67
+ # 2. If the view has alpha 0, it is not visible.
68
+ # 3. If the view has a size (0,0) it is not visible.
69
+ # 4. If the view is not within the bounds of the screen, it is not visible.
70
+ #
71
+ # Visibility is determined using the "hitable" XCUIElement property.
72
+ # XCUITest, particularly under Xcode 7, is not consistent about setting
73
+ # the "hitable" property correctly. Views that are not "hitable" will
74
+ # still respond to gestures. For this reason, gestures use the
75
+ # element["rect"] for computing the touch point.
76
+ #
77
+ # Regarding rule #1 - this is different from the Calabash iOS and Android
78
+ # definition of visibility which requires the mid-point of the view to be
79
+ # visible.
80
+ #
81
+ # Please report visibility problems.
82
+ #
83
+ # ### Results
84
+ #
85
+ # Results are returned as an Array of Hashes. The key/value pairs are
86
+ # similar to those returned by the Calabash iOS Server, but not exactly
87
+ # the same.
88
+ #
89
+ # ```
90
+ # [
91
+ # {
92
+ # "enabled": true,
93
+ # "id": "mostly hidden button",
94
+ # "hitable": true,
95
+ # "rect": {
96
+ # "y": 459,
97
+ # "x": 24,
98
+ # "height": 25,
99
+ # "width": 100
100
+ # },
101
+ # "label": "Mostly Hidden",
102
+ # "type": "Button",
103
+ # "hit_point": {
104
+ # "x": 25,
105
+ # "y": 460
106
+ # },
107
+ # }
108
+ # ]
109
+ # ```
110
+ #
111
+ # @see http://masilotti.com/xctest-documentation/Constants/XCUIElementType.html
112
+ # @param [Hash] uiquery A hash describing the query.
113
+ # @return [Array<Hash>] An array of elements matching the `uiquery`.
114
+ def query(uiquery)
115
+ client.query(uiquery)
116
+ end
117
+
118
+ # Query for the center of a view.
119
+ #
120
+ # @see #query
121
+ #
122
+ # This method waits for the query to match at least one element.
123
+ #
124
+ # @param uiquery See #query
125
+ # @return [Hash] The center of first view matched by query.
126
+ #
127
+ # @raise [RuntimeError] if no view matches the uiquery after waiting.
128
+ def query_for_coordinate(uiquery)
129
+ fail_with_screenshot { client.query_for_coordinate(uiquery) }
130
+ end
131
+
132
+ # Perform a touch on the center of the first view matched the uiquery.
133
+ #
134
+ # This method waits for the query to match at least one element.
135
+ #
136
+ # @see #query
137
+ #
138
+ # @param [Hash] uiquery See #query for examples.
139
+ # @return [Array<Hash>] The view that was touched.
140
+ #
141
+ # @raise [RuntimeError] if no view matches the uiquery after waiting.
142
+ def touch(uiquery)
143
+ fail_with_screenshot { client.touch(uiquery) }
144
+ end
145
+
146
+ # Perform a touch at a coordinate.
147
+ #
148
+ # This method does not wait; the touch is performed immediately.
149
+ #
150
+ # @param [Hash] coordinate The coordinate to touch.
151
+ def touch_coordinate(coordinate)
152
+ client.touch_coordinate(coordinate)
153
+ end
154
+
155
+ # Perform a touch at a point.
156
+ #
157
+ # This method does not wait; the touch is performed immediately.
158
+ #
159
+ # @param [Hash] x the x coordinate
160
+ # @param [Hash] y the y coordinate
161
+ def touch_point(x, y)
162
+ client.touch_point(x, y)
163
+ end
164
+
165
+ # Perform a double tap on the center of the first view matched the uiquery.
166
+ #
167
+ # @see #query
168
+ #
169
+ # This method waits for the query to match at least one element.
170
+ #
171
+ # @param uiquery See #query
172
+ # @return [Array<Hash>] The view that was touched.
173
+ #
174
+ # @raise [RuntimeError] if no view matches the uiquery after waiting.
175
+ def double_tap(uiquery)
176
+ fail_with_screenshot { client.double_tap(uiquery) }
177
+ end
178
+
179
+ # Perform a two finger tap on the center of the first view matched the uiquery.
180
+ #
181
+ # @see #query
182
+ #
183
+ # This method waits for the query to match at least one element.
184
+ #
185
+ # @param uiquery See #query
186
+ # @return [Array<Hash>] The view that was touched.
187
+ #
188
+ # @raise [RuntimeError] if no view matches the uiquery after waiting.
189
+ def two_finger_tap(uiquery)
190
+ fail_with_screenshot { client.two_finger_tap(uiquery) }
191
+ end
192
+
193
+ # Perform a long press on the center of the first view matched the uiquery.
194
+ #
195
+ # @see #query
196
+ #
197
+ # This method waits for the query to match at least one element.
198
+ #
199
+ # @param uiquery See #query
200
+ # @param [Numeric] duration How long to press.
201
+ # @return [Array<Hash>] The view that was touched.
202
+ #
203
+ # @raise [RuntimeError] if no view matches the uiquery after waiting.
204
+ def long_press(uiquery, duration)
205
+ fail_with_screenshot { client.long_press(uiquery, {:duration => duration}) }
206
+ end
207
+
208
+ # Returns true if there is a keyboard visible.
209
+ #
210
+ # Scheduled for removal in 0.21.0. Use Core#keyboard_visible?. If you
211
+ # find an example where Core#keyboard_visible? does not find visible
212
+ # keyboard, please report it.
213
+ #
214
+ # @deprecated 0.21.0 Use Core#keyboard_visible?
215
+ def keyboard_visible?
216
+ client.keyboard_visible?
217
+ end
218
+
219
+ # Enter text into the UITextInput view that is the first responder.
220
+ #
221
+ # The first responder is the view that is attached to the keyboard.
222
+ #
223
+ # Scheduled for removal in 0.21.0. Use Core#enter_text. If you find an
224
+ # example where Core#enter_text does not work, please report it.
225
+ #
226
+ # @param [String] text the text to enter
227
+ #
228
+ # @raise [RuntimeError] if there is no visible keyboard.
229
+ # @deprecated 0.21.0 Use Core#enter_text
230
+ def enter_text(text)
231
+ fail_with_screenshot { client.enter_text(text) }
232
+ end
233
+
234
+ # Enter text into the first view matched by uiquery.
235
+ #
236
+ # This method waits for the query to match at least one element and for
237
+ # the keyboard to appear.
238
+ #
239
+ # Scheduled for removal in 0.21.0. Use Core#enter_text_in. If you find an
240
+ # example where Core#enter_text_in does not work, please report it.
241
+ #
242
+ # @raise [RuntimeError] if no view matches the uiquery after waiting.
243
+ # @raise [RuntimeError] if the touch does not cause a keyboard to appear.
244
+ #
245
+ # @deprecated 0.21.0 Use Core#enter_text
246
+ def enter_text_in(uiquery, text)
247
+ fail_with_screenshot do
248
+ client.touch(uiquery)
249
+ client.wait_for_keyboard
250
+ client.enter_text(text)
251
+ end
252
+ end
253
+
254
+ # EXPERIMENTAL: This API may change.
255
+ #
256
+ # Is an alert generated by your Application visible?
257
+ #
258
+ # This does not detect SpringBoard alerts.
259
+ #
260
+ # @see #springboard_alert
261
+ #
262
+ # @see #springboard_alert
263
+ def app_alert_visible?
264
+ client.alert_visible?
265
+ end
266
+
267
+ # EXPERIMENTAL: This API may change.
268
+ #
269
+ # Queries for an alert generate by your Application.
270
+ #
271
+ # This does not detect SpringBoard alerts.
272
+ #
273
+ # @see #spring_board_alert
274
+ #
275
+ # @return [Array<Hash>] The view that was touched.
276
+ def app_alert
277
+ client.alert
278
+ end
279
+
280
+ # EXPERIMENTAL: This API may change.
281
+ #
282
+ # Is an alert generated by SpringBoard visible?
283
+ #
284
+ # This does not detect alerts generated by your Application.
285
+ #
286
+ # Examples of SpringBoard alerts are:
287
+ # * Privacy Alerts generated by requests for access to protected iOS
288
+ # services like Contacts and Location,
289
+ # * "No SIM card"
290
+ # * iOS Update available
291
+ #
292
+ # @see #alert
293
+ def springboard_alert_visible?
294
+ client.springboard_alert_visible?
295
+ end
296
+
297
+ # EXPERIMENTAL: This API may change.
298
+ #
299
+ # Queries for an alert generated by SpringBoard.
300
+ #
301
+ # This does not detect alerts generated by your Application.
302
+ #
303
+ # Examples of SpringBoard alerts are:
304
+ # * Privacy Alerts generated by requests for access to protected iOS
305
+ # services like Contacts and Location,
306
+ # * "No SIM card"
307
+ # * iOS Update available
308
+ #
309
+ # @see #alert
310
+ def springboard_alert
311
+ client.springboard_alert
312
+ end
313
+
314
+ =begin
315
+ PROTECTED
316
+ =end
317
+ protected
318
+
319
+ # @!visibility private
320
+ def method_missing(name, *args, &block)
321
+ if world.respond_to?(name)
322
+ world.send(name, *args, &block)
323
+ else
324
+ super
325
+ end
326
+ end
327
+
328
+ =begin
329
+ PRIVATE
330
+ =end
331
+ private
332
+
333
+ # @!visibility private
334
+ attr_reader :client, :world
335
+
336
+ # @!visibility private
337
+ def fail_with_screenshot(&block)
338
+ begin
339
+ block.call
340
+ rescue => e
341
+ world.send(:fail, e.message)
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end
@@ -4,6 +4,7 @@ module Calabash
4
4
  module DotDir
5
5
  require "run_loop"
6
6
 
7
+ # @!visibility private
7
8
  def self.directory
8
9
  home = RunLoop::Environment.user_home_directory
9
10
  dir = File.join(home, ".calabash")
@@ -1,5 +1,6 @@
1
1
  module Calabash
2
2
  module Cucumber
3
+ # @!visibility private
3
4
  module Environment
4
5
 
5
6
  require "run_loop"
@@ -1,6 +1,3 @@
1
- require 'calabash-cucumber/device'
2
- require 'calabash-cucumber/launcher'
3
-
4
1
  module Calabash
5
2
  module Cucumber
6
3
 
@@ -12,6 +9,8 @@ module Calabash
12
9
  # be set.
13
10
  module EnvironmentHelpers
14
11
 
12
+ require "calabash-cucumber/device"
13
+
15
14
  # Are the uia* methods available?
16
15
  #
17
16
  # @note
@@ -47,6 +46,7 @@ module Calabash
47
46
  #
48
47
  # @return [Calabash::Cucumber::Device] An instance of Device.
49
48
  def default_device
49
+ require "calabash-cucumber/launcher"
50
50
  l = Calabash::Cucumber::Launcher.launcher_if_used
51
51
  l && l.device
52
52
  end
@@ -133,6 +133,7 @@ module Calabash
133
133
  # @raise [RuntimeError] If the server cannot be reached.
134
134
  # @return [RunLoop::Version] The version of the iOS running on the device.
135
135
  def ios_version
136
+ require "run_loop/version"
136
137
  RunLoop::Version.new(_default_device_or_create.ios_version)
137
138
  end
138
139
 
@@ -15,12 +15,15 @@ module Calabash
15
15
  def self.ping_app
16
16
  endpoint = Calabash::Cucumber::Environment.device_endpoint
17
17
  url = URI.parse(endpoint)
18
-
18
+ path = url.path
19
19
  http = Net::HTTP.new(url.host, url.port)
20
+ if url.scheme == "https"
21
+ http.use_ssl = true
22
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
23
+ end
20
24
  response = http.start do |sess|
21
- sess.request(Net::HTTP::Get.new("version"))
25
+ sess.request(Net::HTTP::Get.new("#{path}version"))
22
26
  end
23
-
24
27
  body = nil
25
28
  success = response.is_a?(Net::HTTPSuccess)
26
29
  if success
@@ -112,4 +115,3 @@ If your app is crashing at launch, find a crash report to determine the cause.
112
115
  end
113
116
  end
114
117
  end
115
-
@@ -1,83 +1,21 @@
1
- require 'calabash-cucumber/core'
2
- require 'calabash-cucumber/tests_helpers'
3
- require 'calabash-cucumber/environment_helpers'
4
-
5
1
  module Calabash
6
2
  module Cucumber
7
3
 
8
- # Raised when there is a problem involving a keyboard mode. There are
9
- # three keyboard modes: docked, split, and undocked.
10
- #
11
- # All iPads support these keyboard modes, but the user can disable them
12
- # in Settings.app.
13
- #
14
- # The iPhone 6+ family also supports keyboard modes, but Calabash does
15
- # support keyboard modes on these devices.
16
- class KeyboardModeError < StandardError; ; end
17
-
18
4
  # Collection of methods for interacting with the keyboard.
19
5
  #
20
6
  # We've gone to great lengths to provide the fastest keyboard entry possible.
21
- #
22
- # If you are having trouble with skipped or are receiving JSON octet
23
- # errors when typing, you might be able to resolve the problems by slowing
24
- # down the rate of typing.
25
- #
26
- # Example: Use keyboard_enter_char + :wait_after_char.
27
- #
28
- # ```
29
- # str.each_char do |char|
30
- # # defaults to 0.05 seconds
31
- # keyboard_enter_char(char, `{wait_after_char:0.5}`)
32
- # end
33
- # ```
34
- #
35
- # Example: Use keyboard_enter_char + POST_ENTER_KEYBOARD
36
- #
37
- # ```
38
- # $ POST_ENTER_KEYBOARD=0.1 bundle exec cucumber
39
- # str.each_char do |char|
40
- # # defaults to 0.05 seconds
41
- # keyboard_enter_char(char)
42
- # end
43
- # ```
44
- #
45
- # @note
46
- # We have an exhaustive set of keyboard related test.s The API is reasonably
47
- # stable. We are fighting against known bugs in Apple's UIAutomation. You
48
- # should only need to fall back to the examples below in unusual situations.
49
7
  module KeyboardHelpers
50
8
 
51
- include Calabash::Cucumber::TestsHelpers
52
-
53
- # @!visibility private
54
- KEYPLANE_NAMES = {
55
- :small_letters => 'small-letters',
56
- :capital_letters => 'capital-letters',
57
- :numbers_and_punctuation => 'numbers-and-punctuation',
58
- :first_alternate => 'first-alternate',
59
- :numbers_and_punctuation_alternate => 'numbers-and-punctuation-alternate'
60
- }
61
-
62
- # @!visibility private
63
- # noinspection RubyStringKeysInHashInspection
64
- UIA_SUPPORTED_CHARS = {
65
- 'Delete' => '\b',
66
- 'Return' => '\n'
67
- # these are not supported yet and I am pretty sure that they
68
- # cannot be touched by passing an escaped character and instead
69
- # the must be found using UIAutomation calls. -jmoody
70
- #'Dictation' => nil,
71
- #'Shift' => nil,
72
- #'International' => nil,
73
- #'More' => nil,
74
- }
9
+ # This module is expected to be included in Calabash::Cucumber::Core.
10
+ # Core includes necessary methods from:
11
+ #
12
+ # StatusBarHelpers
13
+ # EnvironmentHelpers
14
+ # WaitHelpers
15
+ # FailureHelpers
16
+ # UIA
75
17
 
76
- # @!visibility private
77
- # Returns a query string for detecting a keyboard.
78
- def _qstr_for_keyboard
79
- "view:'UIKBKeyplaneView'"
80
- end
18
+ require "calabash-cucumber/map"
81
19
 
82
20
  # Returns true if a docked keyboard is visible.
83
21
  #
@@ -87,21 +25,21 @@ module Calabash
87
25
  #
88
26
  # @return [Boolean] if a keyboard is visible and docked.
89
27
  def docked_keyboard_visible?
90
- res = query(_qstr_for_keyboard).first
28
+ keyboard = _query_for_keyboard
91
29
 
92
- return false if res.nil?
30
+ return false if keyboard.nil?
93
31
 
94
32
  return true if device_family_iphone?
95
33
 
96
- orientation = status_bar_orientation.to_sym
97
- keyboard_height = res['rect']['height']
98
- keyboard_y = res['rect']['y']
99
- scale = screen_dimensions[:scale]
34
+ keyboard_height = keyboard['rect']['height']
35
+ keyboard_y = keyboard['rect']['y']
36
+ dimensions = screen_dimensions
37
+ scale = dimensions[:scale]
100
38
 
101
- if orientation == :left || orientation == :right
102
- screen_height = screen_dimensions[:width]/scale
39
+ if landscape?
40
+ screen_height = dimensions[:width]/scale
103
41
  else
104
- screen_height = screen_dimensions[:height]/scale
42
+ screen_height = dimensions[:height]/scale
105
43
  end
106
44
 
107
45
  screen_height - keyboard_height == keyboard_y
@@ -116,10 +54,10 @@ module Calabash
116
54
  def undocked_keyboard_visible?
117
55
  return false if device_family_iphone?
118
56
 
119
- res = query(_qstr_for_keyboard).first
120
- return false if res.nil?
57
+ keyboard = _query_for_keyboard
58
+ return false if keyboard.nil?
121
59
 
122
- not docked_keyboard_visible?
60
+ !docked_keyboard_visible?
123
61
  end
124
62
 
125
63
  # Returns true if a split keyboard is visible.
@@ -131,15 +69,26 @@ module Calabash
131
69
  # keyboards on the Phone and iPod are docked and not split.
132
70
  def split_keyboard_visible?
133
71
  return false if device_family_iphone?
134
- query("view:'UIKBKeyView'").count > 0 and
135
- element_does_not_exist(_qstr_for_keyboard)
72
+ _query_for_split_keyboard && !_query_for_keyboard
136
73
  end
137
74
 
138
75
  # Returns true if there is a visible keyboard.
139
76
  #
140
77
  # @return [Boolean] Returns true if there is a visible keyboard.
141
78
  def keyboard_visible?
142
- docked_keyboard_visible? or undocked_keyboard_visible? or split_keyboard_visible?
79
+ # Order matters!
80
+ docked_keyboard_visible? ||
81
+ undocked_keyboard_visible? ||
82
+ split_keyboard_visible?
83
+ end
84
+
85
+ # @!visibility private
86
+ # Raises an error ir the keyboard is not visible.
87
+ def expect_keyboard_visible!
88
+ if !keyboard_visible?
89
+ screenshot_and_raise "Keyboard is not visible"
90
+ end
91
+ true
143
92
  end
144
93
 
145
94
  # Waits for a keyboard to appear and once it does appear waits for
@@ -148,608 +97,47 @@ module Calabash
148
97
  # @see Calabash::Cucumber::WaitHelpers#wait_for for other options this
149
98
  # method can handle.
150
99
  #
151
- # @param [Hash] opts controls the `wait_for` behavior
100
+ # @param [Hash] options controls the `wait_for` behavior
152
101
  # @option opts [String] :timeout_message ('keyboard did not appear')
153
102
  # Controls the message that appears in the error.
154
103
  # @option opts [Number] :post_timeout (0.3) Controls how long to wait
155
104
  # _after_ the keyboard has appeared.
156
105
  #
157
106
  # @raise [Calabash::Cucumber::WaitHelpers::WaitError] if no keyboard appears
158
- def wait_for_keyboard(opts={})
159
- default_opts = {:timeout_message => 'keyboard did not appear',
160
- :post_timeout => 0.3}
161
- opts = default_opts.merge(opts)
162
- wait_for(opts) do
107
+ def wait_for_keyboard(options={})
108
+ default_opts = {
109
+ :timeout_message => "Keyboard did not appear",
110
+ :post_timeout => 0.3
111
+ }
112
+
113
+ merged_opts = default_opts.merge(options)
114
+ wait_for(merged_opts) do
163
115
  keyboard_visible?
164
116
  end
117
+ true
165
118
  end
166
119
 
167
- # @!visibility private
168
- # returns an array of possible ipad keyboard modes
169
- def _ipad_keyboard_modes
170
- [:docked, :undocked, :split]
171
- end
172
-
173
- # Returns the keyboard mode.
174
- #
175
- # @example How to use in a wait_* function.
176
- # wait_for do
177
- # ipad_keyboard_mode({:raise_on_no_visible_keyboard => false}) == :split
178
- # end
179
- #
180
- # ```
181
- # keyboard is pinned to bottom of the view #=> :docked
182
- # keyboard is floating in the middle of the view #=> :undocked
183
- # keyboard is floating and split #=> :split
184
- # no keyboard and :raise_on_no_visible_keyboard == false #=> :unknown
185
- # ```
186
- #
187
- # @raise [RuntimeError] if the device under test is not an iPad.
120
+ # Waits for a keyboard to disappear.
188
121
  #
189
- # @raise [RuntimeError] if `:raise_on_no_visible_keyboard` is truthy and
190
- # no keyboard is visible.
191
- # @param [Hash] opts controls the runtime behavior.
192
- # @option opts [Boolean] :raise_on_no_visible_keyboard (true) set to false
193
- # if you don't want to raise an error.
194
- # @return [Symbol] Returns one of `{:docked | :undocked | :split | :unknown}`
195
- def ipad_keyboard_mode(opts = {})
196
- raise 'the keyboard mode does not exist on the iphone or ipod' if device_family_iphone?
197
-
198
- default_opts = {:raise_on_no_visible_keyboard => true}
199
- merged_opts = default_opts.merge(opts)
200
- if merged_opts[:raise_on_no_visible_keyboard]
201
- screenshot_and_raise 'there is no visible keyboard' unless keyboard_visible?
202
- return :docked if docked_keyboard_visible?
203
- return :undocked if undocked_keyboard_visible?
204
- :split
205
- else
206
- return :docked if docked_keyboard_visible?
207
- return :undocked if undocked_keyboard_visible?
208
- return :split if split_keyboard_visible?
209
- :unknown
210
- end
211
- end
212
-
213
- # @!visibility private
214
- # Ensures that there is a keyboard to enter text.
215
- #
216
- # @note
217
- # *IMPORTANT* will always raise an error when the keyboard is split and
218
- # there is no `run_loop`; i.e. UIAutomation is not available.
219
- #
220
- # @param [Hash] opts controls screenshot-ing and error raising conditions
221
- # @option opts [Boolean] :screenshot (true) raise with a screenshot if
222
- # a keyboard cannot be ensured
223
- # @option opts [Boolean] :skip (false) skip any checking (a nop) - used
224
- # when iterating over keyplanes for keys
225
- def _ensure_can_enter_text(opts={})
226
- default_opts = {:screenshot => true,
227
- :skip => false}
228
- opts = default_opts.merge(opts)
229
- return if opts[:skip]
230
-
231
- screenshot = opts[:screenshot]
232
- if !keyboard_visible?
233
- msg = "No visible keyboard."
234
- if screenshot
235
- screenshot_and_raise msg
236
- else
237
- raise msg
238
- end
239
- end
240
- end
241
-
242
- # Use keyboard to enter a character.
243
- #
244
- # @note
245
- # IMPORTANT: Use the `POST_ENTER_KEYBOARD` environmental variable
246
- # to slow down the typing; adds a wait after each character is touched.
247
- # this can fix problems where the typing is too fast and characters are
248
- # skipped.
249
- #
250
- # @note
251
- # There are several special 'characters', some of which do not appear on
252
- # all keyboards; e.g. `Delete`, `Return`.
253
- #
254
- # @note
255
- # Since 0.9.163, this method accepts a Hash as the second parameter. The
256
- # previous second parameter was a Boolean that controlled whether or not
257
- # to screenshot on errors.
258
- #
259
- # @see #keyboard_enter_text
260
- #
261
- # @note
262
- # You should prefer to call `keyboard_enter_text`.
263
- #
264
- # @raise [RuntimeError] if there is no visible keyboard
265
- # @raise [RuntimeError] if the keyboard (layout) is not supported
266
- #
267
- # @param [String] chr the character to type
268
- # @param [Hash] opts options to control the behavior of the method
269
- # @option opts [Boolean] :should_screenshot (true) whether or not to
270
- # screenshot on errors
271
- # @option opts [Float] :wait_after_char ('POST_ENTER_KEYBOARD' or 0.05)
272
- # how long to wait after a character is typed.
273
- def keyboard_enter_char(chr, opts={})
274
- default_opts = {:should_screenshot => true,
275
- # introduce a small wait to avoid skipping characters
276
- # keep this as short as possible
277
- :wait_after_char => (ENV['POST_ENTER_KEYBOARD'] || 0.05).to_f}
278
-
279
- opts = default_opts.merge(opts)
280
-
281
- should_screenshot = opts[:should_screenshot]
282
- _ensure_can_enter_text({:screenshot => should_screenshot,
283
- :skip => (not should_screenshot)})
284
-
285
- if chr.length == 1
286
- uia_type_string_raw chr
287
- else
288
- code = UIA_SUPPORTED_CHARS[chr]
289
-
290
- unless code
291
- raise "typing character '#{chr}' is not yet supported when running with Instruments"
292
- end
293
-
294
- # on iOS 6, the Delete char code is _not_ \b
295
- # on iOS 7, the Delete char code is \b on non-numeric keyboards
296
- # on numeric keyboards, it is actually a button on the
297
- # keyboard and not a key
298
- if code.eql?(UIA_SUPPORTED_CHARS['Delete'])
299
- js_tap_delete = "(function() {"\
300
- "var deleteElement = uia.keyboard().elements().firstWithName('Delete');"\
301
- "deleteElement = deleteElement.isValid() ? deleteElement : uia.keyboard().elements().firstWithName('delete');"\
302
- "deleteElement.tap();"\
303
- "})();"
304
- uia(js_tap_delete)
305
- else
306
- uia_type_string_raw(code)
307
- end
308
- end
309
- # noinspection RubyStringKeysInHashInspection
310
- res = {'results' => []}
311
-
312
- if ENV['POST_ENTER_KEYBOARD']
313
- w = ENV['POST_ENTER_KEYBOARD'].to_f
314
- if w > 0
315
- sleep(w)
316
- end
317
- end
318
- pause = opts[:wait_after_char]
319
- sleep(pause) if pause > 0
320
- res['results']
321
- end
322
-
323
- # Uses the keyboard to enter text.
324
- #
325
- # @param [String] text the text to type.
326
- # @raise [RuntimeError] if the text cannot be typed.
327
- def keyboard_enter_text(text)
328
- _ensure_can_enter_text
329
- text_before = _text_from_first_responder()
330
- text_before = text_before.gsub("\n","\\n") if text_before
331
- uia_type_string(text, text_before)
332
- end
333
-
334
- # @!visibility private
335
- #
336
- # Enters text into view identified by a query
337
- #
338
- # @note
339
- # *IMPORTANT* enter_text defaults to calling 'setValue' in UIAutomation
340
- # on the text field. This is fast, but in some cases might result in slightly
341
- # different behaviour than using `keyboard_enter_text`.
342
- # To force use of `keyboard_enter_text` in `enter_text` use
343
- # option :use_keyboard
344
- #
345
- # @param [String] uiquery the element to enter text into
346
- # @param [String] text the text to enter
347
- # @param [Hash] options controls details of text entry
348
- # @option options [Boolean] :use_keyboard (false) use the iOS keyboard
349
- # to enter each character separately
350
- # @option options [Boolean] :wait (true) call wait_for_element_exists with uiquery
351
- # @option options [Hash] :wait_options ({}) if :wait pass this as options to wait_for_element_exists
352
- def enter_text(uiquery, text, options = {})
353
- default_opts = {:use_keyboard => false, :wait => true, :wait_options => {}}
354
- options = default_opts.merge(options)
355
- wait_for_element_exists(uiquery, options[:wait_options]) if options[:wait]
356
- touch(uiquery, options)
357
- wait_for_keyboard
358
- if options[:use_keyboard]
359
- keyboard_enter_text(text)
360
- else
361
- fast_enter_text(text)
362
- end
363
- end
364
-
365
- # @!visibility private
366
- #
367
- # Enters text into current text input field
368
- #
369
- # @note
370
- # *IMPORTANT* fast_enter_text defaults to calling 'setValue' in UIAutomation
371
- # on the text field. This is fast, but in some cases might result in slightly
372
- # different behaviour than using `keyboard_enter_text`.
373
- # @param [String] text the text to enter
374
- def fast_enter_text(text)
375
- _ensure_can_enter_text
376
- uia_set_responder_value(text)
377
- end
378
-
379
- # Touches the keyboard action key.
380
- #
381
- # The action key depends on the keyboard. Some examples include:
382
- #
383
- # * Return
384
- # * Next
385
- # * Go
386
- # * Join
387
- # * Search
388
- #
389
- # @note
390
- # Not all keyboards have an action key. For example, numeric keyboards
391
- # do not have an action key.
392
- #
393
- # @raise [RuntimeError] if the text cannot be typed.
394
- def tap_keyboard_action_key
395
- keyboard_enter_char 'Return'
396
- end
397
-
398
- # @!visibility private
399
- # Returns the current keyplane.
400
- def _current_keyplane
401
- kp_arr = _do_keyplane(
402
- lambda { query("view:'UIKBKeyplaneView'", 'keyplane', 'componentName') },
403
- lambda { query("view:'UIKBKeyplaneView'", 'keyplane', 'name') })
404
- kp_arr.first.downcase
405
- end
406
-
407
- # @!visibility private
408
- # Searches the available keyplanes for chr and if it is found, types it.
409
- #
410
- # This is a recursive function.
411
- #
412
- # @note
413
- # Use the `KEYPLANE_SEARCH_STEP_PAUSE` variable to control how quickly
414
- # the next keyplane is searched. Increase this value if you encounter
415
- # problems with missed keystrokes.
416
- #
417
- # @note
418
- # When running under instruments, this method is not called.
419
- #
420
- # @raise [RuntimeError] if the char cannot be found
421
- def _search_keyplanes_and_enter_char(chr, visited=Set.new)
422
- cur_kp = _current_keyplane
423
- begin
424
- keyboard_enter_char(chr, {:should_screenshot => false})
425
- return true #found
426
- rescue
427
- pause = (ENV['KEYPLANE_SEARCH_STEP_PAUSE'] || 0.2).to_f
428
- sleep (pause) if pause > 0
429
-
430
- visited.add(cur_kp)
431
-
432
- #figure out keyplane alternates
433
- props = _do_keyplane(
434
- lambda { query("view:'UIKBKeyplaneView'", 'keyplane', 'properties') },
435
- lambda { query("view:'UIKBKeyplaneView'", 'keyplane', 'attributes', 'dict') }
436
- ).first
437
-
438
- known = KEYPLANE_NAMES.values
439
-
440
- found = false
441
- keyplane_selection_keys = ['shift', 'more']
442
- keyplane_selection_keys.each do |key|
443
- sleep (pause) if pause > 0
444
- plane = props["#{key}-alternate"]
445
- if known.member?(plane) and (not visited.member?(plane))
446
- keyboard_enter_char(key.capitalize, {:should_screenshot => false})
447
- found = _search_keyplanes_and_enter_char(chr, visited)
448
- return true if found
449
- #not found => try with other keyplane selection key
450
- keyplane_selection_keys.delete(key)
451
- other_key = keyplane_selection_keys.last
452
- keyboard_enter_char(other_key.capitalize, {:should_screenshot => false})
453
- found = _search_keyplanes_and_enter_char(chr, visited)
454
- return true if found
455
- end
456
- end
457
- return false
458
- end
459
- end
460
-
461
- # @!visibility private
462
- # Process a keyplane.
463
- #
464
- # @raise [RuntimeError] if there is no visible keyplane
465
- def _do_keyplane(kbtree_proc, keyplane_proc)
466
- desc = query("view:'UIKBKeyplaneView'", 'keyplane')
467
- fail('No keyplane (UIKBKeyplaneView keyplane)') if desc.empty?
468
- fail('Several keyplanes (UIKBKeyplaneView keyplane)') if desc.count > 1
469
- kp_desc = desc.first
470
- if /^<UIKBTree/.match(kp_desc)
471
- #ios5+
472
- kbtree_proc.call
473
- elsif /^<UIKBKeyplane/.match(kp_desc)
474
- #ios4
475
- keyplane_proc.call
476
- end
477
- end
478
-
479
- # @!visibility private
480
- # Returns a query string for finding the iPad 'Hide keyboard' button.
481
- def _query_uia_hide_keyboard_button
482
- "uia.keyboard().buttons()['Hide keyboard']"
483
- end
484
-
485
- # Dismisses a iPad keyboard by touching the 'Hide keyboard' button and waits
486
- # for the keyboard to disappear.
487
- #
488
- # @note
489
- # the dismiss keyboard key does not exist on the iPhone or iPod
490
- #
491
- # @raise [RuntimeError] if the device is not an iPad
492
- def dismiss_ipad_keyboard
493
- screenshot_and_raise "Cannot dismiss keyboard on iPhone" if device_family_iphone?
494
- send_uia_command({:command => "#{_query_uia_hide_keyboard_button}.tap()"})
495
-
496
- opts = {:timeout_message => 'keyboard did not disappear'}
497
- wait_for(opts) do
498
- not keyboard_visible?
499
- end
500
- end
501
-
502
- # @!visibility private
503
- # Returns the activation point of the iPad keyboard mode key.
504
- #
505
- # The mode key is also known as the 'Hide keyboard' key.
506
- #
507
- # @note
508
- # This is only available when running under instruments.
509
- #
510
- # @raise [RuntimeError] when the device is not an iPad
511
- # @raise [RuntimeError] the app was not launched with instruments
512
- def _point_for_ipad_keyboard_mode_key
513
- raise "The keyboard mode does not exist on the on the iphone" if device_family_iphone?
514
- res = send_uia_command({:command => "#{_query_uia_hide_keyboard_button}.rect()"})
515
- origin = res['value']['origin']
516
- {:x => origin['x'], :y => origin['y']}
517
- end
518
-
519
- # @!visibility private
520
- # Touches the bottom option on the popup dialog that is presented when the
521
- # the iPad keyboard `mode` key is touched and held.
522
- #
523
- # The `mode` key is also know as the 'Hide keyboard' key.
524
- #
525
- # The `mode` key allows the user to undock, dock, or split the keyboard.
526
- def _touch_bottom_keyboard_mode_row
527
- start_pt = _point_for_ipad_keyboard_mode_key
528
- # there are 10 pt btw the key and the popup and the row is 50 pt
529
- y_offset = 10 + 25
530
- end_pt = {:x => (start_pt[:x] - 40), :y => (start_pt[:y] - y_offset)}
531
- uia_pan_offset(start_pt, end_pt, {})
532
- sleep(1.0)
533
- end
534
-
535
- # Touches the top option on the popup dialog that is presented when the
536
- # the iPad keyboard mode key is touched and held.
537
- #
538
- # The `mode` key is also know as the 'Hide keyboard' key.
539
- #
540
- # The `mode` key allows the user to undock, dock, or split the keyboard.
541
- def _touch_top_keyboard_mode_row
542
- start_pt = _point_for_ipad_keyboard_mode_key
543
- # there are 10 pt btw the key and the popup and each row is 50 pt
544
- # NB: no amount of offsetting seems to allow touching the top row
545
- # when the keyboard is split
546
-
547
- x_offset = 40
548
- y_offset = 10 + 50 + 25
549
- end_pt = {:x => (start_pt[:x] - x_offset), :y => (start_pt[:y] - y_offset)}
550
- uia_pan_offset(start_pt, end_pt, {:duration => 1.0})
551
- end
552
-
553
- # Ensures that the iPad keyboard is docked.
554
- #
555
- # Docked means the keyboard is pinned to bottom of the view.
556
- #
557
- # If the device is not an iPad, this is behaves like a call to
558
- # `wait_for_keyboard`.
559
- #
560
- # @raise [RuntimeError] if there is no visible keyboard
561
- # @raise [RuntimeError] a docked keyboard was not achieved
562
- def ensure_docked_keyboard
563
- wait_for_keyboard
564
-
565
- return if device_family_iphone?
566
-
567
- mode = ipad_keyboard_mode
568
-
569
- return if mode == :docked
570
-
571
- if ios9?
572
- raise KeyboardModeError,
573
- 'Changing keyboard modes is not supported on iOS 9'
574
- else
575
- case mode
576
- when :split then
577
- _touch_bottom_keyboard_mode_row
578
- when :undocked then
579
- _touch_top_keyboard_mode_row
580
- when :docked then
581
- # already docked
582
- else
583
- screenshot_and_raise "expected '#{mode}' to be one of #{_ipad_keyboard_modes}"
584
- end
585
- end
586
-
587
- begin
588
- wait_for({:post_timeout => 1.0}) do
589
- docked_keyboard_visible?
590
- end
591
- rescue
592
- mode = ipad_keyboard_mode
593
- o = status_bar_orientation
594
- screenshot_and_raise "expected keyboard to be ':docked' but found '#{mode}' in orientation '#{o}'"
595
- end
596
- end
597
-
598
- # Ensures that the iPad keyboard is undocked.
599
- #
600
- # Undocked means the keyboard is floating in the middle of the view.
601
- #
602
- # If the device is not an iPad, this is behaves like a call to
603
- # `wait_for_keyboard`.
604
- #
605
- # If the device is not an iPad, this is behaves like a call to
606
- # `wait_for_keyboard`.
607
- #
608
- # @raise [RuntimeError] if there is no visible keyboard
609
- # @raise [RuntimeError] an undocked keyboard was not achieved
610
- def ensure_undocked_keyboard
611
- wait_for_keyboard
612
-
613
- return if device_family_iphone?
614
-
615
- mode = ipad_keyboard_mode
616
-
617
- return if mode == :undocked
618
-
619
- if ios9?
620
- raise KeyboardModeError,
621
- 'Changing keyboard modes is not supported on iOS 9'
622
- else
623
- case mode
624
- when :split then
625
- # keep these condition separate because even though they do the same
626
- # thing, the else condition is a hack
627
- if ios5?
628
- # iOS 5 has no 'Merge' feature in split keyboard, so dock first then
629
- # undock from docked mode
630
- _touch_bottom_keyboard_mode_row
631
- _wait_for_keyboard_in_mode(:docked)
632
- else
633
- # in iOS > 5, it seems to be impossible consistently touch the
634
- # the top keyboard mode popup button, so we punt
635
- _touch_bottom_keyboard_mode_row
636
- _wait_for_keyboard_in_mode(:docked)
637
- end
638
- _touch_top_keyboard_mode_row
639
- when :undocked then
640
- # already undocked
641
- when :docked then
642
- _touch_top_keyboard_mode_row
643
- else
644
- screenshot_and_raise "expected '#{mode}' to be one of #{_ipad_keyboard_modes}"
645
- end
646
- end
647
- _wait_for_keyboard_in_mode(:undocked)
648
- end
649
-
650
-
651
- # Ensures that the iPad keyboard is split.
652
- #
653
- # Split means the keyboard is floating in the middle of the view and is
654
- # split into two sections to enable faster thumb typing.
655
- #
656
- # If the device is not an iPad, this is behaves like a call to
657
- # `wait_for_keyboard`.
658
- #
659
- # If the device is not an iPad, this is behaves like a call to
660
- # `wait_for_keyboard`.
661
- #
662
- # @raise [RuntimeError] if there is no visible keyboard
663
- # @raise [RuntimeError] a split keyboard was not achieved
664
- def ensure_split_keyboard
665
- wait_for_keyboard
666
-
667
- return if device_family_iphone?
668
-
669
- mode = ipad_keyboard_mode
670
-
671
- return if mode == :split
672
-
673
- if ios9?
674
- raise KeyboardModeError,
675
- 'Changing keyboard modes is not supported on iOS 9'
676
- else
677
- case mode
678
- when :split then
679
- # already split
680
- when :undocked then
681
- _touch_bottom_keyboard_mode_row
682
- when :docked then
683
- _touch_bottom_keyboard_mode_row
684
- else
685
- screenshot_and_raise "expected '#{mode}' to be one of #{_ipad_keyboard_modes}"
686
- end
687
- end
688
- _wait_for_keyboard_in_mode(:split)
689
- end
690
-
691
- # @!visibility private
692
- def _wait_for_keyboard_in_mode(mode, opts={})
693
- default_opts = {:post_timeout => 1.0}
694
- opts = default_opts.merge(opts)
695
- begin
696
- wait_for(opts) do
697
- case mode
698
- when :split then
699
- split_keyboard_visible?
700
- when :undocked
701
- undocked_keyboard_visible?
702
- when :docked
703
- docked_keyboard_visible?
704
- else
705
- screenshot_and_raise "expected '#{mode}' to be one of #{_ipad_keyboard_modes}"
706
- end
707
- end
708
- rescue
709
- actual = ipad_keyboard_mode
710
- o = status_bar_orientation
711
- screenshot_and_raise "expected keyboard to be '#{mode}' but found '#{actual}' in orientation '#{o}'"
712
- end
713
- end
714
-
715
- # Used for detecting keyboards that are not normally visible to calabash;
716
- # e.g. the keyboard on the `MFMailComposeViewController`
717
- #
718
- # @note
719
- # IMPORTANT this should only be used when the app does not respond to
720
- # `keyboard_visible?`.
721
- #
722
- # @see #keyboard_visible?
723
- #
724
- # @raise [RuntimeError] if the app was not launched with instruments
725
- def uia_keyboard_visible?
726
- res = uia_query_windows(:keyboard)
727
- not res.eql?(':nil')
728
- end
729
-
730
- # Waits for a keyboard that is not normally visible to calabash;
731
- # e.g. the keyboard on `MFMailComposeViewController`.
732
- #
733
- # @note
734
- # IMPORTANT this should only be used when the app does not respond to
735
- # `keyboard_visible?`.
122
+ # @see Calabash::Cucumber::WaitHelpers#wait_for for other options this
123
+ # method can handle.
736
124
  #
737
- # @see #keyboard_visible?
125
+ # @param [Hash] options controls the `wait_for` behavior
126
+ # @option opts [String] :timeout_message ('keyboard did not appear')
127
+ # Controls the message that appears in the error.
738
128
  #
739
- # @raise [RuntimeError] if the app was not launched with instruments
740
- def uia_wait_for_keyboard(opts={})
741
- default_opts = {:timeout => 10,
742
- :retry_frequency => 0.1,
743
- :post_timeout => 0.5}
744
- opts = default_opts.merge(opts)
745
- unless opts[:timeout_message]
746
- msg = "waited for '#{opts[:timeout]}' for keyboard"
747
- opts[:timeout_message] = msg
748
- end
129
+ # @raise [Calabash::Cucumber::WaitHelpers::WaitError] If keyboard does
130
+ # not disappear.
131
+ def wait_for_no_keyboard(options={})
132
+ default_opts = {
133
+ :timeout_message => "Keyboard is visible",
134
+ }
749
135
 
750
- wait_for(opts) do
751
- uia_keyboard_visible?
136
+ merged_opts = default_opts.merge(options)
137
+ wait_for(merged_opts) do
138
+ !keyboard_visible?
752
139
  end
140
+ true
753
141
  end
754
142
 
755
143
  # Waits for a keyboard to appear and returns the localized name of the
@@ -780,17 +168,47 @@ module Calabash
780
168
  # the first responder.
781
169
  #
782
170
  # @raise [RuntimeError] if there is no visible keyboard
783
- def _text_from_first_responder
784
- raise 'there must be a visible keyboard' unless keyboard_visible?
171
+ def text_from_first_responder
172
+ if !keyboard_visible?
173
+ screenshot_and_raise "There must be a visible keyboard"
174
+ end
785
175
 
786
176
  ['textField', 'textView'].each do |ui_class|
787
- res = query("#{ui_class} isFirstResponder:1", :text)
788
- return res.first unless res.empty?
177
+ query = "#{ui_class} isFirstResponder:1"
178
+ result = _query_wrapper(query, :text)
179
+ if !result.empty?
180
+ return result.first
181
+ end
789
182
  end
790
- #noinspection RubyUnnecessaryReturnStatement
791
- return ''
183
+ ""
792
184
  end
793
185
 
186
+ # @visibility private
187
+ # TODO Remove in 0.21.0
188
+ alias_method :_text_from_first_responder, :text_from_first_responder
189
+
190
+ private
191
+
192
+ # @!visibility private
193
+ KEYBOARD_QUERY = "view:'UIKBKeyplaneView'"
194
+
195
+ # @!visibility private
196
+ SPLIT_KEYBOARD_QUERY = "view:'UIKBKeyView'"
197
+
198
+ # @!visibility private
199
+ def _query_wrapper(query, *args)
200
+ Calabash::Cucumber::Map.map(query, :query, *args)
201
+ end
202
+
203
+ # @!visibility private
204
+ def _query_for_keyboard
205
+ _query_wrapper(KEYBOARD_QUERY).first
206
+ end
207
+
208
+ # @!visibility private
209
+ def _query_for_split_keyboard
210
+ _query_wrapper(SPLIT_KEYBOARD_QUERY).first
211
+ end
794
212
  end
795
213
  end
796
214
  end