Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 571e323

Browse files
committed
fix(ngMock): prevent memory leak due to data attached to $rootElement
Starting with 88bb551, `ngMock` will attach the `$injector` to the `$rootElement`, but will never clean it up, resulting in a memory leak. Since a new `$rootElement` is created for every test, this leak causes Karma to crash on large test-suites. The problem was not detected by our internal tests, because we do our own clean-up in `testabilityPatch.js`. 88bb551 was revert with 1b8590a. This commit incorporates the changes from 88bb551 and prevents the memory leak, by cleaning up all data attached to `$rootElement` after each test. Fixes angular#14094 Closes angular#14098
1 parent bf11cf3 commit 571e323

File tree

4 files changed

+148
-17
lines changed

4 files changed

+148
-17
lines changed

src/jqLite.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,12 @@ function jqLiteHasData(node) {
196196
return false;
197197
}
198198

199+
function jqLiteCleanData(nodes) {
200+
for (var i = 0, ii = nodes.length; i < ii; i++) {
201+
jqLiteRemoveData(nodes[i]);
202+
}
203+
}
204+
199205
function jqLiteBuildFragment(html, context) {
200206
var tmp, tag, wrap,
201207
fragment = context.createDocumentFragment(),
@@ -594,7 +600,8 @@ function getAliasedAttrName(name) {
594600
forEach({
595601
data: jqLiteData,
596602
removeData: jqLiteRemoveData,
597-
hasData: jqLiteHasData
603+
hasData: jqLiteHasData,
604+
cleanData: jqLiteCleanData
598605
}, function(fn, name) {
599606
JQLite[name] = fn;
600607
});

src/ngMock/angular-mocks.js

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,12 @@ angular.mock.$Browser = function() {
127127
};
128128
angular.mock.$Browser.prototype = {
129129

130-
/**
131-
* @name $browser#poll
132-
*
133-
* @description
134-
* run all fns in pollFns
135-
*/
130+
/**
131+
* @name $browser#poll
132+
*
133+
* @description
134+
* run all fns in pollFns
135+
*/
136136
poll: function poll() {
137137
angular.forEach(this.pollFns, function(pollFn) {
138138
pollFn();
@@ -1879,10 +1879,12 @@ angular.mock.$RAFDecorator = ['$delegate', function($delegate) {
18791879
/**
18801880
*
18811881
*/
1882+
var originalRootElement;
18821883
angular.mock.$RootElementProvider = function() {
1883-
this.$get = function() {
1884-
return angular.element('<div ng-app></div>');
1885-
};
1884+
this.$get = ['$injector', function($injector) {
1885+
originalRootElement = angular.element('<div ng-app></div>').data('$injector', $injector);
1886+
return originalRootElement;
1887+
}];
18861888
};
18871889

18881890
/**
@@ -2296,6 +2298,7 @@ if (window.jasmine || window.mocha) {
22962298

22972299

22982300
(window.beforeEach || window.setup)(function() {
2301+
originalRootElement = null;
22992302
annotatedFunctions = [];
23002303
currentSpec = this;
23012304
});
@@ -2318,7 +2321,15 @@ if (window.jasmine || window.mocha) {
23182321
currentSpec = null;
23192322

23202323
if (injector) {
2321-
injector.get('$rootElement').off();
2324+
// Ensure `$rootElement` is instantiated, before checking `originalRootElement`
2325+
var $rootElement = injector.get('$rootElement');
2326+
var rootNode = $rootElement && $rootElement[0];
2327+
var cleanUpNodes = !originalRootElement ? [] : [originalRootElement[0]];
2328+
if (rootNode && (!originalRootElement || rootNode !== originalRootElement[0])) {
2329+
cleanUpNodes.push(rootNode);
2330+
}
2331+
angular.element.cleanData(cleanUpNodes);
2332+
23222333
}
23232334

23242335
// clean up jquery's fragment cache

test/helpers/testabilityPatch.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,8 @@ function dealoc(obj) {
117117
}
118118

119119
function cleanup(element) {
120-
element.off().removeData();
121-
if (window.jQuery) {
122-
// jQuery 2.x doesn't expose the cache storage; ensure all element data
123-
// is removed during its cleanup.
124-
jQuery.cleanData([element]);
125-
}
120+
angular.element.cleanData(element);
121+
126122
// Note: We aren't using element.contents() here. Under jQuery, element.contents() can fail
127123
// for IFRAME elements. jQuery explicitly uses (element.contentDocument ||
128124
// element.contentWindow.document) and both properties are null for IFRAMES that aren't attached

test/ngMock/angular-mocksSpec.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,6 +1596,10 @@ describe('ngMock', function() {
15961596
it('should create mock application root', inject(function($rootElement) {
15971597
expect($rootElement.text()).toEqual('');
15981598
}));
1599+
1600+
it('should attach the `$injector` to `$rootElement`', inject(function($injector, $rootElement) {
1601+
expect($rootElement.injector()).toBe($injector);
1602+
}));
15991603
});
16001604

16011605

@@ -2113,9 +2117,122 @@ describe('ngMockE2E', function() {
21132117
});
21142118
});
21152119

2120+
21162121
describe('make sure that we can create an injector outside of tests', function() {
21172122
//since some libraries create custom injectors outside of tests,
21182123
//we want to make sure that this is not breaking the internals of
21192124
//how we manage annotated function cleanup during tests. See #10967
21202125
angular.injector([function($injector) {}]);
21212126
});
2127+
2128+
2129+
describe('`afterEach` clean-up', function() {
2130+
describe('undecorated `$rootElement`', function() {
2131+
var prevRootElement;
2132+
var prevCleanDataSpy;
2133+
2134+
2135+
it('should set up spies so the next test can verify `$rootElement` was cleaned up', function() {
2136+
module(function($provide) {
2137+
$provide.decorator('$rootElement', function($delegate) {
2138+
prevRootElement = $delegate;
2139+
2140+
// Spy on `angular.element.cleanData()`, so the next test can verify
2141+
// that it has been called as necessary
2142+
prevCleanDataSpy = spyOn(angular.element, 'cleanData').andCallThrough();
2143+
2144+
return $delegate;
2145+
});
2146+
});
2147+
2148+
// Inject the `$rootElement` to ensure it has been created
2149+
inject(function($rootElement) {
2150+
expect($rootElement.injector()).toBeDefined();
2151+
});
2152+
});
2153+
2154+
2155+
it('should clean up `$rootElement` after each test', function() {
2156+
// One call is made by `testabilityPatch`'s `dealoc()`
2157+
// We want to verify the subsequent call, made by `angular-mocks`
2158+
expect(prevCleanDataSpy.callCount).toBe(2);
2159+
2160+
var cleanUpNodes = prevCleanDataSpy.calls[1].args[0];
2161+
expect(cleanUpNodes.length).toBe(1);
2162+
expect(cleanUpNodes[0]).toBe(prevRootElement[0]);
2163+
});
2164+
});
2165+
2166+
2167+
describe('decorated `$rootElement`', function() {
2168+
var prevOriginalRootElement;
2169+
var prevRootElement;
2170+
var prevCleanDataSpy;
2171+
2172+
2173+
it('should set up spies so the next text can verify `$rootElement` was cleaned up', function() {
2174+
module(function($provide) {
2175+
$provide.decorator('$rootElement', function($delegate) {
2176+
prevOriginalRootElement = $delegate;
2177+
2178+
// Mock `$rootElement` to be able to verify that the correct object is cleaned up
2179+
prevRootElement = angular.element('<div></div>');
2180+
2181+
// Spy on `angular.element.cleanData()`, so the next test can verify
2182+
// that it has been called as necessary
2183+
prevCleanDataSpy = spyOn(angular.element, 'cleanData').andCallThrough();
2184+
2185+
return prevRootElement;
2186+
});
2187+
});
2188+
2189+
// Inject the `$rootElement` to ensure it has been created
2190+
inject(function($rootElement) {
2191+
expect($rootElement).toBe(prevRootElement);
2192+
expect(prevOriginalRootElement.injector()).toBeDefined();
2193+
expect(prevRootElement.injector()).toBeUndefined();
2194+
2195+
// If we don't clean up `prevOriginalRootElement`-related data now, `testabilityPatch` will
2196+
// complain about a memory leak, because it doesn't clean up after the original
2197+
// `$rootElement`
2198+
// This is a false alarm, because `angular-mocks` would have cleaned up in a subsequent
2199+
// `afterEach` block
2200+
prevOriginalRootElement.removeData();
2201+
});
2202+
});
2203+
2204+
2205+
it('should clean up `$rootElement` (both original and decorated) after each test', function() {
2206+
// One call is made by `testabilityPatch`'s `dealoc()`
2207+
// We want to verify the subsequent call, made by `angular-mocks`
2208+
expect(prevCleanDataSpy.callCount).toBe(2);
2209+
2210+
var cleanUpNodes = prevCleanDataSpy.calls[1].args[0];
2211+
expect(cleanUpNodes.length).toBe(2);
2212+
expect(cleanUpNodes[0]).toBe(prevOriginalRootElement[0]);
2213+
expect(cleanUpNodes[1]).toBe(prevRootElement[0]);
2214+
});
2215+
});
2216+
2217+
2218+
describe('uninstantiated or falsy `$rootElement`', function() {
2219+
it('should not break if `$rootElement` was never instantiated', function() {
2220+
// Just an empty test to verify that `angular-mocks` doesn't break,
2221+
// when trying to clean up `$rootElement`, if `$rootElement` was never injected in the test
2222+
// (and thus never instantiated/created)
2223+
2224+
// Ensure the `$injector` is created - if there is no `$injector`, no clean-up takes places
2225+
inject(function() {});
2226+
});
2227+
2228+
2229+
it('should not break if the decorated `$rootElement` is falsy (e.g. `null`)', function() {
2230+
module(function($provide) {
2231+
$provide.value('$rootElement', null);
2232+
});
2233+
2234+
// Ensure the `$injector` is created - if there is no `$injector`, no clean-up takes places
2235+
inject(function() {});
2236+
});
2237+
});
2238+
});

0 commit comments

Comments
 (0)