calabash 1.2.1 → 1.9.9.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +39 -0
  3. data/LICENSE +204 -21
  4. data/README.md +36 -6
  5. data/VERSIONING.md +16 -0
  6. data/bin/calabash +95 -0
  7. data/lib/calabash.rb +185 -1
  8. data/lib/calabash/android.rb +64 -0
  9. data/lib/calabash/android/adb.rb +277 -0
  10. data/lib/calabash/android/application.rb +110 -0
  11. data/lib/calabash/android/build.rb +12 -0
  12. data/lib/calabash/android/build/application.rb +13 -0
  13. data/lib/calabash/android/build/build_error.rb +11 -0
  14. data/lib/calabash/android/build/builder.rb +119 -0
  15. data/lib/calabash/android/build/java_keystore.rb +177 -0
  16. data/lib/calabash/android/build/resigner.rb +56 -0
  17. data/lib/calabash/android/build/test_server.rb +27 -0
  18. data/lib/calabash/android/console_helpers.rb +44 -0
  19. data/lib/calabash/android/cucumber.rb +3 -0
  20. data/lib/calabash/android/device.rb +965 -0
  21. data/lib/calabash/android/environment.rb +470 -0
  22. data/lib/calabash/android/gestures.rb +369 -0
  23. data/lib/calabash/android/interactions.rb +45 -0
  24. data/lib/calabash/android/lib/.irbrc +55 -0
  25. data/lib/calabash/android/lib/AndroidManifest.xml +51 -0
  26. data/lib/calabash/android/lib/TestServer.apk +0 -0
  27. data/lib/calabash/android/lib/calmd5/arm64-v8a/calmd5 +0 -0
  28. data/lib/calabash/android/lib/calmd5/arm64-v8a/calmd5-pie +0 -0
  29. data/lib/calabash/android/lib/calmd5/armeabi-v7a/calmd5 +0 -0
  30. data/lib/calabash/android/lib/calmd5/armeabi-v7a/calmd5-pie +0 -0
  31. data/lib/calabash/android/lib/calmd5/armeabi/calmd5 +0 -0
  32. data/lib/calabash/android/lib/calmd5/armeabi/calmd5-pie +0 -0
  33. data/lib/calabash/android/lib/calmd5/mips/calmd5 +0 -0
  34. data/lib/calabash/android/lib/calmd5/mips/calmd5-pie +0 -0
  35. data/lib/calabash/android/lib/calmd5/mips64/calmd5 +0 -0
  36. data/lib/calabash/android/lib/calmd5/mips64/calmd5-pie +0 -0
  37. data/lib/calabash/android/lib/calmd5/x86/calmd5 +0 -0
  38. data/lib/calabash/android/lib/calmd5/x86/calmd5-pie +0 -0
  39. data/lib/calabash/android/lib/calmd5/x86_64/calmd5 +0 -0
  40. data/lib/calabash/android/lib/calmd5/x86_64/calmd5-pie +0 -0
  41. data/lib/calabash/android/lib/screenshot_taker.jar +0 -0
  42. data/lib/calabash/android/life_cycle.rb +37 -0
  43. data/lib/calabash/android/orientation.rb +30 -0
  44. data/lib/calabash/android/physical_buttons.rb +39 -0
  45. data/lib/calabash/android/screenshot.rb +9 -0
  46. data/lib/calabash/android/scroll.rb +5 -0
  47. data/lib/calabash/android/server.rb +10 -0
  48. data/lib/calabash/android/text.rb +54 -0
  49. data/lib/calabash/application.rb +74 -0
  50. data/lib/calabash/cli.rb +12 -0
  51. data/lib/calabash/cli/build.rb +33 -0
  52. data/lib/calabash/cli/console.rb +90 -0
  53. data/lib/calabash/cli/generate.rb +110 -0
  54. data/lib/calabash/cli/helpers.rb +130 -0
  55. data/lib/calabash/cli/resign.rb +33 -0
  56. data/lib/calabash/cli/run.rb +99 -0
  57. data/lib/calabash/cli/setup_keystore.rb +39 -0
  58. data/lib/calabash/color.rb +32 -0
  59. data/lib/calabash/console_helpers.rb +90 -0
  60. data/lib/calabash/defaults.rb +56 -0
  61. data/lib/calabash/device.rb +401 -0
  62. data/lib/calabash/environment.rb +75 -0
  63. data/lib/calabash/gestures.rb +384 -0
  64. data/lib/calabash/http.rb +8 -0
  65. data/lib/calabash/http/error.rb +15 -0
  66. data/lib/calabash/http/request.rb +42 -0
  67. data/lib/calabash/http/retriable_client.rb +156 -0
  68. data/lib/calabash/interactions.rb +105 -0
  69. data/lib/calabash/ios.rb +37 -0
  70. data/lib/calabash/ios/application.rb +119 -0
  71. data/lib/calabash/ios/conditions.rb +79 -0
  72. data/lib/calabash/ios/console_helpers.rb +72 -0
  73. data/lib/calabash/ios/device.rb +24 -0
  74. data/lib/calabash/ios/device/device_implementation.rb +779 -0
  75. data/lib/calabash/ios/device/gestures_mixin.rb +167 -0
  76. data/lib/calabash/ios/device/keyboard_mixin.rb +133 -0
  77. data/lib/calabash/ios/device/physical_device_mixin.rb +266 -0
  78. data/lib/calabash/ios/device/rotation_mixin.rb +124 -0
  79. data/lib/calabash/ios/device/routes/backdoor_route_mixin.rb +86 -0
  80. data/lib/calabash/ios/device/routes/condition_route_mixin.rb +62 -0
  81. data/lib/calabash/ios/device/routes/error.rb +8 -0
  82. data/lib/calabash/ios/device/routes/handle_route_mixin.rb +102 -0
  83. data/lib/calabash/ios/device/routes/map_route_mixin.rb +38 -0
  84. data/lib/calabash/ios/device/routes/playback_route_mixin.rb +70 -0
  85. data/lib/calabash/ios/device/routes/response_parser.rb +48 -0
  86. data/lib/calabash/ios/device/routes/uia_route_mixin.rb +238 -0
  87. data/lib/calabash/ios/device/runtime_attributes.rb +184 -0
  88. data/lib/calabash/ios/device/status_bar_mixin.rb +17 -0
  89. data/lib/calabash/ios/device/text_mixin.rb +19 -0
  90. data/lib/calabash/ios/device/uia_keyboard_mixin.rb +188 -0
  91. data/lib/calabash/ios/device/uia_mixin.rb +12 -0
  92. data/lib/calabash/ios/environment.rb +41 -0
  93. data/lib/calabash/ios/interactions.rb +10 -0
  94. data/lib/calabash/ios/lib/.irbrc +55 -0
  95. data/lib/calabash/ios/lib/recordings/rotate_left_home_down_ipad.base64 +2 -0
  96. data/lib/calabash/ios/lib/recordings/rotate_left_home_down_iphone.base64 +2 -0
  97. data/lib/calabash/ios/lib/recordings/rotate_left_home_left_ipad.base64 +2 -0
  98. data/lib/calabash/ios/lib/recordings/rotate_left_home_left_iphone.base64 +2 -0
  99. data/lib/calabash/ios/lib/recordings/rotate_left_home_right_ipad.base64 +2 -0
  100. data/lib/calabash/ios/lib/recordings/rotate_left_home_right_iphone.base64 +2 -0
  101. data/lib/calabash/ios/lib/recordings/rotate_left_home_up_ipad.base64 +2 -0
  102. data/lib/calabash/ios/lib/recordings/rotate_left_home_up_iphone.base64 +2 -0
  103. data/lib/calabash/ios/lib/recordings/rotate_right_home_down_ipad.base64 +2 -0
  104. data/lib/calabash/ios/lib/recordings/rotate_right_home_down_iphone.base64 +2 -0
  105. data/lib/calabash/ios/lib/recordings/rotate_right_home_left_ipad.base64 +2 -0
  106. data/lib/calabash/ios/lib/recordings/rotate_right_home_left_iphone.base64 +2 -0
  107. data/lib/calabash/ios/lib/recordings/rotate_right_home_right_ipad.base64 +2 -0
  108. data/lib/calabash/ios/lib/recordings/rotate_right_home_right_iphone.base64 +2 -0
  109. data/lib/calabash/ios/lib/recordings/rotate_right_home_up_ipad.base64 +2 -0
  110. data/lib/calabash/ios/lib/recordings/rotate_right_home_up_iphone.base64 +2 -0
  111. data/lib/calabash/ios/orientation.rb +117 -0
  112. data/lib/calabash/ios/scroll.rb +504 -0
  113. data/lib/calabash/ios/server.rb +73 -0
  114. data/lib/calabash/ios/text.rb +248 -0
  115. data/lib/calabash/ios/uia.rb +24 -0
  116. data/lib/calabash/lib/skeleton/config/cucumber.yml +6 -0
  117. data/lib/calabash/lib/skeleton/features/sample.feature +5 -0
  118. data/lib/calabash/lib/skeleton/features/step_definitions/calabash_steps.rb +29 -0
  119. data/lib/calabash/lib/skeleton/features/support/env.rb +54 -0
  120. data/lib/calabash/lib/skeleton/features/support/hooks.rb +83 -0
  121. data/lib/calabash/life_cycle.rb +111 -0
  122. data/lib/calabash/location.rb +51 -0
  123. data/lib/calabash/logger.rb +87 -0
  124. data/lib/calabash/orientation.rb +84 -0
  125. data/lib/calabash/page.rb +35 -0
  126. data/lib/calabash/patch.rb +14 -0
  127. data/lib/calabash/patch/array.rb +16 -0
  128. data/lib/calabash/patch/run_loop.rb +90 -0
  129. data/lib/calabash/query.rb +160 -0
  130. data/lib/calabash/query_result.rb +85 -0
  131. data/lib/calabash/screenshot.rb +89 -0
  132. data/lib/calabash/server.rb +16 -0
  133. data/lib/calabash/text.rb +76 -0
  134. data/lib/calabash/utility.rb +58 -0
  135. data/lib/calabash/version.rb +3 -1
  136. data/lib/calabash/wait.rb +474 -0
  137. metadata +462 -24
@@ -0,0 +1,504 @@
1
+ module Calabash
2
+ module IOS
3
+
4
+ # Scrolling gestures for UIScrollView, UITableView, MKMapView, UIWebView,
5
+ # WKWebView, and UICollectionView.
6
+ #
7
+ # Starting iOS, Apple's official UIAutomation API has been broken on the
8
+ # iOS Simulators for these methods:
9
+ #
10
+ # * dragInsideWithOptions
11
+ # * flickInsideWithOptions
12
+ #
13
+ # The result is that gestures like swipe and flick do not work on
14
+ # iOS Simulators.
15
+ #
16
+ # Calabash iOS implements several scrolling methods that allow you to work
17
+ # around these bugs.
18
+ #
19
+ # @see Calabash::Gestures#pan
20
+ # @see Calabash::Gestures#flick
21
+ module Scroll
22
+
23
+ # Scrolls the first view matching `query` in `direction`.
24
+ #
25
+ # View are scrolled half of their frame size.
26
+ #
27
+ # If `query` matches a view that is not a UIScrollView or a subclass, an
28
+ # error will be raised. UITableView, MKMapView, UIWebView, WKWebView, and
29
+ # UICollectionView are all examples of subclasses of UIScrollView.
30
+ #
31
+ # An error will be raised if more than on view is matched by `query`.
32
+ #
33
+ # To avoid matching more than one UIScrollView (or subclass):
34
+ # * Make the query more specific: "UITableView marked:'table'"
35
+ # * Use the index language feature: "UIScrollView index:0"
36
+ #
37
+ # @example
38
+ # scroll("UITableView", :down)
39
+ #
40
+ # @note This is implemented by calling the Objective-C
41
+ # `setContentOffset:animated:` method and can do things users cannot.
42
+ #
43
+ # This is the only alternative for `pan` and `flick` which do not work
44
+ # on iOS Simulators starting with iOS 7.
45
+ #
46
+ # @see Calabash::Gestures#pan
47
+ # @see Calabash::Gestures#flick
48
+ #
49
+ # @param [String,Query,Hash] query A query describing the view to scroll.
50
+ # @param [Symbol] direction The direction to scroll. Valid directions are:
51
+ # 'up', 'down', 'left', and 'right'
52
+ #
53
+ # @raise [ArgumentError] If direction is invalid.
54
+ # @raise [ArgumentError] If query is invalid.
55
+ # @raise [ViewNotFoundError] If query matches no views.
56
+ # @raise [RuntimeError] If query matches more than one view.
57
+ # @raise [RuntimeError] If query matches a view that is not a UIScrollView.
58
+ def scroll(query, direction)
59
+ allowed_directions = [:up, :down, :left, :right]
60
+ dir_symbol = direction.to_sym
61
+ unless allowed_directions.include?(dir_symbol)
62
+ raise ArgumentError,
63
+ "Expected '#{direction}' to be one of #{allowed_directions.join(',')}"
64
+ end
65
+
66
+ Query.ensure_valid_query(query)
67
+
68
+ begin
69
+ view_to_scroll = _wait_for_exactly_one_scroll_view(query)
70
+ rescue RuntimeError => e
71
+ raise RuntimeError, e
72
+ end
73
+
74
+ results = Device.default.map_route(query, :scroll, direction)
75
+
76
+ if results.first.nil?
77
+ fail("Expected '#{query}' to match a UIScrollView or a subclass")
78
+ end
79
+
80
+ Calabash::QueryResult.create([view_to_scroll], query)
81
+ end
82
+
83
+ # Scroll the UITableView matching `query` to `row` in `section`.
84
+ #
85
+ # If `query` matches a view that is not a UITableView or a subclass, an
86
+ # error will be raised.
87
+ #
88
+ # An error will be raised if more than on view is matched by `query`.
89
+ #
90
+ # To avoid matching more than one UITableView (or subclass):
91
+ # * Make the query more specific: "UITableView marked:'table'"
92
+ # * Use the index language feature: "UITableView index:1"
93
+ #
94
+ # Row and section are zero indexed. The first row is row 0.
95
+ #
96
+ # @example
97
+ # # Scroll to the 5th row in the first section.
98
+ # > scroll_to_row("UITableView", 5)
99
+ #
100
+ # # Scroll to the 3rd row, in the 5th section
101
+ # > scroll_to_row("UITableView", 3, 5)
102
+ #
103
+ # @note This is implement by calling the Objective-C
104
+ # `scrollToRowAtIndexPath:atScrollPosition:animated:` method and can do
105
+ # things that users cannot.
106
+ #
107
+ # This is the only alternative for `pan` and `flick` which do not work
108
+ # on iOS Simulators starting with iOS 7.
109
+ #
110
+ # @see Calabash::Gestures#pan
111
+ # @see Calabash::Gestures#flick
112
+ #
113
+ # @param [String,Query,Hash] query A query describing the table to scroll.
114
+ # @param [Numeric] row The row number to scroll to.
115
+ # @param [Numeric] section The section number to scroll to.
116
+ # @param [Hash] options Options to control the scroll behavior.
117
+ # @option options [Symbol] :scroll_position (:middle) The final position
118
+ # of the row in the view. Can be :top, :middle, :bottom
119
+ # @option options [Boolean] :animate (true) Should the scrolling be
120
+ # animated?
121
+ #
122
+ # @raise [ArgumentError] If the :scroll_position is invalid.
123
+ # @raise [ArgumentError] If the :animate key is not a Boolean.
124
+ # @raise [ArgumentError] If query is invalid.
125
+ # @raise [ViewNotFoundError] If query matches no views.
126
+ # @raise [RuntimeError] If query matches more than one view.
127
+ # @raise [RuntimeError] If query matches a view that is not a UITableView.
128
+ # @raise [RuntimeError] If the row and section are invalid for the table.
129
+ def scroll_to_row(query, row, section=0, **options)
130
+ default_options = {
131
+ :scroll_position => :middle,
132
+ :animate => true
133
+ }
134
+
135
+ merged_options = default_options.merge(options)
136
+
137
+ begin
138
+ _expect_valid_scroll_options(VALID_TABLE_SCROLL_POSITIONS, merged_options)
139
+ rescue ArgumentError => e
140
+ raise ArgumentError, e
141
+ end
142
+
143
+ Query.ensure_valid_query(query)
144
+
145
+ begin
146
+ view_to_scroll = _wait_for_exactly_one_scroll_view(query)
147
+ rescue RuntimeError => e
148
+ raise RuntimeError, e
149
+ end
150
+
151
+ position = merged_options[:scroll_position].to_sym
152
+ animate = merged_options[:animate]
153
+
154
+ results = Device.default.map_route(query, :scrollToRow, row.to_i,
155
+ section.to_i, position, animate)
156
+
157
+ if results.first.nil?
158
+ message = [
159
+ "Could not scroll table to row '#{row}' and section '#{section}'.",
160
+ "Either query '#{query}' did not match a UITableView or",
161
+ "the row '#{row}' in section '#{section}' does not exist."
162
+ ].join("\n")
163
+ fail(message)
164
+ end
165
+
166
+ Calabash::QueryResult.create([view_to_scroll], query)
167
+ end
168
+
169
+ # Scroll the UITableView matching `query` to the row with `mark`. This
170
+ # method is particularly useful when testing tables with dynamic content.
171
+ #
172
+ # An error will be raised If `query` matches a view that is not a
173
+ # UITableView or a subclass.
174
+ #
175
+ # To avoid matching more than one UITableView (or subclass):
176
+ # * Make the query more specific: "UITableView marked:'table'"
177
+ # * Use the index language feature: "UITableView index:1"
178
+ #
179
+ # The `mark` can be on any subview in a UITableViewCell or the cell
180
+ # itself. If no cell with `mark` can be found, an error will be raised.
181
+ #
182
+ # @example
183
+ # > scroll_to_row_with_mark("UITableView", "apples")
184
+ #
185
+ # @note This is implement by calling the Objective-C
186
+ # `scrollToRowAtIndexPath:atScrollPosition:animated:` method and can do
187
+ # things that users cannot. The implementation generates a new cell
188
+ # for every index path in your table. This can cause performance
189
+ # issues if your table is very large.
190
+ #
191
+ # This is the only alternative for `pan` and `flick` which do not work
192
+ # on iOS Simulators starting with iOS 7.
193
+ #
194
+ # @see Calabash::Gestures#pan
195
+ # @see Calabash::Gestures#flick
196
+ #
197
+ # @param [String,Query,Hash] query A query describing the table to scroll.
198
+ # @param [String] mark The cell identifier.
199
+ # @param [Hash] options Options to control the scroll behavior.
200
+ # @option options [Symbol] :scroll_position (:middle) The final position
201
+ # of the row in the view. Can be :top, :middle, :bottom
202
+ # @option options [Boolean] :animate (true) Should the scrolling be
203
+ # animated?
204
+ #
205
+ # @raise [ArgumentError] If mark is nil or the empty string.
206
+ # @raise [ArgumentError] If the :scroll_position is invalid.
207
+ # @raise [ArgumentError] If the :animate key is not a Boolean.
208
+ # @raise [ArgumentError] If query is invalid.
209
+ # @raise [ViewNotFoundError] If query matches no views.
210
+ # @raise [RuntimeError] If query matches more than one view.
211
+ # @raise [RuntimeError] If query matches a view that is not a UITableView.
212
+ # @raise [RuntimeError] If no cell with `mark` is found.
213
+ def scroll_to_row_with_mark(query, mark, options={})
214
+ default_options = {
215
+ :scroll_position => :middle,
216
+ :animate => true
217
+ }
218
+
219
+ merged_options = default_options.merge(options)
220
+
221
+ begin
222
+ _expect_valid_scroll_options(VALID_TABLE_SCROLL_POSITIONS, merged_options)
223
+ _expect_valid_scroll_mark(mark)
224
+ rescue ArgumentError => e
225
+ raise ArgumentError, e
226
+ end
227
+
228
+ Query.ensure_valid_query(query)
229
+
230
+ begin
231
+ view_to_scroll = _wait_for_exactly_one_scroll_view(query)
232
+ rescue RuntimeError => e
233
+ raise RuntimeError e
234
+ end
235
+
236
+ position = merged_options[:scroll_position].to_sym
237
+ animate = merged_options[:animate]
238
+
239
+ results = Device.default.map_route(query, :scrollToRowWithMark, mark,
240
+ position, animate)
241
+
242
+ if results.first.nil?
243
+ message = [
244
+ "Could not scroll table to row with mark: '#{mark}'",
245
+ "Either the '#{query}' did not match a UITableView or",
246
+ "there is no cell with mark '#{mark}'."
247
+ ].join("\n")
248
+ fail(message)
249
+ end
250
+
251
+ Calabash::QueryResult.create([view_to_scroll], query)
252
+ end
253
+
254
+ # Scrolls the UICollectionView matching `query` to `item` in `section`.
255
+ #
256
+ # If `query` matches a view that is not a UICollectionView or a subclass,
257
+ # an error will be raised.
258
+ #
259
+ # An error will be raised if more than on view is matched by `query`.
260
+ #
261
+ # To avoid matching more than one UICollectionView (or subclass):
262
+ # * Make the query more specific: "UICollectionView marked:'gallery'"
263
+ # * Use the index language feature: "UICollectionView index:1"
264
+ #
265
+ #
266
+ # Item and section are zero indexed. The first item is 0.
267
+ #
268
+ # @example
269
+ # # Scroll to the 5th item in the first section.
270
+ # > scroll_to_item("UICollectionView", 5)
271
+ #
272
+ # # Scroll to the 3rd row, in the 5th section
273
+ # > scroll_to_item("UICollectionView", 3, 5)
274
+ #
275
+ # @example The following are the allowed :scroll_position values.
276
+ # :top, :center_vertical, :bottom, :left, :center_horizontal, :right
277
+ #
278
+ # @note This is implement by calling the Objective-C
279
+ # `scrollToItemAtIndexPath:atScrollPosition:animated:` method and can do
280
+ # things that users cannot.
281
+ #
282
+ # This is the only alternative for `pan` and `flick` which do not work
283
+ # on iOS Simulators starting with iOS 7.
284
+ #
285
+ # @see Calabash::Gestures#pan
286
+ # @see Calabash::Gestures#flick
287
+ #
288
+ # @param [String,Query,Hash] query A query describing the table to scroll.
289
+ # @param [Numeric] item The item number to scroll to.
290
+ # @param [Numeric] section The section number to scroll to.
291
+ # @param [Hash] options Options to control the scroll behavior.
292
+ # @option options [Symbol] :scroll_position (:top) The final position
293
+ # of the row in the view. See the examples for valid positions.
294
+ # @option options [Boolean] :animate (true) Should the scrolling be
295
+ # animated?
296
+ #
297
+ # @raise [ArgumentError] If the :scroll_position is invalid.
298
+ # @raise [ArgumentError] If the :animate key is not a Boolean.
299
+ # @raise [ArgumentError] If query is invalid.
300
+ # @raise [ViewNotFoundError] If query matches no views.
301
+ # @raise [RuntimeError] If query matches more than one view.
302
+ # @raise [RuntimeError] If query matches a view that is not a
303
+ # UICollectionView.
304
+ # @raise [RuntimeError] If the item and section are not valid for
305
+ # the collection.
306
+ def scroll_to_item(query, item, section=0, **options)
307
+ default_options = {
308
+ :scroll_position => :top,
309
+ :animate => true
310
+ }
311
+
312
+ merged_options = default_options.merge(options)
313
+
314
+ begin
315
+ _expect_valid_scroll_options(VALID_COLLECTION_SCROLL_POSITIONS, merged_options)
316
+ rescue ArgumentError => e
317
+ raise ArgumentError, e
318
+ end
319
+
320
+ Query.ensure_valid_query(query)
321
+
322
+ begin
323
+ view_to_scroll = _wait_for_exactly_one_scroll_view(query)
324
+ rescue RuntimeError => e
325
+ raise RuntimeError e
326
+ end
327
+
328
+ position = merged_options[:scroll_position].to_sym
329
+ animate = merged_options[:animate]
330
+
331
+ results = Device.default.map_route(query, :collectionViewScroll,
332
+ item.to_i, section.to_i,
333
+ position, animate)
334
+ if results.first.nil?
335
+ message = [
336
+ "Could not scroll collection to item '#{item}' and section '#{section}'.",
337
+ "Either query '#{query}' did not match a UICollectionView or",
338
+ "the item '#{item}' in section '#{section}' does not exist."
339
+ ].join("\n")
340
+ fail(message)
341
+ end
342
+
343
+ Calabash::QueryResult.create([view_to_scroll], query)
344
+ end
345
+
346
+ # Scroll the UICollectionView matching `query` to the row with `mark`.
347
+ # This method is particularly useful when testing collections with dynamic
348
+ # content.
349
+ #
350
+ # An error will be raised If `query` matches a view that is not a
351
+ # UICollectionView or a subclass.
352
+ #
353
+ # To avoid matching more than one UICollectionView (or subclass):
354
+ # * Make the query more specific: "UICollectionView marked:'gallery'"
355
+ # * Use the index language feature: "UICollectionView index:1"
356
+ #
357
+ # The `mark` can be on any subview in a UICollectionViewCell or the cell
358
+ # itself. If no cell with `mark` can be found, an error will be raised.
359
+ #
360
+ # @example
361
+ # > scroll_to_item_with_mark("UICollectionView", "mom")
362
+ #
363
+ # @example The following are the allowed :scroll_position values.
364
+ # :top, :center_vertical, :bottom, :left, :center_horizontal, :right
365
+ #
366
+ # @note This is implement by calling the Objective-C
367
+ # `scrollToRowAtIndexPath:atScrollPosition:animated:` method and can do
368
+ # things that users cannot. The implementation generates a new item
369
+ # for every index path in your collection. This can cause performance
370
+ # issues if your collection is very large.
371
+ #
372
+ # This is the only alternative for `pan` and `flick` which do not work
373
+ # on iOS Simulators starting with iOS 7.
374
+ #
375
+ # @see Calabash::Gestures#pan
376
+ # @see Calabash::Gestures#flick
377
+ #
378
+ # @param [String,Query,Hash] query A query describing the collection to
379
+ # scroll.
380
+ # @param [String] mark The cell identifier.
381
+ # @param [Hash] options Options to control the scroll behavior.
382
+ # @option options [Symbol] :scroll_position (:top) The final position
383
+ # of the row in the view. See the examples for valid positions.
384
+ # @option options [Boolean] :animate (true) Should the scrolling be
385
+ # animated?
386
+ #
387
+ # @raise [ArgumentError] If mark is nil or the empty string.
388
+ # @raise [ArgumentError] If the :scroll_position is invalid.
389
+ # @raise [ArgumentError] If the :animate key is not a Boolean.
390
+ # @raise [ArgumentError] If query is invalid.
391
+ # @raise [ViewNotFoundError] If query matches no views.
392
+ # @raise [RuntimeError] If query matches more than one view.
393
+ # @raise [RuntimeError] If query matches a view that is not a
394
+ # UICollectionView.
395
+ # @raise [RuntimeError] If no item with `mark` is found.
396
+ def scroll_to_item_with_mark(query, mark, options={})
397
+ default_options = {
398
+ :scroll_position => :top,
399
+ :animate => true,
400
+ }
401
+
402
+ merged_options = default_options.merge(options)
403
+
404
+ begin
405
+ _expect_valid_scroll_options(VALID_COLLECTION_SCROLL_POSITIONS, merged_options)
406
+ _expect_valid_scroll_mark(mark)
407
+ rescue ArgumentError => e
408
+ raise ArgumentError, e
409
+ end
410
+
411
+ Query.ensure_valid_query(query)
412
+
413
+ begin
414
+ view_to_scroll = _wait_for_exactly_one_scroll_view(query)
415
+ rescue RuntimeError => e
416
+ raise RuntimeError e
417
+ end
418
+
419
+ position = merged_options[:scroll_position].to_sym
420
+ animate = merged_options[:animate]
421
+
422
+ results = Device.default.map_route(query,
423
+ :collectionViewScrollToItemWithMark,
424
+ mark, position, animate)
425
+
426
+ if results.first.nil?
427
+ message = [
428
+ "Could not scroll collection to item with mark: '#{mark}'",
429
+ "Either the '#{query}' did not match a UICollectionView or",
430
+ "there is no item with mark '#{mark}'."
431
+ ].join("\n")
432
+ fail(message)
433
+ end
434
+
435
+ Calabash::QueryResult.create([view_to_scroll], query)
436
+ end
437
+
438
+ private
439
+
440
+ # !@visibility private
441
+ VALID_TABLE_SCROLL_POSITIONS = [:top, :middle, :bottom]
442
+
443
+ # !@visibility private
444
+ VALID_COLLECTION_SCROLL_POSITIONS = [:top, :center_vertical, :bottom,
445
+ :left, :center_horizontal, :right]
446
+
447
+ # !@visibility private
448
+ def _wait_for_exactly_one_scroll_view(query)
449
+
450
+ results = []
451
+
452
+ found_none = "Expected '#{query}' to match exactly one view, but found no matches."
453
+ query_object = Query.new(query)
454
+ wait_for(found_none) do
455
+ results = query(query_object)
456
+ if results.length > 1
457
+ message = [
458
+ "Expected '#{query}' to match exactly one view, but found '#{results.length}'",
459
+ results.join("\n")
460
+ ].join("\n")
461
+ fail(message)
462
+ else
463
+ results.length == 1
464
+ end
465
+ end
466
+ results.first
467
+ end
468
+
469
+ # !@visibility private
470
+ def _expect_valid_scroll_positions(valid_positions, position)
471
+ unless valid_positions.include?(position.to_sym)
472
+ raise ArgumentError,
473
+ "Expected '#{position}' to be one of #{valid_positions.join(', ')}"
474
+ end
475
+ end
476
+
477
+ # !@visibility private
478
+ def _expect_valid_scroll_animate(animate)
479
+ unless [true, false].include?(animate)
480
+ raise ArgumentError,
481
+ "Expected '#{animate}' to be a Boolean true or false"
482
+ end
483
+ end
484
+
485
+ # !@visibility private
486
+ def _expect_valid_scroll_options(valid_positions, options)
487
+ _expect_valid_scroll_positions(valid_positions, options[:scroll_position])
488
+ _expect_valid_scroll_animate(options[:animate])
489
+ end
490
+
491
+ # !@visibility private
492
+ def _expect_valid_scroll_mark(mark)
493
+ if mark.nil? || mark == ''
494
+ raise ArgumentError,
495
+ if mark.nil?
496
+ 'Mark cannot be nil.'
497
+ else
498
+ 'Mark cannot be an empty string.'
499
+ end
500
+ end
501
+ end
502
+ end
503
+ end
504
+ end