Skip to content

Commit 37bb320

Browse files
AndrewKushnirmhevery
authored andcommitted
test(core): verify onDestroy callbacks are invoked when ComponentRef is destroyed (#39876)
This commit adds a few tests to verify that the `onDestroy` callbacks are invoked when `ComponentRef` instance is destroyed and the logic is consistent between ViewEngine and Ivy. PR Close #39876
1 parent ad93243 commit 37bb320

File tree

3 files changed

+128
-2
lines changed

3 files changed

+128
-2
lines changed

packages/core/src/application_ref.ts

-1
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,6 @@ export class ApplicationRef {
779779

780780
/** @internal */
781781
ngOnDestroy() {
782-
// TODO(alxhub): Dispose of the NgZone.
783782
this._views.slice().forEach((view) => view.destroy());
784783
}
785784

packages/core/test/acceptance/bootstrap_spec.ts

+77-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {COMPILER_OPTIONS, Component, destroyPlatform, NgModule, ViewEncapsulation} from '@angular/core';
9+
import {ApplicationRef, COMPILER_OPTIONS, Component, destroyPlatform, NgModule, TestabilityRegistry, ViewEncapsulation} from '@angular/core';
10+
import {expect} from '@angular/core/testing/src/testing_internal';
1011
import {BrowserModule} from '@angular/platform-browser';
1112
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
1213
import {onlyInIvy, withBody} from '@angular/private/testing';
@@ -151,6 +152,81 @@ describe('bootstrap', () => {
151152
ngModuleRef.destroy();
152153
}));
153154

155+
describe('ApplicationRef cleanup', () => {
156+
it('should cleanup ApplicationRef when Injector is destroyed',
157+
withBody('<my-app></my-app>', async () => {
158+
const TestModule = createComponentAndModule();
159+
160+
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule);
161+
const appRef = ngModuleRef.injector.get(ApplicationRef);
162+
const testabilityRegistry = ngModuleRef.injector.get(TestabilityRegistry);
163+
164+
expect(appRef.components.length).toBe(1);
165+
expect(testabilityRegistry.getAllRootElements().length).toBe(1);
166+
167+
ngModuleRef.destroy(); // also destroys an Injector instance.
168+
169+
expect(appRef.components.length).toBe(0);
170+
expect(testabilityRegistry.getAllRootElements().length).toBe(0);
171+
}));
172+
173+
it('should cleanup ApplicationRef when ComponentRef is destroyed',
174+
withBody('<my-app></my-app>', async () => {
175+
const TestModule = createComponentAndModule();
176+
177+
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule);
178+
const appRef = ngModuleRef.injector.get(ApplicationRef);
179+
const testabilityRegistry = ngModuleRef.injector.get(TestabilityRegistry);
180+
const componentRef = appRef.components[0];
181+
182+
expect(appRef.components.length).toBe(1);
183+
expect(testabilityRegistry.getAllRootElements().length).toBe(1);
184+
185+
componentRef.destroy();
186+
187+
expect(appRef.components.length).toBe(0);
188+
expect(testabilityRegistry.getAllRootElements().length).toBe(0);
189+
}));
190+
191+
it('should not throw in case ComponentRef is destroyed and Injector is destroyed after that',
192+
withBody('<my-app></my-app>', async () => {
193+
const TestModule = createComponentAndModule();
194+
195+
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule);
196+
const appRef = ngModuleRef.injector.get(ApplicationRef);
197+
const testabilityRegistry = ngModuleRef.injector.get(TestabilityRegistry);
198+
const componentRef = appRef.components[0];
199+
200+
expect(appRef.components.length).toBe(1);
201+
expect(testabilityRegistry.getAllRootElements().length).toBe(1);
202+
203+
componentRef.destroy();
204+
ngModuleRef.destroy(); // also destroys an Injector instance.
205+
206+
expect(appRef.components.length).toBe(0);
207+
expect(testabilityRegistry.getAllRootElements().length).toBe(0);
208+
}));
209+
210+
it('should not throw in case Injector is destroyed and ComponentRef is destroyed after that',
211+
withBody('<my-app></my-app>', async () => {
212+
const TestModule = createComponentAndModule();
213+
214+
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule);
215+
const appRef = ngModuleRef.injector.get(ApplicationRef);
216+
const testabilityRegistry = ngModuleRef.injector.get(TestabilityRegistry);
217+
const componentRef = appRef.components[0];
218+
219+
expect(appRef.components.length).toBe(1);
220+
expect(testabilityRegistry.getAllRootElements().length).toBe(1);
221+
222+
ngModuleRef.destroy(); // also destroys an Injector instance.
223+
componentRef.destroy();
224+
225+
expect(appRef.components.length).toBe(0);
226+
expect(testabilityRegistry.getAllRootElements().length).toBe(0);
227+
}));
228+
});
229+
154230
onlyInIvy('options cannot be changed in Ivy').describe('changing bootstrap options', () => {
155231
beforeEach(() => {
156232
spyOn(console, 'error');

packages/core/test/acceptance/component_spec.ts

+51
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,57 @@ describe('component', () => {
303303
expect(wrapperEls.length).toBe(2); // other elements are preserved
304304
});
305305

306+
it('should invoke `onDestroy` callbacks of dynamically created component', () => {
307+
let wasOnDestroyCalled = false;
308+
@Component({
309+
selector: '[comp]',
310+
template: 'comp content',
311+
})
312+
class DynamicComponent {
313+
}
314+
315+
@NgModule({
316+
declarations: [DynamicComponent],
317+
entryComponents: [DynamicComponent], // needed only for ViewEngine
318+
})
319+
class TestModule {
320+
}
321+
322+
@Component({
323+
selector: 'button',
324+
template: '<div id="app-root" #anchor></div>',
325+
})
326+
class App {
327+
@ViewChild('anchor', {read: ViewContainerRef}) anchor!: ViewContainerRef;
328+
329+
constructor(private cfr: ComponentFactoryResolver, private injector: Injector) {}
330+
331+
create() {
332+
const factory = this.cfr.resolveComponentFactory(DynamicComponent);
333+
const componentRef = factory.create(this.injector);
334+
componentRef.onDestroy(() => {
335+
wasOnDestroyCalled = true;
336+
});
337+
this.anchor.insert(componentRef.hostView);
338+
}
339+
340+
clear() {
341+
this.anchor.clear();
342+
}
343+
}
344+
345+
TestBed.configureTestingModule({imports: [TestModule], declarations: [App]});
346+
const fixture = TestBed.createComponent(App);
347+
fixture.detectChanges();
348+
349+
// Add ComponentRef to ViewContainerRef instance.
350+
fixture.componentInstance.create();
351+
// Clear ViewContainerRef to invoke `onDestroy` callbacks on ComponentRef.
352+
fixture.componentInstance.clear();
353+
354+
expect(wasOnDestroyCalled).toBeTrue();
355+
});
356+
306357
describe('invalid host element', () => {
307358
it('should throw when <ng-container> is used as a host element for a Component', () => {
308359
@Component({

0 commit comments

Comments
 (0)