Compare commits

...

33 Commits

Author SHA1 Message Date
Igor Minar 0ffb47bdb6 cutting the 0.9.1 repulsion-field release 2010-10-26 22:18:25 -07:00
Igor Minar ec885489a2 updating CHANGELOG.md for the 0.9.1 release 2010-10-26 22:17:37 -07:00
Igor Minar 01c52e92b1 Adding e2e tests for the PersonalLog app
- added scenario runner
- added scenario specs
- cookie cleaning DSL
- making rmLog independent on ordering in the view
2010-10-26 21:58:00 -07:00
Misko Hevery 943377a091 fix closure compile warnings 2010-10-26 21:06:24 -07:00
Igor Minar 72b7a1c531 adding missing angular-mocksSpec.js file 2010-10-26 16:44:59 -07:00
Misko Hevery 9c0225512c fixes IE related failures, and form submit event handling in ie 2010-10-26 16:33:59 -07:00
Elliott Sprehn 40d7e66f40 Lots of bug fixes in the scenario runner and a bunch of new features.
- By default the runner now creates multiple output formats as it runs. Nodes are created in the DOM with ids: json, xml, and html.

ex. $('#json').html() => json output of the runner
ex. $('#xml').html() => json output of the runner

$result is also an object tree result.

The permitted formats are html,json,xml,object.

If you don't want certain formats you can select specific ones with the new ng:scenario-output attribute on the script tag.

<script src="angular-scenario.js" ng:scenario-output="xml,json">

- Added element(...).count() that returns the number of matching elements for the selector.

- repeater(...).count() now returns 0 if no elements matched which can be used to check if a repeater is empty.

- Added toBe() matcher that does strict equality with ===

- Implement iit and ddescribe. If iit() is used instead of it() then only that test will run. If ddescribe() is used instead of describe() them only it() statements inside of it will run. Several iit/ddescribe() blocks can be used to run isolated tests.

- Implement new event based model for SpecRunner. You can now listen for events in the runner. This is useful for writing your own UI or connecting a remote process (ex. WebDriver). Event callbacks execute on the Runner instance.

Events, if fired, will always be in the below order. All events always happen
except for Failure and Error events which only happen in error conditions.

Events:
  RunnerBegin
  SpecBegin(spec)
  StepBegin(spec, step)
  StepError(spec, step, error)
  StepFailure(spec, step, error)
  StepEnd(spec, step)
  SpecError(spec, step, error)
  SpecEnd(spec)
  RunnerEnd

- Only allow the browser to repaint every 10 steps. Cuts 700ms off Firefox in benchmark, 200ms off Chrome.

- Bug Fix: Manually navigate anchors on click since trigger wont work in Firefox.
2010-10-26 15:17:57 -07:00
Misko Hevery 1d52349440 Reverted change 841013a4c4 which does not work on all browsers 2010-10-26 14:25:01 -07:00
Igor Minar 3eb0c8bc67 Simplify dateGetter() within the date filter. 2010-10-26 13:47:50 -07:00
Igor Minar 42855e4363 Simplify implementation of angular.String.toDate() 2010-10-26 13:47:50 -07:00
Igor Minar 4c61fc01f9 Add TzDate to angular-mocks.js
TzDate is a Date-like type that is independent from the timezone
settings of the machine on which TzDate instances are created.
This property makes it ideal for testing code that deals with
timezones in a manner that makes the code portable between timezones.

Closes #81
2010-10-26 13:47:50 -07:00
Misko Hevery 4fdab37659 create HTML sanitizer to allow inclusion of untrusted HTML in safe manner.
Sanitization works in two phases:
 1) We parse the HTML into sax-like events (start, end, chars).
    HTML parsing is very complex, and so it may very well be that what
    most browser consider valid HTML may not pares properly here,
    but we do best effort. We treat this parser as untrusted.
 2) We have safe sanitizeWriter which treats its input (start, end, chars)
    as untrusted content and escapes everything. It only allows elements
    in the whitelist and only allows attributes which are whitelisted.
    Any attribute value must not start with 'javascript:'. This check
    is performed after escaping for entity (&xAB; etc..) and ignoring
    any whitespace.

 - Correct linky filter to use safeHtmlWriter
 - Correct html filter to use safeHtmlWriter

Close #33; Close #34
2010-10-26 13:41:07 -07:00
Igor Minar 841013a4c4 Add millisecond support for date filter
Date filter should translate input which is a number (or number
string) into a date.
2010-10-25 10:44:03 -07:00
Igor Minar 4e9a2aa10e Revert "added support for treating numbers as date in miliseconds"
This reverts commit 1391f19fb4.
2010-10-23 18:26:47 -07:00
Igor Minar 4fc2141458 improving the PersonalLog app
- adding jsdocs and comments
- logs should be ordered in inverse order
2010-10-23 16:25:53 -07:00
Igor Minar 5b40e87ac6 personalLog demo - initial version with spec 2010-10-23 14:38:08 -07:00
Misko Hevery 1391f19fb4 added support for treating numbers as date in miliseconds 2010-10-23 14:38:08 -07:00
Igor Minar 04a4d8b061 adding ng:submit directive for use with forms
- allows for binding angular expressions to onsubmit events
- prevent default submit action (page reload)
2010-10-23 14:22:54 -07:00
Igor Minar bbd87c9425 simplifying ng:click spec 2010-10-23 14:22:54 -07:00
Elliott Sprehn 64063b5d41 Fix issue where directories don't have a slash on the end and allow specifying a different port 2010-10-23 14:22:54 -07:00
Igor Minar 833e0ae343 $cookieStore should not be a global service
you must use $inject to $inject it as any other non-global service
2010-10-23 14:22:30 -07:00
Misko Hevery d74ef497de Fix for getting into recursive $eval on scope. Close #59
It sort of worked since the browser would throw stack too deep
exception and the angular would then print the error to console.
So as long as you did not have console open you would not notice
this as an error.
2010-10-23 13:42:11 -07:00
Misko Hevery 6ddcf91861 Fix test which was causing the Chrome runner to fail. Upgraded JSTD to latest. Cleanup whitespace. 2010-10-23 13:12:45 -07:00
Misko Hevery 8a867cee22 Workaround for http://bugs.jquery.com/ticket/7292 2010-10-22 22:46:51 -07:00
Misko Hevery 68217d427c Horrible IE + jQuery hack to make the tests pass 2010-10-22 16:40:04 -07:00
Misko Hevery 1efef67b5f corrected buzz.html to use ng:src on images to prevent sporious browser requests/errors 2010-10-22 14:27:03 -07:00
Misko Hevery a6cfa43c19 fixed issue with ie .data() method failing tests 2010-10-22 14:26:18 -07:00
Elliott Sprehn b41bc98c54 Better nodeserver that implements an HTTP server more completely 2010-10-22 00:26:05 +08:00
Misko Hevery aaabeb8c5e fixed more ie test failures 2010-10-20 23:51:49 -07:00
Misko Hevery 05d4971abb fix some of the failing ie tests 2010-10-20 23:17:59 -07:00
Igor Minar c6107fe8ac Rakefile should create the build directory when it is needed and doesn't exist 2010-10-20 17:14:31 -07:00
Igor Minar 68f074c299 adding CHANGELOG.md with 0.9.0 release notes 2010-10-20 17:00:36 -07:00
Igor Minar c53a37ed91 preparing 0.9.1 repulsion-field iteration 2010-10-20 15:52:55 -07:00
80 changed files with 3798 additions and 854 deletions
+1
View File
@@ -1,3 +1,4 @@
build/
angularjs.netrc
jstd.log
.DS_Store
+55
View File
@@ -0,0 +1,55 @@
# <angular/> 0.9.1 repulsion-field (2010-10-26) #
### Security
- added html sanitizer to fix the last few known security issues (issues #33 and #34)
### API
- new ng:submit directive for creating onSubmit handlers on forms (issue #76)
- the date filter now accepts milliseconds as well as date strings (issue #78)
- the html filter now supports 'unsafe' option to bypass html sanitization
### Testability
- lots of improvements related to the scenario runner (commit 40d7e66f)
### Demo
- added a new demo application: Personal Log (src example/personalLog)
### Chores
- lots of fixes to get all tests pass on IE
- added TzDate type to allow us to create timezone idependent tests (issue #88)
### Breaking changes
- $cookieStore service is not globally published any more, if you use it, you must request it via
$inject as any other non-global service
- html filter now sanitizes html content for XSS attacks which may result in different behavior
# <angular/> 0.9.0 dragon-breath (2010-10-20) #
### Security
- angular.fromJson not safer (issue #57)
- readString consumes invalid escapes (issue #56)
- use new Function instead of eval (issue #52)
### Speed
- css cleanup + inline all css and images in the main js (issue #64)
### Testability
- initial version of the built-in end-to-end scenario runner (issues #50, #67, #70)
### API
- allow ng:controller nesting (issue #39)
- new built-in date format filter (issue #45)
- $location needs method you call on updates (issue #32)
### Chores
- release versioning + file renaming (issue #69)
### Breaking changes
- $location.parse was replaced with $location.update
- all css and img files were inlined into the main js file, to support IE7 and older app must host
angular-ie-compat.js file
### Big Thanks to Our Community Contributors
- Vojta Jina
+16 -8
View File
@@ -9,6 +9,7 @@ ANGULAR = [
'src/Parser.js',
'src/Resource.js',
'src/Browser.js',
'src/sanitizer.js',
'src/jqLite.js',
'src/apis.js',
'src/filters.js',
@@ -26,12 +27,16 @@ ANGULAR_SCENARIO = [
'src/scenario/Application.js',
'src/scenario/Describe.js',
'src/scenario/Future.js',
'src/scenario/HtmlUI.js',
'src/scenario/ObjectModel.js',
'src/scenario/Describe.js',
'src/scenario/Runner.js',
'src/scenario/SpecRunner.js',
'src/scenario/dsl.js',
'src/scenario/matchers.js',
'src/scenario/output/Html.js',
'src/scenario/output/Json.js',
'src/scenario/output/Xml.js',
'src/scenario/output/Object.js',
]
BUILD_DIR = 'build'
@@ -39,6 +44,12 @@ BUILD_DIR = 'build'
task :default => [:compile, :test]
desc 'Init the build workspace'
task :init do
FileUtils.mkdir(BUILD_DIR) unless File.directory?(BUILD_DIR)
end
desc 'Clean Generated Files'
task :clean do
FileUtils.rm_r(BUILD_DIR, :force => true)
@@ -47,7 +58,7 @@ end
desc 'Compile Scenario'
task :compile_scenario do
task :compile_scenario => :init do
deps = [
'lib/jquery/jquery-1.4.2.js',
@@ -67,11 +78,8 @@ task :compile_scenario do
end
desc 'Generate IE css js patch'
task :generate_ie_compat do
task :generate_ie_compat => :init do
css = File.open('css/angular.css', 'r') {|f| f.read }
# finds all css rules that contain backround images and extracts the rule name(s), content type of
@@ -127,7 +135,7 @@ end
desc 'Compile JavaScript'
task :compile => [:compile_scenario, :generate_ie_compat] do
task :compile => [:init, :compile_scenario, :generate_ie_compat] do
deps = [
'src/angular.prefix',
@@ -149,7 +157,7 @@ end
desc 'Create angular distribution'
task :package => :compile do
task :package => [:clean, :compile] do
v = YAML::load( File.open( 'version.yaml' ) )['version']
match = v.match(/^([^-]*)(-snapshot)?$/)
version = match[1] + (match[2] ? ('-' + %x(git rev-parse HEAD)[0..7]) : '')
+16 -8
View File
@@ -8,6 +8,10 @@ body {
font-size: 14px;
}
#json, #xml {
display: none;
}
#header {
position: fixed;
width: 100%;
@@ -32,7 +36,7 @@ body {
height: 30px;
}
#frame h2,
#application h2,
#specs h2 {
margin: 0;
padding: 0.5em;
@@ -45,26 +49,26 @@ body {
}
#header,
#frame,
#application,
.test-info,
.test-actions li {
overflow: hidden;
}
#frame {
#application {
margin: 10px;
}
#frame iframe {
#application iframe {
width: 100%;
height: 758px;
}
#frame .popout {
#application .popout {
float: right;
}
#frame iframe {
#application iframe {
border: none;
}
@@ -154,6 +158,10 @@ body {
margin-left: 6em;
}
.test-describe {
padding-bottom: 0.5em;
}
.test-describe .test-describe {
margin: 5px 5px 10px 2em;
}
@@ -178,11 +186,11 @@ body {
}
#specs h2,
#frame h2 {
#application h2 {
background-color: #efefef;
}
#frame {
#application {
border: 1px solid #BABAD1;
}
+3 -3
View File
@@ -23,7 +23,7 @@
<ul>
<li ng:repeat="item in activities.data.items.$filter(filterText)">
<h1>
<img src="{{item.actor.thumbnailUrl}}"/>
<img ng:src="{{item.actor.thumbnailUrl}}"/>
<a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a>
<a href="#" ng:click="expandReplies(item)">Replies: {{item.links.replies[0].count}}</a>
</h1>
@@ -31,14 +31,14 @@
{{item.object.content | html}}
<div>
<a href="{{attachment.links.enclosure[0].href}}" ng:repeat="attachment in item.object.attachments">
<img src="{{attachment.links.preview[0].href}}"/>
<img ng:src="{{attachment.links.preview[0].href}}"/>
</a>
</div>
</div>
<my:expand expand="item.replies.show">
<ul>
<li ng:repeat="reply in item.replies.data.items">
<img src="{{reply.actor.thumbnailUrl}}"/>
<img ng:src="{{reply.actor.thumbnailUrl}}"/>
<a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>:
{{reply.content | html}}
</li>
+30
View File
@@ -0,0 +1,30 @@
<!doctype html>
<html xmlns:ng="http://angularjs.org">
<head>
<title>Personal Log</title>
<script type="text/javascript" src="../../src/angular-bootstrap.js" ng:autobind></script>
<script type="text/javascript" src="personalLog.js"></script>
</head>
<!-- TODO: we need to expose $root so that we can delete cookies in the scenario runner, there
must be a better way to do this -->
<body ng:controller="example.personalLog.LogCtrl" ng:init="$window.$root = $root">
<form action="" ng:submit="addLog(newMsg)">
<input type="text" name="newMsg" />
<input type="submit" value="add" />
<input type="button" value="remove all" ng:click="rmLogs()" />
</form>
<hr/>
<h2>Logs:</h2>
<ul>
<li ng:repeat="log in logs.$orderBy('-at')">
{{log.at | date:'yy-MM-dd HH:mm'}} {{log.msg}}
[<a href="" ng:click="rmLog(log)">x</a>]
</li>
</ul>
</body>
</html>
+76
View File
@@ -0,0 +1,76 @@
/**
* @fileOverview Very simple personal log demo application to demonstrate angular functionality,
* especially:
* - the MVC model
* - testability of controllers
* - dependency injection for controllers via $inject and constructor function
* - $cookieStore for persistent cookie-backed storage
* - simple templating constructs such as ng:repeat and {{}}
* - date filter
* - and binding onSubmit and onClick events to angular expressions
* @author Igor Minar
*/
/** @namespace the 'example' namespace */
var example = example || {};
/** @namespace namespace of the personal log app */
example.personalLog = {};
//name space isolating closure
(function() {
var LOGS = 'logs';
/**
* The controller for the personal log app.
*/
function LogCtrl($cookieStore) {
var self = this,
logs = self.logs = $cookieStore.get(LOGS) || []; //main model
/**
* Adds newMsg to the logs array as a log, persists it and clears newMsg.
* @param {string} msg Message to add (message is passed as parameter to make testing easier).
*/
this.addLog = function(msg) {
var newMsg = msg || self.newMsg;
if (!newMsg) return;
var log = {
at: new Date().getTime(),
msg: newMsg
}
logs.push(log);
$cookieStore.put(LOGS, logs);
self.newMsg = '';
};
/**
* Persistently removes a log from logs.
* @param {object} log The log to remove.
*/
this.rmLog = function(log) {
angular.Array.remove(logs, log);
$cookieStore.put(LOGS, logs);
};
/**
* Persistently removes all logs.
*/
this.rmLogs = function() {
logs.splice(0, logs.length);
$cookieStore.remove(LOGS);
};
}
//inject
LogCtrl.$inject = ['$cookieStore'];
//export
example.personalLog.LogCtrl = LogCtrl;
})();
@@ -0,0 +1,98 @@
describe('personal log', function() {
beforeEach(function() {
navigateTo('../personalLog.html');
});
afterEach(function() {
clearCookies();
});
it('should create new logs and order them in reverse chronological order', function(){
//create first msg
input('newMsg').enter('my first message');
element('form input[type="submit"]').click();
expect(repeater('ul li').count()).toEqual(1);
expect(repeater('ul li').column('log.msg')).toEqual('my first message');
//create second msg
input('newMsg').enter('my second message');
element('form input[type="submit"]').click();
expect(repeater('ul li').count()).toEqual(2);
expect(repeater('ul li').column('log.msg')).toEqual(['my second message', 'my first message']);
});
it('should delete a log when user clicks on the related X link', function() {
//create first msg
input('newMsg').enter('my first message');
element('form input[type="submit"]').click();
//create second msg
input('newMsg').enter('my second message');
element('form input[type="submit"]').click();
expect(repeater('ul li').count()).toEqual(2);
element('ul li a:eq(1)').click();
expect(repeater('ul li').count()).toEqual(1);
expect(repeater('ul li').column('log.msg')).toEqual('my second message');
element('ul li a:eq(0)').click();
expect(repeater('ul li').count()).toEqual(0);
});
it('should delete all cookies when user clicks on "remove all" button', function() {
//create first msg
input('newMsg').enter('my first message');
element('form input[type="submit"]').click();
//create second msg
input('newMsg').enter('my second message');
element('form input[type="submit"]').click();
expect(repeater('ul li').count()).toEqual(2);
element('input[value="remove all"]').click();
expect(repeater('ul li').count()).toEqual(0);
});
it('should preserve logs over page reloads', function() {
input('newMsg').enter('my persistent message');
element('form input[type="submit"]').click();
expect(repeater('ul li').count()).toEqual(1);
navigateTo('about:blank');
navigateTo('../personalLog.html');
expect(repeater('ul li').column('log.msg')).toEqual('my persistent message');
expect(repeater('ul li').count()).toEqual(1);
});
});
/**
* DSL for deleting all cookies.
*/
angular.scenario.dsl('clearCookies', function() {
/**
* Deletes cookies by interacting with the cookie service within the application under test.
*/
return function() {
this.addFutureAction('clear all cookies', function($window, $document, done) {
//TODO: accessing angular services is pretty nasty, we need a better way to reach them
var $cookies = $window.$root.$cookies,
cookieName;
for (cookieName in $cookies) {
console.log('deleting cookie: ' + cookieName);
delete $cookies[cookieName];
}
$window.$root.$eval();
done();
});
};
});
+10
View File
@@ -0,0 +1,10 @@
<!doctype html">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Personal Log Scenario Runner</title>
<script type="text/javascript" src="../../../src/scenario/angular-bootstrap.js"></script>
<script type="text/javascript" src="personalLogScenario.js"></script>
</head>
<body>
</body>
</html>
+122
View File
@@ -0,0 +1,122 @@
describe('example.personalLog.LogCtrl', function() {
var logCtrl;
function createNotesCtrl() {
var scope = angular.scope();
return scope.$new(example.personalLog.LogCtrl);
}
beforeEach(function() {
logCtrl = createNotesCtrl();
});
it('should initialize notes with an empty array', function() {
expect(logCtrl.logs).toEqual([]);
});
describe('addLog', function() {
beforeEach(function() {
expect(logCtrl.logs).toEqual([]);
});
it('should add newMsg to logs as a log entry', function() {
logCtrl.newMsg = 'first log message';
logCtrl.addLog();
expect(logCtrl.logs.length).toBe(1);
expect(logCtrl.logs[0].msg).toBe('first log message');
//one more msg, this time passed in as param
logCtrl.addLog('second log message');
expect(logCtrl.logs.length).toBe(2);
expect(logCtrl.logs[0].msg).toBe('first log message');
expect(logCtrl.logs[1].msg).toBe('second log message');
});
it('should clear newMsg when log entry is persisted', function() {
logCtrl.addLog('first log message');
expect(logCtrl.newMsg).toBe('');
});
it('should store logs in the logs cookie', function() {
expect(logCtrl.$cookies.logs).not.toBeDefined();
logCtrl.addLog('first log message');
expect(logCtrl.$cookies.logs).toBeTruthy();
});
it('should do nothing if newMsg is empty', function() {
logCtrl.addLog('');
expect(logCtrl.logs.length).toBe(0);
});
});
describe('rmLog', function() {
beforeEach(function() {
logCtrl.addLog('message1');
logCtrl.addLog('message2');
logCtrl.addLog('message3');
logCtrl.addLog('message4');
expect(logCtrl.logs.length).toBe(4);
});
it('should delete a message identified by index', function() {
logCtrl.rmLog(logCtrl.logs[1]);
expect(logCtrl.logs.length).toBe(3);
logCtrl.rmLog(logCtrl.logs[2]);
expect(logCtrl.logs.length).toBe(2);
expect(logCtrl.logs[0].msg).toBe('message1');
expect(logCtrl.logs[1].msg).toBe('message3');
});
it('should update cookies when a log is deleted', function() {
expect(logCtrl.$cookies.logs).toMatch(/\[\{.*?\}(,\{.*?\}){3}\]/);
logCtrl.rmLog(logCtrl.logs[1]);
expect(logCtrl.$cookies.logs).toMatch(/\[\{.*?\}(,\{.*?\}){2}\]/);
logCtrl.rmLog(logCtrl.logs[0]);
logCtrl.rmLog(logCtrl.logs[0]);
logCtrl.rmLog(logCtrl.logs[0]);
expect(logCtrl.$cookies.logs).toMatch(/\[\]/);
});
});
describe('rmLogs', function() {
beforeEach(function() {
logCtrl.addLog('message1');
logCtrl.addLog('message2');
logCtrl.addLog('message3');
logCtrl.addLog('message4');
expect(logCtrl.logs.length).toBe(4);
});
it('should remove all logs', function() {
logCtrl.rmLogs();
expect(logCtrl.logs).toEqual([]);
});
it('should remove logs cookie', function() {
expect(logCtrl.$cookies.logs).toBeTruthy();
logCtrl.rmLogs();
expect(logCtrl.$cookies.logs).not.toBeDefined();
});
});
});
+4 -2
View File
@@ -1,7 +1,7 @@
server: http://localhost:9876
load:
- lib/jasmine/jasmine-1.0.1.js
- lib/jasmine-1.0.1/jasmine.js
- lib/jasmine-jstd-adapter/JasmineAdapter.js
- lib/jquery/jquery-1.4.2.js
- test/jquery_alias.js
@@ -10,9 +10,11 @@ load:
- src/*.js
- test/testabilityPatch.js
- src/scenario/Scenario.js
- src/scenario/output/*.js
- src/scenario/*.js
- test/angular-mocks.js
- test/scenario/*.js
- test/scenario/output/*.js
- test/*.js
exclude:
@@ -20,6 +22,6 @@ exclude:
- src/angular.suffix
- src/angular-bootstrap.js
- src/AngularPublic.js
- src/scenario/bootstrap.js
- src/scenario/angular-bootstrap.js
- test/jquery_remove.js
+6 -2
View File
@@ -1,24 +1,28 @@
server: http://localhost:9876
load:
- lib/jasmine/jasmine-1.0.1.js
- lib/jasmine-1.0.1/jasmine.js
- lib/jasmine-jstd-adapter/JasmineAdapter.js
- lib/jquery/jquery-1.4.2.js
- test/jquery_remove.js
- src/Angular.js
- src/JSON.js
- src/*.js
- example/personalLog/*.js
- test/testabilityPatch.js
- src/scenario/Scenario.js
- src/scenario/output/*.js
- src/scenario/*.js
- test/angular-mocks.js
- test/scenario/*.js
- test/scenario/output/*.js
- test/*.js
- example/personalLog/test/*.js
exclude:
- test/jquery_alias.js
- src/angular.prefix
- src/angular.suffix
- src/angular-bootstrap.js
- src/scenario/bootstrap.js
- src/scenario/angular-bootstrap.js
- src/AngularPublic.js
+309
View File
@@ -0,0 +1,309 @@
/*
* HTML Parser By John Resig (ejohn.org)
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*
* // Use like so:
* htmlParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
* // or to get an XML string:
* HTMLtoXML(htmlString);
*
* // or to get an XML DOM Document
* HTMLtoDOM(htmlString);
*
* // or to inject into an existing document/DOM node
* HTMLtoDOM(htmlString, document);
* HTMLtoDOM(htmlString, document.body);
*
*/
(function(){
// Regular Expressions for parsing tags and attributes
var startTag = /^<(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,
endTag = /^<\/(\w+)[^>]*>/,
attr = /(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;
// Empty Elements - HTML 4.01
var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");
// Block Elements - HTML 4.01
var block = makeMap("address,applet,blockquote,button,center,dd,del,dir,div,dl,dt,fieldset,form,frameset,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul");
// Inline Elements - HTML 4.01
var inline = makeMap("a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var");
// Elements that you can, intentionally, leave open
// (and which close themselves)
var closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr");
// Attributes that have their values filled in disabled="disabled"
var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected");
// Special Elements (can contain anything)
var special = makeMap("script,style");
var htmlParser = this.htmlParser = function( html, handler ) {
var index, chars, match, stack = [], last = html;
stack.last = function(){
return this[ this.length - 1 ];
};
while ( html ) {
chars = true;
// Make sure we're not in a script or style element
if ( !stack.last() || !special[ stack.last() ] ) {
// Comment
if ( html.indexOf("<!--") == 0 ) {
index = html.indexOf("-->");
if ( index >= 0 ) {
if ( handler.comment )
handler.comment( html.substring( 4, index ) );
html = html.substring( index + 3 );
chars = false;
}
// end tag
} else if ( html.indexOf("</") == 0 ) {
match = html.match( endTag );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( endTag, parseEndTag );
chars = false;
}
// start tag
} else if ( html.indexOf("<") == 0 ) {
match = html.match( startTag );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( startTag, parseStartTag );
chars = false;
}
}
if ( chars ) {
index = html.indexOf("<");
var text = index < 0 ? html : html.substring( 0, index );
html = index < 0 ? "" : html.substring( index );
if ( handler.chars )
handler.chars( text );
}
} else {
html = html.replace(new RegExp("(.*)<\/" + stack.last() + "[^>]*>"), function(all, text){
text = text.replace(/<!--(.*?)-->/g, "$1")
.replace(/<!\[CDATA\[(.*?)]]>/g, "$1");
if ( handler.chars )
handler.chars( text );
return "";
});
parseEndTag( "", stack.last() );
}
if ( html == last )
throw "Parse Error: " + html;
last = html;
}
// Clean up any remaining tags
parseEndTag();
function parseStartTag( tag, tagName, rest, unary ) {
if ( block[ tagName ] ) {
while ( stack.last() && inline[ stack.last() ] ) {
parseEndTag( "", stack.last() );
}
}
if ( closeSelf[ tagName ] && stack.last() == tagName ) {
parseEndTag( "", tagName );
}
unary = empty[ tagName ] || !!unary;
if ( !unary )
stack.push( tagName );
if ( handler.start ) {
var attrs = [];
rest.replace(attr, function(match, name) {
var value = arguments[2] ? arguments[2] :
arguments[3] ? arguments[3] :
arguments[4] ? arguments[4] :
fillAttrs[name] ? name : "";
attrs.push({
name: name,
value: value,
escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //"
});
});
if ( handler.start )
handler.start( tagName, attrs, unary );
}
}
function parseEndTag( tag, tagName ) {
// If no tag name is provided, clean shop
if ( !tagName )
var pos = 0;
// Find the closest opened tag of the same type
else
for ( var pos = stack.length - 1; pos >= 0; pos-- )
if ( stack[ pos ] == tagName )
break;
if ( pos >= 0 ) {
// Close all the open elements, up the stack
for ( var i = stack.length - 1; i >= pos; i-- )
if ( handler.end )
handler.end( stack[ i ] );
// Remove the open elements from the stack
stack.length = pos;
}
}
};
this.HTMLtoXML = function( html ) {
var results = "";
htmlParser(html, {
start: function( tag, attrs, unary ) {
results += "<" + tag;
for ( var i = 0; i < attrs.length; i++ )
results += " " + attrs[i].name + '="' + attrs[i].escaped + '"';
results += (unary ? "/" : "") + ">";
},
end: function( tag ) {
results += "</" + tag + ">";
},
chars: function( text ) {
results += text;
},
comment: function( text ) {
results += "<!--" + text + "-->";
}
});
return results;
};
this.HTMLtoDOM = function( html, doc ) {
// There can be only one of these elements
var one = makeMap("html,head,body,title");
// Enforce a structure for the document
var structure = {
link: "head",
base: "head"
};
if ( !doc ) {
if ( typeof DOMDocument != "undefined" )
doc = new DOMDocument();
else if ( typeof document != "undefined" && document.implementation && document.implementation.createDocument )
doc = document.implementation.createDocument("", "", null);
else if ( typeof ActiveX != "undefined" )
doc = new ActiveXObject("Msxml.DOMDocument");
} else
doc = doc.ownerDocument ||
doc.getOwnerDocument && doc.getOwnerDocument() ||
doc;
var elems = [],
documentElement = doc.documentElement ||
doc.getDocumentElement && doc.getDocumentElement();
// If we're dealing with an empty document then we
// need to pre-populate it with the HTML document structure
if ( !documentElement && doc.createElement ) (function(){
var html = doc.createElement("html");
var head = doc.createElement("head");
head.appendChild( doc.createElement("title") );
html.appendChild( head );
html.appendChild( doc.createElement("body") );
doc.appendChild( html );
})();
// Find all the unique elements
if ( doc.getElementsByTagName )
for ( var i in one )
one[ i ] = doc.getElementsByTagName( i )[0];
// If we're working with a document, inject contents into
// the body element
var curParentNode = one.body;
htmlParser( html, {
start: function( tagName, attrs, unary ) {
// If it's a pre-built element, then we can ignore
// its construction
if ( one[ tagName ] ) {
curParentNode = one[ tagName ];
return;
}
var elem = doc.createElement( tagName );
for ( var attr in attrs )
elem.setAttribute( attrs[ attr ].name, attrs[ attr ].value );
if ( structure[ tagName ] && typeof one[ structure[ tagName ] ] != "boolean" )
one[ structure[ tagName ] ].appendChild( elem );
else if ( curParentNode && curParentNode.appendChild )
curParentNode.appendChild( elem );
if ( !unary ) {
elems.push( elem );
curParentNode = elem;
}
},
end: function( tag ) {
elems.length -= 1;
// Init the new parentNode
curParentNode = elems[ elems.length - 1 ];
},
chars: function( text ) {
curParentNode.appendChild( doc.createTextNode( text ) );
},
comment: function( text ) {
// create comment node
}
});
return doc;
};
function makeMap(str){
var obj = {}, items = str.split(",");
for ( var i = 0; i < items.length; i++ )
obj[ items[i] ] = true;
return obj;
}
})();
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2008-2010 Pivotal Labs
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+188
View File
@@ -0,0 +1,188 @@
jasmine.TrivialReporter = function(doc) {
this.document = doc || document;
this.suiteDivs = {};
this.logRunningSpecs = false;
};
jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
var el = document.createElement(type);
for (var i = 2; i < arguments.length; i++) {
var child = arguments[i];
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child));
} else {
if (child) { el.appendChild(child); }
}
}
for (var attr in attrs) {
if (attr == "className") {
el[attr] = attrs[attr];
} else {
el.setAttribute(attr, attrs[attr]);
}
}
return el;
};
jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
var showPassed, showSkipped;
this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' },
this.createDom('div', { className: 'banner' },
this.createDom('div', { className: 'logo' },
this.createDom('a', { href: 'http://pivotal.github.com/jasmine/', target: "_blank" }, "Jasmine"),
this.createDom('span', { className: 'version' }, runner.env.versionString())),
this.createDom('div', { className: 'options' },
"Show ",
showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
)
),
this.runnerDiv = this.createDom('div', { className: 'runner running' },
this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
);
this.document.body.appendChild(this.outerDiv);
var suites = runner.suites();
for (var i = 0; i < suites.length; i++) {
var suite = suites[i];
var suiteDiv = this.createDom('div', { className: 'suite' },
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
this.suiteDivs[suite.id] = suiteDiv;
var parentDiv = this.outerDiv;
if (suite.parentSuite) {
parentDiv = this.suiteDivs[suite.parentSuite.id];
}
parentDiv.appendChild(suiteDiv);
}
this.startedAt = new Date();
var self = this;
showPassed.onclick = function(evt) {
if (showPassed.checked) {
self.outerDiv.className += ' show-passed';
} else {
self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
}
};
showSkipped.onclick = function(evt) {
if (showSkipped.checked) {
self.outerDiv.className += ' show-skipped';
} else {
self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
}
};
};
jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
var results = runner.results();
var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
this.runnerDiv.setAttribute("class", className);
//do it twice for IE
this.runnerDiv.setAttribute("className", className);
var specs = runner.specs();
var specCount = 0;
for (var i = 0; i < specs.length; i++) {
if (this.specFilter(specs[i])) {
specCount++;
}
}
var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);
this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
};
jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
var results = suite.results();
var status = results.passed() ? 'passed' : 'failed';
if (results.totalCount == 0) { // todo: change this to check results.skipped
status = 'skipped';
}
this.suiteDivs[suite.id].className += " " + status;
};
jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
if (this.logRunningSpecs) {
this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
}
};
jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
var results = spec.results();
var status = results.passed() ? 'passed' : 'failed';
if (results.skipped) {
status = 'skipped';
}
var specDiv = this.createDom('div', { className: 'spec ' + status },
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
this.createDom('a', {
className: 'description',
href: '?spec=' + encodeURIComponent(spec.getFullName()),
title: spec.getFullName()
}, spec.description));
var resultItems = results.getItems();
var messagesDiv = this.createDom('div', { className: 'messages' });
for (var i = 0; i < resultItems.length; i++) {
var result = resultItems[i];
if (result.type == 'log') {
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
} else if (result.type == 'expect' && result.passed && !result.passed()) {
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
if (result.trace.stack) {
messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
}
}
}
if (messagesDiv.childNodes.length > 0) {
specDiv.appendChild(messagesDiv);
}
this.suiteDivs[spec.suite.id].appendChild(specDiv);
};
jasmine.TrivialReporter.prototype.log = function() {
var console = jasmine.getGlobal().console;
if (console && console.log) {
if (console.log.apply) {
console.log.apply(console, arguments);
} else {
console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
}
}
};
jasmine.TrivialReporter.prototype.getLocation = function() {
return this.document.location;
};
jasmine.TrivialReporter.prototype.specFilter = function(spec) {
var paramMap = {};
var params = this.getLocation().search.substring(1).split('&');
for (var i = 0; i < params.length; i++) {
var p = params[i].split('=');
paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
}
if (!paramMap["spec"]) return true;
return spec.getFullName().indexOf(paramMap["spec"]) == 0;
};
+166
View File
@@ -0,0 +1,166 @@
body {
font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
}
.jasmine_reporter a:visited, .jasmine_reporter a {
color: #303;
}
.jasmine_reporter a:hover, .jasmine_reporter a:active {
color: blue;
}
.run_spec {
float:right;
padding-right: 5px;
font-size: .8em;
text-decoration: none;
}
.jasmine_reporter {
margin: 0 5px;
}
.banner {
color: #303;
background-color: #fef;
padding: 5px;
}
.logo {
float: left;
font-size: 1.1em;
padding-left: 5px;
}
.logo .version {
font-size: .6em;
padding-left: 1em;
}
.runner.running {
background-color: yellow;
}
.options {
text-align: right;
font-size: .8em;
}
.suite {
border: 1px outset gray;
margin: 5px 0;
padding-left: 1em;
}
.suite .suite {
margin: 5px;
}
.suite.passed {
background-color: #dfd;
}
.suite.failed {
background-color: #fdd;
}
.spec {
margin: 5px;
padding-left: 1em;
clear: both;
}
.spec.failed, .spec.passed, .spec.skipped {
padding-bottom: 5px;
border: 1px solid gray;
}
.spec.failed {
background-color: #fbb;
border-color: red;
}
.spec.passed {
background-color: #bfb;
border-color: green;
}
.spec.skipped {
background-color: #bbb;
}
.messages {
border-left: 1px dashed gray;
padding-left: 1em;
padding-right: 1em;
}
.passed {
background-color: #cfc;
display: none;
}
.failed {
background-color: #fbb;
}
.skipped {
color: #777;
background-color: #eee;
display: none;
}
/*.resultMessage {*/
/*white-space: pre;*/
/*}*/
.resultMessage span.result {
display: block;
line-height: 2em;
color: black;
}
.resultMessage .mismatch {
color: black;
}
.stackTrace {
white-space: pre;
font-size: .8em;
margin-left: 10px;
max-height: 5em;
overflow: auto;
border: 1px inset red;
padding: 1em;
background: #eef;
}
.finished-at {
padding-left: 1em;
font-size: .6em;
}
.show-passed .passed,
.show-skipped .skipped {
display: block;
}
#jasmine_content {
position:fixed;
right: 100%;
}
.runner {
border: 1px solid gray;
display: block;
margin: 5px 0;
padding: 2px 0 2px 10px;
}
BIN
View File
Binary file not shown.
+223 -16
View File
@@ -1,22 +1,229 @@
var sys = require('sys'),
http = require('http'),
fs = require('fs');
fs = require('fs'),
url = require('url'),
events = require('events');
http.createServer(function (req, res) {
res.writeHead(200, {});
sys.p('GET ' + req.url);
var file = fs.createReadStream('.' + req.url);
file.addListener('data', bind(res, res.write));
file.addListener('error', function( error ){
sys.p(error);
var DEFAULT_PORT = 8000;
function main(argv) {
new HttpServer({
'GET': (function() {
var servlet = new StaticServlet();
return servlet.handleRequest.bind(servlet)
})()
}).start(Number(argv[2]) || DEFAULT_PORT);
}
function escapeHtml(value) {
return value.toString().
replace('<', '&lt;').
replace('>', '&gt').
replace('"', '&quot;');
}
/**
* An Http server implementation that uses a map of methods to decide
* action routing.
*
* @param {Object} Map of method => Handler function
*/
function HttpServer(handlers) {
this.handlers = handlers;
this.server = http.createServer(this.handleRequest_.bind(this));
}
HttpServer.prototype.start = function(port) {
this.port = port;
this.server.listen(port);
sys.puts('Http Server running at http://127.0.0.1:' + port + '/');
};
HttpServer.prototype.parseUrl_ = function(urlString) {
var parsed = url.parse(urlString);
parsed.pathname = url.resolve('/', parsed.pathname);
return url.parse(url.format(parsed), true);
};
HttpServer.prototype.handleRequest_ = function(req, res) {
var logEntry = req.method + ' ' + req.url;
if (req.headers['user-agent']) {
logEntry += ' ' + req.headers['user-agent'];
}
sys.puts(logEntry);
req.url = this.parseUrl_(req.url);
var handler = this.handlers[req.method];
if (!handler) {
res.writeHead(501);
res.end();
} else {
handler.call(this, req, res);
}
};
/**
* Handles static content.
*/
function StaticServlet() {}
StaticServlet.MimeMap = {
'txt': 'text/plain',
'html': 'text/html',
'css': 'text/css',
'xml': 'application/xml',
'json': 'application/json',
'js': 'application/javascript',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'png': 'image/png'
};
StaticServlet.prototype.handleRequest = function(req, res) {
var self = this;
var path = ('./' + req.url.pathname).replace('//','/');
var parts = path.split('/');
if (parts[parts.length-1].charAt(0) === '.')
return self.sendForbidden_(req, res, path);
fs.stat(path, function(err, stat) {
if (err)
return self.sendMissing_(req, res, path);
if (stat.isDirectory())
return self.sendDirectory_(req, res, path);
return self.sendFile_(req, res, path);
});
}
StaticServlet.prototype.sendError_ = function(req, res, error) {
res.writeHead(500, {
'Content-Type': 'text/html'
});
res.write('<!doctype html>\n');
res.write('<title>Internal Server Error</title>\n');
res.write('<h1>Internal Server Error</h1>');
res.write('<pre>' + escapeHtml(sys.inspect(error)) + '</pre>');
sys.puts('500 Internal Server Error');
sys.puts(sys.inspect(error));
};
StaticServlet.prototype.sendMissing_ = function(req, res, path) {
path = path.substring(1);
res.writeHead(404, {
'Content-Type': 'text/html'
});
res.write('<!doctype html>\n');
res.write('<title>404 Not Found</title>\n');
res.write('<h1>Not Found</h1>');
res.write(
'<p>The requested URL ' +
escapeHtml(path) +
' was not found on this server.</p>'
);
res.end();
sys.puts('404 Not Found: ' + path);
};
StaticServlet.prototype.sendForbidden_ = function(req, res, path) {
path = path.substring(1);
res.writeHead(403, {
'Content-Type': 'text/html'
});
res.write('<!doctype html>\n');
res.write('<title>403 Forbidden</title>\n');
res.write('<h1>Forbidden</h1>');
res.write(
'<p>You do not have permission to access ' +
escapeHtml(path) + ' on this server.</p>'
);
res.end();
sys.puts('403 Forbidden: ' + path);
};
StaticServlet.prototype.sendRedirect_ = function(req, res, redirectUrl) {
res.writeHead(301, {
'Content-Type': 'text/html',
'Location': redirectUrl
});
res.write('<!doctype html>\n');
res.write('<title>301 Moved Permanently</title>\n');
res.write('<h1>Moved Permanently</h1>');
res.write(
'<p>The document has moved <a href="' +
redirectUrl +
'">here</a>.</p>'
);
res.end();
sys.puts('401 Moved Permanently: ' + redirectUrl);
};
StaticServlet.prototype.sendFile_ = function(req, res, path) {
var self = this;
var file = fs.createReadStream(path);
res.writeHead(200, {
'Content-Type': StaticServlet.
MimeMap[path.split('.').pop()] || 'text/plain'
});
file.on('data', res.write.bind(res));
file.on('close', function() {
res.end();
});
file.addListener('close', bind(res, res.end));
}).listen(8000);
sys.puts('Server running at http://127.0.0.1:8000/');
file.on('error', function(error) {
self.sendError_(req, res, error);
});
};
function bind(_this, _fn) {
return function(){
return _fn.apply(_this, arguments);
};
}
StaticServlet.prototype.sendDirectory_ = function(req, res, path) {
var self = this;
if (path.match(/[^\/]$/)) {
req.url.pathname += '/';
var redirectUrl = url.format(url.parse(url.format(req.url)));
return self.sendRedirect_(req, res, redirectUrl);
}
fs.readdir(path, function(err, files) {
if (err)
return self.sendError_(req, res, error);
if (!files.length)
return self.writeDirectoryIndex_(req, res, path, []);
var remaining = files.length;
files.forEach(function(fileName, index) {
fs.stat(path + '/' + fileName, function(err, stat) {
if (err)
return self.sendError_(req, res, err);
if (stat.isDirectory()) {
files[index] = fileName + '/';
}
if (!(--remaining))
return self.writeDirectoryIndex_(req, res, path, files);
});
});
});
};
StaticServlet.prototype.writeDirectoryIndex_ = function(req, res, path, files) {
path = path.substring(1);
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.write('<!doctype html>\n');
res.write('<title>' + escapeHtml(path) + '</title>\n');
res.write('<style>\n');
res.write(' ol { list-style-type: none; font-size: 1.2em; }\n');
res.write('</style>\n');
res.write('<h1>Directory: ' + escapeHtml(path) + '</h1>');
res.write('<ol>');
files.forEach(function(fileName) {
if (fileName.charAt(0) !== '.') {
res.write('<li><a href="' +
escapeHtml(fileName) + '">' +
escapeHtml(fileName) + '</a></li>');
}
});
res.write('</ol>');
res.end();
};
// Must be last,
main(process.argv);
+1 -1
View File
@@ -1 +1 @@
node lib/nodeserver/server.js
node lib/nodeserver/server.js $1
-1
View File
@@ -2,7 +2,6 @@
<html xmlns:ng="http://angularjs.org">
<head>
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
</script>
</head>
<body ng:init="$window.$root = this; data = [{foo: 'foo'},{bar: 'bar'}]">
<p>This is a demo of a potential bug in angular.</p>
+9
View File
@@ -0,0 +1,9 @@
<!DOCTYPE HTML>
<html xmlns:ng="http://angularjs.org">
<head>
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
</head>
<body ng:init="$window.$root = this; data = [{foo: 'foo'},{bar: 'bar'}]">
<ng:include src="'ng_include_this.partial'" scope="this"/>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
included HTML. eval count: {{c=c+1}}
+1 -1
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<script type="text/javascript" src="../angular-scenario.js"></script>
<script type="text/javascript" src="../build/angular-scenario.js"></script>
<script type="text/javascript" src="widgets-scenario.js"></script>
</head>
<body>
+1 -1
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<script type="text/javascript" src="../src/scenario/bootstrap.js"></script>
<script type="text/javascript" src="../src/scenario/angular-bootstrap.js"></script>
<script type="text/javascript" src="widgets-scenario.js"></script>
</head>
<body>
+14 -1
View File
@@ -27,15 +27,28 @@ describe('widgets', function() {
expect(binding('multiselect').fromJson()).toEqual(['A', 'C']);
expect(binding('button').fromJson()).toEqual({'count': 0});
expect(binding('form').fromJson()).toEqual({'count': 0});
element('form a').click();
expect(binding('button').fromJson()).toEqual({'count': 1});
element('input[value="submit"]').click();
element('input[value="submit input"]').click();
expect(binding('button').fromJson()).toEqual({'count': 2});
expect(binding('form').fromJson()).toEqual({'count': 1});
element('button:contains("submit button")').click();
expect(binding('button').fromJson()).toEqual({'count': 2});
expect(binding('form').fromJson()).toEqual({'count': 2});
element('input[value="button"]').click();
expect(binding('button').fromJson()).toEqual({'count': 3});
element('input[type="image"]').click();
expect(binding('button').fromJson()).toEqual({'count': 4});
element('#navigate a').click();
expect(binding('$location.hash')).toEqual('route');
/**
* Custom value parser for futures.
*/
+10 -5
View File
@@ -2,7 +2,6 @@
<html xmlns:ng="http://angularjs.org">
<head>
<link rel="stylesheet" type="text/css" href="style.css"/>
<script type="text/javascript" src="../libs/jquery/jquery-1.4.2.js"></script>
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
</head>
<body ng:init="$window.$scope = this">
@@ -74,15 +73,16 @@
<tr><th colspan="3">Buttons</th></tr>
<tr>
<td>ng:change<br/>ng:click</td>
<td ng:init="button.count = 0">
<form>
<td ng:init="button.count = 0; form.count = 0;">
<form ng:submit="form.count = form.count + 1">
<input type="button" value="button" ng:change="button.count = button.count + 1"/> <br/>
<input type="submit" value="submit" ng:change="button.count = button.count + 1"/><br/>
<input type="submit" value="submit input" ng:change="button.count = button.count + 1"/><br/>
<button type="submit">submit button</button>
<input type="image" src="" ng:change="button.count = button.count + 1"/><br/>
<a href="" ng:click="button.count = button.count + 1">action</a>
</form>
</td>
<td>button={{button}}</td>
<td>button={{button}} form={{form}}</td>
</tr>
<tr><th colspan="3">Repeaters</th></tr>
<tr id="repeater-row">
@@ -94,6 +94,11 @@
</td>
<td></td>
</tr>
<tr id="navigate">
<td>navigate</td>
<td><a href="#route">Go to #route</td>
<td>{{$location.hash}}</td>
</tr>
</table>
</body>
</html>
+1 -1
View File
@@ -1 +1 @@
java -jar lib/jstestdriver/JsTestDriver.jar --port 9876
java -jar lib/jstestdriver/JsTestDriver.jar --port 9876 --browserTimeout 20000
+33 -20
View File
@@ -3,6 +3,25 @@
if (typeof document.getAttribute == $undefined)
document.getAttribute = function() {};
//The below may not be true on browsers in the Turkish locale.
var lowercase = function (value){ return isString(value) ? value.toLowerCase() : value; };
var uppercase = function (value){ return isString(value) ? value.toUpperCase() : value; };
var manualLowercase = function (s) {
return isString(s) ? s.replace(/[A-Z]/g,
function (ch) {return fromCharCode(ch.charCodeAt(0) | 32); }) : s;
};
var manualUppercase = function (s) {
return isString(s) ? s.replace(/[a-z]/g,
function (ch) {return fromCharCode(ch.charCodeAt(0) & ~32); }) : s;
};
if ('i' !== 'I'.toLowerCase()) {
lowercase = manualLowercase;
uppercase = manulaUppercase;
}
function fromCharCode(code) { return String.fromCharCode(code); }
var _undefined = undefined,
_null = null,
$$element = '$element',
@@ -134,15 +153,26 @@ function isNumber(value){ return typeof value == $number;}
function isArray(value) { return value instanceof Array; }
function isFunction(value){ return typeof value == $function;}
function isTextNode(node) { return nodeName(node) == '#text'; }
function lowercase(value){ return isString(value) ? value.toLowerCase() : value; }
function uppercase(value){ return isString(value) ? value.toUpperCase() : value; }
function trim(value) { return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; }
function isElement(node) {
return node && (node.nodeName || node instanceof JQLite || (jQuery && node instanceof jQuery));
}
function HTML(html) {
/**
* HTML class which is the only class which can be used in ng:bind to inline HTML for security reasons.
* @constructor
* @param html raw (unsafe) html
* @param {string=} option if set to 'usafe' then get method will return raw (unsafe/unsanitized) html
*/
function HTML(html, option) {
this.html = html;
this.get = lowercase(option) == 'unsafe' ?
valueFn(html) :
function htmlSanitize() {
var buf = [];
htmlParser(html, htmlSanitizeWriter(buf));
return buf.join('');
};
}
if (msie) {
@@ -297,16 +327,6 @@ function setHtml(node, html) {
}
}
function escapeHtml(html) {
if (!html || !html.replace)
return html;
return html.
replace(/&/g, '&amp;').
replace(/</g, '&lt;').
replace(/>/g, '&gt;');
}
function isRenderableElement(element) {
var name = element && element[0] && element[0].nodeName;
return name && name.charAt(0) != '#' &&
@@ -328,13 +348,6 @@ function elementError(element, type, error) {
}
}
function escapeAttr(html) {
if (!html || !html.replace)
return html;
return html.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g,
'&quot;');
}
function concat(array1, array2, index) {
return array1.concat(slice.call(array2, index, array2.length));
}
+1 -1
View File
@@ -93,7 +93,7 @@ function lex(text, parseStringsForObjects){
}
function isWhitespace(ch) {
return ch == ' ' || ch == '\r' || ch == '\t' ||
ch == '\n' || ch == '\v';
ch == '\n' || ch == '\v' || ch == '\u00A0'; // IE treats non-breaking space as \u00A0
}
function isIdent(ch) {
return 'a' <= ch && ch <= 'z' ||
+1
View File
@@ -53,6 +53,7 @@
addScript("/parser.js");
addScript("/Resource.js");
addScript("/Browser.js");
addScript("/sanitizer.js");
addScript("/AngularPublic.js");
// Extension points
+6
View File
@@ -202,6 +202,12 @@ var angularString = {
}
return chars.join('');
},
/**
* Tries to convert input to date and if successful returns the date, otherwise returns the input.
* @param {string} string
* @return {(Date|string)}
*/
'toDate':function(string){
var match;
if (typeof string == 'string' &&
+27 -4
View File
@@ -26,15 +26,19 @@ angularDirective("ng:bind", function(expression){
return function(element) {
var lastValue = noop, lastError = noop;
this.$onEval(function() {
var error, value, isHtml, isDomElement,
var error, value, html, isHtml, isDomElement,
oldElement = this.hasOwnProperty($$element) ? this.$element : _undefined;
this.$element = element;
value = this.$tryEval(expression, function(e){
error = toJson(e);
});
this.$element = oldElement;
// If we are HTML than save the raw HTML data so that we don't
// recompute sanitization since it is expensive.
// TODO: turn this into a more generic way to compute this
if (isHtml = (value instanceof HTML))
value = (html = value).html;
if (lastValue === value && lastError == error) return;
isHtml = value instanceof HTML;
isDomElement = isElement(value);
if (!isHtml && !isDomElement && isObject(value)) {
value = toJson(value);
@@ -45,7 +49,7 @@ angularDirective("ng:bind", function(expression){
elementError(element, NG_EXCEPTION, error);
if (error) value = error;
if (isHtml) {
element.html(value.html);
element.html(html.get());
} else if (isDomElement) {
element.html('');
element.append(value);
@@ -205,7 +209,7 @@ angularWidget("@ng:repeat", function(expression, element){
*
* Events that are handled via these handler are always configured not to propagate further.
*
* TODO: maybe we should consider allowing users to control even propagation in the future.
* TODO: maybe we should consider allowing users to control event propagation in the future.
*/
angularDirective("ng:click", function(expression, element){
return function(element){
@@ -218,6 +222,25 @@ angularDirective("ng:click", function(expression, element){
};
});
/**
* Enables binding angular expressions to onsubmit events.
*
* Additionally it prevents the default action (which for form means sending the request to the
* server and reloading the current page).
*/
angularDirective("ng:submit", function(expression, element) {
return function(element) {
var self = this;
element.bind('submit', function(event) {
self.$tryEval(expression, element);
self.$root.$eval();
event.preventDefault();
});
};
});
angularDirective("ng:watch", function(expression, element){
return function(element){
var self = this;
+28 -11
View File
@@ -44,7 +44,7 @@ function padNumber(num, digits, trim) {
}
function dateGetter(name, size, offset, trim) {
return function(date) {
var value = date['get' + name].call(date);
var value = date['get' + name]();
if (offset > 0 || value > -offset)
value += offset;
if (value === 0 && offset == -12 ) value = 12;
@@ -73,9 +73,19 @@ var DATE_FORMATS = {
}
};
var DATE_FORMATS_SPLIT = /([^yMdHhmsaZ]*)(y+|M+|d+|H+|h+|m+|s+|a|Z)(.*)/;
var NUMBER_STRING = /^\d+$/;
angularFilter.date = function(date, format) {
if (!(date instanceof Date)) return date;
if (isString(date) && NUMBER_STRING.test(date)) {
date = parseInt(date, 10);
}
if (isNumber(date)) {
date = new Date(date);
} else if (!(date instanceof Date)) {
return date;
}
var text = date.toLocaleDateString(), fn;
if (format && isString(format)) {
text = '';
@@ -101,8 +111,12 @@ angularFilter.lowercase = lowercase;
angularFilter.uppercase = uppercase;
angularFilter.html = function(html){
return new HTML(html);
/**</>
* @exportedAs filter:html
* @param {string=} option if 'unsafe' then do not sanitize the HTML input
*/
angularFilter.html = function(html, option){
return new HTML(html, option);
};
angularFilter.linky = function(text){
@@ -114,15 +128,18 @@ angularFilter.linky = function(text){
var match;
var raw = text;
var html = [];
var writer = htmlSanitizeWriter(html);
var url;
var i;
while (match=raw.match(URL)) {
var url = match[0].replace(/[\.\;\,\(\)\{\}\<\>]$/,'');
var i = raw.indexOf(url);
html.push(escapeHtml(raw.substr(0, i)));
html.push('<a href="' + url + '">');
html.push(url);
html.push('</a>');
url = match[0].replace(/[\.\;\,\(\)\{\}\<\>]$/,'');
i = raw.indexOf(url);
writer.chars(raw.substr(0, i));
writer.start('a', {href:url});
writer.chars(url);
writer.end('a');
raw = raw.substring(i + url.length);
}
html.push(escapeHtml(raw));
writer.chars(raw);
return new HTML(html.join(''));
};
+290
View File
@@ -0,0 +1,290 @@
/*
* HTML Parser By Misko Hevery (misko@hevery.com)
* based on: HTML Parser By John Resig (ejohn.org)
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*
* // Use like so:
* htmlParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
*/
// Regular Expressions for parsing tags and attributes
var START_TAG_REGEXP = /^<\s*([\w:]+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,
END_TAG_REGEXP = /^<\s*\/\s*([\w:]+)[^>]*>/,
ATTR_REGEXP = /(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g,
BEGIN_TAG_REGEXP = /^</,
BEGING_END_TAGE_REGEXP = /^<\s*\//,
COMMENT_REGEXP = /<!--(.*?)-->/g,
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g;
// Empty Elements - HTML 4.01
var emptyElements = makeMap("area,base,basefont,br,col,hr,img,input,isindex,link,param");
// Block Elements - HTML 4.01
var blockElements = makeMap("address,blockquote,button,center,dd,del,dir,div,dl,dt,fieldset,"+
"form,hr,ins,isindex,li,map,menu,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul");
// Inline Elements - HTML 4.01
var inlineElements = makeMap("a,abbr,acronym,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,img,"+
"input,ins,kbd,label,map,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var");
// Elements that you can, intentionally, leave open
// (and which close themselves)
var closeSelfElements = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr");
// Attributes that have their values filled in disabled="disabled"
var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected");
// Special Elements (can contain anything)
var specialElements = makeMap("script,style");
var validElements = extend({}, emptyElements, blockElements, inlineElements, closeSelfElements);
var validAttrs = extend({}, fillAttrs, makeMap(
'abbr,align,alink,alt,archive,axis,background,bgcolor,border,cellpadding,cellspacing,cite,class,classid,clear,code,codebase,'+
'codetype,color,cols,colspan,content,coords,data,dir,face,for,headers,height,href,hreflang,hspace,id,label,lang,language,'+
'link,longdesc,marginheight,marginwidth,maxlength,media,method,name,nowrap,profile,prompt,rel,rev,rows,rowspan,rules,scheme,'+
'scope,scrolling,shape,size,span,src,standby,start,summary,tabindex,target,text,title,type,usemap,valign,value,valuetype,'+
'vlink,vspace,width'));
/**
* @example
* htmlParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
* @param {string} html string
* @param {object} handler
*/
var htmlParser = function( html, handler ) {
var index, chars, match, stack = [], last = html;
stack.last = function(){ return stack[ stack.length - 1 ]; };
while ( html ) {
chars = true;
// Make sure we're not in a script or style element
if ( !stack.last() || !specialElements[ stack.last() ] ) {
// Comment
if ( html.indexOf("<!--") === 0 ) {
index = html.indexOf("-->");
if ( index >= 0 ) {
if ( handler.comment )
handler.comment( html.substring( 4, index ) );
html = html.substring( index + 3 );
chars = false;
}
// end tag
} else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
match = html.match( END_TAG_REGEXP );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( END_TAG_REGEXP, parseEndTag );
chars = false;
}
// start tag
} else if ( BEGIN_TAG_REGEXP.test(html) ) {
match = html.match( START_TAG_REGEXP );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( START_TAG_REGEXP, parseStartTag );
chars = false;
}
}
if ( chars ) {
index = html.indexOf("<");
var text = index < 0 ? html : html.substring( 0, index );
html = index < 0 ? "" : html.substring( index );
if ( handler.chars )
handler.chars( text );
}
} else {
html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){
text = text.
replace(COMMENT_REGEXP, "$1").
replace(CDATA_REGEXP, "$1");
if ( handler.chars )
handler.chars( text );
return "";
});
parseEndTag( "", stack.last() );
}
if ( html == last ) {
throw "Parse Error: " + html;
}
last = html;
}
// Clean up any remaining tags
parseEndTag();
function parseStartTag( tag, tagName, rest, unary ) {
tagName = lowercase(tagName);
if ( blockElements[ tagName ] ) {
while ( stack.last() && inlineElements[ stack.last() ] ) {
parseEndTag( "", stack.last() );
}
}
if ( closeSelfElements[ tagName ] && stack.last() == tagName ) {
parseEndTag( "", tagName );
}
unary = emptyElements[ tagName ] || !!unary;
if ( !unary )
stack.push( tagName );
if ( handler.start ) {
var attrs = {};
rest.replace(ATTR_REGEXP, function(match, name) {
var value = arguments[2] ? arguments[2] :
arguments[3] ? arguments[3] :
arguments[4] ? arguments[4] :
fillAttrs[name] ? name : "";
attrs[name] = value; //value.replace(/(^|[^\\])"/g, '$1\\\"') //"
});
if ( handler.start )
handler.start( tagName, attrs, unary );
}
}
function parseEndTag( tag, tagName ) {
var pos = 0, i;
tagName = lowercase(tagName);
if ( tagName )
// Find the closest opened tag of the same type
for ( pos = stack.length - 1; pos >= 0; pos-- )
if ( stack[ pos ] == tagName )
break;
if ( pos >= 0 ) {
// Close all the open elements, up the stack
for ( i = stack.length - 1; i >= pos; i-- )
if ( handler.end )
handler.end( stack[ i ] );
// Remove the open elements from the stack
stack.length = pos;
}
}
};
/**
* @param str 'key1,key2,...'
* @returns {object} in the form of {key1:true, key2:true, ...}
*/
function makeMap(str){
var obj = {}, items = str.split(","), i;
for ( i = 0; i < items.length; i++ )
obj[ items[i] ] = true;
return obj;
}
/*
* For attack vectors see: http://ha.ckers.org/xss.html
*/
var JAVASCRIPT_URL = /^javascript:/i,
NBSP_REGEXP = /&nbsp;/gim,
HEX_ENTITY_REGEXP = /&#x([\da-f]*);?/igm,
DEC_ENTITY_REGEXP = /&#(\d+);?/igm,
CHAR_REGEXP = /[\w:]/gm,
HEX_DECODE = function(match, code){return fromCharCode(parseInt(code,16));},
DEC_DECODE = function(match, code){return fromCharCode(code);};
/**
* @param {string} url
* @returns true if url decodes to something which starts with 'javascript:' hence unsafe
*/
function isJavaScriptUrl(url) {
var chars = [];
url.replace(NBSP_REGEXP, '').
replace(HEX_ENTITY_REGEXP, HEX_DECODE).
replace(DEC_ENTITY_REGEXP, DEC_DECODE).
// Remove all non \w: characters, unfurtunetly value.replace(/[\w:]/,'') can be defeated using \u0000
replace(CHAR_REGEXP, function(ch){chars.push(ch);});
return JAVASCRIPT_URL.test(lowercase(chars.join('')));
}
/**
* create an HTML/XML writer which writes to buffer
* @param {Array} buf use buf.jain('') to get out sanitized html string
* @returns {object} in the form of {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* }
*/
function htmlSanitizeWriter(buf){
var ignore = false;
var out = bind(buf, buf.push);
return {
start: function(tag, attrs, unary){
tag = lowercase(tag);
if (!ignore && specialElements[tag]) {
ignore = tag;
}
if (!ignore && validElements[tag]) {
out('<');
out(tag);
foreach(attrs, function(value, key){
if (validAttrs[lowercase(key)] && !isJavaScriptUrl(value)) {
out(' ');
out(key);
out('="');
out(value.
replace(/</g, '&lt;').
replace(/>/g, '&gt;').
replace(/\"/g,'&quot;'));
out('"');
}
});
out(unary ? '/>' : '>');
}
},
end: function(tag){
tag = lowercase(tag);
if (!ignore && validElements[tag]) {
out('</');
out(tag);
out('>');
}
if (tag == ignore) {
ignore = false;
}
},
chars: function(chars){
if (!ignore) {
out(chars.
replace(/&(\w+[&;\W])?/g, function(match, entity){return entity?match:'&amp;';}).
replace(/</g, '&lt;').
replace(/>/g, '&gt;'));
}
}
};
}
+47 -14
View File
@@ -1,51 +1,84 @@
/**
* Represents the application currently being tested and abstracts usage
* of iframes or separate windows.
*
* @param {Object} context jQuery wrapper around HTML context.
*/
angular.scenario.Application = function(context) {
this.context = context;
context.append('<h2>Current URL: <a href="about:blank">None</a></h2>');
context.append(
'<h2>Current URL: <a href="about:blank">None</a></h2>' +
'<div id="test-frames"></div>'
);
};
/**
* Gets the jQuery collection of frames. Don't use this directly because
* frames may go stale.
*
* @private
* @return {Object} jQuery collection
*/
angular.scenario.Application.prototype.getFrame = function() {
return this.context.find('> iframe');
angular.scenario.Application.prototype.getFrame_ = function() {
return this.context.find('#test-frames iframe:last');
};
/**
* Gets the window of the test runner frame. Always favor executeAction()
* Gets the window of the test runner frame. Always favor executeAction()
* instead of this method since it prevents you from getting a stale window.
*
* @private
* @return {Object} the window of the frame
*/
angular.scenario.Application.prototype.getWindow = function() {
var contentWindow = this.getFrame().attr('contentWindow');
angular.scenario.Application.prototype.getWindow_ = function() {
var contentWindow = this.getFrame_().attr('contentWindow');
if (!contentWindow)
throw 'No window available because frame not loaded.';
throw 'Frame window is not accessible.';
return contentWindow;
};
/**
* Changes the location of the frame.
*
* @param {string} url The URL. If it begins with a # then only the
* hash of the page is changed.
* @param {Function} onloadFn function($window, $document)
*/
angular.scenario.Application.prototype.navigateTo = function(url, onloadFn) {
this.getFrame().remove();
this.context.append('<iframe src=""></iframe>');
var self = this;
var frame = this.getFrame_();
if (url.charAt(0) === '#') {
url = frame.attr('src').split('#')[0] + url;
frame.attr('src', url);
this.executeAction(onloadFn);
} else {
frame.css('display', 'none').attr('src', 'about:blank');
this.context.find('#test-frames').append('<iframe>');
frame = this.getFrame_();
frame.load(function() {
self.executeAction(onloadFn);
frame.unbind();
}).attr('src', url);
}
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.
* Executes a function in the context of the tested application. Will wait
* for all pending angular xhr requests before executing.
*
* @param {Function} The callback to execute. function($window, $document)
* @param {Function} action The callback to execute. function($window, $document)
* $document is a jQuery wrapped document.
*/
angular.scenario.Application.prototype.executeAction = function(action) {
var $window = this.getWindow();
return action.call(this, $window, _jQuery($window.document));
var self = this;
var $window = this.getWindow_();
if (!$window.angular) {
return action.call(this, $window, _jQuery($window.document));
}
var $browser = $window.angular.service.$browser();
$browser.poll();
$browser.notifyWhenNoOutstandingRequests(function() {
action.call(self, $window, _jQuery($window.document));
});
};
+54 -11
View File
@@ -1,8 +1,12 @@
/**
* The representation of define blocks. Don't used directly, instead use
* define() in your tests.
*
* @param {string} descName Name of the block
* @param {Object} parent describe or undefined if the root.
*/
angular.scenario.Describe = function(descName, parent) {
this.only = parent && parent.only;
this.beforeEachFns = [];
this.afterEachFns = [];
this.its = [];
@@ -10,7 +14,7 @@ angular.scenario.Describe = function(descName, parent) {
this.name = descName;
this.parent = parent;
this.id = angular.scenario.Describe.id++;
/**
* Calls all before functions.
*/
@@ -36,7 +40,7 @@ angular.scenario.Describe.id = 0;
/**
* Defines a block to execute before each it or nested describe.
*
* @param {Function} Body of the block.
* @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.beforeEach = function(body) {
this.beforeEachFns.push(body);
@@ -45,7 +49,7 @@ angular.scenario.Describe.prototype.beforeEach = function(body) {
/**
* Defines a block to execute after each it or nested describe.
*
* @param {Function} Body of the block.
* @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.afterEach = function(body) {
this.afterEachFns.push(body);
@@ -54,8 +58,8 @@ angular.scenario.Describe.prototype.afterEach = function(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.
* @param {string} name Name of the block. Appended to the parent block's name.
* @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.describe = function(name, body) {
var child = new angular.scenario.Describe(name, this);
@@ -63,6 +67,19 @@ angular.scenario.Describe.prototype.describe = function(name, body) {
body.call(child);
};
/**
* Same as describe() but makes ddescribe blocks the only to run.
*
* @param {string} name Name of the test.
* @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.ddescribe = function(name, body) {
var child = new angular.scenario.Describe(name, this);
child.only = true;
this.children.push(child);
body.call(child);
};
/**
* Use to disable a describe block.
*/
@@ -71,20 +88,31 @@ angular.scenario.Describe.prototype.xdescribe = angular.noop;
/**
* Defines a test.
*
* @param {String} Name of the test.
* @param {Function} Body of the block.
* @param {string} name Name of the test.
* @param {Function} vody Body of the block.
*/
angular.scenario.Describe.prototype.it = function(name, body) {
var self = this;
this.its.push({
definition: this,
only: this.only,
name: name,
before: self.setupBefore,
before: this.setupBefore,
body: body,
after: self.setupAfter
after: this.setupAfter
});
};
/**
* Same as it() but makes iit tests the only test to run.
*
* @param {string} name Name of the test.
* @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.iit = function(name, body) {
this.it.apply(this, arguments);
this.its[this.its.length-1].only = true;
};
/**
* Use to disable a test block.
*/
@@ -93,6 +121,15 @@ angular.scenario.Describe.prototype.xit = angular.noop;
/**
* Gets an array of functions representing all the tests (recursively).
* that can be executed with SpecRunner's.
*
* @return {Array<Object>} Array of it blocks {
* definition : Object // parent Describe
* only: boolean
* name: string
* before: Function
* body: Function
* after: Function
* }
*/
angular.scenario.Describe.prototype.getSpecs = function() {
var specs = arguments[0] || [];
@@ -102,5 +139,11 @@ angular.scenario.Describe.prototype.getSpecs = function() {
angular.foreach(this.its, function(it) {
specs.push(it);
});
return specs;
var only = [];
angular.foreach(specs, function(it) {
if (it.only) {
only.push(it);
}
});
return (only.length && only) || specs;
};
+5 -3
View File
@@ -1,9 +1,9 @@
/**
* A future action in a spec.
*
* @param {String} name of the future action
* @param {string} name of the future action
* @param {Function} future callback(error, result)
* @param {String} Optional. function that returns the file/line number.
* @param {Function} Optional. function that returns the file/line number.
*/
angular.scenario.Future = function(name, behavior, line) {
this.name = name;
@@ -17,7 +17,7 @@ angular.scenario.Future = function(name, behavior, line) {
/**
* Executes the behavior of the closure.
*
* @param {Function} Callback function(error, result)
* @param {Function} doneFn Callback function(error, result)
*/
angular.scenario.Future.prototype.execute = function(doneFn) {
var self = this;
@@ -37,6 +37,8 @@ angular.scenario.Future.prototype.execute = function(doneFn) {
/**
* Configures the future to convert it's final with a function fn(value)
*
* @param {Function} fn function(value) that returns the parsed value
*/
angular.scenario.Future.prototype.parsedWith = function(fn) {
this.parser = fn;
-244
View File
@@ -1,244 +0,0 @@
/**
* 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>'
);
};
/**
* The severity order of an error.
*/
angular.scenario.ui.Html.SEVERITY = ['pending', 'success', 'failure', 'error'];
/**
* 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 self = this;
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,
function(status) {
status = self.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 self = this;
var path = [];
var currentContext = this.context.find('#specs');
var currentDefinition = definition;
while (currentDefinition && currentDefinition.name) {
path.unshift(currentDefinition);
currentDefinition = currentDefinition.parent;
}
angular.foreach(path, function(defn) {
var id = 'describe-' + defn.id;
if (!self.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>'
);
self.context.find('#' + id).find('> h2').text('describe: ' + defn.name);
}
currentContext = self.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>' +
'<div class="scrollpane">' +
' <ol class="test-actions">' +
' </ol>' +
'</div>'
);
context.find('> .test-info').click(function() {
var scrollpane = context.find('> .scrollpane');
var actions = scrollpane.find('> .test-actions');
var name = context.find('> .test-info .test-name');
if (actions.find(':visible').length) {
actions.hide();
name.removeClass('open').addClass('closed');
} else {
actions.show();
scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
name.removeClass('closed').addClass('open');
}
});
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.
* @param {Function} function() that returns a string with the file/line number
* where the step was added from.
*/
angular.scenario.ui.Html.Spec.prototype.addStep = function(name, location) {
this.context.find('> .scrollpane .test-actions').append('<li class="status-pending"></li>');
var stepContext = this.context.find('> .scrollpane .test-actions li:last');
var self = this;
return new angular.scenario.ui.Html.Step(stepContext, name, location, function(status) {
if (indexOf(angular.scenario.ui.Html.SEVERITY, status) >
indexOf(angular.scenario.ui.Html.SEVERITY, self.status)) {
self.status = status;
}
var scrollpane = self.context.find('> .scrollpane');
scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
});
};
/**
* 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");
if (this.status === 'success') {
this.context.find('> .test-info .test-name').addClass('closed');
this.context.find('> .scrollpane .test-actions').hide();
}
};
/**
* Finishes the spec, possibly with an error.
*
* @param {Object} An optional error
*/
angular.scenario.ui.Html.Spec.prototype.finish = function() {
this.complete();
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.status = 'error';
this.context.append('<pre></pre>');
this.context.find('> pre').text(formatException(error));
this.finish();
};
/**
* 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} function() that returns file/line number of step.
* @param {Function} Callback function(status) to call when complete.
*/
angular.scenario.ui.Html.Step = function(context, name, location, doneFn) {
this.context = context;
this.name = name;
this.location = location;
this.startTime = new Date().getTime();
this.doneFn = doneFn;
context.append(
'<div class="timer-result"></div>' +
'<div class="test-title"></div>'
);
context.find('> .test-title').text(name);
var scrollpane = context.parents('.scrollpane');
scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
};
/**
* Completes the step and sets the timer value.
*/
angular.scenario.ui.Html.Step.prototype.complete = function(error) {
this.context.removeClass('status-pending');
var endTime = new Date().getTime();
this.context.find(".timer-result").
text((endTime - this.startTime) + "ms");
if (error) {
if (!this.context.find('.test-title pre').length) {
this.context.find('.test-title').append('<pre></pre>');
}
var message = _jQuery.trim(this.location() + '\n\n' + formatException(error));
this.context.find('.test-title pre').text(message);
}
};
/**
* Finishes the step, possibly with an error.
*
* @param {Object} An optional error
*/
angular.scenario.ui.Html.Step.prototype.finish = function(error) {
this.complete(error);
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(error);
this.context.addClass('status-error');
this.doneFn('error');
};
+153
View File
@@ -0,0 +1,153 @@
/**
* Maintains an object tree from the runner events.
*
* @param {Object} runner The scenario Runner instance to connect to.
*
* TODO(esprehn): Every output type creates one of these, but we probably
* want one glonal shared instance. Need to handle events better too
* so the HTML output doesn't need to do spec model.getSpec(spec.id)
* silliness.
*/
angular.scenario.ObjectModel = function(runner) {
var self = this;
this.specMap = {};
this.value = {
name: '',
children: {}
};
runner.on('SpecBegin', function(spec) {
var block = self.value;
angular.foreach(self.getDefinitionPath(spec), function(def) {
if (!block.children[def.name]) {
block.children[def.name] = {
id: def.id,
name: def.name,
children: {},
specs: {}
};
}
block = block.children[def.name];
});
self.specMap[spec.id] = block.specs[spec.name] =
new angular.scenario.ObjectModel.Spec(spec.id, spec.name);
});
runner.on('SpecError', function(spec, error) {
var it = self.getSpec(spec.id);
it.status = 'error';
it.error = error;
});
runner.on('SpecEnd', function(spec) {
var it = self.getSpec(spec.id);
complete(it);
});
runner.on('StepBegin', function(spec, step) {
var it = self.getSpec(spec.id);
it.steps.push(new angular.scenario.ObjectModel.Step(step.name));
});
runner.on('StepEnd', function(spec, step) {
var it = self.getSpec(spec.id);
if (it.getLastStep().name !== step.name)
throw 'Events fired in the wrong order. Step names don\' match.';
complete(it.getLastStep());
});
runner.on('StepFailure', function(spec, step, error) {
var it = self.getSpec(spec.id);
var item = it.getLastStep();
item.error = error;
if (!it.status) {
it.status = item.status = 'failure';
}
});
runner.on('StepError', function(spec, step, error) {
var it = self.getSpec(spec.id);
var item = it.getLastStep();
it.status = 'error';
item.status = 'error';
item.error = error;
});
function complete(item) {
item.endTime = new Date().getTime();
item.duration = item.endTime - item.startTime;
item.status = item.status || 'success';
}
};
/**
* Computes the path of definition describe blocks that wrap around
* this spec.
*
* @param spec Spec to compute the path for.
* @return {Array<Describe>} The describe block path
*/
angular.scenario.ObjectModel.prototype.getDefinitionPath = function(spec) {
var path = [];
var currentDefinition = spec.definition;
while (currentDefinition && currentDefinition.name) {
path.unshift(currentDefinition);
currentDefinition = currentDefinition.parent;
}
return path;
};
/**
* Gets a spec by id.
*
* @param {string} The id of the spec to get the object for.
* @return {Object} the Spec instance
*/
angular.scenario.ObjectModel.prototype.getSpec = function(id) {
return this.specMap[id];
};
/**
* A single it block.
*
* @param {string} id Id of the spec
* @param {string} name Name of the spec
*/
angular.scenario.ObjectModel.Spec = function(id, name) {
this.id = id;
this.name = name;
this.startTime = new Date().getTime();
this.steps = [];
};
/**
* Adds a new step to the Spec.
*
* @param {string} step Name of the step (really name of the future)
* @return {Object} the added step
*/
angular.scenario.ObjectModel.Spec.prototype.addStep = function(name) {
var step = new angular.scenario.ObjectModel.Step(name);
this.steps.push(step);
return step;
};
/**
* Gets the most recent step.
*
* @return {Object} the step
*/
angular.scenario.ObjectModel.Spec.prototype.getLastStep = function() {
return this.steps[this.steps.length-1];
};
/**
* A single step inside a Spec.
*
* @param {string} step Name of the step
*/
angular.scenario.ObjectModel.Step = function(name) {
this.name = name;
this.startTime = new Date().getTime();
};
+107 -21
View File
@@ -2,13 +2,16 @@
* Runner for scenarios.
*/
angular.scenario.Runner = function($window) {
this.listeners = [];
this.$window = $window;
this.rootDescribe = new angular.scenario.Describe();
this.currentDescribe = this.rootDescribe;
this.api = {
it: this.it,
iit: this.iit,
xit: angular.noop,
describe: this.describe,
ddescribe: this.ddescribe,
xdescribe: angular.noop,
beforeEach: this.beforeEach,
afterEach: this.afterEach
@@ -18,11 +21,42 @@ angular.scenario.Runner = function($window) {
}));
};
/**
* Emits an event which notifies listeners and passes extra
* arguments.
*
* @param {string} eventName Name of the event to fire.
*/
angular.scenario.Runner.prototype.emit = function(eventName) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
eventName = eventName.toLowerCase();
if (!this.listeners[eventName])
return;
angular.foreach(this.listeners[eventName], function(listener) {
listener.apply(self, args);
});
};
/**
* Adds a listener for an event.
*
* @param {string} eventName The name of the event to add a handler for
* @param {string} listener The fn(...) that takes the extra arguments from emit()
*/
angular.scenario.Runner.prototype.on = function(eventName, listener) {
eventName = eventName.toLowerCase();
this.listeners[eventName] = this.listeners[eventName] || [];
this.listeners[eventName].push(listener);
};
/**
* Defines a describe block of a spec.
*
* @param {String} Name of the block
* @param {Function} Body of the block
* @see Describe.js
*
* @param {string} name Name of the block
* @param {Function} body Body of the block
*/
angular.scenario.Runner.prototype.describe = function(name, body) {
var self = this;
@@ -37,20 +71,57 @@ angular.scenario.Runner.prototype.describe = function(name, body) {
});
};
/**
* Same as describe, but makes ddescribe the only blocks to run.
*
* @see Describe.js
*
* @param {string} name Name of the block
* @param {Function} body Body of the block
*/
angular.scenario.Runner.prototype.ddescribe = function(name, body) {
var self = this;
this.currentDescribe.ddescribe(name, function() {
var parentDescribe = self.currentDescribe;
self.currentDescribe = this;
try {
body.call(this);
} finally {
self.currentDescribe = parentDescribe;
}
});
};
/**
* Defines a test in a describe block of a spec.
*
* @param {String} Name of the block
* @param {Function} Body of the block
* @see Describe.js
*
* @param {string} name Name of the block
* @param {Function} body Body of the block
*/
angular.scenario.Runner.prototype.it = function(name, body) {
this.currentDescribe.it(name, body);
};
/**
* Same as it, but makes iit tests the only tests to run.
*
* @see Describe.js
*
* @param {string} name Name of the block
* @param {Function} body Body of the block
*/
angular.scenario.Runner.prototype.iit = function(name, body) {
this.currentDescribe.iit(name, body);
};
/**
* Defines a function to be called before each it block in the describe
* (and before all nested describes).
*
* @see Describe.js
*
* @param {Function} Callback to execute
*/
angular.scenario.Runner.prototype.beforeEach = function(body) {
@@ -61,6 +132,8 @@ angular.scenario.Runner.prototype.beforeEach = function(body) {
* Defines a function to be called after each it block in the describe
* (and before all nested describes).
*
* @see Describe.js
*
* @param {Function} Callback to execute
*/
angular.scenario.Runner.prototype.afterEach = function(body) {
@@ -68,24 +141,29 @@ angular.scenario.Runner.prototype.afterEach = function(body) {
};
/**
* Defines a function to be called before each it block in the describe
* (and before all nested describes).
* Creates a new spec runner.
*
* @param {Function} Callback to execute
* @private
* @param {Object} scope parent scope
*/
angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClass, specsDone) {
var $root = angular.scope({}, angular.service);
angular.scenario.Runner.prototype.createSpecRunner_ = function(scope) {
return scope.$new(angular.scenario.SpecRunner);
};
/**
* Runs all the loaded tests with the specified runner class on the
* provided application.
*
* @param {angular.scenario.Application} application App to remote control.
*/
angular.scenario.Runner.prototype.run = function(application) {
var self = this;
var specs = this.rootDescribe.getSpecs();
var $root = angular.scope(this);
$root.application = application;
$root.ui = ui;
$root.setTimeout = function() {
return self.$window.setTimeout.apply(self.$window, arguments);
};
asyncForEach(specs, function(spec, specDone) {
this.emit('RunnerBegin');
asyncForEach(this.rootDescribe.getSpecs(), function(spec, specDone) {
var dslCache = {};
var runner = angular.scope($root);
runner.$become(specRunnerClass);
var runner = self.createSpecRunner_($root);
angular.foreach(angular.scenario.dsl, function(fn, key) {
dslCache[key] = fn.call($root);
});
@@ -105,16 +183,24 @@ angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClas
// Make these methods work on the current chain
scope.addFuture = function() {
Array.prototype.push.call(arguments, line);
return specRunnerClass.prototype.addFuture.apply(scope, arguments);
return angular.scenario.SpecRunner.
prototype.addFuture.apply(scope, arguments);
};
scope.addFutureAction = function() {
Array.prototype.push.call(arguments, line);
return specRunnerClass.prototype.addFutureAction.apply(scope, arguments);
return angular.scenario.SpecRunner.
prototype.addFutureAction.apply(scope, arguments);
};
return scope.dsl[key].apply(scope, arguments);
};
});
runner.run(ui, spec, specDone);
}, specsDone || angular.noop);
runner.run(spec, specDone);
},
function(error) {
if (error) {
self.emit('RunnerError', error);
}
self.emit('RunnerEnd');
});
};
+98 -25
View File
@@ -6,8 +6,15 @@
// Public namespace
angular.scenario = angular.scenario || {};
// Namespace for the UI
angular.scenario.ui = angular.scenario.ui || {};
/**
* Defines a new output format.
*
* @param {string} name the name of the new output format
* @param {Function} fn function(context, runner) that generates the output
*/
angular.scenario.output = angular.scenario.output || function(name, fn) {
angular.scenario.output[name] = fn;
};
/**
* Defines a new DSL statement. If your factory function returns a Future
@@ -18,8 +25,8 @@ angular.scenario.ui = angular.scenario.ui || {};
* 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
* @param {string} name The name of the statement
* @param {Function} fn Factory function(), return a function for
* the statement.
*/
angular.scenario.dsl = angular.scenario.dsl || function(name, fn) {
@@ -54,8 +61,8 @@ angular.scenario.dsl = angular.scenario.dsl || function(name, fn) {
* 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).
* @param {string} name The name of the matcher
* @param {Function} fn The matching function(expected).
*/
angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {
angular.scenario.matcher[name] = function(expected) {
@@ -64,7 +71,7 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {
prefix += 'not ';
}
var self = this;
this.addFuture(prefix + name + ' ' + angular.toJson(expected),
this.addFuture(prefix + name + ' ' + angular.toJson(expected),
function(done) {
var error;
self.actual = self.future.value;
@@ -78,14 +85,56 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {
};
};
/**
* Initialization function for the scenario runner.
*
* @param {angular.scenario.Runner} $scenario The runner to setup
* @param {Object} config Config options
*/
function angularScenarioInit($scenario, config) {
var body = _jQuery(document.body);
var output = [];
if (config.scenario_output) {
output = config.scenario_output.split(',');
}
angular.foreach(angular.scenario.output, function(fn, name) {
if (!output.length || indexOf(output,name) != -1) {
var context = body.append('<div></div>').find('div:last');
context.attr('id', name);
fn.call({}, context, $scenario);
}
});
var appFrame = body.append('<div id="application"></div>').find('#application');
var application = new angular.scenario.Application(appFrame);
$scenario.on('RunnerEnd', function() {
appFrame.css('display', 'none');
appFrame.find('iframe').attr('src', 'about:blank');
});
$scenario.on('RunnerError', function(error) {
if (window.console) {
console.log(formatException(error));
} else {
// Do something for IE
alert(error);
}
});
$scenario.run(application);
}
/**
* 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.
* @param {Array} list list to iterate over
* @param {Function} iterator Callback function(value, continueFunction)
* @param {Function} done Callback function(error, result) called when
* iteration finishes or an error occurs.
*/
function asyncForEach(list, iterator, done) {
var i = 0;
@@ -110,8 +159,8 @@ function asyncForEach(list, iterator, done) {
* Formats an exception into a string with the stack trace, but limits
* to a specific line length.
*
* @param {Object} the exception to format, can be anything throwable
* @param {Number} Optional. max lines of the stack trace to include
* @param {Object} error The exception to format, can be anything throwable
* @param {Number} maxStackLines Optional. max lines of the stack trace to include
* default is 5.
*/
function formatException(error, maxStackLines) {
@@ -129,18 +178,20 @@ function formatException(error, maxStackLines) {
}
/**
* Returns a function that gets the file name and line number from a
* Returns a function that gets the file name and line number from a
* location in the stack if available based on the call site.
*
* Note: this returns another function because accessing .stack is very
* expensive in Chrome.
*
* @param {Number} offset Number of stack lines to skip
*/
function callerFile(offset) {
var error = new Error();
return function() {
var line = (error.stack || '').split('\n')[offset];
// Clean up the stack trace line
if (line) {
if (line.indexOf('@') !== -1) {
@@ -151,7 +202,7 @@ function callerFile(offset) {
line = line.substring(line.indexOf('(')+1).replace(')', '');
}
}
return line || '';
};
}
@@ -161,7 +212,7 @@ function callerFile(offset) {
* not specified.
*
* @param {Object} Either a wrapped jQuery/jqLite node or a DOMElement
* @param {String} Optional event type.
* @param {string} Optional event type.
*/
function browserTrigger(element, type) {
if (element && !element.nodeName) element = element[0];
@@ -188,7 +239,22 @@ function browserTrigger(element, type) {
type = 'change';
}
if (msie) {
switch(element.type) {
case 'radio':
case 'checkbox':
element.checked = !element.checked;
break;
}
element.fireEvent('on' + type);
if (lowercase(element.type) == 'submit') {
while(element) {
if (lowercase(element.nodeName) == 'form') {
element.fireEvent('onsubmit');
break;
}
element = element.parentNode;
}
}
} else {
var evnt = document.createEvent('MouseEvents');
evnt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element);
@@ -200,13 +266,20 @@ function browserTrigger(element, type) {
* Don't use the jQuery trigger method since it works incorrectly.
*
* jQuery notifies listeners and then changes the state of a checkbox and
* does not create a real browser event. A real click changes the state of
* does not create a real browser event. A real click changes the state of
* the checkbox and then notifies listeners.
*
*
* To work around this we instead use our own handler that fires a real event.
*/
_jQuery.fn.trigger = function(type) {
return this.each(function(index, node) {
browserTrigger(node, type);
});
};
(function(fn){
var parentTrigger = fn.trigger;
fn.trigger = function(type) {
if (/(click|change|keyup)/.test(type)) {
return this.each(function(index, node) {
browserTrigger(node, type);
});
}
return parentTrigger.apply(this, arguments);
};
})(_jQuery.fn);
+35 -22
View File
@@ -15,14 +15,16 @@ angular.scenario.SpecRunner = function() {
* 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 {Object} spec A spec object
* @param {Object} specDone 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) {
angular.scenario.SpecRunner.prototype.run = function(spec, specDone) {
var self = this;
var specUI = ui.addSpec(spec);
var count = 0;
this.spec = spec;
this.emit('SpecBegin', spec);
try {
spec.before.call(this);
@@ -30,7 +32,8 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
this.afterIndex = this.futures.length;
spec.after.call(this);
} catch (e) {
specUI.error(e);
this.emit('SpecError', spec, e);
this.emit('SpecEnd', spec);
specDone();
return;
}
@@ -42,32 +45,40 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
self.error = true;
done(null, self.afterIndex);
};
var spec = this;
asyncForEach(
this.futures,
function(future, futureDone) {
var stepUI = specUI.addStep(future.name, future.line);
self.step = future;
self.emit('StepBegin', spec, future);
try {
future.execute(function(error) {
stepUI.finish(error);
if (error) {
self.emit('StepFailure', spec, future, error);
self.emit('StepEnd', spec, future);
return handleError(error, futureDone);
}
spec.$window.setTimeout( function() { futureDone(); }, 0);
self.emit('StepEnd', spec, future);
if ((count++) % 10 === 0) {
self.$window.setTimeout(function() { futureDone(); }, 0);
} else {
futureDone();
}
});
} catch (e) {
stepUI.error(e);
self.emit('StepError', spec, future, e);
self.emit('StepEnd', spec, future);
handleError(e, futureDone);
}
},
function(e) {
if (e) {
specUI.error(e);
} else {
specUI.finish();
self.emit('SpecError', spec, e);
}
specDone();
self.emit('SpecEnd', spec);
// Call done in a timeout so exceptions don't recursively
// call this function
self.$window.setTimeout(function() { specDone(); }, 0);
}
);
};
@@ -77,9 +88,9 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
*
* Note: Do not pass line manually. It happens automatically.
*
* @param {String} Name of the future
* @param {Function} Behavior of the future
* @param {Function} fn() that returns file/line number
* @param {string} name Name of the future
* @param {Function} behavior Behavior of the future
* @param {Function} line fn() that returns file/line number
*/
angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line) {
var future = new angular.scenario.Future(name, angular.bind(this, behavior), line);
@@ -92,14 +103,16 @@ angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line)
*
* Note: Do not pass line manually. It happens automatically.
*
* @param {String} Name of the future
* @param {Function} Behavior of the future
* @param {Function} fn() that returns file/line number
* @param {string} name Name of the future
* @param {Function} behavior Behavior of the future
* @param {Function} line fn() that returns file/line number
*/
angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior, line) {
var self = this;
return this.addFuture(name, function(done) {
this.application.executeAction(function($window, $document) {
//TODO(esprehn): Refactor this so it doesn't need to be in here.
$document.elements = function(selector) {
var args = Array.prototype.slice.call(arguments, 1);
if (self.selector) {
@@ -1,6 +1,6 @@
(function(previousOnLoad){
var prefix = (function(){
var filename = /(.*\/)bootstrap.js(#(.*))?/;
var filename = /(.*\/)angular-bootstrap.js(#(.*))?/;
var scripts = document.getElementsByTagName("script");
for(var j = 0; j < scripts.length; j++) {
var src = scripts[j].src;
@@ -23,50 +23,36 @@
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);
}
}
});
angularScenarioInit($scenario, angularJsConfig(document));
};
addCSS("../../css/angular-scenario.css");
addScript("../../lib/jquery/jquery-1.4.2.js");
document.write(
'<script type="text/javascript">' +
'var _jQuery = jQuery.noConflict(true);' +
'</script>'
);
'<script type="text/javascript">' +
'var _jQuery = jQuery.noConflict(true);' +
'</script>'
);
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");
addScript("ObjectModel.js");
addScript("output/Html.js");
addScript("output/Json.js");
addScript("output/Object.js");
addScript("output/Xml.js");
// Create the runner (which also sets up the global API)
document.write(
'<script type="text/javascript">' +
'var $scenario = new angular.scenario.Runner(window);' +
'var $scenario = new angular.scenario.Runner(window, angular.scenario.SpecRunner);' +
'</script>'
);
+1 -1
View File
@@ -22,4 +22,4 @@
* THE SOFTWARE.
*/
(function(window, document, previousOnLoad){
var _jQuery = window.jQuery.noConflict(true);
var _jQuery = window.jQuery.noConflict(true);
+1 -19
View File
@@ -4,25 +4,7 @@
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);
}
}
});
angularScenarioInit($scenario, angularJsConfig(document));
};
})(window, document, window.onload);
+35 -21
View File
@@ -1,18 +1,19 @@
/**
* Shared DSL statements that are useful to all scenarios.
*/
/**
* Usage:
* wait() waits until you call resume() in the console
*/
angular.scenario.dsl('wait', function() {
angular.scenario.dsl('wait', function() {
return function() {
return this.addFuture('waiting for you to call resume() in the console', function(done) {
return this.addFuture('waiting for you to resume', function(done) {
this.emit('InteractiveWait', this.spec, this.step);
this.$window.resume = function() { done(); };
});
};
});
});
/**
* Usage:
@@ -21,7 +22,7 @@
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);
this.$window.setTimeout(function() { done(null, time * 1000); }, time * 1000);
});
};
});
@@ -49,8 +50,8 @@ angular.scenario.dsl('expect', function() {
/**
* Usage:
* navigateTo(future|string) where url a string or future with a value
* of a URL to navigate to
* navigateTo(url) Loads the url into the frame
* navigateTo(url, fn) where fn(url) is called and returns the URL to navigate to
*/
angular.scenario.dsl('navigateTo', function() {
return function(url, delegate) {
@@ -60,17 +61,7 @@ angular.scenario.dsl('navigateTo', function() {
url = delegate.call(this, url);
}
application.navigateTo(url, function() {
application.executeAction(function($window) {
if ($window.angular) {
var $browser = $window.angular.service.$browser();
$browser.poll();
$browser.notifyWhenNoOutstandingRequests(function() {
done(null, url);
});
} else {
done(null, url);
}
});
done(null, url);
});
});
};
@@ -162,7 +153,11 @@ angular.scenario.dsl('repeater', function() {
chain.count = function() {
return this.addFutureAction('repeater ' + this.selector + ' count', function($window, $document, done) {
done(null, $document.elements().size());
try {
done(null, $document.elements().length);
} catch (e) {
done(null, 0);
}
});
};
@@ -238,6 +233,7 @@ angular.scenario.dsl('select', function() {
/**
* Usage:
* element(selector).count() get the number of elements that match selector
* element(selector).click() clicks an element
* element(selector).attr(name) gets the value of an attribute
* element(selector).attr(name, value) sets the value of an attribute
@@ -248,10 +244,28 @@ angular.scenario.dsl('select', function() {
angular.scenario.dsl('element', function() {
var chain = {};
chain.count = function() {
return this.addFutureAction('element ' + this.selector + ' count', function($window, $document, done) {
try {
done(null, $document.elements().length);
} catch (e) {
done(null, 0);
}
});
};
chain.click = function() {
return this.addFutureAction('element ' + this.selector + ' click', function($window, $document, done) {
$document.elements().trigger('click');
done();
var elements = $document.elements();
var href = elements.attr('href');
elements.trigger('click');
if (href && elements[0].nodeName.toUpperCase() === 'A') {
this.application.navigateTo(href, function() {
done();
});
} else {
done();
}
});
};
+4
View File
@@ -6,6 +6,10 @@ angular.scenario.matcher('toEqual', function(expected) {
return angular.equals(this.actual, expected);
});
angular.scenario.matcher('toBe', function(expected) {
return this.actual === expected;
});
angular.scenario.matcher('toBeDefined', function() {
return angular.isDefined(this.actual);
});
+165
View File
@@ -0,0 +1,165 @@
/**
* User Interface for the Scenario Runner.
*
* TODO(esprehn): This should be refactored now that ObjectModel exists
* to use angular bindings for the UI.
*/
angular.scenario.output('html', function(context, runner) {
var model = new angular.scenario.ObjectModel(runner);
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>'
);
runner.on('InteractiveWait', function(spec, step) {
var ui = model.getSpec(spec.id).getLastStep().ui;
ui.find('.test-title').
html('waiting for you to <a href="javascript:resume()">resume</a>.');
});
runner.on('SpecBegin', function(spec) {
var ui = findContext(spec);
ui.find('> .tests').append(
'<li class="status-pending test-it"></li>'
);
ui = ui.find('> .tests li:last');
ui.append(
'<div class="test-info">' +
' <p class="test-title">' +
' <span class="timer-result"></span>' +
' <span class="test-name"></span>' +
' </p>' +
'</div>' +
'<div class="scrollpane">' +
' <ol class="test-actions"></ol>' +
'</div>'
);
ui.find('> .test-info .test-name').text(spec.name);
ui.find('> .test-info').click(function() {
var scrollpane = ui.find('> .scrollpane');
var actions = scrollpane.find('> .test-actions');
var name = context.find('> .test-info .test-name');
if (actions.find(':visible').length) {
actions.hide();
name.removeClass('open').addClass('closed');
} else {
actions.show();
scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
name.removeClass('closed').addClass('open');
}
});
model.getSpec(spec.id).ui = ui;
});
runner.on('SpecError', function(spec, error) {
var ui = model.getSpec(spec.id).ui;
ui.append('<pre></pre>');
ui.find('> pre').text(formatException(error));
});
runner.on('SpecEnd', function(spec) {
spec = model.getSpec(spec.id);
spec.ui.removeClass('status-pending');
spec.ui.addClass('status-' + spec.status);
spec.ui.find("> .test-info .timer-result").text(spec.duration + "ms");
if (spec.status === 'success') {
spec.ui.find('> .test-info .test-name').addClass('closed');
spec.ui.find('> .scrollpane .test-actions').hide();
}
updateTotals(spec.status);
});
runner.on('StepBegin', function(spec, step) {
spec = model.getSpec(spec.id);
step = spec.getLastStep();
spec.ui.find('> .scrollpane .test-actions').
append('<li class="status-pending"></li>');
step.ui = spec.ui.find('> .scrollpane .test-actions li:last');
step.ui.append(
'<div class="timer-result"></div>' +
'<div class="test-title"></div>'
);
step.ui.find('> .test-title').text(step.name);
var scrollpane = step.ui.parents('.scrollpane');
scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
});
runner.on('StepFailure', function(spec, step, error) {
var ui = model.getSpec(spec.id).getLastStep().ui;
addError(ui, step.line, error);
});
runner.on('StepError', function(spec, step, error) {
var ui = model.getSpec(spec.id).getLastStep().ui;
addError(ui, step.line, error);
});
runner.on('StepEnd', function(spec, step) {
spec = model.getSpec(spec.id);
step = spec.getLastStep();
step.ui.find('.timer-result').text(step.duration + 'ms');
step.ui.removeClass('status-pending');
step.ui.addClass('status-' + step.status);
var scrollpane = spec.ui.find('> .scrollpane');
scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
});
/**
* Finds the context of a spec block defined by the passed definition.
*
* @param {Object} The definition created by the Describe object.
*/
function findContext(spec) {
var currentContext = context.find('#specs');
angular.foreach(model.getDefinitionPath(spec), function(defn) {
var id = 'describe-' + defn.id;
if (!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>'
);
context.find('#' + id).find('> h2').text('describe: ' + defn.name);
}
currentContext = context.find('#' + id);
});
return context.find('#describe-' + spec.definition.id);
};
/**
* Updates the test counter for the status.
*
* @param {string} the status.
*/
function updateTotals(status) {
var legend = context.find('#status-legend .status-' + status);
var parts = legend.text().split(' ');
var value = (parts[0] * 1) + 1;
legend.text(value + ' ' + parts[1]);
}
/**
* Add an error to a step.
*
* @param {Object} The JQuery wrapped context
* @param {Function} fn() that should return the file/line number of the error
* @param {Object} the error.
*/
function addError(context, line, error) {
context.find('.test-title').append('<pre></pre>');
var message = _jQuery.trim(line() + '\n\n' + formatException(error));
context.find('.test-title pre:last').text(message);
};
});
+10
View File
@@ -0,0 +1,10 @@
/**
* Generates JSON output into a context.
*/
angular.scenario.output('json', function(context, runner) {
var model = new angular.scenario.ObjectModel(runner);
runner.on('RunnerEnd', function() {
context.text(angular.toJson(model.value));
});
});
+6
View File
@@ -0,0 +1,6 @@
/**
* Creates a global value $result with the result of the runner.
*/
angular.scenario.output('object', function(context, runner) {
runner.$window.$result = new angular.scenario.ObjectModel(runner).value;
});
+50
View File
@@ -0,0 +1,50 @@
/**
* Generates XML output into a context.
*/
angular.scenario.output('xml', function(context, runner) {
var model = new angular.scenario.ObjectModel(runner);
var $ = function(args) {return new context.init(args);};
runner.on('RunnerEnd', function() {
var scenario = $('<scenario></scenario>');
context.append(scenario);
serializeXml(scenario, model.value);
});
/**
* Convert the tree into XML.
*
* @param {Object} context jQuery context to add the XML to.
* @param {Object} tree node to serialize
*/
function serializeXml(context, tree) {
angular.foreach(tree.children, function(child) {
var describeContext = $('<describe></describe>');
describeContext.attr('id', child.id);
describeContext.attr('name', child.name);
context.append(describeContext);
serializeXml(describeContext, child);
});
var its = $('<its></its>');
context.append(its);
angular.foreach(tree.specs, function(spec) {
var it = $('<it></it>');
it.attr('id', spec.id);
it.attr('name', spec.name);
it.attr('duration', spec.duration);
it.attr('status', spec.status);
its.append(it);
angular.foreach(spec.steps, function(step) {
var stepContext = $('<step></step>');
stepContext.attr('name', step.name);
stepContext.attr('duration', step.duration);
stepContext.attr('status', step.status);
it.append(stepContext);
if (step.error) {
var error = $('<error></error');
stepContext.append(error);
error.text(formatException(stepContext.error));
}
});
});
}
});
+1 -1
View File
@@ -641,4 +641,4 @@ angularServiceInject('$cookieStore', function($store) {
}
};
}, ['$cookies'], EAGER_PUBLISHED);
}, ['$cookies']);
+9 -5
View File
@@ -203,10 +203,6 @@ function inputWidget(events, modelAccessor, viewAccessor, initFn) {
lastValue = model.get();
scope.$tryEval(action, element);
scope.$root.$eval();
// if we have noop initFn than we are just a button,
// therefore we want to prevent default action
if(initFn == noop)
event.preventDefault();
});
}
function updateView(){
@@ -256,11 +252,19 @@ angularWidget('ng:include', function(element){
return extend(function(xhr, element){
var scope = this, childScope;
var changeCounter = 0;
var preventRecursion = false;
function incrementChange(){ changeCounter++;}
this.$watch(srcExp, incrementChange);
this.$watch(scopeExp, incrementChange);
scope.$onEval(function(){
if (childScope) childScope.$eval();
if (childScope && !preventRecursion) {
preventRecursion = true;
try {
childScope.$eval();
} finally {
preventRecursion = false;
}
}
});
this.$watch(function(){return changeCounter;}, function(){
var src = this.$eval(srcExp),
+31 -1
View File
@@ -13,6 +13,15 @@ describe('Angular', function(){
});
});
describe('case', function(){
it('should change case', function(){
expect(lowercase('ABC90')).toEqual('abc90');
expect(manualLowercase('ABC90')).toEqual('abc90');
expect(uppercase('abc90')).toEqual('ABC90');
expect(manualUppercase('abc90')).toEqual('ABC90');
});
});
describe("copy", function(){
it("should return same object", function (){
var obj = {};
@@ -115,7 +124,7 @@ describe('toKeyValue', function() {
toEqual('escaped%20key=escaped%20value');
expect(toKeyValue({emptyKey: ''})).toEqual('emptyKey=');
});
it('should parse true values into flags', function() {
expect(toKeyValue({flag1: true, key: 'value', flag2: true})).toEqual('flag1&key=value&flag2');
});
@@ -187,6 +196,27 @@ describe ('rngScript', function() {
expect('my-angular-app-0.9.0-de0a8612.min.js'.match(rngScript)).toBeNull();
expect('foo/../my-angular-app-0.9.0-de0a8612.min.js'.match(rngScript)).toBeNull();
});
it('should match angular-scenario.js', function() {
expect('angular-scenario.js'.match(rngScript)).not.toBeNull();
expect('angular-scenario.min.js'.match(rngScript)).not.toBeNull();
expect('../angular-scenario.js'.match(rngScript)).not.toBeNull();
expect('foo/angular-scenario.min.js'.match(rngScript)).not.toBeNull();
});
it('should match angular-scenario-0.9.0(.min).js', function() {
expect('angular-scenario-0.9.0.js'.match(rngScript)).not.toBeNull();
expect('angular-scenario-0.9.0.min.js'.match(rngScript)).not.toBeNull();
expect('../angular-scenario-0.9.0.js'.match(rngScript)).not.toBeNull();
expect('foo/angular-scenario-0.9.0.min.js'.match(rngScript)).not.toBeNull();
});
it('should match angular-scenario-0.9.0-de0a8612(.min).js', function() {
expect('angular-scenario-0.9.0-de0a8612.js'.match(rngScript)).not.toBeNull();
expect('angular-scenario-0.9.0-de0a8612.min.js'.match(rngScript)).not.toBeNull();
expect('../angular-scenario-0.9.0-de0a8612.js'.match(rngScript)).not.toBeNull();
expect('foo/angular-scenario-0.9.0-de0a8612.min.js'.match(rngScript)).not.toBeNull();
});
});
+4
View File
@@ -189,6 +189,10 @@ describe('api', function(){
assertEquals(date.getTime(), angular.String.toDate(angular.Date.toString(date)).getTime());
});
it('UTCtoDate', function(){
expect(angular.String.toDate("2003-09-10T13:02:03Z")).toEqual(new Date("Sep 10 2003 13:02:03 GMT"));
});
it('StringFromUTC', function(){
var date = angular.String.toDate("2003-09-10T13:02:03Z");
assertEquals("date", angular.Object.typeOf(date));
+19 -23
View File
@@ -87,23 +87,15 @@ describe('filter', function(){
});
describe('date', function(){
var morning = angular.String.toDate('2010-09-03T23:05:08Z');
var midnight = angular.String.toDate('2010-09-03T23:05:08Z');
var noon = angular.String.toDate('2010-09-03T23:05:08Z');
morning.setHours(7);
noon.setHours(12);
midnight.setHours(0);
//butt-ugly hack: force the date to be 2pm PDT for locale testing
morning.getTimezoneOffset =
noon.getTimezoneOffset =
midnight.getTimezoneOffset =
function() { return 7 * 60; };
var morning = new TzDate(+5, '2010-09-03T12:05:08Z'); //7am
var noon = new TzDate(+5, '2010-09-03T17:05:08Z'); //12pm
var midnight = new TzDate(+5, '2010-09-03T05:05:08Z'); //12am
it('should ignore falsy inputs', function() {
expect(filter.date(null)).toEqual(null);
expect(filter.date('')).toEqual('');
expect(filter.date(123)).toEqual(123);
});
it('should do basic filter', function() {
@@ -111,19 +103,23 @@ describe('filter', function(){
expect(filter.date(noon, '')).toEqual(noon.toLocaleDateString());
});
it('should accept format', function() {
expect(filter.date(midnight, "yyyy-M-d h=H:m:saZ")).
toEqual('2010-9-3 12=0:5:8am0700');
expect(filter.date(midnight, "yyyy-MM-dd hh=HH:mm:ssaZ")).
toEqual('2010-09-03 12=00:05:08am0700');
expect(filter.date(noon, "yyyy-MM-dd hh=HH:mm:ssaZ")).
toEqual('2010-09-03 12=12:05:08pm0700');
it('should accept number or number string representing milliseconds as input', function() {
expect(filter.date(noon.getTime())).toEqual(noon.toLocaleDateString());
expect(filter.date(noon.getTime() + "")).toEqual(noon.toLocaleDateString());
});
it('should accept format', function() {
expect(filter.date(morning, "yy-MM-dd HH:mm:ss")).
toEqual('10-09-03 07:05:08');
expect(filter.date(midnight, "yyyy-M-d h=H:m:saZ")).
toEqual('2010-9-3 12=0:5:8am0500');
expect(filter.date(midnight, "yyyy-MM-dd hh=HH:mm:ssaZ")).
toEqual('2010-09-03 12=00:05:08am0500');
expect(filter.date(noon, "yyyy-MM-dd hh=HH:mm:ssaZ")).
toEqual('2010-09-03 12=12:05:08pm0500');
});
});
});
+102
View File
@@ -162,3 +162,105 @@ MockBrowser.prototype = {
angular.service('$browser', function(){
return new MockBrowser();
});
/**
* Mock of the Date type which has its timezone specified via constroctor arg.
*
* The main purpose is to create Date-like instances with timezone fixed to the specified timezone
* offset, so that we can test code that depends on local timezone settings without dependency on
* the time zone settings of the machine where the code is running.
*
* @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored)
* @param {(number|string)} timestamp Timestamp representing the desired time in *UTC*
*
* @example
* var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
* newYearInBratislava.getTimezoneOffset() => -60;
* newYearInBratislava.getFullYear() => 2010;
* newYearInBratislava.getMonth() => 0;
* newYearInBratislava.getDate() => 1;
* newYearInBratislava.getHours() => 0;
* newYearInBratislava.getMinutes() => 0;
*
*
* !!!! WARNING !!!!!
* This is not a complete Date object so only methods that were implemented can be called safely.
* To make matters worse, TzDate instances inherit stuff from Date via a prototype.
*
* We do our best to intercept calls to "unimplemented" methods, but since the list of methods is
* incomplete we might be missing some non-standard methods. This can result in errors like:
* "Date.prototype.foo called on incompatible Object".
*/
function TzDate(offset, timestamp) {
if (angular.isString(timestamp)) {
var tsStr = timestamp;
timestamp = angular.String.toDate(timestamp).getTime();
if (isNaN(timestamp))
throw {
name: "Illegal Argument",
message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string"
};
}
var localOffset = new Date(timestamp).getTimezoneOffset();
this.offsetDiff = localOffset*60*1000 - offset*1000*60*60;
this.date = new Date(timestamp + this.offsetDiff);
this.getTime = function() {
return this.date.getTime() - this.offsetDiff;
};
this.toLocaleDateString = function() {
return this.date.toLocaleDateString();
};
this.getFullYear = function() {
return this.date.getFullYear();
};
this.getMonth = function() {
return this.date.getMonth();
};
this.getDate = function() {
return this.date.getDate();
};
this.getHours = function() {
return this.date.getHours();
};
this.getMinutes = function() {
return this.date.getMinutes();
};
this.getSeconds = function() {
return this.date.getSeconds();
};
this.getTimezoneOffset = function() {
return offset * 60;
};
//hide all methods not implemented in this mock that the Date prototype exposes
var unimplementedMethods = ['getDay', 'getMilliseconds', 'getTime', 'getUTCDate', 'getUTCDay',
'getUTCFullYear', 'getUTCHours', 'getUTCMilliseconds', 'getUTCMinutes', 'getUTCMonth',
'getUTCSeconds', 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds',
'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear',
'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds',
'setYear', 'toDateString', 'toJSON', 'toGMTString', 'toLocaleFormat', 'toLocaleString',
'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf'];
angular.foreach(unimplementedMethods, function(methodName) {
this[methodName] = function() {
throw {
name: "MethodNotImplemented",
message: "Method '" + methodName + "' is not implemented in the TzDate mock"
};
};
});
}
//make "tzDateInstance instanceof Date" return true
TzDate.prototype = Date.prototype;
+99
View File
@@ -0,0 +1,99 @@
describe('TzDate', function() {
function minutes(min) {
return min*60*1000;
}
it('should take millis as constructor argument', function() {
expect(new TzDate(0, 0).getTime()).toBe(0);
expect(new TzDate(0, 1283555108000).getTime()).toBe(1283555108000);
});
it('should take dateString as constructor argument', function() {
expect(new TzDate(0, '1970-01-01T00:00:00Z').getTime()).toBe(0);
expect(new TzDate(0, '2010-09-03T23:05:08Z').getTime()).toBe(1283555108000);
});
it('should fake getLocalDateString method', function() {
//0 in -3h
var t0 = new TzDate(-3, 0);
expect(t0.toLocaleDateString()).toMatch('1970');
//0 in +0h
var t1 = new TzDate(0, 0);
expect(t1.toLocaleDateString()).toMatch('1970');
//0 in +3h
var t2 = new TzDate(3, 0);
expect(t2.toLocaleDateString()).toMatch('1969');
});
it('should fake getHours method', function() {
//0 in -3h
var t0 = new TzDate(-3, 0);
expect(t0.getHours()).toBe(3);
//0 in +0h
var t1 = new TzDate(0, 0);
expect(t1.getHours()).toBe(0);
//0 in +3h
var t2 = new TzDate(3, 0);
expect(t2.getHours()).toMatch(21);
});
it('should fake getMinutes method', function() {
//0:15 in -3h
var t0 = new TzDate(-3, minutes(15));
expect(t0.getMinutes()).toBe(15);
//0:15 in -3.25h
var t0a = new TzDate(-3.25, minutes(15));
expect(t0a.getMinutes()).toBe(30);
//0 in +0h
var t1 = new TzDate(0, minutes(0));
expect(t1.getMinutes()).toBe(0);
//0:15 in +0h
var t1a = new TzDate(0, minutes(15));
expect(t1a.getMinutes()).toBe(15);
//0:15 in +3h
var t2 = new TzDate(3, minutes(15));
expect(t2.getMinutes()).toMatch(15);
//0:15 in +3.25h
var t2a = new TzDate(3.25, minutes(15));
expect(t2a.getMinutes()).toMatch(0);
});
it('should fake getSeconds method', function() {
//0 in -3h
var t0 = new TzDate(-3, 0);
expect(t0.getSeconds()).toBe(0);
//0 in +0h
var t1 = new TzDate(0, 0);
expect(t1.getSeconds()).toBe(0);
//0 in +3h
var t2 = new TzDate(3, 0);
expect(t2.getSeconds()).toMatch(0);
});
it('should create a date representing new year in Bratislava', function() {
var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
expect(newYearInBratislava.getTimezoneOffset()).toBe(-60);
expect(newYearInBratislava.getFullYear()).toBe(2010);
expect(newYearInBratislava.getMonth()).toBe(0);
expect(newYearInBratislava.getDate()).toBe(1);
expect(newYearInBratislava.getHours()).toBe(0);
expect(newYearInBratislava.getMinutes()).toBe(0);
});
});
+28 -7
View File
@@ -50,11 +50,18 @@ describe("directives", function(){
it('should set html', function() {
var scope = compile('<div ng:bind="html|html"></div>');
scope.html = '<div>hello</div>';
scope.html = '<div unknown>hello</div>';
scope.$eval();
expect(lowercase(element.html())).toEqual('<div>hello</div>');
});
it('should set unsafe html', function() {
var scope = compile('<div ng:bind="html|html:\'unsafe\'"></div>');
scope.html = '<div onclick="">hello</div>';
scope.$eval();
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
});
it('should set element element', function() {
angularFilter.myElement = function() {
return jqLite('<a>hello</a>');
@@ -172,7 +179,7 @@ describe("directives", function(){
});
describe('ng:click', function(){
it('should fire event', function(){
it('should get called on a click', function(){
var scope = compile('<div ng:click="clicked = true"></div>');
scope.$eval();
expect(scope.$get('clicked')).toBeFalsy();
@@ -184,14 +191,28 @@ describe("directives", function(){
it('should stop event propagation', function() {
var scope = compile('<div ng:click="outer = true"><div ng:click="inner = true"></div></div>');
scope.$eval();
expect(scope.$get('outer')).not.toBeDefined();
expect(scope.$get('inner')).not.toBeDefined();
expect(scope.outer).not.toBeDefined();
expect(scope.inner).not.toBeDefined();
var innerDiv = jqLite(element.children()[0]);
var innerDiv = element.children()[0];
browserTrigger(innerDiv, 'click');
expect(scope.$get('outer')).not.toBeDefined();
expect(scope.$get('inner')).toEqual(true);
expect(scope.outer).not.toBeDefined();
expect(scope.inner).toEqual(true);
});
});
describe('ng:submit', function() {
it('should get called on form submit', function() {
var scope = compile('<form action="" ng:submit="submitted = true">' +
'<input type="submit"/>' +
'</form>');
scope.$eval();
expect(scope.submitted).not.toBeDefined();
browserTrigger(element.children()[0]);
expect(scope.submitted).toEqual(true);
});
});
+130
View File
@@ -0,0 +1,130 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Jasmine Test Runner</title>
<link rel="stylesheet" type="text/css" href="../lib/jasmine-1.0.1/jasmine.css">
<script type="text/javascript" src="../lib/jasmine-1.0.1/jasmine.js"></script>
<script type="text/javascript" src="../lib/jasmine-1.0.1/jasmine-html.js"></script>
<!-- include source files here... -->
<script type="text/javascript" src="../lib/jquery/jquery-1.4.2.js"></script>
<script type="text/javascript"> var _jQuery = $;</script>
<script type="text/javascript" src="../src/Angular.js"></script>
<script type="text/javascript" src="../src/JSON.js"></script>
<script type="text/javascript" src="../src/Compiler.js"></script>
<script type="text/javascript" src="../src/Scope.js"></script>
<script type="text/javascript" src="../src/Injector.js"></script>
<script type="text/javascript" src="../src/jqLite.js"></script>
<script type="text/javascript" src="../src/parser.js"></script>
<script type="text/javascript" src="../src/Resource.js"></script>
<script type="text/javascript" src="../src/Browser.js"></script>
<script type="text/javascript" src="../src/AngularPublic.js"></script>
<script type="text/javascript" src="../src/services.js"></script>
<script type="text/javascript" src="../src/apis.js"></script>
<script type="text/javascript" src="../src/filters.js"></script>
<script type="text/javascript" src="../src/formatters.js"></script>
<script type="text/javascript" src="../src/validators.js"></script>
<script type="text/javascript" src="../src/directives.js"></script>
<script type="text/javascript" src="../src/markups.js"></script>
<script type="text/javascript" src="../src/widgets.js"></script>
<script type="text/javascript" src="../src/scenario/Scenario.js"></script>
<script type="text/javascript" src="../src/scenario/Application.js"></script>
<script type="text/javascript" src="../src/scenario/Describe.js"></script>
<script type="text/javascript" src="../src/scenario/Future.js"></script>
<script type="text/javascript" src="../src/scenario/HtmlUI.js"></script>
<script type="text/javascript" src="../src/scenario/Runner.js"></script>
<script type="text/javascript" src="../src/scenario/SpecRunner.js"></script>
<script type="text/javascript" src="../src/scenario/dsl.js"></script>
<script type="text/javascript" src="../src/scenario/matchers.js"></script>
<script type="text/javascript" src="../src/scenario/ObjectModel.js"></script>
<script type="text/javascript" src="../src/scenario/output/Html.js"></script>
<script type="text/javascript" src="../src/scenario/output/Object.js"></script>
<script type="text/javascript" src="../src/scenario/output/Json.js"></script>
<script type="text/javascript" src="../src/scenario/output/Xml.js"></script>
<script type="text/javascript" src="angular-mocks.js"></script>
<script type="text/javascript" src="../test/scenario/mocks.js"></script>
<script type="text/javascript" src="testabilityPatch.js"></script>
<!-- include spec files here... -->
<script type="text/javascript">
describe('manual', function(){
var compile, model, element;
beforeEach(function() {
var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget);
compile = function(html) {
element = jqLite(html);
model = compiler.compile(element)(element);
model.$init();
return model;
};
});
it('should get called on form submit', function() {
var scope = compile('<form action="" ng:submit="submitted = true">' +
'<input type="submit"/>' +
'</form>');
scope.$eval();
expect(scope.submitted).not.toBeDefined();
browserTrigger(element.children()[0]);
expect(scope.submitted).toEqual(true);
});
});
describe('angular.scenario.output.json', function() {
var output, context;
var runner, $window;
var spec, step;
beforeEach(function() {
$window = {};
context = _jQuery('<div>text</div>');
$(document.body).append(context);
runner = new angular.scenario.testing.MockRunner();
output = angular.scenario.output.xml(context, runner);
spec = {
name: 'test spec',
definition: {
id: 10,
name: 'describe'
}
};
step = {
name: 'some step',
line: function() { return 'unknown:-1'; }
};
});
it('should create XML nodes for object model', function() {
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
runner.emit('RunnerEnd');
expect(_jQuery(context).find('it').attr('status')).toEqual('success');
expect(_jQuery(context).find('it step').attr('status')).toEqual('success');
});
});
</script>
</head>
<body>
<script type="text/javascript">
jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
function run(){
jasmine.getEnv().execute();
}
run();
</script>
</body>
</html>
+154
View File
@@ -0,0 +1,154 @@
describe('HTML', function(){
function expectHTML(html) {
return expect(new HTML(html).get());
}
it('should echo html', function(){
expectHTML('hello<b class="1\'23" align=\'""\'>world</b>.').
toEqual('hello<b class="1\'23" align="&quot;&quot;">world</b>.');
});
it('should remove script', function(){
expectHTML('a<SCRIPT>evil< / scrIpt >c.').toEqual('ac.');
});
it('should remove nested script', function(){
expectHTML('a< SCRIPT >A< SCRIPT >evil< / scrIpt >B< / scrIpt >c.').toEqual('ac.');
});
it('should remove attrs', function(){
expectHTML('a<div style="abc">b</div>c').toEqual('a<div>b</div>c');
});
it('should remove style', function(){
expectHTML('a<STyle>evil</stYle>c.').toEqual('ac.');
});
it('should remove script and style', function(){
expectHTML('a<STyle>evil<script></script></stYle>c.').toEqual('ac.');
});
it('should remove double nested script', function(){
expectHTML('a<SCRIPT>ev<script>evil</sCript>il</scrIpt>c.').toEqual('ac.');
});
it('should remove unknown tag names', function(){
expectHTML('a<xxx><B>b</B></xxx>c').toEqual('a<b>b</b>c');
});
it('should remove unsafe value', function(){
expectHTML('<a href="javascript:alert()">').toEqual('<a></a>');
});
it('should handle self closed elements', function(){
expectHTML('a<hr/>c').toEqual('a<hr/>c');
});
it('should handle namespace', function(){
expectHTML('a<my:hr/><my:div>b</my:div>c').toEqual('abc');
});
it('should handle improper html', function(){
expectHTML('< div id="</div>" alt=abc href=\'"\' >text< /div>').
toEqual('<div id="&lt;/div&gt;" alt="abc" href="&quot;">text</div>');
});
it('should handle improper html2', function(){
expectHTML('< div id="</div>" / >').
toEqual('<div id="&lt;/div&gt;"/>');
});
describe('htmlSanitizerWriter', function(){
var writer, html;
beforeEach(function(){
html = '';
writer = htmlSanitizeWriter({push:function(text){html+=text;}});
});
it('should write basic HTML', function(){
writer.chars('before');
writer.start('div', {id:'123'}, false);
writer.chars('in');
writer.end('div');
writer.chars('after');
expect(html).toEqual('before<div id="123">in</div>after');
});
it('should escape text nodes', function(){
writer.chars('a<div>&</div>c');
expect(html).toEqual('a&lt;div&gt;&amp;&lt;/div&gt;c');
});
it('should not double escape entities', function(){
writer.chars('&nbsp;&gt;&lt;');
expect(html).toEqual('&nbsp;&gt;&lt;');
});
it('should escape IE script', function(){
writer.chars('&{}');
expect(html).toEqual('&amp;{}');
});
it('should escape attributes', function(){
writer.start('div', {id:'\"\'<>'});
expect(html).toEqual('<div id="&quot;\'&lt;&gt;">');
});
it('should ignore missformed elements', function(){
writer.start('d>i&v', {});
expect(html).toEqual('');
});
it('should ignore unknown attributes', function(){
writer.start('div', {unknown:""});
expect(html).toEqual('<div>');
});
describe('javascript URL attribute', function(){
beforeEach(function(){
this.addMatchers({
toBeValidUrl: function(){
return !isJavaScriptUrl(this.actual);
}
});
});
it('should ignore javascript:', function(){
expect('JavaScript:abc').not.toBeValidUrl();
expect(' \n Java\n Script:abc').not.toBeValidUrl();
expect('JavaScript/my.js').toBeValidUrl();
});
it('should ignore dec encoded javascript:', function(){
expect('&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;').not.toBeValidUrl();
expect('&#106&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;').not.toBeValidUrl();
expect('&#106 &#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;').not.toBeValidUrl();
});
it('should ignore decimal with leading 0 encodede javascript:', function(){
expect('&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058').not.toBeValidUrl();
expect('&#0000106 &#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058').not.toBeValidUrl();
expect('&#0000106; &#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058').not.toBeValidUrl();
});
it('should ignore hex encoded javascript:', function(){
expect('&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A;').not.toBeValidUrl();
expect('&#x6A;&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A;').not.toBeValidUrl();
expect('&#x6A &#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A;').not.toBeValidUrl();
});
it('should ignore hex encoded whitespace javascript:', function(){
expect('jav&#x09;ascript:alert("A");').not.toBeValidUrl();
expect('jav&#x0A;ascript:alert("B");').not.toBeValidUrl();
expect('jav&#x0A ascript:alert("C");').not.toBeValidUrl();
expect('jav\u0000ascript:alert("D");').not.toBeValidUrl();
expect('java\u0000\u0000script:alert("D");').not.toBeValidUrl();
expect(' &#14; java\u0000\u0000script:alert("D");').not.toBeValidUrl();
});
});
});
});
+66 -26
View File
@@ -7,10 +7,11 @@ describe('angular.scenario.Application', function() {
});
it('should return new $window and $document after navigate', function() {
var called;
var testWindow, testDocument, counter = 0;
app.navigateTo = noop;
app.getWindow = function() {
return {x:counter++, document:{x:counter++}};
app.getWindow_ = function() {
return {x:counter++, document:{x:counter++}};
};
app.navigateTo('http://www.google.com/');
app.executeAction(function($document, $window) {
@@ -21,56 +22,95 @@ describe('angular.scenario.Application', function() {
app.executeAction(function($window, $document) {
expect($window).not.toEqual(testWindow);
expect($document).not.toEqual(testDocument);
called = true;
});
expect(called).toBeTruthy();
});
it('should execute callback with correct arguments', function() {
var called;
var testWindow = {document: {}};
app.getWindow = function() {
return testWindow;
app.getWindow_ = function() {
return testWindow;
};
app.executeAction(function($window, $document) {
expect(this).toEqual(app);
expect($document).toEqual(_jQuery($window.document));
expect($window).toEqual(testWindow);
called = true;
});
expect(called).toBeTruthy();
});
it('should create a new iframe each time', function() {
it('should use a new iframe each time', function() {
app.navigateTo('about:blank');
var frame = app.getFrame();
var frame = app.getFrame_();
frame.attr('test', true);
app.navigateTo('about:blank');
expect(app.getFrame().attr('test')).toBeFalsy();
expect(app.getFrame_().attr('test')).toBeFalsy();
});
it('should URL description bar', function() {
it('should hide old iframes and navigate to about:blank', function() {
app.navigateTo('about:blank#foo');
app.navigateTo('about:blank#bar');
var iframes = frames.find('iframe');
expect(iframes.length).toEqual(2);
expect(iframes[0].src).toEqual('about:blank');
expect(iframes[1].src).toEqual('about:blank#bar');
expect(_jQuery(iframes[0]).css('display')).toEqual('none');
});
it('should URL update 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.getWindow_ = function() {
return {};
};
app.navigateTo('about:blank', function() {
app.navigateTo('about:blank', function($window, $document) {
called = true;
});
var handlers = app.getFrame_().data('events').load;
expect(handlers).toBeDefined();
expect(handlers.length).toEqual(1);
handlers[0].handler();
expect(called).toBeTruthy();
});
it('should wait for pending requests in executeAction', function() {
var called, polled;
var handlers = [];
var testWindow = {
document: _jQuery('<div class="test-foo"></div>'),
angular: {
service: {}
}
};
testWindow.angular.service.$browser = function() {
return {
poll: function() {
polled = true;
},
notifyWhenNoOutstandingRequests: function(fn) {
handlers.push(fn);
}
}
};
app.getWindow_ = function() {
return testWindow;
};
app.executeAction(function($window, $document) {
expect($window).toEqual(testWindow);
expect($document).toBeDefined();
expect($document[0].className).toEqual('test-foo');
});
expect(polled).toBeTruthy();
expect(handlers.length).toEqual(1);
handlers[0]();
});
});
+32 -11
View File
@@ -4,21 +4,21 @@ describe('angular.scenario.Describe', function() {
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 = function(text) {
log.text = log.text + text;
};
log.fn = function(text) {
return function(done){
log(text);
(done || angular.noop)();
return function(done){
log(text);
(done || angular.noop)();
};
};
log.reset = function() {
log.text = '';
log.reset = function() {
log.text = '';
};
log.reset();
});
@@ -50,7 +50,7 @@ describe('angular.scenario.Describe', function() {
specs[1].after();
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);
@@ -65,7 +65,7 @@ describe('angular.scenario.Describe', function() {
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);
@@ -79,7 +79,28 @@ describe('angular.scenario.Describe', function() {
var specs = root.getSpecs();
expect(specs.length).toEqual(0);
});
it('should only return iit and ddescribe if present', function() {
root.describe('A', function() {
this.it('1', angular.noop);
this.iit('2', angular.noop);
this.describe('B', function() {
this.it('3', angular.noop);
this.ddescribe('C', function() {
this.it('4', angular.noop);
this.describe('D', function() {
this.it('5', angular.noop);
});
});
});
});
var specs = root.getSpecs();
expect(specs.length).toEqual(3);
expect(specs[0].name).toEqual('5');
expect(specs[1].name).toEqual('4');
expect(specs[2].name).toEqual('2');
});
it('should create uniqueIds in the tree', function() {
angular.scenario.Describe.id = 0;
var a = new angular.scenario.Describe();
+9 -9
View File
@@ -1,6 +1,6 @@
describe('angular.scenario.Future', function() {
var future;
it('should set the sane defaults', function() {
var behavior = function() {};
var future = new angular.scenario.Future('test name', behavior, 'foo');
@@ -11,7 +11,7 @@ describe('angular.scenario.Future', function() {
expect(future.fulfilled).toBeFalsy();
expect(future.parser).toEqual(angular.identity);
});
it('should be fulfilled after execution and done callback', function() {
var future = new angular.scenario.Future('test name', function(done) {
done();
@@ -19,7 +19,7 @@ describe('angular.scenario.Future', function() {
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);
@@ -29,7 +29,7 @@ describe('angular.scenario.Future', function() {
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);
@@ -37,7 +37,7 @@ describe('angular.scenario.Future', function() {
future.execute(angular.noop);
expect(future.value).toEqual(10);
});
it('should parse json with fromJson', function() {
var future = new angular.scenario.Future('test name', function(done) {
done(null, "{test: 'foo'}");
@@ -45,7 +45,7 @@ describe('angular.scenario.Future', function() {
future.fromJson().execute(angular.noop);
expect(future.value).toEqual({test: 'foo'});
});
it('should convert to json with toJson', function() {
var future = new angular.scenario.Future('test name', function(done) {
done(null, {test: 'foo'});
@@ -53,7 +53,7 @@ describe('angular.scenario.Future', function() {
future.toJson().execute(angular.noop);
expect(future.value).toEqual('{"test":"foo"}');
});
it('should convert with custom parser', function() {
var future = new angular.scenario.Future('test name', function(done) {
done(null, 'foo');
@@ -63,7 +63,7 @@ describe('angular.scenario.Future', function() {
}).execute(angular.noop);
expect(future.value).toEqual('FOO');
});
it('should pass error if parser fails', function() {
var future = new angular.scenario.Future('test name', function(done) {
done(null, '{');
@@ -71,5 +71,5 @@ describe('angular.scenario.Future', function() {
future.fromJson().execute(function(error, result) {
expect(error).toBeDefined();
});
});
});
});
-98
View File
@@ -1,98 +0,0 @@
describe('angular.scenario.HtmlUI', function() {
var ui;
var context;
var spec;
function line() { return 'unknown:-1'; }
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');
// Failure
specUI = ui.addSpec(spec);
specUI.addStep('some step', line).finish('failure');
specUI.finish();
// Failure
specUI = ui.addSpec(spec);
specUI.addStep('some step', line).finish('failure');
specUI.finish();
// Failure
specUI = ui.addSpec(spec);
specUI.addStep('some step', line).finish('failure');
specUI.finish();
// Success
specUI = ui.addSpec(spec);
specUI.addStep('some step', line).finish();
specUI.finish();
// Success
specUI = ui.addSpec(spec);
specUI.addStep('another step', line).finish();
specUI.finish();
expect(parseInt(context.find('#status-legend .status-failure').text(), 10)).
toEqual(3);
expect(parseInt(context.find('#status-legend .status-success').text(), 10)).
toEqual(2);
expect(parseInt(context.find('#status-legend .status-error').text(), 10)).
toEqual(1);
});
it('should update timer when test completes', function() {
// Success
specUI = ui.addSpec(spec);
specUI.addStep('some step', line).finish();
specUI.finish();
// Failure
specUI = ui.addSpec(spec);
specUI.addStep('some step', line).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$/);
});
});
it('should include line if provided', function() {
specUI = ui.addSpec(spec);
specUI.addStep('some step', line).finish('error!');
specUI.finish();
var errorHtml = context.find('#describe-10 .tests li pre').html();
expect(errorHtml.indexOf('unknown:-1')).toEqual(0);
});
});
+112
View File
@@ -0,0 +1,112 @@
describe('angular.scenario.ObjectModel', function() {
var model;
var runner;
var spec, step;
beforeEach(function() {
spec = {
name: 'test spec',
definition: {
id: 10,
name: 'describe 1'
}
};
step = {
name: 'test step',
line: function() { return ''; }
};
runner = new angular.scenario.testing.MockRunner();
model = new angular.scenario.ObjectModel(runner);
});
it('should value default empty value', function() {
expect(model.value).toEqual({
name: '',
children: []
});
});
it('should add spec and create describe blocks on SpecBegin event', function() {
runner.emit('SpecBegin', {
name: 'test spec',
definition: {
id: 10,
name: 'describe 2',
parent: {
id: 12,
name: 'describe 1'
}
}
});
expect(model.value.children['describe 1']).toBeDefined();
expect(model.value.children['describe 1'].children['describe 2']).toBeDefined();
expect(model.value.children['describe 1'].children['describe 2'].specs['test spec']).toBeDefined();
});
it('should add step to spec on StepBegin', function() {
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
expect(model.value.children['describe 1'].specs['test spec'].steps.length).toEqual(1);
});
it('should update spec timer duration on SpecEnd event', function() {
runner.emit('SpecBegin', spec);
runner.emit('SpecEnd', spec);
expect(model.value.children['describe 1'].specs['test spec'].duration).toBeDefined();
});
it('should update step timer duration on StepEnd event', function() {
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
expect(model.value.children['describe 1'].specs['test spec'].steps[0].duration).toBeDefined();
});
it('should set spec status on SpecEnd to success if no status set', function() {
runner.emit('SpecBegin', spec);
runner.emit('SpecEnd', spec);
expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('success');
});
it('should set status to error after SpecError', function() {
runner.emit('SpecBegin', spec);
runner.emit('SpecError', spec, 'error');
expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('error');
});
it('should set spec status to failure if step fails', function() {
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepEnd', spec, step);
runner.emit('StepBegin', spec, step);
runner.emit('StepFailure', spec, step, 'error');
runner.emit('StepEnd', spec, step);
runner.emit('StepBegin', spec, step);
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('failure');
});
it('should set spec status to error if step errors', function() {
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepError', spec, step, 'error');
runner.emit('StepEnd', spec, step);
runner.emit('StepBegin', spec, step);
runner.emit('StepFailure', spec, step, 'error');
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('error');
});
});
+14 -9
View File
@@ -2,7 +2,7 @@
* Mock spec runner.
*/
function MockSpecRunner() {}
MockSpecRunner.prototype.run = function(ui, spec, specDone) {
MockSpecRunner.prototype.run = function(spec, specDone) {
spec.before.call(this);
spec.body.call(this);
spec.after.call(this);
@@ -16,7 +16,7 @@ MockSpecRunner.prototype.addFuture = function(name, fn, line) {
describe('angular.scenario.Runner', function() {
var $window;
var runner;
beforeEach(function() {
// Trick to get the scope out of a DSL statement
angular.scenario.dsl('dslAddFuture', function() {
@@ -41,13 +41,18 @@ describe('angular.scenario.Runner', function() {
location: {}
};
runner = new angular.scenario.Runner($window);
runner.createSpecRunner_ = function(scope) {
return scope.$new(MockSpecRunner);
};
runner.on('SpecError', rethrow);
runner.on('StepError', rethrow);
});
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;
@@ -57,7 +62,7 @@ describe('angular.scenario.Runner', function() {
expect(angular.isFunction(func)).toBeTruthy();
});
});
it('should construct valid describe trees with public API', function() {
var before = [];
var after = [];
@@ -85,16 +90,16 @@ describe('angular.scenario.Runner', function() {
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();
});
});
runner.run(null/*ui*/, null/*application*/, MockSpecRunner, rethrow);
runner.run(null/*application*/);
});
it('should create a new scope for each DSL chain', function() {
$window.describe('describe', function() {
$window.it('1', function() {
@@ -107,6 +112,6 @@ describe('angular.scenario.Runner', function() {
expect(scope.chained).toEqual(2);
});
});
runner.run(null/*ui*/, null/*application*/, MockSpecRunner, rethrow);
runner.run(null/*application*/);
});
});
+52 -81
View File
@@ -1,39 +1,3 @@
/**
* 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
*/
@@ -47,9 +11,9 @@ ApplicationMock.prototype = {
};
describe('angular.scenario.SpecRunner', function() {
var $window;
var $window, $root, log;
var runner;
function createSpec(name, body) {
return {
name: name,
@@ -60,14 +24,22 @@ describe('angular.scenario.SpecRunner', function() {
}
beforeEach(function() {
log = [];
$window = {};
$window.setTimeout = function(fn, timeout) {
fn();
};
runner = angular.scope();
runner.application = new ApplicationMock($window);
runner.$window = $window;
runner.$become(angular.scenario.SpecRunner);
$root = angular.scope({
emit: function(eventName) {
log.push(eventName);
},
on: function(eventName) {
log.push('Listener Added for ' + eventName);
}
});
$root.application = new ApplicationMock($window);
$root.$window = $window;
runner = $root.$new(angular.scenario.SpecRunner);
});
it('should bind futures to the spec', function() {
@@ -92,84 +64,82 @@ describe('angular.scenario.SpecRunner', function() {
it('should execute spec function and notify UI', function() {
var finished;
var ui = new UIMock();
var spec = createSpec('test spec', function() {
this.test = 'some value';
var spec = createSpec('test spec', function() {
this.test = 'some value';
});
runner.addFuture('test future', function(done) {
done();
});
runner.run(ui, spec, function() {
runner.run(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:'
expect(log).toEqual([
'SpecBegin',
'StepBegin',
'StepEnd',
'SpecEnd'
]);
});
it('should execute notify UI on spec setup error', function() {
var finished;
var ui = new UIMock();
var spec = createSpec('test spec', function() {
var spec = createSpec('test spec', function() {
throw 'message';
});
runner.run(ui, spec, function() {
runner.run(spec, function() {
finished = true;
});
expect(finished).toBeTruthy();
expect(ui.log).toEqual([
'addSpec:test spec',
'spec error:message'
expect(log).toEqual([
'SpecBegin',
'SpecError',
'SpecEnd'
]);
});
it('should execute notify UI on step failure', function() {
var finished;
var ui = new UIMock();
var spec = createSpec('test spec');
runner.addFuture('test future', function(done) {
done('failure message');
});
runner.run(ui, spec, function() {
runner.run(spec, function() {
finished = true;
});
expect(finished).toBeTruthy();
expect(ui.log).toEqual([
'addSpec:test spec',
'addStep:test future',
'step finish:failure message',
'spec finish:'
expect(log).toEqual([
'SpecBegin',
'StepBegin',
'StepFailure',
'StepEnd',
'SpecEnd'
]);
});
it('should execute notify UI on step error', function() {
var finished;
var ui = new UIMock();
var spec = createSpec('test spec', function() {
this.addFuture('test future', function(done) {
throw 'error message';
});
});
runner.run(ui, spec, function() {
runner.run(spec, function() {
finished = true;
});
expect(finished).toBeTruthy();
expect(ui.log).toEqual([
'addSpec:test spec',
'addStep:test future',
'step error:error message',
'spec finish:'
expect(log).toEqual([
'SpecBegin',
'StepBegin',
'StepError',
'StepEnd',
'SpecEnd'
]);
});
it('should run after handlers even if error in body of spec', function() {
var finished, after;
var ui = new UIMock();
var spec = createSpec('test spec', function() {
this.addFuture('body', function(done) {
throw 'error message';
@@ -181,18 +151,19 @@ describe('angular.scenario.SpecRunner', function() {
done();
});
};
runner.run(ui, spec, function() {
runner.run(spec, function() {
finished = true;
});
expect(finished).toBeTruthy();
expect(after).toBeTruthy();
expect(ui.log).toEqual([
'addSpec:test spec',
'addStep:body',
'step error:error message',
'addStep:after',
'step finish:',
'spec finish:'
expect(log).toEqual([
'SpecBegin',
'StepBegin',
'StepError',
'StepEnd',
'StepBegin',
'StepEnd',
'SpecEnd'
]);
});
+47 -45
View File
@@ -1,41 +1,21 @@
/**
* Very basic Mock of angular.
*/
function AngularMock() {
this.reset();
this.service = this;
}
AngularMock.prototype.reset = function() {
this.log = [];
};
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;
var $window, $root;
var application, eventLog;
beforeEach(function() {
eventLog = [];
$window = {
document: _jQuery("<div></div>"),
angular: new AngularMock()
angular: new angular.scenario.testing.MockAngular()
};
$root = angular.scope();
$root = angular.scope({
emit: function(eventName) {
eventLog.push(eventName);
},
on: function(eventName) {
eventLog.push('Listener Added for ' + eventName);
}
});
$root.futures = [];
$root.futureLog = [];
$root.$window = $window;
@@ -54,7 +34,7 @@ describe("angular.scenario.dsl", function() {
};
});
$root.application = new angular.scenario.Application($window.document);
$root.application.getWindow = function() {
$root.application.getWindow_ = function() {
return $window;
};
$root.application.navigateTo = function(url, callback) {
@@ -74,13 +54,14 @@ describe("angular.scenario.dsl", function() {
expect($root.futureLog).toEqual([]);
$window.resume();
expect($root.futureLog).
toEqual(['waiting for you to call resume() in the console']);
toEqual(['waiting for you to resume']);
expect(eventLog).toContain('InteractiveWait');
});
});
describe('Pause', function() {
beforeEach(function() {
$root.setTimeout = function(fn, value) {
$root.$window.setTimeout = function(fn, value) {
$root.timerValue = value;
fn();
};
@@ -127,13 +108,6 @@ describe("angular.scenario.dsl", function() {
expect($window.location).toEqual('http://myurl');
expect($root.futureResult).toEqual('http://myurl');
});
it('should wait for angular notify when no requests pending', function() {
$root.dsl.navigateTo('url');
expect($window.angular.log).toContain('$brower.poll()');
expect($window.angular.log).
toContain('$brower.notifyWhenNoOutstandingRequests()');
});
});
describe('Element Finding', function() {
@@ -190,13 +164,33 @@ describe("angular.scenario.dsl", function() {
describe('Element', function() {
it('should execute click', function() {
var clicked;
doc.append('<a href=""></a>');
// Hash is important, otherwise we actually
// go to a different page and break the runner
doc.append('<a href="#"></a>');
doc.find('a').click(function() {
clicked = true;
});
$root.dsl.element('a').click();
});
it('should navigate page if click on anchor', function() {
expect($window.location).not.toEqual('#foo');
doc.append('<a href="#foo"></a>');
$root.dsl.element('a').click();
expect($window.location).toMatch(/#foo$/);
});
it('should count matching elements', function() {
doc.append('<span></span><span></span>');
$root.dsl.element('span').count();
expect($root.futureResult).toEqual(2);
});
it('should return count of 0 if no matching elements', function() {
$root.dsl.element('span').count();
expect($root.futureResult).toEqual(0);
});
it('should get attribute', function() {
doc.append('<div id="test" class="foo"></div>');
$root.dsl.element('#test').attr('class');
@@ -222,11 +216,11 @@ describe("angular.scenario.dsl", function() {
});
it('should execute custom query', function() {
doc.append('<a id="test" href="myUrl"></a>');
doc.append('<a id="test" href="http://example.com/myUrl"></a>');
$root.dsl.element('#test').query(function(elements, done) {
done(null, elements.attr('href'));
});
expect($root.futureResult).toEqual('myUrl');
expect($root.futureResult).toEqual('http://example.com/myUrl');
});
});
@@ -247,6 +241,12 @@ describe("angular.scenario.dsl", function() {
expect($root.futureResult).toEqual(2);
});
it('should return 0 if repeater doesnt match', function() {
doc.find('ul').html('');
chain.count();
expect($root.futureResult).toEqual(0);
});
it('should get a row of bindings', function() {
chain.row(1);
expect($root.futureResult).toEqual(['felisa', 'female']);
@@ -260,12 +260,14 @@ describe("angular.scenario.dsl", function() {
describe('Binding', function() {
it('should select binding by name', function() {
if (msie) return; // TODO reenable!
doc.append('<span ng:bind="foo.bar">some value</span>');
$root.dsl.binding('foo.bar');
expect($root.futureResult).toEqual('some value');
});
it('should select binding in template by name', function() {
if (msie) return; // TODO reenable!
doc.append('<pre ng:bind-template="foo {{bar}} baz">foo some baz</pre>');
$root.dsl.binding('bar');
expect($root.futureResult).toEqual('foo some baz');
+4 -4
View File
@@ -1,6 +1,6 @@
describe('angular.scenario.matchers', function () {
var matchers;
function expectMatcher(value, test) {
delete matchers.error;
delete matchers.future.value;
@@ -10,9 +10,9 @@ describe('angular.scenario.matchers', function () {
test();
expect(matchers.error).toBeUndefined();
}
beforeEach(function() {
/**
/**
* Mock up the future system wrapped around matchers.
*
* @see Scenario.js#angular.scenario.matcher
@@ -27,7 +27,7 @@ describe('angular.scenario.matchers', function () {
};
angular.extend(matchers, angular.scenario.matcher);
});
it('should handle basic matching', function() {
expectMatcher(10, function() { matchers.toEqual(10); });
expectMatcher('value', function() { matchers.toBeDefined(); });
+41
View File
@@ -0,0 +1,41 @@
angular.scenario.testing = angular.scenario.testing || {};
angular.scenario.testing.MockAngular = function() {
this.reset();
this.service = this;
};
angular.scenario.testing.MockAngular.prototype.reset = function() {
this.log = [];
};
angular.scenario.testing.MockAngular.prototype.$browser = function() {
this.log.push('$brower()');
return this;
};
angular.scenario.testing.MockAngular.prototype.poll = function() {
this.log.push('$brower.poll()');
return this;
};
angular.scenario.testing.MockAngular.prototype.notifyWhenNoOutstandingRequests = function(fn) {
this.log.push('$brower.notifyWhenNoOutstandingRequests()');
fn();
};
angular.scenario.testing.MockRunner = function() {
this.listeners = [];
};
angular.scenario.testing.MockRunner.prototype.on = function(eventName, fn) {
this.listeners[eventName] = this.listeners[eventName] || [];
this.listeners[eventName].push(fn);
};
angular.scenario.testing.MockRunner.prototype.emit = function(eventName) {
var args = Array.prototype.slice.call(arguments, 1);
angular.foreach(this.listeners[eventName] || [], function(fn) {
fn.apply(this, args);
});
};
+124
View File
@@ -0,0 +1,124 @@
describe('angular.scenario.output.html', function() {
var runner, spec, listeners;
var ui, context;
beforeEach(function() {
listeners = [];
spec = {
name: 'test spec',
definition: {
id: 10,
name: 'child',
children: [],
parent: {
id: 20,
name: 'parent',
children: []
}
}
};
step = {
name: 'some step',
line: function() { return 'unknown:-1'; }
};
runner = new angular.scenario.testing.MockRunner();
context = _jQuery("<div></div>");
ui = angular.scenario.output.html(context, runner);
});
it('should create nested describe context', function() {
runner.emit('SpecBegin', 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('test spec');
expect(context.find('#describe-10 .tests > li').hasClass('status-pending')).
toBeTruthy();
});
it('should add link on InteractiveWait', function() {
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepEnd', spec, step);
runner.emit('StepBegin', spec, step);
runner.emit('InteractiveWait', spec, step);
expect(context.find('.test-actions .test-title:first').text()).toEqual('some step');
expect(lowercase(context.find('.test-actions .test-title:last').html())).toEqual(
'waiting for you to <a href="javascript:resume()">resume</a>.'
);
});
it('should update totals when steps complete', function() {
// Failure
for (var i=0; i < 3; ++i) {
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepFailure', spec, step, 'error');
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
}
// Error
runner.emit('SpecBegin', spec);
runner.emit('SpecError', spec, 'error');
runner.emit('SpecEnd', spec);
// Error
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepError', spec, step, 'error');
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
// Success
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
expect(parseInt(context.find('#status-legend .status-failure').text(), 10)).
toEqual(3);
expect(parseInt(context.find('#status-legend .status-error').text(), 10)).
toEqual(2);
expect(parseInt(context.find('#status-legend .status-success').text(), 10)).
toEqual(1);
});
it('should update timer when test completes', function() {
// Success
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
// Failure
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepFailure', spec, step, 'error');
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
// Error
runner.emit('SpecBegin', spec);
runner.emit('SpecError', spec, 'error');
runner.emit('SpecEnd', spec);
context.find('#describe-10 .tests > li .test-info .timer-result').
each(function(index, timer) {
expect(timer.innerHTML).toMatch(/ms$/);
});
});
it('should include line if provided', function() {
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepFailure', spec, step, 'error');
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
var errorHtml = context.find('#describe-10 .tests li pre').html();
expect(errorHtml.indexOf('unknown:-1')).toEqual(0);
});
});
+34
View File
@@ -0,0 +1,34 @@
describe('angular.scenario.output.json', function() {
var output, context;
var runner, $window;
var spec, step;
beforeEach(function() {
$window = {};
context = _jQuery('<div></div>');
runner = new angular.scenario.testing.MockRunner();
output = angular.scenario.output.json(context, runner);
spec = {
name: 'test spec',
definition: {
id: 10,
name: 'describe'
}
};
step = {
name: 'some step',
line: function() { return 'unknown:-1'; }
};
});
it('should put json in context on RunnerEnd', function() {
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
runner.emit('RunnerEnd');
expect(angular.fromJson(context.html()).children['describe']
.specs['test spec'].status).toEqual('success');
});
});
+37
View File
@@ -0,0 +1,37 @@
describe('angular.scenario.output.object', function() {
var output;
var runner, $window;
var spec, step;
beforeEach(function() {
$window = {};
runner = new angular.scenario.testing.MockRunner();
runner.$window = $window;
output = angular.scenario.output.object(null, runner);
spec = {
name: 'test spec',
definition: {
id: 10,
name: 'describe',
children: []
}
};
step = {
name: 'some step',
line: function() { return 'unknown:-1'; }
};
});
it('should create a global variable $result', function() {
expect($window.$result).toBeDefined();
});
it('should maintain live state in $result', function() {
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepEnd', spec, step);
expect($window.$result.children['describe']
.specs['test spec'].steps[0].duration).toBeDefined();
});
});
+33
View File
@@ -0,0 +1,33 @@
describe('angular.scenario.output.json', function() {
var output, context;
var runner, $window;
var spec, step;
beforeEach(function() {
$window = {};
context = _jQuery('<div></div>');
runner = new angular.scenario.testing.MockRunner();
output = angular.scenario.output.xml(context, runner);
spec = {
name: 'test spec',
definition: {
id: 10,
name: 'describe'
}
};
step = {
name: 'some step',
line: function() { return 'unknown:-1'; }
};
});
it('should create XML nodes for object model', function() {
runner.emit('SpecBegin', spec);
runner.emit('StepBegin', spec, step);
runner.emit('StepEnd', spec, step);
runner.emit('SpecEnd', spec);
runner.emit('RunnerEnd');
expect(context.find('it').attr('status')).toEqual('success');
expect(context.find('it step').attr('status')).toEqual('success');
});
});
+33 -33
View File
@@ -85,7 +85,7 @@ describe("service", function(){
it("should inject $location", function() {
expect(scope.$location).toBeDefined();
});
it("update should update location object immediately", function() {
var href = 'http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2=';
scope.$location.update(href);
@@ -99,25 +99,25 @@ describe("service", function(){
expect(scope.$location.hashPath).toEqual('path');
expect(scope.$location.hashSearch).toEqual({key: 'value', flag: true, key2: ''});
});
it('toString() should return actual representation', function() {
var href = 'http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2=';
scope.$location.update(href);
expect(scope.$location.toString()).toEqual(href);
scope.$eval();
scope.$location.host = 'new';
scope.$location.path = '';
expect(scope.$location.toString()).toEqual('http://new:123?query=value#path?key=value&flag&key2=');
});
it('toString() should not update browser', function() {
var url = $browser.getUrl();
scope.$location.update('http://www.angularjs.org');
expect(scope.$location.toString()).toEqual('http://www.angularjs.org');
expect($browser.getUrl()).toEqual(url);
});
it('should update browser at the end of $eval', function() {
var url = $browser.getUrl();
scope.$location.update('http://www.angularjs.org/');
@@ -136,12 +136,12 @@ describe("service", function(){
expect(scope.$location.hashPath).toEqual('');
expect(scope.$location.hashSearch).toEqual({});
});
it('should update hash on hashPath or hashSearch update', function() {
scope.$location.update('http://server/#path?a=b');
scope.$eval();
scope.$location.update({hashPath: '', hashSearch: {}});
expect(scope.$location.hash).toEqual('');
});
@@ -154,7 +154,7 @@ describe("service", function(){
expect(scope.$location.hashPath).toEqual('');
expect(scope.$location.hashSearch).toEqual({});
});
it('should update hash on hashPath or hashSearch property change', function() {
scope.$location.update('http://server/#path?a=b');
scope.$eval();
@@ -178,35 +178,35 @@ describe("service", function(){
scope.$eval();
expect(log).toEqual('/abc;');
});
it('udpate() should accept hash object and update only given properties', function() {
scope.$location.update("http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2=");
scope.$location.update({host: 'new', port: 24});
expect(scope.$location.host).toEqual('new');
expect(scope.$location.port).toEqual(24);
expect(scope.$location.protocol).toEqual('http');
expect(scope.$location.href).toEqual("http://new:24/p/a/t/h.html?query=value#path?key=value&flag&key2=");
});
it('updateHash() should accept one string argument to update path', function() {
scope.$location.updateHash('path');
expect(scope.$location.hash).toEqual('path');
expect(scope.$location.hashPath).toEqual('path');
});
it('updateHash() should accept one hash argument to update search', function() {
scope.$location.updateHash({a: 'b'});
expect(scope.$location.hash).toEqual('?a=b');
expect(scope.$location.hashSearch).toEqual({a: 'b'});
});
it('updateHash() should accept path and search both', function() {
scope.$location.updateHash('path', {a: 'b'});
expect(scope.$location.hash).toEqual('path?a=b');
expect(scope.$location.hashSearch).toEqual({a: 'b'});
expect(scope.$location.hashPath).toEqual('path');
});
});
});
describe("$invalidWidgets", function(){
@@ -541,7 +541,7 @@ describe("service", function(){
describe('$cookieStore', function() {
it('should serialize objects to json', function() {
scope.$cookieStore.put('objectCookie', {id: 123, name: 'blah'});
scope.$inject('$cookieStore').put('objectCookie', {id: 123, name: 'blah'});
scope.$eval(); //force eval in test
expect($browser.cookies()).toEqual({'objectCookie': '{"id":123,"name":"blah"}'});
});
@@ -550,65 +550,65 @@ describe("service", function(){
it('should deserialize json to object', function() {
$browser.cookies('objectCookie', '{"id":123,"name":"blah"}');
$browser.poll();
expect(scope.$cookieStore.get('objectCookie')).toEqual({id: 123, name: 'blah'});
expect(scope.$inject('$cookieStore').get('objectCookie')).toEqual({id: 123, name: 'blah'});
});
it('should delete objects from the store when remove is called', function() {
scope.$cookieStore.put('gonner', { "I'll":"Be Back"});
scope.$inject('$cookieStore').put('gonner', { "I'll":"Be Back"});
scope.$eval(); //force eval in test
expect($browser.cookies()).toEqual({'gonner': '{"I\'ll":"Be Back"}'});
});
});
describe('URL_MATCH', function() {
it('should parse basic url', function() {
var match = URL_MATCH.exec('http://www.angularjs.org/path?search#hash?x=x');
expect(match[1]).toEqual('http');
expect(match[3]).toEqual('www.angularjs.org');
expect(match[6]).toEqual('/path');
expect(match[8]).toEqual('search');
expect(match[10]).toEqual('hash?x=x');
});
it('should parse file://', function(){
var match = URL_MATCH.exec('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html');
expect(match[1]).toEqual('file');
expect(match[3]).toEqual('');
expect(match[5]).toEqual(null);
expect(match[5]).toBeFalsy();
expect(match[6]).toEqual('/Users/Shared/misko/work/angular.js/scenario/widgets.html');
expect(match[8]).not.toBeDefined();
expect(match[8]).toBeFalsy();
});
it('should parse url with "-" in host', function(){
var match = URL_MATCH.exec('http://a-b1.c-d.09/path');
expect(match[1]).toEqual('http');
expect(match[3]).toEqual('a-b1.c-d.09');
expect(match[5]).toEqual(null);
expect(match[5]).toBeFalsy();
expect(match[6]).toEqual('/path');
expect(match[8]).not.toBeDefined();
expect(match[8]).toBeFalsy();
});
it('should parse host without "/" at the end', function() {
var match = URL_MATCH.exec('http://host.org');
expect(match[3]).toEqual('host.org');
match = URL_MATCH.exec('http://host.org#');
expect(match[3]).toEqual('host.org');
match = URL_MATCH.exec('http://host.org?');
expect(match[3]).toEqual('host.org');
});
it('should match with just "/" path', function() {
var match = URL_MATCH.exec('http://server/#?book=moby');
expect(match[10]).toEqual('?book=moby');
});
});
+17 -3
View File
@@ -1,5 +1,16 @@
jstd = jstestdriver;
dump = bind(jstd.console, jstd.console.log);
/**
* Here is the problem: http://bugs.jquery.com/ticket/7292
* basically jQuery treats change event on some browsers (IE) as a
* special event and changes it form 'change' to 'click/keyup' and
* few others. This horrible hack removes the special treatment
*/
_jQuery.event.special.change = undefined;
if (window.jstestdriver) {
jstd = jstestdriver;
dump = bind(jstd.console, jstd.console.log);
}
beforeEach(function(){
this.addMatchers({
@@ -74,7 +85,10 @@ function sortedHtml(element) {
var html = "";
foreach(jqLite(element), function toString(node) {
if (node.nodeName == "#text") {
html += escapeHtml(node.nodeValue);
html += node.nodeValue.
replace(/&(\w+[&;\W])?/g, function(match, entity){return entity?match:'&amp;';}).
replace(/</g, '&lt;').
replace(/>/g, '&gt;');
} else {
html += '<' + node.nodeName.toLowerCase();
var attributes = node.attributes || [];
+14 -3
View File
@@ -345,9 +345,9 @@ describe("widget", function(){
it('should initialize to selected', function(){
compile(
'<select name="selection">' +
'<option>A</option>' +
'<option selected>B</option>' +
'</select>');
'<option>A</option>' +
'<option selected>B</option>' +
'</select>');
expect(scope.selection).toEqual('B');
scope.selection = 'A';
scope.$eval();
@@ -497,6 +497,17 @@ describe("widget", function(){
expect(element.text()).toEqual('');
});
it('should allow this for scope', function(){
var element = jqLite('<ng:include src="url" scope="this"></ng:include>');
var scope = angular.compile(element);
scope.url = 'myUrl';
scope.$inject('$xhr.cache').data.myUrl = {value:'{{c=c+1}}'};
scope.$init();
// This should not be 4, but to fix this properly
// we need to have real events on the scopes.
expect(element.text()).toEqual('4');
});
});
describe('a', function() {
+2 -2
View File
@@ -1,4 +1,4 @@
# <angular/> build config file
---
version: 0.9.0
codename: dragon-breath
version: 0.9.1
codename: repulsion-field