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,142 @@
1
+ /**
2
+ *
3
+ * Base64 encode / decode
4
+ * http://www.webtoolkit.info/
5
+ *
6
+ **/
7
+
8
+ var Base64 = {
9
+
10
+ // private property
11
+ _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
12
+
13
+ // public method for encoding
14
+ encode : function (input) {
15
+ var output = "";
16
+ var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
17
+ var i = 0;
18
+
19
+ input = Base64._utf8_encode(input);
20
+
21
+ while (i < input.length) {
22
+
23
+ chr1 = input.charCodeAt(i++);
24
+ chr2 = input.charCodeAt(i++);
25
+ chr3 = input.charCodeAt(i++);
26
+
27
+ enc1 = chr1 >> 2;
28
+ enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
29
+ enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
30
+ enc4 = chr3 & 63;
31
+
32
+ if (isNaN(chr2)) {
33
+ enc3 = enc4 = 64;
34
+ } else if (isNaN(chr3)) {
35
+ enc4 = 64;
36
+ }
37
+
38
+ output = output +
39
+ this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
40
+ this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
41
+
42
+ }
43
+
44
+ return output;
45
+ },
46
+
47
+ // public method for decoding
48
+ decode : function (input) {
49
+ var output = "";
50
+ var chr1, chr2, chr3;
51
+ var enc1, enc2, enc3, enc4;
52
+ var i = 0;
53
+
54
+ input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
55
+
56
+ while (i < input.length) {
57
+
58
+ enc1 = this._keyStr.indexOf(input.charAt(i++));
59
+ enc2 = this._keyStr.indexOf(input.charAt(i++));
60
+ enc3 = this._keyStr.indexOf(input.charAt(i++));
61
+ enc4 = this._keyStr.indexOf(input.charAt(i++));
62
+
63
+ chr1 = (enc1 << 2) | (enc2 >> 4);
64
+ chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
65
+ chr3 = ((enc3 & 3) << 6) | enc4;
66
+
67
+ output = output + String.fromCharCode(chr1);
68
+
69
+ if (enc3 != 64) {
70
+ output = output + String.fromCharCode(chr2);
71
+ }
72
+ if (enc4 != 64) {
73
+ output = output + String.fromCharCode(chr3);
74
+ }
75
+
76
+ }
77
+
78
+ output = Base64._utf8_decode(output);
79
+
80
+ return output;
81
+
82
+ },
83
+
84
+ // private method for UTF-8 encoding
85
+ _utf8_encode : function (string) {
86
+ string = string.replace(/\r\n/g,"\n");
87
+ var utftext = "";
88
+
89
+ for (var n = 0; n < string.length; n++) {
90
+
91
+ var c = string.charCodeAt(n);
92
+
93
+ if (c < 128) {
94
+ utftext += String.fromCharCode(c);
95
+ }
96
+ else if((c > 127) && (c < 2048)) {
97
+ utftext += String.fromCharCode((c >> 6) | 192);
98
+ utftext += String.fromCharCode((c & 63) | 128);
99
+ }
100
+ else {
101
+ utftext += String.fromCharCode((c >> 12) | 224);
102
+ utftext += String.fromCharCode(((c >> 6) & 63) | 128);
103
+ utftext += String.fromCharCode((c & 63) | 128);
104
+ }
105
+
106
+ }
107
+
108
+ return utftext;
109
+ },
110
+
111
+ // private method for UTF-8 decoding
112
+ _utf8_decode : function (utftext) {
113
+ var string = "";
114
+ var i = 0;
115
+ var c = c1 = c2 = 0;
116
+
117
+ while ( i < utftext.length ) {
118
+
119
+ c = utftext.charCodeAt(i);
120
+
121
+ if (c < 128) {
122
+ string += String.fromCharCode(c);
123
+ i++;
124
+ }
125
+ else if((c > 191) && (c < 224)) {
126
+ c2 = utftext.charCodeAt(i+1);
127
+ string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
128
+ i += 2;
129
+ }
130
+ else {
131
+ c2 = utftext.charCodeAt(i+1);
132
+ c3 = utftext.charCodeAt(i+2);
133
+ string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
134
+ i += 3;
135
+ }
136
+
137
+ }
138
+
139
+ return string;
140
+ }
141
+
142
+ }
@@ -0,0 +1,102 @@
1
+ // Bridge.js
2
+ //
3
+ // gives access to (some) native methods in the app, bypassing the UI
4
+
5
+
6
+ var debugBridge = false;
7
+
8
+ (function() {
9
+
10
+ var root = this,
11
+ bridge = null;
12
+
13
+ // put bridge in namespace of importing code
14
+ if (typeof exports !== 'undefined') {
15
+ bridge = exports;
16
+ } else {
17
+ bridge = root.bridge = {};
18
+ }
19
+
20
+ // Exception classes
21
+ bridge.SetupException = makeErrorClass("BridgeSetupException");
22
+
23
+
24
+ var bridgeCallNum = 0; // the call ID of a request in progress
25
+ var bridgeWaitTime = 6; // seconds to wait for response from a bridge call
26
+
27
+ bridge.runNativeMethod = function(selector, arguments_obj) {
28
+
29
+ var arguments = undefined;
30
+ if (arguments_obj !== undefined) arguments = JSON.stringify(arguments_obj);
31
+
32
+ var UID = "Bridge_call_" + (++bridgeCallNum).toString();
33
+ UIALogger.logDebug(["Bridge running native method via '",
34
+ UID,
35
+ "': selector='",
36
+ selector,
37
+ "', arguments='",
38
+ arguments,
39
+ "'"
40
+ ].join(""));
41
+
42
+ var taskArguments = [];
43
+
44
+ var scriptPath = IlluminatorScriptsDirectory + "/UIAutomationBridge.rb";
45
+ taskArguments.push(scriptPath);
46
+
47
+ taskArguments.push("--callUID=" + UID)
48
+
49
+ if (config.isHardware) {
50
+ taskArguments.push("--hardwareID=" + config.targetDeviceID);
51
+ }
52
+
53
+ if (selector !== undefined) {
54
+ taskArguments.push("--selector=" + selector);
55
+ }
56
+
57
+ if (arguments !== undefined) {
58
+ taskArguments.push("--b64argument=" + Base64.encode(arguments));
59
+ }
60
+
61
+ UIALogger.logDebug("Bridge waiting for acknowledgment of UID '"
62
+ + UID + "'" + " from $ /usr/bin/ruby "
63
+ + taskArguments.join(" "));
64
+
65
+ output = target().host().performTaskWithPathArgumentsTimeout("/usr/bin/ruby", taskArguments, 500);
66
+
67
+
68
+ if (output) {
69
+ if ("" == output.stdout.trim()) {
70
+ UIALogger.logWarning("Ruby may not be working; to diagnose, run this same command in a terminal: "
71
+ + "$ ruby " + taskArguments.join(" "));
72
+ throw new bridge.SetupException("Bridge got back an empty/blank string instead of JSON");
73
+ }
74
+ try {
75
+ jsonOutput = JSON.parse(output.stdout);
76
+ } catch(e) {
77
+ throw new IlluminatorRuntimeFailureException("Bridge got back bad JSON: " + output.stdout);
78
+ }
79
+ } else {
80
+ jsonOutput = null;
81
+ }
82
+
83
+ var success = jsonOutput["success"];
84
+ var bridgeFailMsg = jsonOutput["message"];
85
+ var response = jsonOutput["response"];
86
+
87
+ // this status check tries to figure out whether the connection to the sim was successful
88
+ if (!success) {
89
+ throw new IlluminatorRuntimeFailureException("Bridge call failed: " + bridgeFailMsg);
90
+ }
91
+
92
+ return response["result"];
93
+
94
+ };
95
+
96
+ bridge.makeActionFunction = function(selector) {
97
+ return function(parm) {
98
+ bridge.runNativeMethod(selector, parm);
99
+ };
100
+ }
101
+
102
+ }).call(this);
@@ -0,0 +1,92 @@
1
+ // Config.js
2
+ //
3
+ // loads configuration from a generated file, provides sensible defaults
4
+
5
+ (function() {
6
+
7
+ var root = this,
8
+ config = null;
9
+
10
+ // put config in namespace of importing code
11
+ if (typeof exports !== 'undefined') {
12
+ config = exports;
13
+ } else {
14
+ config = root.config = {};
15
+ }
16
+
17
+ config.implementation = 'Unspecified_iOS_Device';
18
+ config.automatorTagsAny = []; // run all by default
19
+ config.automatorTagsAll = []; // none by default
20
+ config.automatorTagsNone = [];
21
+ config.automatorSequenceRandomSeed = undefined;
22
+ config.buildArtifacts = {};
23
+
24
+ config.setField = function (key, value) {
25
+ switch (key) {
26
+ case "automatorSequenceRandomSeed":
27
+ config.automatorSequenceRandomSeed = parseInt(value);
28
+ break;
29
+ default:
30
+ config[key] = value;
31
+ }
32
+ }
33
+
34
+
35
+ // expected keys, and whether they are required
36
+ var expectedKeys = {
37
+ "saltinel": true,
38
+ "entryPoint": true,
39
+ "implementation": true,
40
+ "automatorDesiredSimDevice": true,
41
+ "automatorDesiredSimVersion": true,
42
+ "targetDeviceID": true,
43
+ "isHardware": true,
44
+ "xcodePath": true,
45
+ "automatorTagsAny": false,
46
+ "automatorTagsAll": false,
47
+ "automatorTagsNone": false,
48
+ "automatorScenarioNames": false,
49
+ "automatorSequenceRandomSeed": false,
50
+ "automatorScenarioOffset": true,
51
+ "customConfig": false,
52
+ };
53
+
54
+ var jsonConfig = host().readJSONFromFile(IlluminatorBuildArtifactsDirectory + "/IlluminatorGeneratedConfig.json");
55
+ // check for keys we don't expect
56
+ for (var k in jsonConfig) {
57
+ if (expectedKeys[k] === undefined) {
58
+ UIALogger.logMessage("Config got unexpected key " + k);
59
+ }
60
+ }
61
+
62
+ // test for keys we DO expect
63
+ for (var k in expectedKeys) {
64
+ if (jsonConfig[k] !== undefined) {
65
+ config.setField(k, jsonConfig[k]);
66
+ } else if (expectedKeys[k]) {
67
+ UIALogger.logWarning("Couldn't read " + k + " from generated config");
68
+ }
69
+ }
70
+
71
+ // find the directory where screenshots will go
72
+ IlluminatorInstrumentsOutputDirectory
73
+ // handles globbing of a path that may have spaces in it, assumes newest directory is the run directory
74
+ var findMostRecentDirCmd = 'eval ls -1td "' + IlluminatorInstrumentsOutputDirectory + '/Run*" | head -n 1';
75
+ var output = host().shellAsFunction("/bin/bash", ["-c", findMostRecentDirCmd], 5);
76
+ config.screenshotDir = output.stdout;
77
+
78
+ // create temp dir for build artifacts and note path names
79
+ var tmpDir = IlluminatorBuildArtifactsDirectory + "/UIAutomation-outputs";
80
+ target().host().performTaskWithPathArgumentsTimeout("/bin/mkdir", ["-p", tmpDir], 5);
81
+ config.buildArtifacts.root = tmpDir;
82
+ config.buildArtifacts.appMapMarkdown = tmpDir + "/appMap.md";
83
+ config.buildArtifacts.automatorMarkdown = tmpDir + "/automator.md";
84
+ config.buildArtifacts.automatorJSON = tmpDir + "/automator.json";
85
+ config.buildArtifacts.automatorScenarioJSON = tmpDir + "/automatorScenarios.json";
86
+ config.buildArtifacts.intendedTestList = tmpDir + "/intendedTestList.json";
87
+
88
+ //This line is a placeholder so that the automator can communicate its current operation to the entire set of Illuminator javascript extensions.
89
+ //This is because the logical first choice for implementing this -- automator state -- is not available from Extensions.js
90
+ config.automatorModality = "init";
91
+
92
+ }).call(this);
@@ -0,0 +1,2025 @@
1
+ // Extensions.js - Extensions to Apple's UIAutomation library
2
+
3
+
4
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
5
+ //
6
+ // Exceptions
7
+ //
8
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9
+
10
+ /**
11
+ * Decode a stack trace into something readable
12
+ *
13
+ * UIAutomation has a decent `.backtrace` property for errors, but ONLY for the `Error` class.
14
+ * As of this writing, many attempts to produce that property on user-defined error classes have failed.
15
+ * This function decodes the somewhat-readable `.stack` property into something better
16
+ *
17
+ * Decode the following known types of stack lines:
18
+ * - built-in functions in this form: funcName@[native code]
19
+ * - anonymous functions in this form: file://<path>/file.js:line:col
20
+ * - named functions in this form: funcName@file://<path>/file.js:line:col
21
+ * - top-level calls in this form: global code@file://<path>/file.js:line:col
22
+ *
23
+ * @param trace string returned by the "stack" property of a caught exception
24
+ * @return object
25
+ * { isOK: boolean, whether any errors at all were encountered
26
+ * message: string describing any error encountered
27
+ * errorName: string name of the error
28
+ * stack: array of trace objects [
29
+ * { functionName: string name of function, or undefined if the function was anonymous
30
+ * nativeCode: boolean whether the function was defined in UIAutomation binary code
31
+ * file: if not native code, the basename of the file containing the function
32
+ * line: if not native code, the line where the function is defined
33
+ * column: if not native code, the column where the function is defined
34
+ * }
35
+ * ]
36
+ * }
37
+ *
38
+ */
39
+ function decodeStackTrace(err) {
40
+ if ("string" == (typeof err)) {
41
+ return {isOK: false, message: "[caught string error, not an error class]", stack: []};
42
+ }
43
+
44
+ if (err.stack === undefined) {
45
+ return {isOK: false, message: "[caught an error without a stack]", stack: []};
46
+ }
47
+
48
+ var ret = {isOK: true, stack: []};
49
+ if (err.name !== undefined) {
50
+ ret.errorName = err.name;
51
+ ret.message = "<why are you reading this? there is nothing wrong.>";
52
+ } else {
53
+ ret.errorName = "<unnamed>";
54
+ ret.message = "[Error class was unnamed]";
55
+ }
56
+
57
+ var lines = err.stack.split("\n");
58
+
59
+ for (var i = 0; i < lines.length; ++i) {
60
+ var l = lines[i];
61
+ var r = {};
62
+ var location;
63
+
64
+ // extract @ symbol if it exists, which defines whether function is anonymous
65
+ var atPos = l.indexOf("@");
66
+ if (-1 == atPos) {
67
+ location = l;
68
+ } else {
69
+ r.functionName = l.substring(0, atPos);
70
+ location = l.substring(atPos + 1);
71
+ }
72
+
73
+ // check whether the function is built in to UIAutomation
74
+ r.nativeCode = ("[native code]" == location);
75
+
76
+ // extract file, line, and column if not native code
77
+ if (!r.nativeCode) {
78
+ var tail = location.substring(location.lastIndexOf("/") + 1);
79
+ var items = tail.split(":");
80
+ r.file = items[0];
81
+ r.line = items[1];
82
+ r.column = items[2];
83
+ }
84
+
85
+
86
+ //string.substring(string.indexOf("_") + 1)
87
+ ret.stack.push(r);
88
+ }
89
+
90
+ return ret;
91
+ }
92
+
93
+
94
+ /**
95
+ * Get a stack trace (this function omitted) from any location in code
96
+ *
97
+ * @return just the stack property of decodeStackTrace
98
+ */
99
+ function getStackTrace() {
100
+ try {
101
+ throw new Error("base");
102
+ } catch (e) {
103
+ return decodeStackTrace(e).stack.slice(1);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Get the filename of the current file being executed
109
+ *
110
+ * @return the filename
111
+ */
112
+ function __file__() {
113
+ // just ask for the 1st position on stack, after the __file__ call itself
114
+ return getStackTrace()[1].file;
115
+ }
116
+
117
+ /**
118
+ * Get the name of the current function being executed
119
+ *
120
+ * @param offset integer whether to
121
+ * @return the function name
122
+ */
123
+ function __function__(offset) {
124
+ offset = offset || 0;
125
+ // just ask for the 1st position on stack, after the __function__ call itself
126
+ return getStackTrace()[1 + offset].functionName;
127
+ }
128
+
129
+ /**
130
+ * Shortcut to defining simple error classes
131
+ *
132
+ * @param className string name for the new error class
133
+ * @return a function that is used to construct new error instances
134
+ */
135
+ function makeErrorClass(className) {
136
+ return function (message) {
137
+ this.name = className;
138
+ this.message = message;
139
+ this.toString = function() { return this.name + ": " + this.message; };
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Shortcut to defining error classes that indicate the function/file/line that triggered them
145
+ *
146
+ * These are for cases where the errors are expected to be caught by the global error handler
147
+ *
148
+ * @param fileName string basename of the file where the function is defined (gets stripped out)
149
+ * @param className string name for the new error class
150
+ * @return a function that is used to construct new error instances
151
+ */
152
+ function makeErrorClassWithGlobalLocator(fileName, className) {
153
+
154
+ var _getCallingFunction = function () {
155
+ var stack = getStackTrace();
156
+ // start from 2nd position on stack, after _getCallingFunction and makeErrorClassWithGlobalLocator
157
+ for (var i = 2; i < stack.length; ++i) {
158
+ var l = stack[i];
159
+ if (!(l.nativeCode || fileName == l.file)) {
160
+ return "In " + l.functionName + " at " + l.file + " line " + l.line + " col " + l.column + ": ";
161
+ }
162
+ }
163
+ return "";
164
+ };
165
+
166
+ return function (message) {
167
+ this.name = className;
168
+ this.message = _getCallingFunction() + message;
169
+ this.toString = function() { return this.name + ": " + this.message; };
170
+ };
171
+ }
172
+
173
+ IlluminatorSetupException = makeErrorClass("IlluminatorSetupException");
174
+ IlluminatorRuntimeFailureException = makeErrorClass("IlluminatorRuntimeFailureException");
175
+ IlluminatorRuntimeVerificationException = makeErrorClass("IlluminatorRuntimeVerificationException");
176
+
177
+
178
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
179
+ //
180
+ // General-purpose functions
181
+ //
182
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
183
+
184
+ /**
185
+ * shortcut function to get UIATarget.localTarget(), sets _accessor
186
+ */
187
+ function target() {
188
+ var ret = UIATarget.localTarget();
189
+ ret._accessor = "target()";
190
+ return ret;
191
+ }
192
+
193
+ /**
194
+ * shortcut function to get UIATarget.localTarget().frontMostApp().mainWindow(), sets _accessor
195
+ */
196
+ function mainWindow() {
197
+ var ret = UIATarget.localTarget().frontMostApp().mainWindow();
198
+ ret._accessor = "mainWindow()";
199
+ return ret;
200
+ }
201
+
202
+ /**
203
+ * shortcut function to get UIATarget.host()
204
+ */
205
+ function host() {
206
+ return UIATarget.localTarget().host();
207
+ }
208
+
209
+ /**
210
+ * delay for a number of seconds
211
+ *
212
+ * @param seconds float how long to wait
213
+ */
214
+ function delay(seconds) {
215
+ target().delay(seconds);
216
+ }
217
+
218
+ /**
219
+ * get the current time: seconds since epoch, with decimal for millis
220
+ */
221
+ function getTime() {
222
+ return (new Date).getTime() / 1000;
223
+ }
224
+
225
+
226
+ /**
227
+ * EXTENSION PROFILER
228
+ */
229
+ (function() {
230
+
231
+ var root = this,
232
+ extensionProfiler = null;
233
+
234
+ // put extensionProfiler in namespace of importing code
235
+ if (typeof exports !== 'undefined') {
236
+ extensionProfiler = exports;
237
+ } else {
238
+ extensionProfiler = root.extensionProfiler = {};
239
+ }
240
+
241
+ /**
242
+ * reset the stored criteria costs
243
+ */
244
+ extensionProfiler.resetCriteriaCost = function () {
245
+ extensionProfiler._criteriaCost = {};
246
+ extensionProfiler._criteriaTotalCost = {}
247
+ extensionProfiler._criteriaTotalHits = {};
248
+ extensionProfiler._bufferCriteria = false;
249
+ };
250
+ extensionProfiler.resetCriteriaCost(); // initialize it
251
+
252
+
253
+ /**
254
+ * sometimes critera are evaluated in a loop because we are waiting for something; don't count that
255
+ *
256
+ * indicates that we should store ONLY THE MOST RECENT lookup times in an array
257
+ */
258
+ extensionProfiler.bufferCriteriaCost = function() {
259
+ extensionProfiler._bufferCriteria = true;
260
+ };
261
+
262
+
263
+ /**
264
+ * sometimes critera are evaluated in a loop because we are waiting for something; don't count that
265
+ *
266
+ * indicates that we should store ONLY THE MOST RECENT lookup times in an array
267
+ */
268
+ extensionProfiler.UnbufferCriteriaCost = function() {
269
+ extensionProfiler._bufferCriteria = false;
270
+ // replay the most recent values into the totals
271
+ for (var c in extensionProfiler._criteriaCost) {
272
+ extensionProfiler.recordCriteriaCost(c, extensionProfiler._criteriaCost[c]);
273
+ }
274
+ extensionProfiler._criteriaCost = {};
275
+ };
276
+
277
+
278
+ /**
279
+ * keep track of the cumulative time spent looking for criteria
280
+ *
281
+ * @param criteria the criteria object or object array
282
+ * @param time the time spent looking up that criteria
283
+ */
284
+ extensionProfiler.recordCriteriaCost = function (criteria, time) {
285
+ // criteria can be a string if it comes from our buffered array, so allow it.
286
+ var key = (typeof criteria) == "string" ? criteria : JSON.stringify(criteria);
287
+ if (extensionProfiler._bufferCriteria) {
288
+ extensionProfiler._criteriaCost[key] = time; // only store the most recent one, we'll merge later
289
+ } else {
290
+ if (undefined === extensionProfiler._criteriaTotalCost[key]) {
291
+ extensionProfiler._criteriaTotalCost[key] = 0;
292
+ extensionProfiler._criteriaTotalHits[key] = 0;
293
+ }
294
+ extensionProfiler._criteriaTotalCost[key] += time;
295
+ extensionProfiler._criteriaTotalHits[key]++;
296
+ }
297
+ };
298
+
299
+ /**
300
+ * return an array of objects indicating the cumulative time spent looking for criteria -- high time to low
301
+ *
302
+ * @return array of {criteria: x, time: y, hits: z} objects
303
+ */
304
+ extensionProfiler.getCriteriaCost = function () {
305
+ var ret = [];
306
+ for (var criteria in extensionProfiler._criteriaTotalCost) {
307
+ ret.push({"criteria": criteria,
308
+ "time": extensionProfiler._criteriaTotalCost[criteria],
309
+ "hits": extensionProfiler._criteriaTotalHits[criteria]});
310
+ }
311
+ ret.sort(function(a, b) { return b.time - a.time; });
312
+ return ret;
313
+ };
314
+
315
+ }).call(this);
316
+
317
+
318
+ /**
319
+ * convert a number of seconds to hh:mm:ss.ss
320
+ *
321
+ * @param seconds the number of seconds (decimal OK)
322
+ */
323
+ function secondsToHMS(seconds) {
324
+ var s = Math.floor(seconds);
325
+ var f = seconds - s;
326
+ var h = Math.floor(s / 3600);
327
+ s -= h * 3600;
328
+ var m = Math.floor(s / 60);
329
+ s -= m * 60;
330
+
331
+ // build strings
332
+ h = h > 0 ? (h + ":") : "";
333
+ m = (m > 9 ? m.toString() : ("0" + m.toString())) + ":";
334
+ s = s > 9 ? s.toString() : ("0" + s.toString());
335
+ f = f > 0 ? ("." + Math.round(f * 100).toString()) : "";
336
+ return h + m + s + f;
337
+ }
338
+
339
+ /**
340
+ * Extend an object prototype with an associative array of properties
341
+ *
342
+ * @param baseClass a javascript class
343
+ * @param properties an associative array of properties to add to the prototype of baseClass
344
+ */
345
+ function extendPrototype(baseClass, properties) {
346
+ for (var p in properties) {
347
+ baseClass.prototype[p] = properties[p];
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Return true if the element is usable -- not some form of nil
353
+ *
354
+ * @param elem the element to check
355
+ */
356
+ function isNotNilElement(elem) {
357
+ if (elem === undefined) return false;
358
+ if (elem === null) return false;
359
+ if (elem.isNotNil) return elem.isNotNil();
360
+ return elem.toString() != "[object UIAElementNil]";
361
+ }
362
+
363
+ /**
364
+ * Return true if a selector is "hard" -- referring to one and only one element by nature
365
+ */
366
+ function isHardSelector(selector) {
367
+ switch (typeof selector) {
368
+ case "function": return true;
369
+ case "string": return true;
370
+ default: return false;
371
+ }
372
+ }
373
+
374
+ /**
375
+ * "constructor" for UIAElementNil
376
+ *
377
+ * UIAutomation doesn't give us access to the UIAElementNil constructor, so do it our own way
378
+ */
379
+ function newUIAElementNil() {
380
+ try {
381
+ UIATarget.localTarget().pushTimeout(0);
382
+ return UIATarget.localTarget().frontMostApp().windows().firstWithPredicate("name == 'Illuminator' and name == 'newUIAElementNil()'");
383
+ } catch(e) {
384
+ throw e;
385
+ } finally {
386
+ UIATarget.localTarget().popTimeout();
387
+ }
388
+ }
389
+
390
+
391
+ /**
392
+ * Wait for a function to return a value (i.e. not throw an exception)
393
+ *
394
+ * Execute a function repeatedly. If it returns a value, return that value.
395
+ * If the timeout is reached, re-raise the exception that the function raised.
396
+ * Guaranteed to execute once and only once after timeout has passed, ensuring
397
+ * that the function is given its full allotted time (2 runs minimum if only exceptions are thrown)
398
+ *
399
+ * @param callerName string name of calling function for logging/erroring purposes
400
+ * @param timeout the timeout in seconds
401
+ * @param functionReturningValue the function to execute. can return anything.
402
+ */
403
+ function waitForReturnValue(timeout, callerName, functionReturningValue) {
404
+ var myGetTime = function () {
405
+ return (new Date).getTime() / 1000;
406
+ }
407
+
408
+ switch (typeof timeout) {
409
+ case "number": break;
410
+ default: throw new IlluminatorSetupException("waitForReturnValue got a bad timeout type: (" + (typeof timeout) + ") " + timeout);
411
+ }
412
+
413
+ var stopTime = myGetTime() + timeout;
414
+ var caught = null;
415
+
416
+
417
+ for (var now = myGetTime(), runsAfterTimeout = 0; now < stopTime || runsAfterTimeout < 1; now = myGetTime()) {
418
+ if (now >= stopTime) {
419
+ ++runsAfterTimeout;
420
+ }
421
+
422
+ try {
423
+ return functionReturningValue();
424
+ } catch (e) {
425
+ caught = e;
426
+ }
427
+ delay(0.1); // max 10 Hz
428
+ }
429
+
430
+ throw new IlluminatorRuntimeFailureException(callerName + " failed by timeout after " + timeout + " seconds: " + caught);
431
+ }
432
+
433
+
434
+ /**
435
+ * return unique elements (based on UIAElement.equals()) from a {key: element} object
436
+ *
437
+ * @param elemObject an object containing UIAElements keyed on strings
438
+ */
439
+ function getUniqueElements(elemObject) {
440
+ var ret = {};
441
+ for (var i in elemObject) {
442
+ var elem = elemObject[i];
443
+ var found = false;
444
+ // add elements to return object if they are not already there (via equality)
445
+ for (var j in ret) {
446
+ if (ret[j].equals(elem)) {
447
+ found = true;
448
+ break;
449
+ }
450
+ }
451
+
452
+ if (!found) {
453
+ ret[i] = elem;
454
+ }
455
+ }
456
+ return ret;
457
+ }
458
+
459
+
460
+ /**
461
+ * Get one element from a selector result
462
+ */
463
+ function getOneCriteriaSearchResult(callerName, elemObject, originalCriteria, allowZero) {
464
+ // assert that there is only one element
465
+ var uniq = getUniqueElements(elemObject);
466
+ var size = Object.keys(elemObject).length;
467
+ if (size > 1 || size == 0 && !allowZero) {
468
+ var msg = callerName + ": expected 1 element";
469
+ if (originalCriteria !== undefined) {
470
+ msg += " from selector " + JSON.stringify(originalCriteria);
471
+ }
472
+ msg += ", received " + size.toString();
473
+ if (size > 0) {
474
+ msg += " {";
475
+ for (var k in elemObject) {
476
+ msg += "\n " + k + ": " + elemObject[k].toString();
477
+ }
478
+ msg += "\n}";
479
+ }
480
+ throw new IlluminatorRuntimeFailureException(msg);
481
+ }
482
+
483
+ // they're all the same, so return just one
484
+ for (var k in elemObject) {
485
+ UIALogger.logDebug("Selector found object with canonical name: " + k);
486
+ return elemObject[k];
487
+ }
488
+ return newUIAElementNil();
489
+ }
490
+
491
+
492
+ /**
493
+ * Resolve an expression to a set of UIAElements
494
+ *
495
+ * Criteria can be one of the following:
496
+ * 1. An object of critera to satisfy UIAElement..find() .
497
+ * 2. An array of objects containing UIAElement.find() criteria; elem = UIAElement.find(arr[0])[0..n].find(arr[1])...
498
+ *
499
+ * @param criteria as described above
500
+ * @param parentElem a UIAElement from which the search for elements will begin
501
+ * @param elemAccessor string representation of the accessor required to get the parentElem
502
+ */
503
+ function getElementsFromCriteria(criteria, parentElem, elemAccessor) {
504
+ if (parentElem === undefined) {
505
+ parentElem = target();
506
+ elemAccessor = parentElem._accessor;
507
+ }
508
+
509
+ if (elemAccessor === undefined) {
510
+ elemAccessor = "<root elem>";
511
+ }
512
+
513
+ // search in the appropriate way
514
+ if (!(criteria instanceof Array)) {
515
+ criteria = [criteria];
516
+ }
517
+
518
+ // speed hack: backtrack from visibility=true in criteria to prune search tree
519
+ var rippleVisibility = false;
520
+ for (var i = criteria.length - 1; i >= 0; --i) {
521
+ if (rippleVisibility) {
522
+ criteria[i].isVisible = true;
523
+ } else if (criteria[i].isVisible === true) {
524
+ rippleVisibility = true;
525
+ }
526
+ }
527
+
528
+ // perform a find in several stages
529
+ var segmentedFind = function (criteriaArray, initialElem, initialAccessor) {
530
+ var intermElems = {};
531
+ intermElems[initialAccessor] = initialElem; // intermediate elements
532
+ // go through all criteria
533
+ for (var i = 0; i < criteriaArray.length; ++i) {
534
+ var tmp = {};
535
+ // expand search on each intermediate element using current criteria
536
+ for (var k in intermElems) {
537
+ var newFrontier = intermElems[k].find(criteriaArray[i], k);
538
+ // merge results with temporary storage
539
+ for (var f in newFrontier) {
540
+ tmp[f] = newFrontier[f];
541
+ }
542
+ }
543
+ // move unique elements from temporary storage into loop variable
544
+ intermElems = getUniqueElements(tmp);
545
+ }
546
+ return intermElems;
547
+ }
548
+
549
+ var startTime = getTime();
550
+ try {
551
+ return segmentedFind(criteria, parentElem, elemAccessor);
552
+ } catch (e) {
553
+ throw e;
554
+ } finally {
555
+ var cost = getTime() - startTime;
556
+ extensionProfiler.recordCriteriaCost(criteria, cost);
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Resolve a string expression to a UIAElement using Eval
562
+ *
563
+ * @param selector string
564
+ * @param element the element to use as a starting point
565
+ */
566
+ function getChildElementFromEval(selector, element) {
567
+ // wrapper function for lookups, only return element if element is visible
568
+ var visible = function (elem) {
569
+ return elem.isVisible() ? elem : newUIAElementNil();
570
+ }
571
+
572
+ try {
573
+ return eval(selector);
574
+ } catch (e) {
575
+ if (e instanceof SyntaxError) {
576
+ throw new IlluminatorSetupException("Couldn't evaluate string selector '" + selector + "': " + e);
577
+ } else if (e instanceof TypeError) {
578
+ throw new IlluminatorSetupException("Evaluating string selector on element " + element + " triggered " + e);
579
+ } else {
580
+ throw e;
581
+ }
582
+ }
583
+ }
584
+
585
+ /**
586
+ * construct an input method
587
+ */
588
+ function newInputMethod(methodName, description, isActiveFn, selector, features) {
589
+ var ret = {
590
+ name: methodName,
591
+ description: description,
592
+ isActiveFn: isActiveFn,
593
+ selector: selector,
594
+ features: {}
595
+ };
596
+
597
+ for (var k in features) {
598
+ ret.features[k] = features[k];
599
+ }
600
+
601
+ return ret;
602
+ }
603
+
604
+ var stockKeyboardInputMethod = newInputMethod("defaultKeyboard",
605
+ "Any default iOS keyboard, whether numeric or alphanumeric",
606
+ function () {
607
+ return isNotNilElement(target().frontMostApp().keyboard());
608
+ },
609
+ function (targ) {
610
+ return targ.frontMostApp().keyboard();
611
+ },
612
+ {});
613
+
614
+
615
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
616
+ //
617
+ // Object prototype functions
618
+ //
619
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
620
+
621
+ /**
622
+ * set the (custom) input method for an element
623
+ *
624
+ * @param method the input method
625
+ */
626
+ function setInputMethod(method) {
627
+ this._inputMethod = method;
628
+ }
629
+
630
+ /**
631
+ * Access the custom input method for an element
632
+ */
633
+ function customInputMethod() {
634
+ if (this._inputMethod === undefined) throw new IlluminatorSetupException("No custom input method defined for element " + this);
635
+ var inpMth = this._inputMethod;
636
+
637
+ // open custom input method
638
+ this.checkIsEditable(2);
639
+
640
+ // assign any feature functions to it
641
+ var theInput = target().getOneChildElement(inpMth.selector);
642
+ for (var f in inpMth.features) {
643
+ theInput[f] = inpMth.features[f];
644
+ }
645
+ return theInput;
646
+ }
647
+
648
+ /**
649
+ * type a string in a given text field
650
+ *
651
+ * @param text the string to type
652
+ * @param clear boolean value of whether to clear the text field first
653
+ */
654
+ var typeString = function (text, clear) {
655
+ text = text.toString(); // force string argument to actual string
656
+
657
+ // make sure we can type (side effect: brings up keyboard)
658
+ if (!this.checkIsEditable(2)) {
659
+ throw new IlluminatorRuntimeFailureException("typeString couldn't get the keyboard to appear for element "
660
+ + this.toString() + " with name '" + this.name() + "'");
661
+ }
662
+
663
+ var kb; // keyboard
664
+ var seconds = 2;
665
+ var waitTime = 0.25;
666
+ var maxAttempts = seconds / waitTime;
667
+ var noSuccess = true;
668
+ var failMsg = null;
669
+
670
+
671
+ // get whichever keyboard was specified by the user
672
+ kb = target().getOneChildElement(this._inputMethod.selector);
673
+
674
+ // if keyboard doesn't have a typeString (indicating a custom keyboard) then attempt to load that feature
675
+ if (kb.typeString === undefined) {
676
+ kb.typeString = this._inputMethod.features.typeString;
677
+ if (kb.typeString === undefined) {
678
+ throw new IlluminatorSetupException("Attempted to use typeString() on a custom keyboard that did not define a 'typeString' feature");
679
+ }
680
+ }
681
+
682
+ if (kb.clear === undefined) {
683
+ kb.clear = this._inputMethod.features.clear;
684
+ if (clear && kb.clear === undefined) {
685
+ throw new IlluminatorSetupException("Attempted to use clear() on a custom keyboard that did not define a 'clear' feature");
686
+ }
687
+ }
688
+
689
+ // attempt to get a successful keypress several times -- using the first character
690
+ // this is a hack for iOS 6.x where the keyboard is sometimes "visible" before usable
691
+ while ((clear || noSuccess) && 0 < maxAttempts--) {
692
+ try {
693
+
694
+ // handle clearing
695
+ if (clear) {
696
+ kb.clear(this);
697
+ clear = false; // prevent clear on next iteration
698
+ }
699
+
700
+ if (text.length !== 0) {
701
+ kb.typeString(text.charAt(0));
702
+ }
703
+
704
+ noSuccess = false; // here + no error caught means done
705
+ }
706
+ catch (e) {
707
+ failMsg = e;
708
+ UIATarget.localTarget().delay(waitTime);
709
+ }
710
+ }
711
+
712
+ // report any errors that prevented success
713
+ if (0 > maxAttempts && null !== failMsg) throw new IlluminatorRuntimeFailureException("typeString caught error: " + failMsg);
714
+
715
+ // now type the rest of the string
716
+ try {
717
+ if (text.length > 0) kb.typeString(text.substr(1));
718
+ } catch (e) {
719
+ if (-1 == e.toString().indexOf(" failed to tap ")) throw e;
720
+
721
+ UIALogger.logDebug("Retrying keyboard action, typing slower this time");
722
+ this.typeString("", true);
723
+ kb.setInterKeyDelay(0.2);
724
+ kb.typeString(text);
725
+ }
726
+
727
+ }
728
+
729
+ /**
730
+ * Type a string into a keyboard-like element
731
+ *
732
+ * Element "this" should have UIAKey elements, and this function will attempt to render the string with the available keys
733
+ *
734
+ * @todo get really fancy and solve key sequences for keys that have multiple characters on them
735
+ * @param text the text to type
736
+ */
737
+ function typeStringCustomKeyboard(text) {
738
+ var keySet = this.keys();
739
+ for (var i = 0; i < text.length; ++i) {
740
+ var keyElem = keySet.firstWithName(text[i]);
741
+ if (!isNotNilElement(keyElem)) throw new IlluminatorRuntimeFailureException("typeStringCustomKeyboard failed to find key for " + text[i]);
742
+ keyElem.tap();
743
+ }
744
+ }
745
+
746
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
747
+ //
748
+ // Object prototype extensions
749
+ //
750
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
751
+
752
+
753
+ extendPrototype(UIAElementNil, {
754
+ isNotNil: function () {
755
+ return false;
756
+ },
757
+ isVisible: function () {
758
+ return false;
759
+ }
760
+ });
761
+
762
+
763
+ extendPrototype(UIAElementArray, {
764
+ /**
765
+ * Same as withName, but takes a regular expression
766
+ * @param pattern string regex
767
+ */
768
+ withNameRegex: function (pattern) {
769
+ var ret = [];
770
+ for (var i = 0; i < this.length; ++i) {
771
+ var elem = this[i];
772
+ if (elem && elem.isNotNil && elem.isNotNil() && elem.name() && elem.name().match(pattern) !== null) {
773
+ ret.push(elem);
774
+ }
775
+ }
776
+ return ret;
777
+ },
778
+
779
+ /**
780
+ * Same as firstWithName, but takes a regular expression
781
+ * @param pattern string regex
782
+ */
783
+ firstWithNameRegex: function (pattern) {
784
+ for (var i = 0; i < this.length; ++i) {
785
+ var elem = this[i];
786
+ if (elem && elem.isNotNil && elem.isNotNil() && elem.name()) {
787
+ if (elem.name().match(pattern) !== null) return elem;
788
+ }
789
+ }
790
+ return newUIAElementNil();
791
+ }
792
+ });
793
+
794
+
795
+
796
+ extendPrototype(UIASwitch, {
797
+ /**
798
+ * replacement for setValue on UIASwitch that retries setting value given number of times
799
+ *
800
+ * @param value boolean value to set on switch
801
+ * @param retries integer number of retries to do, defaults to 3
802
+ * @param delaySeconds integer delay between retries in seconds, defaults to 1
803
+ */
804
+ safeSetValue: function (value, retries, delaySeconds) {
805
+ retries = retries || 3;
806
+ delaySeconds = delaySeconds || 1;
807
+ var exception = null;
808
+ for (i = 0; i <= retries; ++i) {
809
+ try {
810
+ this.setValue(value);
811
+ return;
812
+ } catch (e) {
813
+ exception = e
814
+ delay(delaySeconds);
815
+ UIALogger.logWarning("Set switch value failed " + i + " times with error " + e);
816
+ }
817
+ }
818
+ if (exception !== null) {
819
+ throw exception;
820
+ }
821
+ },
822
+ });
823
+
824
+
825
+ extendPrototype(UIAElement, {
826
+
827
+ /**
828
+ * shortcut function: if UIAutomation creates this element, then it must not be nil
829
+ */
830
+ isNotNil: function () {
831
+ return true;
832
+ },
833
+
834
+ /*
835
+ * A note on what a "selector" is:
836
+ *
837
+ * It can be one of 4 things.
838
+ * 1. A lookup function that takes a base element as an argument and returns another UIAElement.
839
+ * 2. A string that contains an expression (starting with "element.") that returns another UIAElement
840
+ * 3. An object containing critera to satisfy UIAElement.find() .
841
+ * 4. An array of objects containing UIAElement.find() criteria; elem = mainWindow.find(arr[0]).find(arr[1])...
842
+ *
843
+ * Selector types 1 and 2 are considered "hard" selectors -- they can return at most one element
844
+ */
845
+
846
+ /**
847
+ * get (possibly several) child elements from Criteria, or none
848
+ *
849
+ * NOTE that this function does not take a selector, just criteria
850
+ * @param criteria
851
+ */
852
+ getChildElements: function (criteria) {
853
+ if (isHardSelector(criteria)) throw new IlluminatorSetupException("getChildElements got a hard selector, which cannot return multiple elements");
854
+ criteria = this.preProcessSelector(criteria);
855
+ var accessor = this._accessor === undefined ? "<unknown>" : this._accessor;
856
+ return getElementsFromCriteria(criteria, this, accessor);
857
+ },
858
+
859
+ /**
860
+ * Common behavior for getting one child element from a selector
861
+ *
862
+ * @param callerName string the name of the calling function, for logging purposes
863
+ * @param selector the selector to use
864
+ * @param allowZero boolean -- if true, failing selector returns UIAElementNil; if false, throw
865
+ */
866
+ _getChildElement: function (callerName, selector, allowZero) {
867
+ switch(typeof selector) {
868
+ case "function":
869
+ return this.preProcessSelector(selector)(this); // TODO: guarantee isNotNil ?
870
+ case "object":
871
+ return getOneCriteriaSearchResult(callerName, this.getChildElements(selector), selector, allowZero);
872
+ case "string":
873
+ return getChildElementFromEval(selector, this)
874
+ default:
875
+ throw new IlluminatorSetupException(caller + " received undefined input type of " + (typeof selector).toString());
876
+ }
877
+ },
878
+
879
+ /**
880
+ * Get one child element from a selector, or UIAElementNil
881
+ * @param selector the selector to use
882
+ */
883
+ getChildElement: function (selector) {
884
+ return this._getChildElement("getChildElement", selector, true);
885
+ },
886
+
887
+ /**
888
+ * Get one and only one child element from a selector, or throw
889
+ * @param selector the selector to use
890
+ */
891
+ getOneChildElement: function (selector) {
892
+ return this._getChildElement("getOneChildElement", selector, false);
893
+ },
894
+
895
+ /**
896
+ * Preprocess a selector
897
+ *
898
+ * This function in the prototype should be overridden by an application-specific function.
899
+ * It allows you to rewrite critera or wrap lookup functions to enable additional functionality.
900
+ *
901
+ * @param selector - a function or set of criteria
902
+ * @return selector
903
+ */
904
+ preProcessSelector: function (selector) {
905
+ return selector;
906
+ },
907
+
908
+
909
+ /**
910
+ * Equality operator
911
+ *
912
+ * Properly detects equality of 2 UIAElement objects
913
+ * - Can return false positives if 2 elements (and ancestors) have the same name, type, and rect()
914
+ * @param elem2 the element to compare to this element
915
+ * @param maxRecursion a recursion limit to observe when checking parent element equality (defaults to -1 for infinite)
916
+ */
917
+ equals: function (elem2, maxRecursion) {
918
+ var sameRect = function (e1, e2) {
919
+ var r1 = e1.rect();
920
+ var r2 = e2.rect();
921
+ return r1.size.width == r2.size.width
922
+ && r1.size.height == r2.size.height
923
+ && r1.origin.x == r2.origin.x
924
+ && r1.origin.y == r2.origin.y;
925
+ }
926
+
927
+ maxRecursion = maxRecursion === undefined ? -1 : maxRecursion;
928
+
929
+ if (this == elem2) return true; // shortcut when x == x
930
+ if (null === elem2) return false; // shortcut when one is null
931
+ if (isNotNilElement(this) != isNotNilElement(elem2)) return false; // both nil or neither
932
+ if (this.toString() != elem2.toString()) return false; // element type
933
+ if (this.name() != elem2.name()) return false;
934
+ if (!sameRect(this, elem2)) return false; // possible false positives!
935
+ if (this.isVisible() != elem2.isVisible()) return false; // hopefully a way to beat false positives
936
+ if (0 == maxRecursion) return true; // stop recursing?
937
+ if (-100 == maxRecursion) UIALogger.logWarning("Passed 100 recursions in UIAElement.equals");
938
+ return this.parent() === null || this.parent().equals(elem2.parent(), maxRecursion - 1); // check parent elem
939
+ },
940
+
941
+ /**
942
+ * General-purpose reduce function
943
+ *
944
+ * Applies the callback function to each node in the element tree starting from the current element.
945
+ *
946
+ * Callback function takes (previousValue, currentValue <UIAElement>, accessor_prefix, toplevel <UIAElement>)
947
+ * where previousValue is: initialValue (first time), otherwise the previous return from the callback
948
+ * currentValue is the UIAElement at the current location in the tree
949
+ * accessor_prefix is the code to access this element from the toplevel element
950
+ * toplevel is the top-level element on which this reduce function was called
951
+ *
952
+ * @param callback function
953
+ * @param initialValue (any type, dependent on callback)
954
+ * @param visibleOnly prunes the search tree to visible elements only
955
+ */
956
+ _reduce: function (callback, initialValue, visibleOnly) {
957
+ var t0 = getTime();
958
+ var currentTimeout = preferences.extensions.reduceTimeout;
959
+ var stopTime = t0 + currentTimeout;
960
+
961
+ var checkTimeout = function (currentOperation) {
962
+ if (stopTime < getTime()) {
963
+ UIALogger.logDebug("_reduce: " + currentOperation + " hit preferences.extensions.reduceTimeout limit"
964
+ + " of " + currentTimeout + " seconds; terminating with possibly incomplete result");
965
+ return true;
966
+ }
967
+ return false;
968
+ };
969
+
970
+ var reduce_helper = function (elem, acc, prefix) {
971
+ var scalars = ["frontMostApp", "mainWindow", "keyboard", "popover"];
972
+ var vectors = [];
973
+
974
+ // iOS 8.1 takes between 3 and 5 milliseconds each (????!?!?!) to evaluate these, so only do it for 7.x
975
+ // also force the fully-annotated output when we logging errors
976
+ if (isSimVersion(7) || config.automatorModality == "handleException") {
977
+ vectors = ["activityIndicators", "buttons", "cells", "collectionViews", "images","keys",
978
+ "links", "navigationBars", "pageIndicators", "pickers", "progressIndicators",
979
+ "scrollViews", "searchBars", "secureTextFields", "segmentedControls", "sliders",
980
+ "staticTexts", "switches", "tabBars", "tableViews", "textFields", "textViews",
981
+ "toolbars", "webViews", "windows"];
982
+ }
983
+
984
+ // function to visit an element, and add it to an array of what was discovered
985
+ var accessed = [];
986
+ var visit = function (someElem, accessor, onlyConsiderNew) {
987
+ // filter invalid
988
+ if (undefined === someElem) return;
989
+ if (!someElem.isNotNil()) return;
990
+
991
+ // filter already visited (in cases where we care)
992
+ if (onlyConsiderNew) {
993
+ for (var i = 0; i < accessed.length; ++i) {
994
+ if (accessed[i].equals(someElem, 0)) return;
995
+ }
996
+ }
997
+ accessed.push(someElem);
998
+
999
+ // filter based on visibility
1000
+ if (visibleOnly && !someElem.isVisible()) return;
1001
+ acc = reduce_helper(someElem, callback(acc, someElem, accessor, this), accessor);
1002
+ };
1003
+
1004
+ // try to access an element by name instead of number
1005
+ var getNamedIndex = function (someArray, numericIndex) {
1006
+ var e = someArray[numericIndex];
1007
+ var name = e.name();
1008
+ if (name !== null && e.equals(someArray.firstWithName(name), 0)) return '"' + name + '"';
1009
+ return numericIndex;
1010
+ }
1011
+
1012
+ // visit scalars
1013
+ for (var i = 0; i < scalars.length; ++i) {
1014
+ if (undefined === elem[scalars[i]]) continue;
1015
+ visit(elem[scalars[i]](), prefix + "." + scalars[i] + "()", false);
1016
+ }
1017
+
1018
+ // visit the elements of the vectors
1019
+ for (var i = 0; i < vectors.length; ++i) {
1020
+ if (undefined === elem[vectors[i]]) continue;
1021
+ var elemArray = elem[vectors[i]]();
1022
+ if (undefined === elemArray) continue;
1023
+ for (var j = 0; j < elemArray.length; ++j) {
1024
+ var newElem = elemArray[j];
1025
+ if (vectors[i] == "windows" && j == 0) continue;
1026
+ visit(newElem, prefix + "." + vectors[i] + "()[" + getNamedIndex(elemArray, j) + "]", false);
1027
+
1028
+ if (checkTimeout("vector loop")) return acc; // respect timeout preference
1029
+ }
1030
+ }
1031
+
1032
+ // visit any un-visited items
1033
+ var elemArray = elem.elements();
1034
+ for (var i = 0; i < elemArray.length; ++i) {
1035
+ visit(elemArray[i], prefix + ".elements()[" + getNamedIndex(elemArray, i) + "]", true);
1036
+
1037
+ if (checkTimeout("element loop")) return acc; // respect timeout preference
1038
+ }
1039
+ return acc;
1040
+ };
1041
+
1042
+ UIATarget.localTarget().pushTimeout(0);
1043
+ try {
1044
+ return reduce_helper(this, initialValue, "");
1045
+ } catch(e) {
1046
+ throw e;
1047
+ } finally {
1048
+ var totalTime = Math.round((getTime() - t0) * 10) / 10;
1049
+ UIALogger.logDebug("_reduce operation on " + this + " completed in " + totalTime + " seconds");
1050
+ UIATarget.localTarget().popTimeout();
1051
+ }
1052
+
1053
+ },
1054
+
1055
+ /**
1056
+ * Reduce function
1057
+ *
1058
+ * Applies the callback function to each node in the element tree starting from the current element.
1059
+ *
1060
+ * Callback function takes (previousValue, currentValue <UIAElement>, accessor_prefix, toplevel <UIAElement>)
1061
+ * where previousValue is: initialValue (first time), otherwise the previous return from the callback
1062
+ * currentValue is the UIAElement at the current location in the tree
1063
+ * accessor_prefix is the code to access this element from the toplevel element
1064
+ * toplevel is the top-level element on which this reduce function was called
1065
+ */
1066
+ reduce: function (callback, initialValue) {
1067
+ return this._reduce(callback, initialValue, false);
1068
+ },
1069
+
1070
+ /**
1071
+ * Reduce function
1072
+ *
1073
+ * Applies the callback function to each visible node in the element tree starting from the current element.
1074
+ *
1075
+ * Callback function takes (previousValue, currentValue <UIAElement>, accessor_prefix, toplevel <UIAElement>)
1076
+ * where previousValue is: initialValue (first time), otherwise the previous return from the callback
1077
+ * currentValue is the UIAElement at the current location in the tree
1078
+ * accessor_prefix is the code to access this element from the toplevel element
1079
+ * toplevel is the top-level element on which this reduce function was called
1080
+ */
1081
+ reduceVisible: function (callback, initialValue) {
1082
+ return this._reduce(callback, initialValue, true);
1083
+ },
1084
+
1085
+ /**
1086
+ * Find function
1087
+ *
1088
+ * Find elements by given criteria. Known criteria options are:
1089
+ * * UIAType: the class name of the UIAElement
1090
+ * * nameRegex: a regular expression that will be applied to the name() method
1091
+ * * rect, hasKeyboardFocus, isEnabled, isValid, isVisible, label, name, value:
1092
+ * these correspond to the values of the UIAelement methods of the same names.
1093
+ *
1094
+ * Return associative array {accessor: element} of results
1095
+ */
1096
+ find: function (criteria, varName) {
1097
+ if (criteria === undefined) {
1098
+ UIALogger.logWarning("No criteria passed to find function, so assuming {} and returning all elements");
1099
+ criteria = {};
1100
+ }
1101
+ varName = varName === undefined ? "<root element>" : varName;
1102
+ var visibleOnly = criteria.isVisible === true;
1103
+
1104
+ var knownOptions = {UIAType: 1, rect: 1, hasKeyboardFocus: 1, isEnabled: 1, isValid: 1,
1105
+ label: 1, name: 1, nameRegex: 1, value: 1, isVisible: 1};
1106
+
1107
+ // helpful check, mostly catching capitalization errors
1108
+ for (var k in criteria) {
1109
+ if (knownOptions[k] === undefined) {
1110
+ UIALogger.logWarning(this.toString() + ".find() received unknown criteria field '" + k + "' "
1111
+ + "(known fields are " + Object.keys(knownOptions).join(", ") + ")");
1112
+
1113
+ }
1114
+ }
1115
+
1116
+ var c = criteria;
1117
+ // don't consider isVisible here, because we do it in this._reduce
1118
+ var collect_fn = function (acc, elem, prefix, _) {
1119
+ if (c.UIAType !== undefined && "[object " + c.UIAType + "]" != elem.toString()) return acc;
1120
+ if (c.rect !== undefined && JSON.stringify(c.rect) != JSON.stringify(elem.rect())) return acc;
1121
+ if (c.hasKeyboardFocus !== undefined && c.hasKeyboardFocus != elem.hasKeyboardFocus()) return acc;
1122
+ if (c.isEnabled !== undefined && c.isEnabled != elem.isEnabled()) return acc;
1123
+ if (c.isValid !== undefined && c.isValid !== elem.isValid()) return acc;
1124
+ if (c.label !== undefined && c.label != elem.label()) return acc;
1125
+ if (c.name !== undefined && c.name != elem.name()) return acc;
1126
+ if (c.nameRegex !== undefined && (elem.name() === null || elem.name().match(c.nameRegex) === null)) return acc;
1127
+ if (c.value !== undefined && c.value != elem.value()) return acc;
1128
+
1129
+ acc[varName + prefix] = elem;
1130
+ elem._accessor = varName + prefix; // annotate the element with its accessor
1131
+ return acc;
1132
+ }
1133
+
1134
+ return this._reduce(collect_fn, {}, visibleOnly);
1135
+ },
1136
+
1137
+ /**
1138
+ * Take a screen shot of this element
1139
+ *
1140
+ * @param imageName A string to use as the name for the resultant image file
1141
+ */
1142
+ captureImage: function (imageName) {
1143
+ target().captureRectWithName(this.rect(), imageName);
1144
+ },
1145
+
1146
+ /**
1147
+ * Capture images for this element and all its child elements
1148
+ *
1149
+ * @param imageName A string to use as the base name for the resultant image files
1150
+ */
1151
+ captureImageTree: function (imageName) {
1152
+ var captureFn = function (acc, element, prefix, _) {
1153
+ element.captureImage(imageName + " element" + prefix);
1154
+ return acc;
1155
+ };
1156
+
1157
+ this._reduce(captureFn, undefined, true);
1158
+ },
1159
+
1160
+ /**
1161
+ * Get a list of valid element references in .js format for copy/paste use in code
1162
+ * @param varname is used as the first element in the canonical name
1163
+ * @param visibleOnly boolean whether to only get visible elements
1164
+ * @return array of strings
1165
+ */
1166
+ getChildElementReferences: function (varName, visibleOnly) {
1167
+ varName = varName === undefined ? "<root element>" : varName;
1168
+
1169
+ var collectFn = function (acc, _, prefix, __) {
1170
+ acc.push(varName + prefix)
1171
+ return acc;
1172
+ };
1173
+
1174
+ return this._reduce(collectFn, [], visibleOnly);
1175
+ },
1176
+
1177
+
1178
+ /**
1179
+ * Get the valid child element references in .js format as one string, delimited by newlines
1180
+ *
1181
+ * @param varname is used as the first element in the canonical name
1182
+ * @param visibleOnly boolean whether to only get visible elements
1183
+ */
1184
+ elementReferenceDump: function (varName, visibleOnly) {
1185
+ varName = varName === undefined ? "<root element>" : varName;
1186
+ var title = "elementReferenceDump";
1187
+ if (visibleOnly === true) {
1188
+ title += " (of visible elements)";
1189
+ switch (this.toString()) {
1190
+ case "[object UIATarget]":
1191
+ case "[object UIAApplication]":
1192
+ break;
1193
+ default:
1194
+ if (this.isVisible()) return title + ": <none, " + varName + " is not visible>";
1195
+ }
1196
+ }
1197
+ var ret = title + " of " + varName + ":\n" + varName + "\n";
1198
+ var refArray = this.getChildElementReferences(varName, visibleOnly);
1199
+ // shorten references if we can
1200
+ for (var i = 0; i < refArray.length; ++i) {
1201
+ ret += refArray[i].replace("target().frontMostApp().mainWindow()", "mainWindow()") + "\n";
1202
+ }
1203
+ return ret;
1204
+ },
1205
+
1206
+
1207
+ /**
1208
+ * Wait for a function on this element to return a value
1209
+ *
1210
+ * @param timeout the timeout in seconds
1211
+ * @param functionName the calling function, for logging purposes
1212
+ * @param propertyName the name of the property being checked (method name)
1213
+ * @param desiredValue the value that will trigger the return of this wait operation, or array of acceptable values
1214
+ * @param actualValueFunction optional function that overrides this[propertyName]()
1215
+ */
1216
+ _waitForPropertyOfElement: function (timeout, functionName, propertyName, desiredValue, actualValueFunction) {
1217
+ // actualValueFunction overrides default behavior: just grab the property name and call it
1218
+ if (undefined === actualValueFunction) {
1219
+ actualValueFunction = function (obj) {
1220
+ if (undefined === obj[propertyName]) throw new IlluminatorSetupException("Couldn't get property '" + propertyName + "' of object " + obj);
1221
+ return obj[propertyName]();
1222
+ }
1223
+ }
1224
+
1225
+ var desiredValues = desiredValue;
1226
+ if (!(desiredValue instanceof Array)) {
1227
+ desiredValues = [desiredValue];
1228
+ }
1229
+
1230
+ var thisObj = this;
1231
+ var wrapFn = function () {
1232
+ var actual = actualValueFunction(thisObj);
1233
+ for (var i = 0; i < desiredValues.length; ++i) {
1234
+ if (desiredValues[i] === actual) return;
1235
+ }
1236
+ var msg = ["Value of property '", propertyName, "'",
1237
+ " on ", thisObj, " \"", thisObj.name(), "\"",
1238
+ " is (" + (typeof actual) + ") '" + actual + "'"].join("");
1239
+ if (desiredValue instanceof Array) {
1240
+ msg += ", not one of the desired values ('" + desiredValues.join("', '") + "')";
1241
+ } else {
1242
+ msg += ", not the desired value (" + (typeof desiredValue) + ") '" + desiredValue + "'";
1243
+ }
1244
+ throw new IlluminatorRuntimeVerificationException(msg);
1245
+ };
1246
+
1247
+ waitForReturnValue(timeout, functionName, wrapFn);
1248
+ return this;
1249
+ },
1250
+
1251
+ /**
1252
+ * Wait for a function on this element to return a value
1253
+ *
1254
+ * @param timeout the timeout in seconds
1255
+ * @param functionName the calling function, for logging purposes
1256
+ * @param inputDescription string describing the input data, for logging purposes (i.e. what isDesiredValue is looking for)
1257
+ * @param returnName the name of the value being returned, for logging purposes
1258
+ * @param isDesiredValueFunction function that determines whether the returned value is acceptable
1259
+ * @param actualValueFunction function that retrieves value from element
1260
+ */
1261
+ _waitForReturnFromElement: function (timeout, functionName, inputDescription, returnName, isDesiredValueFunction, actualValueFunction) {
1262
+ var thisObj = this;
1263
+ var wrapFn = function () {
1264
+ var actual = actualValueFunction(thisObj);
1265
+ // TODO: possibly wrap this in try/catch and use it to detect criteria selectors that return multiples
1266
+ if (isDesiredValueFunction(actual)) return actual;
1267
+ throw new IlluminatorRuntimeFailureException("No acceptable value for " + returnName + " on "
1268
+ + thisObj + " \"" + thisObj.name()
1269
+ + "\" was returned from " + inputDescription);
1270
+ };
1271
+
1272
+ return waitForReturnValue(timeout, functionName, wrapFn);
1273
+ },
1274
+
1275
+ /**
1276
+ * Wait for a selector to produce an element (or lack thereof) in the desired existence state
1277
+ *
1278
+ * @param timeout the timeout in seconds
1279
+ * @param existenceState boolean of whether we want to find an element (vs find no element)
1280
+ * @param description the description of what element we are trying to find
1281
+ * @param selector the selector for the element whose existence will be checked
1282
+ */
1283
+ waitForChildExistence: function (timeout, existenceState, description, selector) {
1284
+ if (undefined === selector) throw new IlluminatorSetupException("waitForChildExistence: No selector was specified");
1285
+
1286
+ var actualValFn = function (thisObj) {
1287
+ // if we expect existence, try to get the element.
1288
+ if (existenceState) return thisObj.getChildElement(selector);
1289
+
1290
+ // else we need to check on the special case where criteria might fail by returning multiple elements
1291
+ if (!isHardSelector(selector)) {
1292
+ // criteria should return 0 elements -- we will check for 2 elements after
1293
+ return {"criteriaResult": thisObj.getChildElements(selector)};
1294
+ }
1295
+
1296
+ // functions should error or return a nil element
1297
+ try {
1298
+ return {"functionResult": thisObj.getChildElement(selector)};
1299
+ } catch (e) {
1300
+ return {"functionResult": newUIAElementNil()};
1301
+ }
1302
+ };
1303
+
1304
+ var isDesired = function (someObj) {
1305
+ // if desired an element, straightforward case
1306
+ if (existenceState) return isNotNilElement(someObj);
1307
+
1308
+ // else, make sure we got 0 elements
1309
+ if (!isHardSelector(selector)) {
1310
+ var result = someObj.criteriaResult;
1311
+ switch (Object.keys(result).length) {
1312
+ case 0: return true;
1313
+ case 1: return false;
1314
+ default:
1315
+ throw new IlluminatorSetupException("Selector (criteria) returned " + Object.keys(result).length + " results, not 0: "
1316
+ + JSON.stringify(result));
1317
+ return false;
1318
+ }
1319
+ }
1320
+
1321
+ // functions should return a nil element
1322
+ return !isNotNilElement(someObj.functionResult);
1323
+ };
1324
+
1325
+ var inputDescription;
1326
+ if (isHardSelector(selector)) {
1327
+ inputDescription = selector;
1328
+ } else {
1329
+ inputDescription = JSON.stringify(selector);
1330
+ }
1331
+
1332
+ try {
1333
+ UIATarget.localTarget().pushTimeout(0);
1334
+ extensionProfiler.bufferCriteriaCost();
1335
+ return this._waitForReturnFromElement(timeout, "waitForChildExistence", inputDescription, description, isDesired, actualValFn);
1336
+ } catch (e) {
1337
+ throw e;
1338
+ } finally {
1339
+ UIATarget.localTarget().popTimeout();
1340
+ extensionProfiler.UnbufferCriteriaCost();
1341
+ }
1342
+ },
1343
+
1344
+ /**
1345
+ * Wait until at least one selector in an associative array of selectors returns a valid lookup.
1346
+ *
1347
+ * Return an associative array of {key: <element found>, elem: <the element that was found>}
1348
+ *
1349
+ * @param timeout the timeout in seconds
1350
+ * @param selectors associative array of {label: selector}
1351
+ */
1352
+ waitForChildSelect: function (timeout, selectors) {
1353
+ if ((typeof selectors) != "object") throw new IlluminatorSetupException("waitForChildSelect expected selectors to be an object, "
1354
+ + "but got: " + (typeof selectors));
1355
+
1356
+ // composite find function
1357
+ var findAll = function (thisObj) {
1358
+ var ret = {};
1359
+ for (var selectorName in selectors) {
1360
+ var selector = selectors[selectorName];
1361
+ try {
1362
+ var el = thisObj.getChildElement(selector);
1363
+ if (isNotNilElement(el)) {
1364
+ ret[selectorName] = el;
1365
+ }
1366
+ }
1367
+ catch (e) {
1368
+ // ignore
1369
+ }
1370
+ }
1371
+ return ret;
1372
+ };
1373
+
1374
+ var foundAtLeastOne = function (resultObj) {
1375
+ return 0 < Object.keys(resultObj).length;
1376
+ };
1377
+
1378
+ var description = "any selector";
1379
+
1380
+ // build a somewhat readable list of the inputs
1381
+ var inputArr = [];
1382
+ for (var selectorName in selectors) {
1383
+ var selector = selectors[selectorName];
1384
+
1385
+ if (isHardSelector(selector)) {
1386
+ inputArr.push(selectorName + ": " + selector);
1387
+ } else {
1388
+ inputArr.push(selectorName + ": " + JSON.stringify(selector));
1389
+ }
1390
+ }
1391
+
1392
+ var inputDescription = "selectors {" + inputArr.join(", ") + "}";
1393
+
1394
+ try {
1395
+ UIATarget.localTarget().pushTimeout(0);
1396
+ extensionProfiler.bufferCriteriaCost();
1397
+ return this._waitForReturnFromElement(timeout, "waitForChildSelect", inputDescription, description, foundAtLeastOne, findAll);
1398
+ } catch (e) {
1399
+ throw e;
1400
+ } finally {
1401
+ UIATarget.localTarget().popTimeout();
1402
+ extensionProfiler.UnbufferCriteriaCost();
1403
+ }
1404
+
1405
+ },
1406
+
1407
+ /**
1408
+ * Wait for this element to become visible
1409
+ *
1410
+ * @param timeout the timeout in seconds
1411
+ * @param visibility boolean whether we want the item to be visible
1412
+ */
1413
+ waitForVisibility: function (timeout, visibility) {
1414
+ return this._waitForPropertyOfElement(timeout, "waitForVisibility", "isVisible", visibility ? 1 : 0);
1415
+ },
1416
+
1417
+ /**
1418
+ * Wait for this element to become valid
1419
+ *
1420
+ * @param timeout the timeout in seconds
1421
+ * @param validity boolean whether we want the item to be valid
1422
+ */
1423
+ waitForValidity: function (timeout, validity) {
1424
+ return this._waitForPropertyOfElement(timeout, "waitForValidity", "checkIsValid", validity ? 1 : 0);
1425
+ },
1426
+
1427
+ /**
1428
+ * Wait for this element to have the given name
1429
+ *
1430
+ * @param timeout the timeout in seconds
1431
+ * @param name string the name we are waiting for
1432
+ */
1433
+ waitForName: function (timeout, name) {
1434
+ return this._waitForPropertyOfElement(timeout, "waitForName", "name", name);
1435
+ },
1436
+
1437
+ /**
1438
+ * A shortcut for waiting an element to become visible and tap.
1439
+ * @param timeout the timeout in seconds
1440
+ */
1441
+ vtap: function (timeout) {
1442
+ timeout = timeout === undefined ? 5 : timeout;
1443
+ this.waitForVisibility(timeout, true);
1444
+ this.tap();
1445
+ },
1446
+
1447
+ /**
1448
+ * A shortcut for scrolling to a visible item and and tap.
1449
+ * @param timeout the timeout in seconds
1450
+ */
1451
+ svtap: function (timeout) {
1452
+ timeout = timeout === undefined ? 5 : timeout;
1453
+ if (this.isVisible()) {
1454
+ try {
1455
+ this.scrollToVisible();
1456
+ } catch (e) {
1457
+ // iOS 6 hack when no scrolling is needed
1458
+ if (e.toString() != "scrollToVisible cannot be used on the element because it does not have a scrollable ancestor.") {
1459
+ throw e;
1460
+ }
1461
+ }
1462
+ }
1463
+ this.vtap(timeout);
1464
+ },
1465
+
1466
+ /**
1467
+ * Check whether tapping this element produces its input method
1468
+ *
1469
+ * (this) - the element that we will tap
1470
+ * @param maxAttempts - Optional, how many times to check (soft-limited to minimum of 1)
1471
+ */
1472
+ checkIsEditable: function (maxAttempts) {
1473
+ // minimum of 1 attempt
1474
+ maxAttempts = (maxAttempts === undefined || maxAttempts < 1) ? 1 : maxAttempts;
1475
+
1476
+ if (this._inputMethod === undefined) return false;
1477
+ var inpMth = this._inputMethod;
1478
+
1479
+ // warn user if this is an object that might be destructively or oddly affected by this check
1480
+ switch (this.toString()) {
1481
+ case "[object UIAButton]":
1482
+ case "[object UIALink]":
1483
+ case "[object UIAActionSheet]":
1484
+ case "[object UIAKey]":
1485
+ case "[object UIAKeyboard]":
1486
+ UIALogger.logWarning("checkIsEditable is going to tap() an object of type " + this.toString());
1487
+ default:
1488
+ // no warning
1489
+ }
1490
+
1491
+ var elem;
1492
+ try {
1493
+ var didFirstTap = false;
1494
+ do {
1495
+ if (didFirstTap) {
1496
+ UIALogger.logDebug("checkIsEditable: retrying element tap, because "
1497
+ + inpMth.name + " = " + elem
1498
+ + " with " + maxAttempts.toString() + " remaining attempts");
1499
+ }
1500
+ maxAttempts--;
1501
+
1502
+ this.tap();
1503
+ didFirstTap = true;
1504
+ delay(0.35); // element should take roughly this long to appear (or disappear if was visible for other field).
1505
+ //bonus: delays the while loop
1506
+
1507
+ elem = target().getChildElement(inpMth.selector);
1508
+ } while (!elem.isNotNil() && 0 < maxAttempts);
1509
+
1510
+ if (!elem.isNotNil()) return false;
1511
+
1512
+ elem.waitForVisibility(1, true);
1513
+ return true;
1514
+ } catch (e) {
1515
+ UIALogger.logDebug("checkIsEditable caught error: " + e);
1516
+ return false;
1517
+ }
1518
+ },
1519
+
1520
+ /**
1521
+ * Treat this element as if it is an editable field
1522
+ *
1523
+ * This function is a workaround for some cases where an editable element (such as a text field) inside another element
1524
+ * (such as a table cell) fails to bring up the keyboard when tapped. The workaround is to tap the table cell instead.
1525
+ * This function adds editability support to elements that ordinarily would not have it.
1526
+ */
1527
+ useAsEditableField: function () {
1528
+ this._inputMethod = stockKeyboardInputMethod;
1529
+ this.setInputMethod = setInputMethod;
1530
+ this.customInputMethod = customInputMethod;
1531
+ this.typeString = typeString;
1532
+ return this;
1533
+ }
1534
+
1535
+ });
1536
+
1537
+
1538
+
1539
+ extendPrototype(UIAApplication, {
1540
+
1541
+ /**
1542
+ * Wait for this element to become visible
1543
+ *
1544
+ * @param timeout the timeout in seconds
1545
+ * @param orientation integer the screen orientation constant (e.g. UIA_DEVICE_ORIENTATION_PORTRAIT)
1546
+ */
1547
+ waitForInterfaceOrientation: function (timeout, orientation) {
1548
+ return this._waitForPropertyOfElement(timeout, "waitForInterfaceOrientation", "interfaceOrientation", orientation);
1549
+ },
1550
+
1551
+ /**
1552
+ * Wait for portrait orientation
1553
+ *
1554
+ * @param timeout the timeout in seconds
1555
+ */
1556
+ waitForPortraitOrientation: function (timeout) {
1557
+ return this._waitForPropertyOfElement(timeout, "waitForInterfaceOrientation", "interfaceOrientation",
1558
+ [UIA_DEVICE_ORIENTATION_PORTRAIT, UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN]);
1559
+ },
1560
+
1561
+ /**
1562
+ * Wait for landscape orientation
1563
+ *
1564
+ * @param timeout the timeout in seconds
1565
+ */
1566
+ waitForLandscapeOrientation: function (timeout) {
1567
+ return this._waitForPropertyOfElement(timeout, "waitForInterfaceOrientation", "interfaceOrientation",
1568
+ [UIA_DEVICE_ORIENTATION_LANDSCAPELEFT, UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT]);
1569
+ }
1570
+ });
1571
+
1572
+
1573
+ extendPrototype(UIAHost, {
1574
+
1575
+ /**
1576
+ * Execute a shell command as if it was a function
1577
+ *
1578
+ * If the command doesn't return succss, raise an error with the from the shell as if it was from a Javascript function
1579
+ *
1580
+ * @param command the command to run
1581
+ * @param args an array of arguments
1582
+ * @param timeout the timeout for the command
1583
+ */
1584
+ shellAsFunction: function (command, args, timeout) {
1585
+ var result = this.performTaskWithPathArgumentsTimeout(command, args, timeout);
1586
+
1587
+ // be verbose if something didn't go well
1588
+ if (0 != result.exitCode) {
1589
+ var owner = __function__(1); // not this function, but the calling function
1590
+ throw new Error(owner + " failed: " + result.stderr);
1591
+ }
1592
+ return result.stdout;
1593
+ },
1594
+
1595
+ /**
1596
+ * Attempt to parse JSON, raise helpful error if it fails, containing the calling function name
1597
+ *
1598
+ * @param maybeJSON string that should contain JSON
1599
+ * @return object
1600
+ */
1601
+ _guardedJSONParse: function (maybeJSON) {
1602
+ try {
1603
+ return JSON.parse(maybeJSON);
1604
+ } catch(e) {
1605
+ var owner = __function__(1);
1606
+ throw new Error(owner + " gave bad JSON: ```" + maybeJSON + "```");
1607
+ }
1608
+ },
1609
+
1610
+ /**
1611
+ * Read data from a file
1612
+ *
1613
+ * @param path the path that should be read
1614
+ * @return string the file contents
1615
+ */
1616
+ readFromFile: function (path) {
1617
+ return this.shellAsFunction("/bin/cat", [path], 10);
1618
+ },
1619
+
1620
+ /**
1621
+ * Read JSON data from a file
1622
+ *
1623
+ * @param path the path that should be read
1624
+ * @return object
1625
+ */
1626
+ readJSONFromFile: function (path) {
1627
+ return this._guardedJSONParse(this.readFromFile(path));
1628
+ },
1629
+
1630
+ /**
1631
+ * Read JSON data from a plist file
1632
+ *
1633
+ * @param path the path that should be read
1634
+ * @return object
1635
+ */
1636
+ readJSONFromPlistFile: function (path) {
1637
+ var scriptPath = IlluminatorScriptsDirectory + "/plist_to_json.sh";
1638
+ UIALogger.logDebug("Running " + scriptPath + " '" + path + "'");
1639
+
1640
+ return this._guardedJSONParse(this.shellAsFunction(scriptPath, [path], 10));
1641
+ },
1642
+
1643
+
1644
+ /**
1645
+ * Write data to a file
1646
+ *
1647
+ * @param path the path that should be (over)written
1648
+ * @data the data of the file to write in string format
1649
+ */
1650
+ writeToFile: function (path, data) {
1651
+ // type check
1652
+ switch (typeof data) {
1653
+ case "string": break;
1654
+ default: throw new TypeError("writeToFile expected data in string form, got type " + (typeof data));
1655
+ }
1656
+
1657
+ var chunkSize = Math.floor(262144 * 0.73) - (path.length + 100); // `getconf ARG_MAX`, adjusted for b64
1658
+
1659
+ var writeHelper = function (b64stuff, outputPath) {
1660
+ var result = target().host().performTaskWithPathArgumentsTimeout("/bin/sh", ["-c",
1661
+ "echo \"$0\" | base64 -D -o \"$1\"",
1662
+ b64stuff,
1663
+ outputPath], 5);
1664
+
1665
+ // be verbose if something didn't go well
1666
+ if (0 != result.exitCode) {
1667
+ UIALogger.logDebug("Exit code was nonzero: " + result.exitCode);
1668
+ UIALogger.logDebug("SDOUT: " + result.stdout);
1669
+ UIALogger.logDebug("STDERR: " + result.stderr);
1670
+ UIALogger.logDebug("I tried this command: ");
1671
+ UIALogger.logDebug("/bin/sh -c \"echo \\\"\\$0\\\" | base64 -D -o \\\"\\$1\\\" " + b64stuff + " " + outputPath);
1672
+ return false;
1673
+ }
1674
+ return true;
1675
+ }
1676
+
1677
+ var result = true;
1678
+ if (data.length < chunkSize) {
1679
+ var b64data = Base64.encode(data);
1680
+ UIALogger.logDebug("Writing " + data.length + " bytes to " + path + " as " + b64data.length + " bytes of b64");
1681
+ result = result && writeHelper(b64data, path);
1682
+
1683
+ } else {
1684
+ // split into chunks to avoid making the command line too long
1685
+ splitRegex = function(str, len) {
1686
+ var regex = new RegExp('[\\s\\S]{1,' + len + '}', 'g');
1687
+ return str.match(regex);
1688
+ }
1689
+
1690
+ // write each chunk to a file
1691
+ var chunks = splitRegex(data, chunkSize);
1692
+ var chunkFiles = [];
1693
+ for (var i = 0; i < chunks.length; ++i) {
1694
+ var chunk = chunks[i];
1695
+ var chunkFile = path + ".chunk" + i;
1696
+ var b64data = Base64.encode(chunk);
1697
+ UIALogger.logDebug("Writing " + chunk.length + " bytes to " + chunkFile + " as " + b64data.length + " bytes of b64");
1698
+ result = result && writeHelper(b64data, chunkFile);
1699
+ chunkFiles.push(chunkFile);
1700
+ }
1701
+
1702
+ // concatenate all the chunks
1703
+ var unchunkCmd = "cat \"" + chunkFiles.join("\" \"") + "\" > \"$0\"";
1704
+ UIALogger.logDebug("Concatenating and deleting " + chunkFiles.length + " chunks, writing " + path);
1705
+ target().host().performTaskWithPathArgumentsTimeout("/bin/sh", ["-c", unchunkCmd, path], 5);
1706
+ target().host().performTaskWithPathArgumentsTimeout("/bin/rm", chunkFiles, 5);
1707
+ }
1708
+
1709
+ return result;
1710
+ }
1711
+
1712
+ });
1713
+
1714
+
1715
+ extendPrototype(UIATarget, {
1716
+
1717
+ /**
1718
+ * Add a photo to the iPhoto library
1719
+ *
1720
+ * @param path the photo path
1721
+ */
1722
+ addPhoto: function (path) {
1723
+ host().shellAsFunction(config.xcodePath + "/usr/bin/simctl", ["addphoto", config.targetDeviceID, path], 15);
1724
+ },
1725
+
1726
+ /**
1727
+ * Connect or disconnect the hardware keyboard
1728
+ *
1729
+ * @param connected boolean whether the keyboard is connected
1730
+ */
1731
+ connectHardwareKeyboard: function (connected) {
1732
+ if (config.isHardware) {
1733
+ throw new IlluminatorSetupException("Can't set the hardware keyboard option for a non-simulated device");
1734
+ }
1735
+ var on = connected ? "1" : "0";
1736
+ var scriptPath = IlluminatorScriptsDirectory + "/set_hardware_keyboard.applescript";
1737
+
1738
+ host().shellAsFunction("/usr/bin/osascript", [scriptPath, on], 5);
1739
+ },
1740
+
1741
+ /**
1742
+ * Open a URL on the target device
1743
+ *
1744
+ * @param url string the URL
1745
+ */
1746
+ openURL: function (url) {
1747
+ host().shellAsFunction(config.xcodePath + "/usr/bin/simctl", ["openurl", config.targetDeviceID, url], 5);
1748
+ },
1749
+
1750
+ /**
1751
+ * Trigger icloud sync
1752
+ *
1753
+ */
1754
+ iCloudSync: function () {
1755
+ host().shellAsFunction(config.xcodePath + "/usr/bin/simctl", ["icloud_sync", config.targetDeviceID], 5);
1756
+ },
1757
+
1758
+ /**
1759
+ * Wait for this element to become visible
1760
+ *
1761
+ * @param timeout the timeout in seconds
1762
+ * @param orientation integer the screen orientation constant (e.g. UIA_DEVICE_ORIENTATION_PORTRAIT)
1763
+ */
1764
+ waitForDeviceOrientation: function (timeout, orientation) {
1765
+ return this._waitForPropertyOfElement(timeout, "waitForDeviceOrientation", "deviceOrientation", orientation);
1766
+ },
1767
+
1768
+ /**
1769
+ * Wait for portrait orientation
1770
+ *
1771
+ * @param timeout the timeout in seconds
1772
+ */
1773
+ waitForPortraitOrientation: function (timeout) {
1774
+ return this._waitForPropertyOfElement(timeout, "waitForDeviceOrientation", "deviceOrientation",
1775
+ [UIA_DEVICE_ORIENTATION_PORTRAIT, UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN]);
1776
+ },
1777
+
1778
+ /**
1779
+ * Wait for landscape orientation
1780
+ *
1781
+ * @param timeout the timeout in seconds
1782
+ */
1783
+ waitForLandscapeOrientation: function (timeout) {
1784
+ return this._waitForPropertyOfElement(timeout, "waitForDeviceOrientation", "deviceOrientation",
1785
+ [UIA_DEVICE_ORIENTATION_LANDSCAPELEFT, UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT]);
1786
+ }
1787
+
1788
+ });
1789
+
1790
+ extendPrototype(UIAKeyboard, {
1791
+ clear: function (inputField) {
1792
+
1793
+ var kb = this; // keyboard
1794
+ var db; // deleteButton
1795
+ var blindDelete = false;
1796
+ var preDeleteVal = "";
1797
+ var postDeleteVal = "";
1798
+
1799
+ // find many types of keyboard delete buttons, then just use the first one we get
1800
+ var getDeletionElement = function () {
1801
+ delButtons = kb.waitForChildSelect(5, {
1802
+ // TODO: handle other languages, possibly by programmatically generating this
1803
+ "key": function (keyboard) { return keyboard.keys()["Delete"]; },
1804
+ "button": function (keyboard) { return keyboard.buttons()["Delete"]; },
1805
+ "element": function (keyboard) { return keyboard.elements()["Delete"]; },
1806
+ });
1807
+ for (var k in delButtons) {
1808
+ return delButtons[k]; // first one is fine
1809
+ }
1810
+
1811
+ return newUIAElementNil();
1812
+ }
1813
+
1814
+ // wrapper for tapping the delete button.
1815
+ var tapDeleteButton = function (duration) {
1816
+ // initial condition
1817
+ if (db === undefined) {
1818
+ db = getDeletionElement();
1819
+ }
1820
+
1821
+ // tap the proper way given whether we want to tap for a duration
1822
+ if (duration) {
1823
+ db.tapWithOptions({duration: duration})
1824
+ } else {
1825
+ var t0 = getTime();
1826
+ db.tap();
1827
+ // this hack keeps deletion snappy when it crosses a word boundary (after which tapping can take 2 seconds)
1828
+ if (0.3 < getTime() - t0) {
1829
+ db = getDeletionElement();
1830
+ }
1831
+ }
1832
+ }
1833
+
1834
+ // calling "this.value()" on an element is a necessary hack to make long-press deletion work on iPad. Seriously.
1835
+ if (inputField.value) {
1836
+ preDeleteVal = inputField.value();
1837
+ }
1838
+
1839
+ // another necessary hack: without it, tapWithOptions / touchAndHold for blind delete doesn't work
1840
+ tapDeleteButton();
1841
+
1842
+ // find out if we affected the input field
1843
+ if (inputField.value) {
1844
+ postDeleteVal = inputField.value();
1845
+ }
1846
+
1847
+ // don't delete blindly if initial val was non-empty and deleting changed the value in the way we expected
1848
+ blindDelete = !(0 < preDeleteVal.length && (1 == preDeleteVal.length - postDeleteVal.length));
1849
+
1850
+ if (blindDelete) {
1851
+ tapDeleteButton(3.7);
1852
+ } else {
1853
+ for (var i = 0; i < postDeleteVal.length; ++i) {
1854
+ tapDeleteButton();
1855
+ }
1856
+ }
1857
+
1858
+ }
1859
+ });
1860
+
1861
+
1862
+ extendPrototype(UIATextField, {
1863
+ typeString: typeString,
1864
+ clear: function () {
1865
+ this.typeString("", true);
1866
+ },
1867
+ _inputMethod: stockKeyboardInputMethod,
1868
+ setInputMethod: setInputMethod,
1869
+ customInputMethod: customInputMethod
1870
+ });
1871
+
1872
+ extendPrototype(UIASecureTextField, {
1873
+ typeString: typeString,
1874
+ clear: function () {
1875
+ this.typeString("", true);
1876
+ },
1877
+ _inputMethod: stockKeyboardInputMethod,
1878
+ setInputMethod: setInputMethod,
1879
+ customInputMethod: customInputMethod
1880
+ });
1881
+
1882
+
1883
+ extendPrototype(UIATextView, {
1884
+ typeString: typeString,
1885
+ clear: function () {
1886
+ this.typeString("", true);
1887
+ },
1888
+ _inputMethod: stockKeyboardInputMethod,
1889
+ setInputMethod: setInputMethod,
1890
+ customInputMethod: customInputMethod
1891
+ });
1892
+
1893
+ extendPrototype(UIAStaticText, {
1894
+ _inputMethod: stockKeyboardInputMethod,
1895
+ setInputMethod: setInputMethod,
1896
+ customInputMethod: customInputMethod
1897
+ });
1898
+
1899
+
1900
+ extendPrototype(UIATableView, {
1901
+ /**
1902
+ * Fix a shortcoming in UIAutomation's ability to scroll to an item - general purpose edition
1903
+ *
1904
+ * @param thingDescription what we are looking for, used in messaging
1905
+ * @param getSomethingFn a function that takes the table as its only argument and returns the element we want (or UIAElementNil)
1906
+ * @return an element
1907
+ */
1908
+ _getSomethingByScrolling: function (thingDescription, getSomethingFn) {
1909
+ var delayToPreventUIAutomationBug = 0.4;
1910
+ var lastApparentSize = this.cells().length;
1911
+ var lastVisibleCell = -1;
1912
+ var thisVisibleCell = -1;
1913
+
1914
+ if (0 == lastApparentSize) return newUIAElementNil();
1915
+
1916
+ // scroll to first cell if we can't see it
1917
+ var initializeScroll = function (self) {
1918
+ self.cells()[0].scrollToVisible();
1919
+ lastVisibleCell = thisVisibleCell = 0;
1920
+ delay(delayToPreventUIAutomationBug);
1921
+ };
1922
+
1923
+ var downScroll = function (self) {
1924
+ UIALogger.logDebug("downScroll");
1925
+ try {
1926
+ self.scrollDown();
1927
+ delay(delayToPreventUIAutomationBug);
1928
+ } catch (e) {
1929
+ UIALogger.logDebug("_getSomethingByScrolling.downScroll caught/ignoring " + e);
1930
+ }
1931
+ };
1932
+
1933
+ // scroll down until we've made all known cells visible at least once
1934
+ var unproductiveScrolls = 0;
1935
+ var maxCells = 0;
1936
+ initializeScroll(this);
1937
+ maxCells = this.cells().length;
1938
+ while (lastVisibleCell < (maxCells - 1)) {
1939
+ // find this visible cell
1940
+ for (var i = lastVisibleCell; this.cells()[i].isVisible(); ++i) {
1941
+ thisVisibleCell = i;
1942
+ }
1943
+ var ret = getSomethingFn(this);
1944
+ if (isNotNilElement(ret)) {
1945
+ if (!ret.isVisible()) {
1946
+ ret.scrollToVisible();
1947
+ delay(delayToPreventUIAutomationBug);
1948
+ }
1949
+ UIALogger.logDebug("_getSomethingByScrolling SUCCESS with " + ret);
1950
+ return ret;
1951
+ }
1952
+
1953
+ UIALogger.logDebug("Cells " + lastVisibleCell
1954
+ + " to " + thisVisibleCell
1955
+ + " of " + maxCells
1956
+ + " didn't match " + thingDescription);
1957
+
1958
+ // check whether scrolling as productive
1959
+ if (lastVisibleCell < thisVisibleCell) {
1960
+ unproductiveScrolls = 0;
1961
+ } else {
1962
+ unproductiveScrolls++;
1963
+ }
1964
+
1965
+ if (5 < unproductiveScrolls) {
1966
+ UIALogger.logDebug("Scrolling does not appear to be revealing more cells, aborting.");
1967
+ return getSomethingFn(this);
1968
+ }
1969
+
1970
+ lastVisibleCell = thisVisibleCell;
1971
+ downScroll(this);
1972
+
1973
+ // work around another UIAutomation bug
1974
+ var newLength = this.cells().length;
1975
+ if (newLength < maxCells) {
1976
+ UIALogger.logDebug("UIAutomation now says that the table has " + newLength + " cells; presuming this to be bogus");
1977
+ } else {
1978
+ maxCells = newLength;
1979
+ }
1980
+
1981
+ }
1982
+
1983
+ return newUIAElementNil();
1984
+ },
1985
+
1986
+ /**
1987
+ * Fix a shortcoming in UIAutomation's ability to scroll to an item by predicate
1988
+ * @param cellPredicate string predicate as defined in UIAutomation spec
1989
+ * @return an element
1990
+ */
1991
+ getCellWithPredicateByScrolling: function (cellPredicate) {
1992
+ try {
1993
+ UIATarget.localTarget().pushTimeout(0);
1994
+ return this._getSomethingByScrolling("predicate: " + cellPredicate, function (thisTable) {
1995
+ return thisTable.cells().firstWithPredicate(cellPredicate);
1996
+ });
1997
+ } catch (e) {
1998
+ UIALogger.logDebug("getCellWithPredicateByScrolling caught/ignoring: " + e);
1999
+ } finally {
2000
+ UIATarget.localTarget().popTimeout();
2001
+ }
2002
+
2003
+ return newUIAElementNil();
2004
+ },
2005
+
2006
+ /**
2007
+ * Fix a shortcoming in UIAutomation's ability to scroll to an item by reference
2008
+ * @param elementDescription string description of what we are looking for
2009
+ * @param selector a selector relative to the table that will return the desired element
2010
+ * @return an element
2011
+ */
2012
+ getChildElementByScrolling: function (elementDescription, selector) {
2013
+ try {
2014
+ return this._getSomethingByScrolling("selector for " + elementDescription, function (thisTable) {
2015
+ return thisTable.getChildElement(selector);
2016
+ });
2017
+ } catch (e) {
2018
+ UIALogger.logDebug("getChildElementByScrolling caught/ignoring: " + e);
2019
+ }
2020
+
2021
+ return newUIAElementNil();
2022
+ }
2023
+
2024
+
2025
+ });