From 3ed8d76b904eb9f29e9df3be3ccc9723db28d03c Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 28 Oct 2014 19:54:32 +0000 Subject: [PATCH] feat(ngMock): decorator that adds Scope#$countChildScopes and Scope#$countWatchers When writing tests it's often useful to check the number of child scopes or watchers within the current current scope subtree. Common use-case for advanced directives is to test that the directive is properly cleaning up after itself. These new methods simplify writing assertions that verify that child scopes were properly destroyed or that watchers were deregistered. --- src/ng/rootScope.js | 4 + src/ngMock/angular-mocks.js | 105 +++++++++++++++++++ test/ngMock/angular-mocksSpec.js | 168 +++++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index b98dccca8a4f..3c3488c6f941 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -113,6 +113,10 @@ function $RootScopeProvider() { expect(parent.salutation).toEqual('Hello'); * ``` * + * When interacting with `Scope` in tests, additional helper methods are available on the + * instances of `Scope` type. See {@link ngMock.$rootScope.Scope ngMock Scope} for additional + * details. + * * * @param {Object.=} providers Map of service factory which need to be * provided for the current scope. Defaults to {@link ng}. diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 71eedb3318f4..2afae9f04c68 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -1823,6 +1823,7 @@ angular.module('ngMock', ['ng']).provider({ $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); $provide.decorator('$$rAF', angular.mock.$RAFDecorator); $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator); + $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator); }]); /** @@ -2031,6 +2032,110 @@ angular.mock.e2e.$httpBackendDecorator = ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; +/** + * @ngdoc type + * @name $rootScope.Scope + * @module ngMock + * @description + * {@link ng.$rootScope.Scope Scope} type decorated with helper methods useful for testing. These + * methods are automatically available on any {@link ng.$rootScope.Scope Scope} instance when + * `ngMock` module is loaded. + * + * In addition to all the regular `Scope` methods, the following helper methods are available: + */ +angular.mock.$RootScopeDecorator = function($delegate) { + + var $rootScopePrototype = Object.getPrototypeOf($delegate); + + $rootScopePrototype.$countChildScopes = countChildScopes; + $rootScopePrototype.$countWatchers = countWatchers; + + // TODO: remove if Object.getPrototypeOF works on IE9 + //var originalScopeNew = $delegate.$new; + //$delegate.$new = decoratedScopeNew; + + return $delegate; + + // ------------------------------------------------------------------------------------------ // + + /** + * @ngdoc method + * @name $rootScope + * @module ngMock + * @description + * Counts all the direct and indirect child scopes of the current scope. + * + * The current scope is excluded from the count. The count includes all isolate child scopes. + * + * @returns {number} Total number of child scopes. + */ + function countChildScopes() { + var count = 0; // exclude the current scope + var pendingChildHeads = [this.$$childHead]; + var currentScope; + + while (pendingChildHeads.length) { + currentScope = pendingChildHeads.shift(); + + while (currentScope) { + count += 1; + pendingChildHeads.push(currentScope.$$childHead); + currentScope = currentScope.$$nextSibling; + } + } + + return count; + } + + + /** + * @ngdoc method + * @name $rootScope + * @module ngMock + * @description + * Counts all the watchers of direct and indirect child scopes of the current scope. + * + * The watchers of the current scope are included in the count and so are all the watchers of + * isolate child scopes. + * + * @returns {number} Total number of watchers. + */ + function countWatchers() { + var count = this.$$watchers ? this.$$watchers.length : 0; // include the current scope + var pendingChildHeads = [this.$$childHead]; + var currentScope; + + while (pendingChildHeads.length) { + currentScope = pendingChildHeads.shift(); + + while (currentScope) { + count += currentScope.$$watchers ? currentScope.$$watchers.length : 0; + pendingChildHeads.push(currentScope.$$childHead); + currentScope = currentScope.$$nextSibling; + } + } + + return count; + } + + + // TODO: remove if Object.getPrototypeOF works on IE9 + ///** + // * preserves monkey patched methods on isolate scopes (which don't inherit from $rootScope) + // * */ + //function decoratedScopeNew(isolate) { + // var newScope = originalScopeNew.apply(this, arguments); + // + // if (isolate) { + // newScope.$countChildScopes = countChildScopes; + // newScope.$countWatchers = countWatchers; + // } + // + // return newScope; + //} +}; + + if (window.jasmine || window.mocha) { var currentSpec = null, diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index 2c7ceb626d10..51bcb0602ad1 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -1507,6 +1507,174 @@ describe('ngMock', function() { expect($rootElement.text()).toEqual(''); })); }); + + + ddescribe('$rootScopeDecorator', function() { + + describe('$countChildScopes', function() { + + it('should return 0 when no child scopes', inject(function($rootScope) { + expect($rootScope.$countChildScopes()).toBe(0); + + var childScope = $rootScope.$new(); + expect($rootScope.$countChildScopes()).toBe(1); + expect(childScope.$countChildScopes()).toBe(0); + + var grandChildScope = childScope.$new(); + expect(childScope.$countChildScopes()).toBe(1); + expect(grandChildScope.$countChildScopes()).toBe(0); + })); + + + it('should correctly navigate complex scope tree', inject(function($rootScope) { + var child; + + $rootScope.$new(); + $rootScope.$new().$new().$new(); + child = $rootScope.$new().$new(); + child.$new(); + child.$new(); + child.$new().$new().$new(); + + expect($rootScope.$countChildScopes()).toBe(11); + })); + + + it('should provide the current count even after child destructions', inject(function($rootScope) { + expect($rootScope.$countChildScopes()).toBe(0); + + var childScope1 = $rootScope.$new(); + expect($rootScope.$countChildScopes()).toBe(1); + + var childScope2 = $rootScope.$new(); + expect($rootScope.$countChildScopes()).toBe(2); + + childScope1.$destroy(); + expect($rootScope.$countChildScopes()).toBe(1); + + childScope2.$destroy(); + expect($rootScope.$countChildScopes()).toBe(0); + })); + + + it('should work with isolate scopes', inject(function($rootScope) { + /* + RS + | + CIS + / \ + GCS GCIS + */ + + var childIsolateScope = $rootScope.$new(true); + expect($rootScope.$countChildScopes()).toBe(1); + + var grandChildScope = childIsolateScope.$new(); + expect($rootScope.$countChildScopes()).toBe(2); + expect(childIsolateScope.$countChildScopes()).toBe(1); + + var grandChildIsolateScope = childIsolateScope.$new(true); + expect($rootScope.$countChildScopes()).toBe(3); + expect(childIsolateScope.$countChildScopes()).toBe(2); + + childIsolateScope.$destroy(); + expect($rootScope.$countChildScopes()).toBe(0); + })); + }); + + + describe('$countWatchers', function() { + + it('should return the sum of watchers for the current scope and all of its children', inject( + function($rootScope) { + + expect($rootScope.$countWatchers()).toBe(0); + + var childScope = $rootScope.$new(); + expect($rootScope.$countWatchers()).toBe(0); + + childScope.$watch('foo'); + expect($rootScope.$countWatchers()).toBe(1); + expect(childScope.$countWatchers()).toBe(1); + + $rootScope.$watch('bar'); + childScope.$watch('baz'); + expect($rootScope.$countWatchers()).toBe(3); + expect(childScope.$countWatchers()).toBe(2); + })); + + + it('should correctly navigate complex scope tree', inject(function($rootScope) { + var child; + + $rootScope.$watch('foo1'); + + $rootScope.$new(); + $rootScope.$new().$new().$new(); + + child = $rootScope.$new().$new(); + child.$watch('foo2'); + child.$new(); + child.$new(); + child = child.$new().$new().$new(); + child.$watch('foo3'); + child.$watch('foo4'); + + expect($rootScope.$countWatchers()).toBe(4); + })); + + + it('should provide the current count even after child destruction and watch deregistration', + inject(function($rootScope) { + + var deregisterWatch1 = $rootScope.$watch('exp1'); + + var childScope = $rootScope.$new(); + childScope.$watch('exp2'); + + expect($rootScope.$countWatchers()).toBe(2); + + childScope.$destroy(); + expect($rootScope.$countWatchers()).toBe(1); + + deregisterWatch1(); + expect($rootScope.$countWatchers()).toBe(0); + })); + + + it('should work with isolate scopes', inject(function($rootScope) { + /* + RS=1 + | + CIS=1 + / \ + GCS=1 GCIS=1 + */ + + $rootScope.$watch('exp1'); + expect($rootScope.$countWatchers()).toBe(1); + + var childIsolateScope = $rootScope.$new(true); + childIsolateScope.$watch('exp2'); + expect($rootScope.$countWatchers()).toBe(2); + expect(childIsolateScope.$countWatchers()).toBe(1); + + var grandChildScope = childIsolateScope.$new(); + grandChildScope.$watch('exp3'); + + var grandChildIsolateScope = childIsolateScope.$new(true); + grandChildIsolateScope.$watch('exp4'); + + expect($rootScope.$countWatchers()).toBe(4); + expect(childIsolateScope.$countWatchers()).toBe(3); + expect(grandChildScope.$countWatchers()).toBe(1); + expect(grandChildIsolateScope.$countWatchers()).toBe(1); + + childIsolateScope.$destroy(); + expect($rootScope.$countWatchers()).toBe(1); + })); + }); + }); });