illuminator 0.1.0

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