1+ import { NgModuleRef } from '@angular/core' ;
12import { 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