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

Lines changed: 0 additions & 1 deletion
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

Lines changed: 77 additions & 1 deletion
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

Lines changed: 51 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy