From 8f0dcbab804180828d6859b1340c86cf161209fb Mon Sep 17 00:00:00 2001
From: Misko Hevery
Date: Wed, 23 Mar 2011 09:33:29 -0700
Subject: 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
---
test/BinderSpec.js | 274 ++++++++++++++++++++++++++---------------------------
1 file changed, 134 insertions(+), 140 deletions(-)
(limited to 'test/BinderSpec.js')
diff --git a/test/BinderSpec.js b/test/BinderSpec.js
index 241bb98d..a84fe68a 100644
--- a/test/BinderSpec.js
+++ b/test/BinderSpec.js
@@ -1,11 +1,10 @@
'use strict';
describe('Binder', function(){
-
beforeEach(function(){
var self = this;
- this.compile = function(html, parent) {
+ this.compile = function(html, parent, logErrors) {
if (self.element) dealoc(self.element);
var element;
if (parent) {
@@ -15,7 +14,8 @@ describe('Binder', function(){
element = jqLite(html);
}
self.element = element;
- return angular.compile(element)();
+ return angular.compile(element)(angular.scope(null,
+ logErrors ? {'$exceptionHandler': $exceptionHandlerMockFactory()} : null));
};
this.compileToHtml = function (content) {
return sortedHtml(this.compile(content).$element);
@@ -31,20 +31,20 @@ describe('Binder', function(){
it('text-field should default to value attribute', function(){
var scope = this.compile('');
- scope.$eval();
+ scope.$apply();
assertEquals('abc', scope.model.price);
});
it('ChangingTextareaUpdatesModel', function(){
var scope = this.compile('');
- scope.$eval();
+ scope.$apply();
assertEquals(scope.model.note, 'abc');
});
it('ChangingRadioUpdatesModel', function(){
var scope = this.compile('
');
- assertEquals(scope.$get('a'), 123);
+ assertEquals(scope.a, 123);
});
it('ExecuteInitializationStatements', function(){
var scope = this.compile('
');
- assertEquals(scope.$get('a'), 123);
- assertEquals(scope.$get('b'), 345);
+ assertEquals(scope.a, 123);
+ assertEquals(scope.b, 345);
});
it('ApplyTextBindings', function(){
var scope = this.compile('
x
');
- scope.$set('model', {a:123});
- scope.$eval();
+ scope.model = {a:123};
+ scope.$apply();
assertEquals('123', scope.$element.text());
});
@@ -145,7 +146,7 @@ describe('Binder', function(){
it('AttributesAreEvaluated', function(){
var scope = this.compile('
');
scope.$eval('a=1;b=2');
- scope.$eval();
+ scope.$apply();
var a = scope.$element;
assertEquals(a.attr('a'), 'a');
assertEquals(a.attr('b'), 'a+b=3');
@@ -154,9 +155,10 @@ describe('Binder', function(){
it('InputTypeButtonActionExecutesInScope', function(){
var savedCalled = false;
var scope = this.compile('
');
- scope.$set("person.save", function(){
+ scope.person = {};
+ scope.person.save = function(){
savedCalled = true;
- });
+ };
browserTrigger(scope.$element, 'click');
assertTrue(savedCalled);
});
@@ -164,9 +166,9 @@ describe('Binder', function(){
it('InputTypeButtonActionExecutesInScope2', function(){
var log = "";
var scope = this.compile('
');
- scope.$set("action", function(){
+ scope.action = function(){
log += 'click;';
- });
+ };
expect(log).toEqual('');
browserTrigger(scope.$element, 'click');
expect(log).toEqual('click;');
@@ -175,9 +177,10 @@ describe('Binder', function(){
it('ButtonElementActionExecutesInScope', function(){
var savedCalled = false;
var scope = this.compile('
');
- scope.$set("person.save", function(){
+ scope.person = {};
+ scope.person.save = function(){
savedCalled = true;
- });
+ };
browserTrigger(scope.$element, 'click');
assertTrue(savedCalled);
});
@@ -186,9 +189,9 @@ describe('Binder', function(){
var scope = this.compile('
');
var form = scope.$element;
var items = [{a:"A"}, {a:"B"}];
- scope.$set('model', {items:items});
+ scope.model = {items:items};
- scope.$eval();
+ scope.$apply();
assertEquals('
' +
'<#comment>#comment>' +
'- A
' +
@@ -196,7 +199,7 @@ describe('Binder', function(){
'
', sortedHtml(form));
items.unshift({a:'C'});
- scope.$eval();
+ scope.$apply();
assertEquals('
' +
'<#comment>#comment>' +
'- C
' +
@@ -205,7 +208,7 @@ describe('Binder', function(){
'
', sortedHtml(form));
items.shift();
- scope.$eval();
+ scope.$apply();
assertEquals('
' +
'<#comment>#comment>' +
'- A
' +
@@ -214,13 +217,13 @@ describe('Binder', function(){
items.shift();
items.shift();
- scope.$eval();
+ scope.$apply();
});
it('RepeaterContentDoesNotBind', function(){
var scope = this.compile('');
- scope.$set('model', {items:[{a:"A"}]});
- scope.$eval();
+ scope.model = {items:[{a:"A"}]};
+ scope.$apply();
assertEquals('' +
'<#comment>#comment>' +
'- A
' +
@@ -234,59 +237,62 @@ describe('Binder', function(){
it('RepeaterAdd', function(){
var scope = this.compile('');
- scope.$set('items', [{x:'a'}, {x:'b'}]);
- scope.$eval();
+ scope.items = [{x:'a'}, {x:'b'}];
+ scope.$apply();
var first = childNode(scope.$element, 1);
var second = childNode(scope.$element, 2);
- assertEquals('a', first.val());
- assertEquals('b', second.val());
+ expect(first.val()).toEqual('a');
+ expect(second.val()).toEqual('b');
+ return
first.val('ABC');
browserTrigger(first, 'keydown');
scope.$service('$browser').defer.flush();
- assertEquals(scope.items[0].x, 'ABC');
+ expect(scope.items[0].x).toEqual('ABC');
});
it('ItShouldRemoveExtraChildrenWhenIteratingOverHash', function(){
var scope = this.compile('');
var items = {};
- scope.$set("items", items);
+ scope.items = items;
- scope.$eval();
+ scope.$apply();
expect(scope.$element[0].childNodes.length - 1).toEqual(0);
items.name = "misko";
- scope.$eval();
+ scope.$apply();
expect(scope.$element[0].childNodes.length - 1).toEqual(1);
delete items.name;
- scope.$eval();
+ scope.$apply();
expect(scope.$element[0].childNodes.length - 1).toEqual(0);
});
it('IfTextBindingThrowsErrorDecorateTheSpan', function(){
- var scope = this.compile('{{error.throw()}}
');
+ var scope = this.compile('{{error.throw()}}
', null, true);
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.$eval();
+ scope.error = {
+ 'throw': function(){throw "ErrorMsg1";}
+ };
+ scope.$apply();
var span = childNode(doc, 0);
assertTrue(span.hasClass('ng-exception'));
assertTrue(!!span.text().match(/ErrorMsg1/));
assertTrue(!!span.attr('ng-exception').match(/ErrorMsg1/));
assertEquals(['ErrorMsg1'], errorLogs.shift());
- scope.$set('error.throw', function(){throw "MyError";});
- scope.$eval();
+ scope.error['throw'] = function(){throw "MyError";};
+ scope.$apply();
span = childNode(doc, 0);
assertTrue(span.hasClass('ng-exception'));
assertTrue(span.text(), span.text().match('MyError') !== null);
assertEquals('MyError', span.attr('ng-exception'));
assertEquals(['MyError'], errorLogs.shift());
- scope.$set('error.throw', function(){return "ok";});
- scope.$eval();
+ scope.error['throw'] = function(){return "ok";};
+ scope.$apply();
assertFalse(span.hasClass('ng-exception'));
assertEquals('ok', span.text());
assertEquals(null, span.attr('ng-exception'));
@@ -294,23 +300,21 @@ describe('Binder', function(){
});
it('IfAttrBindingThrowsErrorDecorateTheAttribute', function(){
- var scope = this.compile('');
+ var scope = this.compile('', null, true);
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.$eval();
- assertTrue('ng-exception', doc.hasClass('ng-exception'));
- assertEquals('"ErrorMsg"', doc.attr('ng-exception'));
- assertEquals('before "ErrorMsg" after', doc.attr('attr'));
- assertEquals(['ErrorMsg'], errorLogs.shift());
-
- scope.$set('error.throw', function(){ return 'X';});
- scope.$eval();
- assertFalse('!ng-exception', doc.hasClass('ng-exception'));
- assertEquals('before X after', doc.attr('attr'));
- assertEquals(null, doc.attr('ng-exception'));
- assertEquals(0, errorLogs.length);
+ scope.error = {
+ 'throw': function(){throw new Error("ErrorMsg" + (++count));}
+ };
+ scope.$apply();
+ expect(errorLogs.length).toMatch(1);
+ expect(errorLogs.shift()).toMatch(/ErrorMsg1/);
+
+ scope.error['throw'] = function(){ return 'X';};
+ scope.$apply();
+ expect(errorLogs.length).toMatch(0);
});
it('NestedRepeater', function(){
@@ -318,8 +322,8 @@ describe('Binder', function(){
'' +
'
');
- scope.$set('model', [{name:'a', item:['a1', 'a2']}, {name:'b', item:['b1', 'b2']}]);
- scope.$eval();
+ scope.model = [{name:'a', item:['a1', 'a2']}, {name:'b', item:['b1', 'b2']}];
+ scope.$apply();
assertEquals(''+
'<#comment>#comment>'+
@@ -338,13 +342,13 @@ describe('Binder', function(){
it('HideBindingExpression', function(){
var scope = this.compile('
');
- scope.$set('hidden', 3);
- scope.$eval();
+ scope.hidden = 3;
+ scope.$apply();
assertHidden(scope.$element);
- scope.$set('hidden', 2);
- scope.$eval();
+ scope.hidden = 2;
+ scope.$apply();
assertVisible(scope.$element);
});
@@ -352,18 +356,18 @@ describe('Binder', function(){
it('HideBinding', function(){
var scope = this.compile('
');
- scope.$set('hidden', 'true');
- scope.$eval();
+ scope.hidden = 'true';
+ scope.$apply();
assertHidden(scope.$element);
- scope.$set('hidden', 'false');
- scope.$eval();
+ scope.hidden = 'false';
+ scope.$apply();
assertVisible(scope.$element);
- scope.$set('hidden', '');
- scope.$eval();
+ scope.hidden = '';
+ scope.$apply();
assertVisible(scope.$element);
});
@@ -371,25 +375,25 @@ describe('Binder', function(){
it('ShowBinding', function(){
var scope = this.compile('
');
- scope.$set('show', 'true');
- scope.$eval();
+ scope.show = 'true';
+ scope.$apply();
assertVisible(scope.$element);
- scope.$set('show', 'false');
- scope.$eval();
+ scope.show = 'false';
+ scope.$apply();
assertHidden(scope.$element);
- scope.$set('show', '');
- scope.$eval();
+ scope.show = '';
+ scope.$apply();
assertHidden(scope.$element);
});
it('BindClassUndefined', function(){
var scope = this.compile('
');
- scope.$eval();
+ scope.$apply();
assertEquals(
'
',
@@ -397,22 +401,22 @@ describe('Binder', function(){
});
it('BindClass', function(){
- var scope = this.compile('
');
+ var scope = this.compile('
');
- scope.$set('class', 'testClass');
- scope.$eval();
+ scope.clazz = 'testClass';
+ scope.$apply();
- assertEquals('
', sortedHtml(scope.$element));
+ assertEquals('
', sortedHtml(scope.$element));
- scope.$set('class', ['a', 'b']);
- scope.$eval();
+ scope.clazz = ['a', 'b'];
+ scope.$apply();
- assertEquals('
', sortedHtml(scope.$element));
+ assertEquals('
', sortedHtml(scope.$element));
});
it('BindClassEvenOdd', function(){
var scope = this.compile('
');
- scope.$eval();
+ scope.$apply();
var d1 = jqLite(scope.$element[0].childNodes[1]);
var d2 = jqLite(scope.$element[0].childNodes[2]);
expect(d1.hasClass('o')).toBeTruthy();
@@ -428,31 +432,22 @@ describe('Binder', function(){
var scope = this.compile('
');
scope.$eval('style={height: "10px"}');
- scope.$eval();
+ scope.$apply();
assertEquals("10px", scope.$element.css('height'));
scope.$eval('style={}');
- scope.$eval();
+ scope.$apply();
});
it('ActionOnAHrefThrowsError', function(){
- var scope = this.compile('
Add Phone');
+ var scope = this.compile('
Add Phone', null, true);
scope.action = function(){
throw new Error('MyError');
};
var input = scope.$element;
browserTrigger(input, 'click');
- var error = input.attr('ng-exception');
- 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'));
+ expect(scope.$service('$exceptionHandler').errors[0]).toMatch(/MyError/);
});
it('ShoulIgnoreVbNonBindable', function(){
@@ -460,16 +455,15 @@ describe('Binder', function(){
"
{{a}}
" +
"
{{b}}
" +
"
{{c}}
");
- scope.$set('a', 123);
- scope.$eval();
+ scope.a = 123;
+ scope.$apply();
assertEquals('123{{a}}{{b}}{{c}}', scope.$element.text());
});
-
it('RepeaterShouldBindInputsDefaults', function () {
var scope = this.compile('