illuminator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/gem/README.md +37 -0
  3. data/gem/bin/illuminatorTestRunner.rb +22 -0
  4. data/gem/lib/illuminator.rb +171 -0
  5. data/gem/lib/illuminator/argument-parsing.rb +299 -0
  6. data/gem/lib/illuminator/automation-builder.rb +39 -0
  7. data/gem/lib/illuminator/automation-runner.rb +589 -0
  8. data/gem/lib/illuminator/build-artifacts.rb +118 -0
  9. data/gem/lib/illuminator/device-installer.rb +45 -0
  10. data/gem/lib/illuminator/host-utils.rb +42 -0
  11. data/gem/lib/illuminator/instruments-runner.rb +301 -0
  12. data/gem/lib/illuminator/javascript-runner.rb +98 -0
  13. data/gem/lib/illuminator/listeners/console-logger.rb +32 -0
  14. data/gem/lib/illuminator/listeners/full-output.rb +13 -0
  15. data/gem/lib/illuminator/listeners/instruments-listener.rb +22 -0
  16. data/gem/lib/illuminator/listeners/intermittent-failure-detector.rb +49 -0
  17. data/gem/lib/illuminator/listeners/pretty-output.rb +26 -0
  18. data/gem/lib/illuminator/listeners/saltinel-agent.rb +66 -0
  19. data/gem/lib/illuminator/listeners/saltinel-listener.rb +26 -0
  20. data/gem/lib/illuminator/listeners/start-detector.rb +52 -0
  21. data/gem/lib/illuminator/listeners/stop-detector.rb +46 -0
  22. data/gem/lib/illuminator/listeners/test-listener.rb +58 -0
  23. data/gem/lib/illuminator/listeners/trace-error-detector.rb +38 -0
  24. data/gem/lib/illuminator/options.rb +96 -0
  25. data/gem/lib/illuminator/resources/IlluminatorGeneratedEnvironment.erb +13 -0
  26. data/gem/lib/illuminator/resources/IlluminatorGeneratedRunnerForInstruments.erb +19 -0
  27. data/gem/lib/illuminator/test-definitions.rb +23 -0
  28. data/gem/lib/illuminator/test-suite.rb +155 -0
  29. data/gem/lib/illuminator/version.rb +3 -0
  30. data/gem/lib/illuminator/xcode-builder.rb +144 -0
  31. data/gem/lib/illuminator/xcode-utils.rb +219 -0
  32. data/gem/resources/BuildConfiguration.xcconfig +10 -0
  33. data/gem/resources/js/AppMap.js +767 -0
  34. data/gem/resources/js/Automator.js +1132 -0
  35. data/gem/resources/js/Base64.js +142 -0
  36. data/gem/resources/js/Bridge.js +102 -0
  37. data/gem/resources/js/Config.js +92 -0
  38. data/gem/resources/js/Extensions.js +2025 -0
  39. data/gem/resources/js/Illuminator.js +228 -0
  40. data/gem/resources/js/Preferences.js +24 -0
  41. data/gem/resources/scripts/UIAutomationBridge.rb +248 -0
  42. data/gem/resources/scripts/common.applescript +25 -0
  43. data/gem/resources/scripts/diff_png.sh +61 -0
  44. data/gem/resources/scripts/kill_all_sim_processes.sh +17 -0
  45. data/gem/resources/scripts/plist_to_json.sh +40 -0
  46. data/gem/resources/scripts/set_hardware_keyboard.applescript +0 -0
  47. metadata +225 -0
@@ -0,0 +1,219 @@
1
+ require 'singleton'
2
+ require 'fileutils'
3
+
4
+ module Illuminator
5
+
6
+ # Convenience functions for command-line actions done in Xcode
7
+ class XcodeUtils
8
+ include Singleton
9
+
10
+ def initialize
11
+ @xcode_path = `/usr/bin/xcode-select -print-path`.chomp.sub(/^\s+/, '')
12
+ @xcode_version = nil
13
+ @sdk_path = nil
14
+ @instruments_path = nil
15
+ @_simulator_devices_text = nil
16
+ @simulator_device_types = nil
17
+ @simulator_runtimes = nil
18
+ end
19
+
20
+ def get_xcode_path
21
+ @xcode_path
22
+ end
23
+
24
+ def get_xcode_app_path
25
+ Illuminator::HostUtils.realpath(File.join(@xcode_path, "../../"))
26
+ end
27
+
28
+ def get_xcode_simctl_path
29
+ Illuminator::HostUtils.realpath(File.join(@xcode_path, "/usr/bin/simctl"))
30
+ end
31
+
32
+ def get_xcode_version
33
+ if @xcode_version.nil?
34
+ xcode_version = `xcodebuild -version`
35
+ needle = 'Xcode (.*)'
36
+ match = xcode_version.match(needle)
37
+ @xcode_version = match.captures[0]
38
+ end
39
+ @xcode_version
40
+ end
41
+
42
+ def is_xcode_major_version ver
43
+ # should update this with 'version' gem
44
+ needle = '(\d+)\.?(\d+)?'
45
+ match = get_xcode_version.match(needle)
46
+ return match.captures[0].to_i == ver
47
+ end
48
+
49
+ # Get the path to the SDK
50
+ def get_sdk_path
51
+ @sdk_path ||= `/usr/bin/xcodebuild -version -sdk iphoneos | grep PlatformPath`.split(':')[1].chomp.sub(/^\s+/, '')
52
+ end
53
+
54
+ # Get the path to the instruments bundle
55
+ def get_instruments_path
56
+ if @instruments_path.nil?
57
+ if File.directory? "#{@xcode_path}/../Applications/Instruments.app/Contents/PlugIns/AutomationInstrument.xrplugin/"
58
+ @instruments_path = "AutomationInstrument.xrplugin";
59
+ else
60
+ #fallback to old instruments bundle (pre Xcode6)
61
+ @instruments_path = "AutomationInstrument.bundle";
62
+ end
63
+ end
64
+ @instruments_path
65
+ end
66
+
67
+ # Get the path to the instruments template
68
+ def get_instruments_template_path
69
+ File.join(@xcode_path,
70
+ "../Applications/Instruments.app/Contents/PlugIns",
71
+ get_instruments_path,
72
+ "Contents/Resources/Automation.tracetemplate")
73
+ end
74
+
75
+ def _get_all_simulator_info
76
+ info = `#{get_xcode_simctl_path} list`.split("\n")
77
+
78
+ output = {"devices" => [], "runtimes" => []}
79
+ pointer = nil
80
+ needle = nil
81
+
82
+ info.each do |line|
83
+ case line
84
+ when "== Device Types =="
85
+ # all data we want is followed by " (" e.g. "iPhone 5 (com.apple.C......."
86
+ pointer = "devices"
87
+ needle = '([^(]*) '
88
+ when "== Runtimes =="
89
+ # all data we want is in the form "iOS 7.0 (7.0.3 - ........." and we want the 7.0
90
+ pointer = "runtimes"
91
+ needle = 'iOS ([^(]*) '
92
+ when "== Devices =="
93
+ pointer = nil
94
+ needle = nil
95
+ else
96
+ unless pointer.nil?
97
+ match = line.match(needle)
98
+ if match
99
+ output[pointer] << match.captures[0]
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ @simulator_device_types = output["devices"]
106
+ @simulator_runtimes = output["runtimes"]
107
+ end
108
+
109
+ def get_simulator_device_types
110
+ if @simulator_device_types.nil?
111
+ _get_all_simulator_info
112
+ end
113
+ @simulator_device_types
114
+ end
115
+
116
+ def get_simulator_runtimes
117
+ if @simulator_runtimes.nil?
118
+ _get_all_simulator_info
119
+ end
120
+ @simulator_runtimes
121
+ end
122
+
123
+ def get_simulator_devices
124
+ if @_simulator_devices_text.nil?
125
+ @_simulator_devices_text = `instruments -s devices`
126
+ end
127
+ @_simulator_devices_text
128
+ end
129
+
130
+ # Based on the desired device and version, get the ID of the simulator that will be passed to instruments
131
+ def get_simulator_id (sim_device, sim_version)
132
+ devices = get_simulator_devices
133
+ needle = sim_device + ' \(' + sim_version + ' Simulator\) \[(.*)\]'
134
+ match = devices.match(needle)
135
+ if match
136
+ puts "Found device match: #{match}".green
137
+ return match.captures[0]
138
+ elsif
139
+ puts "Did not find UDID of device '#{sim_device}' for version '#{sim_version}'".green
140
+ if XcodeUtils.instance.is_xcode_major_version 5
141
+ fallback_name = "#{sim_device} - Simulator - iOS #{sim_version}"
142
+ puts "Falling back to Xcode5 name #{fallback_name}".green
143
+ return fallback_name
144
+ end
145
+ end
146
+
147
+ return nil
148
+ end
149
+
150
+ def get_crash_directory
151
+ return "#{ENV['HOME']}/Library/Logs/DiagnosticReports"
152
+ end
153
+
154
+ # Create a crash report
155
+ def create_symbolicated_crash_report (app_path, crash_path, crash_report_path)
156
+ # find symbolicatecrash file, which is different depending on the Xcode version (we assume either 5 or 6)
157
+ frameworks_path = "#{@xcode_path}/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks"
158
+ symbolicator_path = "#{frameworks_path}/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash"
159
+ if not File.exist?(symbolicator_path)
160
+ symbolicator_path = "#{frameworks_path}/DTDeviceKit.framework/Versions/A/Resources/symbolicatecrash"
161
+ end
162
+ if not File.exist?(symbolicator_path)
163
+ symbolicator_path = File.join(get_xcode_app_path,
164
+ "Contents/SharedFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash")
165
+ end
166
+
167
+ command = "DEVELOPER_DIR='#{@xcode_path}' "
168
+ command << "'#{symbolicator_path}' "
169
+ command << "-o '#{crash_report_path}' '#{crash_path}' '#{app_path}.dSYM' 2>&1"
170
+
171
+ output = `#{command}`
172
+
173
+ # log the output of the crash reporting if the file didn't appear
174
+ unless File.exist?(crash_report_path)
175
+ puts command.green
176
+ puts output
177
+ return false
178
+ end
179
+ return true
180
+ end
181
+
182
+ # use the provided applescript to reset the content and settings of the simulator
183
+ def reset_simulator device_id
184
+ command = "#{get_xcode_simctl_path} erase #{device_id}"
185
+ puts command.green
186
+ puts `#{command}`
187
+
188
+ end
189
+
190
+ def shutdown_simulator device_id
191
+ command = "#{get_xcode_simctl_path} shutdown #{device_id}"
192
+ puts command.green
193
+ puts `#{command}`
194
+ end
195
+
196
+ # remove any apps in the specified directory
197
+ def self.remove_existing_apps xcode_output_dir
198
+ Dir["#{xcode_output_dir}/*.app"].each do |app|
199
+ puts "XcodeUtils: removing #{app}"
200
+ FileUtils.rm_rf app
201
+ end
202
+ end
203
+
204
+ def self.kill_all_simulator_processes(device_id = nil)
205
+ XcodeUtils.instance.shutdown_simulator(device_id) unless device_id.nil?
206
+ command = HostUtils.realpath(File.join(File.dirname(__FILE__), "../../resources/scripts/kill_all_sim_processes.sh"))
207
+ puts "Running #{command}"
208
+ puts `'#{command}'`
209
+ end
210
+
211
+ def self.kill_all_instruments_processes
212
+ command = "killall -9 instruments"
213
+ puts "Running #{command}"
214
+ puts `#{command}`
215
+ end
216
+
217
+ end
218
+
219
+ end
@@ -0,0 +1,10 @@
1
+ //
2
+ // CoverageConfiguration.xcconfig
3
+ //
4
+ //
5
+ // Created by Erceg,Boris on 8/5/13.
6
+ //
7
+ //
8
+
9
+ GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES
10
+ GCC_GENERATE_TEST_COVERAGE_FILES = YES
@@ -0,0 +1,767 @@
1
+ // AppMap.js
2
+ //
3
+ // creates 'appmap' which can build and store app definitions
4
+ // apps have screens
5
+ // screens have actions
6
+ // actions have parameters
7
+ // actions have device-specific implementations
8
+
9
+ var debugAppmap = false;
10
+
11
+ (function() {
12
+
13
+ var root = this,
14
+ appmap = null;
15
+
16
+ // put appmap in namespace of importing code
17
+ if (typeof exports !== 'undefined') {
18
+ appmap = exports;
19
+ } else {
20
+ appmap = root.appmap = {};
21
+ }
22
+
23
+ // Exception classes
24
+ appmap.SetupException = makeErrorClassWithGlobalLocator(__file__(), "AppMapSetupException");
25
+ appmap.ParameterException = makeErrorClass("AppMapParameterException");
26
+
27
+ // internal variables
28
+ appmap.apps = {}; // all possible apps, keyed by string
29
+ var lastApp; // state variable for building app
30
+ var lastAppName; // name of last app
31
+ var lastScreen; // state variable for building screen
32
+ var lastScreenName; // name of last screen
33
+ var lastAction; // state variable for building action
34
+ var lastActionName; // name of last action
35
+ var lastScreenActiveFn; // action function map of last screen
36
+
37
+ appmap.inputMethods = {}; // all possible input methods, keyed by string
38
+ var lastInputMethod; // state variable for building input method
39
+ var lastInputMethodName; // name of last input method
40
+
41
+
42
+ /**
43
+ * Create a new app in the appmap with the given name.
44
+ *
45
+ * All following screen defintions will be associated with this app.
46
+ *
47
+ * @param appName the desired app name
48
+ * @return this
49
+ */
50
+ appmap.createApp = function(appName) {
51
+ appmap.lastApp = {}; // screens is empty
52
+ appmap.lastAppName = appName;
53
+ appmap.apps[appName] = appmap.lastApp;
54
+ appmap.lastScreen = null;
55
+ appmap.lastAction = null;
56
+ return this;
57
+ };
58
+
59
+ /**
60
+ * whether an app exists
61
+ *
62
+ * @param appName the app name to test
63
+ * @return bool
64
+ */
65
+ appmap.hasApp = function(appName) {
66
+ return appName in appmap.apps;
67
+ };
68
+
69
+ /**
70
+ * All following screen defintions will be associated with this app.
71
+ *
72
+ * @param appName the desired app name
73
+ * @return this
74
+ */
75
+ appmap.augmentApp = function(appName) {
76
+ appmap.lastApp = appmap.apps[appName];
77
+ appmap.lastAppName = appName;
78
+ return this;
79
+ };
80
+
81
+ /**
82
+ * Create a new app if it does not already exist.
83
+ *
84
+ * All following screen definitions will be associated with this app.
85
+ *
86
+ * @param appName the desired app name
87
+ * @return this
88
+ */
89
+ appmap.createOrAugmentApp = function(appName) {
90
+ return appmap.hasApp(appName) ? appmap.augmentApp(appName) : appmap.createApp(appName);
91
+ }
92
+
93
+
94
+ /**
95
+ * Create a new screen in the appmap with the given name.
96
+ *
97
+ * All following target/action defintions will be associated with this screen.
98
+ *
99
+ * @param screenName the desired screen name
100
+ * @return this
101
+ */
102
+ appmap.withNewScreen = function(screenName) {
103
+ appmap.lastScreen = {};
104
+ appmap.lastScreenName = screenName;
105
+ appmap.lastScreenActiveFn = {};
106
+ appmap.lastAction = null;
107
+
108
+ appmap.lastApp[appmap.lastScreenName] = appmap.lastScreen;
109
+ if (debugAppmap) UIALogger.logDebug(" adding screen " + appmap.lastScreenName);
110
+
111
+ return this;
112
+ };
113
+
114
+ /**
115
+ * whether a screen exists
116
+ *
117
+ * @param appName the app to look in
118
+ * @param screenName the screen name to test
119
+ * @return bool
120
+ */
121
+ appmap.hasScreen = function(appName, screenName) {
122
+ return appmap.hasApp(appName) && (screenName in appmap.apps[appName]);
123
+ }
124
+
125
+ /**
126
+ * All following target / action defintions will be associated with this screen
127
+ *
128
+ * @param appName the desired app name
129
+ * @return this
130
+ */
131
+ appmap.augmentScreen = function(screenName) {
132
+ appmap.lastScreenName = screenName;
133
+
134
+ appmap.lastScreen = appmap.lastApp[appmap.lastScreenName];
135
+ if (debugAppmap) UIALogger.logDebug(" augmenting screen " + appmap.lastScreenName);
136
+ return this;
137
+ }
138
+
139
+ /**
140
+ * Create a new screen if it does not already exist.
141
+ *
142
+ * All following target / action definitions will be associated with this screen.
143
+ *
144
+ * @param screenName the desired screen name
145
+ * @return this
146
+ */
147
+ appmap.withScreen = function(screenName) {
148
+ return appmap.hasScreen(appmap.lastAppName, screenName) ? appmap.augmentScreen(screenName) : appmap.withNewScreen(screenName);
149
+ }
150
+
151
+ /**
152
+ * Enable the screen on a given target device
153
+ *
154
+ * @param targetName the name of the target device (e.g. "iPhone", "iPad")
155
+ * @param isActiveFn function that should return true when the screen is currently both visible and accessible
156
+ * @return this
157
+ */
158
+ appmap.onTarget = function(targetName, isActiveFn) {
159
+ var lastScreenName = appmap.lastScreenName;
160
+ if (debugAppmap) UIALogger.logDebug(" on Target " + targetName);
161
+ appmap.lastScreenActiveFn[targetName] = isActiveFn;
162
+
163
+ appmap.withAction("verifyIsActive", "Null op to verify that the " + appmap.lastScreenName + " screen is active");
164
+ // slightly hacky, withImplementation expects actions to come AFTER all the onTarget calls
165
+ appmap.lastAction.isCorrectScreen[targetName] = isActiveFn;
166
+ appmap.withImplementation(function() {}, targetName);
167
+
168
+ appmap.withAction("verifyNotActive", "Verify that the " + appmap.lastScreenName + " screen is NOT active")
169
+ // slightly hacky, withImplementation expects actions to come AFTER all the onTarget calls
170
+ appmap.lastAction.isCorrectScreen[targetName] = isActiveFn;
171
+ appmap.withImplementation(function() {
172
+ if (isActiveFn()) {
173
+ throw new IlluminatorRuntimeVerificationException("Failed assertion that '" + lastScreenName + "' is NOT active ");
174
+ }
175
+ }, targetName);
176
+
177
+ // now modify verifyNotActive's isCorrectScreen array to always return true. slighly hacky.
178
+ // this is because the meat of the function runs in our generated action
179
+ for (var d in appmap.lastAction.isCorrectScreen) {
180
+ appmap.lastAction.isCorrectScreen[d] = function (parm) {
181
+ delay((parm === undefined || parm.delay === undefined) ? 0.35 : parm.delay); // wait for animations to complete
182
+ UIALogger.logDebug("verifyNotActive is skipping the default screenIsActive function");
183
+ return true;
184
+ };
185
+ }
186
+
187
+ return this;
188
+ };
189
+
190
+
191
+ /**
192
+ * Create a new action in the appmap with the given name.
193
+ *
194
+ * All following implementation / parameter definitions will be associated with this action.
195
+ *
196
+ * @param actionName the desired screen name
197
+ * @param desc the description of this action that will be used for logging purposes
198
+ * @return this
199
+ */
200
+ appmap.withNewAction = function(actionName, desc) {
201
+ // we need this hack to prevent problems with the above isCorrectScreen hack --
202
+ // so that we don't change the original reference, we rebuild the {targetname : function} map manually
203
+ var frozen = function(k) { return appmap.lastScreenActiveFn[k]; };
204
+ var isActiveMap = {};
205
+ for (var k in appmap.lastScreenActiveFn) {
206
+ isActiveMap[k] = frozen(k);
207
+ }
208
+
209
+ // we add screen params to the action so that we can deal in actions alone
210
+ appmap.lastAction = {
211
+ name: actionName,
212
+ isCorrectScreen: isActiveMap,
213
+ appName: appmap.lastAppName,
214
+ screenName: appmap.lastScreenName,
215
+ actionFn: {},
216
+ description: desc,
217
+ params: {}
218
+ };
219
+ appmap.lastActionName = actionName;
220
+ if (debugAppmap) UIALogger.logDebug(" adding action " + appmap.lastActionName);
221
+ appmap.lastScreen[appmap.lastActionName] = appmap.lastAction;
222
+ return this;
223
+ };
224
+
225
+
226
+ /**
227
+ * whether an action exists
228
+ *
229
+ * @param appName the app to look in
230
+ * @param screenName tge screen to look in
231
+ * @param actionName the screen name to test
232
+ * @return bool
233
+ */
234
+ appmap.hasAction = function(appName, screenName, actionName) {
235
+ return appmap.hasScreen(appName, screenName) && (actionName in appmap.apps[appName][screenName]);
236
+ }
237
+
238
+
239
+ /**
240
+ * All following implementation / parameter definitions will be associated with this screen
241
+ *
242
+ * @param appName the desired app name
243
+ * @return this
244
+ */
245
+ appmap.augmentAction = function(actionName) {
246
+ if (debugAppmap) UIALogger.logDebug(" augmenting action " + actionName);
247
+ appmap.lastAction = appmap.lastScreen[actionName];
248
+ appmap.lastActionName = actionName;
249
+ return this;
250
+ }
251
+
252
+ /**
253
+ * Create a new action in the appmap with the given name if it does not already exist.
254
+ *
255
+ * All following implementation / parameter definitions will be associated with this action.
256
+ *
257
+ * @param actionName the desired screen name
258
+ * @param desc the description of this action that will be used for logging purposes
259
+ * @return this
260
+ */
261
+ appmap.withAction = function(actionName, desc) {
262
+ return appmap.hasAction(appmap.lastAppName, appmap.lastScreenName, actionName) ? appmap.augmentAction(actionName) : appmap.withNewAction(actionName, desc);
263
+ }
264
+
265
+ /**
266
+ * Set the function that will carry out the current action on the given target device
267
+ *
268
+ * targetName is optional; if omitted, the function will be used for all target devices
269
+ *
270
+ * @param actFn function that should be executed when the current action is requested to run
271
+ * @param targetName optional the name of the target device (e.g. "iPhone", "iPad")
272
+ * @return this
273
+ */
274
+ appmap.withImplementation = function(actFn, targetName) {
275
+ targetName = targetName === undefined ? "default" : targetName;
276
+
277
+ // catch implementations for nonexistent targets
278
+ if ("default" != targetName && !(targetName in appmap.lastAction.isCorrectScreen)) {
279
+ var targets = [];
280
+ for (var k in appmap.lastAction.isCorrectScreen) {
281
+ targets.push(k);
282
+ }
283
+ var msg = "Screen " + appmap.lastAppName + "." + appmap.lastScreenName;
284
+ msg += " only has defined targets: '" + targets.join("', '") + "' but tried to add an implementation";
285
+ msg += " for target device '" + targetName + "' in action '" + appmap.lastActionName + "'";
286
+ throw new appmap.SetupException(msg);
287
+ }
288
+
289
+ if (debugAppmap) UIALogger.logDebug(" adding implementation on " + targetName);
290
+ appmap.lastAction.actionFn[targetName] = actFn;
291
+ return this;
292
+ }
293
+
294
+ /**
295
+ * Define a new parameter in the latest action with the given attributes
296
+ *
297
+ * @param paramName the name of the parameter
298
+ * @param desc a description of the parameter
299
+ * @param required bool whether the parameter is required for the action
300
+ * @param useInSummary whether the value of the parameter should be logged when the action executes
301
+ */
302
+ appmap.withParam = function(paramName, desc, required, useInSummary) {
303
+ if (debugAppmap) UIALogger.logDebug(" adding parameter " + paramName);
304
+ useInSummmary = useInSummary === undefined ? false : useInSummary;
305
+ appmap.lastAction.params[paramName] = {
306
+ description: desc,
307
+ required: required,
308
+ useInSummary: useInSummary
309
+ };
310
+ return this;
311
+ };
312
+
313
+
314
+ /**
315
+ * Create a new input method in the appmap with the given name.
316
+ *
317
+ * All following feature defintions will be associated with this app.
318
+ *
319
+ * @param inputMethodName the desired input method name
320
+ * @param description string the description of the input method
321
+ * @param isActiveFn function that should return true when the screen is currently both visible and accessible
322
+ * @param selector selector relative to target() that returns this input method
323
+ * @return this
324
+ */
325
+ appmap.createInputMethod = function(inputMethodName, description, isActiveFn, selector) {
326
+ appmap.lastInputMethod = newInputMethod(inputMethodName, description, isActiveFn, selector, {});
327
+ appmap.lastInputMethodName = inputMethodName;
328
+ appmap.inputMethods[inputMethodName] = appmap.lastInputMethod;
329
+ return this;
330
+ };
331
+
332
+ /**
333
+ * whether an input method exists
334
+ *
335
+ * @param inputMethodName the input method name to test
336
+ * @return bool
337
+ */
338
+ appmap.hasInputMethod = function(inputMethodName) {
339
+ return inputMethodName in appmap.inputMethods;
340
+ };
341
+
342
+ /**
343
+ * All following feature defintions will be associated with this input method.
344
+ *
345
+ * @param inputMethodName the desired input method name
346
+ * @return this
347
+ */
348
+ appmap.augmentInputMethod = function(inputMethodName) {
349
+ appmap.lastInputMethodName = inputMethodName;
350
+ appmap.inputMethods[inputMethodName] = appmap.lastInputMethod;
351
+ return this;
352
+ };
353
+
354
+ /**
355
+ * Add a feature function to an input method
356
+ *
357
+ * @param featureName string name of the function as it will exist in the input method
358
+ * @param implementationFunction function that will be the implementation
359
+ * @return this
360
+ */
361
+ appmap.withFeature = function (featureName, implementationFunction) {
362
+ appmap.lastInputMethod.features[featureName] = implementationFunction;
363
+ return this;
364
+ }
365
+
366
+
367
+ /**
368
+ * return an array of all defined apps
369
+ */
370
+ appmap.getApps = function () {
371
+ var ret = [];
372
+ for (d in appmap.apps) ret.push(d);
373
+ return ret;
374
+ };
375
+
376
+ /**
377
+ * return an array of all defined screens for the given app
378
+ *
379
+ * @param app the app name
380
+ */
381
+ appmap.getScreens = function (app) {
382
+ var ret = [];
383
+ for (s in appmap.apps[app]) ret.push(s);
384
+ return ret;
385
+ };
386
+
387
+ /**
388
+ * return an array of all defined input methods
389
+ */
390
+ appmap.getInputMethods = function () {
391
+ var ret = [];
392
+ for (s in appmap.inputMethods) ret.push(s);
393
+ return ret;
394
+ };
395
+
396
+ /**
397
+ * Return an input method by name
398
+ *
399
+ * @param methodName string the name of the input method
400
+ */
401
+ appmap.getInputMethod = function (methodName) {
402
+ if (methodName in appmap.inputMethods) return appmap.inputMethods[methodName];
403
+ throw new IlluminatorSetupException("Input method " + methodName + " is not defined. " +
404
+ "Choices are [" + appmap.getInputMethods().join(",") + "]");
405
+ }
406
+
407
+ /**
408
+ * Returns Markdown string describing all the apps, screens, targets, actions, implementations, and parameters
409
+ */
410
+ appmap.toMarkdown = function () {
411
+ // print input methods first, since they are common to all apps
412
+ var ret = ["The following input methods are defined in the Illuminator AppMap (in addition to the iOS default keyboard):"];
413
+ for (var k in appmap.inputMethods) {
414
+ var mth = appmap.inputMethods[k];
415
+ ret.push("* `" + k + "`: " + mth.description);
416
+ // print features
417
+ for (var f in mth.features) {
418
+ ret.push(" * `" + f + "`");
419
+ }
420
+ }
421
+
422
+ // formatting the title, making good looking markdown
423
+ var title = function (rank, text) {
424
+ // insert blank lines before the title
425
+ var total = 4;
426
+ for (var i = 0; i <= (total - rank); ++i) {
427
+ ret.push("");
428
+ }
429
+
430
+ switch (rank) {
431
+ case 1:
432
+ ret.push(text);
433
+ ret.push(Array(Math.max(10, text.length) + 1).join("="));
434
+ break;
435
+ case 2:
436
+ ret.push(text);
437
+ ret.push(Array(Math.max(10, text.length) + 1).join("-"));
438
+ break;
439
+ default:
440
+ ret.push(Array(rank + 1).join("#") + " " + text);
441
+ }
442
+ };
443
+
444
+ // if an action is defined on the same targets as its parent screen (not just a subset)
445
+ var onSameTargets = function (action) {
446
+ if ("default" == Object.keys(action.actionFn)[0]) return true;
447
+ for (d in action.isCorrectScreen) if (undefined === action.actionFn[d]) return false;
448
+ return true;
449
+ };
450
+
451
+ ret.push("The following apps are defined in the Illuminator AppMap:");
452
+
453
+ // iterate over apps
454
+ var apps = Object.keys(appmap.apps).sort();
455
+ for (var i = 0; i < apps.length; ++i) {
456
+ var appName = apps[i];
457
+ var app = appmap.apps[appName];
458
+ title(1, appName);
459
+ ret.push("This app has the following screens:");
460
+
461
+ // iterate over screens
462
+ var screens = Object.keys(app).sort();
463
+ for (var j = 0; j < screens.length; ++j) {
464
+ var scnName = screens[j];
465
+ var scn = app[scnName];
466
+ title(2, scnName);
467
+
468
+ // just use the first action on the screen to get the targets - from isCorrectScreen map
469
+ var screenTargets = "`" + Object.keys(scn[Object.keys(scn)[0]].isCorrectScreen).join("`, `") + "`";
470
+ ret.push("Defined for " + screenTargets + ", with the following actions:");
471
+ ret.push(""); // need blank line before bulleted lists
472
+
473
+ // iterate over actions
474
+ var actions = Object.keys(scn).sort();
475
+ for (var k = 0; k < actions.length; ++k) {
476
+ var actName = actions[k];
477
+ var act = scn[actName];
478
+ var actionTargets = onSameTargets(act) ? "" : " `" + Object.keys(act.actionFn).join("`, `") + "`";
479
+ var parms = Object.keys(act.params).length == 0 ? "" : " (parameterized)";
480
+ ret.push("* **" + actName + "**" + parms + actionTargets + ": " + act.description);
481
+
482
+ // iterate over parameters
483
+ var params = Object.keys(act.params).sort();
484
+ for (var m = 0; m < params.length; ++m) {
485
+ var paramName = params[m];
486
+ var par = act.params[paramName];
487
+ ret.push(" * `" + paramName + "`" + (par.required ? "" : " (optional)") + ": " + par.description);
488
+ }
489
+ }
490
+
491
+ }
492
+ }
493
+
494
+ return ret.join("\n");
495
+ };
496
+
497
+
498
+ // create an action builder to enable easy one-liners for common actions
499
+ // intended use is to say var ab = appmap.actionBuilder.makeAction;
500
+ // then in appmap: .withImplementation(ab.verifyElement.visibility({name: "blah"}))
501
+ appmap.actionBuilder = {};
502
+ appmap.actionBuilder.makeAction = {};
503
+ appmap.actionBuilder.makeAction.verifyElement = {};
504
+ appmap.actionBuilder.makeAction.element = {};
505
+ appmap.actionBuilder.makeAction.selector = {};
506
+ appmap.actionBuilder.makeAction.screenIsActive = {};
507
+
508
+
509
+ /**
510
+ * Internal function for getting child elements and raising the appropriate errors
511
+ *
512
+ * @param selector the selector to get the element
513
+ * @param retryDelay optional integer, if provided and the selector fails then the selector will be retried
514
+ */
515
+ appmap.actionBuilder._getElement = function(selector, retryDelay) {
516
+ try {
517
+ return target().getOneChildElement(selector);
518
+ } catch (e) {
519
+ // it's possible that the selector returned multiple things, so re-raise that
520
+ if (!isHardSelector(selector)) {
521
+ var elems = target().getChildElements(selector);
522
+ if (Object.keys(getUniqueElements(elems)).length > 1) throw e;
523
+ }
524
+
525
+ // one consequence-free failure allowed if retryDelay was specified
526
+ if (retryDelay === undefined) throw e;
527
+ delay(retryDelay);
528
+ return target().getOneChildElement(selector);
529
+ }
530
+ };
531
+
532
+
533
+ /**
534
+ * Internal function for building actions that verify predicate functions
535
+ *
536
+ * @param selector the element to select
537
+ * @param elemName the name of what is being selected
538
+ * @param predicateFn the function (taking an element as an argument, returning a boolean) that should be evaluated
539
+ * @param predicateDesc the name of the predicate function, for logging purposes
540
+ * @param retryDelay optional integer, if provided and the selector fails then the selector will be retried
541
+ * @return a function (taking an object with fields {expected: boolean} as its only argument) that asserts the predicate
542
+ */
543
+ appmap.actionBuilder.makeAction.verifyElement._predicate = function(selector, elemName, predicateFn, predicateDesc, retryDelay) {
544
+ return function(parm) {
545
+ var elem = appmap.actionBuilder._getElement(selector, retryDelay);
546
+
547
+ // prevent any funny business with integer comparisons
548
+ if ((predicateFn(elem) == true) != (parm.expected == true)) {
549
+ throw new IlluminatorRuntimeVerificationException("Element " + elemName + " failed predicate " + predicateDesc
550
+ + " (expected value: " + parm.expected + ")");
551
+ }
552
+ };
553
+ };
554
+
555
+
556
+ /**
557
+ * Internal function for building actions that interact with elements
558
+ *
559
+ * @param selector the element to select
560
+ * @param elemName the name of what is being selected
561
+ * @param workFn the function (taking an element as an argument) that should be performed
562
+ * @param retryDelay optional integer, if provided and the selector fails then the selector will be retried
563
+ * @param return a function (taking no arguments) that executes the work function on the selected element
564
+ */
565
+ appmap.actionBuilder.makeAction.element._interaction = function(selector, elemName, workFn, retryDelay) {
566
+ return function(parm) {
567
+ var elem = appmap.actionBuilder._getElement(selector, retryDelay);
568
+ workFn(elem, parm);
569
+ };
570
+ };
571
+
572
+
573
+
574
+ /**
575
+ * create a screenIsActive function by looking for a specific element.
576
+ *
577
+ * @param screenName the screen name, for logging purposes
578
+ * @param elementName what is being selected, for logging purposes
579
+ * @param selector the selector to watch
580
+ * @param timeout how long to wait
581
+ * @return a no-argument function that returns a boolean
582
+ */
583
+ appmap.actionBuilder.makeAction.screenIsActive.byElement = function (screenName, elementName, selector, timeout) {
584
+ switch (typeof timeout) {
585
+ case "number": break;
586
+ default: throw new appmap.SetupException("makeAction.screenIsActive.byElement got a bad timeout value: (" + (typeof timeout) + ") " + timeout);
587
+ }
588
+
589
+ return function () {
590
+ try {
591
+ target().waitForChildExistence(timeout, true, elementName, selector);
592
+ return true;
593
+ } catch (e) {
594
+ UIALogger.logDebug("screenIsActive.byElement function for " + screenName + " got error: " + e);
595
+ return false;
596
+ }
597
+ };
598
+ };
599
+
600
+
601
+ /**
602
+ * build an action function that verifies whether an element is enabled
603
+ *
604
+ * @param selector reference to the element
605
+ * @param elemName the name of what we are looking for
606
+ * @param retryDelay optional integer how long to wait before a retry if selector fails
607
+ * @return a function that takes an object with fields {expected: boolean} as its argument
608
+ */
609
+ appmap.actionBuilder.makeAction.verifyElement.enabled = function(selector, elemName, retryDelay) {
610
+ return appmap.actionBuilder.makeAction.verifyElement._predicate(selector, elemName, function (elem) {
611
+ return elem.isEnabled();
612
+ }, "isEnabled", retryDelay);
613
+ };
614
+
615
+
616
+ /**
617
+ * build an action function that verifies element existence
618
+ *
619
+ * @param selector reference to the element
620
+ * @param elemName the name of what we are looking for
621
+ * @param retryDelay optional integer how long to wait before a retry if selector fails
622
+ * @return a function that takes an object with fields {expected: boolean} as its argument
623
+ */
624
+ appmap.actionBuilder.makeAction.verifyElement.existence = function(selector, elemName, retryDelay) {
625
+ return function(parm) {
626
+ var msg = "";
627
+ try {
628
+ var elem = appmap.actionBuilder._getElement(selector, retryDelay);
629
+ if ((parm.expected === true) == isNotNilElement(elem)) return;
630
+ } catch (e) {
631
+ msg = ": " + e.toString();
632
+ if (!(parm.expected === true)) return;
633
+ }
634
+ throw new IlluminatorRuntimeVerificationException("Element " + elemName + " failed existence check "
635
+ + "(expected: " + parm.expected + ")" + msg);
636
+ };
637
+ };
638
+
639
+
640
+ /**
641
+ * build an action function that verifies element visibility
642
+ *
643
+ * @param selector reference to the element
644
+ * @param elemName the name of what we are looking for
645
+ * @param retryDelay optional integer how long to wait before a retry if selector fails
646
+ * @return a function that takes an object with fields {expected: boolean} as its argument
647
+ */
648
+ appmap.actionBuilder.makeAction.verifyElement.visibility = function(selector, elemName, retryDelay) {
649
+ return function(parm) {
650
+ var msg = "";
651
+ try {
652
+ var elem = appmap.actionBuilder._getElement(selector, retryDelay);
653
+ if ((elem && elem.isVisible()) == parm.expected) return;
654
+ } catch (e) {
655
+ msg = ": " + e.toString();
656
+ if (!(parm.expected === true)) return;
657
+ }
658
+ throw new IlluminatorRuntimeVerificationException("Element " + elemName + " failed visibility check "
659
+ + "(expected: " + parm.expected + ")" + msg);
660
+ };
661
+ };
662
+
663
+ /**
664
+ * build an action function that verifies element editability
665
+ *
666
+ * @param selector reference to the element
667
+ * @param elemName the name of what we are looking for
668
+ * @param retryDelay optional integer how long to wait before a retry if selector fails
669
+ * @return a function that takes an object with fields {expected: boolean} as its argument
670
+ */
671
+ appmap.actionBuilder.makeAction.verifyElement.editability = function(selector, elemName, retryDelay) {
672
+ return appmap.actionBuilder.makeAction.verifyElement._predicate(selector, elemName, function (elem) {
673
+ return elem.checkIsEditable();
674
+ }, "isEditable", retryDelay);
675
+ };
676
+
677
+ /**
678
+ * build an action function that taps an element
679
+ *
680
+ * @param selector reference to the element
681
+ * @param elemName the name of what we are looking for
682
+ * @param retryDelay optional integer how long to wait before a retry if selector fails
683
+ * @return a no-argument function
684
+ */
685
+ appmap.actionBuilder.makeAction.element.tap = function(selector, elemName, retryDelay) {
686
+ return appmap.actionBuilder.makeAction.element._interaction(selector, elemName, function (elem, parm) {
687
+ elem.tap()
688
+ }, retryDelay);
689
+ };
690
+
691
+ /**
692
+ * build an action function that vtaps an element
693
+ *
694
+ * @param selector reference to the element
695
+ * @param elemName the name of what we are looking for
696
+ * @param retryDelay optional integer how long to wait before a retry if selector fails
697
+ * @return a no-argument function
698
+ */
699
+ appmap.actionBuilder.makeAction.element.vtap = function(selector, elemName, retryDelay) {
700
+ return appmap.actionBuilder.makeAction.element._interaction(selector, elemName, function (elem, parm) {
701
+ elem.vtap(4)
702
+ }, retryDelay);
703
+ };
704
+
705
+ /**
706
+ * build an action function that svtaps an element
707
+ *
708
+ * @param selector reference to the element
709
+ * @param elemName the name of what we are looking for
710
+ * @param retryDelay optional integer how long to wait before a retry if selector fails
711
+ * @return a no-argument function
712
+ */
713
+ appmap.actionBuilder.makeAction.element.svtap = function(selector, elemName, retryDelay) {
714
+ return appmap.actionBuilder.makeAction.element._interaction(selector, elemName, function (elem, parm) {
715
+ elem.svtap(4)
716
+ }, retryDelay);
717
+ };
718
+
719
+ /**
720
+ * build an action function that types text in an element
721
+ *
722
+ * @param selector reference to the element
723
+ * @param elemName the name of what we are looking for
724
+ * @param retryDelay optional integer how long to wait before a retry if selector fails
725
+ * @return a function that takes an object with fields {text: string, clear: boolean} as its argument
726
+ */
727
+ appmap.actionBuilder.makeAction.element.typeString = function(selector, elemName, retryDelay) {
728
+ return appmap.actionBuilder.makeAction.element._interaction(selector, elemName, function (elem, parm) {
729
+ elem.typeString(parm.text, parm.clear === true);
730
+ }, retryDelay);
731
+ };
732
+
733
+ /**
734
+ * build an action function that checks the existence of a child selector
735
+ *
736
+ * @param retryDelay optional integer how long to wait before a retry if selector fails
737
+ * @param parentSelector reference to the parent element
738
+ * @return a function that takes an object with fields {selector: selector} as its argument
739
+ */
740
+ appmap.actionBuilder.makeAction.selector.verifyExists = function(retryDelay, parentSelector) {
741
+ return function(parm) {
742
+ var fullSelector;
743
+ if (undefined === parentSelector || !(parentSelector) || 0 == Object.keys(parentSelector).length) {
744
+ fullSelector = parm.selector;
745
+ } else {
746
+ fullSelector = parentSelector;
747
+ fullSelector.push(parm.selector);
748
+ }
749
+
750
+ //TODO: possibly cache the parentSelector element and use that instead of target()
751
+
752
+ var elemsObj = target().getChildElements(fullSelector);
753
+ // one retry allowed
754
+ if (Object.keys(elemsObj).length == 0 && retryDelay !== undefined && retryDelay >= 0) {
755
+ elemsObj = target().getChildElements(fullSelector);
756
+ }
757
+
758
+ // react to number of elements
759
+ switch (Object.keys(elemsObj).length) {
760
+ case 0: throw new IlluminatorRuntimeFailureException("Selector found no elements: " + JSON.stringify(fullSelector));
761
+ case 1: return;
762
+ default: UIALogger.logMessage("Selector (for existence) found multiple elements: " + JSON.stringify(elemsObj));
763
+ }
764
+ };
765
+ };
766
+
767
+ }).call(this);