frank-pivotal 1.2.3.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/Gemfile +4 -0
  4. data/Rakefile +38 -0
  5. data/bin/frank +6 -0
  6. data/bin/frank-skeleton +33 -0
  7. data/frank-pivotal.gemspec +37 -0
  8. data/frank-skeleton/features/my_first.feature +12 -0
  9. data/frank-skeleton/features/step_definitions/launch_steps.rb +20 -0
  10. data/frank-skeleton/features/support/env.rb +8 -0
  11. data/frank-skeleton/frank_static_resources.bundle/ViewAttributeMapping.plist +63 -0
  12. data/frank-skeleton/frank_static_resources.bundle/ViewAttributeMappingMac.plist +99 -0
  13. data/frank-skeleton/frank_static_resources.bundle/images/ajax-loader.gif +0 -0
  14. data/frank-skeleton/frank_static_resources.bundle/images/file.gif +0 -0
  15. data/frank-skeleton/frank_static_resources.bundle/images/folder-closed.gif +0 -0
  16. data/frank-skeleton/frank_static_resources.bundle/images/folder.gif +0 -0
  17. data/frank-skeleton/frank_static_resources.bundle/images/loader.gif +0 -0
  18. data/frank-skeleton/frank_static_resources.bundle/images/loader.png +0 -0
  19. data/frank-skeleton/frank_static_resources.bundle/images/minus.gif +0 -0
  20. data/frank-skeleton/frank_static_resources.bundle/images/plus.gif +0 -0
  21. data/frank-skeleton/frank_static_resources.bundle/images/treeview-black-line.gif +0 -0
  22. data/frank-skeleton/frank_static_resources.bundle/images/treeview-black.gif +0 -0
  23. data/frank-skeleton/frank_static_resources.bundle/images/treeview-default-line.gif +0 -0
  24. data/frank-skeleton/frank_static_resources.bundle/images/treeview-default.gif +0 -0
  25. data/frank-skeleton/frank_static_resources.bundle/images/treeview-famfamfam-line.gif +0 -0
  26. data/frank-skeleton/frank_static_resources.bundle/images/treeview-famfamfam.gif +0 -0
  27. data/frank-skeleton/frank_static_resources.bundle/images/treeview-gray-line.gif +0 -0
  28. data/frank-skeleton/frank_static_resources.bundle/images/treeview-gray.gif +0 -0
  29. data/frank-skeleton/frank_static_resources.bundle/images/treeview-red-line.gif +0 -0
  30. data/frank-skeleton/frank_static_resources.bundle/images/treeview-red.gif +0 -0
  31. data/frank-skeleton/frank_static_resources.bundle/index.html +86 -0
  32. data/frank-skeleton/frank_static_resources.bundle/index.html.haml +76 -0
  33. data/frank-skeleton/frank_static_resources.bundle/js/accessible_views_view.coffee +41 -0
  34. data/frank-skeleton/frank_static_resources.bundle/js/accessible_views_view.js +46 -0
  35. data/frank-skeleton/frank_static_resources.bundle/js/controller.coffee +134 -0
  36. data/frank-skeleton/frank_static_resources.bundle/js/controller.js +139 -0
  37. data/frank-skeleton/frank_static_resources.bundle/js/details_view.coffee +42 -0
  38. data/frank-skeleton/frank_static_resources.bundle/js/details_view.js +51 -0
  39. data/frank-skeleton/frank_static_resources.bundle/js/dropdown_control.coffee +64 -0
  40. data/frank-skeleton/frank_static_resources.bundle/js/dropdown_control.js +73 -0
  41. data/frank-skeleton/frank_static_resources.bundle/js/ersatz_model.coffee +46 -0
  42. data/frank-skeleton/frank_static_resources.bundle/js/ersatz_model.js +60 -0
  43. data/frank-skeleton/frank_static_resources.bundle/js/ersatz_view.coffee +167 -0
  44. data/frank-skeleton/frank_static_resources.bundle/js/ersatz_view.js +205 -0
  45. data/frank-skeleton/frank_static_resources.bundle/js/experiment_bar_model.coffee +10 -0
  46. data/frank-skeleton/frank_static_resources.bundle/js/experiment_bar_model.js +17 -0
  47. data/frank-skeleton/frank_static_resources.bundle/js/experiment_bar_view.coffee +44 -0
  48. data/frank-skeleton/frank_static_resources.bundle/js/experiment_bar_view.js +63 -0
  49. data/frank-skeleton/frank_static_resources.bundle/js/frank.coffee +96 -0
  50. data/frank-skeleton/frank_static_resources.bundle/js/frank.js +146 -0
  51. data/frank-skeleton/frank_static_resources.bundle/js/lib/backbone.js +1431 -0
  52. data/frank-skeleton/frank_static_resources.bundle/js/lib/coffee-script.js +8 -0
  53. data/frank-skeleton/frank_static_resources.bundle/js/lib/jquery-ui.min.js +405 -0
  54. data/frank-skeleton/frank_static_resources.bundle/js/lib/jquery.min.js +4 -0
  55. data/frank-skeleton/frank_static_resources.bundle/js/lib/jquery.treeview.js +251 -0
  56. data/frank-skeleton/frank_static_resources.bundle/js/lib/json2.js +481 -0
  57. data/frank-skeleton/frank_static_resources.bundle/js/lib/raphael.js +5815 -0
  58. data/frank-skeleton/frank_static_resources.bundle/js/lib/require.js +2053 -0
  59. data/frank-skeleton/frank_static_resources.bundle/js/lib/underscore.js +1059 -0
  60. data/frank-skeleton/frank_static_resources.bundle/js/main.coffee +27 -0
  61. data/frank-skeleton/frank_static_resources.bundle/js/main.js +29 -0
  62. data/frank-skeleton/frank_static_resources.bundle/js/tabs_controller.coffee +13 -0
  63. data/frank-skeleton/frank_static_resources.bundle/js/tabs_controller.js +22 -0
  64. data/frank-skeleton/frank_static_resources.bundle/js/toast_controller.coffee +15 -0
  65. data/frank-skeleton/frank_static_resources.bundle/js/toast_controller.js +28 -0
  66. data/frank-skeleton/frank_static_resources.bundle/js/transform_stack.coffee +59 -0
  67. data/frank-skeleton/frank_static_resources.bundle/js/transform_stack.js +78 -0
  68. data/frank-skeleton/frank_static_resources.bundle/js/tree_view.coffee +53 -0
  69. data/frank-skeleton/frank_static_resources.bundle/js/tree_view.js +64 -0
  70. data/frank-skeleton/frank_static_resources.bundle/js/view_hier_model.coffee +37 -0
  71. data/frank-skeleton/frank_static_resources.bundle/js/view_hier_model.js +48 -0
  72. data/frank-skeleton/frank_static_resources.bundle/js/view_model.coffee +39 -0
  73. data/frank-skeleton/frank_static_resources.bundle/js/view_model.js +62 -0
  74. data/frank-skeleton/frank_static_resources.bundle/pictos/index.html +329 -0
  75. data/frank-skeleton/frank_static_resources.bundle/pictos/pictos-web.eot +0 -0
  76. data/frank-skeleton/frank_static_resources.bundle/pictos/pictos-web.svg +114 -0
  77. data/frank-skeleton/frank_static_resources.bundle/pictos/pictos-web.ttf +0 -0
  78. data/frank-skeleton/frank_static_resources.bundle/pictos/pictos-web.woff +0 -0
  79. data/frank-skeleton/frank_static_resources.bundle/pictos/pictos.css +20 -0
  80. data/frank-skeleton/frank_static_resources.bundle/pictos/pictos_base64.css +18 -0
  81. data/frank-skeleton/frank_static_resources.bundle/stylesheets/css/symbiote.css +1 -0
  82. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_elements.scss +28 -0
  83. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_header.scss +61 -0
  84. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_inspect_tabs_list_tabs.scss +194 -0
  85. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_jquery.treeview.scss +68 -0
  86. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_jqui.scss +2 -0
  87. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_layout.scss +13 -0
  88. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_mixins.sass +137 -0
  89. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_reset.scss +32 -0
  90. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_selector_test_toolbar.scss +81 -0
  91. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_solarized.scss +16 -0
  92. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_typography.scss +11 -0
  93. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_unicode.scss +3 -0
  94. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_z_index.scss +2 -0
  95. data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/symbiote.scss +26 -0
  96. data/frank-skeleton/frankify.xcconfig.tt +6 -0
  97. data/frank-skeleton/libCocoaAsyncSocket.a +0 -0
  98. data/frank-skeleton/libCocoaAsyncSocketMac.a +0 -0
  99. data/frank-skeleton/libCocoaHTTPServer.a +0 -0
  100. data/frank-skeleton/libCocoaHTTPServerMac.a +0 -0
  101. data/frank-skeleton/libCocoaLumberjack.a +0 -0
  102. data/frank-skeleton/libCocoaLumberjackMac.a +0 -0
  103. data/frank-skeleton/libFrank.a +0 -0
  104. data/frank-skeleton/libFrankMac.a +0 -0
  105. data/frank-skeleton/libShelley.a +0 -0
  106. data/frank-skeleton/libShelleyMac.a +0 -0
  107. data/frank-skeleton/plugins/.empty_directory +0 -0
  108. data/lib/frank-pivotal/app_bundle_locator.rb +58 -0
  109. data/lib/frank-pivotal/bonjour.rb +78 -0
  110. data/lib/frank-pivotal/cli.rb +307 -0
  111. data/lib/frank-pivotal/color_helper.rb +13 -0
  112. data/lib/frank-pivotal/console.rb +28 -0
  113. data/lib/frank-pivotal/core_frank_steps.rb +264 -0
  114. data/lib/frank-pivotal/frank.xcconfig.erb +17 -0
  115. data/lib/frank-pivotal/frank_helper.rb +467 -0
  116. data/lib/frank-pivotal/frank_localize.rb +43 -0
  117. data/lib/frank-pivotal/frank_mac_helper.rb +120 -0
  118. data/lib/frank-pivotal/frankifier.rb +153 -0
  119. data/lib/frank-pivotal/gateway.rb +135 -0
  120. data/lib/frank-pivotal/gesture_helper.rb +99 -0
  121. data/lib/frank-pivotal/host_scripting.rb +102 -0
  122. data/lib/frank-pivotal/keyboard_helper.rb +69 -0
  123. data/lib/frank-pivotal/launcher.rb +118 -0
  124. data/lib/frank-pivotal/localize.yml +104 -0
  125. data/lib/frank-pivotal/location_helper.rb +20 -0
  126. data/lib/frank-pivotal/plugins/plugin.rb +57 -0
  127. data/lib/frank-pivotal/rect.rb +26 -0
  128. data/lib/frank-pivotal/scroll_helper.rb +24 -0
  129. data/lib/frank-pivotal/version.rb +5 -0
  130. data/lib/frank-pivotal/wait_helper.rb +57 -0
  131. data/lib/frank-pivotal.rb +14 -0
  132. data/test/keyboard_helper_test.rb +84 -0
  133. data/test/launcher_test.rb +57 -0
  134. data/test/rect_test.rb +25 -0
  135. data/test/test_helper.rb +16 -0
  136. metadata +367 -0
@@ -0,0 +1,467 @@
1
+ require 'json'
2
+ require 'frank-cucumber/gateway'
3
+ require 'frank-cucumber/host_scripting'
4
+ require 'frank-cucumber/wait_helper'
5
+ require 'frank-cucumber/keyboard_helper'
6
+ require 'frank-cucumber/scroll_helper'
7
+ require 'frank-cucumber/gesture_helper'
8
+ require 'frank-cucumber/location_helper'
9
+ require 'frank-cucumber/bonjour'
10
+ require 'frank-cucumber/rect.rb'
11
+
12
+ module Frank module Cucumber
13
+
14
+ # FrankHelper provides a core set of helper functions for use when interacting with Frank.
15
+ #
16
+ # == Most helpful methods
17
+ # * {#touch}
18
+ # * {#wait_for_element_to_exist}
19
+ # * {#wait_for_element_to_exist_and_then_touch_it}
20
+ # * {#wait_for_nothing_to_be_animating}
21
+ # * {#app_exec}
22
+ #
23
+ # == Configuring the Frank driver
24
+ # There are some class-level facilities which configure how all Frank interactions work. For example you can specify which selector engine to use
25
+ # with {FrankHelper.selector_engine}. You can specify the base url which the native app's Frank server is listening on with {FrankHelper.server_base_url}.
26
+ #
27
+ # Two common use cases are covered more conveniently with {FrankHelper.use_shelley_from_now_on} and {FrankHelper.test_on_physical_device_via_bonjour}.
28
+ module FrankHelper
29
+ include WaitHelper
30
+ include KeyboardHelper
31
+ include ScrollHelper
32
+ include GestureHelper
33
+ include HostScripting
34
+ include LocationHelper
35
+
36
+ # @!attribute [rw] selector_engine
37
+ class << self
38
+ # @return [String] the selector engine we tell Frank to use when interpreting view selectors.
39
+ attr_accessor :selector_engine
40
+ # @return [String] the base url which the Frank server is running on. All Frank commands will be sent to that server.
41
+ attr_accessor :server_base_url
42
+
43
+ # After calling this method all subsequent commands will ask Frank to use the Shelley selector engine to interpret view selectors.
44
+ def use_shelley_from_now_on
45
+ @selector_engine = 'shelley_compat'
46
+ end
47
+
48
+ # Use Bonjour to search for a running Frank server. The server found will be the recipient for all subsequent Frank commands.
49
+ # @raise a generic exception if no Frank server could be found via Bonjour
50
+ def test_on_physical_device_via_bonjour
51
+ @server_base_url = Bonjour.new.lookup_frank_base_uri
52
+ raise 'could not detect running Frank server' unless @server_base_url
53
+ end
54
+ end
55
+
56
+ # Get the correct quote for the selector
57
+ def get_selector_quote(selector)
58
+ if selector.index("'") == nil
59
+ return "'"
60
+ else
61
+ return '"'
62
+ end
63
+
64
+ # Specify ip address to run on
65
+ def test_on_physical_device_with_ip(ip_address)
66
+ @server_base_url = ip_address
67
+ raise 'IP Address is incorrect' unless @server_base_url.match(%r{\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b})
68
+ puts "Running on Frank server #{@server_base_url}"
69
+ end
70
+ end
71
+
72
+ #@api private
73
+ #@return [:String] convient shorthand for {Frank::Cucumber::FrankHelper.selector_engine}, defaulting to 'uiquery'
74
+ def selector_engine
75
+ Frank::Cucumber::FrankHelper.selector_engine || 'uiquery' # default to UIQuery for backwards compatibility
76
+ end
77
+
78
+ #@api private
79
+ #@return [:String] convient shorthand for {Frank::Cucumber::FrankHelper.server_base_url}
80
+ def base_server_url
81
+ Frank::Cucumber::FrankHelper.server_base_url
82
+ end
83
+
84
+ # Ask Frank to touch all views matching the specified selector. There may be views in the view heirarchy which match the selector but
85
+ # which Frank cannot or will not touch - for example views which are outside the current viewport. You can discover which of the matching
86
+ # views were actually touched by inspecting the Array which is returned.
87
+ #
88
+ # @param [String] selector a view selector.
89
+ # @return [Array<Boolean>] an array indicating for each view which matched the selector whether it was touched or not.
90
+ # @raise an expection if no views matched the selector
91
+ # @raise an expection if no views which matched the selector could be touched
92
+ def touch( selector )
93
+ touch_successes = frankly_map( selector, 'touch' )
94
+ raise "could not find anything matching [#{selector}] to touch" if touch_successes.empty?
95
+ raise "some views could not be touched (probably because they are not within the current viewport)" if touch_successes.include?(false)
96
+ touch_successes
97
+ end
98
+
99
+ # Fill in text in a text field.
100
+ #
101
+ # @param [String] the placeholder text for the desired text field
102
+ # @param [Hash{Symbol => String}] a hash with a :with key and a string of text to fill in
103
+ # @raise an exception if the :with key DSL syntax is missing
104
+ # @raise an exception if a text field with the given placeholder text could not be found
105
+ def fill_in( placeholder_field_name, options={} )
106
+ raise "Must pass a hash containing the key :with" unless (options.is_a?(Hash) && options.has_key?(:with))
107
+ text_to_type = options[:with]
108
+
109
+ quote = get_selector_quote(placeholder_field_name)
110
+ text_fields_modified = frankly_map( "textField placeholder:#{quote}#{placeholder_field_name}#{quote}", "setText:", text_to_type )
111
+ raise "could not find text fields with placeholder #{quote}#{placeholder_field_name}#{quote}" if text_fields_modified.empty?
112
+ #TODO raise warning if text_fields_modified.count > 1
113
+ end
114
+
115
+ # Indicate whether there are any views in the current view heirarchy which match the specified selector.
116
+ # @param [String] selector a view selector.
117
+ # @return [Boolean]
118
+ # @see #check_element_exists
119
+ def element_exists( selector )
120
+ matches = frankly_map( selector, 'FEX_accessibilityLabel' )
121
+ # TODO: raise warning if matches.count > 1
122
+ !matches.empty?
123
+ end
124
+
125
+ # Assert whether there are any views in the current view heirarchy which match the specified selector.
126
+ # @param [String] selector a view selector.
127
+ # @raise an rspec exception if the assertion fails
128
+ # @see #element_exists, #check_element_does_not_exist
129
+ def check_element_exists( selector )
130
+ element_exists( selector ).should be_true, "Could not find element matching selector (#{selector})"
131
+ end
132
+
133
+ def check_element_exists_and_is_visible( selector )
134
+ element_is_not_hidden( selector ).should be_true, "Could not find visible element matching selector (#{selector})"
135
+ end
136
+
137
+ # Assert whether there are no views in the current view heirarchy which match the specified selector.
138
+ # @param [String] selector a view selector.
139
+ # @raise an rspec exception if the assertion fails
140
+ # @see #element_exists, #check_element_exists
141
+ def check_element_does_not_exist( selector )
142
+ element_exists( selector ).should be_false, "Found element matching selector when it should not exist (#{selector})"
143
+ end
144
+
145
+ def check_element_does_not_exist_or_is_not_visible( selector )
146
+ element_is_not_hidden( selector ).should be_false, "Found visible element matching selector when it should not be visible (#{selector})"
147
+ end
148
+
149
+ # Indicate whether there are any views in the current view heirarchy which contain the specified accessibility label.
150
+ # @param [String] expected_mark the expected accessibility label
151
+ # @return [Boolean]
152
+ # @see #check_view_with_mark_exists
153
+ def view_with_mark_exists(expected_mark)
154
+ quote = get_selector_quote(expected_mark)
155
+ element_exists( "view marked:#{quote}#{expected_mark}#{quote}" )
156
+ end
157
+
158
+ # Assert whether there are any views in the current view heirarchy which contain the specified accessibility label.
159
+ # @param [String] expected_mark the expected accessibility label
160
+ # @raise an rspec exception if the assertion fails
161
+ # @see #view_with_mark_exists
162
+ def check_view_with_mark_exists(expected_mark)
163
+ quote = get_selector_quote(expected_mark)
164
+ check_element_exists( "view marked:#{quote}#{expected_mark}#{quote}" )
165
+ end
166
+
167
+ # Assert whether there are no views in the current view heirarchy which contain the specified accessibility label.
168
+ # @param [String] expected_mark the expected accessibility label
169
+ # @raise an rspec exception if the assertion fails
170
+ # @see #view_with_mark_exists, #check_view_with_mark_exists
171
+ def check_view_with_mark_does_not_exist(expected_mark)
172
+ quote = get_selector_quote(expected_mark)
173
+ check_element_does_not_exist( "view marked:#{quote}#{expected_mark}#{quote}" )
174
+ end
175
+
176
+
177
+ # Waits for any of the specified selectors to match a view.
178
+ #
179
+ # Checks each selector in turn within a {http://sauceio.com/index.php/2011/04/how-to-lose-races-and-win-at-selenium/ spin assert} loop and yields the first one which is found to exist in the view heirarchy.
180
+ # Raises an exception if no views could be found to match any of the provided selectors within {WaitHelper::TIMEOUT} seconds.
181
+ #
182
+ # @see WaitHelper#wait_until
183
+ def wait_for_element_to_exist(*selectors,&block)
184
+ wait_until(:message => "Waited for element matching any of #{selectors.join(', ')} to exist") do
185
+ at_least_one_exists = false
186
+ selectors.each do |selector|
187
+ if element_exists( selector )
188
+ at_least_one_exists = true
189
+ block.call(selector) if block
190
+ end
191
+ end
192
+ at_least_one_exists
193
+ end
194
+ end
195
+
196
+ # Waits for the specified selector to not match any views.
197
+ #
198
+ # Uses {WaitHelper#wait_until} to check for any matching views within a {http://sauceio.com/index.php/2011/04/how-to-lose-races-and-win-at-selenium/ spin assert} loop.
199
+ # Returns as soon as no views match the specified selector.
200
+ # Raises an exception if there continued to be at least one view which matched the selector by the time {WaitHelper::TIMEOUT} seconds passed.
201
+ #
202
+ # @see check_element_does_not_exist
203
+ # @see wait_for_element_to_not_exist
204
+ def wait_for_element_to_not_exist(selector)
205
+ wait_until(:message => "Waited for element #{selector} to not exist") do
206
+ !element_exists(selector)
207
+ end
208
+ end
209
+
210
+ # Waits for a view to exist and then send a touch command to that view.
211
+ #
212
+ # @param selectors takes one or more selectors to use to search for a view. The first selector which is found to matches a view is the selector
213
+ # which is then used to send a touch command.
214
+ #
215
+ # Raises an exception if no views could be found to match any of the provided selectors within {WaitHelper::TIMEOUT} seconds.
216
+ def wait_for_element_to_exist_and_then_touch_it(*selectors)
217
+ wait_for_element_to_exist(*selectors) do |sel|
218
+ touch(sel)
219
+ end
220
+ end
221
+
222
+ # Waits for there to be no views which report an isAnimated property of true.
223
+ #
224
+ # @param timeout [Number] number of seconds to wait for nothing to be animating before timeout out. Defaults to {WaitHelper::TIMEOUT}
225
+ #
226
+ # Raises an exception if there were still views animating after {timeout} seconds.
227
+ def wait_for_nothing_to_be_animating( timeout = false )
228
+ wait_until :timeout => timeout do
229
+ !element_exists('view isAnimating')
230
+ end
231
+ end
232
+
233
+
234
+ # Checks that the specified selector matches at least one view, and that at least one of the matched
235
+ # views has an isHidden property set to false
236
+ #
237
+ # a better name for this method would be element_exists_and_is_not_hidden
238
+ def element_is_not_hidden(selector)
239
+ matches = frankly_map( selector, 'FEX_isVisible' )
240
+ matches.delete(false)
241
+ !matches.empty?
242
+ end
243
+
244
+ def accessibility_frame(selector)
245
+ frames = frankly_map( selector, 'FEX_accessibilityFrame' )
246
+ raise "the supplied selector [#{selector}] did not match any views" if frames.empty?
247
+ raise "the supplied selector [#{selector}] matched more than one views (#{frames.count} views matched)" if frames.count > 1
248
+ Rect.from_api_repr( frames.first )
249
+ end
250
+
251
+ def drag_with_initial_delay(args)
252
+ from, to = args.values_at(:from,:to)
253
+ raise ArgumentError.new('must specify a :from parameter') if from.nil?
254
+ raise ArgumentError.new('must specify a :to parameter') if to.nil?
255
+
256
+ dest_frame = accessibility_frame(to)
257
+
258
+ if is_mac
259
+ from_frame = accessibility_frame(from)
260
+
261
+ frankly_map( from, 'FEX_mouseDownX:y:', from_frame.center.x, from_frame.center.y )
262
+
263
+ sleep 0.3
264
+
265
+ frankly_map( from, 'FEX_dragToX:y:', dest_frame.center.x, dest_frame.center.y )
266
+
267
+ sleep 0.3
268
+
269
+ frankly_map( from, 'FEX_mouseUpX:y:', dest_frame.center.x, dest_frame.center.y )
270
+
271
+ else
272
+
273
+ frankly_map( from, 'FEX_dragWithInitialDelayToX:y:', dest_frame.center.x, dest_frame.center.y )
274
+
275
+ end
276
+
277
+ end
278
+
279
+
280
+ # Ask Frank to invoke the specified method on the app delegate of the iOS application under automation.
281
+ # @param method_sig [String] the method signature
282
+ # @param method_args the method arguments
283
+ #
284
+ # @example
285
+ # # the same as calling
286
+ # # [[[UIApplication sharedApplication] appDelegate] setServiceBaseUrl:@"http://example.com/my_api" withPort:8080]
287
+ # # from your native app
288
+ # app_exec( "setServiceBaseUrl:withPort:", "http://example.com/my_api", 8080 )
289
+ #
290
+ #
291
+ def app_exec(method_sig, *method_args)
292
+ operation_map = Gateway.build_operation_map(method_sig.to_s, method_args)
293
+
294
+ res = frank_server.send_post(
295
+ 'app_exec',
296
+ :operation => operation_map
297
+ )
298
+
299
+ return Gateway.evaluate_frankly_response( res, "app_exec #{method_sig}" )
300
+ end
301
+
302
+ # Ask Frank to execute an arbitrary Objective-C method on each view which matches the specified selector.
303
+ #
304
+ # @return [Array] an array with an element for each view matched by the selector, each element in the array gives the return value from invoking the specified method on that view.
305
+ def frankly_map( selector, method_name, *method_args )
306
+ operation_map = Gateway.build_operation_map(method_name.to_s, method_args)
307
+ res = frank_server.send_post(
308
+ 'map',
309
+ :query => selector,
310
+ :operation => operation_map,
311
+ :selector_engine => selector_engine
312
+ )
313
+
314
+ return Gateway.evaluate_frankly_response( res, "frankly_map #{selector} #{method_name}" )
315
+ end
316
+
317
+ # print a JSON-formatted dump of the current view heirarchy to stdout
318
+ def frankly_dump
319
+ res = frank_server.send_get( 'dump' )
320
+ puts JSON.pretty_generate(JSON.parse(res)) rescue puts res #dumping a super-deep DOM causes errors
321
+ end
322
+
323
+ # grab a screenshot of the application under automation and save it to the specified file.
324
+ #
325
+ # @param filename [String] where to save the screenshot image file
326
+ # @param subframe describes which section of the screen to grab. If unspecified then the entire screen will be captured. #TODO document what format this parameter takes.
327
+ # @param allwindows [Boolean] If true then all UIWindows in the current UIScreen will be included in the screenshot. If false then only the main window will be captured.
328
+ def frankly_screenshot(filename, subframe=nil, allwindows=true)
329
+ path = 'screenshot'
330
+ path += '/allwindows' if allwindows
331
+ path += "/frame/" + URI.escape(subframe) if (subframe != nil)
332
+
333
+ data = frank_server.send_get( path )
334
+
335
+ open(filename, "wb") do |file|
336
+ file.write(data)
337
+ end
338
+ end
339
+
340
+ # @return [Boolean] true if the device running the application currently in a portrait orientation
341
+ # @note wil return false if the device is in a flat or unknown orientation. Sometimes the iOS simulator will report this state when first launched.
342
+ def frankly_oriented_portrait?
343
+ 'portrait' == frankly_current_orientation
344
+ end
345
+
346
+ # @return [Boolean] true if the device running the application currently in a landscape orientation
347
+ # @note wil return false if the device is in a flat or unknown orientation. Sometimes the iOS simulator will report this state when first launched.
348
+ def frankly_oriented_landscape?
349
+ 'landscape' == frankly_current_orientation
350
+ end
351
+
352
+ # @return [String] the orientation of the device running the application under automation.
353
+ # @note this is a low-level API. In most cases you should use {frankly_oriented_portrait} or {frankly_oriented_landscape} instead.
354
+ def frankly_current_orientation
355
+ res = frank_server.send_get( 'orientation' )
356
+ orientation = JSON.parse( res )['orientation']
357
+ puts "orientation reported as '#{orientation}'" if $DEBUG
358
+ orientation
359
+ end
360
+
361
+
362
+ # set the device orientation
363
+ # @param orientation can be 'landscape','landscape_left','landscape_right','portrait', or 'portrait_upside_down'
364
+ def frankly_set_orientation(orientation)
365
+ orientation = orientation.to_s
366
+ orientation = 'landscape_left' if orientation == 'landscape'
367
+ res = frank_server.send_post( 'orientation', orientation )
368
+ return Gateway.evaluate_frankly_response( res, "set_orientation #{orientation}" )
369
+ end
370
+
371
+ # @return [Boolean] Does the device running the application have accessibility enabled.
372
+ # If accessibility is not enabled then a lot of Frank functionality will not work.
373
+ def frankly_is_accessibility_enabled
374
+ res = frank_server.send_get( 'accessibility_check' )
375
+ JSON.parse( res )['accessibility_enabled'] == 'true'
376
+ end
377
+
378
+ # wait for the application under automation to be ready to receive automation commands.
379
+ #
380
+ # Has some basic heuristics to cope with cases where the Frank server is intermittently available when first launching.
381
+ #
382
+ # @raise [Timeout::TimeoutError] if nothing is ready within 20 seconds
383
+ # @raise generic error if the device hosting the application does not appear to have accessibility enabled.
384
+ def wait_for_frank_to_come_up
385
+ num_consec_successes = 0
386
+ num_consec_failures = 0
387
+ Timeout.timeout(20) do
388
+ while num_consec_successes <= 6
389
+ if frankly_ping
390
+ num_consec_failures = 0
391
+ num_consec_successes += 1
392
+ else
393
+ num_consec_successes = 0
394
+ num_consec_failures += 1
395
+ if num_consec_failures >= 5 # don't show small timing errors
396
+ print (num_consec_failures == 5 ) ? "\n" : "\r"
397
+ print "PING FAILED" + "!"*num_consec_failures
398
+ end
399
+ end
400
+ STDOUT.flush
401
+ sleep 0.2
402
+ end
403
+
404
+ if num_consec_successes < 6
405
+ print (num_consec_successes == 1 ) ? "\n" : "\r"
406
+ print "FRANK!".slice(0,num_consec_successes)
407
+ STDOUT.flush
408
+ puts ''
409
+ end
410
+
411
+ if num_consec_failures >= 5
412
+ puts ''
413
+ end
414
+ end
415
+
416
+ unless frankly_is_accessibility_enabled
417
+ raise "ACCESSIBILITY DOES NOT APPEAR TO BE ENABLED ON YOUR SIMULATOR. Hit the home button, go to settings, select Accessibility, and turn the inspector on."
418
+ end
419
+ end
420
+
421
+ # @return [String] the name of the device currently running the application
422
+ # @note this is a low-level API. In most cases you should use {is_iphone}, {is_ipad} or {is_mac} instead.
423
+ def frankly_device_name
424
+ res = frank_server.send_get( 'device' )
425
+ device = JSON.parse( res )['device']
426
+ puts "device reported as '#{device}'" if $DEBUG
427
+ device
428
+ end
429
+
430
+ # @return [Boolean] is the device running the application an iPhone.
431
+ def is_iphone
432
+ return frankly_device_name == "iphone"
433
+ end
434
+
435
+ # @return [Boolean] is the device running the application an iPhone.
436
+ def is_ipad
437
+ return frankly_device_name == "ipad"
438
+ end
439
+
440
+ # @return [Boolean] is the device running the application a Mac.
441
+ def is_mac
442
+ return frankly_device_name == "mac"
443
+ end
444
+
445
+ # @return [String] the operating system version currently running the application
446
+ def frankly_os_version
447
+ res = frank_server.send_get( 'device' )
448
+ os_version = JSON.parse( res )['os_version']
449
+ puts "os_version reported as '#{os_version}'" if $DEBUG
450
+ os_version
451
+ end
452
+
453
+ # Check whether Frank is able to communicate with the application under automation
454
+ def frankly_ping
455
+ frank_server.ping
456
+ end
457
+
458
+ #@api private
459
+ #@return [Frank::Cucumber::Gateway] a gateway for sending Frank commands to the application under automation
460
+ def frank_server
461
+ @_frank_server ||= Frank::Cucumber::Gateway.new( base_server_url )
462
+ end
463
+
464
+ end
465
+
466
+
467
+ end end
@@ -0,0 +1,43 @@
1
+ require 'i18n'
2
+
3
+ module Frank
4
+ module Cucumber
5
+ module Localize
6
+
7
+ def self.system_locale
8
+ case ENV['LANG']
9
+ when /^fr_/
10
+ :fr
11
+ when /^de_/
12
+ :de
13
+ when /^ru_/
14
+ :ru
15
+ when /^zh_/
16
+ :zh
17
+ when /^ja_/
18
+ :ja
19
+ when /^es_/
20
+ :es
21
+ when /^it_/
22
+ :it
23
+ else
24
+ :en
25
+ end
26
+ end
27
+
28
+ def self.load_translations
29
+ if I18n.backend.send(:translations).size == 0
30
+ I18n.locale = self.system_locale
31
+ I18n.load_path = [ File.join(File.dirname(__FILE__), 'localize.yml') ]
32
+ I18n.backend.load_translations
33
+ end
34
+ end
35
+
36
+ def self.t(key)
37
+ self.load_translations
38
+ I18n.t(key)
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,120 @@
1
+ require 'frank-cucumber/frank_helper'
2
+
3
+ module Frank module Cucumber
4
+
5
+ module FrankMacHelper
6
+
7
+ # Performs a click at the center of the selected object.
8
+ #
9
+ # This method creates CGEvents to move and click the mouse. Before calling this method,
10
+ # you should make sure that the element that you want to click is frontmost by calling
11
+ # bring_to_front.
12
+ #
13
+ # This method is designed to act on a single object, so do not pass a selector that will
14
+ # return multiple objects.
15
+ def click ( selector )
16
+ frame = accessibility_frame(selector)
17
+ frankly_map( selector, 'FEX_mouseDownX:y:', frame.center.x, frame.center.y )
18
+ frankly_map( selector, 'FEX_mouseUpX:y:', frame.center.x, frame.center.y )
19
+ end
20
+
21
+ # Performs a double-click at the center of the selected object.
22
+ #
23
+ # This method creates CGEvents to move and click the mouse. Before calling this method,
24
+ # you should make sure that the element that you want to click is frontmost by calling
25
+ # bring_to_front.
26
+ #
27
+ # This method is designed to act on a single object, so do not pass a selector that will
28
+ # return multiple objects.
29
+ def double_click ( selector )
30
+ click(selector)
31
+ click(selector)
32
+ end
33
+
34
+ #@api private
35
+ def perform_action_on_selector( action, selector )
36
+ touch_successes = frankly_map( selector, action )
37
+ raise "could not find anything matching [#{selector}] which supports that action" if touch_successes == nil or touch_successes.empty?
38
+ raise "some objects do not support that action" if touch_successes.include?(false)
39
+ end
40
+
41
+ # Simulates the effect of clicking on the selected objects without actually moving the
42
+ # mouse or clicking.
43
+ #
44
+ # If the object supports the NSAccessibilityPressAction, that action is performed.
45
+ # Otherwise, if the object is a table cell or row, that row is selected,
46
+ # Otherwise, if the object is an NSView, it is made the first responder, if possible
47
+ # Otherwise, if the object is a menu item, it opens its submenu if it has one, or
48
+ # performs it's action if not.
49
+ def simulate_click( selector )
50
+ perform_action_on_selector( 'FEX_simulateClick', selector )
51
+ end
52
+
53
+ # Brings the selected application or window to the front.
54
+ def bring_to_front( selector )
55
+ perform_action_on_selector( 'FEX_raise', selector )
56
+ end
57
+
58
+ # Cancels the current action, such as editing a text field.
59
+ def cancel( selector )
60
+ perform_action_on_selector( 'FEX_cancel', selector )
61
+ end
62
+
63
+ # Finishes the current action, such as editing a text field.
64
+ def confirm( selector )
65
+ perform_action_on_selector( 'FEX_confirm', selector )
66
+ end
67
+
68
+ # Decrements the value of NSSliders, NSSteppers, and similar controls
69
+ def decrement_value( selector )
70
+ perform_action_on_selector( 'FEX_decrement', selector )
71
+ end
72
+
73
+ # Deletes the user-inputted value
74
+ def delete_value( selector )
75
+ perform_action_on_selector( 'FEX_delete', selector )
76
+ end
77
+
78
+ # Increments the value of NSSliders, NSSteppers, and similar controls
79
+ def increment_value( selector )
80
+ perform_action_on_selector( 'FEX_increment', selector )
81
+ end
82
+
83
+ # Selects a menu item. This action is considered "outdated" by the Accessibility API, so
84
+ # don't use it without a good reason.
85
+ def pick( selector )
86
+ perform_action_on_selector( 'FEX_pick', selector )
87
+ end
88
+
89
+ # Shows the contextual menu associated with the selector. This is the menu that would
90
+ # appear if the user right-clicked the selector, or, in some cases, held down the left
91
+ # mouse button on the selector.
92
+ def show_menu( selector )
93
+ perform_action_on_selector( 'FEX_showMenu', selector )
94
+ end
95
+
96
+ # Expands the row the selector belongs to in an NSOutlineView. Do not expand more than
97
+ # one row at a time.
98
+ def expand_row( selector )
99
+ perform_action_on_selector( 'FEX_expand', selector )
100
+ end
101
+
102
+ # Collapses the row the selector belongs to in an NSOutlineView. Do not expand more than
103
+ # one row at a time.
104
+ def collapse_row( selector )
105
+ perform_action_on_selector( 'FEX_collapse', selector )
106
+ end
107
+
108
+ # @return [Boolean] whether the rows the selectors belong to in an NSOutlineView are all
109
+ # expanded
110
+ def row_is_expanded( selector )
111
+ successes = frankly_map( selector, "FEX_isExpanded" )
112
+ return false if successes == nil or successes.empty?
113
+ return !successes.include?(false)
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+
120
+ end