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,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
+ });