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/AngularSpec.js | 8 +- test/BinderSpec.js | 274 ++++++++-------- test/CompilerSpec.js | 41 ++- test/ParserSpec.js | 40 +-- test/ResourceSpec.js | 2 +- test/ScenarioSpec.js | 23 +- test/ScopeSpec.js | 617 ++++++++++++++++++++++++----------- test/ValidatorsSpec.js | 8 +- test/directivesSpec.js | 69 ++-- test/markupSpec.js | 35 +- test/mocks.js | 2 +- test/scenario/SpecRunnerSpec.js | 15 +- test/scenario/dslSpec.js | 15 +- test/service/cookieStoreSpec.js | 6 +- test/service/cookiesSpec.js | 18 +- test/service/deferSpec.js | 22 +- test/service/exceptionHandlerSpec.js | 5 +- test/service/invalidWidgetsSpec.js | 8 +- test/service/locationSpec.js | 35 +- test/service/logSpec.js | 24 +- test/service/routeSpec.js | 71 ++-- test/service/updateViewSpec.js | 6 +- test/service/xhr.bulkSpec.js | 5 +- test/service/xhr.cacheSpec.js | 14 +- test/service/xhr.errorSpec.js | 2 +- test/service/xhrSpec.js | 3 +- test/testabilityPatch.js | 9 +- test/widgetsSpec.js | 348 ++++++++++---------- 28 files changed, 1020 insertions(+), 705 deletions(-) (limited to 'test') diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 0166503c..abb34f3e 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -63,7 +63,8 @@ describe('angular', function(){ it('should return true if same object', function(){ var o = {}; 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); }); @@ -550,6 +551,7 @@ describe('angular', function(){ it('should link to existing node and create scope', function(){ template = angular.element('
Hello {{name}}!');
- scope.$set("name", "World");
- scope.$eval();
+ scope.name = "World";
+ scope.$apply();
assertEquals('Hello World!', sortedHtml(scope.$element)); }); @@ -486,9 +480,9 @@ describe('Binder', function(){ it('FillInOptionValueWhenMissing', function(){ var scope = this.compile( ''); - scope.$set('a', 'A'); - scope.$set('b', 'B'); - scope.$eval(); + scope.a = 'A'; + scope.b = 'B'; + scope.$apply(); var optionA = childNode(scope.$element, 0); var optionB = childNode(scope.$element, 1); var optionC = childNode(scope.$element, 2); @@ -508,39 +502,39 @@ describe('Binder', function(){ '', jqLite(document.body)); var items = [{}, {}]; - scope.$set("items", items); - scope.$eval(); + scope.items = items; + scope.$apply(); assertEquals(3, scope.$service('$invalidWidgets').length); - scope.$set('name', ''); - scope.$eval(); + scope.name = ''; + scope.$apply(); assertEquals(3, scope.$service('$invalidWidgets').length); - scope.$set('name', ' '); - scope.$eval(); + scope.name = ' '; + scope.$apply(); assertEquals(3, scope.$service('$invalidWidgets').length); - scope.$set('name', 'abc'); - scope.$eval(); + scope.name = 'abc'; + scope.$apply(); assertEquals(2, scope.$service('$invalidWidgets').length); items[0].name = 'abc'; - scope.$eval(); + scope.$apply(); assertEquals(1, scope.$service('$invalidWidgets').length); items[1].name = 'abc'; - scope.$eval(); + scope.$apply(); assertEquals(0, scope.$service('$invalidWidgets').length); }); it('ValidateOnlyVisibleItems', function(){ var scope = this.compile('', jqLite(document.body)); - scope.$set("show", true); - scope.$eval(); + scope.show = true; + scope.$apply(); assertEquals(2, scope.$service('$invalidWidgets').length); - scope.$set("show", false); - scope.$eval(); + scope.show = false; + scope.$apply(); assertEquals(1, scope.$service('$invalidWidgets').visible()); }); @@ -549,7 +543,7 @@ describe('Binder', function(){ '' + '' + ''); - scope.$eval(); + scope.$apply(); function assertChild(index, disabled) { var child = childNode(scope.$element, index); assertEquals(sortedHtml(child), disabled, !!child.attr('disabled')); @@ -566,7 +560,7 @@ describe('Binder', function(){ it('ItShouldDisplayErrorWhenActionIsSyntacticlyIncorrect', function(){ var scope = this.compile('
- * var scope = angular.scope(null, {'$exceptionHandler': $exceptionHandlerMockFactory});
+ * var scope = angular.scope(null, {'$exceptionHandler': $exceptionHandlerMockFactory()});
*
*
*/
diff --git a/test/scenario/SpecRunnerSpec.js b/test/scenario/SpecRunnerSpec.js
index 0e1ffac1..92f000ba 100644
--- a/test/scenario/SpecRunnerSpec.js
+++ b/test/scenario/SpecRunnerSpec.js
@@ -31,14 +31,13 @@ describe('angular.scenario.SpecRunner', function() {
$window.setTimeout = function(fn, timeout) {
fn();
};
- $root = angular.scope({
- emit: function(eventName) {
- log.push(eventName);
- },
- on: function(eventName) {
- log.push('Listener Added for ' + eventName);
- }
- });
+ $root = angular.scope();
+ $root.emit = function(eventName) {
+ log.push(eventName);
+ };
+ $root.on = function(eventName) {
+ log.push('Listener Added for ' + eventName);
+ };
$root.application = new ApplicationMock($window);
$root.$window = $window;
runner = $root.$new(angular.scenario.SpecRunner);
diff --git a/test/scenario/dslSpec.js b/test/scenario/dslSpec.js
index a07d411e..5485fe52 100644
--- a/test/scenario/dslSpec.js
+++ b/test/scenario/dslSpec.js
@@ -10,14 +10,13 @@ describe("angular.scenario.dsl", function() {
document: _jQuery(""),
angular: new angular.scenario.testing.MockAngular()
};
- $root = angular.scope({
- emit: function(eventName) {
- eventLog.push(eventName);
- },
- on: function(eventName) {
- eventLog.push('Listener Added for ' + eventName);
- }
- });
+ $root = angular.scope();
+ $root.emit = function(eventName) {
+ eventLog.push(eventName);
+ };
+ $root.on = function(eventName) {
+ eventLog.push('Listener Added for ' + eventName);
+ };
$root.futures = [];
$root.futureLog = [];
$root.$window = $window;
diff --git a/test/service/cookieStoreSpec.js b/test/service/cookieStoreSpec.js
index b37e9cb0..75be924c 100644
--- a/test/service/cookieStoreSpec.js
+++ b/test/service/cookieStoreSpec.js
@@ -16,7 +16,7 @@ describe('$cookieStore', function() {
it('should serialize objects to json', function() {
$cookieStore.put('objectCookie', {id: 123, name: 'blah'});
- scope.$eval(); //force eval in test
+ scope.$flush();
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() {
$cookieStore.put('gonner', { "I'll":"Be Back"});
- scope.$eval(); //force eval in test
+ scope.$flush(); //force eval in test
$browser.poll();
expect($browser.cookies()).toEqual({'gonner': '{"I\'ll":"Be Back"}'});
$cookieStore.remove('gonner');
- scope.$eval();
+ scope.$flush();
expect($browser.cookies()).toEqual({});
});
});
diff --git a/test/service/cookiesSpec.js b/test/service/cookiesSpec.js
index 3248fe23..cc667b56 100644
--- a/test/service/cookiesSpec.js
+++ b/test/service/cookiesSpec.js
@@ -6,7 +6,7 @@ describe('$cookies', function() {
beforeEach(function() {
$browser = new MockBrowser();
$browser.cookieHash['preexisting'] = 'oldCookie';
- scope = angular.scope(null, angular.service, {$browser: $browser});
+ scope = angular.scope(angular.service, {$browser: $browser});
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() {
scope.$cookies.oatmealCookie = 'nom nom';
- scope.$eval();
+ scope.$flush();
expect($browser.cookies()).
toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'});
scope.$cookies.oatmealCookie = 'gone';
- scope.$eval();
+ scope.$flush();
expect($browser.cookies()).
toEqual({'preexisting': 'oldCookie', 'oatmealCookie': 'gone'});
@@ -56,7 +56,7 @@ describe('$cookies', function() {
scope.$cookies.nullVal = null;
scope.$cookies.undefVal = undefined;
scope.$cookies.preexisting = function(){};
- scope.$eval();
+ scope.$flush();
expect($browser.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() {
scope.$cookies.oatmealCookie = 'nom nom';
- scope.$eval();
+ scope.$flush();
$browser.poll();
expect($browser.cookies()).
toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'});
delete scope.$cookies.oatmealCookie;
- scope.$eval();
+ scope.$flush();
expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'});
});
@@ -85,16 +85,16 @@ describe('$cookies', function() {
//drop if no previous value
scope.$cookies.longCookie = longVal;
- scope.$eval();
+ scope.$flush();
expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'});
//reset if previous value existed
scope.$cookies.longCookie = 'shortVal';
- scope.$eval();
+ scope.$flush();
expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'});
scope.$cookies.longCookie = longVal;
- scope.$eval();
+ scope.$flush();
expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'});
});
});
diff --git a/test/service/deferSpec.js b/test/service/deferSpec.js
index 7e59978c..4f651522 100644
--- a/test/service/deferSpec.js
+++ b/test/service/deferSpec.js
@@ -4,7 +4,7 @@ describe('$defer', function() {
var scope, $browser, $defer, $exceptionHandler;
beforeEach(function(){
- scope = angular.scope({}, angular.service,
+ scope = angular.scope(angular.service,
{'$exceptionHandler': jasmine.createSpy('$exceptionHandler')});
$browser = scope.$service('$browser');
$defer = scope.$service('$defer');
@@ -41,32 +41,32 @@ describe('$defer', function() {
});
- it('should call eval after each callback is executed', function() {
- var evalSpy = this.spyOn(scope, '$eval').andCallThrough();
+ it('should call $apply after each callback is executed', function() {
+ var applySpy = this.spyOn(scope, '$apply').andCallThrough();
$defer(function() {});
- expect(evalSpy).not.toHaveBeenCalled();
+ expect(applySpy).not.toHaveBeenCalled();
$browser.defer.flush();
- expect(evalSpy).toHaveBeenCalled();
+ expect(applySpy).toHaveBeenCalled();
- evalSpy.reset(); //reset the spy;
+ applySpy.reset(); //reset the spy;
$defer(function() {});
$defer(function() {});
$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() {
- var evalSpy = this.spyOn(scope, '$eval').andCallThrough();
+ it('should call $apply even if an exception is thrown in callback', function() {
+ var applySpy = this.spyOn(scope, '$apply').andCallThrough();
$defer(function() {throw "Test Error";});
- expect(evalSpy).not.toHaveBeenCalled();
+ expect(applySpy).not.toHaveBeenCalled();
$browser.defer.flush();
- expect(evalSpy).toHaveBeenCalled();
+ expect(applySpy).toHaveBeenCalled();
});
it('should allow you to specify the delay time', function(){
diff --git a/test/service/exceptionHandlerSpec.js b/test/service/exceptionHandlerSpec.js
index c6f13161..74f37cb9 100644
--- a/test/service/exceptionHandlerSpec.js
+++ b/test/service/exceptionHandlerSpec.js
@@ -14,11 +14,12 @@ describe('$exceptionHandler', function() {
it('should log errors', function(){
- var scope = createScope({}, {$exceptionHandler: $exceptionHandlerFactory},
- {$log: $logMock}),
+ var scope = createScope({$exceptionHandler: $exceptionHandlerFactory},
+ {$log: $logMock}),
$log = scope.$service('$log'),
$exceptionHandler = scope.$service('$exceptionHandler');
+ $log.error.rethrow = false;
$exceptionHandler('myError');
expect($log.error.logs.shift()).toEqual(['myError']);
});
diff --git a/test/service/invalidWidgetsSpec.js b/test/service/invalidWidgetsSpec.js
index 027d8d7c..fe7efe38 100644
--- a/test/service/invalidWidgetsSpec.js
+++ b/test/service/invalidWidgetsSpec.js
@@ -21,21 +21,21 @@ describe('$invalidWidgets', function() {
expect($invalidWidgets.length).toEqual(1);
scope.price = 123;
- scope.$eval();
+ scope.$digest();
expect($invalidWidgets.length).toEqual(0);
scope.$element.remove();
scope.price = 'abc';
- scope.$eval();
+ scope.$digest();
expect($invalidWidgets.length).toEqual(0);
jqLite(document.body).append(scope.$element);
scope.price = 'abcd'; //force revalidation, maybe this should be done automatically?
- scope.$eval();
+ scope.$digest();
expect($invalidWidgets.length).toEqual(1);
jqLite(document.body).html('');
- scope.$eval();
+ scope.$digest();
expect($invalidWidgets.length).toEqual(0);
});
});
diff --git a/test/service/locationSpec.js b/test/service/locationSpec.js
index f5a8c6b2..73e5e43e 100644
--- a/test/service/locationSpec.js
+++ b/test/service/locationSpec.js
@@ -46,9 +46,10 @@ describe('$location', function() {
$location.update('http://www.angularjs.org/');
$location.update({path: '/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');
+ $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() {
$location.update('http://server/#path?a=b');
- scope.$eval();
+ scope.$digest();
$location.update({hashPath: '', hashSearch: {}});
expect($location.hash).toEqual('');
@@ -74,10 +75,10 @@ describe('$location', function() {
it('should update hashPath and hashSearch on $location.hash change upon eval', function(){
$location.update('http://server/#path?a=b');
- scope.$eval();
+ scope.$digest();
$location.hash = '';
- scope.$eval();
+ scope.$digest();
expect($location.href).toEqual('http://server/');
expect($location.hashPath).toEqual('');
@@ -88,11 +89,13 @@ describe('$location', function() {
it('should update hash on $location.hashPath or $location.hashSearch change upon eval',
function() {
$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.hashSearch = {};
-
- scope.$eval();
+ scope.$digest();
expect($location.href).toEqual('http://server/');
expect($location.hash).toEqual('');
@@ -103,14 +106,14 @@ describe('$location', function() {
scope.$location = scope.$service('$location'); //publish to the scope for $watch
var log = '';
- scope.$watch('$location.hash', function(){
- log += this.$location.hashPath + ';';
- });
+ scope.$watch('$location.hash', function(scope){
+ log += scope.$location.hashPath + ';';
+ })();
expect(log).toEqual(';');
log = '';
scope.$location.hash = '/abc';
- scope.$eval();
+ scope.$digest();
expect(scope.$location.hash).toEqual('/abc');
expect(log).toEqual('/abc;');
});
@@ -120,7 +123,7 @@ describe('$location', function() {
it('should update hash with escaped hashPath', function() {
$location.hashPath = 'foo=bar';
- scope.$eval();
+ scope.$digest();
expect($location.hash).toBe('foo%3Dbar');
});
@@ -133,7 +136,7 @@ describe('$location', function() {
$location.host = 'host';
$location.href = 'https://hrefhost:23/hrefpath';
- scope.$eval();
+ scope.$digest();
expect($location).toEqualData({href: 'https://hrefhost:23/hrefpath',
protocol: 'https',
@@ -156,7 +159,7 @@ describe('$location', function() {
$location.host = 'host';
$location.path = '/path';
- scope.$eval();
+ scope.$digest();
expect($location).toEqualData({href: 'http://host:333/path#hash',
protocol: 'http',
@@ -237,7 +240,7 @@ describe('$location', function() {
expect($location.href).toBe('http://server');
expect($location.hash).toBe('');
- scope.$eval();
+ scope.$digest();
expect($location.href).toBe('http://server');
expect($location.hash).toBe('');
diff --git a/test/service/logSpec.js b/test/service/logSpec.js
index 80d085b8..499447ad 100644
--- a/test/service/logSpec.js
+++ b/test/service/logSpec.js
@@ -19,12 +19,12 @@ describe('$log', function() {
function warn(){ logger+= 'warn;'; }
function info(){ logger+= 'info;'; }
function error(){ logger+= 'error;'; }
- var scope = createScope({}, {$log: $logFactory},
- {$exceptionHandler: rethrow,
- $window: {console: {log: log,
- warn: warn,
- info: info,
- error: error}}}),
+ var scope = createScope({$log: $logFactory},
+ {$exceptionHandler: rethrow,
+ $window: {console: {log: log,
+ warn: warn,
+ info: info,
+ error: error}}}),
$log = scope.$service('$log');
$log.log();
@@ -38,9 +38,9 @@ describe('$log', function() {
it('should use console.log() if other not present', function(){
var logger = "";
function log(){ logger+= 'log;'; }
- var scope = createScope({}, {$log: $logFactory},
- {$window: {console:{log:log}},
- $exceptionHandler: rethrow});
+ var scope = createScope({$log: $logFactory},
+ {$window: {console:{log:log}},
+ $exceptionHandler: rethrow});
var $log = scope.$service('$log');
$log.log();
$log.warn();
@@ -51,9 +51,9 @@ describe('$log', function() {
it('should use noop if no console', function(){
- var scope = createScope({}, {$log: $logFactory},
- {$window: {},
- $exceptionHandler: rethrow}),
+ var scope = createScope({$log: $logFactory},
+ {$window: {},
+ $exceptionHandler: rethrow}),
$log = scope.$service('$log');
$log.log();
$log.warn();
diff --git a/test/service/routeSpec.js b/test/service/routeSpec.js
index fc2c7f9d..6c6c0868 100644
--- a/test/service/routeSpec.js
+++ b/test/service/routeSpec.js
@@ -18,7 +18,7 @@ describe('$route', function() {
$location, $route;
function BookChapter() {
- this.log = '