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.
- 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
|