Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 74981c9

Browse files
IgorMinarcaitp
authored andcommitted
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. Closes #9926 Closes #9871
1 parent dc4b065 commit 74981c9

File tree

3 files changed

+259
-0
lines changed

3 files changed

+259
-0
lines changed

src/ng/rootScope.js

+4
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ function $RootScopeProvider() {
113113
expect(parent.salutation).toEqual('Hello');
114114
* ```
115115
*
116+
* When interacting with `Scope` in tests, additional helper methods are available on the
117+
* instances of `Scope` type. See {@link ngMock.$rootScope.Scope ngMock Scope} for additional
118+
* details.
119+
*
116120
*
117121
* @param {Object.<string, function()>=} providers Map of service factory which need to be
118122
* provided for the current scope. Defaults to {@link ng}.

src/ngMock/angular-mocks.js

+87
Original file line numberDiff line numberDiff line change
@@ -1823,6 +1823,7 @@ angular.module('ngMock', ['ng']).provider({
18231823
$provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
18241824
$provide.decorator('$$rAF', angular.mock.$RAFDecorator);
18251825
$provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator);
1826+
$provide.decorator('$rootScope', angular.mock.$RootScopeDecorator);
18261827
}]);
18271828

18281829
/**
@@ -2031,6 +2032,92 @@ angular.mock.e2e.$httpBackendDecorator =
20312032
['$rootScope', '$delegate', '$browser', createHttpBackendMock];
20322033

20332034

2035+
/**
2036+
* @ngdoc type
2037+
* @name $rootScope.Scope
2038+
* @module ngMock
2039+
* @description
2040+
* {@link ng.$rootScope.Scope Scope} type decorated with helper methods useful for testing. These
2041+
* methods are automatically available on any {@link ng.$rootScope.Scope Scope} instance when
2042+
* `ngMock` module is loaded.
2043+
*
2044+
* In addition to all the regular `Scope` methods, the following helper methods are available:
2045+
*/
2046+
angular.mock.$RootScopeDecorator = function($delegate) {
2047+
2048+
var $rootScopePrototype = Object.getPrototypeOf($delegate);
2049+
2050+
$rootScopePrototype.$countChildScopes = countChildScopes;
2051+
$rootScopePrototype.$countWatchers = countWatchers;
2052+
2053+
return $delegate;
2054+
2055+
// ------------------------------------------------------------------------------------------ //
2056+
2057+
/**
2058+
* @ngdoc method
2059+
* @name $rootScope.Scope#$countChildScopes
2060+
* @module ngMock
2061+
* @description
2062+
* Counts all the direct and indirect child scopes of the current scope.
2063+
*
2064+
* The current scope is excluded from the count. The count includes all isolate child scopes.
2065+
*
2066+
* @returns {number} Total number of child scopes.
2067+
*/
2068+
function countChildScopes() {
2069+
// jshint validthis: true
2070+
var count = 0; // exclude the current scope
2071+
var pendingChildHeads = [this.$$childHead];
2072+
var currentScope;
2073+
2074+
while (pendingChildHeads.length) {
2075+
currentScope = pendingChildHeads.shift();
2076+
2077+
while (currentScope) {
2078+
count += 1;
2079+
pendingChildHeads.push(currentScope.$$childHead);
2080+
currentScope = currentScope.$$nextSibling;
2081+
}
2082+
}
2083+
2084+
return count;
2085+
}
2086+
2087+
2088+
/**
2089+
* @ngdoc method
2090+
* @name $rootScope.Scope#$countWatchers
2091+
* @module ngMock
2092+
* @description
2093+
* Counts all the watchers of direct and indirect child scopes of the current scope.
2094+
*
2095+
* The watchers of the current scope are included in the count and so are all the watchers of
2096+
* isolate child scopes.
2097+
*
2098+
* @returns {number} Total number of watchers.
2099+
*/
2100+
function countWatchers() {
2101+
// jshint validthis: true
2102+
var count = this.$$watchers ? this.$$watchers.length : 0; // include the current scope
2103+
var pendingChildHeads = [this.$$childHead];
2104+
var currentScope;
2105+
2106+
while (pendingChildHeads.length) {
2107+
currentScope = pendingChildHeads.shift();
2108+
2109+
while (currentScope) {
2110+
count += currentScope.$$watchers ? currentScope.$$watchers.length : 0;
2111+
pendingChildHeads.push(currentScope.$$childHead);
2112+
currentScope = currentScope.$$nextSibling;
2113+
}
2114+
}
2115+
2116+
return count;
2117+
}
2118+
};
2119+
2120+
20342121
if (window.jasmine || window.mocha) {
20352122

20362123
var currentSpec = null,

test/ngMock/angular-mocksSpec.js

+168
Original file line numberDiff line numberDiff line change
@@ -1507,6 +1507,174 @@ describe('ngMock', function() {
15071507
expect($rootElement.text()).toEqual('');
15081508
}));
15091509
});
1510+
1511+
1512+
describe('$rootScopeDecorator', function() {
1513+
1514+
describe('$countChildScopes', function() {
1515+
1516+
it('should return 0 when no child scopes', inject(function($rootScope) {
1517+
expect($rootScope.$countChildScopes()).toBe(0);
1518+
1519+
var childScope = $rootScope.$new();
1520+
expect($rootScope.$countChildScopes()).toBe(1);
1521+
expect(childScope.$countChildScopes()).toBe(0);
1522+
1523+
var grandChildScope = childScope.$new();
1524+
expect(childScope.$countChildScopes()).toBe(1);
1525+
expect(grandChildScope.$countChildScopes()).toBe(0);
1526+
}));
1527+
1528+
1529+
it('should correctly navigate complex scope tree', inject(function($rootScope) {
1530+
var child;
1531+
1532+
$rootScope.$new();
1533+
$rootScope.$new().$new().$new();
1534+
child = $rootScope.$new().$new();
1535+
child.$new();
1536+
child.$new();
1537+
child.$new().$new().$new();
1538+
1539+
expect($rootScope.$countChildScopes()).toBe(11);
1540+
}));
1541+
1542+
1543+
it('should provide the current count even after child destructions', inject(function($rootScope) {
1544+
expect($rootScope.$countChildScopes()).toBe(0);
1545+
1546+
var childScope1 = $rootScope.$new();
1547+
expect($rootScope.$countChildScopes()).toBe(1);
1548+
1549+
var childScope2 = $rootScope.$new();
1550+
expect($rootScope.$countChildScopes()).toBe(2);
1551+
1552+
childScope1.$destroy();
1553+
expect($rootScope.$countChildScopes()).toBe(1);
1554+
1555+
childScope2.$destroy();
1556+
expect($rootScope.$countChildScopes()).toBe(0);
1557+
}));
1558+
1559+
1560+
it('should work with isolate scopes', inject(function($rootScope) {
1561+
/*
1562+
RS
1563+
|
1564+
CIS
1565+
/ \
1566+
GCS GCIS
1567+
*/
1568+
1569+
var childIsolateScope = $rootScope.$new(true);
1570+
expect($rootScope.$countChildScopes()).toBe(1);
1571+
1572+
var grandChildScope = childIsolateScope.$new();
1573+
expect($rootScope.$countChildScopes()).toBe(2);
1574+
expect(childIsolateScope.$countChildScopes()).toBe(1);
1575+
1576+
var grandChildIsolateScope = childIsolateScope.$new(true);
1577+
expect($rootScope.$countChildScopes()).toBe(3);
1578+
expect(childIsolateScope.$countChildScopes()).toBe(2);
1579+
1580+
childIsolateScope.$destroy();
1581+
expect($rootScope.$countChildScopes()).toBe(0);
1582+
}));
1583+
});
1584+
1585+
1586+
describe('$countWatchers', function() {
1587+
1588+
it('should return the sum of watchers for the current scope and all of its children', inject(
1589+
function($rootScope) {
1590+
1591+
expect($rootScope.$countWatchers()).toBe(0);
1592+
1593+
var childScope = $rootScope.$new();
1594+
expect($rootScope.$countWatchers()).toBe(0);
1595+
1596+
childScope.$watch('foo');
1597+
expect($rootScope.$countWatchers()).toBe(1);
1598+
expect(childScope.$countWatchers()).toBe(1);
1599+
1600+
$rootScope.$watch('bar');
1601+
childScope.$watch('baz');
1602+
expect($rootScope.$countWatchers()).toBe(3);
1603+
expect(childScope.$countWatchers()).toBe(2);
1604+
}));
1605+
1606+
1607+
it('should correctly navigate complex scope tree', inject(function($rootScope) {
1608+
var child;
1609+
1610+
$rootScope.$watch('foo1');
1611+
1612+
$rootScope.$new();
1613+
$rootScope.$new().$new().$new();
1614+
1615+
child = $rootScope.$new().$new();
1616+
child.$watch('foo2');
1617+
child.$new();
1618+
child.$new();
1619+
child = child.$new().$new().$new();
1620+
child.$watch('foo3');
1621+
child.$watch('foo4');
1622+
1623+
expect($rootScope.$countWatchers()).toBe(4);
1624+
}));
1625+
1626+
1627+
it('should provide the current count even after child destruction and watch deregistration',
1628+
inject(function($rootScope) {
1629+
1630+
var deregisterWatch1 = $rootScope.$watch('exp1');
1631+
1632+
var childScope = $rootScope.$new();
1633+
childScope.$watch('exp2');
1634+
1635+
expect($rootScope.$countWatchers()).toBe(2);
1636+
1637+
childScope.$destroy();
1638+
expect($rootScope.$countWatchers()).toBe(1);
1639+
1640+
deregisterWatch1();
1641+
expect($rootScope.$countWatchers()).toBe(0);
1642+
}));
1643+
1644+
1645+
it('should work with isolate scopes', inject(function($rootScope) {
1646+
/*
1647+
RS=1
1648+
|
1649+
CIS=1
1650+
/ \
1651+
GCS=1 GCIS=1
1652+
*/
1653+
1654+
$rootScope.$watch('exp1');
1655+
expect($rootScope.$countWatchers()).toBe(1);
1656+
1657+
var childIsolateScope = $rootScope.$new(true);
1658+
childIsolateScope.$watch('exp2');
1659+
expect($rootScope.$countWatchers()).toBe(2);
1660+
expect(childIsolateScope.$countWatchers()).toBe(1);
1661+
1662+
var grandChildScope = childIsolateScope.$new();
1663+
grandChildScope.$watch('exp3');
1664+
1665+
var grandChildIsolateScope = childIsolateScope.$new(true);
1666+
grandChildIsolateScope.$watch('exp4');
1667+
1668+
expect($rootScope.$countWatchers()).toBe(4);
1669+
expect(childIsolateScope.$countWatchers()).toBe(3);
1670+
expect(grandChildScope.$countWatchers()).toBe(1);
1671+
expect(grandChildIsolateScope.$countWatchers()).toBe(1);
1672+
1673+
childIsolateScope.$destroy();
1674+
expect($rootScope.$countWatchers()).toBe(1);
1675+
}));
1676+
});
1677+
});
15101678
});
15111679

15121680

0 commit comments

Comments
 (0)