frank-pivotal 1.2.3.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +38 -0
- data/bin/frank +6 -0
- data/bin/frank-skeleton +33 -0
- data/frank-pivotal.gemspec +37 -0
- data/frank-skeleton/features/my_first.feature +12 -0
- data/frank-skeleton/features/step_definitions/launch_steps.rb +20 -0
- data/frank-skeleton/features/support/env.rb +8 -0
- data/frank-skeleton/frank_static_resources.bundle/ViewAttributeMapping.plist +63 -0
- data/frank-skeleton/frank_static_resources.bundle/ViewAttributeMappingMac.plist +99 -0
- data/frank-skeleton/frank_static_resources.bundle/images/ajax-loader.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/file.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/folder-closed.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/folder.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/loader.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/loader.png +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/minus.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/plus.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/treeview-black-line.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/treeview-black.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/treeview-default-line.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/treeview-default.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/treeview-famfamfam-line.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/treeview-famfamfam.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/treeview-gray-line.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/treeview-gray.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/treeview-red-line.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/images/treeview-red.gif +0 -0
- data/frank-skeleton/frank_static_resources.bundle/index.html +86 -0
- data/frank-skeleton/frank_static_resources.bundle/index.html.haml +76 -0
- data/frank-skeleton/frank_static_resources.bundle/js/accessible_views_view.coffee +41 -0
- data/frank-skeleton/frank_static_resources.bundle/js/accessible_views_view.js +46 -0
- data/frank-skeleton/frank_static_resources.bundle/js/controller.coffee +134 -0
- data/frank-skeleton/frank_static_resources.bundle/js/controller.js +139 -0
- data/frank-skeleton/frank_static_resources.bundle/js/details_view.coffee +42 -0
- data/frank-skeleton/frank_static_resources.bundle/js/details_view.js +51 -0
- data/frank-skeleton/frank_static_resources.bundle/js/dropdown_control.coffee +64 -0
- data/frank-skeleton/frank_static_resources.bundle/js/dropdown_control.js +73 -0
- data/frank-skeleton/frank_static_resources.bundle/js/ersatz_model.coffee +46 -0
- data/frank-skeleton/frank_static_resources.bundle/js/ersatz_model.js +60 -0
- data/frank-skeleton/frank_static_resources.bundle/js/ersatz_view.coffee +167 -0
- data/frank-skeleton/frank_static_resources.bundle/js/ersatz_view.js +205 -0
- data/frank-skeleton/frank_static_resources.bundle/js/experiment_bar_model.coffee +10 -0
- data/frank-skeleton/frank_static_resources.bundle/js/experiment_bar_model.js +17 -0
- data/frank-skeleton/frank_static_resources.bundle/js/experiment_bar_view.coffee +44 -0
- data/frank-skeleton/frank_static_resources.bundle/js/experiment_bar_view.js +63 -0
- data/frank-skeleton/frank_static_resources.bundle/js/frank.coffee +96 -0
- data/frank-skeleton/frank_static_resources.bundle/js/frank.js +146 -0
- data/frank-skeleton/frank_static_resources.bundle/js/lib/backbone.js +1431 -0
- data/frank-skeleton/frank_static_resources.bundle/js/lib/coffee-script.js +8 -0
- data/frank-skeleton/frank_static_resources.bundle/js/lib/jquery-ui.min.js +405 -0
- data/frank-skeleton/frank_static_resources.bundle/js/lib/jquery.min.js +4 -0
- data/frank-skeleton/frank_static_resources.bundle/js/lib/jquery.treeview.js +251 -0
- data/frank-skeleton/frank_static_resources.bundle/js/lib/json2.js +481 -0
- data/frank-skeleton/frank_static_resources.bundle/js/lib/raphael.js +5815 -0
- data/frank-skeleton/frank_static_resources.bundle/js/lib/require.js +2053 -0
- data/frank-skeleton/frank_static_resources.bundle/js/lib/underscore.js +1059 -0
- data/frank-skeleton/frank_static_resources.bundle/js/main.coffee +27 -0
- data/frank-skeleton/frank_static_resources.bundle/js/main.js +29 -0
- data/frank-skeleton/frank_static_resources.bundle/js/tabs_controller.coffee +13 -0
- data/frank-skeleton/frank_static_resources.bundle/js/tabs_controller.js +22 -0
- data/frank-skeleton/frank_static_resources.bundle/js/toast_controller.coffee +15 -0
- data/frank-skeleton/frank_static_resources.bundle/js/toast_controller.js +28 -0
- data/frank-skeleton/frank_static_resources.bundle/js/transform_stack.coffee +59 -0
- data/frank-skeleton/frank_static_resources.bundle/js/transform_stack.js +78 -0
- data/frank-skeleton/frank_static_resources.bundle/js/tree_view.coffee +53 -0
- data/frank-skeleton/frank_static_resources.bundle/js/tree_view.js +64 -0
- data/frank-skeleton/frank_static_resources.bundle/js/view_hier_model.coffee +37 -0
- data/frank-skeleton/frank_static_resources.bundle/js/view_hier_model.js +48 -0
- data/frank-skeleton/frank_static_resources.bundle/js/view_model.coffee +39 -0
- data/frank-skeleton/frank_static_resources.bundle/js/view_model.js +62 -0
- data/frank-skeleton/frank_static_resources.bundle/pictos/index.html +329 -0
- data/frank-skeleton/frank_static_resources.bundle/pictos/pictos-web.eot +0 -0
- data/frank-skeleton/frank_static_resources.bundle/pictos/pictos-web.svg +114 -0
- data/frank-skeleton/frank_static_resources.bundle/pictos/pictos-web.ttf +0 -0
- data/frank-skeleton/frank_static_resources.bundle/pictos/pictos-web.woff +0 -0
- data/frank-skeleton/frank_static_resources.bundle/pictos/pictos.css +20 -0
- data/frank-skeleton/frank_static_resources.bundle/pictos/pictos_base64.css +18 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/css/symbiote.css +1 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_elements.scss +28 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_header.scss +61 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_inspect_tabs_list_tabs.scss +194 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_jquery.treeview.scss +68 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_jqui.scss +2 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_layout.scss +13 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_mixins.sass +137 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_reset.scss +32 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_selector_test_toolbar.scss +81 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_solarized.scss +16 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_typography.scss +11 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_unicode.scss +3 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/_z_index.scss +2 -0
- data/frank-skeleton/frank_static_resources.bundle/stylesheets/sass/symbiote.scss +26 -0
- data/frank-skeleton/frankify.xcconfig.tt +6 -0
- data/frank-skeleton/libCocoaAsyncSocket.a +0 -0
- data/frank-skeleton/libCocoaAsyncSocketMac.a +0 -0
- data/frank-skeleton/libCocoaHTTPServer.a +0 -0
- data/frank-skeleton/libCocoaHTTPServerMac.a +0 -0
- data/frank-skeleton/libCocoaLumberjack.a +0 -0
- data/frank-skeleton/libCocoaLumberjackMac.a +0 -0
- data/frank-skeleton/libFrank.a +0 -0
- data/frank-skeleton/libFrankMac.a +0 -0
- data/frank-skeleton/libShelley.a +0 -0
- data/frank-skeleton/libShelleyMac.a +0 -0
- data/frank-skeleton/plugins/.empty_directory +0 -0
- data/lib/frank-pivotal/app_bundle_locator.rb +58 -0
- data/lib/frank-pivotal/bonjour.rb +78 -0
- data/lib/frank-pivotal/cli.rb +307 -0
- data/lib/frank-pivotal/color_helper.rb +13 -0
- data/lib/frank-pivotal/console.rb +28 -0
- data/lib/frank-pivotal/core_frank_steps.rb +264 -0
- data/lib/frank-pivotal/frank.xcconfig.erb +17 -0
- data/lib/frank-pivotal/frank_helper.rb +467 -0
- data/lib/frank-pivotal/frank_localize.rb +43 -0
- data/lib/frank-pivotal/frank_mac_helper.rb +120 -0
- data/lib/frank-pivotal/frankifier.rb +153 -0
- data/lib/frank-pivotal/gateway.rb +135 -0
- data/lib/frank-pivotal/gesture_helper.rb +99 -0
- data/lib/frank-pivotal/host_scripting.rb +102 -0
- data/lib/frank-pivotal/keyboard_helper.rb +69 -0
- data/lib/frank-pivotal/launcher.rb +118 -0
- data/lib/frank-pivotal/localize.yml +104 -0
- data/lib/frank-pivotal/location_helper.rb +20 -0
- data/lib/frank-pivotal/plugins/plugin.rb +57 -0
- data/lib/frank-pivotal/rect.rb +26 -0
- data/lib/frank-pivotal/scroll_helper.rb +24 -0
- data/lib/frank-pivotal/version.rb +5 -0
- data/lib/frank-pivotal/wait_helper.rb +57 -0
- data/lib/frank-pivotal.rb +14 -0
- data/test/keyboard_helper_test.rb +84 -0
- data/test/launcher_test.rb +57 -0
- data/test/rect_test.rb +25 -0
- data/test/test_helper.rb +16 -0
- 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
|