Skip to content

Commit

Permalink
test(core): verify onDestroy callbacks are invoked when ComponentRe…
Browse files Browse the repository at this point in the history
…f 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
  • Loading branch information
AndrewKushnir authored and mhevery committed Dec 2, 2020
1 parent df27027 commit a55e01b
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 2 deletions.
1 change: 0 additions & 1 deletion packages/core/src/application_ref.ts
Expand Up @@ -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();
}
Expand Down
78 changes: 77 additions & 1 deletion packages/core/test/acceptance/bootstrap_spec.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -151,6 +152,81 @@ describe('bootstrap', () => {
ngModuleRef.destroy();
}));

describe('ApplicationRef cleanup', () => {
it('should cleanup ApplicationRef when Injector is destroyed',
withBody('<my-app></my-app>', 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('<my-app></my-app>', 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('<my-app></my-app>', 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('<my-app></my-app>', 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');
Expand Down
51 changes: 51 additions & 0 deletions packages/core/test/acceptance/component_spec.ts
Expand Up @@ -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: '<div id="app-root" #anchor></div>',
})
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 <ng-container> is used as a host element for a Component', () => {
@Component({
Expand Down

0 comments on commit a55e01b

Please sign in to comment.