feat(scope): new and improved scope implementation

- Speed improvements (about 4x on flush phase)
- Memory improvements (uses no function closures)
- Break $eval into $apply, $dispatch, $flush
- Introduced $watch and $observe

Breaks angular.equals() use === instead of ==
Breaks angular.scope() does not take parent as first argument
Breaks scope.$watch() takes scope as first argument
Breaks scope.$set(), scope.$get are removed
Breaks scope.$config is removed
Breaks $route.onChange callback has not "this" bounded
This commit is contained in:
Misko Hevery 2011-03-23 09:33:29 -07:00 committed by Vojta Jina
parent 1f4b417184
commit 8f0dcbab80
60 changed files with 2539 additions and 1721 deletions

View file

@ -1,6 +1,22 @@
<a name="0.9.19"><a/> <a name="0.9.19"><a/>
# 0.9.19 canine-psychokinesis (in-progress) # # 0.9.19 canine-psychokinesis (in-progress) #
# Breaking Changes
- Controller constructor functions are now looked up on scope first and then on window.
- angular.equals now use === which means that things which used to be equal are no longer.
Example '0' !== 0 and [] !== ''
- angular.scope (http://docs.angularjs.org/#!angular.scope) now (providers, cache) instead of
(parent, providers, cache)
- Watch functions (see http://docs.angularjs.org/#!angular.scope.$watch) used to take
fn(newValue, oldValue) and be bound to scope, now they take fn(scope, newValue, oldValue)
- calling $eval() [no args] should be replaced with call to $apply()
(http://docs.angularjs.org/#!angular.scope.$apply) ($eval(exp) should remain as is see
http://docs.angularjs.org/#!angular.scope.$eval)
- scope $set/$get have been removed. ($get is same as $eval; no replacement for $set)
- $route.onChange() callback (http://docs.angularjs.org/#!angular.service.$route)
no longer has this bound.
- Removed undocumented $config in root scope. (You should have not been depending on this.)

View file

@ -64,7 +64,7 @@ no connection between the controller and the view.
}); });
this.$location.hashSearch.board = rows.join(';') + '/' + this.nextMove; this.$location.hashSearch.board = rows.join(';') + '/' + this.nextMove;
}, },
readUrl: function(value) { readUrl: function(scope, value) {
if (value) { if (value) {
value = value.split('/'); value = value.split('/');
this.nextMove = value[1]; this.nextMove = value[1];

View file

@ -1,39 +0,0 @@
@workInProgress
@ngdoc overview
@name Developer Guide: Scopes: Applying Controllers to Scopes
@description
When a controller function is applied to a scope, the scope is augmented with the behavior defined
in the controller. The end result is that the scope behaves as if it were the controller:
<pre>
var scope = angular.scope();
scope.salutation = 'Hello';
scope.name = 'World';
expect(scope.greeting).toEqual(undefined);
scope.$watch('name', function(){
this.greeting = this.salutation + ' ' + this.name + '!';
});
expect(scope.greeting).toEqual('Hello World!');
scope.name = 'Misko';
// scope.$eval() will propagate the change to listeners
expect(scope.greeting).toEqual('Hello World!');
scope.$eval();
expect(scope.greeting).toEqual('Hello Misko!');
</pre>
## Related Topics
* {@link dev_guide.scopes Angular Scope Objects}
* {@link dev_guide.scopes.understanding_scopes Understanding Angular Scopes}
* {@link dev_guide.scopes.working_scopes Working With Angular Scopes}
* {@link dev_guide.scopes.updating_scopes Updating Angular Scopes}
## Related API
* {@link api/angular.scope Angular Scope API}

View file

@ -0,0 +1,196 @@
@workInProgress
@ngdoc overview
@name Developer Guide: Scopes: Scope Internals
@description
## What is a scope?
A scope is an execution context for {@link dev_guide.expressions expressions}. You can think of a
scope as a JavaScript object that has an extra set of APIs for registering change listeners and for
managing its own life cycle. In Angular's implementation of the model-view-controller design
pattern, a scope's properties comprise both the model and the controller methods.
### Scope characteristics
- Scopes provide APIs ($watch and $observe) to observe model mutations.
- Scopes provide APIs ($apply) to propagate any model changes through the system into the view from
outside of the "Angular realm" (controllers, services, Angular event handlers).
- Scopes can be nested to isolate application components while providing access to shared model
properties. A scope (prototypically) inherits properties from its parent scope.
- In some parts of the system (such as controllers, services and directives), the scope is made
available as `this` within the given context. (Note: This functionality will change before 1.0 is
released.)
### Root scope
Every application has a root scope, which is the ancestor of all other scopes. The root scope is
responsible for creating the injector which is assigned to the {@link api/angular.scope.$service
$service} property, and initializing the services.
### What is scope used for?
{@link dev_guide.expressions Expressions} in the view are evaluated against the current scope. When
HTML DOM elements are attached to a scope, expressions in those elements are evaluated against the
attached scope.
There are two kinds of expressions:
- Binding expressions, which are observations of property changes. Property changes are reflected
in the view during the {@link api/angular.scope.$flush flush cycle}.
- Action expressions, which are expressions with side effects. Typically, the side effects cause
execution of a method in a controller in response to a user action, such as clicking on a button.
### Scope inheritance
A scope (prototypically) inherits properties from its parent scope. Since a given property may not
reside on a child scope, if a property read does not find the property on a scope, the read will
recursively check the parent scope, grandparent scope, etc. all the way to the root scope before
defaulting to undefined.
{@link api/angular.directive Directives} associated with elements (ng:controller, ng:repeat,
ng:include, etc.) create new child scopes that inherit properties from the current parent scope.
Any code in Angular is free to create a new scope. Whether or not your code does so is an
implementation detail of the directive, that is, you can decide when or if this happens.
Inheritance typically mimics HTML DOM element nesting, but does not do so with the same
granularity.
A property write will always write to the current scope. This means that a write can hide a parent
property within the scope it writes to, as shown in the following example.
<pre>
var root = angular.scope();
var child = root.$new();
root.name = 'angular';
expect(child.name).toEqual('angular');
expect(root.name).toEqual('angular');
child.name = 'super-heroic framework';
expect(child.name).toEqual('super-heroic framework');
expect(root.name).toEqual('angular');
</pre>
## Scopes in Angular applications
To understand how Angular applications work, you need to understand how scopes work within an
application. This section describes the typical life cycle of an application so you can see how
scopes come into play throughout and get a sense of their interactions.
### How scopes interact in applications
1. At application compile time, a root scope is created and is attached to the root `<HTML>` DOM
element.
1. The root scope creates an {@link api/angular.injector injector} which is assigned to the
{@link api/angular.scope.$service $service} property of the root scope.
2. Any eager {@link api/angular.scope.$service services} are initialized at this point.
2. During the compilation phase, the {@link dev_guide.compiler compiler} matches {@link
api/angular.directive directives} against the DOM template. The directives usually fall into one of
two categories:
- Observing {@link api/angular.directive directives}, such as double-curly expressions
`{{expression}}`, register listeners using the {@link api/angular.scope.$observe $observe()}
method. This type of directive needs to be notified whenever the expression changes so that it can
update the view.
- Listener directives, such as {@link api/angular.directive.ng:click ng:click}, register a
listener with the DOM. When the DOM listener fires, the directive executes the associated
expression and updates the view using the {@link api/angular.scope.$apply $apply()} method.
3. When an external event (such as a user action, timer or XHR) is received, the associated {@link
dev_guide.expressions expression} must be applied to the scope through the {@link
api/angular.scope.$apply $apply()} method so that all listeners are updated correctly.
### Directives that create scopes
In most cases, {@link api/angular.directive directives} and scopes interact but do not create new
instances of scope. However, some directives, such as {@link api/angular.directive.ng:controller
ng:controller} and {@link api/angular.widget.@ng:repeat ng:repeat}, create new child scopes using
the {@link api/angular.scope.$new $new()} method and then attach the child scope to the
corresponding DOM element. You can retrieve a scope for any DOM element by using an
`angular.element(aDomElement).scope()` method call.)
### Controllers and scopes
Scopes and controllers interact with each other in the following situations:
- Controllers use scopes to expose controller methods to templates (see {@link
api/angular.directive.ng:controller ng:controller}).
- Controllers define methods (behavior) that can mutate the model (properties on the scope).
- Controllers may register {@link api/angular.scope.$watch watches} on the model. These watches
execute immediately after the controller behavior executes, but before the DOM gets updated.
See the {@link dev_guide.mvc.understanding_controller controller docs} for more information.
### Updating scope properties
You can update a scope by calling its {@link api/angular.scope.$eval $eval()} method, but usually
you do not have to do this explicitly. In most cases, angular intercepts all external events (such
as user interactions, XHRs, and timers) and calls the `$eval()` method on the scope object for you
at the right time. The only time you might need to call `$eval()` explicitly is when you create
your own custom widget or service.
The reason it is unnecessary to call `$eval()` from within your controller functions when you use
built-in angular widgets and services is because a change in the data model triggers a call to the
`$eval()` method on the scope object where the data model changed.
When a user inputs data, angularized widgets copy the data to the appropriate scope and then call
the `$eval()` method on the root scope to update the view. It works this way because scopes are
inherited, and a child scope `$eval()` overrides its parent's `$eval()` method. Updating the whole
page requires a call to `$eval()` on the root scope as `$root.$eval()`. Similarly, when a request
to fetch data from a server is made and the response comes back, the data is written into the model
and then `$eval()` is called to push updates through to the view and any other dependents.
A widget that creates scopes (such as {@link api/angular.widget.@ng:repeat ng:repeat}) is
responsible for forwarding `$eval()` calls from the parent to those child scopes. That way, calling
`$eval()` on the root scope will update the whole page. This creates a spreadsheet-like behavior
for your app; the bound views update immediately as the user enters data.
## Scopes in unit-testing
You can create scopes, including the root scope, in tests using the {@link api/angular.scope} API.
This allows you to mimic the run-time environment and have full control over the life cycle of the
scope so that you can assert correct model transitions. Since these scopes are created outside the
normal compilation process, their life cycles must be managed by the test.
There is a key difference between the way scopes are called in Angular applications and in Angular
tests. In tests, the {@link api/angular.service.$updateView $updateView} calls the {@link
api/angular.scope.$flush $flush()} method synchronously.(This is in contrast to the asynchronous
calls used for applications.) Because test calls to scopes are synchronous, your tests are simpler
to write.
### Using scopes in unit-testing
The following example demonstrates how the scope life cycle needs to be manually triggered from
within the unit-tests.
<pre>
// example of a test
var scope = angular.scope();
scope.$watch('name', function(scope, name){
scope.greeting = 'Hello ' + name + '!';
});
scope.name = 'angular';
// The watch does not fire yet since we have to manually trigger the digest phase.
expect(scope.greeting).toEqual(undefined);
// manually trigger digest phase from the test
scope.$digest();
expect(scope.greeting).toEqual('Hello Angular!');
</pre>
### Dependency injection in Tests
When you find it necessary to inject your own mocks in your tests, use a scope to override the
service instances, as shown in the following example.
<pre>
var myLocation = {};
var scope = angular.scope(null, {$location: myLocation});
expect(scope.$service('$location')).toEqual(myLocation);
</pre>
## Related Topics
* {@link dev_guide.scopes Angular Scope Objects}
* {@link dev_guide.scopes.understanding_scopes Understanding Scopes}
## Related API
* {@link api/angular.scope Angular Scope API}

View file

@ -1,36 +1,34 @@
@workInProgress @workInProgress
@ngdoc overview @ngdoc overview
@name Developer Guide: Angular Scopes @name Developer Guide: Scopes
@description @description
An angular scope is a JavaScript type defined by angular. Instances of this type are objects that An Angular scope is a JavaScript object with additional APIs useful for watching property changes,
serve as the context within which all model and controller methods live and get evaluated. Angular scope is the model in Model-View-Controller paradigm. Instances of scope serve as the
context within which all {@link dev_guide.expressions expressions} get evaluated.
Angular links scope objects to specific points in a compiled (processed) template. This linkage You can think of Angular scope objects as the medium through which the model, view, and controller
provides the contexts in which angular creates data-bindings between the model and the view. You communicate. Scopes are linked during the compilation process with the view. This linkage provides
can think of angular scope objects as the medium through which the model, view, and controller the contexts in which Angular creates data-bindings between the model and the view.
communicate.
In addition to providing the context in which data is evaluated, angular scope objects watch for In addition to providing the context in which data is evaluated, Angular scope objects watch for
model changes. The scope objects also notify all components interested in any model changes (for model changes. The scope objects also notify all components interested in any model changes (for
example, functions registered through {@link api/angular.scope.$watch $watch}, bindings created by example, functions registered through {@link api/angular.scope.$watch $watch}, bindings created by
{@link api/angular.directive.ng:bind ng:bind}, or HTML input elements). {@link api/angular.directive.ng:bind ng:bind}, or HTML input elements).
Angular scope objects are responsible for: Angular scope objects:
* Gluing the model, controller and view template together. * Link the model, controller and view template together.
* Providing the mechanism to watch for model changes ({@link api/angular.scope.$watch}). * Provide the mechanism to watch for model changes ({@link api/angular.scope.$watch}).
* Notifying interested components when the model changes ({@link api/angular.scope.$eval}). * Notify interested components when the model changes ({@link api/angular.scope.$eval}).
* Providing the context in which all controller functions and angular expressions are evaluated. * Provide the context in which expressions are evaluated.
## Related Topics ## Related Topics
* {@link dev_guide.scopes.understanding_scopes Understanding Scopes} * {@link dev_guide.scopes.understanding_scopes Understanding Scopes}
* {@link dev_guide.scopes.working_scopes Working With Scopes} * {@link dev_guide.scopes.internals Scopes Internals}
* {@link dev_guide.scopes.controlling_scopes Applying Controllers to Scopes}
* {@link dev_guide.scopes.updating_scopes Updating Scopes}
## Related API ## Related API

View file

@ -60,9 +60,7 @@ The following illustration shows the DOM and angular scopes for the example abov
## Related Topics ## Related Topics
* {@link dev_guide.scopes Angular Scope Objects} * {@link dev_guide.scopes Angular Scope Objects}
* {@link dev_guide.scopes.working_scopes Working With Scopes} * {@link dev_guide.scopes.internals Scopes Internals}
* {@link dev_guide.scopes.controlling_scopes Applying Controllers to Scopes}
* {@link dev_guide.scopes.updating_scopes Updating Scopes}
## Related API ## Related API

View file

@ -1,38 +0,0 @@
@workInProgress
@ngdoc overview
@name Developer Guide: Scopes: Updating Scope Properties
@description
You can update a scope by calling its {@link api/angular.scope.$eval $eval()} method, but usually
you do not have to do this explicitly. In most cases, angular intercepts all external events (such
as user interactions, XHRs, and timers) and calls the `$eval()` method on the scope object for you
at the right time. The only time you might need to call `$eval()` explicitly is when you create
your own custom widget or service.
The reason it is unnecessary to call `$eval()` from within your controller functions when you use
built-in angular widgets and services is because a change in the data model triggers a call to the
`$eval()` method on the scope object where the data model changed.
When a user inputs data, angularized widgets copy the data to the appropriate scope and then call
the `$eval()` method on the root scope to update the view. It works this way because scopes are
inherited, and a child scope `$eval()` overrides its parent's `$eval()` method. Updating the whole
page requires a call to `$eval()` on the root scope as `$root.$eval()`. Similarly, when a request
to fetch data from a server is made and the response comes back, the data is written into the model
and then `$eval()` is called to push updates through to the view and any other dependents.
A widget that creates scopes (such as {@link api/angular.widget.@ng:repeat ng:repeat}) is
responsible for forwarding `$eval()` calls from the parent to those child scopes. That way, calling
`$eval()` on the root scope will update the whole page. This creates a spreadsheet-like behavior
for your app; the bound views update immediately as the user enters data.
## Related Documents
* {@link dev_guide.scopes Angular Scope Objects}
* {@link dev_guide.scopes.understanding_scopes Understanding Angular Scope Objects}
* {@link dev_guide.scopes.working_scopes Working With Angular Scopes}
* {@link dev_guide.scopes.controlling_scopes Applying Controllers to Scopes}
## Related API
* {@link api/angular.scope Angular Scope API}

View file

@ -1,52 +0,0 @@
@workInProgress
@ngdoc overview
@name Developer Guide: Scopes: Working With Angular Scopes
@description
When you use {@link api/angular.directive.ng:autobind ng:autobind} to bootstrap your application,
angular creates the root scope automatically for you. If you need more control over the
bootstrapping process, or if you need to create a root scope for a test, you can do so using the
{@link api/angular.scope angular.scope()} API.
Here is a simple code snippet that demonstrates how to create a scope object, assign model
properties to it, and register listeners to watch for changes to the model properties:
<pre>
var scope = angular.scope();
scope.salutation = 'Hello';
scope.name = 'World';
// Verify that greeting is undefined
expect(scope.greeting).toEqual(undefined);
// Set up the watcher...
scope.$watch('name', function(){
// when 'name' changes, set 'greeting'...
this.greeting = this.salutation + ' ' + this.name + '!';
}
);
// verify that 'greeting' was set...
expect(scope.greeting).toEqual('Hello World!');
// 'name' changed!
scope.name = 'Misko';
// scope.$eval() will propagate the change to listeners
expect(scope.greeting).toEqual('Hello World!');
scope.$eval();
// verify that '$eval' propagated the change
expect(scope.greeting).toEqual('Hello Misko!');
</pre>
## Related Topics
* {@link dev_guide.scopes Angular Scope Objects}
* {@link dev_guide.scopes.understanding_scopes Understanding Scopes}
* {@link dev_guide.scopes.controlling_scopes Applying Controllers to Scopes}
* {@link dev_guide.scopes.updating_scopes Updating Scopes}
## Related API
* {@link api/angular.scope Angular Scope API}

View file

@ -30,9 +30,7 @@ of the following documents before returning here to the Developer Guide:
## {@link dev_guide.scopes Angular Scope Objects} ## {@link dev_guide.scopes Angular Scope Objects}
* {@link dev_guide.scopes.understanding_scopes Understanding Angular Scope Objects} * {@link dev_guide.scopes.understanding_scopes Understanding Angular Scope Objects}
* {@link dev_guide.scopes.working_scopes Working With Angular Scopes} * {@link dev_guide.scopes.internals Angular Scope Internals}
* {@link dev_guide.scopes.controlling_scopes Applying Controllers to Scopes}
* {@link dev_guide.scopes.updating_scopes Updating Scope Properties}
## {@link dev_guide.compiler Angular HTML Compiler} ## {@link dev_guide.compiler Angular HTML Compiler}

View file

@ -398,3 +398,25 @@ li {
margin: 0em 2em 1em 0em; margin: 0em 2em 1em 0em;
float:right; float:right;
} }
.table {
border-collapse: collapse;
}
.table th:first-child {
text-align: right;
}
.table th,
.table td {
border: 1px solid black;
padding: .5em 1em;
}
.table th {
white-space: nowrap;
}
.table th.section {
text-align: left;
background-color: lightgray;
}

View file

@ -17,7 +17,7 @@ function DocsController($location, $browser, $window, $cookies) {
$location.hashPath = '!/api'; $location.hashPath = '!/api';
} }
this.$watch('$location.hashPath', function(hashPath) { this.$watch('$location.hashPath', function(scope, hashPath) {
if (hashPath.match(/^!/)) { if (hashPath.match(/^!/)) {
var parts = hashPath.substring(1).split('/'); var parts = hashPath.substring(1).split('/');
self.sectionId = parts[1]; self.sectionId = parts[1];
@ -36,7 +36,7 @@ function DocsController($location, $browser, $window, $cookies) {
delete self.partialId; delete self.partialId;
} }
} }
}); })();
this.getUrl = function(page){ this.getUrl = function(page){
return '#!/' + page.section + '/' + page.id; return '#!/' + page.section + '/' + page.id;

21
perf/MiscPerf.js Normal file
View file

@ -0,0 +1,21 @@
describe('perf misc', function(){
it('operation speeds', function(){
perf(
function typeByTypeof(){ return typeof noop == 'function'; }, // WINNER
function typeByProperty() { return noop.apply && noop.call; },
function typeByConstructor() { return noop.constructor == Function; }
);
});
it('property access', function(){
var name = 'value';
var none = 'x';
var scope = {};
perf(
function direct(){ return scope.value; }, // WINNER
function byName() { return scope[name]; },
function undefinedDirect(){ return scope.x; },
function undefiendByName() { return scope[none]; }
);
});
});

View file

@ -56,7 +56,6 @@ function fromCharCode(code) { return String.fromCharCode(code); }
var _undefined = undefined, var _undefined = undefined,
_null = null, _null = null,
$$element = '$element',
$$scope = '$scope', $$scope = '$scope',
$$validate = '$validate', $$validate = '$validate',
$angular = 'angular', $angular = 'angular',
@ -65,7 +64,6 @@ var _undefined = undefined,
$console = 'console', $console = 'console',
$date = 'date', $date = 'date',
$display = 'display', $display = 'display',
$element = 'element',
$function = 'function', $function = 'function',
$length = 'length', $length = 'length',
$name = 'name', $name = 'name',
@ -573,6 +571,16 @@ function isLeafNode (node) {
return false; return false;
} }
/**
* @workInProgress
* @ngdoc function
* @name angular.copy
* @function
*
* @description
* Alias for {@link angular.Object.copy}
*/
/** /**
* @ngdoc function * @ngdoc function
* @name angular.Object.copy * @name angular.Object.copy
@ -657,6 +665,15 @@ function copy(source, destination){
return destination; return destination;
} }
/**
* @workInProgress
* @ngdoc function
* @name angular.equals
* @function
*
* @description
* Alias for {@link angular.Object.equals}
*/
/** /**
* @ngdoc function * @ngdoc function
@ -666,8 +683,8 @@ function copy(source, destination){
* @description * @description
* Determines if two objects or value are equivalent. * Determines if two objects or value are equivalent.
* *
* To be equivalent, they must pass `==` comparison or be of the same type and have all their * To be equivalent, they must pass `===` comparison or be of the same type and have all their
* properties pass `==` comparison. During property comparision properties of `function` type and * properties pass `===` comparison. During property comparision properties of `function` type and
* properties with name starting with `$` are ignored. * properties with name starting with `$` are ignored.
* *
* Supports values types, arrays and objects. * Supports values types, arrays and objects.
@ -707,7 +724,7 @@ function copy(source, destination){
* </doc:example> * </doc:example>
*/ */
function equals(o1, o2) { function equals(o1, o2) {
if (o1 == o2) return true; if (o1 === o2) return true;
if (o1 === null || o2 === null) return false; if (o1 === null || o2 === null) return false;
var t1 = typeof o1, t2 = typeof o2, length, key, keySet; var t1 = typeof o1, t2 = typeof o2, length, key, keySet;
if (t1 == t2 && t1 == 'object') { if (t1 == t2 && t1 == 'object') {
@ -779,6 +796,10 @@ function concat(array1, array2, index) {
return array1.concat(slice.call(array2, index, array2.length)); return array1.concat(slice.call(array2, index, array2.length));
} }
function sliceArgs(args, startIndex) {
return slice.call(args, startIndex || 0, args.length);
}
/** /**
* @workInProgress * @workInProgress
@ -797,9 +818,7 @@ function concat(array1, array2, index) {
* @returns {function()} Function that wraps the `fn` with all the specified bindings. * @returns {function()} Function that wraps the `fn` with all the specified bindings.
*/ */
function bind(self, fn) { function bind(self, fn) {
var curryArgs = arguments.length > 2 var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : [];
? slice.call(arguments, 2, arguments.length)
: [];
if (typeof fn == $function && !(fn instanceof RegExp)) { if (typeof fn == $function && !(fn instanceof RegExp)) {
return curryArgs.length return curryArgs.length
? function() { ? function() {
@ -939,13 +958,14 @@ function angularInit(config, document){
if (autobind) { if (autobind) {
var element = isString(autobind) ? document.getElementById(autobind) : document, var element = isString(autobind) ? document.getElementById(autobind) : document,
scope = compile(element)(createScope({'$config':config})), scope = compile(element)(createScope()),
$browser = scope.$service('$browser'); $browser = scope.$service('$browser');
if (config.css) if (config.css)
$browser.addCss(config.base_url + config.css); $browser.addCss(config.base_url + config.css);
else if(msie<8) else if(msie<8)
$browser.addJs(config.ie_compat, config.ie_compat_id); $browser.addJs(config.ie_compat, config.ie_compat_id);
scope.$apply();
} }
} }
@ -1001,7 +1021,8 @@ function assertArg(arg, name, reason) {
} }
function assertArgFn(arg, name) { function assertArgFn(arg, name) {
assertArg(isFunction(arg, name, 'not a function')); assertArg(isFunction(arg), name, 'not a function, got ' +
(typeof arg == 'object' ? arg.constructor.name : typeof arg));
} }

View file

@ -60,7 +60,7 @@ function Browser(window, document, body, XHR, $log) {
*/ */
function completeOutstandingRequest(fn) { function completeOutstandingRequest(fn) {
try { try {
fn.apply(null, slice.call(arguments, 1)); fn.apply(null, sliceArgs(arguments, 1));
} finally { } finally {
outstandingRequestCount--; outstandingRequestCount--;
if (outstandingRequestCount === 0) { if (outstandingRequestCount === 0) {

View file

@ -29,15 +29,20 @@ Template.prototype = {
inits[this.priority] = queue = []; inits[this.priority] = queue = [];
} }
if (this.newScope) { if (this.newScope) {
childScope = createScope(scope); childScope = isFunction(this.newScope) ? scope.$new(this.newScope(scope)) : scope.$new();
scope.$onEval(childScope.$eval);
element.data($$scope, childScope); element.data($$scope, childScope);
} }
// TODO(misko): refactor this!!!
// Why are inits even here?
forEach(this.inits, function(fn) { forEach(this.inits, function(fn) {
queue.push(function() { queue.push(function() {
childScope.$tryEval(function(){ childScope.$eval(function(){
return childScope.$service.invoke(childScope, fn, [element]); try {
}, element); return childScope.$service.invoke(childScope, fn, [element]);
} catch (e) {
childScope.$service('$exceptionHandler')(e);
}
});
}); });
}); });
var i, var i,
@ -218,7 +223,6 @@ Compiler.prototype = {
scope.$element = element; scope.$element = element;
(cloneConnectFn||noop)(element, scope); (cloneConnectFn||noop)(element, scope);
template.attach(element, scope); template.attach(element, scope);
scope.$eval();
return scope; return scope;
}; };
}, },
@ -228,6 +232,7 @@ Compiler.prototype = {
* @workInProgress * @workInProgress
* @ngdoc directive * @ngdoc directive
* @name angular.directive.ng:eval-order * @name angular.directive.ng:eval-order
* @deprecated
* *
* @description * @description
* Normally the view is updated from top to bottom. This usually is * Normally the view is updated from top to bottom. This usually is
@ -244,9 +249,9 @@ Compiler.prototype = {
* @example * @example
<doc:example> <doc:example>
<doc:source> <doc:source>
<div>TOTAL: without ng:eval-order {{ items.$sum('total') | currency }}</div> <div>TOTAL: without ng:eval-order {{ total | currency }}</div>
<div ng:eval-order='LAST'>TOTAL: with ng:eval-order {{ items.$sum('total') | currency }}</div> <div ng:eval-order='LAST'>TOTAL: with ng:eval-order {{ total | currency }}</div>
<table ng:init="items=[{qty:1, cost:9.99, desc:'gadget'}]"> <table ng:init="items=[{qty:1, cost:9.99, desc:'gadget'}];total=0;">
<tr> <tr>
<td>QTY</td> <td>QTY</td>
<td>Description</td> <td>Description</td>
@ -258,22 +263,22 @@ Compiler.prototype = {
<td><input name="item.qty"/></td> <td><input name="item.qty"/></td>
<td><input name="item.desc"/></td> <td><input name="item.desc"/></td>
<td><input name="item.cost"/></td> <td><input name="item.cost"/></td>
<td>{{item.total = item.qty * item.cost | currency}}</td> <td>{{item.qty * item.cost | currency}}</td>
<td><a href="" ng:click="items.$remove(item)">X</a></td> <td><a href="" ng:click="items.$remove(item)">X</a></td>
</tr> </tr>
<tr> <tr>
<td colspan="3"><a href="" ng:click="items.$add()">add</a></td> <td colspan="3"><a href="" ng:click="items.$add()">add</a></td>
<td>{{ items.$sum('total') | currency }}</td> <td>{{ total = items.$sum('qty*cost') | currency }}</td>
</tr> </tr>
</table> </table>
</doc:source> </doc:source>
<doc:scenario> <doc:scenario>
it('should check ng:format', function(){ it('should check ng:format', function(){
expect(using('.doc-example-live div:first').binding("items.$sum('total')")).toBe('$9.99'); expect(using('.doc-example-live div:first').binding("total")).toBe('$0.00');
expect(using('.doc-example-live div:last').binding("items.$sum('total')")).toBe('$9.99'); expect(using('.doc-example-live div:last').binding("total")).toBe('$9.99');
input('item.qty').enter('2'); input('item.qty').enter('2');
expect(using('.doc-example-live div:first').binding("items.$sum('total')")).toBe('$9.99'); expect(using('.doc-example-live div:first').binding("total")).toBe('$9.99');
expect(using('.doc-example-live div:last').binding("items.$sum('total')")).toBe('$19.98'); expect(using('.doc-example-live div:last').binding("total")).toBe('$19.98');
}); });
</doc:scenario> </doc:scenario>
</doc:example> </doc:example>

View file

@ -116,6 +116,9 @@ function toJsonArray(buf, obj, pretty, stack) {
sep = true; sep = true;
} }
buf.push("]"); buf.push("]");
} else if (isElement(obj)) {
// TODO(misko): maybe in dev mode have a better error reporting?
buf.push('DOM_ELEMENT');
} else if (isDate(obj)) { } else if (isDate(obj)) {
buf.push(angular.String.quoteUnicode(angular.Date.toString(obj))); buf.push(angular.String.quoteUnicode(angular.Date.toString(obj)));
} else { } else {

File diff suppressed because it is too large Load diff

View file

@ -374,7 +374,7 @@ angular.service('$browser', function(){
* See {@link angular.mock} for more info on angular mocks. * See {@link angular.mock} for more info on angular mocks.
*/ */
angular.service('$exceptionHandler', function() { angular.service('$exceptionHandler', function() {
return function(e) { throw e;}; return function(e) { throw e; };
}); });

View file

@ -7,7 +7,7 @@ var angularGlobal = {
if (type == $object) { if (type == $object) {
if (obj instanceof Array) return $array; if (obj instanceof Array) return $array;
if (isDate(obj)) return $date; if (isDate(obj)) return $date;
if (obj.nodeType == 1) return $element; if (obj.nodeType == 1) return 'element';
} }
return type; return type;
} }
@ -180,7 +180,7 @@ var angularArray = {
</doc:example> </doc:example>
*/ */
'sum':function(array, expression) { 'sum':function(array, expression) {
var fn = angular['Function']['compile'](expression); var fn = angularFunction.compile(expression);
var sum = 0; var sum = 0;
for (var i = 0; i < array.length; i++) { for (var i = 0; i < array.length; i++) {
var value = 1 * fn(array[i]); var value = 1 * fn(array[i]);
@ -522,21 +522,21 @@ var angularArray = {
</doc:source> </doc:source>
<doc:scenario> <doc:scenario>
it('should calculate counts', function() { it('should calculate counts', function() {
expect(binding('items.$count(\'points==1\')')).toEqual(2); expect(binding('items.$count(\'points==1\')')).toEqual('2');
expect(binding('items.$count(\'points>1\')')).toEqual(1); expect(binding('items.$count(\'points>1\')')).toEqual('1');
}); });
it('should recalculate when updated', function() { it('should recalculate when updated', function() {
using('.doc-example-live li:first-child').input('item.points').enter('23'); using('.doc-example-live li:first-child').input('item.points').enter('23');
expect(binding('items.$count(\'points==1\')')).toEqual(1); expect(binding('items.$count(\'points==1\')')).toEqual('1');
expect(binding('items.$count(\'points>1\')')).toEqual(2); expect(binding('items.$count(\'points>1\')')).toEqual('2');
}); });
</doc:scenario> </doc:scenario>
</doc:example> </doc:example>
*/ */
'count':function(array, condition) { 'count':function(array, condition) {
if (!condition) return array.length; if (!condition) return array.length;
var fn = angular['Function']['compile'](condition), count = 0; var fn = angularFunction.compile(condition), count = 0;
forEach(array, function(value){ forEach(array, function(value){
if (fn(value)) { if (fn(value)) {
count ++; count ++;
@ -635,7 +635,7 @@ var angularArray = {
descending = predicate.charAt(0) == '-'; descending = predicate.charAt(0) == '-';
predicate = predicate.substring(1); predicate = predicate.substring(1);
} }
get = expressionCompile(predicate).fnSelf; get = expressionCompile(predicate);
} }
return reverseComparator(function(a,b){ return reverseComparator(function(a,b){
return compare(get(a),get(b)); return compare(get(a),get(b));
@ -796,14 +796,14 @@ var angularDate = {
}; };
var angularFunction = { var angularFunction = {
'compile':function(expression) { 'compile': function(expression) {
if (isFunction(expression)){ if (isFunction(expression)){
return expression; return expression;
} else if (expression){ } else if (expression){
return expressionCompile(expression).fnSelf; return expressionCompile(expression);
} else { } else {
return identity; return identity;
} }
} }
}; };

View file

@ -73,7 +73,7 @@
*/ */
angularDirective("ng:init", function(expression){ angularDirective("ng:init", function(expression){
return function(element){ return function(element){
this.$tryEval(expression, element); this.$eval(expression);
}; };
}); });
@ -165,19 +165,19 @@ angularDirective("ng:init", function(expression){
</doc:example> </doc:example>
*/ */
angularDirective("ng:controller", function(expression){ angularDirective("ng:controller", function(expression){
this.scope(true); this.scope(function(scope){
return function(element){ var Controller =
var controller = getter(window, expression, true) || getter(this, expression, true); getter(scope, expression, true) ||
if (!controller) getter(window, expression, true);
throw "Can not find '"+expression+"' controller."; assertArgFn(Controller, expression);
if (!isFunction(controller)) return Controller;
throw "Reference '"+expression+"' is not a class."; });
this.$become(controller); return noop;
};
}); });
/** /**
* @workInProgress * @workInProgress
* @deprecated
* @ngdoc directive * @ngdoc directive
* @name angular.directive.ng:eval * @name angular.directive.ng:eval
* *
@ -208,17 +208,18 @@ angularDirective("ng:controller", function(expression){
<doc:scenario> <doc:scenario>
it('should check eval', function(){ it('should check eval', function(){
expect(binding('obj.divide')).toBe('3'); expect(binding('obj.divide')).toBe('3');
expect(binding('obj.updateCount')).toBe('2'); expect(binding('obj.updateCount')).toBe('1');
input('obj.a').enter('12'); input('obj.a').enter('12');
expect(binding('obj.divide')).toBe('6'); expect(binding('obj.divide')).toBe('6');
expect(binding('obj.updateCount')).toBe('3'); expect(binding('obj.updateCount')).toBe('2');
}); });
</doc:scenario> </doc:scenario>
</doc:example> </doc:example>
*/ */
// TODO(misko): remove me
angularDirective("ng:eval", function(expression){ angularDirective("ng:eval", function(expression){
return function(element){ return function(element){
this.$onEval(expression, element); this.$observe(expression);
}; };
}); });
@ -257,15 +258,26 @@ angularDirective("ng:bind", function(expression, element){
element.addClass('ng-binding'); element.addClass('ng-binding');
return function(element) { return function(element) {
var lastValue = noop, lastError = noop; var lastValue = noop, lastError = noop;
this.$onEval(function() { this.$observe(function(scope) {
// TODO(misko): remove error handling https://github.com/angular/angular.js/issues/347
var error, value, html, isHtml, isDomElement, var error, value, html, isHtml, isDomElement,
oldElement = this.hasOwnProperty($$element) ? this.$element : undefined; hadOwnElement = scope.hasOwnProperty('$element'),
this.$element = element; oldElement = scope.$element;
value = this.$tryEval(expression, function(e){ // TODO(misko): get rid of $element https://github.com/angular/angular.js/issues/348
scope.$element = element;
try {
value = scope.$eval(expression);
} catch (e) {
scope.$service('$exceptionHandler')(e);
error = formatError(e); error = formatError(e);
}); } finally {
this.$element = oldElement; if (hadOwnElement) {
// If we are HTML then save the raw HTML data so that we don't scope.$element = oldElement;
} else {
delete scope.$element;
}
}
// If we are HTML than save the raw HTML data so that we don't
// recompute sanitization since it is expensive. // recompute sanitization since it is expensive.
// TODO: turn this into a more generic way to compute this // TODO: turn this into a more generic way to compute this
if (isHtml = (value instanceof HTML)) if (isHtml = (value instanceof HTML))
@ -289,7 +301,7 @@ angularDirective("ng:bind", function(expression, element){
element.text(value == undefined ? '' : value); element.text(value == undefined ? '' : value);
} }
} }
}, element); });
}; };
}); });
@ -301,10 +313,14 @@ function compileBindTemplate(template){
forEach(parseBindings(template), function(text){ forEach(parseBindings(template), function(text){
var exp = binding(text); var exp = binding(text);
bindings.push(exp bindings.push(exp
? function(element){ ? function(scope, element) {
var error, value = this.$tryEval(exp, function(e){ var error, value;
try {
value = scope.$eval(exp);
} catch(e) {
scope.$service('$exceptionHandler')(e);
error = toJson(e); error = toJson(e);
}); }
elementError(element, NG_EXCEPTION, error); elementError(element, NG_EXCEPTION, error);
return error ? error : value; return error ? error : value;
} }
@ -312,20 +328,30 @@ function compileBindTemplate(template){
return text; return text;
}); });
}); });
bindTemplateCache[template] = fn = function(element, prettyPrintJson){ bindTemplateCache[template] = fn = function(scope, element, prettyPrintJson) {
var parts = [], self = this, var parts = [],
oldElement = this.hasOwnProperty($$element) ? self.$element : undefined; hadOwnElement = scope.hasOwnProperty('$element'),
self.$element = element; oldElement = scope.$element;
for ( var i = 0; i < bindings.length; i++) {
var value = bindings[i].call(self, element); // TODO(misko): get rid of $element
if (isElement(value)) scope.$element = element;
value = ''; try {
else if (isObject(value)) for (var i = 0; i < bindings.length; i++) {
value = toJson(value, prettyPrintJson); var value = bindings[i](scope, element);
parts.push(value); if (isElement(value))
value = '';
else if (isObject(value))
value = toJson(value, prettyPrintJson);
parts.push(value);
}
return parts.join('');
} finally {
if (hadOwnElement) {
scope.$element = oldElement;
} else {
delete scope.$element;
}
} }
self.$element = oldElement;
return parts.join('');
}; };
} }
return fn; return fn;
@ -372,13 +398,13 @@ angularDirective("ng:bind-template", function(expression, element){
var templateFn = compileBindTemplate(expression); var templateFn = compileBindTemplate(expression);
return function(element) { return function(element) {
var lastValue; var lastValue;
this.$onEval(function() { this.$observe(function(scope) {
var value = templateFn.call(this, element, true); var value = templateFn(scope, element, true);
if (value != lastValue) { if (value != lastValue) {
element.text(value); element.text(value);
lastValue = value; lastValue = value;
} }
}, element); });
}; };
}); });
@ -446,10 +472,10 @@ var REMOVE_ATTRIBUTES = {
angularDirective("ng:bind-attr", function(expression){ angularDirective("ng:bind-attr", function(expression){
return function(element){ return function(element){
var lastValue = {}; var lastValue = {};
this.$onEval(function(){ this.$observe(function(scope){
var values = this.$eval(expression); var values = scope.$eval(expression);
for(var key in values) { for(var key in values) {
var value = compileBindTemplate(values[key]).call(this, element), var value = compileBindTemplate(values[key])(scope, element),
specialName = REMOVE_ATTRIBUTES[lowercase(key)]; specialName = REMOVE_ATTRIBUTES[lowercase(key)];
if (lastValue[key] !== value) { if (lastValue[key] !== value) {
lastValue[key] = value; lastValue[key] = value;
@ -467,7 +493,7 @@ angularDirective("ng:bind-attr", function(expression){
} }
} }
} }
}, element); });
}; };
}); });
@ -510,14 +536,13 @@ angularDirective("ng:bind-attr", function(expression){
* TODO: maybe we should consider allowing users to control event propagation in the future. * TODO: maybe we should consider allowing users to control event propagation in the future.
*/ */
angularDirective("ng:click", function(expression, element){ angularDirective("ng:click", function(expression, element){
return annotate('$updateView', function($updateView, element){ return function(element){
var self = this; var self = this;
element.bind('click', function(event){ element.bind('click', function(event){
self.$tryEval(expression, element); self.$apply(expression);
$updateView();
event.stopPropagation(); event.stopPropagation();
}); });
}); };
}); });
@ -555,28 +580,27 @@ angularDirective("ng:click", function(expression, element){
</doc:example> </doc:example>
*/ */
angularDirective("ng:submit", function(expression, element) { angularDirective("ng:submit", function(expression, element) {
return annotate('$updateView', function($updateView, element) { return function(element) {
var self = this; var self = this;
element.bind('submit', function(event) { element.bind('submit', function(event) {
self.$tryEval(expression, element); self.$apply(expression);
$updateView();
event.preventDefault(); event.preventDefault();
}); });
}); };
}); });
function ngClass(selector) { function ngClass(selector) {
return function(expression, element){ return function(expression, element) {
var existing = element[0].className + ' '; var existing = element[0].className + ' ';
return function(element){ return function(element) {
this.$onEval(function(){ this.$observe(function(scope) {
if (selector(this.$index)) { if (selector(scope.$index)) {
var value = this.$eval(expression); var value = scope.$eval(expression);
if (isArray(value)) value = value.join(' '); if (isArray(value)) value = value.join(' ');
element[0].className = trim(existing + value); element[0].className = trim(existing + value);
} }
}, element); });
}; };
}; };
} }
@ -732,9 +756,9 @@ angularDirective("ng:class-even", ngClass(function(i){return i % 2 === 1;}));
*/ */
angularDirective("ng:show", function(expression, element){ angularDirective("ng:show", function(expression, element){
return function(element){ return function(element){
this.$onEval(function(){ this.$observe(expression, function(scope, value){
toBoolean(this.$eval(expression)) ? element.show() : element.hide(); toBoolean(value) ? element.show() : element.hide();
}, element); });
}; };
}); });
@ -773,9 +797,9 @@ angularDirective("ng:show", function(expression, element){
*/ */
angularDirective("ng:hide", function(expression, element){ angularDirective("ng:hide", function(expression, element){
return function(element){ return function(element){
this.$onEval(function(){ this.$observe(expression, function(scope, value){
toBoolean(this.$eval(expression)) ? element.hide() : element.show(); toBoolean(value) ? element.hide() : element.show();
}, element); });
}; };
}); });
@ -815,8 +839,8 @@ angularDirective("ng:hide", function(expression, element){
angularDirective("ng:style", function(expression, element){ angularDirective("ng:style", function(expression, element){
return function(element){ return function(element){
var resetStyle = getStyle(element); var resetStyle = getStyle(element);
this.$onEval(function(){ this.$observe(function(scope){
var style = this.$eval(expression) || {}, key, mergedStyle = {}; var style = scope.$eval(expression) || {}, key, mergedStyle = {};
for(key in style) { for(key in style) {
if (resetStyle[key] === undefined) resetStyle[key] = ''; if (resetStyle[key] === undefined) resetStyle[key] = '';
mergedStyle[key] = style[key]; mergedStyle[key] = style[key];
@ -825,7 +849,7 @@ angularDirective("ng:style", function(expression, element){
mergedStyle[key] = mergedStyle[key] || resetStyle[key]; mergedStyle[key] = mergedStyle[key] || resetStyle[key];
} }
element.css(mergedStyle); element.css(mergedStyle);
}, element); });
}; };
}); });

View file

@ -645,25 +645,26 @@ angularFilter.html = function(html, option){
</doc:scenario> </doc:scenario>
</doc:example> </doc:example>
*/ */
//TODO: externalize all regexps var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/,
angularFilter.linky = function(text){ MAILTO_REGEXP = /^mailto:/;
angularFilter.linky = function(text) {
if (!text) return text; if (!text) return text;
var URL = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/;
var match; var match;
var raw = text; var raw = text;
var html = []; var html = [];
var writer = htmlSanitizeWriter(html); var writer = htmlSanitizeWriter(html);
var url; var url;
var i; var i;
while (match=raw.match(URL)) { while (match = raw.match(LINKY_URL_REGEXP)) {
// We can not end in these as they are sometimes found at the end of the sentence // We can not end in these as they are sometimes found at the end of the sentence
url = match[0]; url = match[0];
// if we did not match ftp/http/mailto then assume mailto // if we did not match ftp/http/mailto then assume mailto
if (match[2]==match[3]) url = 'mailto:' + url; if (match[2] == match[3]) url = 'mailto:' + url;
i = match.index; i = match.index;
writer.chars(raw.substr(0, i)); writer.chars(raw.substr(0, i));
writer.start('a', {href:url}); writer.start('a', {href:url});
writer.chars(match[0].replace(/^mailto:/, '')); writer.chars(match[0].replace(MAILTO_REGEXP, ''));
writer.end('a'); writer.end('a');
raw = raw.substring(i + match[0].length); raw = raw.substring(i + match[0].length);
} }

View file

@ -659,5 +659,116 @@ function parser(text, json){
} }
} }
//////////////////////////////////////////////////
// Parser helper functions
//////////////////////////////////////////////////
function setter(obj, path, setValue) {
var element = path.split('.');
for (var i = 0; element.length > 1; i++) {
var key = element.shift();
var propertyObj = obj[key];
if (!propertyObj) {
propertyObj = {};
obj[key] = propertyObj;
}
obj = propertyObj;
}
obj[element.shift()] = setValue;
return setValue;
}
/**
* Return the value accesible from the object by path. Any undefined traversals are ignored
* @param {Object} obj starting object
* @param {string} path path to traverse
* @param {boolean=true} bindFnToScope
* @returns value as accesbile by path
*/
function getter(obj, path, bindFnToScope) {
if (!path) return obj;
var keys = path.split('.');
var key;
var lastInstance = obj;
var len = keys.length;
for (var i = 0; i < len; i++) {
key = keys[i];
if (obj) {
obj = (lastInstance = obj)[key];
}
if (isUndefined(obj) && key.charAt(0) == '$') {
var type = angularGlobal.typeOf(lastInstance);
type = angular[type.charAt(0).toUpperCase()+type.substring(1)];
var fn = type ? type[[key.substring(1)]] : _undefined;
if (fn) {
return obj = bind(lastInstance, fn, lastInstance);
}
}
}
if (!bindFnToScope && isFunction(obj)) {
return bind(lastInstance, obj);
}
return obj;
}
var getterFnCache = {},
compileCache = {},
JS_KEYWORDS = {};
forEach(
("abstract,boolean,break,byte,case,catch,char,class,const,continue,debugger,default," +
"delete,do,double,else,enum,export,extends,false,final,finally,float,for,function,goto," +
"if,implements,import,ininstanceof,intinterface,long,native,new,null,package,private," +
"protected,public,return,short,static,super,switch,synchronized,this,throw,throws," +
"transient,true,try,typeof,var,volatile,void,undefined,while,with").split(/,/),
function(key){ JS_KEYWORDS[key] = true;}
);
function getterFn(path) {
var fn = getterFnCache[path];
if (fn) return fn;
var code = 'var l, fn, t;\n';
forEach(path.split('.'), function(key) {
key = (JS_KEYWORDS[key]) ? '["' + key + '"]' : '.' + key;
code += 'if(!s) return s;\n' +
'l=s;\n' +
's=s' + key + ';\n' +
'if(typeof s=="function" && !(s instanceof RegExp)) s = function(){ return l' +
key + '.apply(l, arguments); };\n';
if (key.charAt(1) == '$') {
// special code for super-imposed functions
var name = key.substr(2);
code += 'if(!s) {\n' +
' t = angular.Global.typeOf(l);\n' +
' fn = (angular[t.charAt(0).toUpperCase() + t.substring(1)]||{})["' + name + '"];\n' +
' if (fn) s = function(){ return fn.apply(l, ' +
'[l].concat(Array.prototype.slice.call(arguments, 0, arguments.length))); };\n' +
'}\n';
}
});
code += 'return s;';
fn = Function('s', code);
fn["toString"] = function(){ return code; };
return getterFnCache[path] = fn;
}
///////////////////////////////////
// TODO(misko): Should this function be public?
function compileExpr(expr) {
return parser(expr).statements();
}
// TODO(misko): Deprecate? Remove!
// I think that compilation should be a service.
function expressionCompile(exp) {
if (typeof exp === $function) return exp;
var fn = compileCache[exp];
if (!fn) {
fn = compileCache[exp] = parser(exp).statements();
}
return fn;
}

View file

@ -163,9 +163,13 @@ angular.scenario.Runner.prototype.createSpecRunner_ = function(scope) {
*/ */
angular.scenario.Runner.prototype.run = function(application) { angular.scenario.Runner.prototype.run = function(application) {
var self = this; var self = this;
var $root = angular.scope(this); var $root = angular.scope();
angular.extend($root, this);
angular.forEach(angular.scenario.Runner.prototype, function(fn, name) {
$root[name] = angular.bind(self, fn);
});
$root.application = application; $root.application = application;
this.emit('RunnerBegin'); $root.emit('RunnerBegin');
asyncForEach(this.rootDescribe.getSpecs(), function(spec, specDone) { asyncForEach(this.rootDescribe.getSpecs(), function(spec, specDone) {
var dslCache = {}; var dslCache = {};
var runner = self.createSpecRunner_($root); var runner = self.createSpecRunner_($root);
@ -175,7 +179,7 @@ angular.scenario.Runner.prototype.run = function(application) {
angular.forEach(angular.scenario.dsl, function(fn, key) { angular.forEach(angular.scenario.dsl, function(fn, key) {
self.$window[key] = function() { self.$window[key] = function() {
var line = callerFile(3); var line = callerFile(3);
var scope = angular.scope(runner); var scope = runner.$new();
// Make the dsl accessible on the current chain // Make the dsl accessible on the current chain
scope.dsl = {}; scope.dsl = {};
@ -200,7 +204,10 @@ angular.scenario.Runner.prototype.run = function(application) {
return scope.dsl[key].apply(scope, arguments); return scope.dsl[key].apply(scope, arguments);
}; };
}); });
runner.run(spec, specDone); runner.run(spec, function() {
runner.$destroy();
specDone.apply(this, arguments);
});
}, },
function(error) { function(error) {
if (error) { if (error) {

View file

@ -245,7 +245,6 @@ angular.scenario.dsl('repeater', function() {
chain.row = function(index) { chain.row = function(index) {
return this.addFutureAction("repeater '" + this.label + "' row '" + index + "'", function($window, $document, done) { return this.addFutureAction("repeater '" + this.label + "' row '" + index + "'", function($window, $document, done) {
var values = [];
var matches = $document.elements().slice(index, index + 1); var matches = $document.elements().slice(index, index + 1);
if (!matches.length) if (!matches.length)
return done('row ' + index + ' out of bounds'); return done('row ' + index + ' out of bounds');

View file

@ -28,7 +28,7 @@ angularServiceInject('$cookies', function($browser) {
lastBrowserCookies = currentCookies; lastBrowserCookies = currentCookies;
copy(currentCookies, lastCookies); copy(currentCookies, lastCookies);
copy(currentCookies, cookies); copy(currentCookies, cookies);
if (runEval) rootScope.$eval(); if (runEval) rootScope.$apply();
} }
})(); })();
@ -37,7 +37,7 @@ angularServiceInject('$cookies', function($browser) {
//at the end of each eval, push cookies //at the end of each eval, push cookies
//TODO: this should happen before the "delayed" watches fire, because if some cookies are not //TODO: this should happen before the "delayed" watches fire, because if some cookies are not
// strings or browser refuses to store some cookies, we update the model in the push fn. // strings or browser refuses to store some cookies, we update the model in the push fn.
this.$onEval(PRIORITY_LAST, push); this.$observe(push);
return cookies; return cookies;

View file

@ -18,16 +18,11 @@
* @param {function()} fn A function, who's execution should be deferred. * @param {function()} fn A function, who's execution should be deferred.
* @param {number=} [delay=0] of milliseconds to defer the function execution. * @param {number=} [delay=0] of milliseconds to defer the function execution.
*/ */
angularServiceInject('$defer', function($browser, $exceptionHandler, $updateView) { angularServiceInject('$defer', function($browser) {
var scope = this;
return function(fn, delay) { return function(fn, delay) {
$browser.defer(function() { $browser.defer(function() {
try { scope.$apply(fn);
fn();
} catch(e) {
$exceptionHandler(e);
} finally {
$updateView();
}
}, delay); }, delay);
}; };
}, ['$browser', '$exceptionHandler', '$updateView']); }, ['$browser', '$exceptionHandler', '$updateView']);

View file

@ -42,7 +42,7 @@ angularServiceInject("$invalidWidgets", function(){
/* At the end of each eval removes all invalid widgets that are not part of the current DOM. */ /* At the end of each eval removes all invalid widgets that are not part of the current DOM. */
this.$onEval(PRIORITY_LAST, function() { this.$watch(function() {
for(var i = 0; i < invalidWidgets.length;) { for(var i = 0; i < invalidWidgets.length;) {
var widget = invalidWidgets[i]; var widget = invalidWidgets[i];
if (isOrphan(widget[0])) { if (isOrphan(widget[0])) {
@ -56,7 +56,7 @@ angularServiceInject("$invalidWidgets", function(){
/** /**
* Traverses DOM element's (widget's) parents and considers the element to be an orphant if one of * Traverses DOM element's (widget's) parents and considers the element to be an orphan if one of
* it's parents isn't the current window.document. * it's parents isn't the current window.document.
*/ */
function isOrphan(widget) { function isOrphan(widget) {

View file

@ -69,18 +69,14 @@ var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+)
</doc:example> </doc:example>
*/ */
angularServiceInject("$location", function($browser) { angularServiceInject("$location", function($browser) {
var scope = this, var location = {update: update, updateHash: updateHash};
location = {update:update, updateHash: updateHash}, var lastLocation = {}; // last state since last update().
lastLocation = {};
$browser.onHashChange(function() { //register $browser.onHashChange(bind(this, this.$apply, function() { //register
update($browser.getUrl()); update($browser.getUrl());
copy(location, lastLocation); }))(); //initialize
scope.$eval();
})(); //initialize
this.$onEval(PRIORITY_FIRST, sync); this.$watch(sync);
this.$onEval(PRIORITY_LAST, updateBrowser);
return location; return location;
@ -94,6 +90,8 @@ angularServiceInject("$location", function($browser) {
* *
* @description * @description
* Updates the location object. * Updates the location object.
* Does not immediately update the browser
* Browser is updated at the end of $flush()
* *
* Does not immediately update the browser. Instead the browser is updated at the end of $eval() * Does not immediately update the browser. Instead the browser is updated at the end of $eval()
* cycle. * cycle.
@ -122,6 +120,8 @@ angularServiceInject("$location", function($browser) {
location.href = composeHref(location); location.href = composeHref(location);
} }
$browser.setUrl(location.href);
copy(location, lastLocation);
} }
/** /**
@ -188,33 +188,20 @@ angularServiceInject("$location", function($browser) {
if (!equals(location, lastLocation)) { if (!equals(location, lastLocation)) {
if (location.href != lastLocation.href) { if (location.href != lastLocation.href) {
update(location.href); update(location.href);
return;
}
if (location.hash != lastLocation.hash) {
var hash = parseHash(location.hash);
updateHash(hash.hashPath, hash.hashSearch);
} else { } else {
location.hash = composeHash(location); if (location.hash != lastLocation.hash) {
location.href = composeHref(location); var hash = parseHash(location.hash);
updateHash(hash.hashPath, hash.hashSearch);
} else {
location.hash = composeHash(location);
location.href = composeHref(location);
}
update(location.href);
} }
update(location.href);
} }
} }
/**
* If location has changed, update the browser
* This method is called at the end of $eval() phase
*/
function updateBrowser() {
sync();
if ($browser.getUrl() != location.href) {
$browser.setUrl(location.href);
copy(location, lastLocation);
}
}
/** /**
* Compose href string from a location object * Compose href string from a location object
* *

View file

@ -62,7 +62,7 @@
</doc:scenario> </doc:scenario>
</doc:example> </doc:example>
*/ */
angularServiceInject('$route', function(location, $updateView) { angularServiceInject('$route', function($location, $updateView) {
var routes = {}, var routes = {},
onChange = [], onChange = [],
matcher = switchRouteMatcher, matcher = switchRouteMatcher,
@ -207,66 +207,67 @@ angularServiceInject('$route', function(location, $updateView) {
function updateRoute(){ function updateRoute(){
var childScope, routeParams, pathParams, segmentMatch, key, redir; var selectedRoute, pathParams, segmentMatch, key, redir;
if ($route.current && $route.current.scope) {
$route.current.scope.$destroy();
}
$route.current = null; $route.current = null;
// Match a route
forEach(routes, function(rParams, rPath) { forEach(routes, function(rParams, rPath) {
if (!pathParams) { if (!pathParams) {
if (pathParams = matcher(location.hashPath, rPath)) { if (pathParams = matcher($location.hashPath, rPath)) {
routeParams = rParams; selectedRoute = rParams;
} }
} }
}); });
// "otherwise" fallback // No route matched; fallback to "otherwise" route
routeParams = routeParams || routes[null]; selectedRoute = selectedRoute || routes[null];
if(routeParams) { if(selectedRoute) {
if (routeParams.redirectTo) { if (selectedRoute.redirectTo) {
if (isString(routeParams.redirectTo)) { if (isString(selectedRoute.redirectTo)) {
// interpolate the redirectTo string // interpolate the redirectTo string
redir = {hashPath: '', redir = {hashPath: '',
hashSearch: extend({}, location.hashSearch, pathParams)}; hashSearch: extend({}, $location.hashSearch, pathParams)};
forEach(routeParams.redirectTo.split(':'), function(segment, i) { forEach(selectedRoute.redirectTo.split(':'), function(segment, i) {
if (i==0) { if (i==0) {
redir.hashPath += segment; redir.hashPath += segment;
} else { } else {
segmentMatch = segment.match(/(\w+)(.*)/); segmentMatch = segment.match(/(\w+)(.*)/);
key = segmentMatch[1]; key = segmentMatch[1];
redir.hashPath += pathParams[key] || location.hashSearch[key]; redir.hashPath += pathParams[key] || $location.hashSearch[key];
redir.hashPath += segmentMatch[2] || ''; redir.hashPath += segmentMatch[2] || '';
delete redir.hashSearch[key]; delete redir.hashSearch[key];
} }
}); });
} else { } else {
// call custom redirectTo function // call custom redirectTo function
redir = {hash: routeParams.redirectTo(pathParams, location.hash, location.hashPath, redir = {hash: selectedRoute.redirectTo(pathParams, $location.hash, $location.hashPath,
location.hashSearch)}; $location.hashSearch)};
} }
location.update(redir); $location.update(redir);
$updateView(); //TODO this is to work around the $location<=>$browser issues
return; return;
} }
childScope = createScope(parentScope); $route.current = extend({}, selectedRoute);
$route.current = extend({}, routeParams, { $route.current.params = extend({}, $location.hashSearch, pathParams);
scope: childScope,
params: extend({}, location.hashSearch, pathParams)
});
} }
//fire onChange callbacks //fire onChange callbacks
forEach(onChange, parentScope.$tryEval); forEach(onChange, parentScope.$eval, parentScope);
if (childScope) { // Create the scope if we have mtched a route
childScope.$become($route.current.controller); if ($route.current) {
$route.current.scope = parentScope.$new($route.current.controller);
} }
} }
this.$watch(function(){return dirty + location.hash;}, updateRoute); this.$watch(function(){return dirty + $location.hash;}, updateRoute)();
return $route; return $route;
}, ['$location', '$updateView']); }, ['$location', '$updateView']);

View file

@ -35,8 +35,8 @@
* without angular knowledge and you may need to call '$updateView()' directly. * without angular knowledge and you may need to call '$updateView()' directly.
* *
* Note: if you wish to update the view immediately (without delay), you can do so by calling * Note: if you wish to update the view immediately (without delay), you can do so by calling
* {@link angular.scope.$eval} at any time from your code: * {@link angular.scope.$apply} at any time from your code:
* <pre>scope.$root.$eval()</pre> * <pre>scope.$apply()</pre>
* *
* In unit-test mode the update is instantaneous and synchronous to simplify writing tests. * In unit-test mode the update is instantaneous and synchronous to simplify writing tests.
* *
@ -47,7 +47,7 @@ function serviceUpdateViewFactory($browser){
var scheduled; var scheduled;
function update(){ function update(){
scheduled = false; scheduled = false;
rootScope.$eval(); rootScope.$flush();
} }
return $browser.isMock ? update : function(){ return $browser.isMock ? update : function(){
if (!scheduled) { if (!scheduled) {

View file

@ -82,6 +82,6 @@ angularServiceInject('$xhr.bulk', function($xhr, $error, $log){
} }
}); });
}; };
this.$onEval(PRIORITY_LAST, bulkXHR.flush); this.$observe(bulkXHR.flush);
return bulkXHR; return bulkXHR;
}, ['$xhr', '$xhr.error', '$log']); }, ['$xhr', '$xhr.error', '$log']);

View file

@ -183,9 +183,7 @@ function modelAccessor(scope, element) {
}, },
set: function(value) { set: function(value) {
if (value !== undefined) { if (value !== undefined) {
return scope.$tryEval(function(){ assignFn(scope, value);
assignFn(scope, value);
}, element);
} }
} }
}; };
@ -332,7 +330,7 @@ function valueAccessor(scope, element) {
format = formatter.format; format = formatter.format;
parse = formatter.parse; parse = formatter.parse;
if (requiredExpr) { if (requiredExpr) {
scope.$watch(requiredExpr, function(newValue) { scope.$watch(requiredExpr, function(scope, newValue) {
required = newValue; required = newValue;
validate(); validate();
}); });
@ -529,32 +527,33 @@ function radioInit(model, view, element) {
</doc:example> </doc:example>
*/ */
function inputWidget(events, modelAccessor, viewAccessor, initFn, textBox) { function inputWidget(events, modelAccessor, viewAccessor, initFn, textBox) {
return annotate('$updateView', '$defer', function($updateView, $defer, element) { return annotate('$defer', function($defer, element) {
var scope = this, var scope = this,
model = modelAccessor(scope, element), model = modelAccessor(scope, element),
view = viewAccessor(scope, element), view = viewAccessor(scope, element),
action = element.attr('ng:change') || '', action = element.attr('ng:change') || noop,
lastValue; lastValue;
if (model) { if (model) {
initFn.call(scope, model, view, element); initFn.call(scope, model, view, element);
this.$eval(element.attr('ng:init')||''); scope.$eval(element.attr('ng:init') || noop);
element.bind(events, function(event){ element.bind(events, function(event){
function handler(){ function handler(){
var value = view.get(); scope.$apply(function() {
if (!textBox || value != lastValue) { var value = view.get();
model.set(value); if (!textBox || value != lastValue) {
lastValue = model.get(); model.set(value);
scope.$tryEval(action, element); lastValue = model.get();
$updateView(); scope.$eval(action);
} }
});
} }
event.type == 'keydown' ? $defer(handler) : handler(); event.type == 'keydown' ? $defer(handler) : handler();
}); });
scope.$watch(model.get, function(value){ scope.$watch(model.get, function(scope, value) {
if (lastValue !== value) { if (!equals(lastValue, value)) {
view.set(lastValue = value); view.set(lastValue = value);
} }
}); })();
} }
}); });
} }
@ -693,7 +692,7 @@ angularWidget('select', function(element){
var isMultiselect = element.attr('multiple'), var isMultiselect = element.attr('multiple'),
expression = element.attr('ng:options'), expression = element.attr('ng:options'),
onChange = expressionCompile(element.attr('ng:change') || "").fnSelf, onChange = expressionCompile(element.attr('ng:change') || ""),
match; match;
if (!expression) { if (!expression) {
@ -705,12 +704,12 @@ angularWidget('select', function(element){
" but got '" + expression + "'."); " but got '" + expression + "'.");
} }
var displayFn = expressionCompile(match[2] || match[1]).fnSelf, var displayFn = expressionCompile(match[2] || match[1]),
valueName = match[4] || match[6], valueName = match[4] || match[6],
keyName = match[5], keyName = match[5],
groupByFn = expressionCompile(match[3] || '').fnSelf, groupByFn = expressionCompile(match[3] || ''),
valueFn = expressionCompile(match[2] ? match[1] : valueName).fnSelf, valueFn = expressionCompile(match[2] ? match[1] : valueName),
valuesFn = expressionCompile(match[7]).fnSelf, valuesFn = expressionCompile(match[7]),
// we can't just jqLite('<option>') since jqLite is not smart enough // we can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise. // to create it in <select> and IE barfs otherwise.
optionTemplate = jqLite(document.createElement('option')), optionTemplate = jqLite(document.createElement('option')),
@ -773,17 +772,14 @@ angularWidget('select', function(element){
onChange(scope); onChange(scope);
model.set(value); model.set(value);
} }
scope.$tryEval(function(){ scope.$root.$apply();
scope.$root.$eval();
});
} finally { } finally {
tempScope = null; // TODO(misko): needs to be $destroy tempScope = null; // TODO(misko): needs to be $destroy
} }
}); });
scope.$onEval(function(){ scope.$observe(function(scope) {
var scope = this, var optionGroups = {'':[]}, // Temporary location for the option groups before we render them
optionGroups = {'':[]}, // Temporary location for the option groups before we render them
optionGroupNames = [''], optionGroupNames = [''],
optionGroupName, optionGroupName,
optionGroup, optionGroup,
@ -934,7 +930,7 @@ angularWidget('select', function(element){
optionGroupsCache.pop()[0].element.remove(); optionGroupsCache.pop()[0].element.remove();
} }
} finally { } finally {
optionScope = null; // TODO(misko): needs to be $destroy() optionScope.$destroy();
} }
}); });
}; };
@ -998,33 +994,36 @@ angularWidget('ng:include', function(element){
} else { } else {
element[0]['ng:compiled'] = true; element[0]['ng:compiled'] = true;
return extend(function(xhr, element){ return extend(function(xhr, element){
var scope = this, childScope; var scope = this,
var changeCounter = 0; changeCounter = 0,
var preventRecursion = false; releaseScopes = [],
function incrementChange(){ changeCounter++;} childScope,
this.$watch(srcExp, incrementChange); oldScope;
this.$watch(scopeExp, incrementChange);
// note that this propagates eval to the current childScope, where childScope is dynamically function incrementChange(){ changeCounter++;}
// bound (via $route.onChange callback) to the current scope created by $route this.$observe(srcExp, incrementChange);
scope.$onEval(function(){ this.$observe(function(scope){
if (childScope && !preventRecursion) { var newScope = scope.$eval(scopeExp);
preventRecursion = true; if (newScope !== oldScope) {
try { oldScope = newScope;
childScope.$eval(); incrementChange();
} finally {
preventRecursion = false;
}
} }
}); });
this.$watch(function(){return changeCounter;}, function(){ this.$observe(function(){return changeCounter;}, function(scope) {
var src = this.$eval(srcExp), var src = scope.$eval(srcExp),
useScope = this.$eval(scopeExp); useScope = scope.$eval(scopeExp);
while(releaseScopes.length) {
releaseScopes.pop().$destroy();
}
if (src) { if (src) {
xhr('GET', src, null, function(code, response){ xhr('GET', src, null, function(code, response){
element.html(response); element.html(response);
childScope = useScope || createScope(scope); if (useScope) {
childScope = useScope;
} else {
releaseScopes.push(childScope = scope.$new());
}
compiler.compile(element)(childScope); compiler.compile(element)(childScope);
scope.$eval(onloadExp); scope.$eval(onloadExp);
}, false, true); }, false, true);
@ -1091,69 +1090,56 @@ angularWidget('ng:include', function(element){
</doc:scenario> </doc:scenario>
</doc:example> </doc:example>
*/ */
//TODO(im): remove all the code related to using and inline equals angularWidget('ng:switch', function (element) {
var ngSwitch = angularWidget('ng:switch', function (element){
var compiler = this, var compiler = this,
watchExpr = element.attr("on"), watchExpr = element.attr("on"),
usingExpr = (element.attr("using") || 'equals'), changeExpr = element.attr('change'),
usingExprParams = usingExpr.split(":"), casesTemplate = {},
usingFn = ngSwitch[usingExprParams.shift()], defaultCaseTemplate,
changeExpr = element.attr('change') || '', children = element.children(),
cases = []; length = children.length,
if (!usingFn) throw "Using expression '" + usingExpr + "' unknown."; child,
if (!watchExpr) throw "Missing 'on' attribute."; when;
eachNode(element, function(caseElement){
var when = caseElement.attr('ng:switch-when'); if (!watchExpr) throw new Error("Missing 'on' attribute.");
var switchCase = { while(length--) {
change: changeExpr, child = jqLite(children[length]);
element: caseElement, // this needs to be here for IE
template: compiler.compile(caseElement) child.remove();
}; when = child.attr('ng:switch-when');
if (isString(when)) { if (isString(when)) {
switchCase.when = function(scope, value){ casesTemplate[when] = compiler.compile(child);
var args = [value, when]; } else if (isString(child.attr('ng:switch-default'))) {
forEach(usingExprParams, function(arg){ defaultCaseTemplate = compiler.compile(child);
args.push(arg);
});
return usingFn.apply(scope, args);
};
cases.unshift(switchCase);
} else if (isString(caseElement.attr('ng:switch-default'))) {
switchCase.when = valueFn(true);
cases.push(switchCase);
} }
}); }
children = null; // release memory;
// this needs to be here for IE
forEach(cases, function(_case){
_case.element.remove();
});
element.html(''); element.html('');
return function(element){ return function(element){
var scope = this, childScope; var changeCounter = 0;
this.$watch(watchExpr, function(value){ var childScope;
var found = false; var selectedTemplate;
this.$watch(watchExpr, function(scope, value) {
element.html(''); element.html('');
childScope = createScope(scope); if (selectedTemplate = casesTemplate[value] || defaultCaseTemplate) {
forEach(cases, function(switchCase){ changeCounter++;
if (!found && switchCase.when(childScope, value)) { if (childScope) childScope.$destroy();
found = true; childScope = scope.$new();
childScope.$tryEval(switchCase.change, element); childScope.$eval(changeExpr);
switchCase.template(childScope, function(caseElement){ }
element.append(caseElement); })();
});
} this.$observe(function(){return changeCounter;}, function() {
}); element.html('');
}); if (selectedTemplate) {
scope.$onEval(function(){ selectedTemplate(childScope, function(caseElement) {
if (childScope) childScope.$eval(); element.append(caseElement);
});
}
}); });
}; };
}, {
equals: function(on, when) {
return ''+on == when;
}
}); });
@ -1267,15 +1253,16 @@ angularWidget('@ng:repeat', function(expression, element){
valueIdent = match[3] || match[1]; valueIdent = match[3] || match[1];
keyIdent = match[2]; keyIdent = match[2];
var children = [], currentScope = this; var childScopes = [];
this.$onEval(function(){ var childElements = [iterStartElement];
var parentScope = this;
this.$observe(function(scope){
var index = 0, var index = 0,
childCount = children.length, childCount = childScopes.length,
lastIterElement = iterStartElement, collection = scope.$eval(rhs),
collection = this.$tryEval(rhs, iterStartElement),
collectionLength = size(collection, true), collectionLength = size(collection, true),
fragment = (element[0].nodeName != 'OPTION') ? document.createDocumentFragment() : null, fragment = document.createDocumentFragment(),
addFragment, addFragmentTo = (childCount < collectionLength) ? childElements[childCount] : null,
childScope, childScope,
key; key;
@ -1283,35 +1270,32 @@ angularWidget('@ng:repeat', function(expression, element){
if (collection.hasOwnProperty(key)) { if (collection.hasOwnProperty(key)) {
if (index < childCount) { if (index < childCount) {
// reuse existing child // reuse existing child
childScope = children[index]; childScope = childScopes[index];
childScope[valueIdent] = collection[key]; childScope[valueIdent] = collection[key];
if (keyIdent) childScope[keyIdent] = key; if (keyIdent) childScope[keyIdent] = key;
lastIterElement = childScope.$element;
childScope.$position = index == 0 childScope.$position = index == 0
? 'first' ? 'first'
: (index == collectionLength - 1 ? 'last' : 'middle'); : (index == collectionLength - 1 ? 'last' : 'middle');
childScope.$eval(); childScope.$eval();
} else { } else {
// grow children // grow children
childScope = createScope(currentScope); childScope = parentScope.$new();
childScope[valueIdent] = collection[key]; childScope[valueIdent] = collection[key];
if (keyIdent) childScope[keyIdent] = key; if (keyIdent) childScope[keyIdent] = key;
childScope.$index = index; childScope.$index = index;
childScope.$position = index == 0 childScope.$position = index == 0
? 'first' ? 'first'
: (index == collectionLength - 1 ? 'last' : 'middle'); : (index == collectionLength - 1 ? 'last' : 'middle');
children.push(childScope); childScopes.push(childScope);
linker(childScope, function(clone){ linker(childScope, function(clone){
clone.attr('ng:repeat-index', index); clone.attr('ng:repeat-index', index);
fragment.appendChild(clone[0]);
if (fragment) { // TODO(misko): Temporary hack - maybe think about it - removed after we add fragment after $flush()
fragment.appendChild(clone[0]); // This causes double $flush for children
addFragment = true; // The first flush will couse a lot of DOM access (initial)
} else { // Second flush shuld be noop since nothing has change hence no DOM access.
//temporarily preserve old way for option element childScope.$flush();
lastIterElement.after(clone); childElements[index + 1] = clone;
lastIterElement = clone;
}
}); });
} }
index ++; index ++;
@ -1319,15 +1303,19 @@ angularWidget('@ng:repeat', function(expression, element){
} }
//attach new nodes buffered in doc fragment //attach new nodes buffered in doc fragment
if (addFragment) { if (addFragmentTo) {
lastIterElement.after(jqLite(fragment)); // TODO(misko): For performance reasons, we should do the addition after all other widgets
// have run. For this should happend after $flush() is done!
addFragmentTo.after(jqLite(fragment));
} }
// shrink children // shrink children
while(children.length > index) { while(childScopes.length > index) {
children.pop().$element.remove(); // can not use $destroy(true) since there may be multiple iterators on same parent.
childScopes.pop().$destroy();
childElements.pop().remove();
} }
}, iterStartElement); });
}; };
}); });
@ -1438,39 +1426,29 @@ angularWidget('ng:view', function(element) {
if (!element[0]['ng:compiled']) { if (!element[0]['ng:compiled']) {
element[0]['ng:compiled'] = true; element[0]['ng:compiled'] = true;
return annotate('$xhr.cache', '$route', function($xhr, $route, element){ return annotate('$xhr.cache', '$route', function($xhr, $route, element){
var parentScope = this, var template;
childScope; var changeCounter = 0;
$route.onChange(function(){ $route.onChange(function(){
var src; changeCounter++;
})(); //initialize the state forcefully, it's possible that we missed the initial
//$route#onChange already
if ($route.current) { this.$observe(function(){return changeCounter;}, function() {
src = $route.current.template; var template = $route.current && $route.current.template;
childScope = $route.current.scope; if (template) {
}
if (src) {
//xhr's callback must be async, see commit history for more info //xhr's callback must be async, see commit history for more info
$xhr('GET', src, function(code, response){ $xhr('GET', template, function(code, response) {
element.html(response); element.html(response);
compiler.compile(element)(childScope); compiler.compile(element)($route.current.scope);
}); });
} else { } else {
element.html(''); element.html('');
} }
})(); //initialize the state forcefully, it's possible that we missed the initial
//$route#onChange already
// note that this propagates eval to the current childScope, where childScope is dynamically
// bound (via $route.onChange callback) to the current scope created by $route
parentScope.$onEval(function() {
if (childScope) {
childScope.$eval();
}
}); });
}); });
} else { } else {
this.descend(true); compiler.descend(true);
this.directives(true); compiler.directives(true);
} }
}); });

View file

@ -63,7 +63,8 @@ describe('angular', function(){
it('should return true if same object', function(){ it('should return true if same object', function(){
var o = {}; var o = {};
expect(equals(o, o)).toEqual(true); expect(equals(o, o)).toEqual(true);
expect(equals(1, '1')).toEqual(true); expect(equals(o, {})).toEqual(true);
expect(equals(1, '1')).toEqual(false);
expect(equals(1, '2')).toEqual(false); expect(equals(1, '2')).toEqual(false);
}); });
@ -550,6 +551,7 @@ describe('angular', function(){
it('should link to existing node and create scope', function(){ it('should link to existing node and create scope', function(){
template = angular.element('<div>{{greeting = "hello world"}}</div>'); template = angular.element('<div>{{greeting = "hello world"}}</div>');
scope = angular.compile(template)(); scope = angular.compile(template)();
scope.$flush();
expect(template.text()).toEqual('hello world'); expect(template.text()).toEqual('hello world');
expect(scope.greeting).toEqual('hello world'); expect(scope.greeting).toEqual('hello world');
}); });
@ -558,6 +560,7 @@ describe('angular', function(){
scope = angular.scope(); scope = angular.scope();
template = angular.element('<div>{{greeting = "hello world"}}</div>'); template = angular.element('<div>{{greeting = "hello world"}}</div>');
angular.compile(template)(scope); angular.compile(template)(scope);
scope.$flush();
expect(template.text()).toEqual('hello world'); expect(template.text()).toEqual('hello world');
expect(scope).toEqual(scope); expect(scope).toEqual(scope);
}); });
@ -572,6 +575,7 @@ describe('angular', function(){
templateFn(scope, function(clone){ templateFn(scope, function(clone){
templateClone = clone; templateClone = clone;
}); });
scope.$flush();
expect(template.text()).toEqual(''); expect(template.text()).toEqual('');
expect(scope.$element.text()).toEqual('hello world'); expect(scope.$element.text()).toEqual('hello world');
@ -582,7 +586,7 @@ describe('angular', function(){
it('should link to cloned node and create scope', function(){ it('should link to cloned node and create scope', function(){
scope = angular.scope(); scope = angular.scope();
template = jqLite('<div>{{greeting = "hello world"}}</div>'); template = jqLite('<div>{{greeting = "hello world"}}</div>');
angular.compile(template)(scope, noop); angular.compile(template)(scope, noop).$flush();
expect(template.text()).toEqual(''); expect(template.text()).toEqual('');
expect(scope.$element.text()).toEqual('hello world'); expect(scope.$element.text()).toEqual('hello world');
expect(scope.greeting).toEqual('hello world'); expect(scope.greeting).toEqual('hello world');

View file

@ -1,11 +1,10 @@
'use strict'; 'use strict';
describe('Binder', function(){ describe('Binder', function(){
beforeEach(function(){ beforeEach(function(){
var self = this; var self = this;
this.compile = function(html, parent) { this.compile = function(html, parent, logErrors) {
if (self.element) dealoc(self.element); if (self.element) dealoc(self.element);
var element; var element;
if (parent) { if (parent) {
@ -15,7 +14,8 @@ describe('Binder', function(){
element = jqLite(html); element = jqLite(html);
} }
self.element = element; self.element = element;
return angular.compile(element)(); return angular.compile(element)(angular.scope(null,
logErrors ? {'$exceptionHandler': $exceptionHandlerMockFactory()} : null));
}; };
this.compileToHtml = function (content) { this.compileToHtml = function (content) {
return sortedHtml(this.compile(content).$element); return sortedHtml(this.compile(content).$element);
@ -31,20 +31,20 @@ describe('Binder', function(){
it('text-field should default to value attribute', function(){ it('text-field should default to value attribute', function(){
var scope = this.compile('<input type="text" name="model.price" value="abc">'); var scope = this.compile('<input type="text" name="model.price" value="abc">');
scope.$eval(); scope.$apply();
assertEquals('abc', scope.model.price); assertEquals('abc', scope.model.price);
}); });
it('ChangingTextareaUpdatesModel', function(){ it('ChangingTextareaUpdatesModel', function(){
var scope = this.compile('<textarea name="model.note">abc</textarea>'); var scope = this.compile('<textarea name="model.note">abc</textarea>');
scope.$eval(); scope.$apply();
assertEquals(scope.model.note, 'abc'); assertEquals(scope.model.note, 'abc');
}); });
it('ChangingRadioUpdatesModel', function(){ it('ChangingRadioUpdatesModel', function(){
var scope = this.compile('<div><input type="radio" name="model.price" value="A" checked>' + var scope = this.compile('<div><input type="radio" name="model.price" value="A" checked>' +
'<input type="radio" name="model.price" value="B"></div>'); '<input type="radio" name="model.price" value="B"></div>');
scope.$eval(); scope.$apply();
assertEquals(scope.model.price, 'A'); assertEquals(scope.model.price, 'A');
}); });
@ -55,7 +55,8 @@ describe('Binder', function(){
it('BindUpdate', function(){ it('BindUpdate', function(){
var scope = this.compile('<div ng:eval="a=123"/>'); var scope = this.compile('<div ng:eval="a=123"/>');
assertEquals(123, scope.$get('a')); scope.$flush();
assertEquals(123, scope.a);
}); });
it('ChangingSelectNonSelectedUpdatesModel', function(){ it('ChangingSelectNonSelectedUpdatesModel', function(){
@ -69,7 +70,7 @@ describe('Binder', function(){
'<option value="B" selected>Extra padding</option>' + '<option value="B" selected>Extra padding</option>' +
'<option value="C">Expedite</option>' + '<option value="C">Expedite</option>' +
'</select>'); '</select>');
assertJsonEquals(["A", "B"], scope.$get('Invoice').options); assertJsonEquals(["A", "B"], scope.Invoice.options);
}); });
it('ChangingSelectSelectedUpdatesModel', function(){ it('ChangingSelectSelectedUpdatesModel', function(){
@ -79,19 +80,19 @@ describe('Binder', function(){
it('ExecuteInitialization', function(){ it('ExecuteInitialization', function(){
var scope = this.compile('<div ng:init="a=123">'); var scope = this.compile('<div ng:init="a=123">');
assertEquals(scope.$get('a'), 123); assertEquals(scope.a, 123);
}); });
it('ExecuteInitializationStatements', function(){ it('ExecuteInitializationStatements', function(){
var scope = this.compile('<div ng:init="a=123;b=345">'); var scope = this.compile('<div ng:init="a=123;b=345">');
assertEquals(scope.$get('a'), 123); assertEquals(scope.a, 123);
assertEquals(scope.$get('b'), 345); assertEquals(scope.b, 345);
}); });
it('ApplyTextBindings', function(){ it('ApplyTextBindings', function(){
var scope = this.compile('<div ng:bind="model.a">x</div>'); var scope = this.compile('<div ng:bind="model.a">x</div>');
scope.$set('model', {a:123}); scope.model = {a:123};
scope.$eval(); scope.$apply();
assertEquals('123', scope.$element.text()); assertEquals('123', scope.$element.text());
}); });
@ -145,7 +146,7 @@ describe('Binder', function(){
it('AttributesAreEvaluated', function(){ it('AttributesAreEvaluated', function(){
var scope = this.compile('<a ng:bind-attr=\'{"a":"a", "b":"a+b={{a+b}}"}\'></a>'); var scope = this.compile('<a ng:bind-attr=\'{"a":"a", "b":"a+b={{a+b}}"}\'></a>');
scope.$eval('a=1;b=2'); scope.$eval('a=1;b=2');
scope.$eval(); scope.$apply();
var a = scope.$element; var a = scope.$element;
assertEquals(a.attr('a'), 'a'); assertEquals(a.attr('a'), 'a');
assertEquals(a.attr('b'), 'a+b=3'); assertEquals(a.attr('b'), 'a+b=3');
@ -154,9 +155,10 @@ describe('Binder', function(){
it('InputTypeButtonActionExecutesInScope', function(){ it('InputTypeButtonActionExecutesInScope', function(){
var savedCalled = false; var savedCalled = false;
var scope = this.compile('<input type="button" ng:click="person.save()" value="Apply">'); var scope = this.compile('<input type="button" ng:click="person.save()" value="Apply">');
scope.$set("person.save", function(){ scope.person = {};
scope.person.save = function(){
savedCalled = true; savedCalled = true;
}); };
browserTrigger(scope.$element, 'click'); browserTrigger(scope.$element, 'click');
assertTrue(savedCalled); assertTrue(savedCalled);
}); });
@ -164,9 +166,9 @@ describe('Binder', function(){
it('InputTypeButtonActionExecutesInScope2', function(){ it('InputTypeButtonActionExecutesInScope2', function(){
var log = ""; var log = "";
var scope = this.compile('<input type="image" ng:click="action()">'); var scope = this.compile('<input type="image" ng:click="action()">');
scope.$set("action", function(){ scope.action = function(){
log += 'click;'; log += 'click;';
}); };
expect(log).toEqual(''); expect(log).toEqual('');
browserTrigger(scope.$element, 'click'); browserTrigger(scope.$element, 'click');
expect(log).toEqual('click;'); expect(log).toEqual('click;');
@ -175,9 +177,10 @@ describe('Binder', function(){
it('ButtonElementActionExecutesInScope', function(){ it('ButtonElementActionExecutesInScope', function(){
var savedCalled = false; var savedCalled = false;
var scope = this.compile('<button ng:click="person.save()">Apply</button>'); var scope = this.compile('<button ng:click="person.save()">Apply</button>');
scope.$set("person.save", function(){ scope.person = {};
scope.person.save = function(){
savedCalled = true; savedCalled = true;
}); };
browserTrigger(scope.$element, 'click'); browserTrigger(scope.$element, 'click');
assertTrue(savedCalled); assertTrue(savedCalled);
}); });
@ -186,9 +189,9 @@ describe('Binder', function(){
var scope = this.compile('<ul><LI ng:repeat="item in model.items" ng:bind="item.a"/></ul>'); var scope = this.compile('<ul><LI ng:repeat="item in model.items" ng:bind="item.a"/></ul>');
var form = scope.$element; var form = scope.$element;
var items = [{a:"A"}, {a:"B"}]; var items = [{a:"A"}, {a:"B"}];
scope.$set('model', {items:items}); scope.model = {items:items};
scope.$eval(); scope.$apply();
assertEquals('<ul>' + assertEquals('<ul>' +
'<#comment></#comment>' + '<#comment></#comment>' +
'<li ng:bind="item.a" ng:repeat-index="0">A</li>' + '<li ng:bind="item.a" ng:repeat-index="0">A</li>' +
@ -196,7 +199,7 @@ describe('Binder', function(){
'</ul>', sortedHtml(form)); '</ul>', sortedHtml(form));
items.unshift({a:'C'}); items.unshift({a:'C'});
scope.$eval(); scope.$apply();
assertEquals('<ul>' + assertEquals('<ul>' +
'<#comment></#comment>' + '<#comment></#comment>' +
'<li ng:bind="item.a" ng:repeat-index="0">C</li>' + '<li ng:bind="item.a" ng:repeat-index="0">C</li>' +
@ -205,7 +208,7 @@ describe('Binder', function(){
'</ul>', sortedHtml(form)); '</ul>', sortedHtml(form));
items.shift(); items.shift();
scope.$eval(); scope.$apply();
assertEquals('<ul>' + assertEquals('<ul>' +
'<#comment></#comment>' + '<#comment></#comment>' +
'<li ng:bind="item.a" ng:repeat-index="0">A</li>' + '<li ng:bind="item.a" ng:repeat-index="0">A</li>' +
@ -214,13 +217,13 @@ describe('Binder', function(){
items.shift(); items.shift();
items.shift(); items.shift();
scope.$eval(); scope.$apply();
}); });
it('RepeaterContentDoesNotBind', function(){ it('RepeaterContentDoesNotBind', function(){
var scope = this.compile('<ul><LI ng:repeat="item in model.items"><span ng:bind="item.a"></span></li></ul>'); var scope = this.compile('<ul><LI ng:repeat="item in model.items"><span ng:bind="item.a"></span></li></ul>');
scope.$set('model', {items:[{a:"A"}]}); scope.model = {items:[{a:"A"}]};
scope.$eval(); scope.$apply();
assertEquals('<ul>' + assertEquals('<ul>' +
'<#comment></#comment>' + '<#comment></#comment>' +
'<li ng:repeat-index="0"><span ng:bind="item.a">A</span></li>' + '<li ng:repeat-index="0"><span ng:bind="item.a">A</span></li>' +
@ -234,59 +237,62 @@ describe('Binder', function(){
it('RepeaterAdd', function(){ it('RepeaterAdd', function(){
var scope = this.compile('<div><input type="text" name="item.x" ng:repeat="item in items"></div>'); var scope = this.compile('<div><input type="text" name="item.x" ng:repeat="item in items"></div>');
scope.$set('items', [{x:'a'}, {x:'b'}]); scope.items = [{x:'a'}, {x:'b'}];
scope.$eval(); scope.$apply();
var first = childNode(scope.$element, 1); var first = childNode(scope.$element, 1);
var second = childNode(scope.$element, 2); var second = childNode(scope.$element, 2);
assertEquals('a', first.val()); expect(first.val()).toEqual('a');
assertEquals('b', second.val()); expect(second.val()).toEqual('b');
return
first.val('ABC'); first.val('ABC');
browserTrigger(first, 'keydown'); browserTrigger(first, 'keydown');
scope.$service('$browser').defer.flush(); scope.$service('$browser').defer.flush();
assertEquals(scope.items[0].x, 'ABC'); expect(scope.items[0].x).toEqual('ABC');
}); });
it('ItShouldRemoveExtraChildrenWhenIteratingOverHash', function(){ it('ItShouldRemoveExtraChildrenWhenIteratingOverHash', function(){
var scope = this.compile('<div><div ng:repeat="i in items">{{i}}</div></div>'); var scope = this.compile('<div><div ng:repeat="i in items">{{i}}</div></div>');
var items = {}; var items = {};
scope.$set("items", items); scope.items = items;
scope.$eval(); scope.$apply();
expect(scope.$element[0].childNodes.length - 1).toEqual(0); expect(scope.$element[0].childNodes.length - 1).toEqual(0);
items.name = "misko"; items.name = "misko";
scope.$eval(); scope.$apply();
expect(scope.$element[0].childNodes.length - 1).toEqual(1); expect(scope.$element[0].childNodes.length - 1).toEqual(1);
delete items.name; delete items.name;
scope.$eval(); scope.$apply();
expect(scope.$element[0].childNodes.length - 1).toEqual(0); expect(scope.$element[0].childNodes.length - 1).toEqual(0);
}); });
it('IfTextBindingThrowsErrorDecorateTheSpan', function(){ it('IfTextBindingThrowsErrorDecorateTheSpan', function(){
var scope = this.compile('<div>{{error.throw()}}</div>'); var scope = this.compile('<div>{{error.throw()}}</div>', null, true);
var doc = scope.$element; var doc = scope.$element;
var errorLogs = scope.$service('$log').error.logs; var errorLogs = scope.$service('$exceptionHandler').errors;
scope.$set('error.throw', function(){throw "ErrorMsg1";}); scope.error = {
scope.$eval(); 'throw': function(){throw "ErrorMsg1";}
};
scope.$apply();
var span = childNode(doc, 0); var span = childNode(doc, 0);
assertTrue(span.hasClass('ng-exception')); assertTrue(span.hasClass('ng-exception'));
assertTrue(!!span.text().match(/ErrorMsg1/)); assertTrue(!!span.text().match(/ErrorMsg1/));
assertTrue(!!span.attr('ng-exception').match(/ErrorMsg1/)); assertTrue(!!span.attr('ng-exception').match(/ErrorMsg1/));
assertEquals(['ErrorMsg1'], errorLogs.shift()); assertEquals(['ErrorMsg1'], errorLogs.shift());
scope.$set('error.throw', function(){throw "MyError";}); scope.error['throw'] = function(){throw "MyError";};
scope.$eval(); scope.$apply();
span = childNode(doc, 0); span = childNode(doc, 0);
assertTrue(span.hasClass('ng-exception')); assertTrue(span.hasClass('ng-exception'));
assertTrue(span.text(), span.text().match('MyError') !== null); assertTrue(span.text(), span.text().match('MyError') !== null);
assertEquals('MyError', span.attr('ng-exception')); assertEquals('MyError', span.attr('ng-exception'));
assertEquals(['MyError'], errorLogs.shift()); assertEquals(['MyError'], errorLogs.shift());
scope.$set('error.throw', function(){return "ok";}); scope.error['throw'] = function(){return "ok";};
scope.$eval(); scope.$apply();
assertFalse(span.hasClass('ng-exception')); assertFalse(span.hasClass('ng-exception'));
assertEquals('ok', span.text()); assertEquals('ok', span.text());
assertEquals(null, span.attr('ng-exception')); assertEquals(null, span.attr('ng-exception'));
@ -294,23 +300,21 @@ describe('Binder', function(){
}); });
it('IfAttrBindingThrowsErrorDecorateTheAttribute', function(){ it('IfAttrBindingThrowsErrorDecorateTheAttribute', function(){
var scope = this.compile('<div attr="before {{error.throw()}} after"></div>'); var scope = this.compile('<div attr="before {{error.throw()}} after"></div>', null, true);
var doc = scope.$element; var doc = scope.$element;
var errorLogs = scope.$service('$log').error.logs; var errorLogs = scope.$service('$exceptionHandler').errors;
var count = 0;
scope.$set('error.throw', function(){throw "ErrorMsg";}); scope.error = {
scope.$eval(); 'throw': function(){throw new Error("ErrorMsg" + (++count));}
assertTrue('ng-exception', doc.hasClass('ng-exception')); };
assertEquals('"ErrorMsg"', doc.attr('ng-exception')); scope.$apply();
assertEquals('before "ErrorMsg" after', doc.attr('attr')); expect(errorLogs.length).toMatch(1);
assertEquals(['ErrorMsg'], errorLogs.shift()); expect(errorLogs.shift()).toMatch(/ErrorMsg1/);
scope.$set('error.throw', function(){ return 'X';}); scope.error['throw'] = function(){ return 'X';};
scope.$eval(); scope.$apply();
assertFalse('!ng-exception', doc.hasClass('ng-exception')); expect(errorLogs.length).toMatch(0);
assertEquals('before X after', doc.attr('attr'));
assertEquals(null, doc.attr('ng-exception'));
assertEquals(0, errorLogs.length);
}); });
it('NestedRepeater', function(){ it('NestedRepeater', function(){
@ -318,8 +322,8 @@ describe('Binder', function(){
'<ul name="{{i}}" ng:repeat="i in m.item"></ul>' + '<ul name="{{i}}" ng:repeat="i in m.item"></ul>' +
'</div></div>'); '</div></div>');
scope.$set('model', [{name:'a', item:['a1', 'a2']}, {name:'b', item:['b1', 'b2']}]); scope.model = [{name:'a', item:['a1', 'a2']}, {name:'b', item:['b1', 'b2']}];
scope.$eval(); scope.$apply();
assertEquals('<div>'+ assertEquals('<div>'+
'<#comment></#comment>'+ '<#comment></#comment>'+
@ -338,13 +342,13 @@ describe('Binder', function(){
it('HideBindingExpression', function(){ it('HideBindingExpression', function(){
var scope = this.compile('<div ng:hide="hidden == 3"/>'); var scope = this.compile('<div ng:hide="hidden == 3"/>');
scope.$set('hidden', 3); scope.hidden = 3;
scope.$eval(); scope.$apply();
assertHidden(scope.$element); assertHidden(scope.$element);
scope.$set('hidden', 2); scope.hidden = 2;
scope.$eval(); scope.$apply();
assertVisible(scope.$element); assertVisible(scope.$element);
}); });
@ -352,18 +356,18 @@ describe('Binder', function(){
it('HideBinding', function(){ it('HideBinding', function(){
var scope = this.compile('<div ng:hide="hidden"/>'); var scope = this.compile('<div ng:hide="hidden"/>');
scope.$set('hidden', 'true'); scope.hidden = 'true';
scope.$eval(); scope.$apply();
assertHidden(scope.$element); assertHidden(scope.$element);
scope.$set('hidden', 'false'); scope.hidden = 'false';
scope.$eval(); scope.$apply();
assertVisible(scope.$element); assertVisible(scope.$element);
scope.$set('hidden', ''); scope.hidden = '';
scope.$eval(); scope.$apply();
assertVisible(scope.$element); assertVisible(scope.$element);
}); });
@ -371,25 +375,25 @@ describe('Binder', function(){
it('ShowBinding', function(){ it('ShowBinding', function(){
var scope = this.compile('<div ng:show="show"/>'); var scope = this.compile('<div ng:show="show"/>');
scope.$set('show', 'true'); scope.show = 'true';
scope.$eval(); scope.$apply();
assertVisible(scope.$element); assertVisible(scope.$element);
scope.$set('show', 'false'); scope.show = 'false';
scope.$eval(); scope.$apply();
assertHidden(scope.$element); assertHidden(scope.$element);
scope.$set('show', ''); scope.show = '';
scope.$eval(); scope.$apply();
assertHidden(scope.$element); assertHidden(scope.$element);
}); });
it('BindClassUndefined', function(){ it('BindClassUndefined', function(){
var scope = this.compile('<div ng:class="undefined"/>'); var scope = this.compile('<div ng:class="undefined"/>');
scope.$eval(); scope.$apply();
assertEquals( assertEquals(
'<div class="undefined" ng:class="undefined"></div>', '<div class="undefined" ng:class="undefined"></div>',
@ -397,22 +401,22 @@ describe('Binder', function(){
}); });
it('BindClass', function(){ it('BindClass', function(){
var scope = this.compile('<div ng:class="class"/>'); var scope = this.compile('<div ng:class="clazz"/>');
scope.$set('class', 'testClass'); scope.clazz = 'testClass';
scope.$eval(); scope.$apply();
assertEquals('<div class="testClass" ng:class="class"></div>', sortedHtml(scope.$element)); assertEquals('<div class="testClass" ng:class="clazz"></div>', sortedHtml(scope.$element));
scope.$set('class', ['a', 'b']); scope.clazz = ['a', 'b'];
scope.$eval(); scope.$apply();
assertEquals('<div class="a b" ng:class="class"></div>', sortedHtml(scope.$element)); assertEquals('<div class="a b" ng:class="clazz"></div>', sortedHtml(scope.$element));
}); });
it('BindClassEvenOdd', function(){ it('BindClassEvenOdd', function(){
var scope = this.compile('<div><div ng:repeat="i in [0,1]" ng:class-even="\'e\'" ng:class-odd="\'o\'"></div></div>'); var scope = this.compile('<div><div ng:repeat="i in [0,1]" ng:class-even="\'e\'" ng:class-odd="\'o\'"></div></div>');
scope.$eval(); scope.$apply();
var d1 = jqLite(scope.$element[0].childNodes[1]); var d1 = jqLite(scope.$element[0].childNodes[1]);
var d2 = jqLite(scope.$element[0].childNodes[2]); var d2 = jqLite(scope.$element[0].childNodes[2]);
expect(d1.hasClass('o')).toBeTruthy(); expect(d1.hasClass('o')).toBeTruthy();
@ -428,31 +432,22 @@ describe('Binder', function(){
var scope = this.compile('<div ng:style="style"/>'); var scope = this.compile('<div ng:style="style"/>');
scope.$eval('style={height: "10px"}'); scope.$eval('style={height: "10px"}');
scope.$eval(); scope.$apply();
assertEquals("10px", scope.$element.css('height')); assertEquals("10px", scope.$element.css('height'));
scope.$eval('style={}'); scope.$eval('style={}');
scope.$eval(); scope.$apply();
}); });
it('ActionOnAHrefThrowsError', function(){ it('ActionOnAHrefThrowsError', function(){
var scope = this.compile('<a ng:click="action()">Add Phone</a>'); var scope = this.compile('<a ng:click="action()">Add Phone</a>', null, true);
scope.action = function(){ scope.action = function(){
throw new Error('MyError'); throw new Error('MyError');
}; };
var input = scope.$element; var input = scope.$element;
browserTrigger(input, 'click'); browserTrigger(input, 'click');
var error = input.attr('ng-exception'); expect(scope.$service('$exceptionHandler').errors[0]).toMatch(/MyError/);
assertTrue(!!error.match(/MyError/));
assertTrue("should have an error class", input.hasClass('ng-exception'));
assertTrue(!!scope.$service('$log').error.logs.shift()[0].message.match(/MyError/));
// TODO: I think that exception should never get cleared so this portion of test makes no sense
//c.scope.action = noop;
//browserTrigger(input, 'click');
//dump(input.attr('ng-error'));
//assertFalse('error class should be cleared', input.hasClass('ng-exception'));
}); });
it('ShoulIgnoreVbNonBindable', function(){ it('ShoulIgnoreVbNonBindable', function(){
@ -460,16 +455,15 @@ describe('Binder', function(){
"<div ng:non-bindable>{{a}}</div>" + "<div ng:non-bindable>{{a}}</div>" +
"<div ng:non-bindable=''>{{b}}</div>" + "<div ng:non-bindable=''>{{b}}</div>" +
"<div ng:non-bindable='true'>{{c}}</div></div>"); "<div ng:non-bindable='true'>{{c}}</div></div>");
scope.$set('a', 123); scope.a = 123;
scope.$eval(); scope.$apply();
assertEquals('123{{a}}{{b}}{{c}}', scope.$element.text()); assertEquals('123{{a}}{{b}}{{c}}', scope.$element.text());
}); });
it('RepeaterShouldBindInputsDefaults', function () { it('RepeaterShouldBindInputsDefaults', function () {
var scope = this.compile('<div><input value="123" name="item.name" ng:repeat="item in items"></div>'); var scope = this.compile('<div><input value="123" name="item.name" ng:repeat="item in items"></div>');
scope.$set('items', [{}, {name:'misko'}]); scope.items = [{}, {name:'misko'}];
scope.$eval(); scope.$apply();
assertEquals("123", scope.$eval('items[0].name')); assertEquals("123", scope.$eval('items[0].name'));
assertEquals("misko", scope.$eval('items[1].name')); assertEquals("misko", scope.$eval('items[1].name'));
@ -477,8 +471,8 @@ describe('Binder', function(){
it('ShouldTemplateBindPreElements', function () { it('ShouldTemplateBindPreElements', function () {
var scope = this.compile('<pre>Hello {{name}}!</pre>'); var scope = this.compile('<pre>Hello {{name}}!</pre>');
scope.$set("name", "World"); scope.name = "World";
scope.$eval(); scope.$apply();
assertEquals('<pre ng:bind-template="Hello {{name}}!">Hello World!</pre>', sortedHtml(scope.$element)); assertEquals('<pre ng:bind-template="Hello {{name}}!">Hello World!</pre>', sortedHtml(scope.$element));
}); });
@ -486,9 +480,9 @@ describe('Binder', function(){
it('FillInOptionValueWhenMissing', function(){ it('FillInOptionValueWhenMissing', function(){
var scope = this.compile( var scope = this.compile(
'<select name="foo"><option selected="true">{{a}}</option><option value="">{{b}}</option><option>C</option></select>'); '<select name="foo"><option selected="true">{{a}}</option><option value="">{{b}}</option><option>C</option></select>');
scope.$set('a', 'A'); scope.a = 'A';
scope.$set('b', 'B'); scope.b = 'B';
scope.$eval(); scope.$apply();
var optionA = childNode(scope.$element, 0); var optionA = childNode(scope.$element, 0);
var optionB = childNode(scope.$element, 1); var optionB = childNode(scope.$element, 1);
var optionC = childNode(scope.$element, 2); var optionC = childNode(scope.$element, 2);
@ -508,39 +502,39 @@ describe('Binder', function(){
'<input ng:repeat="item in items" name="item.name" ng:required/></div>', '<input ng:repeat="item in items" name="item.name" ng:required/></div>',
jqLite(document.body)); jqLite(document.body));
var items = [{}, {}]; var items = [{}, {}];
scope.$set("items", items); scope.items = items;
scope.$eval(); scope.$apply();
assertEquals(3, scope.$service('$invalidWidgets').length); assertEquals(3, scope.$service('$invalidWidgets').length);
scope.$set('name', ''); scope.name = '';
scope.$eval(); scope.$apply();
assertEquals(3, scope.$service('$invalidWidgets').length); assertEquals(3, scope.$service('$invalidWidgets').length);
scope.$set('name', ' '); scope.name = ' ';
scope.$eval(); scope.$apply();
assertEquals(3, scope.$service('$invalidWidgets').length); assertEquals(3, scope.$service('$invalidWidgets').length);
scope.$set('name', 'abc'); scope.name = 'abc';
scope.$eval(); scope.$apply();
assertEquals(2, scope.$service('$invalidWidgets').length); assertEquals(2, scope.$service('$invalidWidgets').length);
items[0].name = 'abc'; items[0].name = 'abc';
scope.$eval(); scope.$apply();
assertEquals(1, scope.$service('$invalidWidgets').length); assertEquals(1, scope.$service('$invalidWidgets').length);
items[1].name = 'abc'; items[1].name = 'abc';
scope.$eval(); scope.$apply();
assertEquals(0, scope.$service('$invalidWidgets').length); assertEquals(0, scope.$service('$invalidWidgets').length);
}); });
it('ValidateOnlyVisibleItems', function(){ it('ValidateOnlyVisibleItems', function(){
var scope = this.compile('<div><input name="name" ng:required><input ng:show="show" name="name" ng:required></div>', jqLite(document.body)); var scope = this.compile('<div><input name="name" ng:required><input ng:show="show" name="name" ng:required></div>', jqLite(document.body));
scope.$set("show", true); scope.show = true;
scope.$eval(); scope.$apply();
assertEquals(2, scope.$service('$invalidWidgets').length); assertEquals(2, scope.$service('$invalidWidgets').length);
scope.$set("show", false); scope.show = false;
scope.$eval(); scope.$apply();
assertEquals(1, scope.$service('$invalidWidgets').visible()); assertEquals(1, scope.$service('$invalidWidgets').visible());
}); });
@ -549,7 +543,7 @@ describe('Binder', function(){
'<input name="a0" ng:bind-attr="{disabled:\'{{true}}\'}"><input name="a1" ng:bind-attr="{disabled:\'{{false}}\'}">' + '<input name="a0" ng:bind-attr="{disabled:\'{{true}}\'}"><input name="a1" ng:bind-attr="{disabled:\'{{false}}\'}">' +
'<input name="b0" ng:bind-attr="{disabled:\'{{1}}\'}"><input name="b1" ng:bind-attr="{disabled:\'{{0}}\'}">' + '<input name="b0" ng:bind-attr="{disabled:\'{{1}}\'}"><input name="b1" ng:bind-attr="{disabled:\'{{0}}\'}">' +
'<input name="c0" ng:bind-attr="{disabled:\'{{[0]}}\'}"><input name="c1" ng:bind-attr="{disabled:\'{{[]}}\'}"></div>'); '<input name="c0" ng:bind-attr="{disabled:\'{{[0]}}\'}"><input name="c1" ng:bind-attr="{disabled:\'{{[]}}\'}"></div>');
scope.$eval(); scope.$apply();
function assertChild(index, disabled) { function assertChild(index, disabled) {
var child = childNode(scope.$element, index); var child = childNode(scope.$element, index);
assertEquals(sortedHtml(child), disabled, !!child.attr('disabled')); assertEquals(sortedHtml(child), disabled, !!child.attr('disabled'));
@ -566,7 +560,7 @@ describe('Binder', function(){
it('ItShouldDisplayErrorWhenActionIsSyntacticlyIncorrect', function(){ it('ItShouldDisplayErrorWhenActionIsSyntacticlyIncorrect', function(){
var scope = this.compile('<div>' + var scope = this.compile('<div>' +
'<input type="button" ng:click="greeting=\'ABC\'"/>' + '<input type="button" ng:click="greeting=\'ABC\'"/>' +
'<input type="button" ng:click=":garbage:"/></div>'); '<input type="button" ng:click=":garbage:"/></div>', null, true);
var first = jqLite(scope.$element[0].childNodes[0]); var first = jqLite(scope.$element[0].childNodes[0]);
var second = jqLite(scope.$element[0].childNodes[1]); var second = jqLite(scope.$element[0].childNodes[1]);
var errorLogs = scope.$service('$log').error.logs; var errorLogs = scope.$service('$log').error.logs;
@ -576,8 +570,8 @@ describe('Binder', function(){
expect(errorLogs).toEqual([]); expect(errorLogs).toEqual([]);
browserTrigger(second, 'click'); browserTrigger(second, 'click');
assertTrue(second.hasClass("ng-exception")); expect(scope.$service('$exceptionHandler').errors[0]).
expect(errorLogs.shift()[0]).toMatchError(/Syntax Error: Token ':' not a primary expression/); toMatchError(/Syntax Error: Token ':' not a primary expression/);
}); });
it('ItShouldSelectTheCorrectRadioBox', function(){ it('ItShouldSelectTheCorrectRadioBox', function(){
@ -602,7 +596,7 @@ describe('Binder', function(){
it('ItShouldRepeatOnHashes', function(){ it('ItShouldRepeatOnHashes', function(){
var scope = this.compile('<ul><li ng:repeat="(k,v) in {a:0,b:1}" ng:bind=\"k + v\"></li></ul>'); var scope = this.compile('<ul><li ng:repeat="(k,v) in {a:0,b:1}" ng:bind=\"k + v\"></li></ul>');
scope.$eval(); scope.$apply();
assertEquals('<ul>' + assertEquals('<ul>' +
'<#comment></#comment>' + '<#comment></#comment>' +
'<li ng:bind=\"k + v\" ng:repeat-index="0">a0</li>' + '<li ng:bind=\"k + v\" ng:repeat-index="0">a0</li>' +
@ -613,11 +607,11 @@ describe('Binder', function(){
it('ItShouldFireChangeListenersBeforeUpdate', function(){ it('ItShouldFireChangeListenersBeforeUpdate', function(){
var scope = this.compile('<div ng:bind="name"></div>'); var scope = this.compile('<div ng:bind="name"></div>');
scope.$set("name", ""); scope.name = "";
scope.$watch("watched", "name=123"); scope.$watch("watched", "name=123");
scope.$set("watched", "change"); scope.watched = "change";
scope.$eval(); scope.$apply();
assertEquals(123, scope.$get("name")); assertEquals(123, scope.name);
assertEquals( assertEquals(
'<div ng:bind="name">123</div>', '<div ng:bind="name">123</div>',
sortedHtml(scope.$element)); sortedHtml(scope.$element));
@ -625,26 +619,26 @@ describe('Binder', function(){
it('ItShouldHandleMultilineBindings', function(){ it('ItShouldHandleMultilineBindings', function(){
var scope = this.compile('<div>{{\n 1 \n + \n 2 \n}}</div>'); var scope = this.compile('<div>{{\n 1 \n + \n 2 \n}}</div>');
scope.$eval(); scope.$apply();
assertEquals("3", scope.$element.text()); assertEquals("3", scope.$element.text());
}); });
it('ItBindHiddenInputFields', function(){ it('ItBindHiddenInputFields', function(){
var scope = this.compile('<input type="hidden" name="myName" value="abc" />'); var scope = this.compile('<input type="hidden" name="myName" value="abc" />');
scope.$eval(); scope.$apply();
assertEquals("abc", scope.$get("myName")); assertEquals("abc", scope.myName);
}); });
it('ItShouldUseFormaterForText', function(){ it('ItShouldUseFormaterForText', function(){
var scope = this.compile('<input name="a" ng:format="list" value="a,b">'); var scope = this.compile('<input name="a" ng:format="list" value="a,b">');
scope.$eval(); scope.$apply();
assertEquals(['a','b'], scope.$get('a')); assertEquals(['a','b'], scope.a);
var input = scope.$element; var input = scope.$element;
input[0].value = ' x,,yz'; input[0].value = ' x,,yz';
browserTrigger(input, 'change'); browserTrigger(input, 'change');
assertEquals(['x','yz'], scope.$get('a')); assertEquals(['x','yz'], scope.a);
scope.$set('a', [1 ,2, 3]); scope.a = [1 ,2, 3];
scope.$eval(); scope.$apply();
assertEquals('1, 2, 3', input[0].value); assertEquals('1, 2, 3', input[0].value);
}); });

View file

@ -13,9 +13,9 @@ describe('compiler', function(){
}; };
}, },
watch: function(expression, element){ observe: function(expression, element){
return function() { return function() {
this.$watch(expression, function(val){ this.$observe(expression, function(scope, val){
if (val) if (val)
log += ":" + val; log += ":" + val;
}); });
@ -33,10 +33,12 @@ describe('compiler', function(){
}; };
}); });
afterEach(function(){ afterEach(function(){
dealoc(scope); dealoc(scope);
}); });
it('should not allow compilation of multiple roots', function(){ it('should not allow compilation of multiple roots', function(){
expect(function(){ expect(function(){
compiler.compile('<div>A</div><span></span>'); compiler.compile('<div>A</div><span></span>');
@ -46,6 +48,7 @@ describe('compiler', function(){
} }
}); });
it('should recognize a directive', function(){ it('should recognize a directive', function(){
var e = jqLite('<div directive="expr" ignore="me"></div>'); var e = jqLite('<div directive="expr" ignore="me"></div>');
directives.directive = function(expression, element){ directives.directive = function(expression, element){
@ -63,50 +66,56 @@ describe('compiler', function(){
expect(log).toEqual("found:init"); expect(log).toEqual("found:init");
}); });
it('should recurse to children', function(){ it('should recurse to children', function(){
scope = compile('<div><span hello="misko"/></div>'); scope = compile('<div><span hello="misko"/></div>');
expect(log).toEqual("hello misko"); expect(log).toEqual("hello misko");
}); });
it('should watch scope', function(){
scope = compile('<span watch="name"/>'); it('should observe scope', function(){
scope = compile('<span observe="name">');
expect(log).toEqual(""); expect(log).toEqual("");
scope.$eval(); scope.$flush();
scope.$set('name', 'misko'); scope.name = 'misko';
scope.$eval(); scope.$flush();
scope.$eval(); scope.$flush();
scope.$set('name', 'adam'); scope.name = 'adam';
scope.$eval(); scope.$flush();
scope.$eval(); scope.$flush();
expect(log).toEqual(":misko:adam"); expect(log).toEqual(":misko:adam");
}); });
it('should prevent descend', function(){ it('should prevent descend', function(){
directives.stop = function(){ this.descend(false); }; directives.stop = function(){ this.descend(false); };
scope = compile('<span hello="misko" stop="true"><span hello="adam"/></span>'); scope = compile('<span hello="misko" stop="true"><span hello="adam"/></span>');
expect(log).toEqual("hello misko"); expect(log).toEqual("hello misko");
}); });
it('should allow creation of templates', function(){ it('should allow creation of templates', function(){
directives.duplicate = function(expr, element){ directives.duplicate = function(expr, element){
element.replaceWith(document.createComment("marker")); element.replaceWith(document.createComment("marker"));
element.removeAttr("duplicate"); element.removeAttr("duplicate");
var linker = this.compile(element); var linker = this.compile(element);
return function(marker) { return function(marker) {
this.$onEval(function() { this.$observe(function() {
var scope = linker(angular.scope(), noop); var scope = linker(angular.scope(), noop);
marker.after(scope.$element); marker.after(scope.$element);
}); });
}; };
}; };
scope = compile('before<span duplicate="expr">x</span>after'); scope = compile('before<span duplicate="expr">x</span>after');
scope.$flush();
expect(sortedHtml(scope.$element)).toEqual('<div>before<#comment></#comment><span>x</span>after</div>'); expect(sortedHtml(scope.$element)).toEqual('<div>before<#comment></#comment><span>x</span>after</div>');
scope.$eval(); scope.$flush();
expect(sortedHtml(scope.$element)).toEqual('<div>before<#comment></#comment><span>x</span><span>x</span>after</div>'); expect(sortedHtml(scope.$element)).toEqual('<div>before<#comment></#comment><span>x</span><span>x</span>after</div>');
scope.$eval(); scope.$flush();
expect(sortedHtml(scope.$element)).toEqual('<div>before<#comment></#comment><span>x</span><span>x</span><span>x</span>after</div>'); expect(sortedHtml(scope.$element)).toEqual('<div>before<#comment></#comment><span>x</span><span>x</span><span>x</span>after</div>');
}); });
it('should process markup before directives', function(){ it('should process markup before directives', function(){
markup.push(function(text, textNode, parentNode) { markup.push(function(text, textNode, parentNode) {
if (text == 'middle') { if (text == 'middle') {
@ -120,6 +129,7 @@ describe('compiler', function(){
expect(log).toEqual("hello middle"); expect(log).toEqual("hello middle");
}); });
it('should replace widgets', function(){ it('should replace widgets', function(){
widgets['NG:BUTTON'] = function(element) { widgets['NG:BUTTON'] = function(element) {
expect(element.hasClass('ng-widget')).toEqual(true); expect(element.hasClass('ng-widget')).toEqual(true);
@ -133,6 +143,7 @@ describe('compiler', function(){
expect(log).toEqual('init'); expect(log).toEqual('init');
}); });
it('should use the replaced element after calling widget', function(){ it('should use the replaced element after calling widget', function(){
widgets['H1'] = function(element) { widgets['H1'] = function(element) {
// HTML elements which are augmented by acting as widgets, should not be marked as so // HTML elements which are augmented by acting as widgets, should not be marked as so
@ -151,6 +162,7 @@ describe('compiler', function(){
expect(scope.$element.text()).toEqual('3'); expect(scope.$element.text()).toEqual('3');
}); });
it('should allow multiple markups per text element', function(){ it('should allow multiple markups per text element', function(){
markup.push(function(text, textNode, parent){ markup.push(function(text, textNode, parent){
var index = text.indexOf('---'); var index = text.indexOf('---');
@ -174,6 +186,7 @@ describe('compiler', function(){
expect(sortedHtml(scope.$element)).toEqual('<div>A<hr></hr>B<hr></hr>C<p></p>D</div>'); expect(sortedHtml(scope.$element)).toEqual('<div>A<hr></hr>B<hr></hr>C<p></p>D</div>');
}); });
it('should add class for namespace elements', function(){ it('should add class for namespace elements', function(){
scope = compile('<ng:space>abc</ng:space>'); scope = compile('<ng:space>abc</ng:space>');
var space = jqLite(scope.$element[0].firstChild); var space = jqLite(scope.$element[0].firstChild);

View file

@ -204,15 +204,15 @@ describe('parser', function() {
scope.$eval("1|nonExistant"); scope.$eval("1|nonExistant");
}).toThrow(new Error("Syntax Error: Token 'nonExistant' should be a function at column 3 of the expression [1|nonExistant] starting at [nonExistant].")); }).toThrow(new Error("Syntax Error: Token 'nonExistant' should be a function at column 3 of the expression [1|nonExistant] starting at [nonExistant]."));
scope.$set('offset', 3); scope.offset = 3;
expect(scope.$eval("'abcd'|upper._case")).toEqual("ABCD"); expect(scope.$eval("'abcd'|upper._case")).toEqual("ABCD");
expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc"); expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc");
expect(scope.$eval("'abcd'|substring:1:3|upper._case")).toEqual("BC"); expect(scope.$eval("'abcd'|substring:1:3|upper._case")).toEqual("BC");
}); });
it('should access scope', function() { it('should access scope', function() {
scope.$set('a', 123); scope.a = 123;
scope.$set('b.c', 456); scope.b = {c: 456};
expect(scope.$eval("a", scope)).toEqual(123); expect(scope.$eval("a", scope)).toEqual(123);
expect(scope.$eval("b.c", scope)).toEqual(456); expect(scope.$eval("b.c", scope)).toEqual(456);
expect(scope.$eval("x.y.z", scope)).not.toBeDefined(); expect(scope.$eval("x.y.z", scope)).not.toBeDefined();
@ -224,32 +224,32 @@ describe('parser', function() {
it('should evaluate assignments', function() { it('should evaluate assignments', function() {
expect(scope.$eval("a=12")).toEqual(12); expect(scope.$eval("a=12")).toEqual(12);
expect(scope.$get("a")).toEqual(12); expect(scope.a).toEqual(12);
scope = createScope(); scope = createScope();
expect(scope.$eval("x.y.z=123;")).toEqual(123); expect(scope.$eval("x.y.z=123;")).toEqual(123);
expect(scope.$get("x.y.z")).toEqual(123); expect(scope.x.y.z).toEqual(123);
expect(scope.$eval("a=123; b=234")).toEqual(234); expect(scope.$eval("a=123; b=234")).toEqual(234);
expect(scope.$get("a")).toEqual(123); expect(scope.a).toEqual(123);
expect(scope.$get("b")).toEqual(234); expect(scope.b).toEqual(234);
}); });
it('should evaluate function call without arguments', function() { it('should evaluate function call without arguments', function() {
scope.$set('const', function(a,b){return 123;}); scope['const'] = function(a,b){return 123;};
expect(scope.$eval("const()")).toEqual(123); expect(scope.$eval("const()")).toEqual(123);
}); });
it('should evaluate function call with arguments', function() { it('should evaluate function call with arguments', function() {
scope.$set('add', function(a,b) { scope.add = function(a,b) {
return a+b; return a+b;
}); };
expect(scope.$eval("add(1,2)")).toEqual(3); expect(scope.$eval("add(1,2)")).toEqual(3);
}); });
it('should evaluate multiplication and division', function() { it('should evaluate multiplication and division', function() {
scope.$set('taxRate', 8); scope.taxRate = 8;
scope.$set('subTotal', 100); scope.subTotal = 100;
expect(scope.$eval("taxRate / 100 * subTotal")).toEqual(8); expect(scope.$eval("taxRate / 100 * subTotal")).toEqual(8);
expect(scope.$eval("subTotal * taxRate / 100")).toEqual(8); expect(scope.$eval("subTotal * taxRate / 100")).toEqual(8);
}); });
@ -297,7 +297,7 @@ describe('parser', function() {
return this.a; return this.a;
}; };
scope.$set("obj", new C()); scope.obj = new C();
expect(scope.$eval("obj.getA()")).toEqual(123); expect(scope.$eval("obj.getA()")).toEqual(123);
}); });
@ -312,29 +312,29 @@ describe('parser', function() {
return this.a; return this.a;
}; };
scope.$set("obj", new C()); scope.obj = new C();
expect(scope.$eval("obj.sum(obj.getA())")).toEqual(246); expect(scope.$eval("obj.sum(obj.getA())")).toEqual(246);
}); });
it('should evaluate objects on scope context', function() { it('should evaluate objects on scope context', function() {
scope.$set('a', "abc"); scope.a = "abc";
expect(scope.$eval("{a:a}").a).toEqual("abc"); expect(scope.$eval("{a:a}").a).toEqual("abc");
}); });
it('should evaluate field access on function call result', function() { it('should evaluate field access on function call result', function() {
scope.$set('a', function() { scope.a = function() {
return {name:'misko'}; return {name:'misko'};
}); };
expect(scope.$eval("a().name")).toEqual("misko"); expect(scope.$eval("a().name")).toEqual("misko");
}); });
it('should evaluate field access after array access', function () { it('should evaluate field access after array access', function () {
scope.$set('items', [{}, {name:'misko'}]); scope.items = [{}, {name:'misko'}];
expect(scope.$eval('items[1].name')).toEqual("misko"); expect(scope.$eval('items[1].name')).toEqual("misko");
}); });
it('should evaluate array assignment', function() { it('should evaluate array assignment', function() {
scope.$set('items', []); scope.items = [];
expect(scope.$eval('items[1] = "abc"')).toEqual("abc"); expect(scope.$eval('items[1] = "abc"')).toEqual("abc");
expect(scope.$eval('items[1]')).toEqual("abc"); expect(scope.$eval('items[1]')).toEqual("abc");
@ -388,7 +388,7 @@ describe('parser', function() {
it('should evaluate undefined', function() { it('should evaluate undefined', function() {
expect(scope.$eval("undefined")).not.toBeDefined(); expect(scope.$eval("undefined")).not.toBeDefined();
expect(scope.$eval("a=undefined")).not.toBeDefined(); expect(scope.$eval("a=undefined")).not.toBeDefined();
expect(scope.$get("a")).not.toBeDefined(); expect(scope.a).not.toBeDefined();
}); });
it('should allow assignment after array dereference', function(){ it('should allow assignment after array dereference', function(){

View file

@ -4,7 +4,7 @@ describe("resource", function() {
var xhr, resource, CreditCard, callback, $xhrErr; var xhr, resource, CreditCard, callback, $xhrErr;
beforeEach(function() { beforeEach(function() {
var scope = angular.scope({}, null, {'$xhr.error': $xhrErr = jasmine.createSpy('xhr.error')}); var scope = angular.scope(angularService, {'$xhr.error': $xhrErr = jasmine.createSpy('xhr.error')});
xhr = scope.$service('$browser').xhr; xhr = scope.$service('$browser').xhr;
resource = new ResourceFactory(scope.$service('$xhr')); resource = new ResourceFactory(scope.$service('$xhr'));
CreditCard = resource.route('/CreditCard/:id:verb', {id:'@id.key'}, { CreditCard = resource.route('/CreditCard/:id:verb', {id:'@id.key'}, {

View file

@ -15,41 +15,24 @@ describe("ScenarioSpec: Compilation", function(){
it("should compile dom node and return scope", function(){ it("should compile dom node and return scope", function(){
var node = jqLite('<div ng:init="a=1">{{b=a+1}}</div>')[0]; var node = jqLite('<div ng:init="a=1">{{b=a+1}}</div>')[0];
scope = angular.compile(node)(); scope = angular.compile(node)();
scope.$flush();
expect(scope.a).toEqual(1); expect(scope.a).toEqual(1);
expect(scope.b).toEqual(2); expect(scope.b).toEqual(2);
}); });
it("should compile jQuery node and return scope", function(){ it("should compile jQuery node and return scope", function(){
scope = compile(jqLite('<div>{{a=123}}</div>'))(); scope = compile(jqLite('<div>{{a=123}}</div>'))();
scope.$flush();
expect(jqLite(scope.$element).text()).toEqual('123'); expect(jqLite(scope.$element).text()).toEqual('123');
}); });
it("should compile text node and return scope", function(){ it("should compile text node and return scope", function(){
scope = angular.compile('<div>{{a=123}}</div>')(); scope = angular.compile('<div>{{a=123}}</div>')();
scope.$flush();
expect(jqLite(scope.$element).text()).toEqual('123'); expect(jqLite(scope.$element).text()).toEqual('123');
}); });
}); });
describe('scope', function(){
it("should have $set, $get, $eval, $updateView methods", function(){
scope = angular.compile('<div>{{a}}</div>')();
scope.$eval("$invalidWidgets.push({})");
expect(scope.$set("a", 2)).toEqual(2);
expect(scope.$get("a")).toEqual(2);
expect(scope.$eval("a=3")).toEqual(3);
scope.$eval();
expect(jqLite(scope.$element).text()).toEqual('3');
});
it("should have $ objects", function(){
scope = angular.compile('<div></div>')(angular.scope({$config: {a:"b"}}));
expect(scope.$service('$location')).toBeDefined();
expect(scope.$get('$eval')).toBeDefined();
expect(scope.$get('$config')).toBeDefined();
expect(scope.$get('$config.a')).toEqual("b");
});
});
describe("configuration", function(){ describe("configuration", function(){
it("should take location object", function(){ it("should take location object", function(){
var url = "http://server/#?book=moby"; var url = "http://server/#?book=moby";

View file

@ -1,246 +1,497 @@
'use strict'; 'use strict';
describe('scope/model', function(){ describe('Scope', function(){
var root, mockHandler;
var temp; beforeEach(function(){
root = createScope(angular.service, {
beforeEach(function() { $updateView: function(){
temp = window.temp = {}; root.$flush();
temp.InjectController = function(exampleService, extra) { },
this.localService = exampleService; '$exceptionHandler': $exceptionHandlerMockFactory()
this.extra = extra; });
this.$root.injectController = this; mockHandler = root.$service('$exceptionHandler');
};
temp.InjectController.$inject = ["exampleService"];
}); });
afterEach(function() {
window.temp = undefined; describe('$root', function(){
it('should point to itself', function(){
expect(root.$root).toEqual(root);
expect(root.hasOwnProperty('$root')).toBeTruthy();
});
it('should not have $root on children, but should inherit', function(){
var child = root.$new();
expect(child.$root).toEqual(root);
expect(child.hasOwnProperty('$root')).toBeFalsy();
});
}); });
it('should create a scope with parent', function(){
var model = createScope({name:'Misko'}); describe('$parent', function(){
expect(model.name).toEqual('Misko'); it('should point to itself in root', function(){
expect(root.$root).toEqual(root);
});
it('should point to parent', function(){
var child = root.$new();
expect(root.$parent).toEqual(null);
expect(child.$parent).toEqual(root);
expect(child.$new().$parent).toEqual(child);
});
}); });
it('should have $get/$set/$parent', function(){
var parent = {}; describe('$id', function(){
var model = createScope(parent); it('should have a unique id', function(){
model.$set('name', 'adam'); expect(root.$id < root.$new().$id).toBeTruthy();
expect(model.name).toEqual('adam'); });
expect(model.$get('name')).toEqual('adam');
expect(model.$parent).toEqual(model);
expect(model.$root).toEqual(model);
}); });
it('should return noop function when LHS is undefined', function(){
var model = createScope(); describe('this', function(){
expect(model.$eval('x.$filter()')).toEqual(undefined); it('should have a \'this\'', function(){
expect(root['this']).toEqual(root);
});
}); });
describe('$new()', function(){
it('should create a child scope', function(){
var child = root.$new();
root.a = 123;
expect(child.a).toEqual(123);
});
it('should instantiate controller and bind functions', function(){
function Cntl($browser, name){
this.$browser = $browser;
this.callCount = 0;
this.name = name;
}
Cntl.$inject = ['$browser'];
Cntl.prototype = {
myFn: function(){
expect(this).toEqual(cntl);
this.callCount++;
}
};
var cntl = root.$new(Cntl, ['misko']);
expect(root.$browser).toBeUndefined();
expect(root.myFn).toBeUndefined();
expect(cntl.$browser).toBeDefined();
expect(cntl.name).toEqual('misko');
cntl.myFn();
cntl.$new().myFn();
expect(cntl.callCount).toEqual(2);
});
});
describe('$service', function(){
it('should have it on root', function(){
expect(root.hasOwnProperty('$service')).toBeTruthy();
});
});
describe('$watch/$digest', function(){
it('should watch and fire on simple property change', function(){
var spy = jasmine.createSpy();
root.$watch('name', spy);
expect(spy).not.wasCalled();
root.$digest();
expect(spy).not.wasCalled();
root.name = 'misko';
root.$digest();
expect(spy).wasCalledWith(root, 'misko', undefined);
});
it('should watch and fire on expression change', function(){
var spy = jasmine.createSpy();
root.$watch('name.first', spy);
root.name = {};
expect(spy).not.wasCalled();
root.$digest();
expect(spy).not.wasCalled();
root.name.first = 'misko';
root.$digest();
expect(spy).wasCalled();
});
it('should delegate exceptions', function(){
root.$watch('a', function(){throw new Error('abc');});
root.a = 1;
root.$digest();
expect(mockHandler.errors[0].message).toEqual('abc');
$logMock.error.logs.length = 0;
});
it('should fire watches in order of addition', function(){
// this is not an external guarantee, just our own sanity
var log = '';
root.$watch('a', function(){ log += 'a'; });
root.$watch('b', function(){ log += 'b'; });
root.$watch('c', function(){ log += 'c'; });
root.a = root.b = root.c = 1;
root.$digest();
expect(log).toEqual('abc');
});
it('should delegate $digest to children in addition order', function(){
// this is not an external guarantee, just our own sanity
var log = '';
var childA = root.$new();
var childB = root.$new();
var childC = root.$new();
childA.$watch('a', function(){ log += 'a'; });
childB.$watch('b', function(){ log += 'b'; });
childC.$watch('c', function(){ log += 'c'; });
childA.a = childB.b = childC.c = 1;
root.$digest();
expect(log).toEqual('abc');
});
it('should repeat watch cycle while model changes are identified', function(){
var log = '';
root.$watch('c', function(self, v){self.d = v; log+='c'; });
root.$watch('b', function(self, v){self.c = v; log+='b'; });
root.$watch('a', function(self, v){self.b = v; log+='a'; });
root.a = 1;
expect(root.$digest()).toEqual(3);
expect(root.b).toEqual(1);
expect(root.c).toEqual(1);
expect(root.d).toEqual(1);
expect(log).toEqual('abc');
});
it('should prevent infinite recursion', function(){
root.$watch('a', function(self, v){self.b++;});
root.$watch('b', function(self, v){self.a++;});
root.a = root.b = 0;
expect(function(){
root.$digest();
}).toThrow('100 $digest() iterations reached. Aborting!');
});
it('should not fire upon $watch registration on initial $digest', function(){
var log = '';
root.a = 1;
root.$watch('a', function(){ log += 'a'; });
root.$watch('b', function(){ log += 'b'; });
expect(log).toEqual('');
expect(root.$digest()).toEqual(0);
expect(log).toEqual('');
});
it('should return the listener to force a initial watch', function(){
var log = '';
root.a = 1;
root.$watch('a', function(scope, o1, o2){ log += scope.a + ':' + (o1 == o2 == 1) ; })();
expect(log).toEqual('1:true');
expect(root.$digest()).toEqual(0);
expect(log).toEqual('1:true');
});
it('should watch objects', function(){
var log = '';
root.a = [];
root.b = {};
root.$watch('a', function(){ log +='.';});
root.$watch('b', function(){ log +='!';});
root.$digest();
expect(log).toEqual('');
root.a.push({});
root.b.name = '';
root.$digest();
expect(log).toEqual('.!');
});
it('should prevent recursion', function(){
var callCount = 0;
root.$watch('name', function(){
expect(function(){
root.$digest();
}).toThrow('$digest already in progress');
expect(function(){
root.$flush();
}).toThrow('$digest already in progress');
callCount++;
});
root.name = 'a';
root.$digest();
expect(callCount).toEqual(1);
});
});
describe('$observe/$flush', function(){
it('should register simple property observer and fire on change', function(){
var spy = jasmine.createSpy();
root.$observe('name', spy);
expect(spy).not.wasCalled();
root.$flush();
expect(spy).wasCalled();
expect(spy.mostRecentCall.args[0]).toEqual(root);
expect(spy.mostRecentCall.args[1]).toEqual(undefined);
expect(spy.mostRecentCall.args[2].toString()).toEqual(NaN.toString());
root.name = 'misko';
root.$flush();
expect(spy).wasCalledWith(root, 'misko', undefined);
});
it('should register expression observers and fire them on change', function(){
var spy = jasmine.createSpy();
root.$observe('name.first', spy);
root.name = {};
expect(spy).not.wasCalled();
root.$flush();
expect(spy).wasCalled();
root.name.first = 'misko';
root.$flush();
expect(spy).wasCalled();
});
it('should delegate exceptions', function(){
root.$observe('a', function(){throw new Error('abc');});
root.a = 1;
root.$flush();
expect(mockHandler.errors[0].message).toEqual('abc');
$logMock.error.logs.shift();
});
it('should fire observers in order of addition', function(){
// this is not an external guarantee, just our own sanity
var log = '';
root.$observe('a', function(){ log += 'a'; });
root.$observe('b', function(){ log += 'b'; });
root.$observe('c', function(){ log += 'c'; });
root.a = root.b = root.c = 1;
root.$flush();
expect(log).toEqual('abc');
});
it('should delegate $flush to children in addition order', function(){
// this is not an external guarantee, just our own sanity
var log = '';
var childA = root.$new();
var childB = root.$new();
var childC = root.$new();
childA.$observe('a', function(){ log += 'a'; });
childB.$observe('b', function(){ log += 'b'; });
childC.$observe('c', function(){ log += 'c'; });
childA.a = childB.b = childC.c = 1;
root.$flush();
expect(log).toEqual('abc');
});
it('should fire observers once at beggining and on change', function(){
var log = '';
root.$observe('c', function(self, v){self.d = v; log += 'c';});
root.$observe('b', function(self, v){self.c = v; log += 'b';});
root.$observe('a', function(self, v){self.b = v; log += 'a';});
root.a = 1;
root.$flush();
expect(root.b).toEqual(1);
expect(log).toEqual('cba');
root.$flush();
expect(root.c).toEqual(1);
expect(log).toEqual('cbab');
root.$flush();
expect(root.d).toEqual(1);
expect(log).toEqual('cbabc');
});
it('should fire on initial observe', function(){
var log = '';
root.a = 1;
root.$observe('a', function(){ log += 'a'; });
root.$observe('b', function(){ log += 'b'; });
expect(log).toEqual('');
root.$flush();
expect(log).toEqual('ab');
});
it('should observe objects', function(){
var log = '';
root.a = [];
root.b = {};
root.$observe('a', function(){ log +='.';});
root.$observe('a', function(){ log +='!';});
root.$flush();
expect(log).toEqual('.!');
root.$flush();
expect(log).toEqual('.!');
root.a.push({});
root.b.name = '';
root.$digest();
expect(log).toEqual('.!');
});
it('should prevent recursion', function(){
var callCount = 0;
root.$observe('name', function(){
expect(function(){
root.$digest();
}).toThrow('$flush already in progress');
expect(function(){
root.$flush();
}).toThrow('$flush already in progress');
callCount++;
});
root.name = 'a';
root.$flush();
expect(callCount).toEqual(1);
});
});
describe('$destroy', function(){
var first, middle, last, log;
beforeEach(function(){
log = '';
first = root.$new();
middle = root.$new();
last = root.$new();
first.$watch(function(){ log += '1';});
middle.$watch(function(){ log += '2';});
last.$watch(function(){ log += '3';});
log = '';
});
it('should ignore remove on root', function(){
root.$destroy();
root.$digest();
expect(log).toEqual('123');
});
it('should remove first', function(){
first.$destroy();
root.$digest();
expect(log).toEqual('23');
});
it('should remove middle', function(){
middle.$destroy();
root.$digest();
expect(log).toEqual('13');
});
it('should remove last', function(){
last.$destroy();
root.$digest();
expect(log).toEqual('12');
});
});
describe('$eval', function(){ describe('$eval', function(){
var model; it('should eval an expression', function(){
expect(root.$eval('a=1')).toEqual(1);
expect(root.a).toEqual(1);
beforeEach(function(){model = createScope();}); root.$eval(function(self){self.b=2;});
expect(root.b).toEqual(2);
it('should eval function with correct this', function(){
model.$eval(function(){
this.name = 'works';
});
expect(model.name).toEqual('works');
}); });
it('should eval expression with correct this', function(){
model.$eval('name="works"');
expect(model.name).toEqual('works');
});
it('should not bind regexps', function(){
model.exp = /abc/;
expect(model.$eval('exp')).toEqual(model.exp);
});
it('should do nothing on empty string and not update view', function(){
var onEval = jasmine.createSpy('onEval');
model.$onEval(onEval);
model.$eval('');
expect(onEval).not.toHaveBeenCalled();
});
it('should ignore none string/function', function(){
model.$eval(null);
model.$eval({});
model.$tryEval(null);
model.$tryEval({});
});
}); });
describe('$watch', function(){
it('should watch an expression for change', function(){ describe('$apply', function(){
var model = createScope(); it('should apply expression with full lifecycle', function(){
model.oldValue = ""; var log = '';
var nameCount = 0, evalCount = 0; var child = root.$new();
model.name = 'adam'; root.$watch('a', function(scope, a){ log += '1'; });
model.$watch('name', function(){ nameCount ++; }); root.$observe('a', function(scope, a){ log += '2'; });
model.$watch(function(){return model.name;}, function(newValue, oldValue){ child.$apply('$parent.a=0');
this.newValue = newValue; expect(log).toEqual('12');
this.oldValue = oldValue;
});
model.$onEval(function(){evalCount ++;});
model.name = 'misko';
model.$eval();
expect(nameCount).toEqual(2);
expect(evalCount).toEqual(1);
expect(model.newValue).toEqual('misko');
expect(model.oldValue).toEqual('adam');
}); });
it('should eval with no arguments', function(){
var model = createScope(); it('should catch exceptions', function(){
var count = 0; var log = '';
model.$onEval(function(){count++;}); var child = root.$new();
model.$eval(); root.$watch('a', function(scope, a){ log += '1'; });
expect(count).toEqual(1); root.$observe('a', function(scope, a){ log += '2'; });
root.a = 0;
child.$apply(function(){ throw new Error('MyError'); });
expect(log).toEqual('12');
expect(mockHandler.errors[0].message).toEqual('MyError');
$logMock.error.logs.shift();
}); });
it('should run listener upon registration by default', function() {
var model = createScope();
var count = 0,
nameNewVal = 'crazy val 1',
nameOldVal = 'crazy val 2';
model.$watch('name', function(newVal, oldVal){ describe('exceptions', function(){
count ++; var $exceptionHandler, $updateView, log;
nameNewVal = newVal; beforeEach(function(){
nameOldVal = oldVal; log = '';
$exceptionHandler = jasmine.createSpy('$exceptionHandler');
$updateView = jasmine.createSpy('$updateView');
root.$service = function(name) {
return {$updateView:$updateView, $exceptionHandler:$exceptionHandler}[name];
};
root.$watch(function(){ log += '$digest;'; });
log = '';
}); });
expect(count).toBe(1);
expect(nameNewVal).not.toBeDefined();
expect(nameOldVal).not.toBeDefined();
});
it('should not run listener upon registration if flag is passed in', function() { it('should execute and return value and update', function(){
var model = createScope(); root.name = 'abc';
var count = 0, expect(root.$apply(function(scope){
nameNewVal = 'crazy val 1', return scope.name;
nameOldVal = 'crazy val 2'; })).toEqual('abc');
expect(log).toEqual('$digest;');
model.$watch('name', function(newVal, oldVal){ expect($exceptionHandler).not.wasCalled();
count ++; expect($updateView).wasCalled();
nameNewVal = newVal;
nameOldVal = oldVal;
}, undefined, false);
expect(count).toBe(0);
expect(nameNewVal).toBe('crazy val 1');
expect(nameOldVal).toBe('crazy val 2');
});
});
describe('$bind', function(){
it('should curry a function with respect to scope', function(){
var model = createScope();
model.name = 'misko';
expect(model.$bind(function(){return this.name;})()).toEqual('misko');
});
});
describe('$tryEval', function(){
it('should report error using the provided error handler and $log.error', function(){
var scope = createScope(),
errorLogs = scope.$service('$log').error.logs;
scope.$tryEval(function(){throw "myError";}, function(error){
scope.error = error;
}); });
expect(scope.error).toEqual('myError');
expect(errorLogs.shift()[0]).toBe("myError");
});
it('should report error on visible element', function(){
var element = jqLite('<div></div>'),
scope = createScope(),
errorLogs = scope.$service('$log').error.logs;
scope.$tryEval(function(){throw "myError";}, element); it('should catch exception and update', function(){
expect(element.attr('ng-exception')).toEqual('myError'); var error = new Error('MyError');
expect(element.hasClass('ng-exception')).toBeTruthy(); root.$apply(function(){ throw error; });
expect(errorLogs.shift()[0]).toBe("myError"); expect(log).toEqual('$digest;');
}); expect($exceptionHandler).wasCalledWith(error);
expect($updateView).wasCalled();
it('should report error on $excetionHandler', function(){ });
var scope = createScope(null, {$exceptionHandler: $exceptionHandlerMockFactory},
{$log: $logMock});
scope.$tryEval(function(){throw "myError";});
expect(scope.$service('$exceptionHandler').errors.shift()).toEqual("myError");
expect(scope.$service('$log').error.logs.shift()).toEqual(["myError"]);
}); });
}); });
// $onEval
describe('$onEval', function(){
it("should eval using priority", function(){
var scope = createScope();
scope.log = "";
scope.$onEval('log = log + "middle;"');
scope.$onEval(-1, 'log = log + "first;"');
scope.$onEval(1, 'log = log + "last;"');
scope.$eval();
expect(scope.log).toEqual('first;middle;last;');
});
it("should have $root and $parent", function(){
var parent = createScope();
var scope = createScope(parent);
expect(scope.$root).toEqual(parent);
expect(scope.$parent).toEqual(parent);
});
});
describe('getterFn', function(){
it('should get chain', function(){
expect(getterFn('a.b')(undefined)).toEqual(undefined);
expect(getterFn('a.b')({})).toEqual(undefined);
expect(getterFn('a.b')({a:null})).toEqual(undefined);
expect(getterFn('a.b')({a:{}})).toEqual(undefined);
expect(getterFn('a.b')({a:{b:null}})).toEqual(null);
expect(getterFn('a.b')({a:{b:0}})).toEqual(0);
expect(getterFn('a.b')({a:{b:'abc'}})).toEqual('abc');
});
it('should map type method on top of expression', function(){
expect(getterFn('a.$filter')({a:[]})('')).toEqual([]);
});
it('should bind function this', function(){
expect(getterFn('a')({a:function($){return this.b + $;}, b:1})(2)).toEqual(3);
});
});
describe('$new', function(){
it('should create new child scope and $become controller', function(){
var parent = createScope(null, angularService, {exampleService: 'Example Service'});
var child = parent.$new(temp.InjectController, 10);
expect(child.localService).toEqual('Example Service');
expect(child.extra).toEqual(10);
child.$onEval(function(){ this.run = true; });
parent.$eval();
expect(child.run).toEqual(true);
});
});
describe('$become', function(){
it('should inject properties on controller defined in $inject', function(){
var parent = createScope(null, angularService, {exampleService: 'Example Service'});
var child = createScope(parent);
child.$become(temp.InjectController, 10);
expect(child.localService).toEqual('Example Service');
expect(child.extra).toEqual(10);
});
});
}); });

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
describe('ValidatorTest', function(){ describe('Validator', function(){
it('ShouldHaveThisSet', function() { it('ShouldHaveThisSet', function() {
var validator = {}; var validator = {};
@ -11,7 +11,7 @@ describe('ValidatorTest', function(){
}; };
var scope = compile('<input name="name" ng:validate="myValidator:\'hevery\'"/>')(); var scope = compile('<input name="name" ng:validate="myValidator:\'hevery\'"/>')();
scope.name = 'misko'; scope.name = 'misko';
scope.$eval(); scope.$digest();
assertEquals('misko', validator.first); assertEquals('misko', validator.first);
assertEquals('hevery', validator.last); assertEquals('hevery', validator.last);
expect(validator._this.$id).toEqual(scope.$id); expect(validator._this.$id).toEqual(scope.$id);
@ -118,7 +118,7 @@ describe('ValidatorTest', function(){
value=v; fn=f; value=v; fn=f;
}; };
scope.name = "misko"; scope.name = "misko";
scope.$eval(); scope.$digest();
expect(value).toEqual('misko'); expect(value).toEqual('misko');
expect(input.hasClass('ng-input-indicator-wait')).toBeTruthy(); expect(input.hasClass('ng-input-indicator-wait')).toBeTruthy();
fn("myError"); fn("myError");
@ -158,7 +158,7 @@ describe('ValidatorTest', function(){
scope.asyncFn = jasmine.createSpy(); scope.asyncFn = jasmine.createSpy();
scope.updateFn = jasmine.createSpy(); scope.updateFn = jasmine.createSpy();
scope.name = 'misko'; scope.name = 'misko';
scope.$eval(); scope.$digest();
expect(scope.asyncFn).toHaveBeenCalledWith('misko', scope.asyncFn.mostRecentCall.args[1]); expect(scope.asyncFn).toHaveBeenCalledWith('misko', scope.asyncFn.mostRecentCall.args[1]);
assertTrue(scope.$element.hasClass('ng-input-indicator-wait')); assertTrue(scope.$element.hasClass('ng-input-indicator-wait'));
scope.asyncFn.mostRecentCall.args[1]('myError', {id: 1234, data:'data'}); scope.asyncFn.mostRecentCall.args[1]('myError', {id: 1234, data:'data'});

View file

@ -22,8 +22,9 @@ describe("directive", function(){
it("should ng:eval", function() { it("should ng:eval", function() {
var scope = compile('<div ng:init="a=0" ng:eval="a = a + 1"></div>'); var scope = compile('<div ng:init="a=0" ng:eval="a = a + 1"></div>');
scope.$flush();
expect(scope.a).toEqual(1); expect(scope.a).toEqual(1);
scope.$eval(); scope.$flush();
expect(scope.a).toEqual(2); expect(scope.a).toEqual(2);
}); });
@ -32,7 +33,7 @@ describe("directive", function(){
var scope = compile('<div ng:bind="a"></div>'); var scope = compile('<div ng:bind="a"></div>');
expect(element.text()).toEqual(''); expect(element.text()).toEqual('');
scope.a = 'misko'; scope.a = 'misko';
scope.$eval(); scope.$flush();
expect(element.hasClass('ng-binding')).toEqual(true); expect(element.hasClass('ng-binding')).toEqual(true);
expect(element.text()).toEqual('misko'); expect(element.text()).toEqual('misko');
}); });
@ -40,24 +41,24 @@ describe("directive", function(){
it('should set text to blank if undefined', function() { it('should set text to blank if undefined', function() {
var scope = compile('<div ng:bind="a"></div>'); var scope = compile('<div ng:bind="a"></div>');
scope.a = 'misko'; scope.a = 'misko';
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('misko'); expect(element.text()).toEqual('misko');
scope.a = undefined; scope.a = undefined;
scope.$eval(); scope.$flush();
expect(element.text()).toEqual(''); expect(element.text()).toEqual('');
}); });
it('should set html', function() { it('should set html', function() {
var scope = compile('<div ng:bind="html|html"></div>'); var scope = compile('<div ng:bind="html|html"></div>');
scope.html = '<div unknown>hello</div>'; scope.html = '<div unknown>hello</div>';
scope.$eval(); scope.$flush();
expect(lowercase(element.html())).toEqual('<div>hello</div>'); expect(lowercase(element.html())).toEqual('<div>hello</div>');
}); });
it('should set unsafe html', function() { it('should set unsafe html', function() {
var scope = compile('<div ng:bind="html|html:\'unsafe\'"></div>'); var scope = compile('<div ng:bind="html|html:\'unsafe\'"></div>');
scope.html = '<div onclick="">hello</div>'; scope.html = '<div onclick="">hello</div>';
scope.$eval(); scope.$flush();
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>'); expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
}); });
@ -66,7 +67,7 @@ describe("directive", function(){
return jqLite('<a>hello</a>'); return jqLite('<a>hello</a>');
}; };
var scope = compile('<div ng:bind="0|myElement"></div>'); var scope = compile('<div ng:bind="0|myElement"></div>');
scope.$eval(); scope.$flush();
expect(lowercase(element.html())).toEqual('<a>hello</a>'); expect(lowercase(element.html())).toEqual('<a>hello</a>');
}); });
@ -76,12 +77,14 @@ describe("directive", function(){
return 'HELLO'; return 'HELLO';
}; };
var scope = compile('<div>before<div ng:bind="0|myFilter"></div>after</div>'); var scope = compile('<div>before<div ng:bind="0|myFilter"></div>after</div>');
scope.$flush();
expect(sortedHtml(scope.$element)).toEqual('<div>before<div class="filter" ng:bind="0|myFilter">HELLO</div>after</div>'); expect(sortedHtml(scope.$element)).toEqual('<div>before<div class="filter" ng:bind="0|myFilter">HELLO</div>after</div>');
}); });
it('should suppress rendering of falsy values', function(){ it('should suppress rendering of falsy values', function(){
var scope = compile('<div>{{ null }}{{ undefined }}{{ "" }}-{{ 0 }}{{ false }}</div>'); var scope = compile('<div>{{ null }}{{ undefined }}{{ "" }}-{{ 0 }}{{ false }}</div>');
scope.$flush();
expect(scope.$element.text()).toEqual('-0false'); expect(scope.$element.text()).toEqual('-0false');
}); });
@ -90,8 +93,8 @@ describe("directive", function(){
describe('ng:bind-template', function(){ describe('ng:bind-template', function(){
it('should ng:bind-template', function() { it('should ng:bind-template', function() {
var scope = compile('<div ng:bind-template="Hello {{name}}!"></div>'); var scope = compile('<div ng:bind-template="Hello {{name}}!"></div>');
scope.$set('name', 'Misko'); scope.name = 'Misko';
scope.$eval(); scope.$flush();
expect(element.hasClass('ng-binding')).toEqual(true); expect(element.hasClass('ng-binding')).toEqual(true);
expect(element.text()).toEqual('Hello Misko!'); expect(element.text()).toEqual('Hello Misko!');
}); });
@ -103,6 +106,7 @@ describe("directive", function(){
return text; return text;
}; };
var scope = compile('<div>before<span ng:bind-template="{{\'HELLO\'|myFilter}}">INNER</span>after</div>'); var scope = compile('<div>before<span ng:bind-template="{{\'HELLO\'|myFilter}}">INNER</span>after</div>');
scope.$flush();
expect(scope.$element.text()).toEqual("beforeHELLOafter"); expect(scope.$element.text()).toEqual("beforeHELLOafter");
expect(innerText).toEqual('INNER'); expect(innerText).toEqual('INNER');
}); });
@ -112,12 +116,14 @@ describe("directive", function(){
describe('ng:bind-attr', function(){ describe('ng:bind-attr', function(){
it('should bind attributes', function(){ it('should bind attributes', function(){
var scope = compile('<div ng:bind-attr="{src:\'http://localhost/mysrc\', alt:\'myalt\'}"/>'); var scope = compile('<div ng:bind-attr="{src:\'http://localhost/mysrc\', alt:\'myalt\'}"/>');
scope.$flush();
expect(element.attr('src')).toEqual('http://localhost/mysrc'); expect(element.attr('src')).toEqual('http://localhost/mysrc');
expect(element.attr('alt')).toEqual('myalt'); expect(element.attr('alt')).toEqual('myalt');
}); });
it('should not pretty print JSON in attributes', function(){ it('should not pretty print JSON in attributes', function(){
var scope = compile('<img alt="{{ {a:1} }}"/>'); var scope = compile('<img alt="{{ {a:1} }}"/>');
scope.$flush();
expect(element.attr('alt')).toEqual('{"a":1}'); expect(element.attr('alt')).toEqual('{"a":1}');
}); });
}); });
@ -132,7 +138,7 @@ describe("directive", function(){
scope.disabled = true; scope.disabled = true;
scope.readonly = true; scope.readonly = true;
scope.checked = true; scope.checked = true;
scope.$eval(); scope.$flush();
expect(input.disabled).toEqual(true); expect(input.disabled).toEqual(true);
expect(input.readOnly).toEqual(true); expect(input.readOnly).toEqual(true);
@ -142,16 +148,16 @@ describe("directive", function(){
describe('ng:click', function(){ describe('ng:click', function(){
it('should get called on a click', function(){ it('should get called on a click', function(){
var scope = compile('<div ng:click="clicked = true"></div>'); var scope = compile('<div ng:click="clicked = true"></div>');
scope.$eval(); scope.$flush();
expect(scope.$get('clicked')).toBeFalsy(); expect(scope.clicked).toBeFalsy();
browserTrigger(element, 'click'); browserTrigger(element, 'click');
expect(scope.$get('clicked')).toEqual(true); expect(scope.clicked).toEqual(true);
}); });
it('should stop event propagation', function() { it('should stop event propagation', function() {
var scope = compile('<div ng:click="outer = true"><div ng:click="inner = true"></div></div>'); var scope = compile('<div ng:click="outer = true"><div ng:click="inner = true"></div></div>');
scope.$eval(); scope.$flush();
expect(scope.outer).not.toBeDefined(); expect(scope.outer).not.toBeDefined();
expect(scope.inner).not.toBeDefined(); expect(scope.inner).not.toBeDefined();
@ -169,7 +175,7 @@ describe("directive", function(){
var scope = compile('<form action="" ng:submit="submitted = true">' + var scope = compile('<form action="" ng:submit="submitted = true">' +
'<input type="submit"/>' + '<input type="submit"/>' +
'</form>'); '</form>');
scope.$eval(); scope.$flush();
expect(scope.submitted).not.toBeDefined(); expect(scope.submitted).not.toBeDefined();
browserTrigger(element.children()[0]); browserTrigger(element.children()[0]);
@ -177,23 +183,22 @@ describe("directive", function(){
}); });
}); });
describe('ng:class', function() { describe('ng:class', function() {
it('should add new and remove old classes dynamically', function() { it('should add new and remove old classes dynamically', function() {
var scope = compile('<div class="existing" ng:class="dynClass"></div>'); var scope = compile('<div class="existing" ng:class="dynClass"></div>');
scope.dynClass = 'A'; scope.dynClass = 'A';
scope.$eval(); scope.$flush();
expect(element.hasClass('existing')).toBe(true); expect(element.hasClass('existing')).toBe(true);
expect(element.hasClass('A')).toBe(true); expect(element.hasClass('A')).toBe(true);
scope.dynClass = 'B'; scope.dynClass = 'B';
scope.$eval(); scope.$flush();
expect(element.hasClass('existing')).toBe(true); expect(element.hasClass('existing')).toBe(true);
expect(element.hasClass('A')).toBe(false); expect(element.hasClass('A')).toBe(false);
expect(element.hasClass('B')).toBe(true); expect(element.hasClass('B')).toBe(true);
delete scope.dynClass; delete scope.dynClass;
scope.$eval(); scope.$flush();
expect(element.hasClass('existing')).toBe(true); expect(element.hasClass('existing')).toBe(true);
expect(element.hasClass('A')).toBe(false); expect(element.hasClass('A')).toBe(false);
expect(element.hasClass('B')).toBe(false); expect(element.hasClass('B')).toBe(false);
@ -201,7 +206,7 @@ describe("directive", function(){
it('should support adding multiple classes', function(){ it('should support adding multiple classes', function(){
var scope = compile('<div class="existing" ng:class="[\'A\', \'B\']"></div>'); var scope = compile('<div class="existing" ng:class="[\'A\', \'B\']"></div>');
scope.$eval(); scope.$flush();
expect(element.hasClass('existing')).toBeTruthy(); expect(element.hasClass('existing')).toBeTruthy();
expect(element.hasClass('A')).toBeTruthy(); expect(element.hasClass('A')).toBeTruthy();
expect(element.hasClass('B')).toBeTruthy(); expect(element.hasClass('B')).toBeTruthy();
@ -211,7 +216,7 @@ describe("directive", function(){
it('should ng:class odd/even', function(){ it('should ng:class odd/even', function(){
var scope = compile('<ul><li ng:repeat="i in [0,1]" class="existing" ng:class-odd="\'odd\'" ng:class-even="\'even\'"></li><ul>'); var scope = compile('<ul><li ng:repeat="i in [0,1]" class="existing" ng:class-odd="\'odd\'" ng:class-even="\'even\'"></li><ul>');
scope.$eval(); scope.$flush();
var e1 = jqLite(element[0].childNodes[1]); var e1 = jqLite(element[0].childNodes[1]);
var e2 = jqLite(element[0].childNodes[2]); var e2 = jqLite(element[0].childNodes[2]);
expect(e1.hasClass('existing')).toBeTruthy(); expect(e1.hasClass('existing')).toBeTruthy();
@ -223,32 +228,32 @@ describe("directive", function(){
describe('ng:style', function(){ describe('ng:style', function(){
it('should set', function(){ it('should set', function(){
var scope = compile('<div ng:style="{height: \'40px\'}"></div>'); var scope = compile('<div ng:style="{height: \'40px\'}"></div>');
scope.$eval(); scope.$flush();
expect(element.css('height')).toEqual('40px'); expect(element.css('height')).toEqual('40px');
}); });
it('should silently ignore undefined style', function() { it('should silently ignore undefined style', function() {
var scope = compile('<div ng:style="myStyle"></div>'); var scope = compile('<div ng:style="myStyle"></div>');
scope.$eval(); scope.$flush();
expect(element.hasClass('ng-exception')).toBeFalsy(); expect(element.hasClass('ng-exception')).toBeFalsy();
}); });
it('should preserve and remove previous style', function(){ it('should preserve and remove previous style', function(){
var scope = compile('<div style="height: 10px;" ng:style="myStyle"></div>'); var scope = compile('<div style="height: 10px;" ng:style="myStyle"></div>');
scope.$eval(); scope.$flush();
expect(getStyle(element)).toEqual({height: '10px'}); expect(getStyle(element)).toEqual({height: '10px'});
scope.myStyle = {height: '20px', width: '10px'}; scope.myStyle = {height: '20px', width: '10px'};
scope.$eval(); scope.$flush();
expect(getStyle(element)).toEqual({height: '20px', width: '10px'}); expect(getStyle(element)).toEqual({height: '20px', width: '10px'});
scope.myStyle = {}; scope.myStyle = {};
scope.$eval(); scope.$flush();
expect(getStyle(element)).toEqual({height: '10px'}); expect(getStyle(element)).toEqual({height: '10px'});
}); });
}); });
it('should silently ignore undefined ng:style', function() { it('should silently ignore undefined ng:style', function() {
var scope = compile('<div ng:style="myStyle"></div>'); var scope = compile('<div ng:style="myStyle"></div>');
scope.$eval(); scope.$flush();
expect(element.hasClass('ng-exception')).toBeFalsy(); expect(element.hasClass('ng-exception')).toBeFalsy();
}); });
@ -258,9 +263,10 @@ describe("directive", function(){
var element = jqLite('<div ng:show="exp"></div>'), var element = jqLite('<div ng:show="exp"></div>'),
scope = compile(element); scope = compile(element);
scope.$flush();
expect(isCssVisible(element)).toEqual(false); expect(isCssVisible(element)).toEqual(false);
scope.exp = true; scope.exp = true;
scope.$eval(); scope.$flush();
expect(isCssVisible(element)).toEqual(true); expect(isCssVisible(element)).toEqual(true);
}); });
@ -271,7 +277,7 @@ describe("directive", function(){
expect(isCssVisible(element)).toBe(false); expect(isCssVisible(element)).toBe(false);
scope.exp = true; scope.exp = true;
scope.$eval(); scope.$flush();
expect(isCssVisible(element)).toBe(true); expect(isCssVisible(element)).toBe(true);
}); });
}); });
@ -283,7 +289,7 @@ describe("directive", function(){
expect(isCssVisible(element)).toBe(true); expect(isCssVisible(element)).toBe(true);
scope.exp = true; scope.exp = true;
scope.$eval(); scope.$flush();
expect(isCssVisible(element)).toBe(false); expect(isCssVisible(element)).toBe(false);
}); });
}); });
@ -333,11 +339,13 @@ describe("directive", function(){
expect(scope.greeter.greeting).toEqual('hello'); expect(scope.greeter.greeting).toEqual('hello');
expect(scope.childGreeter.greeting).toEqual('hey'); expect(scope.childGreeter.greeting).toEqual('hey');
expect(scope.childGreeter.$parent.greeting).toEqual('hello'); expect(scope.childGreeter.$parent.greeting).toEqual('hello');
scope.$flush();
expect(scope.$element.text()).toEqual('hey dude!'); expect(scope.$element.text()).toEqual('hey dude!');
}); });
}); });
//TODO(misko): this needs to be deleted when ng:eval-order is gone
it('should eval things according to ng:eval-order', function(){ it('should eval things according to ng:eval-order', function(){
var scope = compile( var scope = compile(
'<div ng:init="log=\'\'">' + '<div ng:init="log=\'\'">' +
@ -348,6 +356,7 @@ describe("directive", function(){
'<span bind-template="{{log = log + \'d\'}}"></span>' + '<span bind-template="{{log = log + \'d\'}}"></span>' +
'</span>' + '</span>' +
'</div>'); '</div>');
scope.$flush();
expect(scope.log).toEqual('abcde'); expect(scope.log).toEqual('abcde');
}); });

View file

@ -20,24 +20,25 @@ describe("markups", function(){
it('should translate {{}} in text', function(){ it('should translate {{}} in text', function(){
compile('<div>hello {{name}}!</div>'); compile('<div>hello {{name}}!</div>');
expect(sortedHtml(element)).toEqual('<div>hello <span ng:bind="name"></span>!</div>'); expect(sortedHtml(element)).toEqual('<div>hello <span ng:bind="name"></span>!</div>');
scope.$set('name', 'Misko'); scope.name = 'Misko';
scope.$eval(); scope.$flush();
expect(sortedHtml(element)).toEqual('<div>hello <span ng:bind="name">Misko</span>!</div>'); expect(sortedHtml(element)).toEqual('<div>hello <span ng:bind="name">Misko</span>!</div>');
}); });
it('should translate {{}} in terminal nodes', function(){ it('should translate {{}} in terminal nodes', function(){
compile('<select name="x"><option value="">Greet {{name}}!</option></select>'); compile('<select name="x"><option value="">Greet {{name}}!</option></select>');
scope.$flush();
expect(sortedHtml(element).replace(' selected="true"', '')).toEqual('<select name="x"><option ng:bind-template="Greet {{name}}!">Greet !</option></select>'); expect(sortedHtml(element).replace(' selected="true"', '')).toEqual('<select name="x"><option ng:bind-template="Greet {{name}}!">Greet !</option></select>');
scope.$set('name', 'Misko'); scope.name = 'Misko';
scope.$eval(); scope.$flush();
expect(sortedHtml(element).replace(' selected="true"', '')).toEqual('<select name="x"><option ng:bind-template="Greet {{name}}!">Greet Misko!</option></select>'); expect(sortedHtml(element).replace(' selected="true"', '')).toEqual('<select name="x"><option ng:bind-template="Greet {{name}}!">Greet Misko!</option></select>');
}); });
it('should translate {{}} in attributes', function(){ it('should translate {{}} in attributes', function(){
compile('<div src="http://server/{{path}}.png"/>'); compile('<div src="http://server/{{path}}.png"/>');
expect(element.attr('ng:bind-attr')).toEqual('{"src":"http://server/{{path}}.png"}'); expect(element.attr('ng:bind-attr')).toEqual('{"src":"http://server/{{path}}.png"}');
scope.$set('path', 'a/b'); scope.path = 'a/b';
scope.$eval(); scope.$flush();
expect(element.attr('src')).toEqual("http://server/a/b.png"); expect(element.attr('src')).toEqual("http://server/a/b.png");
}); });
@ -94,57 +95,57 @@ describe("markups", function(){
it('should bind disabled', function() { it('should bind disabled', function() {
compile('<button ng:disabled="{{isDisabled}}">Button</button>'); compile('<button ng:disabled="{{isDisabled}}">Button</button>');
scope.isDisabled = false; scope.isDisabled = false;
scope.$eval(); scope.$flush();
expect(element.attr('disabled')).toBeFalsy(); expect(element.attr('disabled')).toBeFalsy();
scope.isDisabled = true; scope.isDisabled = true;
scope.$eval(); scope.$flush();
expect(element.attr('disabled')).toBeTruthy(); expect(element.attr('disabled')).toBeTruthy();
}); });
it('should bind checked', function() { it('should bind checked', function() {
compile('<input type="checkbox" ng:checked="{{isChecked}}" />'); compile('<input type="checkbox" ng:checked="{{isChecked}}" />');
scope.isChecked = false; scope.isChecked = false;
scope.$eval(); scope.$flush();
expect(element.attr('checked')).toBeFalsy(); expect(element.attr('checked')).toBeFalsy();
scope.isChecked=true; scope.isChecked=true;
scope.$eval(); scope.$flush();
expect(element.attr('checked')).toBeTruthy(); expect(element.attr('checked')).toBeTruthy();
}); });
it('should bind selected', function() { it('should bind selected', function() {
compile('<select><option value=""></option><option ng:selected="{{isSelected}}">Greetings!</option></select>'); compile('<select><option value=""></option><option ng:selected="{{isSelected}}">Greetings!</option></select>');
scope.isSelected=false; scope.isSelected=false;
scope.$eval(); scope.$flush();
expect(element.children()[1].selected).toBeFalsy(); expect(element.children()[1].selected).toBeFalsy();
scope.isSelected=true; scope.isSelected=true;
scope.$eval(); scope.$flush();
expect(element.children()[1].selected).toBeTruthy(); expect(element.children()[1].selected).toBeTruthy();
}); });
it('should bind readonly', function() { it('should bind readonly', function() {
compile('<input type="text" ng:readonly="{{isReadonly}}" />'); compile('<input type="text" ng:readonly="{{isReadonly}}" />');
scope.isReadonly=false; scope.isReadonly=false;
scope.$eval(); scope.$flush();
expect(element.attr('readOnly')).toBeFalsy(); expect(element.attr('readOnly')).toBeFalsy();
scope.isReadonly=true; scope.isReadonly=true;
scope.$eval(); scope.$flush();
expect(element.attr('readOnly')).toBeTruthy(); expect(element.attr('readOnly')).toBeTruthy();
}); });
it('should bind multiple', function() { it('should bind multiple', function() {
compile('<select ng:multiple="{{isMultiple}}"></select>'); compile('<select ng:multiple="{{isMultiple}}"></select>');
scope.isMultiple=false; scope.isMultiple=false;
scope.$eval(); scope.$flush();
expect(element.attr('multiple')).toBeFalsy(); expect(element.attr('multiple')).toBeFalsy();
scope.isMultiple='multiple'; scope.isMultiple='multiple';
scope.$eval(); scope.$flush();
expect(element.attr('multiple')).toBeTruthy(); expect(element.attr('multiple')).toBeTruthy();
}); });
it('should bind src', function() { it('should bind src', function() {
compile('<div ng:src="{{url}}" />'); compile('<div ng:src="{{url}}" />');
scope.url = 'http://localhost/'; scope.url = 'http://localhost/';
scope.$eval(); scope.$flush();
expect(element.attr('src')).toEqual('http://localhost/'); expect(element.attr('src')).toEqual('http://localhost/');
}); });

View file

@ -55,7 +55,7 @@ angular.service('$log', function() {
* this: * this:
* *
* <pre> * <pre>
* var scope = angular.scope(null, {'$exceptionHandler': $exceptionHandlerMockFactory}); * var scope = angular.scope(null, {'$exceptionHandler': $exceptionHandlerMockFactory()});
* </pre> * </pre>
* *
*/ */

View file

@ -31,14 +31,13 @@ describe('angular.scenario.SpecRunner', function() {
$window.setTimeout = function(fn, timeout) { $window.setTimeout = function(fn, timeout) {
fn(); fn();
}; };
$root = angular.scope({ $root = angular.scope();
emit: function(eventName) { $root.emit = function(eventName) {
log.push(eventName); log.push(eventName);
}, };
on: function(eventName) { $root.on = function(eventName) {
log.push('Listener Added for ' + eventName); log.push('Listener Added for ' + eventName);
} };
});
$root.application = new ApplicationMock($window); $root.application = new ApplicationMock($window);
$root.$window = $window; $root.$window = $window;
runner = $root.$new(angular.scenario.SpecRunner); runner = $root.$new(angular.scenario.SpecRunner);

View file

@ -10,14 +10,13 @@ describe("angular.scenario.dsl", function() {
document: _jQuery("<div></div>"), document: _jQuery("<div></div>"),
angular: new angular.scenario.testing.MockAngular() angular: new angular.scenario.testing.MockAngular()
}; };
$root = angular.scope({ $root = angular.scope();
emit: function(eventName) { $root.emit = function(eventName) {
eventLog.push(eventName); eventLog.push(eventName);
}, };
on: function(eventName) { $root.on = function(eventName) {
eventLog.push('Listener Added for ' + eventName); eventLog.push('Listener Added for ' + eventName);
} };
});
$root.futures = []; $root.futures = [];
$root.futureLog = []; $root.futureLog = [];
$root.$window = $window; $root.$window = $window;

View file

@ -16,7 +16,7 @@ describe('$cookieStore', function() {
it('should serialize objects to json', function() { it('should serialize objects to json', function() {
$cookieStore.put('objectCookie', {id: 123, name: 'blah'}); $cookieStore.put('objectCookie', {id: 123, name: 'blah'});
scope.$eval(); //force eval in test scope.$flush();
expect($browser.cookies()).toEqual({'objectCookie': '{"id":123,"name":"blah"}'}); expect($browser.cookies()).toEqual({'objectCookie': '{"id":123,"name":"blah"}'});
}); });
@ -30,12 +30,12 @@ describe('$cookieStore', function() {
it('should delete objects from the store when remove is called', function() { it('should delete objects from the store when remove is called', function() {
$cookieStore.put('gonner', { "I'll":"Be Back"}); $cookieStore.put('gonner', { "I'll":"Be Back"});
scope.$eval(); //force eval in test scope.$flush(); //force eval in test
$browser.poll(); $browser.poll();
expect($browser.cookies()).toEqual({'gonner': '{"I\'ll":"Be Back"}'}); expect($browser.cookies()).toEqual({'gonner': '{"I\'ll":"Be Back"}'});
$cookieStore.remove('gonner'); $cookieStore.remove('gonner');
scope.$eval(); scope.$flush();
expect($browser.cookies()).toEqual({}); expect($browser.cookies()).toEqual({});
}); });
}); });

View file

@ -6,7 +6,7 @@ describe('$cookies', function() {
beforeEach(function() { beforeEach(function() {
$browser = new MockBrowser(); $browser = new MockBrowser();
$browser.cookieHash['preexisting'] = 'oldCookie'; $browser.cookieHash['preexisting'] = 'oldCookie';
scope = angular.scope(null, angular.service, {$browser: $browser}); scope = angular.scope(angular.service, {$browser: $browser});
scope.$cookies = scope.$service('$cookies'); scope.$cookies = scope.$service('$cookies');
}); });
@ -38,13 +38,13 @@ describe('$cookies', function() {
it('should create or update a cookie when a value is assigned to a property', function() { it('should create or update a cookie when a value is assigned to a property', function() {
scope.$cookies.oatmealCookie = 'nom nom'; scope.$cookies.oatmealCookie = 'nom nom';
scope.$eval(); scope.$flush();
expect($browser.cookies()). expect($browser.cookies()).
toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'}); toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'});
scope.$cookies.oatmealCookie = 'gone'; scope.$cookies.oatmealCookie = 'gone';
scope.$eval(); scope.$flush();
expect($browser.cookies()). expect($browser.cookies()).
toEqual({'preexisting': 'oldCookie', 'oatmealCookie': 'gone'}); toEqual({'preexisting': 'oldCookie', 'oatmealCookie': 'gone'});
@ -56,7 +56,7 @@ describe('$cookies', function() {
scope.$cookies.nullVal = null; scope.$cookies.nullVal = null;
scope.$cookies.undefVal = undefined; scope.$cookies.undefVal = undefined;
scope.$cookies.preexisting = function(){}; scope.$cookies.preexisting = function(){};
scope.$eval(); scope.$flush();
expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'}); expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'});
expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'}); expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'});
}); });
@ -64,13 +64,13 @@ describe('$cookies', function() {
it('should remove a cookie when a $cookies property is deleted', function() { it('should remove a cookie when a $cookies property is deleted', function() {
scope.$cookies.oatmealCookie = 'nom nom'; scope.$cookies.oatmealCookie = 'nom nom';
scope.$eval(); scope.$flush();
$browser.poll(); $browser.poll();
expect($browser.cookies()). expect($browser.cookies()).
toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'}); toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'});
delete scope.$cookies.oatmealCookie; delete scope.$cookies.oatmealCookie;
scope.$eval(); scope.$flush();
expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'}); expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'});
}); });
@ -85,16 +85,16 @@ describe('$cookies', function() {
//drop if no previous value //drop if no previous value
scope.$cookies.longCookie = longVal; scope.$cookies.longCookie = longVal;
scope.$eval(); scope.$flush();
expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'}); expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'});
//reset if previous value existed //reset if previous value existed
scope.$cookies.longCookie = 'shortVal'; scope.$cookies.longCookie = 'shortVal';
scope.$eval(); scope.$flush();
expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'}); expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'});
scope.$cookies.longCookie = longVal; scope.$cookies.longCookie = longVal;
scope.$eval(); scope.$flush();
expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'}); expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'});
}); });
}); });

View file

@ -4,7 +4,7 @@ describe('$defer', function() {
var scope, $browser, $defer, $exceptionHandler; var scope, $browser, $defer, $exceptionHandler;
beforeEach(function(){ beforeEach(function(){
scope = angular.scope({}, angular.service, scope = angular.scope(angular.service,
{'$exceptionHandler': jasmine.createSpy('$exceptionHandler')}); {'$exceptionHandler': jasmine.createSpy('$exceptionHandler')});
$browser = scope.$service('$browser'); $browser = scope.$service('$browser');
$defer = scope.$service('$defer'); $defer = scope.$service('$defer');
@ -41,32 +41,32 @@ describe('$defer', function() {
}); });
it('should call eval after each callback is executed', function() { it('should call $apply after each callback is executed', function() {
var evalSpy = this.spyOn(scope, '$eval').andCallThrough(); var applySpy = this.spyOn(scope, '$apply').andCallThrough();
$defer(function() {}); $defer(function() {});
expect(evalSpy).not.toHaveBeenCalled(); expect(applySpy).not.toHaveBeenCalled();
$browser.defer.flush(); $browser.defer.flush();
expect(evalSpy).toHaveBeenCalled(); expect(applySpy).toHaveBeenCalled();
evalSpy.reset(); //reset the spy; applySpy.reset(); //reset the spy;
$defer(function() {}); $defer(function() {});
$defer(function() {}); $defer(function() {});
$browser.defer.flush(); $browser.defer.flush();
expect(evalSpy.callCount).toBe(2); expect(applySpy.callCount).toBe(2);
}); });
it('should call eval even if an exception is thrown in callback', function() { it('should call $apply even if an exception is thrown in callback', function() {
var evalSpy = this.spyOn(scope, '$eval').andCallThrough(); var applySpy = this.spyOn(scope, '$apply').andCallThrough();
$defer(function() {throw "Test Error";}); $defer(function() {throw "Test Error";});
expect(evalSpy).not.toHaveBeenCalled(); expect(applySpy).not.toHaveBeenCalled();
$browser.defer.flush(); $browser.defer.flush();
expect(evalSpy).toHaveBeenCalled(); expect(applySpy).toHaveBeenCalled();
}); });
it('should allow you to specify the delay time', function(){ it('should allow you to specify the delay time', function(){

View file

@ -14,11 +14,12 @@ describe('$exceptionHandler', function() {
it('should log errors', function(){ it('should log errors', function(){
var scope = createScope({}, {$exceptionHandler: $exceptionHandlerFactory}, var scope = createScope({$exceptionHandler: $exceptionHandlerFactory},
{$log: $logMock}), {$log: $logMock}),
$log = scope.$service('$log'), $log = scope.$service('$log'),
$exceptionHandler = scope.$service('$exceptionHandler'); $exceptionHandler = scope.$service('$exceptionHandler');
$log.error.rethrow = false;
$exceptionHandler('myError'); $exceptionHandler('myError');
expect($log.error.logs.shift()).toEqual(['myError']); expect($log.error.logs.shift()).toEqual(['myError']);
}); });

View file

@ -21,21 +21,21 @@ describe('$invalidWidgets', function() {
expect($invalidWidgets.length).toEqual(1); expect($invalidWidgets.length).toEqual(1);
scope.price = 123; scope.price = 123;
scope.$eval(); scope.$digest();
expect($invalidWidgets.length).toEqual(0); expect($invalidWidgets.length).toEqual(0);
scope.$element.remove(); scope.$element.remove();
scope.price = 'abc'; scope.price = 'abc';
scope.$eval(); scope.$digest();
expect($invalidWidgets.length).toEqual(0); expect($invalidWidgets.length).toEqual(0);
jqLite(document.body).append(scope.$element); jqLite(document.body).append(scope.$element);
scope.price = 'abcd'; //force revalidation, maybe this should be done automatically? scope.price = 'abcd'; //force revalidation, maybe this should be done automatically?
scope.$eval(); scope.$digest();
expect($invalidWidgets.length).toEqual(1); expect($invalidWidgets.length).toEqual(1);
jqLite(document.body).html(''); jqLite(document.body).html('');
scope.$eval(); scope.$digest();
expect($invalidWidgets.length).toEqual(0); expect($invalidWidgets.length).toEqual(0);
}); });
}); });

View file

@ -46,9 +46,10 @@ describe('$location', function() {
$location.update('http://www.angularjs.org/'); $location.update('http://www.angularjs.org/');
$location.update({path: '/a/b'}); $location.update({path: '/a/b'});
expect($location.href).toEqual('http://www.angularjs.org/a/b'); expect($location.href).toEqual('http://www.angularjs.org/a/b');
expect($browser.getUrl()).toEqual(origBrowserUrl);
scope.$eval();
expect($browser.getUrl()).toEqual('http://www.angularjs.org/a/b'); expect($browser.getUrl()).toEqual('http://www.angularjs.org/a/b');
$location.path = '/c';
scope.$digest();
expect($browser.getUrl()).toEqual('http://www.angularjs.org/c');
}); });
@ -65,7 +66,7 @@ describe('$location', function() {
it('should update hash on hashPath or hashSearch update', function() { it('should update hash on hashPath or hashSearch update', function() {
$location.update('http://server/#path?a=b'); $location.update('http://server/#path?a=b');
scope.$eval(); scope.$digest();
$location.update({hashPath: '', hashSearch: {}}); $location.update({hashPath: '', hashSearch: {}});
expect($location.hash).toEqual(''); expect($location.hash).toEqual('');
@ -74,10 +75,10 @@ describe('$location', function() {
it('should update hashPath and hashSearch on $location.hash change upon eval', function(){ it('should update hashPath and hashSearch on $location.hash change upon eval', function(){
$location.update('http://server/#path?a=b'); $location.update('http://server/#path?a=b');
scope.$eval(); scope.$digest();
$location.hash = ''; $location.hash = '';
scope.$eval(); scope.$digest();
expect($location.href).toEqual('http://server/'); expect($location.href).toEqual('http://server/');
expect($location.hashPath).toEqual(''); expect($location.hashPath).toEqual('');
@ -88,11 +89,13 @@ describe('$location', function() {
it('should update hash on $location.hashPath or $location.hashSearch change upon eval', it('should update hash on $location.hashPath or $location.hashSearch change upon eval',
function() { function() {
$location.update('http://server/#path?a=b'); $location.update('http://server/#path?a=b');
scope.$eval(); expect($location.href).toEqual('http://server/#path?a=b');
expect($location.hashPath).toEqual('path');
expect($location.hashSearch).toEqual({a:'b'});
$location.hashPath = ''; $location.hashPath = '';
$location.hashSearch = {}; $location.hashSearch = {};
scope.$digest();
scope.$eval();
expect($location.href).toEqual('http://server/'); expect($location.href).toEqual('http://server/');
expect($location.hash).toEqual(''); expect($location.hash).toEqual('');
@ -103,14 +106,14 @@ describe('$location', function() {
scope.$location = scope.$service('$location'); //publish to the scope for $watch scope.$location = scope.$service('$location'); //publish to the scope for $watch
var log = ''; var log = '';
scope.$watch('$location.hash', function(){ scope.$watch('$location.hash', function(scope){
log += this.$location.hashPath + ';'; log += scope.$location.hashPath + ';';
}); })();
expect(log).toEqual(';'); expect(log).toEqual(';');
log = ''; log = '';
scope.$location.hash = '/abc'; scope.$location.hash = '/abc';
scope.$eval(); scope.$digest();
expect(scope.$location.hash).toEqual('/abc'); expect(scope.$location.hash).toEqual('/abc');
expect(log).toEqual('/abc;'); expect(log).toEqual('/abc;');
}); });
@ -120,7 +123,7 @@ describe('$location', function() {
it('should update hash with escaped hashPath', function() { it('should update hash with escaped hashPath', function() {
$location.hashPath = 'foo=bar'; $location.hashPath = 'foo=bar';
scope.$eval(); scope.$digest();
expect($location.hash).toBe('foo%3Dbar'); expect($location.hash).toBe('foo%3Dbar');
}); });
@ -133,7 +136,7 @@ describe('$location', function() {
$location.host = 'host'; $location.host = 'host';
$location.href = 'https://hrefhost:23/hrefpath'; $location.href = 'https://hrefhost:23/hrefpath';
scope.$eval(); scope.$digest();
expect($location).toEqualData({href: 'https://hrefhost:23/hrefpath', expect($location).toEqualData({href: 'https://hrefhost:23/hrefpath',
protocol: 'https', protocol: 'https',
@ -156,7 +159,7 @@ describe('$location', function() {
$location.host = 'host'; $location.host = 'host';
$location.path = '/path'; $location.path = '/path';
scope.$eval(); scope.$digest();
expect($location).toEqualData({href: 'http://host:333/path#hash', expect($location).toEqualData({href: 'http://host:333/path#hash',
protocol: 'http', protocol: 'http',
@ -237,7 +240,7 @@ describe('$location', function() {
expect($location.href).toBe('http://server'); expect($location.href).toBe('http://server');
expect($location.hash).toBe(''); expect($location.hash).toBe('');
scope.$eval(); scope.$digest();
expect($location.href).toBe('http://server'); expect($location.href).toBe('http://server');
expect($location.hash).toBe(''); expect($location.hash).toBe('');

View file

@ -19,12 +19,12 @@ describe('$log', function() {
function warn(){ logger+= 'warn;'; } function warn(){ logger+= 'warn;'; }
function info(){ logger+= 'info;'; } function info(){ logger+= 'info;'; }
function error(){ logger+= 'error;'; } function error(){ logger+= 'error;'; }
var scope = createScope({}, {$log: $logFactory}, var scope = createScope({$log: $logFactory},
{$exceptionHandler: rethrow, {$exceptionHandler: rethrow,
$window: {console: {log: log, $window: {console: {log: log,
warn: warn, warn: warn,
info: info, info: info,
error: error}}}), error: error}}}),
$log = scope.$service('$log'); $log = scope.$service('$log');
$log.log(); $log.log();
@ -38,9 +38,9 @@ describe('$log', function() {
it('should use console.log() if other not present', function(){ it('should use console.log() if other not present', function(){
var logger = ""; var logger = "";
function log(){ logger+= 'log;'; } function log(){ logger+= 'log;'; }
var scope = createScope({}, {$log: $logFactory}, var scope = createScope({$log: $logFactory},
{$window: {console:{log:log}}, {$window: {console:{log:log}},
$exceptionHandler: rethrow}); $exceptionHandler: rethrow});
var $log = scope.$service('$log'); var $log = scope.$service('$log');
$log.log(); $log.log();
$log.warn(); $log.warn();
@ -51,9 +51,9 @@ describe('$log', function() {
it('should use noop if no console', function(){ it('should use noop if no console', function(){
var scope = createScope({}, {$log: $logFactory}, var scope = createScope({$log: $logFactory},
{$window: {}, {$window: {},
$exceptionHandler: rethrow}), $exceptionHandler: rethrow}),
$log = scope.$service('$log'); $log = scope.$service('$log');
$log.log(); $log.log();
$log.warn(); $log.warn();

View file

@ -18,7 +18,7 @@ describe('$route', function() {
$location, $route; $location, $route;
function BookChapter() { function BookChapter() {
this.log = '<init>'; log += '<init>';
} }
scope = compile('<div></div>')(); scope = compile('<div></div>')();
$location = scope.$service('$location'); $location = scope.$service('$location');
@ -28,28 +28,28 @@ describe('$route', function() {
$route.onChange(function(){ $route.onChange(function(){
log += 'onChange();'; log += 'onChange();';
}); });
$location.update('http://server#/Book/Moby/Chapter/Intro?p=123'); $location.update('http://server#/Book/Moby/Chapter/Intro?p=123');
scope.$eval(); scope.$digest();
expect(log).toEqual('onChange();'); expect(log).toEqual('onChange();<init>');
expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', p:'123'}); expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', p:'123'});
expect($route.current.scope.log).toEqual('<init>');
var lastId = $route.current.scope.$id; var lastId = $route.current.scope.$id;
log = ''; log = '';
$location.update('http://server#/Blank?ignore'); $location.update('http://server#/Blank?ignore');
scope.$eval(); scope.$digest();
expect(log).toEqual('onChange();'); expect(log).toEqual('onChange();');
expect($route.current.params).toEqual({ignore:true}); expect($route.current.params).toEqual({ignore:true});
expect($route.current.scope.$id).not.toEqual(lastId); expect($route.current.scope.$id).not.toEqual(lastId);
log = ''; log = '';
$location.update('http://server#/NONE'); $location.update('http://server#/NONE');
scope.$eval(); scope.$digest();
expect(log).toEqual('onChange();'); expect(log).toEqual('onChange();');
expect($route.current).toEqual(null); expect($route.current).toEqual(null);
$route.when('/NONE', {template:'instant update'}); $route.when('/NONE', {template:'instant update'});
scope.$eval(); scope.$digest();
expect($route.current.template).toEqual('instant update'); expect($route.current.template).toEqual('instant update');
}); });
@ -75,7 +75,7 @@ describe('$route', function() {
expect(onChangeSpy).not.toHaveBeenCalled(); expect(onChangeSpy).not.toHaveBeenCalled();
$location.updateHash('/foo'); $location.updateHash('/foo');
scope.$eval(); scope.$digest();
expect($route.current.template).toEqual('foo.html'); expect($route.current.template).toEqual('foo.html');
expect($route.current.controller).toBeUndefined(); expect($route.current.controller).toBeUndefined();
@ -98,7 +98,7 @@ describe('$route', function() {
expect(onChangeSpy).not.toHaveBeenCalled(); expect(onChangeSpy).not.toHaveBeenCalled();
$location.updateHash('/unknownRoute'); $location.updateHash('/unknownRoute');
scope.$eval(); scope.$digest();
expect($route.current.template).toBe('404.html'); expect($route.current.template).toBe('404.html');
expect($route.current.controller).toBe(NotFoundCtrl); expect($route.current.controller).toBe(NotFoundCtrl);
@ -107,7 +107,7 @@ describe('$route', function() {
onChangeSpy.reset(); onChangeSpy.reset();
$location.updateHash('/foo'); $location.updateHash('/foo');
scope.$eval(); scope.$digest();
expect($route.current.template).toEqual('foo.html'); expect($route.current.template).toEqual('foo.html');
expect($route.current.controller).toBeUndefined(); expect($route.current.controller).toBeUndefined();
@ -115,6 +115,39 @@ describe('$route', function() {
expect(onChangeSpy).toHaveBeenCalled(); expect(onChangeSpy).toHaveBeenCalled();
}); });
it('should $destroy old routes', function(){
var scope = angular.scope(),
$location = scope.$service('$location'),
$route = scope.$service('$route');
$route.when('/foo', {template: 'foo.html', controller: function(){ this.name = 'FOO';}});
$route.when('/bar', {template: 'bar.html', controller: function(){ this.name = 'BAR';}});
$route.when('/baz', {template: 'baz.html'});
expect(scope.$childHead).toEqual(null);
$location.updateHash('/foo');
scope.$digest();
expect(scope.$$childHead).toBeTruthy();
expect(scope.$$childHead).toEqual(scope.$$childTail);
$location.updateHash('/bar');
scope.$digest();
expect(scope.$$childHead).toBeTruthy();
expect(scope.$$childHead).toEqual(scope.$$childTail);
return
$location.updateHash('/baz');
scope.$digest();
expect(scope.$$childHead).toBeTruthy();
expect(scope.$$childHead).toEqual(scope.$$childTail);
$location.updateHash('/');
scope.$digest();
expect(scope.$$childHead).toEqual(null);
expect(scope.$$childTail).toEqual(null);
});
describe('redirection', function() { describe('redirection', function() {
@ -134,7 +167,7 @@ describe('$route', function() {
expect($route.current).toBeNull(); expect($route.current).toBeNull();
expect(onChangeSpy).not.toHaveBeenCalled(); expect(onChangeSpy).not.toHaveBeenCalled();
scope.$eval(); //triggers initial route change - match the redirect route scope.$digest(); //triggers initial route change - match the redirect route
$browser.defer.flush(); //triger route change - match the route we redirected to $browser.defer.flush(); //triger route change - match the route we redirected to
expect($location.hash).toBe('/foo'); expect($location.hash).toBe('/foo');
@ -143,7 +176,7 @@ describe('$route', function() {
onChangeSpy.reset(); onChangeSpy.reset();
$location.updateHash(''); $location.updateHash('');
scope.$eval(); //match the redirect route + update $browser scope.$digest(); //match the redirect route + update $browser
$browser.defer.flush(); //match the route we redirected to $browser.defer.flush(); //match the route we redirected to
expect($location.hash).toBe('/foo'); expect($location.hash).toBe('/foo');
@ -152,7 +185,7 @@ describe('$route', function() {
onChangeSpy.reset(); onChangeSpy.reset();
$location.updateHash('/baz'); $location.updateHash('/baz');
scope.$eval(); //match the redirect route + update $browser scope.$digest(); //match the redirect route + update $browser
$browser.defer.flush(); //match the route we redirected to $browser.defer.flush(); //match the route we redirected to
expect($location.hash).toBe('/bar'); expect($location.hash).toBe('/bar');
@ -170,10 +203,10 @@ describe('$route', function() {
$route.when('/foo/:id/foo/:subid/:extraId', {redirectTo: '/bar/:id/:subid/23'}); $route.when('/foo/:id/foo/:subid/:extraId', {redirectTo: '/bar/:id/:subid/23'});
$route.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'}); $route.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'});
scope.$eval(); scope.$digest();
$location.updateHash('/foo/id1/foo/subid3/gah'); $location.updateHash('/foo/id1/foo/subid3/gah');
scope.$eval(); //triggers initial route change - match the redirect route scope.$digest(); //triggers initial route change - match the redirect route
$browser.defer.flush(); //triger route change - match the route we redirected to $browser.defer.flush(); //triger route change - match the route we redirected to
expect($location.hash).toBe('/bar/id1/subid3/23?extraId=gah'); expect($location.hash).toBe('/bar/id1/subid3/23?extraId=gah');
@ -190,10 +223,10 @@ describe('$route', function() {
$route.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'}); $route.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'});
$route.when('/foo/:id/:extra', {redirectTo: '/bar/:id/:subid/99'}); $route.when('/foo/:id/:extra', {redirectTo: '/bar/:id/:subid/99'});
scope.$eval(); scope.$digest();
$location.hash = '/foo/id3/eId?subid=sid1&appended=true'; $location.hash = '/foo/id3/eId?subid=sid1&appended=true';
scope.$eval(); //triggers initial route change - match the redirect route scope.$digest(); //triggers initial route change - match the redirect route
$browser.defer.flush(); //triger route change - match the route we redirected to $browser.defer.flush(); //triger route change - match the route we redirected to
expect($location.hash).toBe('/bar/id3/sid1/99?appended=true&extra=eId'); expect($location.hash).toBe('/bar/id3/sid1/99?appended=true&extra=eId');
@ -210,10 +243,10 @@ describe('$route', function() {
$route.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'}); $route.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'});
$route.when('/foo/:id', $route.when('/foo/:id',
{redirectTo: customRedirectFn}); {redirectTo: customRedirectFn});
scope.$eval(); scope.$digest();
$location.hash = '/foo/id3?subid=sid1&appended=true'; $location.hash = '/foo/id3?subid=sid1&appended=true';
scope.$eval(); //triggers initial route change - match the redirect route scope.$digest(); //triggers initial route change - match the redirect route
$browser.defer.flush(); //triger route change - match the route we redirected to $browser.defer.flush(); //triger route change - match the route we redirected to
expect($location.hash).toBe('custom'); expect($location.hash).toBe('custom');

View file

@ -9,9 +9,9 @@ describe('$updateView', function() {
browser.isMock = false; browser.isMock = false;
browser.defer = jasmine.createSpy('defer'); browser.defer = jasmine.createSpy('defer');
scope = angular.scope(null, null, {$browser:browser}); scope = angular.scope(null, {$browser:browser});
$updateView = scope.$service('$updateView'); $updateView = scope.$service('$updateView');
scope.$onEval(function(){ evalCount++; }); scope.$observe(function(){ evalCount++; });
evalCount = 0; evalCount = 0;
}); });
@ -55,7 +55,7 @@ describe('$updateView', function() {
it('should update immediatelly in test/mock mode', function(){ it('should update immediatelly in test/mock mode', function(){
scope = angular.scope(); scope = angular.scope();
scope.$onEval(function(){ evalCount++; }); scope.$observe(function(){ evalCount++; });
expect(evalCount).toEqual(0); expect(evalCount).toEqual(0);
scope.$service('$updateView')(); scope.$service('$updateView')();
expect(evalCount).toEqual(1); expect(evalCount).toEqual(1);

View file

@ -4,7 +4,10 @@ describe('$xhr.bulk', function() {
var scope, $browser, $browserXhr, $log, $xhrBulk, $xhrError, log; var scope, $browser, $browserXhr, $log, $xhrBulk, $xhrError, log;
beforeEach(function(){ beforeEach(function(){
scope = angular.scope({}, null, {'$xhr.error': $xhrError = jasmine.createSpy('$xhr.error')}); scope = angular.scope(angular.service, {
'$xhr.error': $xhrError = jasmine.createSpy('$xhr.error'),
'$log': $log = {}
});
$browser = scope.$service('$browser'); $browser = scope.$service('$browser');
$browserXhr = $browser.xhr; $browserXhr = $browser.xhr;
$xhrBulk = scope.$service('$xhr.bulk'); $xhrBulk = scope.$service('$xhr.bulk');

View file

@ -4,7 +4,7 @@ describe('$xhr.cache', function() {
var scope, $browser, $browserXhr, $xhrErr, cache, log; var scope, $browser, $browserXhr, $xhrErr, cache, log;
beforeEach(function() { beforeEach(function() {
scope = angular.scope({}, null, {'$xhr.error': $xhrErr = jasmine.createSpy('$xhr.error')}); scope = angular.scope(angularService, {'$xhr.error': $xhrErr = jasmine.createSpy('$xhr.error')});
$browser = scope.$service('$browser'); $browser = scope.$service('$browser');
$browserXhr = $browser.xhr; $browserXhr = $browser.xhr;
cache = scope.$service('$xhr.cache'); cache = scope.$service('$xhr.cache');
@ -126,22 +126,22 @@ describe('$xhr.cache', function() {
it('should call eval after callbacks for both cache hit and cache miss execute', function() { it('should call eval after callbacks for both cache hit and cache miss execute', function() {
var evalSpy = this.spyOn(scope, '$eval').andCallThrough(); var flushSpy = this.spyOn(scope, '$flush').andCallThrough();
$browserXhr.expectGET('/url').respond('+'); $browserXhr.expectGET('/url').respond('+');
cache('GET', '/url', null, callback); cache('GET', '/url', null, callback);
expect(evalSpy).not.toHaveBeenCalled(); expect(flushSpy).not.toHaveBeenCalled();
$browserXhr.flush(); $browserXhr.flush();
expect(evalSpy).toHaveBeenCalled(); expect(flushSpy).toHaveBeenCalled();
evalSpy.reset(); //reset the spy flushSpy.reset(); //reset the spy
cache('GET', '/url', null, callback); cache('GET', '/url', null, callback);
expect(evalSpy).not.toHaveBeenCalled(); expect(flushSpy).not.toHaveBeenCalled();
$browser.defer.flush(); $browser.defer.flush();
expect(evalSpy).toHaveBeenCalled(); expect(flushSpy).toHaveBeenCalled();
}); });
it('should call the error callback on error if provided', function() { it('should call the error callback on error if provided', function() {

View file

@ -4,7 +4,7 @@ describe('$xhr.error', function() {
var scope, $browser, $browserXhr, $xhr, $xhrError, log; var scope, $browser, $browserXhr, $xhr, $xhrError, log;
beforeEach(function(){ beforeEach(function(){
scope = angular.scope({}, angular.service, { scope = angular.scope(angular.service, {
'$xhr.error': $xhrError = jasmine.createSpy('$xhr.error') '$xhr.error': $xhrError = jasmine.createSpy('$xhr.error')
}); });
$browser = scope.$service('$browser'); $browser = scope.$service('$browser');

View file

@ -4,7 +4,8 @@ describe('$xhr', function() {
var scope, $browser, $browserXhr, $log, $xhr, $xhrErr, log; var scope, $browser, $browserXhr, $log, $xhr, $xhrErr, log;
beforeEach(function(){ beforeEach(function(){
var scope = angular.scope({}, null, {'$xhr.error': $xhrErr = jasmine.createSpy('xhr.error')}); var scope = angular.scope(angular.service, {
'$xhr.error': $xhrErr = jasmine.createSpy('xhr.error')});
$log = scope.$service('$log'); $log = scope.$service('$log');
$browser = scope.$service('$browser'); $browser = scope.$service('$browser');
$browserXhr = $browser.xhr; $browserXhr = $browser.xhr;

View file

@ -130,10 +130,11 @@ function clearJqCache(){
count ++; count ++;
delete jqCache[key]; delete jqCache[key];
forEach(value, function(value, key){ forEach(value, function(value, key){
if (value.$element) if (value.$element) {
dump(key, sortedHtml(value.$element)); dump('LEAK', key, value.$id, sortedHtml(value.$element));
else } else {
dump(key, toJson(value)); dump('LEAK', key, toJson(value));
}
}); });
}); });
if (count) { if (count) {

View file

@ -13,7 +13,9 @@ describe("widget", function(){
} else { } else {
element = jqLite(html); element = jqLite(html);
} }
return scope = angular.compile(element)(); scope = angular.compile(element)();
scope.$apply();
return scope;
}; };
}); });
@ -26,25 +28,25 @@ describe("widget", function(){
describe("text", function(){ describe("text", function(){
it('should input-text auto init and handle keydown/change events', function(){ it('should input-text auto init and handle keydown/change events', function(){
compile('<input type="Text" name="name" value="Misko" ng:change="count = count + 1" ng:init="count=0"/>'); compile('<input type="Text" name="name" value="Misko" ng:change="count = count + 1" ng:init="count=0"/>');
expect(scope.$get('name')).toEqual("Misko"); expect(scope.name).toEqual("Misko");
expect(scope.$get('count')).toEqual(0); expect(scope.count).toEqual(0);
scope.$set('name', 'Adam'); scope.name = 'Adam';
scope.$eval(); scope.$digest();
expect(element.val()).toEqual("Adam"); expect(element.val()).toEqual("Adam");
element.val('Shyam'); element.val('Shyam');
browserTrigger(element, 'keydown'); browserTrigger(element, 'keydown');
// keydown event must be deferred // keydown event must be deferred
expect(scope.$get('name')).toEqual('Adam'); expect(scope.name).toEqual('Adam');
scope.$service('$browser').defer.flush(); scope.$service('$browser').defer.flush();
expect(scope.$get('name')).toEqual('Shyam'); expect(scope.name).toEqual('Shyam');
expect(scope.$get('count')).toEqual(1); expect(scope.count).toEqual(1);
element.val('Kai'); element.val('Kai');
browserTrigger(element, 'change'); browserTrigger(element, 'change');
expect(scope.$get('name')).toEqual('Kai'); expect(scope.name).toEqual('Kai');
expect(scope.$get('count')).toEqual(2); expect(scope.count).toEqual(2);
}); });
it('should not trigger eval if value does not change', function(){ it('should not trigger eval if value does not change', function(){
@ -67,15 +69,15 @@ describe("widget", function(){
it("should format text", function(){ it("should format text", function(){
compile('<input type="Text" name="list" value="a,b,c" ng:format="list"/>'); compile('<input type="Text" name="list" value="a,b,c" ng:format="list"/>');
expect(scope.$get('list')).toEqual(['a', 'b', 'c']); expect(scope.list).toEqual(['a', 'b', 'c']);
scope.$set('list', ['x', 'y', 'z']); scope.list = ['x', 'y', 'z'];
scope.$eval(); scope.$digest();
expect(element.val()).toEqual("x, y, z"); expect(element.val()).toEqual("x, y, z");
element.val('1, 2, 3'); element.val('1, 2, 3');
browserTrigger(element); browserTrigger(element);
expect(scope.$get('list')).toEqual(['1', '2', '3']); expect(scope.list).toEqual(['1', '2', '3']);
}); });
it("should come up blank if null", function(){ it("should come up blank if null", function(){
@ -87,7 +89,7 @@ describe("widget", function(){
it("should show incorect text while number does not parse", function(){ it("should show incorect text while number does not parse", function(){
compile('<input type="text" name="age" ng:format="number"/>'); compile('<input type="text" name="age" ng:format="number"/>');
scope.age = 123; scope.age = 123;
scope.$eval(); scope.$digest();
scope.$element.val('123X'); scope.$element.val('123X');
browserTrigger(scope.$element, 'change'); browserTrigger(scope.$element, 'change');
expect(scope.$element.val()).toEqual('123X'); expect(scope.$element.val()).toEqual('123X');
@ -98,11 +100,11 @@ describe("widget", function(){
it("should clober incorect text if model changes", function(){ it("should clober incorect text if model changes", function(){
compile('<input type="text" name="age" ng:format="number" value="123X"/>'); compile('<input type="text" name="age" ng:format="number" value="123X"/>');
scope.age = 456; scope.age = 456;
scope.$eval(); scope.$digest();
expect(scope.$element.val()).toEqual('456'); expect(scope.$element.val()).toEqual('456');
}); });
it("should not clober text if model changes doe to itself", function(){ it("should not clober text if model changes due to itself", function(){
compile('<input type="text" name="list" ng:format="list" value="a"/>'); compile('<input type="text" name="list" ng:format="list" value="a"/>');
scope.$element.val('a '); scope.$element.val('a ');
@ -128,7 +130,7 @@ describe("widget", function(){
it("should come up blank when no value specifiend", function(){ it("should come up blank when no value specifiend", function(){
compile('<input type="text" name="age" ng:format="number"/>'); compile('<input type="text" name="age" ng:format="number"/>');
scope.$eval(); scope.$digest();
expect(scope.$element.val()).toEqual(''); expect(scope.$element.val()).toEqual('');
expect(scope.age).toEqual(null); expect(scope.age).toEqual(null);
}); });
@ -173,7 +175,7 @@ describe("widget", function(){
expect(scope.$element[0].checked).toEqual(false); expect(scope.$element[0].checked).toEqual(false);
scope.state = "Worked"; scope.state = "Worked";
scope.$eval(); scope.$digest();
expect(scope.state).toEqual("Worked"); expect(scope.state).toEqual("Worked");
expect(scope.$element[0].checked).toEqual(true); expect(scope.$element[0].checked).toEqual(true);
}); });
@ -186,8 +188,8 @@ describe("widget", function(){
expect(element.hasClass('ng-validation-error')).toBeTruthy(); expect(element.hasClass('ng-validation-error')).toBeTruthy();
expect(element.attr('ng-validation-error')).toEqual('Not a number'); expect(element.attr('ng-validation-error')).toEqual('Not a number');
scope.$set('price', '123'); scope.price = '123';
scope.$eval(); scope.$digest();
expect(element.hasClass('ng-validation-error')).toBeFalsy(); expect(element.hasClass('ng-validation-error')).toBeFalsy();
expect(element.attr('ng-validation-error')).toBeFalsy(); expect(element.attr('ng-validation-error')).toBeFalsy();
@ -202,8 +204,8 @@ describe("widget", function(){
expect(element.hasClass('ng-validation-error')).toBeTruthy(); expect(element.hasClass('ng-validation-error')).toBeTruthy();
expect(element.attr('ng-validation-error')).toEqual('Required'); expect(element.attr('ng-validation-error')).toEqual('Required');
scope.$set('price', '123'); scope.price = '123';
scope.$eval(); scope.$digest();
expect(element.hasClass('ng-validation-error')).toBeFalsy(); expect(element.hasClass('ng-validation-error')).toBeFalsy();
expect(element.attr('ng-validation-error')).toBeFalsy(); expect(element.attr('ng-validation-error')).toBeFalsy();
}); });
@ -215,7 +217,7 @@ describe("widget", function(){
expect(lastValue).toEqual("NOT_CALLED"); expect(lastValue).toEqual("NOT_CALLED");
scope.url = 'http://server'; scope.url = 'http://server';
scope.$eval(); scope.$digest();
expect(lastValue).toEqual("http://server"); expect(lastValue).toEqual("http://server");
delete angularValidator.myValidator; delete angularValidator.myValidator;
@ -240,8 +242,8 @@ describe("widget", function(){
expect(element.hasClass('ng-validation-error')).toBeTruthy(); expect(element.hasClass('ng-validation-error')).toBeTruthy();
expect(element.attr('ng-validation-error')).toEqual('Required'); expect(element.attr('ng-validation-error')).toEqual('Required');
scope.$set('price', 'xxx'); scope.price = 'xxx';
scope.$eval(); scope.$digest();
expect(element.hasClass('ng-validation-error')).toBeFalsy(); expect(element.hasClass('ng-validation-error')).toBeFalsy();
expect(element.attr('ng-validation-error')).toBeFalsy(); expect(element.attr('ng-validation-error')).toBeFalsy();
@ -254,19 +256,19 @@ describe("widget", function(){
it('should allow conditions on ng:required', function() { it('should allow conditions on ng:required', function() {
compile('<input type="text" name="price" ng:required="ineedz"/>', compile('<input type="text" name="price" ng:required="ineedz"/>',
jqLite(document.body)); jqLite(document.body));
scope.$set('ineedz', false); scope.ineedz = false;
scope.$eval(); scope.$digest();
expect(element.hasClass('ng-validation-error')).toBeFalsy(); expect(element.hasClass('ng-validation-error')).toBeFalsy();
expect(element.attr('ng-validation-error')).toBeFalsy(); expect(element.attr('ng-validation-error')).toBeFalsy();
scope.$set('price', 'xxx'); scope.price = 'xxx';
scope.$eval(); scope.$digest();
expect(element.hasClass('ng-validation-error')).toBeFalsy(); expect(element.hasClass('ng-validation-error')).toBeFalsy();
expect(element.attr('ng-validation-error')).toBeFalsy(); expect(element.attr('ng-validation-error')).toBeFalsy();
scope.$set('price', ''); scope.price = '';
scope.$set('ineedz', true); scope.ineedz = true;
scope.$eval(); scope.$digest();
expect(element.hasClass('ng-validation-error')).toBeTruthy(); expect(element.hasClass('ng-validation-error')).toBeTruthy();
expect(element.attr('ng-validation-error')).toEqual('Required'); expect(element.attr('ng-validation-error')).toEqual('Required');
@ -278,31 +280,31 @@ describe("widget", function(){
it("should process ng:required2", function() { it("should process ng:required2", function() {
compile('<textarea name="name">Misko</textarea>'); compile('<textarea name="name">Misko</textarea>');
expect(scope.$get('name')).toEqual("Misko"); expect(scope.name).toEqual("Misko");
scope.$set('name', 'Adam'); scope.name = 'Adam';
scope.$eval(); scope.$digest();
expect(element.val()).toEqual("Adam"); expect(element.val()).toEqual("Adam");
element.val('Shyam'); element.val('Shyam');
browserTrigger(element); browserTrigger(element);
expect(scope.$get('name')).toEqual('Shyam'); expect(scope.name).toEqual('Shyam');
element.val('Kai'); element.val('Kai');
browserTrigger(element); browserTrigger(element);
expect(scope.$get('name')).toEqual('Kai'); expect(scope.name).toEqual('Kai');
}); });
it('should call ng:change on button click', function(){ it('should call ng:change on button click', function(){
compile('<input type="button" value="Click Me" ng:change="clicked = true"/>'); compile('<input type="button" value="Click Me" ng:change="clicked = true"/>');
browserTrigger(element); browserTrigger(element);
expect(scope.$get('clicked')).toEqual(true); expect(scope.clicked).toEqual(true);
}); });
it('should support button alias', function(){ it('should support button alias', function(){
compile('<button ng:change="clicked = true">Click {{"Me"}}.</button>'); compile('<button ng:change="clicked = true">Click {{"Me"}}.</button>');
browserTrigger(element); browserTrigger(element);
expect(scope.$get('clicked')).toEqual(true); expect(scope.clicked).toEqual(true);
expect(scope.$element.text()).toEqual("Click Me."); expect(scope.$element.text()).toEqual("Click Me.");
}); });
@ -319,11 +321,11 @@ describe("widget", function(){
expect(b.name.split('@')[1]).toEqual('chose'); expect(b.name.split('@')[1]).toEqual('chose');
expect(scope.chose).toEqual('B'); expect(scope.chose).toEqual('B');
scope.chose = 'A'; scope.chose = 'A';
scope.$eval(); scope.$digest();
expect(a.checked).toEqual(true); expect(a.checked).toEqual(true);
scope.chose = 'B'; scope.chose = 'B';
scope.$eval(); scope.$digest();
expect(a.checked).toEqual(false); expect(a.checked).toEqual(false);
expect(b.checked).toEqual(true); expect(b.checked).toEqual(true);
expect(scope.clicked).not.toBeDefined(); expect(scope.clicked).not.toBeDefined();
@ -364,12 +366,11 @@ describe("widget", function(){
'</select>'); '</select>');
expect(scope.selection).toEqual('B'); expect(scope.selection).toEqual('B');
scope.selection = 'A'; scope.selection = 'A';
scope.$eval(); scope.$digest();
expect(scope.selection).toEqual('A'); expect(scope.selection).toEqual('A');
expect(element[0].childNodes[0].selected).toEqual(true); expect(element[0].childNodes[0].selected).toEqual(true);
}); });
it('should compile children of a select without a name, but not create a model for it', it('should compile children of a select without a name, but not create a model for it',
function() { function() {
compile('<select>' + compile('<select>' +
@ -379,7 +380,7 @@ describe("widget", function(){
'</select>'); '</select>');
scope.a = 'foo'; scope.a = 'foo';
scope.b = 'bar'; scope.b = 'bar';
scope.$eval(); scope.$flush();
expect(scope.$element.text()).toBe('foobarC'); expect(scope.$element.text()).toBe('foobarC');
}); });
@ -394,9 +395,10 @@ describe("widget", function(){
'</select>'); '</select>');
expect(scope.selection).toEqual(['B']); expect(scope.selection).toEqual(['B']);
scope.selection = ['A']; scope.selection = ['A'];
scope.$eval(); scope.$digest();
expect(element[0].childNodes[0].selected).toEqual(true); expect(element[0].childNodes[0].selected).toEqual(true);
}); });
}); });
it('should ignore text widget which have no name', function(){ it('should ignore text widget which have no name', function(){
@ -412,19 +414,12 @@ describe("widget", function(){
}); });
it('should report error on assignment error', function(){ it('should report error on assignment error', function(){
compile('<input type="text" name="throw \'\'" value="x"/>'); expect(function(){
expect(element.hasClass('ng-exception')).toBeTruthy(); compile('<input type="text" name="throw \'\'" value="x"/>');
expect(scope.$service('$log').error.logs.shift()[0]). }).toThrow("Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at [''].");
toMatchError(/Syntax Error: Token '''' is an unexpected token/); $logMock.error.logs.shift();
}); });
it('should report error on ng:change exception', function(){
compile('<button ng:change="a-2=x">click</button>');
browserTrigger(element);
expect(element.hasClass('ng-exception')).toBeTruthy();
expect(scope.$service('$log').error.logs.shift()[0]).
toMatchError(/Syntax Error: Token '=' implies assignment but \[a-2\] can not be assigned to/);
});
}); });
describe('ng:switch', function(){ describe('ng:switch', function(){
@ -436,43 +431,38 @@ describe("widget", function(){
'</ng:switch>'); '</ng:switch>');
expect(element.html()).toEqual(''); expect(element.html()).toEqual('');
scope.select = 1; scope.select = 1;
scope.$eval(); scope.$apply();
expect(element.text()).toEqual('first:'); expect(element.text()).toEqual('first:');
scope.name="shyam"; scope.name="shyam";
scope.$eval(); scope.$apply();
expect(element.text()).toEqual('first:shyam'); expect(element.text()).toEqual('first:shyam');
scope.select = 2; scope.select = 2;
scope.$eval(); scope.$apply();
expect(element.text()).toEqual('second:shyam'); expect(element.text()).toEqual('second:shyam');
scope.name = 'misko'; scope.name = 'misko';
scope.$eval(); scope.$apply();
expect(element.text()).toEqual('second:misko'); expect(element.text()).toEqual('second:misko');
scope.select = true; scope.select = true;
scope.$eval(); scope.$apply();
expect(element.text()).toEqual('true:misko'); expect(element.text()).toEqual('true:misko');
}); });
it("should compare stringified versions", function(){
var switchWidget = angular.widget('ng:switch');
expect(switchWidget.equals(true, 'true')).toEqual(true);
});
it('should switch on switch-when-default', function(){ it('should switch on switch-when-default', function(){
compile('<ng:switch on="select">' + compile('<ng:switch on="select">' +
'<div ng:switch-when="1">one</div>' + '<div ng:switch-when="1">one</div>' +
'<div ng:switch-default>other</div>' + '<div ng:switch-default>other</div>' +
'</ng:switch>'); '</ng:switch>');
scope.$eval(); scope.$apply();
expect(element.text()).toEqual('other'); expect(element.text()).toEqual('other');
scope.select = 1; scope.select = 1;
scope.$eval(); scope.$apply();
expect(element.text()).toEqual('one'); expect(element.text()).toEqual('one');
}); });
it('should call change on switch', function(){ it('should call change on switch', function(){
var scope = angular.compile('<ng:switch on="url" change="name=\'works\'"><div ng:switch-when="a">{{name}}</div></ng:switch>')(); var scope = angular.compile('<ng:switch on="url" change="name=\'works\'"><div ng:switch-when="a">{{name}}</div></ng:switch>')();
scope.url = 'a'; scope.url = 'a';
scope.$eval(); scope.$apply();
expect(scope.name).toEqual(undefined); expect(scope.name).toEqual(undefined);
expect(scope.$element.text()).toEqual('works'); expect(scope.$element.text()).toEqual('works');
dealoc(scope); dealoc(scope);
@ -483,11 +473,11 @@ describe("widget", function(){
it('should include on external file', function() { it('should include on external file', function() {
var element = jqLite('<ng:include src="url" scope="childScope"></ng:include>'); var element = jqLite('<ng:include src="url" scope="childScope"></ng:include>');
var scope = angular.compile(element)(); var scope = angular.compile(element)();
scope.childScope = createScope(); scope.childScope = scope.$new();
scope.childScope.name = 'misko'; scope.childScope.name = 'misko';
scope.url = 'myUrl'; scope.url = 'myUrl';
scope.$service('$xhr.cache').data.myUrl = {value:'{{name}}'}; scope.$service('$xhr.cache').data.myUrl = {value:'{{name}}'};
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('misko'); expect(element.text()).toEqual('misko');
dealoc(scope); dealoc(scope);
}); });
@ -495,16 +485,16 @@ describe("widget", function(){
it('should remove previously included text if a falsy value is bound to src', function() { it('should remove previously included text if a falsy value is bound to src', function() {
var element = jqLite('<ng:include src="url" scope="childScope"></ng:include>'); var element = jqLite('<ng:include src="url" scope="childScope"></ng:include>');
var scope = angular.compile(element)(); var scope = angular.compile(element)();
scope.childScope = createScope(); scope.childScope = scope.$new();
scope.childScope.name = 'igor'; scope.childScope.name = 'igor';
scope.url = 'myUrl'; scope.url = 'myUrl';
scope.$service('$xhr.cache').data.myUrl = {value:'{{name}}'}; scope.$service('$xhr.cache').data.myUrl = {value:'{{name}}'};
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('igor'); expect(element.text()).toEqual('igor');
scope.url = undefined; scope.url = undefined;
scope.$eval(); scope.$flush();
expect(element.text()).toEqual(''); expect(element.text()).toEqual('');
dealoc(scope); dealoc(scope);
@ -515,11 +505,14 @@ describe("widget", function(){
var scope = angular.compile(element)(); var scope = angular.compile(element)();
scope.url = 'myUrl'; scope.url = 'myUrl';
scope.$service('$xhr.cache').data.myUrl = {value:'{{c=c+1}}'}; scope.$service('$xhr.cache').data.myUrl = {value:'{{c=c+1}}'};
scope.$eval(); scope.$flush();
// TODO(misko): because we are using scope==this, the eval gets registered
// during the flush phase and hence does not get called.
// I don't think passing 'this' makes sense. Does having scope on ng:include makes sense?
// should we make scope="this" ilegal?
scope.$flush();
// this one should really be just '1', but due to lack of real events things are not working expect(element.text()).toEqual('1');
// properly. see discussion at: http://is.gd/ighKk
expect(element.text()).toEqual('4');
dealoc(element); dealoc(element);
}); });
@ -531,11 +524,28 @@ describe("widget", function(){
scope.url = 'myUrl'; scope.url = 'myUrl';
scope.$service('$xhr.cache').data.myUrl = {value:'my partial'}; scope.$service('$xhr.cache').data.myUrl = {value:'my partial'};
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('my partial'); expect(element.text()).toEqual('my partial');
expect(scope.loaded).toBe(true); expect(scope.loaded).toBe(true);
dealoc(element); dealoc(element);
}); });
it('should destroy old scope', function(){
var element = jqLite('<ng:include src="url"></ng:include>');
var scope = angular.compile(element)();
expect(scope.$$childHead).toBeFalsy();
scope.url = 'myUrl';
scope.$service('$xhr.cache').data.myUrl = {value:'my partial'};
scope.$flush();
expect(scope.$$childHead).toBeTruthy();
scope.url = null;
scope.$flush();
expect(scope.$$childHead).toBeFalsy();
dealoc(element);
});
}); });
describe('a', function() { describe('a', function() {
@ -624,7 +634,7 @@ describe("widget", function(){
createSingleSelect(); createSingleSelect();
scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
var options = select.find('option'); var options = select.find('option');
expect(options.length).toEqual(3); expect(options.length).toEqual(3);
expect(sortedHtml(options[0])).toEqual('<option value="0">A</option>'); expect(sortedHtml(options[0])).toEqual('<option value="0">A</option>');
@ -639,7 +649,7 @@ describe("widget", function(){
}); });
scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'};
scope.selected = scope.object.red; scope.selected = scope.object.red;
scope.$eval(); scope.$flush();
var options = select.find('option'); var options = select.find('option');
expect(options.length).toEqual(3); expect(options.length).toEqual(3);
expect(sortedHtml(options[0])).toEqual('<option value="blue">blue</option>'); expect(sortedHtml(options[0])).toEqual('<option value="blue">blue</option>');
@ -648,7 +658,7 @@ describe("widget", function(){
expect(options[2].selected).toEqual(true); expect(options[2].selected).toEqual(true);
scope.object.azur = '8888FF'; scope.object.azur = '8888FF';
scope.$eval(); scope.$flush();
options = select.find('option'); options = select.find('option');
expect(options[3].selected).toEqual(true); expect(options[3].selected).toEqual(true);
}); });
@ -656,18 +666,18 @@ describe("widget", function(){
it('should grow list', function(){ it('should grow list', function(){
createSingleSelect(); createSingleSelect();
scope.values = []; scope.values = [];
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(1); // because we add special empty option expect(select.find('option').length).toEqual(1); // because we add special empty option
expect(sortedHtml(select.find('option')[0])).toEqual('<option value="?"></option>'); expect(sortedHtml(select.find('option')[0])).toEqual('<option value="?"></option>');
scope.values.push({name:'A'}); scope.values.push({name:'A'});
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(1); expect(select.find('option').length).toEqual(1);
expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>');
scope.values.push({name:'B'}); scope.values.push({name:'B'});
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(2); expect(select.find('option').length).toEqual(2);
expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>');
expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>'); expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>');
@ -677,23 +687,23 @@ describe("widget", function(){
createSingleSelect(); createSingleSelect();
scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(3); expect(select.find('option').length).toEqual(3);
scope.values.pop(); scope.values.pop();
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(2); expect(select.find('option').length).toEqual(2);
expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>');
expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>'); expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>');
scope.values.pop(); scope.values.pop();
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(1); expect(select.find('option').length).toEqual(1);
expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>');
scope.values.pop(); scope.values.pop();
scope.selected = null; scope.selected = null;
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(1); // we add back the special empty option expect(select.find('option').length).toEqual(1); // we add back the special empty option
}); });
@ -701,17 +711,17 @@ describe("widget", function(){
createSingleSelect(); createSingleSelect();
scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(3); expect(select.find('option').length).toEqual(3);
scope.values = [{name:'1'}, {name:'2'}]; scope.values = [{name:'1'}, {name:'2'}];
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(2); expect(select.find('option').length).toEqual(2);
scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(3); expect(select.find('option').length).toEqual(3);
}); });
@ -719,11 +729,11 @@ describe("widget", function(){
createSingleSelect(); createSingleSelect();
scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
scope.values = [{name:'B'}, {name:'C'}, {name:'D'}]; scope.values = [{name:'B'}, {name:'C'}, {name:'D'}];
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
var options = select.find('option'); var options = select.find('option');
expect(options.length).toEqual(3); expect(options.length).toEqual(3);
expect(sortedHtml(options[0])).toEqual('<option value="0">B</option>'); expect(sortedHtml(options[0])).toEqual('<option value="0">B</option>');
@ -734,19 +744,19 @@ describe("widget", function(){
it('should preserve existing options', function(){ it('should preserve existing options', function(){
createSingleSelect(true); createSingleSelect(true);
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(1); expect(select.find('option').length).toEqual(1);
scope.values = [{name:'A'}]; scope.values = [{name:'A'}];
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(2); expect(select.find('option').length).toEqual(2);
expect(jqLite(select.find('option')[0]).text()).toEqual('blank'); expect(jqLite(select.find('option')[0]).text()).toEqual('blank');
expect(jqLite(select.find('option')[1]).text()).toEqual('A'); expect(jqLite(select.find('option')[1]).text()).toEqual('A');
scope.values = []; scope.values = [];
scope.selected = null; scope.selected = null;
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(1); expect(select.find('option').length).toEqual(1);
expect(jqLite(select.find('option')[0]).text()).toEqual('blank'); expect(jqLite(select.find('option')[0]).text()).toEqual('blank');
}); });
@ -756,11 +766,11 @@ describe("widget", function(){
createSingleSelect(); createSingleSelect();
scope.values = [{name:'A'}, {name:'B'}]; scope.values = [{name:'A'}, {name:'B'}];
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('0'); expect(select.val()).toEqual('0');
scope.selected = scope.values[1]; scope.selected = scope.values[1];
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('1'); expect(select.val()).toEqual('1');
}); });
@ -775,7 +785,7 @@ describe("widget", function(){
{name:'D', group:'first'}, {name:'D', group:'first'},
{name:'E', group:'second'}]; {name:'E', group:'second'}];
scope.selected = scope.values[3]; scope.selected = scope.values[3];
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('3'); expect(select.val()).toEqual('3');
var first = jqLite(select.find('optgroup')[0]); var first = jqLite(select.find('optgroup')[0]);
@ -793,7 +803,7 @@ describe("widget", function(){
expect(e.text()).toEqual('E'); expect(e.text()).toEqual('E');
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('0'); expect(select.val()).toEqual('0');
}); });
@ -801,11 +811,11 @@ describe("widget", function(){
createSelect({'name':'selected', 'ng:options':'item.id as item.name for item in values'}); createSelect({'name':'selected', 'ng:options':'item.id as item.name for item in values'});
scope.values = [{id:10, name:'A'}, {id:20, name:'B'}]; scope.values = [{id:10, name:'A'}, {id:20, name:'B'}];
scope.selected = scope.values[0].id; scope.selected = scope.values[0].id;
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('0'); expect(select.val()).toEqual('0');
scope.selected = scope.values[1].id; scope.selected = scope.values[1].id;
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('1'); expect(select.val()).toEqual('1');
}); });
@ -816,11 +826,11 @@ describe("widget", function(){
}); });
scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'};
scope.selected = 'green'; scope.selected = 'green';
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('green'); expect(select.val()).toEqual('green');
scope.selected = 'blue'; scope.selected = 'blue';
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('blue'); expect(select.val()).toEqual('blue');
}); });
@ -831,11 +841,11 @@ describe("widget", function(){
}); });
scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'};
scope.selected = '00FF00'; scope.selected = '00FF00';
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('green'); expect(select.val()).toEqual('green');
scope.selected = '0000FF'; scope.selected = '0000FF';
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('blue'); expect(select.val()).toEqual('blue');
}); });
@ -843,13 +853,13 @@ describe("widget", function(){
createSingleSelect(); createSingleSelect();
scope.values = [{name:'A'}]; scope.values = [{name:'A'}];
scope.selected = null; scope.selected = null;
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(2); expect(select.find('option').length).toEqual(2);
expect(select.val()).toEqual(''); expect(select.val()).toEqual('');
expect(jqLite(select.find('option')[0]).val()).toEqual(''); expect(jqLite(select.find('option')[0]).val()).toEqual('');
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('0'); expect(select.val()).toEqual('0');
expect(select.find('option').length).toEqual(1); expect(select.find('option').length).toEqual(1);
}); });
@ -858,13 +868,13 @@ describe("widget", function(){
createSingleSelect(true); createSingleSelect(true);
scope.values = [{name:'A'}]; scope.values = [{name:'A'}];
scope.selected = null; scope.selected = null;
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(2); expect(select.find('option').length).toEqual(2);
expect(select.val()).toEqual(''); expect(select.val()).toEqual('');
expect(jqLite(select.find('option')[0]).val()).toEqual(''); expect(jqLite(select.find('option')[0]).val()).toEqual('');
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('0'); expect(select.val()).toEqual('0');
expect(select.find('option').length).toEqual(2); expect(select.find('option').length).toEqual(2);
}); });
@ -873,13 +883,13 @@ describe("widget", function(){
createSingleSelect(); createSingleSelect();
scope.values = [{name:'A'}]; scope.values = [{name:'A'}];
scope.selected = {}; scope.selected = {};
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(2); expect(select.find('option').length).toEqual(2);
expect(select.val()).toEqual('?'); expect(select.val()).toEqual('?');
expect(jqLite(select.find('option')[0]).val()).toEqual('?'); expect(jqLite(select.find('option')[0]).val()).toEqual('?');
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('0'); expect(select.val()).toEqual('0');
expect(select.find('option').length).toEqual(1); expect(select.find('option').length).toEqual(1);
}); });
@ -890,7 +900,7 @@ describe("widget", function(){
createSingleSelect(); createSingleSelect();
scope.values = [{name:'A'}, {name:'B'}]; scope.values = [{name:'A'}, {name:'B'}];
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('0'); expect(select.val()).toEqual('0');
select.val('1'); select.val('1');
@ -907,7 +917,7 @@ describe("widget", function(){
scope.values = [{name:'A'}, {name:'B'}]; scope.values = [{name:'A'}, {name:'B'}];
scope.selected = scope.values[0]; scope.selected = scope.values[0];
scope.count = 0; scope.count = 0;
scope.$eval(); scope.$flush();
expect(scope.count).toEqual(0); expect(scope.count).toEqual(0);
select.val('1'); select.val('1');
@ -924,7 +934,7 @@ describe("widget", function(){
createSelect({name:'selected', 'ng:options':'item.id as item.name for item in values'}); createSelect({name:'selected', 'ng:options':'item.id as item.name for item in values'});
scope.values = [{id:10, name:'A'}, {id:20, name:'B'}]; scope.values = [{id:10, name:'A'}, {id:20, name:'B'}];
scope.selected = scope.values[0].id; scope.selected = scope.values[0].id;
scope.$eval(); scope.$flush();
expect(select.val()).toEqual('0'); expect(select.val()).toEqual('0');
select.val('1'); select.val('1');
@ -937,7 +947,7 @@ describe("widget", function(){
scope.values = [{name:'A'}, {name:'B'}]; scope.values = [{name:'A'}, {name:'B'}];
scope.selected = scope.values[0]; scope.selected = scope.values[0];
select.val('0'); select.val('0');
scope.$eval(); scope.$flush();
select.val(''); select.val('');
browserTrigger(select, 'change'); browserTrigger(select, 'change');
@ -951,19 +961,19 @@ describe("widget", function(){
scope.values = [{name:'A'}, {name:'B'}]; scope.values = [{name:'A'}, {name:'B'}];
scope.selected = []; scope.selected = [];
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(2); expect(select.find('option').length).toEqual(2);
expect(jqLite(select.find('option')[0]).attr('selected')).toEqual(false); expect(jqLite(select.find('option')[0]).attr('selected')).toEqual(false);
expect(jqLite(select.find('option')[1]).attr('selected')).toEqual(false); expect(jqLite(select.find('option')[1]).attr('selected')).toEqual(false);
scope.selected.push(scope.values[1]); scope.selected.push(scope.values[1]);
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(2); expect(select.find('option').length).toEqual(2);
expect(select.find('option')[0].selected).toEqual(false); expect(select.find('option')[0].selected).toEqual(false);
expect(select.find('option')[1].selected).toEqual(true); expect(select.find('option')[1].selected).toEqual(true);
scope.selected.push(scope.values[0]); scope.selected.push(scope.values[0]);
scope.$eval(); scope.$flush();
expect(select.find('option').length).toEqual(2); expect(select.find('option').length).toEqual(2);
expect(select.find('option')[0].selected).toEqual(true); expect(select.find('option')[0].selected).toEqual(true);
expect(select.find('option')[1].selected).toEqual(true); expect(select.find('option')[1].selected).toEqual(true);
@ -974,7 +984,7 @@ describe("widget", function(){
scope.values = [{name:'A'}, {name:'B'}]; scope.values = [{name:'A'}, {name:'B'}];
scope.selected = []; scope.selected = [];
scope.$eval(); scope.$flush();
select.find('option')[0].selected = true; select.find('option')[0].selected = true;
browserTrigger(select, 'change'); browserTrigger(select, 'change');
@ -991,24 +1001,30 @@ describe("widget", function(){
var scope = compile('<ul><li ng:repeat="item in items" ng:init="suffix = \';\'" ng:bind="item + suffix"></li></ul>'); var scope = compile('<ul><li ng:repeat="item in items" ng:init="suffix = \';\'" ng:bind="item + suffix"></li></ul>');
Array.prototype.extraProperty = "should be ignored"; Array.prototype.extraProperty = "should be ignored";
// INIT
scope.items = ['misko', 'shyam']; scope.items = ['misko', 'shyam'];
scope.$eval(); scope.$flush();
expect(element.find('li').length).toEqual(2);
expect(element.text()).toEqual('misko;shyam;'); expect(element.text()).toEqual('misko;shyam;');
delete Array.prototype.extraProperty; delete Array.prototype.extraProperty;
// GROW
scope.items = ['adam', 'kai', 'brad']; scope.items = ['adam', 'kai', 'brad'];
scope.$eval(); scope.$flush();
expect(element.find('li').length).toEqual(3);
expect(element.text()).toEqual('adam;kai;brad;'); expect(element.text()).toEqual('adam;kai;brad;');
// SHRINK
scope.items = ['brad']; scope.items = ['brad'];
scope.$eval(); scope.$flush();
expect(element.find('li').length).toEqual(1);
expect(element.text()).toEqual('brad;'); expect(element.text()).toEqual('brad;');
}); });
it('should ng:repeat over object', function(){ it('should ng:repeat over object', function(){
var scope = compile('<ul><li ng:repeat="(key, value) in items" ng:bind="key + \':\' + value + \';\' "></li></ul>'); var scope = compile('<ul><li ng:repeat="(key, value) in items" ng:bind="key + \':\' + value + \';\' "></li></ul>');
scope.$set('items', {misko:'swe', shyam:'set'}); scope.items = {misko:'swe', shyam:'set'};
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('misko:swe;shyam:set;'); expect(element.text()).toEqual('misko:swe;shyam:set;');
}); });
@ -1020,28 +1036,23 @@ describe("widget", function(){
var scope = compile('<ul><li ng:repeat="(key, value) in items" ng:bind="key + \':\' + value + \';\' "></li></ul>'); var scope = compile('<ul><li ng:repeat="(key, value) in items" ng:bind="key + \':\' + value + \';\' "></li></ul>');
scope.items = new Class(); scope.items = new Class();
scope.items.name = 'value'; scope.items.name = 'value';
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('name:value;'); expect(element.text()).toEqual('name:value;');
}); });
it('should error on wrong parsing of ng:repeat', function(){ it('should error on wrong parsing of ng:repeat', function(){
var scope = compile('<ul><li ng:repeat="i dont parse"></li></ul>'); expect(function(){
compile('<ul><li ng:repeat="i dont parse"></li></ul>');
}).toThrow("Expected ng:repeat in form of '_item_ in _collection_' but got 'i dont parse'.");
expect(scope.$service('$log').error.logs.shift()[0]). $logMock.error.logs.shift();
toEqualError("Expected ng:repeat in form of '_item_ in _collection_' but got 'i dont parse'.");
expect(scope.$element.attr('ng-exception')).
toMatch(/Expected ng:repeat in form of '_item_ in _collection_' but got 'i dont parse'/);
expect(scope.$element).toHaveClass('ng-exception');
dealoc(scope);
}); });
it('should expose iterator offset as $index when iterating over arrays', function() { it('should expose iterator offset as $index when iterating over arrays', function() {
var scope = compile('<ul><li ng:repeat="item in items" ' + var scope = compile('<ul><li ng:repeat="item in items" ' +
'ng:bind="item + $index + \'|\'"></li></ul>'); 'ng:bind="item + $index + \'|\'"></li></ul>');
scope.items = ['misko', 'shyam', 'frodo']; scope.items = ['misko', 'shyam', 'frodo'];
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('misko0|shyam1|frodo2|'); expect(element.text()).toEqual('misko0|shyam1|frodo2|');
}); });
@ -1049,7 +1060,7 @@ describe("widget", function(){
var scope = compile('<ul><li ng:repeat="(key, val) in items" ' + var scope = compile('<ul><li ng:repeat="(key, val) in items" ' +
'ng:bind="key + \':\' + val + $index + \'|\'"></li></ul>'); 'ng:bind="key + \':\' + val + $index + \'|\'"></li></ul>');
scope.items = {'misko':'m', 'shyam':'s', 'frodo':'f'}; scope.items = {'misko':'m', 'shyam':'s', 'frodo':'f'};
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('misko:m0|shyam:s1|frodo:f2|'); expect(element.text()).toEqual('misko:m0|shyam:s1|frodo:f2|');
}); });
@ -1057,16 +1068,16 @@ describe("widget", function(){
var scope = compile('<ul><li ng:repeat="item in items" ' + var scope = compile('<ul><li ng:repeat="item in items" ' +
'ng:bind="item + \':\' + $position + \'|\'"></li></ul>'); 'ng:bind="item + \':\' + $position + \'|\'"></li></ul>');
scope.items = ['misko', 'shyam', 'doug']; scope.items = ['misko', 'shyam', 'doug'];
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('misko:first|shyam:middle|doug:last|'); expect(element.text()).toEqual('misko:first|shyam:middle|doug:last|');
scope.items.push('frodo'); scope.items.push('frodo');
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('misko:first|shyam:middle|doug:middle|frodo:last|'); expect(element.text()).toEqual('misko:first|shyam:middle|doug:middle|frodo:last|');
scope.items.pop(); scope.items.pop();
scope.items.pop(); scope.items.pop();
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('misko:first|shyam:last|'); expect(element.text()).toEqual('misko:first|shyam:last|');
}); });
@ -1074,12 +1085,12 @@ describe("widget", function(){
var scope = compile('<ul><li ng:repeat="(key, val) in items" ' + var scope = compile('<ul><li ng:repeat="(key, val) in items" ' +
'ng:bind="key + \':\' + val + \':\' + $position + \'|\'"></li></ul>'); 'ng:bind="key + \':\' + val + \':\' + $position + \'|\'"></li></ul>');
scope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f'}; scope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f'};
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('misko:m:first|shyam:s:middle|doug:d:middle|frodo:f:last|'); expect(element.text()).toEqual('misko:m:first|shyam:s:middle|doug:d:middle|frodo:f:last|');
delete scope.items.doug; delete scope.items.doug;
delete scope.items.frodo; delete scope.items.frodo;
scope.$eval(); scope.$flush();
expect(element.text()).toEqual('misko:m:first|shyam:s:last|'); expect(element.text()).toEqual('misko:m:first|shyam:s:last|');
}); });
}); });
@ -1089,8 +1100,8 @@ describe("widget", function(){
it('should prevent compilation of the owning element and its children', function(){ it('should prevent compilation of the owning element and its children', function(){
var scope = compile('<div ng:non-bindable><span ng:bind="name"></span></div>'); var scope = compile('<div ng:non-bindable><span ng:bind="name"></span></div>');
scope.$set('name', 'misko'); scope.name = 'misko';
scope.$eval(); scope.$digest();
expect(element.text()).toEqual(''); expect(element.text()).toEqual('');
}); });
}); });
@ -1113,7 +1124,7 @@ describe("widget", function(){
it('should do nothing when no routes are defined', function() { it('should do nothing when no routes are defined', function() {
$location.updateHash('/unknown'); $location.updateHash('/unknown');
rootScope.$eval(); rootScope.$digest();
expect(rootScope.$element.text()).toEqual(''); expect(rootScope.$element.text()).toEqual('');
}); });
@ -1126,13 +1137,15 @@ describe("widget", function(){
$location.updateHash('/foo'); $location.updateHash('/foo');
$browser.xhr.expectGET('myUrl1').respond('<div>{{1+3}}</div>'); $browser.xhr.expectGET('myUrl1').respond('<div>{{1+3}}</div>');
rootScope.$eval(); rootScope.$digest();
rootScope.$flush();
$browser.xhr.flush(); $browser.xhr.flush();
expect(rootScope.$element.text()).toEqual('4'); expect(rootScope.$element.text()).toEqual('4');
$location.updateHash('/bar'); $location.updateHash('/bar');
$browser.xhr.expectGET('myUrl2').respond('angular is da best'); $browser.xhr.expectGET('myUrl2').respond('angular is da best');
rootScope.$eval(); rootScope.$digest();
rootScope.$flush();
$browser.xhr.flush(); $browser.xhr.flush();
expect(rootScope.$element.text()).toEqual('angular is da best'); expect(rootScope.$element.text()).toEqual('angular is da best');
}); });
@ -1142,12 +1155,14 @@ describe("widget", function(){
$location.updateHash('/foo'); $location.updateHash('/foo');
$browser.xhr.expectGET('myUrl1').respond('<div>{{1+3}}</div>'); $browser.xhr.expectGET('myUrl1').respond('<div>{{1+3}}</div>');
rootScope.$eval(); rootScope.$digest();
rootScope.$flush();
$browser.xhr.flush(); $browser.xhr.flush();
expect(rootScope.$element.text()).toEqual('4'); expect(rootScope.$element.text()).toEqual('4');
$location.updateHash('/unknown'); $location.updateHash('/unknown');
rootScope.$eval(); rootScope.$digest();
rootScope.$flush();
expect(rootScope.$element.text()).toEqual(''); expect(rootScope.$element.text()).toEqual('');
}); });
@ -1157,16 +1172,20 @@ describe("widget", function(){
$location.updateHash('/foo'); $location.updateHash('/foo');
$browser.xhr.expectGET('myUrl1').respond('<div>{{parentVar}}</div>'); $browser.xhr.expectGET('myUrl1').respond('<div>{{parentVar}}</div>');
rootScope.$eval(); rootScope.$digest();
rootScope.$flush();
$browser.xhr.flush(); $browser.xhr.flush();
expect(rootScope.$element.text()).toEqual('parent'); expect(rootScope.$element.text()).toEqual('parent');
rootScope.parentVar = 'new parent'; rootScope.parentVar = 'new parent';
rootScope.$eval(); rootScope.$digest();
rootScope.$flush();
expect(rootScope.$element.text()).toEqual('new parent'); expect(rootScope.$element.text()).toEqual('new parent');
}); });
it('should be possible to nest ng:view in ng:include', function() { it('should be possible to nest ng:view in ng:include', function() {
dealoc(rootScope); // we are about to override it.
var myApp = angular.scope(); var myApp = angular.scope();
var $browser = myApp.$service('$browser'); var $browser = myApp.$service('$browser');
$browser.xhr.expectGET('includePartial.html').respond('view: <ng:view></ng:view>'); $browser.xhr.expectGET('includePartial.html').respond('view: <ng:view></ng:view>');
@ -1175,13 +1194,14 @@ describe("widget", function(){
var $route = myApp.$service('$route'); var $route = myApp.$service('$route');
$route.when('/foo', {controller: angular.noop, template: 'viewPartial.html'}); $route.when('/foo', {controller: angular.noop, template: 'viewPartial.html'});
dealoc(rootScope); // we are about to override it.
rootScope = angular.compile( rootScope = angular.compile(
'<div>' + '<div>' +
'include: <ng:include src="\'includePartial.html\'">' + 'include: <ng:include src="\'includePartial.html\'">' +
'</ng:include></div>')(myApp); '</ng:include></div>')(myApp);
rootScope.$apply();
$browser.xhr.expectGET('viewPartial.html').respond('content'); $browser.xhr.expectGET('viewPartial.html').respond('content');
rootScope.$flush();
$browser.xhr.flush(); $browser.xhr.flush();
expect(rootScope.$element.text()).toEqual('include: view: content'); expect(rootScope.$element.text()).toEqual('include: view: content');
@ -1211,21 +1231,21 @@ describe("widget", function(){
respond('<div ng:init="log.push(\'init\')">' + respond('<div ng:init="log.push(\'init\')">' +
'<div ng:controller="ChildCtrl"></div>' + '<div ng:controller="ChildCtrl"></div>' +
'</div>'); '</div>');
rootScope.$eval(); rootScope.$apply();
$browser.xhr.flush(); $browser.xhr.flush();
expect(rootScope.log).toEqual(['parent', 'init', 'child']); expect(rootScope.log).toEqual(['parent', 'child', 'init']);
$location.updateHash(''); $location.updateHash('');
rootScope.$eval(); rootScope.$apply();
expect(rootScope.log).toEqual(['parent', 'init', 'child']); expect(rootScope.log).toEqual(['parent', 'child', 'init']);
rootScope.log = []; rootScope.log = [];
$location.updateHash('/foo'); $location.updateHash('/foo');
rootScope.$eval(); rootScope.$apply();
$browser.defer.flush(); $browser.defer.flush();
expect(rootScope.log).toEqual(['parent', 'init', 'child']); expect(rootScope.log).toEqual(['parent', 'child', 'init']);
}); });
}); });
}); });