Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 37bb320

Browse filesBrowse 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
Copy full SHA for 37bb320

File tree

3 files changed

+128
-2
lines changed
Filter options

3 files changed

+128
-2
lines changed

‎packages/core/src/application_ref.ts

Copy file name to clipboardExpand all lines: packages/core/src/application_ref.ts
-1Lines 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

Copy file name to clipboardExpand all lines: packages/core/test/acceptance/bootstrap_spec.ts
+77-1Lines 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

Copy file name to clipboardExpand all lines: packages/core/test/acceptance/component_spec.ts
+51Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.