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.
- checksums.yaml +7 -0
- data/gem/README.md +37 -0
- data/gem/bin/illuminatorTestRunner.rb +22 -0
- data/gem/lib/illuminator.rb +171 -0
- data/gem/lib/illuminator/argument-parsing.rb +299 -0
- data/gem/lib/illuminator/automation-builder.rb +39 -0
- data/gem/lib/illuminator/automation-runner.rb +589 -0
- data/gem/lib/illuminator/build-artifacts.rb +118 -0
- data/gem/lib/illuminator/device-installer.rb +45 -0
- data/gem/lib/illuminator/host-utils.rb +42 -0
- data/gem/lib/illuminator/instruments-runner.rb +301 -0
- data/gem/lib/illuminator/javascript-runner.rb +98 -0
- data/gem/lib/illuminator/listeners/console-logger.rb +32 -0
- data/gem/lib/illuminator/listeners/full-output.rb +13 -0
- data/gem/lib/illuminator/listeners/instruments-listener.rb +22 -0
- data/gem/lib/illuminator/listeners/intermittent-failure-detector.rb +49 -0
- data/gem/lib/illuminator/listeners/pretty-output.rb +26 -0
- data/gem/lib/illuminator/listeners/saltinel-agent.rb +66 -0
- data/gem/lib/illuminator/listeners/saltinel-listener.rb +26 -0
- data/gem/lib/illuminator/listeners/start-detector.rb +52 -0
- data/gem/lib/illuminator/listeners/stop-detector.rb +46 -0
- data/gem/lib/illuminator/listeners/test-listener.rb +58 -0
- data/gem/lib/illuminator/listeners/trace-error-detector.rb +38 -0
- data/gem/lib/illuminator/options.rb +96 -0
- data/gem/lib/illuminator/resources/IlluminatorGeneratedEnvironment.erb +13 -0
- data/gem/lib/illuminator/resources/IlluminatorGeneratedRunnerForInstruments.erb +19 -0
- data/gem/lib/illuminator/test-definitions.rb +23 -0
- data/gem/lib/illuminator/test-suite.rb +155 -0
- data/gem/lib/illuminator/version.rb +3 -0
- data/gem/lib/illuminator/xcode-builder.rb +144 -0
- data/gem/lib/illuminator/xcode-utils.rb +219 -0
- data/gem/resources/BuildConfiguration.xcconfig +10 -0
- data/gem/resources/js/AppMap.js +767 -0
- data/gem/resources/js/Automator.js +1132 -0
- data/gem/resources/js/Base64.js +142 -0
- data/gem/resources/js/Bridge.js +102 -0
- data/gem/resources/js/Config.js +92 -0
- data/gem/resources/js/Extensions.js +2025 -0
- data/gem/resources/js/Illuminator.js +228 -0
- data/gem/resources/js/Preferences.js +24 -0
- data/gem/resources/scripts/UIAutomationBridge.rb +248 -0
- data/gem/resources/scripts/common.applescript +25 -0
- data/gem/resources/scripts/diff_png.sh +61 -0
- data/gem/resources/scripts/kill_all_sim_processes.sh +17 -0
- data/gem/resources/scripts/plist_to_json.sh +40 -0
- data/gem/resources/scripts/set_hardware_keyboard.applescript +0 -0
- 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
|
+
});
|