New Angular Scenario runner and DSL system with redesigned HTML UI.

Uses the Jasmine syntax for tests, ex:

describe('widgets', function() {
  it('should verify that basic widgets work', function(){
    navigateTo('widgets.html');
    input('text.basic').enter('Carlos');
    expect(binding('text.basic')).toEqual('Carlos');
    input('text.basic').enter('Carlos Santana');
    expect(binding('text.basic')).not().toEqual('Carlos Boozer');
    input('text.password').enter('secret');
    expect(binding('text.password')).toEqual('secret');
    expect(binding('text.hidden')).toEqual('hiddenValue');
    expect(binding('gender')).toEqual('male');
    input('gender').select('female');
    expect(binding('gender')).toEqual('female');
  });
});

Note: To create new UI's implement the interface shown in angular.scenario.ui.Html.
This commit is contained in:
Elliott Sprehn
2010-10-08 16:43:40 -07:00
parent 0f104317df
commit 03df6cbddb
32 changed files with 2004 additions and 925 deletions

109
Rakefile
View File

@@ -1,5 +1,46 @@
include FileUtils
ANGULAR = [
'src/Angular.js',
'src/JSON.js',
'src/Compiler.js',
'src/Scope.js',
'src/Injector.js',
'src/Parser.js',
'src/Resource.js',
'src/Browser.js',
'src/jqLite.js',
'src/apis.js',
'src/filters.js',
'src/formatters.js',
'src/validators.js',
'src/services.js',
'src/directives.js',
'src/markups.js',
'src/widgets.js',
'src/AngularPublic.js',
]
ANGULAR_SCENARIO = [
'src/scenario/Scenario.js',
'src/scenario/Application.js',
'src/scenario/Describe.js',
'src/scenario/Future.js',
'src/scenario/HtmlUI.js',
'src/scenario/Describe.js',
'src/scenario/Runner.js',
'src/scenario/SpecRunner.js',
'src/scenario/dsl.js',
'src/scenario/matchers.js',
]
GENERATED_FILES = [
'angular-debug.js',
'angular-minified.js',
'angular-minified.map',
'angular-scenario.js',
]
task :default => [:compile, :test]
desc 'Generate Externs'
@@ -20,31 +61,27 @@ task :compile_externs do
out.close
end
desc 'Clean Generated Files'
task :clean do
GENERATED_FILES.each do |file|
`rm #{file}`
end
end
desc 'Compile Scenario'
task :compile_scenario do
concat = %x(cat \
lib/jquery/jquery-1.4.2.js \
src/scenario/angular.prefix \
src/Angular.js \
src/jqLite.js \
src/JSON.js \
src/Scope.js \
src/Injector.js \
src/Parser.js \
src/Resource.js \
src/Browser.js \
src/apis.js \
src/services.js \
src/AngularPublic.js \
src/scenario/DSL.js \
src/scenario/Future.js \
src/scenario/Matcher.js \
src/scenario/Runner.js \
src/scenario/angular.suffix \
)
deps = [
'lib/jquery/jquery-1.4.2.js',
'src/scenario/angular.prefix',
ANGULAR,
ANGULAR_SCENARIO,
'src/scenario/angular.suffix',
]
css = %x(cat css/angular-scenario.css)
concat = 'cat ' + deps.flatten.join(' ')
f = File.new("angular-scenario.js", 'w')
f.write(concat)
f.write(%x{#{concat}})
f.write('document.write(\'<style type="text/css">\n')
f.write(css.gsub(/'/, "\\'").gsub(/\n/, "\\n"));
f.write('\n</style>\');')
@@ -54,30 +91,14 @@ end
desc 'Compile JavaScript'
task :compile => [:compile_externs, :compile_scenario] do
concat = %x(cat \
src/angular.prefix \
src/Angular.js \
src/JSON.js \
src/Compiler.js \
src/Scope.js \
src/Injector.js \
src/Parser.js \
src/Resource.js \
src/Browser.js \
src/jqLite.js \
src/apis.js \
src/filters.js \
src/formatters.js \
src/validators.js \
src/services.js \
src/directives.js \
src/markups.js \
src/widgets.js \
src/AngularPublic.js \
src/angular.suffix \
)
deps = [
'src/angular.prefix',
ANGULAR,
'src/angular.suffix',
]
f = File.new("angular-debug.js", 'w')
f.write(concat)
concat = 'cat ' + deps.flatten.join(' ')
f.write(%x{#{concat}})
f.close
%x(java -jar lib/compiler-closure/compiler.jar \

View File

@@ -1,76 +1,199 @@
@charset "UTF-8";
/* CSS Document */
#runner {
position: absolute;
top:5px;
left:10px;
right:10px;
height: 200px;
/** Structure */
body {
font-family: Arial, sans-serif;
margin: 0;
font-size: 14px;
}
.console {
display: block;
overflow: scroll;
height: 200px;
border: 1px solid black;
}
#testView {
position: absolute;
bottom:10px;
top:230px;
left:10px;
right:10px;
}
#testView iframe {
#header {
position: fixed;
width: 100%;
height: 100%;
}
li.running > span {
background-color: yellow;
#specs {
padding-top: 50px;
}
#runner span {
background-color: green;
#header .angular {
font-family: Courier New, monospace;
font-weight: bold;
}
#runner .fail > span {
background-color: red;
#header h1 {
font-weight: normal;
float: left;
font-size: 30px;
line-height: 30px;
margin: 0;
padding: 10px 10px;
height: 30px;
}
.collapsed > ul {
display: none;
#frame h2,
#specs h2 {
margin: 0;
padding: 0.5em;
font-size: 1.1em;
}
#status-legend {
margin-top: 10px;
margin-right: 10px;
}
//////
#header,
#frame,
.test-info,
.test-actions li {
overflow: hidden;
}
.run, .info, .error {
display: block;
padding: 0 1em;
#frame {
margin: 10px;
}
#frame iframe {
width: 100%;
height: 758px;
}
#frame .popout {
float: right;
}
#frame iframe {
border: none;
}
.tests li,
.test-actions li,
.test-it li,
.test-it ol,
.status-display {
list-style-type: none;
}
.tests,
.test-it ol,
.status-display {
margin: 0;
padding: 0;
}
.test-info {
margin-left: 1em;
margin-top: 0.5em;
border-radius: 8px 0 0 8px;
-webkit-border-radius: 8px 0 0 8px;
-moz-border-radius: 8px 0 0 8px;
}
.test-it ol {
margin-left: 2.5em;
}
.status-display,
.status-display li {
float: right;
}
.status-display li {
padding: 5px 10px;
}
.timer-result,
.test-title {
display: inline-block;
margin: 0;
padding: 4px;
}
.timer-result {
width: 4em;
padding: 0 10px;
text-align: right;
font-family: monospace;
white-space: pre;
}
.run {
background-color: lightgrey;
padding: 0 .2em;
.test-it pre,
.test-actions pre {
clear: left;
margin-left: 6em;
}
.run.pass {
background-color: lightgreen;
.test-describe .test-describe {
margin: 5px 5px 10px 2em;
}
.run.fail {
background-color: lightred;
.test-actions .status-pending .test-title:before {
content: '» ';
}
.name, .time, .state {
padding-right: 2em;
/** Colors */
#header {
background-color: #F2C200;
}
error {
color: red;
}
#specs h2 {
border-top: 2px solid #BABAD1;
}
#specs h2,
#frame h2 {
background-color: #efefef;
}
#frame {
border: 1px solid #BABAD1;
}
.test-describe .test-describe {
border-left: 1px solid #BABAD1;
border-right: 1px solid #BABAD1;
border-bottom: 1px solid #BABAD1;
}
.status-display {
border: 1px solid #777;
}
.status-display .status-pending,
.status-pending .test-info {
background-color: #F9EEBC;
}
.status-display .status-success,
.status-success .test-info {
background-color: #B1D7A1;
}
.status-display .status-failure,
.status-failure .test-info {
background-color: #FF8286;
}
.status-display .status-error,
.status-error .test-info {
background-color: black;
color: white;
}
.test-actions .status-success .test-title {
color: #30B30A;
}
.test-actions .status-failure .test-title {
color: #DF0000;
}
.test-actions .status-error .test-title {
color: black;
}
.test-actions .timer-result {
color: #888;
}

View File

@@ -9,7 +9,7 @@ load:
- src/JSON.js
- src/*.js
- test/testabilityPatch.js
- src/scenario/Runner.js
- src/scenario/Scenario.js
- src/scenario/*.js
- test/angular-mocks.js
- test/scenario/*.js

View File

@@ -9,7 +9,7 @@ load:
- src/JSON.js
- src/*.js
- test/testabilityPatch.js
- src/scenario/Runner.js
- src/scenario/Scenario.js
- src/scenario/*.js
- test/angular-mocks.js
- test/scenario/*.js

View File

@@ -5,3 +5,7 @@ th {
tr {
border: 1px solid black;
}
.redbox {
background-color: red;
}

View File

@@ -1,25 +1,58 @@
describe('widgets', function(){
describe('widgets', function() {
it('should verify that basic widgets work', function(){
browser.navigateTo('widgets.html');
expect('{{text.basic}}').toEqual('');
input('text.basic').enter('John');
expect('{{text.basic}}').toEqual('John');
expect('{{text.password}}').toEqual('');
navigateTo('widgets.html');
input('text.basic').enter('Carlos');
expect(binding('text.basic')).toEqual('Carlos');
pause(2);
input('text.basic').enter('Carlos Santana');
pause(2);
expect(binding('text.basic')).not().toEqual('Carlos Boozer');
pause(2);
input('text.password').enter('secret');
expect('{{text.password}}').toEqual('secret');
expect('{{text.hidden}}').toEqual('hiddenValue');
expect('{{gender}}').toEqual('male');
expect(binding('text.password')).toEqual('secret');
expect(binding('text.hidden')).toEqual('hiddenValue');
expect(binding('gender')).toEqual('male');
pause(2);
input('gender').select('female');
input('gender').isChecked('female');
expect('{{gender}}').toEqual('female');
// expect('{{tea}}').toBeChecked();
// input('gender').select('female');
// expect('{{gender}}').toEqual('female');
expect(binding('gender')).toEqual('female');
pause(2);
});
describe('do it again', function() {
it('should verify that basic widgets work', function(){
navigateTo('widgets.html');
input('text.basic').enter('Carlos');
expect(binding('text.basic')).toEqual('Carlos');
pause(2);
input('text.basic').enter('Carlos Santana');
pause(2);
expect(binding('text.basic')).toEqual('Carlos Santana');
pause(2);
input('text.password').enter('secret');
expect(binding('text.password')).toEqual('secret');
expect(binding('text.hidden')).toEqual('hiddenValue');
expect(binding('gender')).toEqual('male');
pause(2);
input('gender').select('female');
expect(binding('gender')).toEqual('female');
pause(2);
});
});
it('should verify that basic widgets work', function(){
navigateTo('widgets.html');
input('text.basic').enter('Carlos');
expect(binding('text.basic')).toEqual('Carlos');
pause(2);
input('text.basic').enter('Carlos Santana');
pause(2);
expect(binding('text.basic')).toEqual('Carlos Santana');
pause(2);
input('text.password').enter('secret');
expect(binding('text.password')).toEqual('secret');
expect(binding('text.hidden')).toEqual('hiddenValue');
expect(binding('gender')).toEqual('male');
pause(2);
input('gender').select('female');
expect(binding('gender')).toEqual('female');
pause(2);
});
});

View File

@@ -270,11 +270,11 @@ function equals(o1, o2) {
} else {
keySet = {};
for(key in o1) {
if (key.charAt(0) !== '$' && !equals(o1[key], o2[key])) return false;
if (key.charAt(0) !== '$' && !isFunction(o1[key]) && !equals(o1[key], o2[key])) return false;
keySet[key] = true;
}
for(key in o2) {
if (key.charAt(0) !== '$' && keySet[key] !== true) return false;
if (!keySet[key] && key.charAt(0) !== '$' && !isFunction(o2[key])) return false;
}
return true;
}

View File

@@ -0,0 +1,51 @@
/**
* Represents the application currently being tested and abstracts usage
* of iframes or separate windows.
*/
angular.scenario.Application = function(context) {
this.context = context;
context.append('<h2>Current URL: <a href="about:blank">None</a></h2>');
};
/**
* Gets the jQuery collection of frames. Don't use this directly because
* frames may go stale.
*
* @return {Object} jQuery collection
*/
angular.scenario.Application.prototype.getFrame = function() {
return this.context.find('> iframe');
};
/**
* Gets the window of the test runner frame. Always favor executeAction()
* instead of this method since it prevents you from getting a stale window.
*
* @return {Object} the window of the frame
*/
angular.scenario.Application.prototype.getWindow = function() {
var contentWindow = this.getFrame().attr('contentWindow');
if (!contentWindow)
throw 'No window available because frame not loaded.';
return contentWindow;
};
/**
* Changes the location of the frame.
*/
angular.scenario.Application.prototype.navigateTo = function(url, onloadFn) {
this.getFrame().remove();
this.context.append('<iframe src=""></iframe>');
this.context.find('> h2 a').attr('href', url).text(url);
this.getFrame().attr('src', url).load(onloadFn);
};
/**
* Executes a function in the context of the tested application.
*
* @param {Function} The callback to execute. function($window, $document)
*/
angular.scenario.Application.prototype.executeAction = function(action) {
var $window = this.getWindow();
return action.call($window, _jQuery($window.document), $window);
};

View File

@@ -1,131 +1,134 @@
angular.scenario.dsl.browser = {
navigateTo: function(url){
var location = this.location;
return $scenario.addFuture('Navigate to: ' + url, function(done){
var self = this;
this.testFrame.load(function(){
self.testFrame.unbind();
self.testWindow = self.testFrame[0].contentWindow;
self.testDocument = self.jQuery(self.testWindow.document);
self.$browser = self.testWindow.angular.service.$browser();
self.notifyWhenNoOutstandingRequests =
bind(self.$browser, self.$browser.notifyWhenNoOutstandingRequests);
self.notifyWhenNoOutstandingRequests(done);
});
if (this.testFrame.attr('src') == url) {
this.testFrame[0].contentWindow.location.reload();
} else {
this.testFrame.attr('src', url);
location.setLocation(url);
}
});
},
location: {
href: "",
hash: "",
toEqual: function(url) {
return (this.hash === "" ? (url == this.href) :
(url == (this.href + "/#/" + this.hash)));
},
setLocation: function(url) {
var urlParts = url.split("/#/");
this.href = urlParts[0] || "";
this.hash = urlParts[1] || "";
}
}
};
/**
* Shared DSL statements that are useful to all scenarios.
*/
angular.scenario.dsl.input = function(selector) {
var namePrefix = "input '" + selector + "'";
return {
enter: function(value) {
return $scenario.addFuture(namePrefix + " enter '" + value + "'", function(done) {
var input = this.testDocument.find('input[name=' + selector + ']');
input.val(value);
this.testWindow.angular.element(input[0]).trigger('change');
done();
});
},
select: function(value) {
return $scenario.addFuture(namePrefix + " select '" + value + "'", function(done) {
var input = this.testDocument.
find(':radio[name$=@' + selector + '][value=' + value + ']');
jqLiteWrap(input[0]).trigger('click');
input[0].checked = true;
done();
});
}
/**
* Usage:
* pause(seconds) pauses the test for specified number of seconds
*/
angular.scenario.dsl('pause', function() {
return function(time) {
return this.addFuture('pause for ' + time + ' seconds', function(done) {
this.setTimeout(function() { done(null, time * 1000); }, time * 1000);
});
};
});
/**
* Usage:
* expect(future).{matcher} where matcher is one of the matchers defined
* with angular.scenario.matcher
*
* ex. expect(binding("name")).toEqual("Elliott")
*/
angular.scenario.dsl('expect', function() {
var chain = angular.extend({}, angular.scenario.matcher);
chain.not = function() {
this.inverse = true;
return chain;
};
};
angular.scenario.dsl.NG_BIND_PATTERN =/\{\{[^\}]+\}\}/;
angular.scenario.dsl.repeater = function(selector) {
var namePrefix = "repeater '" + selector + "'";
return {
count: function() {
return $scenario.addFuture(namePrefix + ' count', function(done) {
done(this.testDocument.find(selector).size());
});
},
collect: function(collectSelector) {
return $scenario.addFuture(
namePrefix + " collect '" + collectSelector + "'",
function(done) {
var self = this;
var doCollect = bind(this, function() {
var repeaterArray = [], ngBindPattern;
var startIndex = collectSelector.search(
angular.scenario.dsl.NG_BIND_PATTERN);
if (startIndex >= 0) {
ngBindPattern = collectSelector.substring(
startIndex + 2, collectSelector.length - 2);
collectSelector = '*';
}
this.testDocument.find(selector).each(function() {
var element = self.jQuery(this);
element.find(collectSelector).
each(function() {
var foundElem = self.jQuery(this);
if (foundElem.attr('ng:bind') == ngBindPattern) {
repeaterArray.push(foundElem.text());
}
});
});
return repeaterArray;
});
done(doCollect());
});
}
return function(future) {
this.future = future;
return chain;
};
};
});
angular.scenario.dsl.element = function(selector) {
var namePrefix = "Element '" + selector + "'";
var futureJquery = {};
for (key in (jQuery || _jQuery).fn) {
(function(){
var jqFnName = key;
var jqFn = (jQuery || _jQuery).fn[key];
futureJquery[key] = function() {
var jqArgs = arguments;
return $scenario.addFuture(namePrefix + "." + jqFnName + "()",
function(done) {
var self = this, repeaterArray = [], ngBindPattern;
var startIndex = selector.search(angular.scenario.dsl.NG_BIND_PATTERN);
if (startIndex >= 0) {
ngBindPattern = selector.substring(startIndex + 2, selector.length - 2);
var element = this.testDocument.find('*').filter(function() {
return self.jQuery(this).attr('ng:bind') == ngBindPattern;
/**
* Usage:
* navigateTo(future|string) where url a string or future with a value
* of a URL to navigate to
*/
angular.scenario.dsl('navigateTo', function() {
return function(url) {
var application = this.application;
var name = url;
if (url.name) {
name = ' value of ' + url.name;
}
return this.addFuture('navigate to ' + name, function(done) {
application.navigateTo(url.value || url, function() {
application.executeAction(function() {
if (this.angular) {
var $browser = this.angular.service.$browser();
$browser.poll();
$browser.notifyWhenNoOutstandingRequests(function() {
done(null, url.value || url);
});
done(jqFn.apply(element, jqArgs));
} else {
done(jqFn.apply(this.testDocument.find(selector), jqArgs));
done(null, url.value || url);
}
});
};
})();
}
return futureJquery;
};
});
});
};
});
/**
* Usage:
* input(name).enter(value) enters value in input with specified name
* input(name).check() checks checkbox
* input(name).select(value) selects the readio button with specified name/value
*/
angular.scenario.dsl('input', function() {
var chain = {};
chain.enter = function(value) {
var spec = this;
return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function(done) {
var input = _jQuery(this.document).find('input[name=' + spec.name + ']');
if (!input.length)
return done("Input named '" + spec.name + "' does not exist.");
input.val(value);
this.angular.element(input[0]).trigger('change');
done();
});
};
chain.check = function() {
var spec = this;
return this.addFutureAction("checkbox '" + this.name + "' toggle", function(done) {
var input = _jQuery(this.document).
find('input:checkbox[name=' + spec.name + ']');
if (!input.length)
return done("Input named '" + spec.name + "' does not exist.");
this.angular.element(input[0]).trigger('click');
input.attr('checked', !input.attr('checked'));
done();
});
};
chain.select = function(value) {
var spec = this;
return this.addFutureAction("radio button '" + this.name + "' toggle '" + value + "'", function(done) {
var input = _jQuery(this.document).
find('input:radio[name$="@' + spec.name + '"][value="' + value + '"]');
if (!input.length)
return done("Input named '" + spec.name + "' does not exist.");
this.angular.element(input[0]).trigger('click');
input.attr('checked', !input.attr('checked'));
done();
});
};
return function(name) {
this.name = name;
return chain;
};
});
/**
* Usage:
* binding(name) returns the value of a binding
*/
angular.scenario.dsl('binding', function() {
return function(name) {
return this.addFutureAction("select binding '" + name + "'", function(done) {
var element = _jQuery(this.document).find('[ng\\:bind="' + name + '"]');
if (!element.length)
return done("Binding named '" + name + "' does not exist.");
done(null, element.text());
});
};
});

108
src/scenario/Describe.js Normal file
View File

@@ -0,0 +1,108 @@
/**
* The representation of define blocks. Don't used directly, instead use
* define() in your tests.
*/
angular.scenario.Describe = function(descName, parent) {
this.beforeEachFns = [];
this.afterEachFns = [];
this.its = [];
this.children = [];
this.name = descName;
this.parent = parent;
this.id = angular.scenario.Describe.id++;
/**
* Calls all before functions.
*/
var beforeEachFns = this.beforeEachFns;
this.setupBefore = function() {
if (parent) parent.setupBefore.call(this);
angular.foreach(beforeEachFns, function(fn) { fn.call(this); }, this);
};
/**
* Calls all after functions.
*/
var afterEachFns = this.afterEachFns;
this.setupAfter = function() {
angular.foreach(afterEachFns, function(fn) { fn.call(this); }, this);
if (parent) parent.setupAfter.call(this);
};
};
// Shared Unique ID generator for every describe block
angular.scenario.Describe.id = 0;
/**
* Defines a block to execute before each it or nested describe.
*
* @param {Function} Body of the block.
*/
angular.scenario.Describe.prototype.beforeEach = function(body) {
this.beforeEachFns.push(body);
};
/**
* Defines a block to execute after each it or nested describe.
*
* @param {Function} Body of the block.
*/
angular.scenario.Describe.prototype.afterEach = function(body) {
this.afterEachFns.push(body);
};
/**
* Creates a new describe block that's a child of this one.
*
* @param {String} Name of the block. Appended to the parent block's name.
* @param {Function} Body of the block.
*/
angular.scenario.Describe.prototype.describe = function(name, body) {
var child = new angular.scenario.Describe(name, this);
this.children.push(child);
body.call(child);
};
/**
* Use to disable a describe block.
*/
angular.scenario.Describe.prototype.xdescribe = angular.noop;
/**
* Defines a test.
*
* @param {String} Name of the test.
* @param {Function} Body of the block.
*/
angular.scenario.Describe.prototype.it = function(name, body) {
var self = this;
this.its.push({
definition: this,
name: name,
fn: function() {
self.setupBefore.call(this);
body.call(this);
self.setupAfter.call(this);
}
});
};
/**
* Use to disable a test block.
*/
angular.scenario.Describe.prototype.xit = angular.noop;
/**
* Gets an array of functions representing all the tests (recursively).
* that can be executed with SpecRunner's.
*/
angular.scenario.Describe.prototype.getSpecs = function() {
var specs = arguments[0] || [];
angular.foreach(this.children, function(child) {
child.getSpecs(specs);
});
angular.foreach(this.its, function(it) {
specs.push(it);
});
return specs;
};

View File

@@ -1,13 +1,22 @@
function Future(name, behavior) {
/**
* A future action in a spec.
*/
angular.scenario.Future = function(name, behavior) {
this.name = name;
this.behavior = behavior;
this.fulfilled = false;
this.value = _undefined;
}
Future.prototype = {
fulfill: function(value) {
this.fulfilled = true;
this.value = value;
}
this.value = undefined;
};
/**
* Executes the behavior of the closure.
*
* @param {Function} Callback function(error, result)
*/
angular.scenario.Future.prototype.execute = function(doneFn) {
this.behavior(angular.bind(this, function(error, result) {
this.fulfilled = true;
this.value = error || result;
doneFn(error, result);
}));
};

204
src/scenario/HtmlUI.js Normal file
View File

@@ -0,0 +1,204 @@
/**
* User Interface for the Scenario Runner.
*
* @param {Object} The jQuery UI object for the UI.
*/
angular.scenario.ui.Html = function(context) {
this.context = context;
context.append(
'<div id="header">' +
' <h1><span class="angular">&lt;angular/&gt;</span>: Scenario Test Runner</h1>' +
' <ul id="status-legend" class="status-display">' +
' <li class="status-error">0 Errors</li>' +
' <li class="status-failure">0 Failures</li>' +
' <li class="status-success">0 Passed</li>' +
' </ul>' +
'</div>' +
'<div id="specs">' +
' <div class="test-children"></div>' +
'</div>'
);
};
/**
* Adds a new spec to the UI.
*
* @param {Object} The spec object created by the Describe object.
*/
angular.scenario.ui.Html.prototype.addSpec = function(spec) {
var specContext = this.findContext(spec.definition);
specContext.find('> .tests').append(
'<li class="status-pending test-it"></li>'
);
specContext = specContext.find('> .tests li:last');
return new angular.scenario.ui.Html.Spec(specContext, spec.name,
angular.bind(this, function(status) {
var status = this.context.find('#status-legend .status-' + status);
var parts = status.text().split(' ');
var value = (parts[0] * 1) + 1;
status.text(value + ' ' + parts[1]);
})
);
};
/**
* Finds the context of a spec block defined by the passed definition.
*
* @param {Object} The definition created by the Describe object.
*/
angular.scenario.ui.Html.prototype.findContext = function(definition) {
var path = [];
var currentContext = this.context.find('#specs');
var currentDefinition = definition;
while (currentDefinition && currentDefinition.name) {
path.unshift(currentDefinition);
currentDefinition = currentDefinition.parent;
}
angular.foreach(path, angular.bind(this, function(defn) {
var id = 'describe-' + defn.id;
if (!this.context.find('#' + id).length) {
currentContext.find('> .test-children').append(
'<div class="test-describe" id="' + id + '">' +
' <h2></h2>' +
' <div class="test-children"></div>' +
' <ul class="tests"></ul>' +
'</div>'
);
this.context.find('#' + id).find('> h2').text('describe: ' + defn.name);
}
currentContext = this.context.find('#' + id);
}));
return this.context.find('#describe-' + definition.id);
};
/**
* A spec block in the UI.
*
* @param {Object} The jQuery object for the context of the spec.
* @param {String} The name of the spec.
* @param {Function} Callback function(status) to call when complete.
*/
angular.scenario.ui.Html.Spec = function(context, name, doneFn) {
this.status = 'pending';
this.context = context;
this.startTime = new Date().getTime();
this.doneFn = doneFn;
context.append(
'<div class="test-info">' +
' <p class="test-title">' +
' <span class="timer-result"></span>' +
' <span class="test-name"></span>' +
' </p>' +
'</div>' +
'<ol class="test-actions">' +
'</ol>'
);
context.find('> .test-info .test-name').text('it ' + name);
};
/**
* Adds a new Step to this spec and returns it.
*
* @param {String} The name of the step.
*/
angular.scenario.ui.Html.Spec.prototype.addStep = function(name) {
this.context.find('> .test-actions').append('<li class="status-pending"></li>');
var stepContext = this.context.find('> .test-actions li:last');
var self = this;
return new angular.scenario.ui.Html.Step(stepContext, name, function(status) {
self.status = status;
});
};
/**
* Completes the spec and sets the timer value.
*/
angular.scenario.ui.Html.Spec.prototype.complete = function() {
this.context.removeClass('status-pending');
var endTime = new Date().getTime();
this.context.find("> .test-info .timer-result")
.text((endTime - this.startTime) + "ms");
};
/**
* Finishes the spec, possibly with an error.
*
* @param {Object} An optional error
*/
angular.scenario.ui.Html.Spec.prototype.finish = function(error) {
this.complete();
if (error) {
if (this.status !== 'failure') {
this.status = 'error';
}
this.context.append('<pre></pre>');
this.context.find('pre:first').text(error.stack || error.toString());
}
this.context.addClass('status-' + this.status);
this.doneFn(this.status);
};
/**
* Finishes the spec, but with a Fatal Error.
*
* @param {Object} Required error
*/
angular.scenario.ui.Html.Spec.prototype.error = function(error) {
this.finish(error);
};
/**
* A single step inside an it block (or a before/after function).
*
* @param {Object} The jQuery object for the context of the step.
* @param {String} The name of the step.
* @param {Function} Callback function(status) to call when complete.
*/
angular.scenario.ui.Html.Step = function(context, name, doneFn) {
this.context = context;
this.name = name;
this.startTime = new Date().getTime();
this.doneFn = doneFn;
context.append(
'<span class="timer-result"></span>' +
'<span class="test-title"></span>'
);
context.find('> .test-title').text(name);
};
/**
* Completes the step and sets the timer value.
*/
angular.scenario.ui.Html.Step.prototype.complete = function() {
this.context.removeClass('status-pending');
var endTime = new Date().getTime();
this.context.find(".timer-result")
.text((endTime - this.startTime) + "ms");
};
/**
* Finishes the step, possibly with an error.
*
* @param {Object} An optional error
*/
angular.scenario.ui.Html.Step.prototype.finish = function(error) {
this.complete();
if (error) {
this.context.addClass('status-failure');
this.doneFn('failure');
} else {
this.context.addClass('status-success');
this.doneFn('success');
}
};
/**
* Finishes the step, but with a Fatal Error.
*
* @param {Object} Required error
*/
angular.scenario.ui.Html.Step.prototype.error = function(error) {
this.complete();
this.context.addClass('status-error');
this.doneFn('error');
};

View File

@@ -1,21 +0,0 @@
function Matcher(scope, future, logger) {
var self = scope.$scenario = this;
this.logger = logger;
this.future = future;
}
Matcher.addMatcher = function(name, matcher) {
Matcher.prototype[name] = function(expected) {
var future = this.future;
$scenario.addFuture(
'expect ' + future.name + ' ' + name + ' ' + expected,
function(done){
if (!matcher(future.value, expected))
throw "Expected " + expected + ' but was ' + future.value;
done();
}
);
};
};
Matcher.addMatcher('toEqual', angular.equals);

View File

@@ -1,183 +1,95 @@
angular['scenario'] = angular['scenario'] || (angular['scenario'] = {});
angular.scenario['dsl'] = angular.scenario['dsl'] || (angular.scenario['dsl'] = {});
angular.scenario.Runner = function(scope, jQuery){
var self = scope.$scenario = this;
this.scope = scope;
this.jQuery = jQuery;
this.scope.$testrun = {done: false, results: []};
var specs = this.specs = {};
this.currentSpec = {name: '', futures: []};
var path = [];
this.scope.describe = function(name, body){
path.push(name);
body();
path.pop();
};
var beforeEach = noop;
var afterEach = noop;
this.scope.beforeEach = function(body) {
beforeEach = body;
};
this.scope.afterEach = function(body) {
afterEach = body;
};
this.scope.expect = function(future) {
return new Matcher(self, future, self.logger);
};
this.scope.it = function(name, body) {
var specName = path.join(' ') + ': it ' + name;
self.currentSpec = specs[specName] = {
name: specName,
futures: []
};
try {
beforeEach();
body();
} catch(err) {
self.addFuture(err.message || 'ERROR', function(){
throw err;
});
} finally {
afterEach();
}
self.currentSpec = _null;
};
this.logger = function returnNoop(){
return extend(returnNoop, {close:noop, fail:noop});
/**
* Runner for scenarios.
*/
angular.scenario.Runner = function($window) {
this.$window = $window;
this.rootDescribe = new angular.scenario.Describe();
this.currentDescribe = this.rootDescribe;
this.api = {
it: this.it,
xit: angular.noop,
describe: this.describe,
xdescribe: angular.noop,
beforeEach: this.beforeEach,
afterEach: this.afterEach
};
angular.foreach(this.api, angular.bind(this, function(fn, key) {
this.$window[key] = angular.bind(this, fn);
}));
};
angular.scenario.Runner.prototype = {
run: function(body){
var jQuery = this.jQuery;
body.append(
'<div id="runner">' +
'<div class="console"></div>' +
'</div>' +
'<div id="testView">' +
'<iframe></iframe>' +
'</div>');
var console = body.find('#runner .console');
console.find('li').live('click', function(){
jQuery(this).toggleClass('collapsed');
});
this.testFrame = body.find('#testView iframe');
function logger(parent) {
var container;
return function(type, text) {
if (!container) {
container = jQuery('<ul></ul>');
parent.append(container);
}
var element = jQuery('<li class="running '+type+'"><span></span></li>');
element.find('span').text(text);
container.append(element);
return extend(logger(element), {
close: function(){
element.removeClass('running');
if(!element.hasClass('fail'))
element.addClass('collapsed');
console.scrollTop(console[0].scrollHeight);
},
fail: function(){
element.removeClass('running');
var current = element;
while (current[0] != console[0]) {
if (current.is('li'))
current.addClass('fail');
current = current.parent();
}
}
});
};
/**
* Defines a describe block of a spec.
*
* @param {String} Name of the block
* @param {Function} Body of the block
*/
angular.scenario.Runner.prototype.describe = function(name, body) {
var self = this;
this.currentDescribe.describe(name, function() {
var parentDescribe = self.currentDescribe;
self.currentDescribe = this;
try {
body.call(this);
} finally {
self.currentDescribe = parentDescribe;
}
this.logger = logger(console);
var specNames = [];
foreach(this.specs, function(spec, name){
specNames.push(name);
}, this);
specNames.sort();
var self = this;
function callback(){
var next = specNames.shift();
if(next) {
self.execute(next, callback);
} else {
self.scope.$testrun.done = true;
});
};
/**
* Defines a test in a describe block of a spec.
*
* @param {String} Name of the block
* @param {Function} Body of the block
*/
angular.scenario.Runner.prototype.it = function(name, body) {
this.currentDescribe.it(name, body);
};
/**
* Defines a function to be called before each it block in the describe
* (and before all nested describes).
*
* @param {Function} Callback to execute
*/
angular.scenario.Runner.prototype.beforeEach = function(body) {
this.currentDescribe.beforeEach(body);
};
/**
* Defines a function to be called after each it block in the describe
* (and before all nested describes).
*
* @param {Function} Callback to execute
*/
angular.scenario.Runner.prototype.afterEach = function(body) {
this.currentDescribe.afterEach(body);
};
/**
* Defines a function to be called before each it block in the describe
* (and before all nested describes).
*
* @param {Function} Callback to execute
*/
angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClass, specsDone) {
var $root = angular.scope({}, angular.service);
var self = this;
var specs = this.rootDescribe.getSpecs();
$root.application = application;
$root.ui = ui;
$root.setTimeout = function() {
return self.$window.setTimeout.apply(self.$window, arguments);
};
asyncForEach(specs, angular.bind(this, function(spec, specDone) {
var runner = angular.scope($root);
runner.$become(specRunnerClass);
angular.foreach(angular.scenario.dsl, angular.bind(this, function(fn, key) {
this.$window[key] = function() {
return fn.call($root).apply(angular.scope(runner), arguments);
}
}
callback();
},
addFuture: function(name, behavior) {
var future = new Future(name, behavior);
this.currentSpec.futures.push(future);
return future;
},
execute: function(name, callback) {
var spec = this.specs[name],
self = this,
futuresFulfilled = [],
result = {
passed: false,
failed: false,
finished: false,
fail: function(error) {
result.passed = false;
result.failed = true;
result.error = error;
result.log('fail', isString(error) ? error : toJson(error)).fail();
}
},
specThis = createScope({
result: result,
jQuery: this.jQuery,
testFrame: this.testFrame,
testWindow: this.testWindow
}, angularService, {});
this.self = specThis;
var futureLogger = this.logger('spec', name);
spec.nextFutureIndex = 0;
function done() {
result.finished = true;
futureLogger.close();
self.self = _null;
(callback||noop).call(specThis);
}
function next(value){
if (spec.nextFutureIndex > 0) {
spec.futures[spec.nextFutureIndex - 1].fulfill(value);
}
var future = spec.futures[spec.nextFutureIndex];
(result.log || {close:noop}).close();
result.log = _null;
if (future) {
spec.nextFutureIndex ++;
result.log = futureLogger('future', future.name);
futuresFulfilled.push(future.name);
try {
future.behavior.call(specThis, next);
} catch (e) {
console.error(e);
result.fail(e);
self.scope.$testrun.results.push(
{name: name, passed: false, error: e, steps: futuresFulfilled});
done();
}
} else {
result.passed = !result.failed;
self.scope.$testrun.results.push({
name: name,
passed: !result.failed,
error: result.error,
steps: futuresFulfilled});
done();
}
}
next();
return specThis;
}
};
}));
runner.run(ui, spec, specDone);
}), specsDone || angular.noop);
};

103
src/scenario/Scenario.js Normal file
View File

@@ -0,0 +1,103 @@
/**
* Setup file for the Scenario.
* Must be first in the compilation/bootstrap list.
*/
// Public namespace
angular.scenario = {};
// Namespace for the UI
angular.scenario.ui = {};
/**
* Defines a new DSL statement. If your factory function returns a Future
* it's returned, otherwise the result is assumed to be a map of functions
* for chaining. Chained functions are subject to the same rules.
*
* Note: All functions on the chain are bound to the chain scope so values
* set on "this" in your statement function are available in the chained
* functions.
*
* @param {String} The name of the statement
* @param {Function} Factory function(application), return a function for
* the statement.
*/
angular.scenario.dsl = function(name, fn) {
angular.scenario.dsl[name] = function() {
function executeStatement(statement, args) {
var result = statement.apply(this, args);
if (angular.isFunction(result) || result instanceof angular.scenario.Future)
return result;
var self = this;
var chain = angular.extend({}, result);
angular.foreach(chain, function(value, name) {
if (angular.isFunction(value)) {
chain[name] = angular.bind(self, function() {
return executeStatement.call(self, value, arguments);
});
} else {
chain[name] = value;
}
});
return chain;
}
var statement = fn.apply(this, arguments);
return function() {
return executeStatement.call(this, statement, arguments);
};
};
};
/**
* Defines a new matcher for use with the expects() statement. The value
* this.actual (like in Jasmine) is available in your matcher to compare
* against. Your function should return a boolean. The future is automatically
* created for you.
*
* @param {String} The name of the matcher
* @param {Function} The matching function(expected).
*/
angular.scenario.matcher = function(name, fn) {
angular.scenario.matcher[name] = function(expected) {
var prefix = 'expect ' + this.future.name + ' ';
if (this.inverse) {
prefix += 'not ';
}
this.addFuture(prefix + name + ' ' + angular.toJson(expected),
angular.bind(this, function(done) {
this.actual = this.future.value;
if ((this.inverse && fn.call(this, expected)) ||
(!this.inverse && !fn.call(this, expected))) {
this.error = 'expected ' + angular.toJson(expected) +
' but was ' + angular.toJson(this.actual);
}
done(this.error);
})
);
};
};
/**
* Iterates through list with iterator function that must call the
* continueFunction to continute iterating.
*
* @param {Array} list to iterate over
* @param {Function} Callback function(value, continueFunction)
* @param {Function} Callback function(error, result) called when iteration
* finishes or an error occurs.
*/
function asyncForEach(list, iterator, done) {
var i = 0;
function loop(error) {
if (error || i >= list.length) {
done(error);
} else {
try {
iterator(list[i++], loop);
} catch (e) {
done(e);
}
}
}
loop();
}

View File

@@ -0,0 +1,78 @@
/**
* This class is the "this" of the it/beforeEach/afterEach method.
* Responsibilities:
* - "this" for it/beforeEach/afterEach
* - keep state for single it/beforeEach/afterEach execution
* - keep track of all of the futures to execute
* - run single spec (execute each future)
*/
angular.scenario.SpecRunner = function() {
this.futures = [];
};
/**
* Executes a spec which is an it block with associated before/after functions
* based on the describe nesting.
*
* @param {Object} An angular.scenario.UI implementation
* @param {Object} A spec object
* @param {Object} An angular.scenario.Application instance
* @param {Function} Callback function that is called when the spec finshes.
*/
angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
var specUI = ui.addSpec(spec);
try {
spec.fn.call(this);
} catch (e) {
specUI.error(e);
specDone();
return;
}
asyncForEach(
this.futures,
function(future, futureDone) {
var stepUI = specUI.addStep(future.name);
try {
future.execute(function(error) {
stepUI.finish(error);
futureDone(error);
});
} catch (e) {
stepUI.error(e);
rethrow(e);
}
},
function(e) {
specUI.finish(e);
specDone();
}
);
};
/**
* Adds a new future action.
*
* @param {String} Name of the future
* @param {Function} Behavior of the future
*/
angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior) {
var future = new angular.scenario.Future(name, angular.bind(this, behavior));
this.futures.push(future);
return future;
};
/**
* Adds a new future action to be executed on the application window.
*
* @param {String} Name of the future
* @param {Function} Behavior of the future
*/
angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior) {
return this.addFuture(name, function(done) {
this.application.executeAction(function() {
behavior.call(this, done);
});
});
};

View File

@@ -22,9 +22,3 @@
* THE SOFTWARE.
*/
(function(window, document, previousOnLoad){
window.angular = {
scenario: {
dsl: window
}
};

View File

@@ -1,11 +1,31 @@
var $scenario = new angular.scenario.Runner(window);
var $scenarioRunner = new angular.scenario.Runner(window, jQuery);
window.onload = function(){
window.onload = function() {
try {
if (previousOnLoad) previousOnLoad();
} catch(e) {}
$scenarioRunner.run(jQuery(window.document.body));
jQuery(document.body).append(
'<div id="runner"></div>' +
'<div id="frame"></div>'
);
var frame = jQuery('#frame');
var runner = jQuery('#runner');
var application = new angular.scenario.Application(frame);
var ui = new angular.scenario.ui.Html(runner);
$scenario.run(ui, application, function(error) {
frame.remove();
if (error) {
if (window.console) {
console.log(error);
if (error.stack) {
console.log(error.stack);
}
} else {
// Do something for IE
alert(error);
}
}
});
};
})(window, document, window.onload);

View File

@@ -1,4 +1,4 @@
(function(onLoadDelegate){
(function(previousOnLoad){
var prefix = (function(){
var filename = /(.*\/)bootstrap.js(#(.*))?/;
var scripts = document.getElementsByTagName("script");
@@ -10,6 +10,7 @@
}
}
})();
function addScript(path) {
document.write('<script type="text/javascript" src="' + prefix + path + '"></script>');
}
@@ -18,26 +19,51 @@
document.write('<link rel="stylesheet" type="text/css" href="' + prefix + path + '"/>');
}
window.angular = {
scenario: {
dsl: window
}
window.onload = function(){
try {
if (previousOnLoad) previousOnLoad();
} catch(e) {}
_jQuery(document.body).append(
'<div id="runner"></div>' +
'<div id="frame"></div>'
);
var frame = _jQuery('#frame');
var runner = _jQuery('#runner');
var application = new angular.scenario.Application(frame);
var ui = new angular.scenario.ui.Html(runner);
$scenario.run(ui, application, angular.scenario.SpecRunner, function(error) {
frame.remove();
if (error) {
if (window.console) {
console.log(error.stack || error);
} else {
// Do something for IE
alert(error);
}
}
});
};
window.onload = function(){
setTimeout(function(){
$scenarioRunner.run(jQuery(window.document.body));
}, 0);
(onLoadDelegate||function(){})();
};
addCSS("../../css/angular-scenario.css");
addScript("../../lib/jquery/jquery-1.4.2.js");
addScript("Runner.js");
addScript("../Angular.js");
addScript("../JSON.js");
addScript("DSL.js");
document.write('<script type="text/javascript">' +
'$scenarioRunner = new angular.scenario.Runner(window, jQuery);' +
'</script>');
})(window.onload);
addScript("../angular-bootstrap.js");
addScript("Scenario.js");
addScript("Application.js");
addScript("Describe.js");
addScript("Future.js");
addScript("HtmlUI.js");
addScript("Runner.js");
addScript("SpecRunner.js");
addScript("dsl.js");
addScript("matchers.js");
// Create the runner (which also sets up the global API)
document.write(
'<script type="text/javascript">' +
'var _jQuery = jQuery.noConflict(true);' +
'var $scenario = new angular.scenario.Runner(window);' +
'</script>'
);
})(window.onload);

39
src/scenario/matchers.js Normal file
View File

@@ -0,0 +1,39 @@
/**
* Matchers for implementing specs. Follows the Jasmine spec conventions.
*/
angular.scenario.matcher('toEqual', function(expected) {
return angular.equals(this.actual, expected);
});
angular.scenario.matcher('toBeDefined', function() {
return angular.isDefined(this.actual);
});
angular.scenario.matcher('toBeTruthy', function() {
return this.actual;
});
angular.scenario.matcher('toBeFalsy', function() {
return !this.actual;
});
angular.scenario.matcher('toMatch', function(expected) {
return new RegExp(expected).test(this.actual);
});
angular.scenario.matcher('toBeNull', function() {
return this.actual === null;
});
angular.scenario.matcher('toContain', function(expected) {
return includes(this.actual, expected);
});
angular.scenario.matcher('toBeLessThan', function(expected) {
return this.actual < expected;
});
angular.scenario.matcher('toBeGreaterThan', function(expected) {
return this.actual > expected;
});

View File

@@ -86,6 +86,10 @@ describe('equals', function(){
expect(equals({name:'misko'}, {name:'misko', $id:2})).toEqual(true);
expect(equals({name:'misko', $id:1}, {name:'misko'})).toEqual(true);
});
it('should ignore functions', function(){
expect(equals({func: function() {}}, {bar: function() {}})).toEqual(true);
});
});
describe('parseKeyValue', function() {

View File

@@ -0,0 +1,75 @@
describe('angular.scenario.Application', function() {
var app, frames;
beforeEach(function() {
frames = _jQuery("<div></div>");
app = new angular.scenario.Application(frames);
});
it('should return new $window and $document after navigate', function() {
var testWindow, testDocument, counter = 0;
app.navigateTo = noop;
app.getWindow = function() {
return {x:counter++, document:{x:counter++}};
};
app.navigateTo('http://www.google.com/');
app.executeAction(function($document, $window) {
testWindow = $window;
testDocument = $document;
});
app.navigateTo('http://www.google.com/');
app.executeAction(function($document, $window) {
expect($window).not.toEqual(testWindow);
expect($document).not.toEqual(testDocument);
});
});
it('should execute callback on $window of frame', function() {
var testWindow = {document: {}};
app.getWindow = function() {
return testWindow;
};
app.executeAction(function($document, $window) {
expect(this).toEqual($window);
expect(this).toEqual(testWindow);
});
});
it('should create a new iframe each time', function() {
app.navigateTo('about:blank');
var frame = app.getFrame();
frame.attr('test', true);
app.navigateTo('about:blank');
expect(app.getFrame().attr('test')).toBeFalsy();
});
it('should URL description bar', function() {
app.navigateTo('about:blank');
var anchor = frames.find('> h2 a');
expect(anchor.attr('href')).toEqual('about:blank');
expect(anchor.text()).toEqual('about:blank');
});
it('should call onload handler when frame loads', function() {
var called;
app.getFrame = function() {
// Mock a little jQuery
var result = {
remove: function() {
return result;
},
attr: function(key, value) {
return (!value) ? 'attribute value' : result;
},
load: function() {
called = true;
}
};
return result;
};
app.navigateTo('about:blank', function() {
called = true;
});
expect(called).toBeTruthy();
});
});

View File

@@ -1,181 +1,232 @@
describe("DSL", function() {
/**
* Very basic Mock of angular.
*/
function AngularMock() {
this.reset();
this.service = this;
}
var lastDocument, executeFuture, Expect;
AngularMock.prototype.reset = function() {
this.log = [];
};
AngularMock.prototype.element = function(node) {
this.log.push('element(' + node.nodeName.toLowerCase() + ')');
return this;
};
AngularMock.prototype.trigger = function(value) {
this.log.push('element().trigger(' + value + ')');
};
AngularMock.prototype.$browser = function() {
this.log.push('$brower()');
return this;
};
AngularMock.prototype.poll = function() {
this.log.push('$brower.poll()');
return this;
};
AngularMock.prototype.notifyWhenNoOutstandingRequests = function(fn) {
this.log.push('$brower.notifyWhenNoOutstandingRequests()');
fn();
};
describe("angular.scenario.dsl", function() {
var $window;
var $root;
var application;
beforeEach(function() {
setUpContext();
executeFuture = function(future, html, callback) {
lastDocument = _jQuery('<div>' + html + '</div>');
lastFrame = _jQuery('<iframe>' + lastDocument + '</iframe>');
_jQuery(document.body).append(lastDocument);
var specThis = {
testWindow: window,
testDocument: lastDocument,
testFrame: lastFrame,
jQuery: _jQuery
};
future.behavior.call(specThis, callback || noop);
$window = {
document: _jQuery("<div></div>"),
angular: new AngularMock()
};
Expect = _window.expect;
});
describe("input", function() {
var input = angular.scenario.dsl.input;
it('should enter', function() {
var future = input('name').enter('John');
expect(future.name).toEqual("input 'name' enter 'John'");
executeFuture(future, '<input type="text" name="name" />');
expect(lastDocument.find('input').val()).toEqual('John');
});
it('should select', function() {
var future = input('gender').select('female');
expect(future.name).toEqual("input 'gender' select 'female'");
executeFuture(future,
'<input type="radio" name="0@gender" value="male" checked/>' +
'<input type="radio" name="0@gender" value="female"/>');
expect(lastDocument.find(':radio:checked').length).toEqual(1);
expect(lastDocument.find(':radio:checked').val()).toEqual('female');
});
});
describe('browser', function() {
var browser = angular.scenario.dsl.browser;
it('shoud return true if location with empty hash provided is same ' +
'as location of the page', function() {
browser.location.href = "http://server";
expect(browser.location.toEqual("http://server")).toEqual(true);
});
it('shoud return true if location with hash provided is same ' +
'as location of the page', function() {
browser.location.href = "http://server";
browser.location.hash = "hashPath";
expect(browser.location.toEqual("http://server/#/hashPath")).toEqual(true);
});
it('should return true if the location provided is the same as which ' +
'browser navigated to', function() {
var future = browser.navigateTo("http://server/#/hashPath");
expect(future.name).toEqual("Navigate to: http://server/#/hashPath");
executeFuture(future, '<input type="text" name="name" />');
expect(browser.location.toEqual("http://server/#/hashPath")).toEqual(true);
expect(browser.location.toEqual("http://server/")).toEqual(false);
future = browser.navigateTo("http://server/");
expect(future.name).toEqual("Navigate to: http://server/");
executeFuture(future, '<input type="text" name="name" />');
expect(browser.location.toEqual("http://server/")).toEqual(true);
});
});
describe('repeater', function() {
var repeater = angular.scenario.dsl.repeater;
var html;
beforeEach(function() {
html = "<table>" +
"<tr class='epic'>" +
"<td class='hero-name'>" +
"<span ng:bind='hero'>John Marston</span>" +
"</td>" +
"<td class='game-name'>" +
"<span ng:bind='game'>Red Dead Redemption</span>" +
"</td>" +
"</tr>" +
"<tr class='epic'>" +
"<td class='hero-name'>" +
"<span ng:bind='hero'>Nathan Drake</span>" +
"</td>" +
"<td class='game-name'>" +
"<span ng:bind='game'>Uncharted</span>" +
"</td>" +
"</tr>" +
"</table>";
});
it('should count', function() {
var future = repeater('.repeater-row').count();
expect(future.name).toEqual("repeater '.repeater-row' count");
executeFuture(future,
"<div class='repeater-row'>a</div>" +
"<div class='repeater-row'>b</div>",
function(value) {
future.fulfill(value);
$root = angular.scope({}, angular.service);
$root.futures = [];
$root.addFuture = function(name, fn) {
this.futures.push(name);
fn.call(this, function(error, result) {
$root.futureError = error;
$root.futureResult = result;
});
expect(future.fulfilled).toBeTruthy();
expect(future.value).toEqual(2);
});
function assertFutureState(future, expectedName, expectedValue) {
expect(future.name).toEqual(expectedName);
executeFuture(future, html, function(value) {
future.fulfill(value);
});
expect(future.fulfilled).toBeTruthy();
expect(future.value).toEqual(expectedValue);
}
it('should collect bindings', function() {
assertFutureState(repeater('.epic').collect('{{hero}}'),
"repeater '.epic' collect '{{hero}}'",
['John Marston', 'Nathan Drake']);
assertFutureState(repeater('.epic').collect('{{game}}'),
"repeater '.epic' collect '{{game}}'",
['Red Dead Redemption', 'Uncharted']);
});
it('should collect normal selectors', function() {
assertFutureState(repeater('.epic').collect('.hero-name'),
"repeater '.epic' collect '.hero-name'",
['John Marston', 'Nathan Drake']);
assertFutureState(repeater('.epic').collect('.game-name'),
"repeater '.epic' collect '.game-name'",
['Red Dead Redemption', 'Uncharted']);
});
it('should collect normal attributes', function() {
//TODO(shyamseshadri) : Left as an exercise to the user
});
};
$root.application = new angular.scenario.Application($window.document);
$root.application.getWindow = function() {
return $window;
};
$root.application.navigateTo = function(url, callback) {
$window.location = url;
callback();
};
// Just use the real one since it delegates to this.addFuture
$root.addFutureAction = angular.scenario.
SpecRunner.prototype.addFutureAction;
});
describe('element', function() {
var element = angular.scenario.dsl.element;
var html;
describe('Pause', function() {
beforeEach(function() {
html = '<div class="container">' +
'<div class="reports-detail">' +
'<span class="desc">Description : ' +
'<span ng:bind="report.description">Details...</span>' +
'</span>' +
'<span>Date created: ' +
'<span ng:bind="report.creationDate">01/01/01</span>' +
'</span>' +
'</div>' +
'</div>';
$root.setTimeout = function(fn, value) {
$root.timerValue = value;
fn();
};
});
function timeTravel(future) {
executeFuture(future, html, function(value) { future.fulfill(value); });
expect(future.fulfilled).toBeTruthy();
}
it('should find elements on the page and provide jquery api', function() {
var future = element('.reports-detail').text();
expect(future.name).toEqual("Element '.reports-detail'.text()");
timeTravel(future);
expect(future.value).
toEqual('Description : Details...Date created: 01/01/01');
// expect(future.value.find('.desc').text()).
// toEqual('Description : Details...');
});
it('should find elements with angular syntax', function() {
var future = element('{{report.description}}').text();
expect(future.name).toEqual("Element '{{report.description}}'.text()");
timeTravel(future);
expect(future.value).toEqual('Details...');
// expect(future.value.attr('ng:bind')).toEqual('report.description');
});
it('should be able to click elements', function(){
var future = element('.link-class').click();
expect(future.name).toEqual("Element '.link-class'.click()");
executeFuture(future, html, function(value) { future.fulfill(value); });
expect(future.fulfilled).toBeTruthy();
// TODO(rajat): look for some side effect from click happening?
it('should pause for specified seconds', function() {
angular.scenario.dsl.pause.call($root).call($root, 10);
expect($root.timerValue).toEqual(10000);
expect($root.futureResult).toEqual(10000);
});
});
describe('Expect', function() {
it('should chain and execute matcher', function() {
var future = {value: 10};
var result = angular.scenario.dsl.expect.call($root).call($root, future);
result.toEqual(10);
expect($root.futureError).toBeUndefined();
expect($root.futureResult).toBeUndefined();
var result = angular.scenario.dsl.expect.call($root).call($root, future);
result.toEqual(20);
expect($root.futureError).toBeDefined();
});
});
describe('NavigateTo', function() {
it('should allow a string url', function() {
angular.scenario.dsl.navigateTo.call($root).call($root, 'http://myurl');
expect($window.location).toEqual('http://myurl');
expect($root.futureResult).toEqual('http://myurl');
});
it('should allow a future url', function() {
var future = {name: 'future name', value: 'http://myurl'};
angular.scenario.dsl.navigateTo.call($root).call($root, future);
expect($window.location).toEqual('http://myurl');
expect($root.futureResult).toEqual('http://myurl');
});
it('should complete if angular is missing from app frame', function() {
delete $window.angular;
angular.scenario.dsl.navigateTo.call($root).call($root, 'http://myurl');
expect($window.location).toEqual('http://myurl');
expect($root.futureResult).toEqual('http://myurl');
});
it('should wait for angular notify when no requests pending', function() {
angular.scenario.dsl.navigateTo.call($root).call($root, 'url');
expect($window.angular.log).toContain('$brower.poll()');
expect($window.angular.log)
.toContain('$brower.notifyWhenNoOutstandingRequests()');
});
});
describe('Element Finding', function() {
var doc;
//TODO(esprehn): Work around a bug in jQuery where attribute selectors
// only work if they are executed on a real document, not an element.
//
// ex. jQuery('#foo').find('[name="bar"]') // fails
// ex. jQuery('#foo [name="bar"]') // works, wtf?
//
beforeEach(function() {
doc = _jQuery('<div id="angular-scenario-binding"></div>');
_jQuery(document.body).append(doc);
$window.document = window.document;
});
afterEach(function() {
_jQuery(document.body)
.find('#angular-scenario-binding')
.remove();
});
describe('Binding', function() {
it('should select binding by name', function() {
doc.append('<span ng:bind="foo.bar">some value</span>');
angular.scenario.dsl.binding.call($root).call($root, 'foo.bar');
expect($root.futureResult).toEqual('some value');
});
it('should return error if no binding exists', function() {
angular.scenario.dsl.binding.call($root).call($root, 'foo.bar');
expect($root.futureError).toMatch(/does not exist/);
});
});
describe('Input', function() {
it('should change value in text input', function() {
doc.append('<input name="test.input" value="something">');
var chain = angular.scenario.dsl.input
.call($root).call($root, 'test.input');
chain.enter('foo');
expect($window.angular.log).toContain('element(input)');
expect($window.angular.log).toContain('element().trigger(change)');
expect(_jQuery('input[name="test.input"]').val()).toEqual('foo');
});
it('should return error if no input exists', function() {
var chain = angular.scenario.dsl.input
.call($root).call($root, 'test.input');
chain.enter('foo');
expect($root.futureError).toMatch(/does not exist/);
});
it('should toggle checkbox state', function() {
doc.append('<input type="checkbox" name="test.input" checked>');
expect(_jQuery('input[name="test.input"]')
.attr('checked')).toBeTruthy();
var chain = angular.scenario.dsl.input
.call($root).call($root, 'test.input');
chain.check();
expect($window.angular.log).toContain('element(input)');
expect($window.angular.log).toContain('element().trigger(click)');
expect(_jQuery('input[name="test.input"]')
.attr('checked')).toBeFalsy();
$window.angular.reset();
chain.check();
expect($window.angular.log).toContain('element(input)');
expect($window.angular.log).toContain('element().trigger(click)');
expect(_jQuery('input[name="test.input"]')
.attr('checked')).toBeTruthy();
});
it('should return error if checkbox does not exist', function() {
var chain = angular.scenario.dsl.input
.call($root).call($root, 'test.input');
chain.check();
expect($root.futureError).toMatch(/does not exist/);
});
it('should select option from radio group', function() {
doc.append(
'<input type="radio" name="0@test.input" value="foo">' +
'<input type="radio" name="0@test.input" value="bar" checked>'
);
expect(_jQuery('input[name="0@test.input"][value="bar"]')
.attr('checked')).toBeTruthy();
expect(_jQuery('input[name="0@test.input"][value="foo"]')
.attr('checked')).toBeFalsy();
var chain = angular.scenario.dsl.input
.call($root).call($root, 'test.input');
chain.select('foo');
expect($window.angular.log).toContain('element(input)');
expect($window.angular.log).toContain('element().trigger(click)');
expect(_jQuery('input[name="0@test.input"][value="bar"]')
.attr('checked')).toBeFalsy();
expect(_jQuery('input[name="0@test.input"][value="foo"]')
.attr('checked')).toBeTruthy();
});
it('should return error if radio button does not exist', function() {
var chain = angular.scenario.dsl.input
.call($root).call($root, 'test.input');
chain.select('foo');
expect($root.futureError).toMatch(/does not exist/);
});
});
});
});

View File

@@ -0,0 +1,85 @@
describe('angular.scenario.Describe', function() {
var log;
var root;
beforeEach(function() {
root = new angular.scenario.Describe();
/**
* Simple callback logging system. Use to assert proper order of calls.
*/
log = function(text) {
log.text = log.text + text;
};
log.fn = function(text) {
return function(done){
log(text);
(done || angular.noop)();
};
};
log.reset = function() {
log.text = '';
};
log.reset();
});
it('should handle basic nested case', function() {
root.describe('A', function(){
this.beforeEach(log.fn('{'));
this.afterEach(log.fn('}'));
this.it('1', log.fn('1'));
this.describe('B', function(){
this.beforeEach(log.fn('('));
this.afterEach(log.fn(')'));
this.it('2', log.fn('2'));
});
});
var specs = root.getSpecs();
expect(specs.length).toEqual(2);
expect(specs[0].name).toEqual('2');
specs[0].fn();
expect(log.text).toEqual('{(2)}');
log.reset();
expect(specs[1].name).toEqual('1');
specs[1].fn();
expect(log.text).toEqual('{1}');
});
it('should link nested describe blocks with parent and children', function() {
root.describe('A', function() {
this.it('1', angular.noop);
this.describe('B', function() {
this.it('2', angular.noop);
this.describe('C', function() {
this.it('3', angular.noop);
});
});
});
var specs = root.getSpecs();
expect(specs[2].definition.parent).toEqual(root);
expect(specs[0].definition.parent).toEqual(specs[2].definition.children[0]);
});
it('should not process xit and xdescribe', function() {
root.describe('A', function() {
this.xit('1', angular.noop);
this.xdescribe('B', function() {
this.it('2', angular.noop);
this.describe('C', function() {
this.it('3', angular.noop);
});
});
});
var specs = root.getSpecs();
expect(specs.length).toEqual(0);
});
it('should create uniqueIds in the tree', function() {
angular.scenario.Describe.id = 0;
var a = new angular.scenario.Describe();
var b = new angular.scenario.Describe();
expect(a.id).toNotEqual(b.id);
});
});

View File

@@ -0,0 +1,38 @@
describe('angular.scenario.Future', function() {
var future;
it('should set the name and behavior', function() {
var behavior = function() {};
var future = new angular.scenario.Future('test name', behavior);
expect(future.name).toEqual('test name');
expect(future.behavior).toEqual(behavior);
expect(future.value).toBeUndefined();
expect(future.fulfilled).toBeFalsy();
});
it('should be fulfilled after execution and done callback', function() {
var future = new angular.scenario.Future('test name', function(done) {
done();
});
future.execute(angular.noop);
expect(future.fulfilled).toBeTruthy();
});
it('should take callback with (error, result) and forward', function() {
var future = new angular.scenario.Future('test name', function(done) {
done(10, 20);
});
future.execute(function(error, result) {
expect(error).toEqual(10);
expect(result).toEqual(20);
});
});
it('should use error as value if provided', function() {
var future = new angular.scenario.Future('test name', function(done) {
done(10, 20);
});
future.execute(angular.noop);
expect(future.value).toEqual(10);
});
});

View File

@@ -0,0 +1,87 @@
describe('angular.scenario.HtmlUI', function() {
var ui;
var context;
var spec;
beforeEach(function() {
spec = {
name: 'test spec',
definition: {
id: 10,
name: 'child',
children: [],
parent: {
id: 20,
name: 'parent',
children: []
}
}
};
context = _jQuery("<div></div>");
ui = new angular.scenario.ui.Html(context);
});
it('should create nested describe context', function() {
ui.addSpec(spec);
expect(context.find('#describe-20 #describe-10 > h2').text())
.toEqual('describe: child');
expect(context.find('#describe-20 > h2').text()).toEqual('describe: parent');
expect(context.find('#describe-10 .tests > li .test-info .test-name').text())
.toEqual('it test spec');
expect(context.find('#describe-10 .tests > li').hasClass('status-pending'))
.toBeTruthy();
});
it('should update totals when steps complete', function() {
// Error
ui.addSpec(spec).error('error');
// Error
specUI = ui.addSpec(spec);
specUI.addStep('some step').finish();
specUI.finish('error');
// Failure
specUI = ui.addSpec(spec);
specUI.addStep('some step').finish('failure');
specUI.finish('failure');
// Failure
specUI = ui.addSpec(spec);
specUI.addStep('some step').finish('failure');
specUI.finish('failure');
// Failure
specUI = ui.addSpec(spec);
specUI.addStep('some step').finish('failure');
specUI.finish('failure');
// Success
specUI = ui.addSpec(spec);
specUI.addStep('some step').finish();
specUI.finish();
expect(parseInt(context.find('#status-legend .status-failure').text()))
.toEqual(3);
expect(parseInt(context.find('#status-legend .status-error').text()))
.toEqual(2);
expect(parseInt(context.find('#status-legend .status-success').text()))
.toEqual(1);
});
it('should update timer when test completes', function() {
// Success
specUI = ui.addSpec(spec);
specUI.addStep('some step').finish();
specUI.finish();
// Failure
specUI = ui.addSpec(spec);
specUI.addStep('some step').finish('failure');
specUI.finish('failure');
// Error
specUI = ui.addSpec(spec).error('error');
context.find('#describe-10 .tests > li .test-info .timer-result')
.each(function(index, timer) {
expect(timer.innerHTML).toMatch(/ms$/);
});
});
});

View File

@@ -1,38 +0,0 @@
describe('Matcher', function () {
function executeFutures() {
for(var i in $scenario.currentSpec.futures) {
var future = $scenario.currentSpec.futures[i];
future.behavior.call({}, function(value) { future.fulfill(value); });
}
}
var matcher;
beforeEach(function() {
setUpContext();
var future = $scenario.addFuture('Calculate first future', function(done) {
done(123);
});
matcher = new Matcher(this, future);
});
it('should correctly match toEqual', function() {
matcher.toEqual(123);
executeFutures();
});
it('should throw an error when incorrect match toEqual', function() {
matcher.toEqual(456);
try {
executeFutures();
fail();
} catch (e) {
expect(e).toEqual('Expected 456 but was 123');
}
});
it('should correctly match arrays', function() {
var future = $scenario.addFuture('Calculate first future', function(done) {
done(['a', 'b']);
});
matcher = new Matcher(this, future);
matcher.toEqual(['a', 'b']);
executeFutures();
});
});

View File

@@ -1,238 +1,96 @@
describe('Runner', function() {
var Describe, It, BeforeEach, AfterEach, body;
/**
* Mock spec runner.
*/
function MockSpecRunner() {}
MockSpecRunner.prototype.run = function(ui, spec, specDone) {
spec.fn.call(this);
specDone();
};
describe('angular.scenario.Runner', function() {
var $window;
var runner;
beforeEach(function() {
setUpContext();
Describe = _window.describe;
It = _window.it;
BeforeEach = _window.beforeEach;
AfterEach = _window.afterEach;
body = _jQuery('<div></div>');
});
describe('describe', function() {
it('should consume the describe functions', function() {
Describe('describe name', logger('body'));
expect(log).toEqual('body');
// Trick to get the scope out of a DSL statement
angular.scenario.dsl('dslScope', function() {
var scope = this;
return function() { return scope; };
});
describe('it', function() {
it('should consume it', function() {
Describe('describe name', function() {
It('should text', logger('body'));
});
expect(log).toEqual('body');
var spec = $scenario.specs['describe name: it should text'];
expect(spec.futures).toEqual([]);
expect(spec.name).toEqual('describe name: it should text');
});
it('should complain on duplicate it', function() {
// WRITE ME!!!!
});
it('should create a failing future if there is a javascript error', function() {
var spec;
Describe('D1', function() {
It('I1', function() {
spec = $scenario.currentSpec;
throw {message: 'blah'};
});
});
var future = spec.futures[0];
expect(future.name).toEqual('blah');
try {
future.behavior();
fail();
} catch (e) {
expect(e.message).toEqual('blah');
}
});
});
describe('beforeEach', function() {
it('should execute beforeEach before every it', function() {
Describe('describe name', function() {
BeforeEach(logger('before;'));
It('should text', logger('body;'));
It('should text2', logger('body2;'));
});
expect(log).toEqual('before;body;before;body2;');
});
});
describe('afterEach', function() {
it('should execute afterEach after every it', function() {
Describe('describe name', function() {
AfterEach(logger('after;'));
It('should text1', logger('body1;'));
It('should text2', logger('body2;'));
});
expect(log).toEqual('body1;after;body2;after;');
});
it('should always execute afterEach after every it', function() {
Describe('describe name', function() {
AfterEach(logger('after;'));
It('should text', function() {
logger('body1;')();
throw "MyError";
});
It('should text2', logger('body2;'));
});
expect(log).toEqual('body1;after;body2;after;');
});
it('should report an error if afterEach fails', function() {
var next;
Describe('describe name', function() {
AfterEach(function() {
$scenario.addFuture('afterEachLog', logger('after;'));
$scenario.addFuture('afterEachThrow', function() {
throw "AfterError";
});
});
It('should text1', function() {
$scenario.addFuture('future1', logger('future1;'));
});
It('should text2', function() {
$scenario.addFuture('future2', logger('future2;'));
});
});
$scenario.run(body);
expect(log).toEqual('future1;after;future2;after;');
expect(_window.$testrun.results).toEqual([
{ name : 'describe name: it should text1',
passed : false,
error : 'AfterError',
steps : [ 'future1', 'afterEachLog', 'afterEachThrow' ] },
{ name : 'describe name: it should text2',
passed : false,
error : 'AfterError',
steps : [ 'future2', 'afterEachLog', 'afterEachThrow' ] }]);
});
});
});
describe('future building', function() {
it('should queue futures', function() {
function behavior(){}
Describe('name', function() {
It('should', function() {
$scenario.addFuture('futureName', behavior);
});
});
expect($scenario.specs['name: it should'].futures[0].name).
toEqual('futureName');
});
});
describe('execution', function() {
it('should execute the queued futures', function() {
var next, firstThis, secondThis, doneThis, spec;
$scenario.specs['spec'] = {
futures: [
new Future('future1', function(done) {
next = done;
log += 'first;';
firstThis = this;
}),
new Future('future2', function(done) {
next = done;
log += 'second;';
secondThis = this;
})
]
// Trick to get the scope out of a DSL statement
angular.scenario.dsl('dslChain', function() {
return function() {
this.chained = 0;
this.chain = function() { this.chained++; return this; };
return this;
};
spec = $scenario.execute('spec', function(done){
log += 'done;';
doneThis = this;
});
expect(log).toEqual('first;');
next();
expect(log).toEqual('first;second;');
next();
expect(log).toEqual('first;second;done;');
expect(spec === window).toEqual(false);
expect(spec).toEqual(firstThis);
expect(spec).toEqual(secondThis);
expect(spec).toEqual(doneThis);
expect(spec.result.failed).toEqual(false);
expect(spec.result.finished).toEqual(true);
expect(spec.result.error).toBeUndefined();
expect(spec.result.passed).toEqual(true);
});
it('should handle exceptions in a future', function() {
$scenario.specs['spec'] = {
futures: [
new Future('first future', function(done) {
done();
}),
new Future('error', function(done) {
throw "MyError";
}),
new Future('should not execute', function(done) {
done();
})
]
};
var spec = $scenario.execute('spec');
expect(spec.result.passed).toEqual(false);
expect(spec.result.failed).toEqual(true);
expect(spec.result.finished).toEqual(true);
expect(spec.result.error).toEqual("MyError");
expect(_window.$testrun.results).toEqual([{
name: 'spec',
passed: false,
error: 'MyError',
steps: ['first future', 'error']}]);
$window = {};
runner = new angular.scenario.Runner($window);
});
afterEach(function() {
delete angular.scenario.dsl.dslScope;
delete angular.scenario.dsl.dslChain;
});
it('should publish the functions in the public API', function() {
angular.foreach(runner.api, function(fn, name) {
var func;
if (name in $window) {
func = $window[name];
}
expect(angular.isFunction(func)).toBeTruthy();
});
});
describe('run', function() {
var next;
beforeEach(function() {
Describe('d1', function() {
It('it1', function() { $scenario.addFuture('s1', logger('s1,')); });
It('it2', function() {
$scenario.addFuture('s2', logger('s2,'));
$scenario.addFuture('s2.2', function(done){ next = done; });
it('should construct valid describe trees with public API', function() {
var before = [];
var after = [];
$window.describe('A', function() {
$window.beforeEach(function() { before.push('A'); });
$window.afterEach(function() { after.push('A'); });
$window.it('1', angular.noop);
$window.describe('B', function() {
$window.beforeEach(function() { before.push('B'); });
$window.afterEach(function() { after.push('B'); });
$window.it('2', angular.noop);
$window.describe('C', function() {
$window.beforeEach(function() { before.push('C'); });
$window.afterEach(function() { after.push('C'); });
$window.it('3', angular.noop);
});
});
Describe('d2', function() {
It('it3', function() { $scenario.addFuture('s3', logger('s3,')); });
It('it4', function() { $scenario.addFuture('s4', logger('s4,')); });
});
var specs = runner.rootDescribe.getSpecs();
specs[0].fn();
expect(before).toEqual(['A', 'B', 'C']);
expect(after).toEqual(['C', 'B', 'A']);
expect(specs[2].definition.parent).toEqual(runner.rootDescribe);
expect(specs[0].definition.parent).toEqual(specs[2].definition.children[0]);
});
it('should publish the DSL statements to the $window', function() {
$window.describe('describe', function() {
$window.it('1', function() {
expect($window.dslScope).toBeDefined();
});
});
it('should execute all specs', function() {
$scenario.run(body);
expect(log).toEqual('s1,s2,');
next();
expect(log).toEqual('s1,s2,s3,s4,');
});
it('should publish done state and results as tests are run', function() {
expect(_window.$testrun.done).toBeFalsy();
expect(_window.$testrun.results).toEqual([]);
$scenario.run(body);
expect(_window.$testrun.done).toBeFalsy();
expect(_window.$testrun.results).toEqual([
{name: 'd1: it it1', passed: true, error: undefined, steps: ['s1']}
]);
next();
expect(_window.$testrun.done).toBeTruthy();
expect(_window.$testrun.results).toEqual([
{name: 'd1: it it1', passed: true, error: undefined, steps: ['s1']},
{name: 'd1: it it2', passed: true, error: undefined, steps: ['s2', 's2.2']},
{name: 'd2: it it3', passed: true, error: undefined, steps: ['s3']},
{name: 'd2: it it4', passed: true, error: undefined, steps: ['s4']}
]);
});
runner.run(null/*ui*/, null/*application*/, MockSpecRunner, rethrow);
});
});
it('should create a new scope for each DSL chain', function() {
$window.describe('describe', function() {
$window.it('1', function() {
var scope = $window.dslScope();
scope.test = "foo";
expect($window.dslScope().test).toBeUndefined();
});
$window.it('2', function() {
var scope = $window.dslChain().chain().chain();
expect(scope.chained).toEqual(2);
});
});
runner.run(null/*ui*/, null/*application*/, MockSpecRunner, rethrow);
});
});

View File

@@ -0,0 +1,165 @@
/**
* Mock of all required UI classes/methods. (UI, Spec, Step).
*/
function UIMock() {
this.log = [];
}
UIMock.prototype = {
addSpec: function(spec) {
var log = this.log;
log.push('addSpec:' + spec.name);
return {
addStep: function(name) {
log.push('addStep:' + name);
return {
finish: function(e) {
log.push('step finish:' + (e ? e : ''));
return this;
},
error: function(e) {
log.push('step error:' + (e ? e : ''));
return this;
}
};
},
finish: function(e) {
log.push('spec finish:' + (e ? e : ''));
return this;
},
error: function(e) {
log.push('spec error:' + (e ? e : ''));
return this;
}
};
},
};
/**
* Mock Application
*/
function ApplicationMock($window) {
this.$window = $window;
}
ApplicationMock.prototype = {
executeAction: function(callback) {
callback.call(this.$window);
}
};
describe('angular.scenario.SpecRunner', function() {
var $window;
var runner;
beforeEach(function() {
$window = {};
runner = angular.scope();
runner.application = new ApplicationMock($window);
runner.$become(angular.scenario.SpecRunner);
});
it('should bind futures to the spec', function() {
runner.addFuture('test future', function(done) {
this.application.value = 10;
done();
});
runner.futures[0].execute(angular.noop);
expect(runner.application.value).toEqual(10);
});
it('should pass done to future action behavior', function() {
runner.addFutureAction('test future', function(done) {
expect(angular.isFunction(done)).toBeTruthy();
done(10, 20);
});
runner.futures[0].execute(function(error, result) {
expect(error).toEqual(10);
expect(result).toEqual(20);
});
});
it('should pass execute future action on the $window', function() {
runner.addFutureAction('test future', function(done) {
this.test = 'test value';
done();
});
runner.futures[0].execute(angular.noop);
expect($window.test).toEqual('test value');
});
it('should execute spec function and notify UI', function() {
var finished = false;
var ui = new UIMock();
var spec = {name: 'test spec', fn: function() {
this.test = 'some value';
}};
runner.addFuture('test future', function(done) {
done();
});
runner.run(ui, spec, function() {
finished = true;
});
expect(runner.test).toEqual('some value');
expect(finished).toBeTruthy();
expect(ui.log).toEqual([
'addSpec:test spec',
'addStep:test future',
'step finish:',
'spec finish:'
]);
});
it('should execute notify UI on spec setup error', function() {
var finished = false;
var ui = new UIMock();
var spec = {name: 'test spec', fn: function() {
throw 'message';
}};
runner.run(ui, spec, function() {
finished = true;
});
expect(finished).toBeTruthy();
expect(ui.log).toEqual([
'addSpec:test spec',
'spec error:message'
]);
});
it('should execute notify UI on step failure', function() {
var finished = false;
var ui = new UIMock();
var spec = {name: 'test spec', fn: angular.noop};
runner.addFuture('test future', function(done) {
done('failure message');
});
runner.run(ui, spec, function() {
finished = true;
});
expect(finished).toBeTruthy();
expect(ui.log).toEqual([
'addSpec:test spec',
'addStep:test future',
'step finish:failure message',
'spec finish:failure message'
]);
});
it('should execute notify UI on step error', function() {
var finished = false;
var ui = new UIMock();
var spec = {name: 'test spec', fn: angular.noop};
runner.addFuture('test future', function(done) {
throw 'error message';
});
runner.run(ui, spec, function() {
finished = true;
});
expect(finished).toBeTruthy();
expect(ui.log).toEqual([
'addSpec:test spec',
'addStep:test future',
'step error:error message',
'spec finish:error message'
]);
});
});

View File

@@ -1,15 +0,0 @@
var _window, runner, log, $scenario;
function logger(text) {
return function(done){
log += text;
(done||noop)();
};
}
function setUpContext() {
_window = {};
runner = new angular.scenario.Runner(_window, _jQuery);
$scenario = _window.$scenario;
log = '';
}

View File

@@ -0,0 +1,43 @@
describe('angular.scenario.matchers', function () {
var matchers;
function expectMatcher(value, test) {
delete matchers.error;
delete matchers.future.value;
if (value !== undefined) {
matchers.future.value = value;
}
test();
expect(matchers.error).toBeUndefined();
}
beforeEach(function() {
/**
* Mock up the future system wrapped around matchers.
*
* @see Scenario.js#angular.scenario.matcher
*/
matchers = {
future: { name: 'test' }
};
matchers.addFuture = function(name, callback) {
callback(function(error) {
matchers.error = error;
});
};
angular.extend(matchers, angular.scenario.matcher);
});
it('should handle basic matching', function() {
expectMatcher(10, function() { matchers.toEqual(10); });
expectMatcher('value', function() { matchers.toBeDefined(); });
expectMatcher([1], function() { matchers.toBeTruthy(); });
expectMatcher("", function() { matchers.toBeFalsy(); });
expectMatcher(0, function() { matchers.toBeFalsy(); });
expectMatcher('foo', function() { matchers.toMatch('.o.'); });
expectMatcher(null, function() { matchers.toBeNull(); });
expectMatcher([1, 2, 3], function() { matchers.toContain(2); });
expectMatcher(3, function() { matchers.toBeLessThan(10); });
expectMatcher(3, function() { matchers.toBeGreaterThan(-5); });
});
});

View File

@@ -22,6 +22,19 @@ beforeEach(function(){
return "Expected to not have class 'ng-validation-error' but found.";
};
return !hasClass;
},
toEqualData: function(expected) {
return equals(this.actual, expected);
},
toHaveClass: function(clazz) {
this.message = function(){
return "Expected '" + sortedHtml(this.actual) + "' to have class '" + clazz + "'.";
};
return this.actual.hasClass ?
this.actual.hasClass(clazz) :
jqLite(this.actual).hasClass(clazz);
}
});
});
@@ -194,3 +207,9 @@ function click(element) {
JQLite.prototype.trigger.call(element, 'click');
}
}
function rethrow(e) {
if(e) {
throw e;
}
}