Skip to content

Commit efac27f

Browse files
committed
feat(tests): Add option to detect memory leaks in tests
1 parent a038a03 commit efac27f

File tree

2 files changed

+86
-7
lines changed

2 files changed

+86
-7
lines changed

projects/igniteui-angular/karma.watch.conf.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ module.exports = function (config) {
3838
colors: true,
3939
logLevel: config.LOG_INFO,
4040
autoWatch: true,
41-
browsers: ['Chrome'],
41+
browsers: ['ChromeWithGC'],
42+
customLaunchers: {
43+
ChromeWithGC: {
44+
base: 'Chrome',
45+
flags: ['--js-flags="--expose-gc"'],
46+
debug: false
47+
}
48+
},
4249
singleRun: false
4350
});
4451
};

projects/igniteui-angular/src/lib/test-utils/configure-suite.ts

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,42 @@
1+
import { NgModuleRef } from '@angular/core';
12
import { TestBed, getTestBed, ComponentFixture, waitForAsync } from '@angular/core/testing';
23

4+
const checkLeaks = 'gc' in window;
5+
6+
const debug = false;
7+
function debugLog(...args) {
8+
if (debug) {
9+
console.log(...args);
10+
}
11+
}
12+
13+
interface ConfigureOptions {
14+
/**
15+
* Check for memory leaks when the tests finishes.
16+
* Note, this only works in Chrome configurations with expose the gc.
17+
* Caveats: if there are pending (non-cancelled) timers or animation frames it may report false positives.
18+
*/
19+
checkLeaks?: boolean;
20+
}
21+
322
/**
423
* Per https://github.com/angular/angular/issues/12409#issuecomment-391087831
524
* Destroy fixtures after each, reset testing module after all
625
*
726
* @hidden
827
*/
928

10-
export const configureTestSuite = (configureAction?: () => TestBed) => {
29+
export const configureTestSuite = (configureActionOrOptions?: (() => TestBed) | ConfigureOptions, options: ConfigureOptions = {}) => {
30+
const configureAction = typeof configureActionOrOptions === 'function' ? configureActionOrOptions : undefined;
31+
options = (configureActionOrOptions && typeof configureActionOrOptions === 'object') ? configureActionOrOptions : options;
32+
options.checkLeaks = options.checkLeaks && checkLeaks;
33+
34+
let componentRefs: WeakRef<{}>[];
35+
const moduleRefs = new Set<NgModuleRef<any>>();
36+
1137
const testBed = getTestBed();
1238
const originReset = testBed.resetTestingModule;
39+
const originCreateComponent = testBed.createComponent;
1340

1441
const clearStyles = () => {
1542
document.querySelectorAll('style').forEach(tag => tag.remove());
@@ -21,7 +48,20 @@ export const configureTestSuite = (configureAction?: () => TestBed) => {
2148

2249
beforeAll(() => {
2350
testBed.resetTestingModule();
24-
testBed.resetTestingModule = () => testBed;
51+
testBed.resetTestingModule = () => {
52+
softResetTestingModule();
53+
return testBed;
54+
};
55+
56+
if (options.checkLeaks) {
57+
componentRefs = [];
58+
testBed.createComponent = function () {
59+
const fixture = originCreateComponent.apply(testBed, arguments);
60+
componentRefs.push(new WeakRef(fixture.componentInstance));
61+
return fixture;
62+
};
63+
}
64+
2565
jasmine.getEnv().allowRespy(true);
2666
});
2767

@@ -31,24 +71,56 @@ export const configureTestSuite = (configureAction?: () => TestBed) => {
3171
}));
3272
}
3373

34-
afterEach(() => {
74+
function reportLeaks() {
75+
gc();
76+
const leaks = componentRefs.map(ref => ref.deref()).filter(i => !!i);
77+
if (leaks.length > 0) {
78+
console.warn(`Detected ${leaks.length} leaks:`);
79+
const classNames = [...new Set(leaks.map(i => i.constructor.name))];
80+
for (const name of classNames) {
81+
const count = leaks.filter(i => i.constructor.name === name).length;
82+
console.warn(` · ${name}: ${count}`);
83+
}
84+
} else {
85+
debugLog('No leaks detected');
86+
}
87+
}
88+
89+
function softResetTestingModule() {
90+
debugLog("Soft-reset testing module");
3591
clearStyles();
3692
clearSVGContainer();
3793
(testBed as any)._activeFixtures.forEach((fixture: ComponentFixture<any>) => {
3894
const element = fixture.debugElement.nativeElement as HTMLElement;
3995
fixture.destroy();
96+
debugLog("Destroying fixture for component:", fixture.componentInstance.constructor.name);
4097
// If the fixture element ID changes, then it's not properly disposed
4198
element?.remove();
4299
});
100+
(testBed as any)._activeFixtures = [];
101+
43102
// reset ViewEngine TestBed
44103
(testBed as any)._instantiated = false;
104+
45105
// reset Ivy TestBed
46-
(testBed as any)._testModuleRef = null;
47-
});
106+
const moduleRef = testBed['_testModuleRef'];
107+
moduleRefs.add(moduleRef);
108+
testBed['_testModuleRef'] = null;
109+
}
48110

49111
afterAll(() => {
50112
testBed.resetTestingModule = originReset;
51-
testBed.resetTestingModule();
113+
debugLog(`Destroying ${moduleRefs.size} module refs`);
114+
for (const moduleRef of moduleRefs) {
115+
testBed['_testModuleRef'] = moduleRef;
116+
testBed.resetTestingModule();
117+
}
118+
moduleRefs.clear();
119+
120+
testBed.createComponent = originCreateComponent;
121+
if (options.checkLeaks) {
122+
reportLeaks();
123+
}
52124
});
53125
};
54126

0 commit comments

Comments
 (0)