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
This commit is contained in:
parent
df27027ecb
commit
a55e01b89c
|
@ -804,7 +804,6 @@ export class ApplicationRef {
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
// TODO(alxhub): Dispose of the NgZone.
|
|
||||||
this._views.slice().forEach((view) => view.destroy());
|
this._views.slice().forEach((view) => view.destroy());
|
||||||
this._onMicrotaskEmptySubscription.unsubscribe();
|
this._onMicrotaskEmptySubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {BrowserModule} from '@angular/platform-browser';
|
||||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
import {onlyInIvy, withBody} from '@angular/private/testing';
|
import {onlyInIvy, withBody} from '@angular/private/testing';
|
||||||
|
@ -151,6 +152,81 @@ describe('bootstrap', () => {
|
||||||
ngModuleRef.destroy();
|
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', () => {
|
onlyInIvy('options cannot be changed in Ivy').describe('changing bootstrap options', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(console, 'error');
|
spyOn(console, 'error');
|
||||||
|
|
|
@ -303,6 +303,57 @@ describe('component', () => {
|
||||||
expect(wrapperEls.length).toBe(2); // other elements are preserved
|
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', () => {
|
describe('invalid host element', () => {
|
||||||
it('should throw when <ng-container> is used as a host element for a Component', () => {
|
it('should throw when <ng-container> is used as a host element for a Component', () => {
|
||||||
@Component({
|
@Component({
|
||||||
|
|
Loading…
Reference in New Issue