diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts index d7f024bbb2..ace2cec25b 100644 --- a/packages/core/src/application_ref.ts +++ b/packages/core/src/application_ref.ts @@ -804,7 +804,6 @@ export class ApplicationRef { /** @internal */ ngOnDestroy() { - // TODO(alxhub): Dispose of the NgZone. this._views.slice().forEach((view) => view.destroy()); this._onMicrotaskEmptySubscription.unsubscribe(); } diff --git a/packages/core/test/acceptance/bootstrap_spec.ts b/packages/core/test/acceptance/bootstrap_spec.ts index 7024c3b6c5..020470d1a8 100644 --- a/packages/core/test/acceptance/bootstrap_spec.ts +++ b/packages/core/test/acceptance/bootstrap_spec.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {COMPILER_OPTIONS, Component, destroyPlatform, NgModule, ViewEncapsulation} from '@angular/core'; +import {ApplicationRef, COMPILER_OPTIONS, Component, destroyPlatform, NgModule, TestabilityRegistry, ViewEncapsulation} from '@angular/core'; +import {expect} from '@angular/core/testing/src/testing_internal'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {onlyInIvy, withBody} from '@angular/private/testing'; @@ -151,6 +152,81 @@ describe('bootstrap', () => { ngModuleRef.destroy(); })); + describe('ApplicationRef cleanup', () => { + it('should cleanup ApplicationRef when Injector is destroyed', + withBody('', async () => { + const TestModule = createComponentAndModule(); + + const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule); + const appRef = ngModuleRef.injector.get(ApplicationRef); + const testabilityRegistry = ngModuleRef.injector.get(TestabilityRegistry); + + expect(appRef.components.length).toBe(1); + expect(testabilityRegistry.getAllRootElements().length).toBe(1); + + ngModuleRef.destroy(); // also destroys an Injector instance. + + expect(appRef.components.length).toBe(0); + expect(testabilityRegistry.getAllRootElements().length).toBe(0); + })); + + it('should cleanup ApplicationRef when ComponentRef is destroyed', + withBody('', async () => { + const TestModule = createComponentAndModule(); + + const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule); + const appRef = ngModuleRef.injector.get(ApplicationRef); + const testabilityRegistry = ngModuleRef.injector.get(TestabilityRegistry); + const componentRef = appRef.components[0]; + + expect(appRef.components.length).toBe(1); + expect(testabilityRegistry.getAllRootElements().length).toBe(1); + + componentRef.destroy(); + + expect(appRef.components.length).toBe(0); + expect(testabilityRegistry.getAllRootElements().length).toBe(0); + })); + + it('should not throw in case ComponentRef is destroyed and Injector is destroyed after that', + withBody('', async () => { + const TestModule = createComponentAndModule(); + + const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule); + const appRef = ngModuleRef.injector.get(ApplicationRef); + const testabilityRegistry = ngModuleRef.injector.get(TestabilityRegistry); + const componentRef = appRef.components[0]; + + expect(appRef.components.length).toBe(1); + expect(testabilityRegistry.getAllRootElements().length).toBe(1); + + componentRef.destroy(); + ngModuleRef.destroy(); // also destroys an Injector instance. + + expect(appRef.components.length).toBe(0); + expect(testabilityRegistry.getAllRootElements().length).toBe(0); + })); + + it('should not throw in case Injector is destroyed and ComponentRef is destroyed after that', + withBody('', async () => { + const TestModule = createComponentAndModule(); + + const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule); + const appRef = ngModuleRef.injector.get(ApplicationRef); + const testabilityRegistry = ngModuleRef.injector.get(TestabilityRegistry); + const componentRef = appRef.components[0]; + + expect(appRef.components.length).toBe(1); + expect(testabilityRegistry.getAllRootElements().length).toBe(1); + + ngModuleRef.destroy(); // also destroys an Injector instance. + componentRef.destroy(); + + expect(appRef.components.length).toBe(0); + expect(testabilityRegistry.getAllRootElements().length).toBe(0); + })); + }); + onlyInIvy('options cannot be changed in Ivy').describe('changing bootstrap options', () => { beforeEach(() => { spyOn(console, 'error'); diff --git a/packages/core/test/acceptance/component_spec.ts b/packages/core/test/acceptance/component_spec.ts index 4023e77118..4637595771 100644 --- a/packages/core/test/acceptance/component_spec.ts +++ b/packages/core/test/acceptance/component_spec.ts @@ -303,6 +303,57 @@ describe('component', () => { expect(wrapperEls.length).toBe(2); // other elements are preserved }); + it('should invoke `onDestroy` callbacks of dynamically created component', () => { + let wasOnDestroyCalled = false; + @Component({ + selector: '[comp]', + template: 'comp content', + }) + class DynamicComponent { + } + + @NgModule({ + declarations: [DynamicComponent], + entryComponents: [DynamicComponent], // needed only for ViewEngine + }) + class TestModule { + } + + @Component({ + selector: 'button', + template: '
', + }) + class App { + @ViewChild('anchor', {read: ViewContainerRef}) anchor!: ViewContainerRef; + + constructor(private cfr: ComponentFactoryResolver, private injector: Injector) {} + + create() { + const factory = this.cfr.resolveComponentFactory(DynamicComponent); + const componentRef = factory.create(this.injector); + componentRef.onDestroy(() => { + wasOnDestroyCalled = true; + }); + this.anchor.insert(componentRef.hostView); + } + + clear() { + this.anchor.clear(); + } + } + + TestBed.configureTestingModule({imports: [TestModule], declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + // Add ComponentRef to ViewContainerRef instance. + fixture.componentInstance.create(); + // Clear ViewContainerRef to invoke `onDestroy` callbacks on ComponentRef. + fixture.componentInstance.clear(); + + expect(wasOnDestroyCalled).toBeTrue(); + }); + describe('invalid host element', () => { it('should throw when is used as a host element for a Component', () => { @Component({