smart_monkey 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +77 -0
- data/Rakefile +57 -0
- data/Troubleshooting.md +61 -0
- data/VERSION +1 -0
- data/bin/smart_monkey +53 -0
- data/lib/bootstrap/css/bootstrap-responsive.css +1109 -0
- data/lib/bootstrap/css/bootstrap-responsive.min.css +9 -0
- data/lib/bootstrap/css/bootstrap.css +6167 -0
- data/lib/bootstrap/css/bootstrap.min.css +9 -0
- data/lib/bootstrap/img/glyphicons-halflings-white.png +0 -0
- data/lib/bootstrap/img/glyphicons-halflings.png +0 -0
- data/lib/bootstrap/js/bootstrap.js +2280 -0
- data/lib/bootstrap/js/bootstrap.min.js +6 -0
- data/lib/ios_device_log/deviceconsole +0 -0
- data/lib/smart_monkey.rb +2 -0
- data/lib/smart_monkey/command_helper.rb +71 -0
- data/lib/smart_monkey/monkey_runner.rb +549 -0
- data/lib/smart_monkey/templates/automation_result.xsl +61 -0
- data/lib/smart_monkey/templates/index.html.erb +77 -0
- data/lib/smart_monkey/templates/result.html.erb +110 -0
- data/lib/smart_monkey/templates/result_view.coffee +160 -0
- data/lib/smart_monkey/templates/result_view.js +250 -0
- data/lib/ui-auto-monkey/UIAutoMonkey.js +470 -0
- data/lib/ui-auto-monkey/custom.js +73 -0
- data/lib/ui-auto-monkey/handler/buttonHandler.js +111 -0
- data/lib/ui-auto-monkey/handler/wbScrollViewButtonHandler.js +114 -0
- data/lib/ui-auto-monkey/tuneup/LICENSE +20 -0
- data/lib/ui-auto-monkey/tuneup/assertions.js +402 -0
- data/lib/ui-auto-monkey/tuneup/image_asserter +26 -0
- data/lib/ui-auto-monkey/tuneup/image_assertion.js +65 -0
- data/lib/ui-auto-monkey/tuneup/image_assertion.rb +102 -0
- data/lib/ui-auto-monkey/tuneup/lang-ext.js +76 -0
- data/lib/ui-auto-monkey/tuneup/screen.js +11 -0
- data/lib/ui-auto-monkey/tuneup/test.js +71 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/abbreviated_console_output.rb +38 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/colored_console_output.rb +27 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/console_output.rb +17 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/preprocessor.rb +25 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/run +343 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/xunit_output.rb +114 -0
- data/lib/ui-auto-monkey/tuneup/tuneup.js +6 -0
- data/lib/ui-auto-monkey/tuneup/tuneup_js.podspec +52 -0
- data/lib/ui-auto-monkey/tuneup/uiautomation-ext.js +965 -0
- data/smart_monkey.gemspec +112 -0
- data/spec/spec_helper.rb +12 -0
- metadata +192 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
#import "UIAutoMonkey.js"
|
2
|
+
#import "handler/buttonHandler.js"
|
3
|
+
#import "handler/wbScrollViewButtonHandler.js"
|
4
|
+
#import "tuneup/tuneup.js"
|
5
|
+
|
6
|
+
// Configure the monkey: use the default configuration but a bit tweaked
|
7
|
+
monkey = new UIAutoMonkey();
|
8
|
+
monkey.config.numberOfEvents = 50; // turn off to make clear that we want minutes
|
9
|
+
monkey.config.delayBetweenEvents = 0.05;
|
10
|
+
monkey.config.eventWeights = {
|
11
|
+
tap: 100,
|
12
|
+
drag: 10,
|
13
|
+
flick: 10,
|
14
|
+
orientation: 1,
|
15
|
+
lock: 1,
|
16
|
+
pinchClose: 1,
|
17
|
+
pinchOpen: 1,
|
18
|
+
shake: 1
|
19
|
+
};
|
20
|
+
|
21
|
+
monkey.config.touchProbability = {
|
22
|
+
multipleTaps: 0.05,
|
23
|
+
multipleTouches: 0.05,
|
24
|
+
longPress: 0.05
|
25
|
+
};
|
26
|
+
|
27
|
+
monkey.config.frame = {
|
28
|
+
origin:
|
29
|
+
{
|
30
|
+
x: parseInt(UIATarget.localTarget().frontMostApp().rect().origin.x),
|
31
|
+
y: parseInt(UIATarget.localTarget().frontMostApp().rect().origin.y)+10
|
32
|
+
},
|
33
|
+
size: {
|
34
|
+
width: parseInt(UIATarget.localTarget().frontMostApp().rect().size.width),
|
35
|
+
height: parseInt(UIATarget.localTarget().frontMostApp().rect().size.height)-20
|
36
|
+
}
|
37
|
+
};// Ignore the UIAStatusBar area, avoid to drag out the notification page.
|
38
|
+
|
39
|
+
//UI Holes handlers
|
40
|
+
var handlers = [];
|
41
|
+
handlers.push(new ButtonHandler("WBBack", 10, true));
|
42
|
+
handlers.push(new WBScrollViewButtonHandler("weatherLeftBack", 5, false, 1));
|
43
|
+
handlers.push(new ButtonHandler("取消", 3, true));
|
44
|
+
handlers.push(new ButtonHandler("CloseX", 3, true));
|
45
|
+
handlers.push(new ButtonHandler("确定", 3, false));
|
46
|
+
|
47
|
+
monkey.config.conditionHandlers = handlers;
|
48
|
+
|
49
|
+
//ANR settings
|
50
|
+
var aFingerprintFunction = function() {
|
51
|
+
var mainWindow = UIATarget.localTarget().frontMostApp().mainWindow();
|
52
|
+
//if an error occurs log it and make it the fingerprint
|
53
|
+
try {
|
54
|
+
var aString = mainWindow.elementAccessorDump("tree", true);
|
55
|
+
// var aString = mainWindow.logElementTree();
|
56
|
+
// var aString = mainWindow.logElementJSON(["name"])
|
57
|
+
if (monkey.config.anrSettings.debug) {
|
58
|
+
UIALogger.logDebug("fingerprintFunction tree=" + aString);
|
59
|
+
}
|
60
|
+
}
|
61
|
+
catch (e) {
|
62
|
+
aString = "fingerprintFunction error:" + e;
|
63
|
+
UIALogger.logWarning(aString);
|
64
|
+
}
|
65
|
+
return aString;
|
66
|
+
};
|
67
|
+
monkey.config.anrSettings.fingerprintFunction = false;//false | aFingerprintFunction
|
68
|
+
monkey.config.anrSettings.eventsBeforeANRDeclared = 18; //throw exception if the fingerprint hasn't changed within this number of events
|
69
|
+
monkey.config.anrSettings.eventsBetweenSnapshots = 8; //how often (in events) to take a snapshot using the fingerprintFunction
|
70
|
+
monkey.config.anrSettings.debug = true; //log extra info on ANR state changes
|
71
|
+
|
72
|
+
// Release the monkey!
|
73
|
+
monkey.RELEASE_THE_MONKEY();
|
@@ -0,0 +1,111 @@
|
|
1
|
+
// Copyright (c) 2015 Yahoo inc. (http://www.yahoo-inc.com)
|
2
|
+
|
3
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
// of this software and associated documentation files (the "Software"), to deal
|
5
|
+
// in the Software without restriction, including without limitation the rights
|
6
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
// copies of the Software, and to permit persons to whom the Software is
|
8
|
+
// furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
// The above copyright notice and this permission notice shall be included in
|
11
|
+
// all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
// THE SOFTWARE.
|
20
|
+
|
21
|
+
"use strict";
|
22
|
+
//Conforms to the ConditionHandler protocol in UIAutoMonkey
|
23
|
+
//Usage
|
24
|
+
// var handlers = [ ];
|
25
|
+
// var handlerInterval = 20; //every how many events to process. Can vary by each handler, but often useful to group them
|
26
|
+
// handlers.push(new ButtonHandler("Done", handlerInterval, false)); //every 20 events, press "Done" button if found as a top level button (no nav bar).
|
27
|
+
// ...
|
28
|
+
// config.conditionHandlers = handlers
|
29
|
+
//
|
30
|
+
function ButtonHandler(buttonName, checkEveryNumber, useNavBar, optionalIsTrueFunction) {
|
31
|
+
this.buttonName = buttonName;
|
32
|
+
this.checkEveryNumber = checkEveryNumber || 10;
|
33
|
+
if (useNavBar == undefined) {
|
34
|
+
useNavBar = true;
|
35
|
+
};
|
36
|
+
this.useNavBar = useNavBar;
|
37
|
+
this.optionalIsTrueFunction = optionalIsTrueFunction || null;
|
38
|
+
//stats
|
39
|
+
this.statsIsTrueInvokedCount = 0;
|
40
|
+
this.statsIsTrueReturnedTrue = 0;
|
41
|
+
this.statsIsTrueReturnedFalse = 0;
|
42
|
+
this.statsHandleInvokedCount = 0;
|
43
|
+
this.statsHandleNotValidAndVisibleCount = 0;
|
44
|
+
this.statsHandleErrorCount = 0;
|
45
|
+
}
|
46
|
+
|
47
|
+
// return true if we our button is visible
|
48
|
+
ButtonHandler.prototype.isTrue = function(target, eventCount, mainWindow) {
|
49
|
+
this.statsIsTrueInvokedCount++;
|
50
|
+
var result;
|
51
|
+
if (this.optionalIsTrueFunction == null) {
|
52
|
+
var aButton = this.findButton(target);
|
53
|
+
// result = aButton.isNotNil() && aButton.validAndVisible();
|
54
|
+
result = aButton.isNotNil() && aButton.isValid() && aButton.isVisible();
|
55
|
+
} else {
|
56
|
+
result = this.optionalIsTrueFunction(target, eventCount, mainWindow);
|
57
|
+
}
|
58
|
+
if (result) {
|
59
|
+
this.statsIsTrueReturnedTrue++;
|
60
|
+
} else {
|
61
|
+
this.statsIsTrueReturnedFalse++;
|
62
|
+
};
|
63
|
+
return result;
|
64
|
+
};
|
65
|
+
|
66
|
+
ButtonHandler.prototype.findButton = function(target) {
|
67
|
+
return this.useNavBar ?
|
68
|
+
target.frontMostApp().mainWindow().navigationBar().buttons()[this.buttonName] :
|
69
|
+
target.frontMostApp().mainWindow().buttons()[this.buttonName];
|
70
|
+
};
|
71
|
+
|
72
|
+
//every checkEvery() number of events our isTrue() method will be queried.
|
73
|
+
ButtonHandler.prototype.checkEvery = function() {
|
74
|
+
return this.checkEveryNumber;
|
75
|
+
};
|
76
|
+
|
77
|
+
// if true then after we handle an event consider the particular Monkey event handled, and don't process the other condition handlers.
|
78
|
+
ButtonHandler.prototype.isExclusive = function() {
|
79
|
+
return true;
|
80
|
+
};
|
81
|
+
|
82
|
+
// Press our button
|
83
|
+
ButtonHandler.prototype.handle = function(target, mainWindow) {
|
84
|
+
this.statsHandleInvokedCount++;
|
85
|
+
var button = this.findButton(target);
|
86
|
+
if (button.isValid() && button.isVisible()) {
|
87
|
+
try{
|
88
|
+
button.tap();
|
89
|
+
} catch(err) {
|
90
|
+
this.statsHandleErrorCount++;
|
91
|
+
UIALogger.logWarning(err);
|
92
|
+
}
|
93
|
+
} else {
|
94
|
+
this.statsHandleNotValidAndVisibleCount++
|
95
|
+
//UIALogger.logWarning(this.toString() + " button is not validAndVisible");
|
96
|
+
};
|
97
|
+
};
|
98
|
+
|
99
|
+
ButtonHandler.prototype.toString = function() {
|
100
|
+
return ["MonkeyTest::ButtonHandler(" + this.buttonName, this.checkEveryNumber, this.useNavBar, ")"].join();
|
101
|
+
};
|
102
|
+
|
103
|
+
ButtonHandler.prototype.logStats = function() {
|
104
|
+
UIALogger.logDebug([this.toString(),
|
105
|
+
"IsTrueInvokedCount", this.statsIsTrueInvokedCount,
|
106
|
+
"IsTrueReturnedTrue", this.statsIsTrueReturnedTrue,
|
107
|
+
"IsTrueReturnedFalse", this.statsIsTrueReturnedFalse,
|
108
|
+
"HandleInvokedCount", this.statsHandleInvokedCount,
|
109
|
+
"HandleNotValidAndVisibleCount", this.statsHandleNotValidAndVisibleCount,
|
110
|
+
"HandleErrorCount", this.statsHandleErrorCount].join());
|
111
|
+
};
|
@@ -0,0 +1,114 @@
|
|
1
|
+
// Copyright (c) 2015 Yahoo inc. (http://www.yahoo-inc.com)
|
2
|
+
|
3
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
// of this software and associated documentation files (the "Software"), to deal
|
5
|
+
// in the Software without restriction, including without limitation the rights
|
6
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
// copies of the Software, and to permit persons to whom the Software is
|
8
|
+
// furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
// The above copyright notice and this permission notice shall be included in
|
11
|
+
// all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
// THE SOFTWARE.
|
20
|
+
|
21
|
+
"use strict";
|
22
|
+
//Conforms to the ConditionHandler protocol in UIAutoMonkey
|
23
|
+
//Usage
|
24
|
+
// var handlers = [ ];
|
25
|
+
// var handlerInterval = 20; //every how many events to process. Can vary by each handler, but often useful to group them
|
26
|
+
// handlers.push(new ButtonHandler("Done", handlerInterval, false)); //every 20 events, press "Done" button if found as a top level button (no nav bar).
|
27
|
+
// ...
|
28
|
+
// config.conditionHandlers = handlers
|
29
|
+
//
|
30
|
+
function WBScrollViewButtonHandler(buttonName, checkEveryNumber, useNavBar, scrollViewIndex, optionalIsTrueFunction) {
|
31
|
+
this.buttonName = buttonName;
|
32
|
+
this.scrollViewIndex = scrollViewIndex;
|
33
|
+
this.checkEveryNumber = checkEveryNumber || 10;
|
34
|
+
if (useNavBar == undefined) {
|
35
|
+
useNavBar = true;
|
36
|
+
};
|
37
|
+
this.useNavBar = useNavBar;
|
38
|
+
this.optionalIsTrueFunction = optionalIsTrueFunction || null;
|
39
|
+
//stats
|
40
|
+
this.statsIsTrueInvokedCount = 0;
|
41
|
+
this.statsIsTrueReturnedTrue = 0;
|
42
|
+
this.statsIsTrueReturnedFalse = 0;
|
43
|
+
this.statsHandleInvokedCount = 0;
|
44
|
+
this.statsHandleNotValidAndVisibleCount = 0;
|
45
|
+
this.statsHandleErrorCount = 0;
|
46
|
+
}
|
47
|
+
|
48
|
+
// return true if we our button is visible
|
49
|
+
WBScrollViewButtonHandler.prototype.isTrue = function(target, eventCount, mainWindow) {
|
50
|
+
this.statsIsTrueInvokedCount++;
|
51
|
+
var result;
|
52
|
+
if (this.optionalIsTrueFunction == null) {
|
53
|
+
var aButton = this.findButton(target);
|
54
|
+
// result = aButton.isNotNil() && aButton.validAndVisible();
|
55
|
+
result = aButton.isNotNil() && aButton.isValid();
|
56
|
+
} else {
|
57
|
+
result = this.optionalIsTrueFunction(target, eventCount, mainWindow);
|
58
|
+
}
|
59
|
+
if (result) {
|
60
|
+
this.statsIsTrueReturnedTrue++;
|
61
|
+
} else {
|
62
|
+
this.statsIsTrueReturnedFalse++;
|
63
|
+
};
|
64
|
+
return result;
|
65
|
+
};
|
66
|
+
|
67
|
+
WBScrollViewButtonHandler.prototype.findButton = function(target) {
|
68
|
+
return this.useNavBar ?
|
69
|
+
target.frontMostApp().mainWindow().navigationBar().buttons()[this.buttonName]:
|
70
|
+
target.frontMostApp().mainWindow().scrollViews()[this.scrollViewIndex].buttons()[this.buttonName];
|
71
|
+
};
|
72
|
+
|
73
|
+
//every checkEvery() number of events our isTrue() method will be queried.
|
74
|
+
WBScrollViewButtonHandler.prototype.checkEvery = function() {
|
75
|
+
return this.checkEveryNumber;
|
76
|
+
};
|
77
|
+
|
78
|
+
// if true then after we handle an event consider the particular Monkey event handled, and don't process the other condition handlers.
|
79
|
+
WBScrollViewButtonHandler.prototype.isExclusive = function() {
|
80
|
+
return true;
|
81
|
+
};
|
82
|
+
|
83
|
+
// Press our button
|
84
|
+
WBScrollViewButtonHandler.prototype.handle = function(target, mainWindow) {
|
85
|
+
this.statsHandleInvokedCount++;
|
86
|
+
var button = this.findButton(target);
|
87
|
+
if (button.isValid()) {
|
88
|
+
try{
|
89
|
+
var x = button.rect().origin.x;
|
90
|
+
var y = button.rect().origin.y;
|
91
|
+
target.tap({x:x, y:y});
|
92
|
+
} catch(err) {
|
93
|
+
this.statsHandleErrorCount++;
|
94
|
+
UIALogger.logWarning(err);
|
95
|
+
}
|
96
|
+
} else {
|
97
|
+
this.statsHandleNotValidAndVisibleCount++
|
98
|
+
//UIALogger.logWarning(this.toString() + " button is not validAndVisible");
|
99
|
+
};
|
100
|
+
};
|
101
|
+
|
102
|
+
WBScrollViewButtonHandler.prototype.toString = function() {
|
103
|
+
return ["MonkeyTest::WBScrollViewButtonHandler(" + this.buttonName, this.checkEveryNumber, this.useNavBar, this.scrollViewIndex, ")"].join();
|
104
|
+
};
|
105
|
+
|
106
|
+
WBScrollViewButtonHandler.prototype.logStats = function() {
|
107
|
+
UIALogger.logDebug([this.toString(),
|
108
|
+
"IsTrueInvokedCount", this.statsIsTrueInvokedCount,
|
109
|
+
"IsTrueReturnedTrue", this.statsIsTrueReturnedTrue,
|
110
|
+
"IsTrueReturnedFalse", this.statsIsTrueReturnedFalse,
|
111
|
+
"HandleInvokedCount", this.statsHandleInvokedCount,
|
112
|
+
"HandleNotValidAndVisibleCount", this.statsHandleNotValidAndVisibleCount,
|
113
|
+
"HandleErrorCount", this.statsHandleErrorCount].join());
|
114
|
+
};
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Alex Vollmer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,402 @@
|
|
1
|
+
/**
|
2
|
+
* The exception thrown when a 'fail' is used.
|
3
|
+
*
|
4
|
+
* @param message - reason the test failed/aborted
|
5
|
+
*/
|
6
|
+
function FailureException(message) {
|
7
|
+
this.name = 'FailureException';
|
8
|
+
this.message = message;
|
9
|
+
this.toString = function() {
|
10
|
+
return this.name + ': "' + this.message + '"';
|
11
|
+
};
|
12
|
+
}
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Just flat-out fail the test with the given message
|
16
|
+
*/
|
17
|
+
function fail(message) {
|
18
|
+
throw new FailureException(message);
|
19
|
+
}
|
20
|
+
|
21
|
+
/**
|
22
|
+
* Perform an assertion several times. If the assertion passes before the
|
23
|
+
* maximum number of iterations, the assertion passes. Otherwise the
|
24
|
+
* assertion fails
|
25
|
+
* @param f The function to perform (possibly) multiple times
|
26
|
+
* @param maxTries (optional) The maximum number of attempts
|
27
|
+
* @param delay (optional) The amount of time to pause between attempts
|
28
|
+
*/
|
29
|
+
function retry() {
|
30
|
+
var f = arguments[0];
|
31
|
+
var maxTries = 3;
|
32
|
+
var delay = 0.5;
|
33
|
+
if (arguments.length > 1) {
|
34
|
+
maxTries = arguments[1];
|
35
|
+
}
|
36
|
+
if (arguments.length > 2) {
|
37
|
+
delay = arguments[2];
|
38
|
+
}
|
39
|
+
|
40
|
+
var tries = 0;
|
41
|
+
var exception = null;
|
42
|
+
while (tries < maxTries) {
|
43
|
+
try {
|
44
|
+
f();
|
45
|
+
return; // if we get here, our function must have passed (no exceptions)
|
46
|
+
}
|
47
|
+
catch(e) {
|
48
|
+
exception = e;
|
49
|
+
tries++;
|
50
|
+
UIATarget.localTarget().delay(delay);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
throw exception;
|
54
|
+
}
|
55
|
+
|
56
|
+
/**
|
57
|
+
* The exception thrown for all assert* failures.
|
58
|
+
*
|
59
|
+
* @param message - reason the assertion failed
|
60
|
+
*/
|
61
|
+
function AssertionException(message) {
|
62
|
+
this.name = 'AssertionException';
|
63
|
+
this.message = message;
|
64
|
+
this.toString = function() {
|
65
|
+
return this.name + ': "' + this.message + '"';
|
66
|
+
};
|
67
|
+
}
|
68
|
+
|
69
|
+
/**
|
70
|
+
* Asserts that the given expression is true and throws an exception with
|
71
|
+
* a default message, or the optional +message+ parameter
|
72
|
+
*/
|
73
|
+
function assertTrue(expression, message) {
|
74
|
+
if (! expression) {
|
75
|
+
if (! message) {
|
76
|
+
message = "Assertion failed";
|
77
|
+
}
|
78
|
+
throw new AssertionException(message);
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
/**
|
83
|
+
* Asserts that the given regular expression matches the result of the
|
84
|
+
* given message.
|
85
|
+
* @param pattern - the pattern to match
|
86
|
+
* @param expression - the expression to match against
|
87
|
+
* @param message - an optional string message
|
88
|
+
*/
|
89
|
+
function assertMatch(regExp, expression, message) {
|
90
|
+
var defMessage = "'" + expression + "' does not match '" + regExp + "'";
|
91
|
+
assertTrue(regExp.test(expression), message ? message + ": " + defMessage : defMessage);
|
92
|
+
}
|
93
|
+
|
94
|
+
/**
|
95
|
+
* Assert that the +received+ object matches the +expected+ object (using
|
96
|
+
* plain ol' ==). If it doesn't, this method throws an exception with either
|
97
|
+
* a default message, or the one given as the last (optional) argument
|
98
|
+
*/
|
99
|
+
function assertEquals(expected, received, message) {
|
100
|
+
var defMessage = "Expected <" + expected + "> but received <" + received + ">";
|
101
|
+
assertTrue(expected == received, message ? message + ": " + defMessage : defMessage);
|
102
|
+
}
|
103
|
+
|
104
|
+
/**
|
105
|
+
* Assert that the +received+ object does not matches the +expected+ object (using
|
106
|
+
* plain ol' !=). If it doesn't, this method throws an exception with either
|
107
|
+
* a default message, or the one given as the last (optional) argument
|
108
|
+
*/
|
109
|
+
function assertNotEquals(expected, received, message) {
|
110
|
+
var defMessage = "Expected not <" + expected + "> but received <" + received + ">";
|
111
|
+
assertTrue(expected != received, message ? message + ": " + defMessage : defMessage);
|
112
|
+
}
|
113
|
+
|
114
|
+
/**
|
115
|
+
* Asserts that the given expression is false and otherwise throws an
|
116
|
+
* exception with a default message, or the optional +message+ parameter
|
117
|
+
*/
|
118
|
+
function assertFalse(expression, message) {
|
119
|
+
assertTrue(! expression, message);
|
120
|
+
}
|
121
|
+
|
122
|
+
/**
|
123
|
+
* Asserts that the given object is null or UIAElementNil (UIAutomation's
|
124
|
+
* version of a null stand-in). If the given object is not one of these,
|
125
|
+
* an exception is thrown with a default message or the given optional
|
126
|
+
* +message+ parameter.
|
127
|
+
*/
|
128
|
+
function assertNull(thingie, message) {
|
129
|
+
var defMessage = "Expected a null object, but received <" + thingie + ">";
|
130
|
+
// TODO: string-matching on UIAElementNil makes my tummy feel bad. Fix it.
|
131
|
+
assertTrue(thingie === null || thingie.toString() == "[object UIAElementNil]",
|
132
|
+
message ? message + ": " + defMessage : defMessage);
|
133
|
+
}
|
134
|
+
|
135
|
+
/**
|
136
|
+
* Asserts that the given object is not null or UIAElementNil (UIAutomation's
|
137
|
+
* version of a null stand-in). If it is null, an exception is thrown with
|
138
|
+
* a default message or the given optional +message+ parameter
|
139
|
+
*/
|
140
|
+
function assertNotNull(thingie, message) {
|
141
|
+
var defMessage = "Expected not null object";
|
142
|
+
assertTrue(thingie !== null && thingie.toString() != "[object UIAElementNil]",
|
143
|
+
message ? message + ": " + defMessage : defMessage);
|
144
|
+
}
|
145
|
+
|
146
|
+
function OnPassException(message) {
|
147
|
+
this.name = 'OnPassException';
|
148
|
+
this.message = message;
|
149
|
+
this.toString = function() {
|
150
|
+
return this.name + ': "' + this.message + '"';
|
151
|
+
};
|
152
|
+
}
|
153
|
+
|
154
|
+
function PropertyMismatchException(propName, expected, given) {
|
155
|
+
this.name = 'PropertyMismatchException';
|
156
|
+
this.message = propName + ": expected <" + expected + "> given <" + given + ">";
|
157
|
+
this.toString = function() {
|
158
|
+
return this.name + ": " + this.message;
|
159
|
+
}
|
160
|
+
}
|
161
|
+
|
162
|
+
/**
|
163
|
+
* Assert that the given definition matches the given element. The
|
164
|
+
* definition is a JavaScript object whose property hierarchy matches
|
165
|
+
* the given UIAElement. Property names in the given definition that match a
|
166
|
+
* method will cause that method to be invoked and the matching to be performed
|
167
|
+
* and the result. For example, the UITableView exposes all UITableViewCells through
|
168
|
+
* the cells() method. You only need to specify a 'cells' property to
|
169
|
+
* cause the method to be invoked.
|
170
|
+
*/
|
171
|
+
function assertElementTree(element, definition) {
|
172
|
+
var onPass = null;
|
173
|
+
if (definition.onPass) {
|
174
|
+
onPass = definition.onPass;
|
175
|
+
delete definition.onPass;
|
176
|
+
}
|
177
|
+
|
178
|
+
try {
|
179
|
+
assertPropertiesMatch(definition, element, 0);
|
180
|
+
}
|
181
|
+
catch(e) {
|
182
|
+
fail(e.toString())
|
183
|
+
}
|
184
|
+
|
185
|
+
if (onPass) {
|
186
|
+
try {
|
187
|
+
onPass(element);
|
188
|
+
}
|
189
|
+
catch(e) {
|
190
|
+
throw new OnPassException("Failed to execute 'onPass' callback: " + e);
|
191
|
+
}
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
/**
|
196
|
+
* Assert that the given window definition matches the current main window. The
|
197
|
+
* window definition is a JavaScript object whose property hierarchy matches
|
198
|
+
* the main UIAWindow. Property names in the given definition that match a
|
199
|
+
* method will cause that method to be invoked and the matching to be performed
|
200
|
+
* and the result. For example, the UIAWindow exposes all UITableViews through
|
201
|
+
* the tableViews() method. You only need to specify a 'tableViews' property to
|
202
|
+
* cause the method to be invoked.
|
203
|
+
*
|
204
|
+
* PROPERTY HIERARCHY Property definitions can be nested as deeply as
|
205
|
+
* necessary. Matching is done by traversing the same path in the main
|
206
|
+
* UIAWindow as your screen definition. For example, to make assertions about
|
207
|
+
* the left and right buttons in a UINavigationBar you can do this:
|
208
|
+
*
|
209
|
+
* assertWindow({
|
210
|
+
* navigationBar: {
|
211
|
+
* leftButton: { name: "Back" },
|
212
|
+
* rightButton: ( name: "Done" },
|
213
|
+
* }
|
214
|
+
* });
|
215
|
+
*
|
216
|
+
* PROPERTY MATCHERS For each property you wish to make an assertion about, you
|
217
|
+
* can specify a string, number regular expression or function. Strings and
|
218
|
+
* numbers are matches using the assertEquals() method. Regular expressions are
|
219
|
+
* matches using the assertMatch() method.
|
220
|
+
*
|
221
|
+
* If you specify 'null' for a property, it means you don't care to match.
|
222
|
+
* Typically this is done inside of arrays where you need to match the number
|
223
|
+
* of elements, but don't necessarily care to make assertions about each one.
|
224
|
+
*
|
225
|
+
* Functions are given the matching property as the single argument. For
|
226
|
+
* example:
|
227
|
+
*
|
228
|
+
* assertWindow({
|
229
|
+
* navigationBar: {
|
230
|
+
* leftButton: function(button) {
|
231
|
+
* // make custom assertions here
|
232
|
+
* }
|
233
|
+
* }
|
234
|
+
* });
|
235
|
+
*
|
236
|
+
* ARRAYS
|
237
|
+
* If a property you want to match is an array (e.g. tableViews()), you can
|
238
|
+
* specify one of the above matchers for each element of the array. If the
|
239
|
+
* number of provided matchers does not match the number of given elements, the
|
240
|
+
* assertion will fail (throw an exception)
|
241
|
+
*
|
242
|
+
* In any case, you specify another object definition for each property to
|
243
|
+
* drill-down into the atomic properties you wish to test. For example:
|
244
|
+
*
|
245
|
+
* assertWindow({
|
246
|
+
* navigationBar: {
|
247
|
+
* leftButton: { name: "Back" },
|
248
|
+
* rightButton: ( name: "Done" },
|
249
|
+
* },
|
250
|
+
* tableViews: [
|
251
|
+
* {
|
252
|
+
* groups: [
|
253
|
+
* { name: "First Group" },
|
254
|
+
* { name: "Second Group" }
|
255
|
+
* ],
|
256
|
+
* cells: [
|
257
|
+
* { name: "Cell 1" },
|
258
|
+
* { name: "Cell 2" },
|
259
|
+
* { name: "Cell 3" },
|
260
|
+
* { name: "Cell 4" }
|
261
|
+
* ]
|
262
|
+
* }
|
263
|
+
* ]
|
264
|
+
* });
|
265
|
+
*
|
266
|
+
* HANDLING FAILURE If any match fails, an appropriate exception will be
|
267
|
+
* thrown. If you are using the test structure provided by tuneup, this will be
|
268
|
+
* caught and detailed correctly in Instruments.
|
269
|
+
*
|
270
|
+
* POST-PROCESSING If your screen definition provides an 'onPass' property that
|
271
|
+
* points to a function, that function will be invoked after all matching has
|
272
|
+
* been peformed on the current window and all assertions have passed. This
|
273
|
+
* means you can assert the structure of your screen and operate on it in one
|
274
|
+
* pass:
|
275
|
+
*
|
276
|
+
* assertWindow({
|
277
|
+
* navigationBar: {
|
278
|
+
* leftButton: { name: "Back" }
|
279
|
+
* },
|
280
|
+
* onPass: function(window) {
|
281
|
+
* var leftButton = window.navigationBar().leftButton();
|
282
|
+
* leftButton.tap();
|
283
|
+
* }
|
284
|
+
* });
|
285
|
+
*/
|
286
|
+
function assertWindow(window) {
|
287
|
+
target = UIATarget.localTarget();
|
288
|
+
application = target.frontMostApp();
|
289
|
+
mainWindow = application.mainWindow();
|
290
|
+
|
291
|
+
assertElementTree(mainWindow, window);
|
292
|
+
}
|
293
|
+
|
294
|
+
/**
|
295
|
+
* Asserts that the +expected+ object matches the +given+ object by making
|
296
|
+
* assertions appropriate based on the type of each property in the
|
297
|
+
* +expected+ object. This method will recurse through the structure,
|
298
|
+
* applying assertions for each matching property path. See the description
|
299
|
+
* for +assertWindow+ for details on the matchers.
|
300
|
+
*/
|
301
|
+
function assertPropertiesMatch(expected, given, level) {
|
302
|
+
for (var propName in expected) {
|
303
|
+
if (expected.hasOwnProperty(propName)) {
|
304
|
+
var expectedProp = expected[propName];
|
305
|
+
|
306
|
+
if (propName.match(/~iphone$/)) {
|
307
|
+
if (UIATarget.localTarget().model().match(/^iPad/) !== null ||
|
308
|
+
UIATarget.localTarget().name().match(/^iPad Simulator$/) !== null) {
|
309
|
+
continue; // we're on the wrong platform, ignore
|
310
|
+
}
|
311
|
+
else {
|
312
|
+
propName = propName.match(/^(.*)~iphone/)[1];
|
313
|
+
}
|
314
|
+
}
|
315
|
+
else if (propName.match(/~ipad$/)) {
|
316
|
+
if (UIATarget.localTarget().model().match(/^iPad/) === null &&
|
317
|
+
UIATarget.localTarget().name().match(/^iPad Simulator/) === null) {
|
318
|
+
continue; // we're on the wrong platform, ignore
|
319
|
+
}
|
320
|
+
else {
|
321
|
+
propName = propName.match(/^(.*)~ipad/)[1];
|
322
|
+
}
|
323
|
+
}
|
324
|
+
|
325
|
+
var givenProp = given[propName];
|
326
|
+
|
327
|
+
if (typeof(givenProp) == "function") {
|
328
|
+
try {
|
329
|
+
// We have to use eval (shudder) because calling functions on
|
330
|
+
// UIAutomation objects with () operator crashes
|
331
|
+
// See Radar bug 8496138
|
332
|
+
givenProp = eval("given." + propName + "()");
|
333
|
+
}
|
334
|
+
catch (e) {
|
335
|
+
UIALogger.logError("[" + propName + "]: Unable to evaluate against " + given);
|
336
|
+
continue;
|
337
|
+
}
|
338
|
+
}
|
339
|
+
|
340
|
+
if (givenProp === null) {
|
341
|
+
throw new AssertionException("Could not find given " + given + " property named: " + propName);
|
342
|
+
}
|
343
|
+
else {
|
344
|
+
var objType = Object.prototype.toString.call(givenProp);
|
345
|
+
if (objType == "[object UIAElementNil]") {
|
346
|
+
throw new AssertionException("found no elements for " + given.toString() + '.' + propName + "()");
|
347
|
+
}
|
348
|
+
else if (objType == "[object Undefined]") {
|
349
|
+
throw new AssertionException(given.toString() + '.' + propName + "() method not found.");
|
350
|
+
}
|
351
|
+
}
|
352
|
+
|
353
|
+
// null indicates we don't care to match
|
354
|
+
if (expectedProp === null) {
|
355
|
+
continue;
|
356
|
+
}
|
357
|
+
|
358
|
+
var expectedPropType = typeof(expectedProp);
|
359
|
+
if (expectedPropType == "string") {
|
360
|
+
assertEquals(expectedProp, givenProp);
|
361
|
+
}
|
362
|
+
else if (expectedPropType == "number") {
|
363
|
+
assertEquals(expectedProp, givenProp);
|
364
|
+
}
|
365
|
+
else if (expectedPropType == "boolean") {
|
366
|
+
assertEquals(expectedProp, givenProp);
|
367
|
+
}
|
368
|
+
else if (expectedPropType == "function") {
|
369
|
+
if (expectedProp.constructor == RegExp) {
|
370
|
+
assertMatch(expectedProp, givenProp);
|
371
|
+
}
|
372
|
+
else {
|
373
|
+
expectedProp(givenProp);
|
374
|
+
}
|
375
|
+
}
|
376
|
+
else if (expectedPropType == "object") {
|
377
|
+
if (expectedProp.constructor === Array) {
|
378
|
+
assertEquals(expectedProp.length, givenProp.length, "Length of " + propName + " does not match");
|
379
|
+
for (var i = 0; i < expectedProp.length; i++) {
|
380
|
+
var exp = expectedProp[i];
|
381
|
+
var giv = givenProp[i];
|
382
|
+
assertPropertiesMatch(exp, giv, level + 1);
|
383
|
+
}
|
384
|
+
}
|
385
|
+
else if (expectedProp.constructor === RegExp) {
|
386
|
+
assertMatch(expectedProp, givenProp);
|
387
|
+
}
|
388
|
+
else if (typeof(givenProp) == "object") {
|
389
|
+
assertPropertiesMatch(expectedProp, givenProp, level + 1);
|
390
|
+
}
|
391
|
+
else {
|
392
|
+
var message = "[" + propName + "]: Unknown type of object constructor: " + expectedProp.constructor;
|
393
|
+
UIALogger.logError(message);
|
394
|
+
throw new AssertionException(message);
|
395
|
+
}
|
396
|
+
}
|
397
|
+
else {
|
398
|
+
UIALogger.logError("[" + propName + "]: unknown type for expectedProp: " + typeof(expectedProp));
|
399
|
+
}
|
400
|
+
}
|
401
|
+
}
|
402
|
+
}
|