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.
- checksums.yaml +4 -4
- data/dylibs/libCalabashDyn.dylib +0 -0
- data/dylibs/libCalabashDynSim.dylib +0 -0
- data/lib/calabash-cucumber.rb +9 -2
- data/lib/calabash-cucumber/abstract.rb +23 -0
- data/lib/calabash-cucumber/automator/automator.rb +158 -0
- data/lib/calabash-cucumber/automator/coordinates.rb +401 -0
- data/lib/calabash-cucumber/automator/device_agent.rb +424 -0
- data/lib/calabash-cucumber/automator/instruments.rb +441 -0
- data/lib/calabash-cucumber/connection_helpers.rb +1 -0
- data/lib/calabash-cucumber/core.rb +632 -138
- data/lib/calabash-cucumber/device_agent.rb +346 -0
- data/lib/calabash-cucumber/dot_dir.rb +1 -0
- data/lib/calabash-cucumber/environment.rb +1 -0
- data/lib/calabash-cucumber/environment_helpers.rb +4 -3
- data/lib/calabash-cucumber/http/http.rb +6 -4
- data/lib/calabash-cucumber/keyboard_helpers.rb +97 -679
- data/lib/calabash-cucumber/launcher.rb +107 -31
- data/lib/calabash-cucumber/log_tailer.rb +46 -0
- data/lib/calabash-cucumber/map.rb +7 -1
- data/lib/calabash-cucumber/rotation_helpers.rb +47 -139
- data/lib/calabash-cucumber/status_bar_helpers.rb +51 -20
- data/lib/calabash-cucumber/store/preferences.rb +3 -0
- data/lib/calabash-cucumber/uia.rb +333 -2
- data/lib/calabash-cucumber/usage_tracker.rb +2 -0
- data/lib/calabash-cucumber/version.rb +2 -2
- data/lib/calabash-cucumber/wait_helpers.rb +2 -0
- data/lib/calabash/formatters/html.rb +6 -1
- data/lib/frank-calabash.rb +10 -4
- data/scripts/.irbrc +3 -0
- data/staticlib/calabash.framework.zip +0 -0
- data/staticlib/libFrankCalabash.a +0 -0
- metadata +11 -6
- 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
|
@@ -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
|
-
|
52
|
-
|
53
|
-
#
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
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
|
-
|
28
|
+
keyboard = _query_for_keyboard
|
91
29
|
|
92
|
-
return false if
|
30
|
+
return false if keyboard.nil?
|
93
31
|
|
94
32
|
return true if device_family_iphone?
|
95
33
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
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
|
102
|
-
screen_height =
|
39
|
+
if landscape?
|
40
|
+
screen_height = dimensions[:width]/scale
|
103
41
|
else
|
104
|
-
screen_height =
|
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
|
-
|
120
|
-
return false if
|
57
|
+
keyboard = _query_for_keyboard
|
58
|
+
return false if keyboard.nil?
|
121
59
|
|
122
|
-
|
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
|
-
|
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
|
-
|
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]
|
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(
|
159
|
-
default_opts = {
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
-
#
|
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
|
-
# @
|
190
|
-
#
|
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
|
-
# @
|
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 [
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
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
|
-
|
751
|
-
|
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
|
784
|
-
|
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
|
-
|
788
|
-
|
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
|
-
|
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
|