calabash 1.2.1 → 1.9.9.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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