calabash-cucumber 0.19.2 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
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