From 2ea991633a4a6fb2c9f81eec656043b4e2a480ea Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Tue, 28 Jun 2022 15:40:23 +0200 Subject: [PATCH] test(core): convert view container TemplateFixture tests to TestBed (#46544) Convert all ViewContainerRef tests with the hand-written generated code to TestBed (and use code generated by the JiT compiler). PR Close #46544 --- .../core/test/acceptance/bootstrap_spec.ts | 28 +- .../acceptance/view_container_ref_spec.ts | 169 ++++++- .../test/render3/view_container_ref_spec.ts | 451 ------------------ 3 files changed, 192 insertions(+), 456 deletions(-) delete mode 100644 packages/core/test/render3/view_container_ref_spec.ts diff --git a/packages/core/test/acceptance/bootstrap_spec.ts b/packages/core/test/acceptance/bootstrap_spec.ts index 24c8acc54b8..1e8f1bd599a 100644 --- a/packages/core/test/acceptance/bootstrap_spec.ts +++ b/packages/core/test/acceptance/bootstrap_spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationRef, COMPILER_OPTIONS, Component, destroyPlatform, forwardRef, NgModule, NgZone, TestabilityRegistry, ViewEncapsulation} from '@angular/core'; -import {BrowserModule} from '@angular/platform-browser'; +import {ApplicationRef, COMPILER_OPTIONS, Component, destroyPlatform, forwardRef, NgModule, NgZone, TestabilityRegistry, ViewContainerRef, ViewEncapsulation} from '@angular/core'; +import {bootstrapApplication, BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {withBody} from '@angular/private/testing'; @@ -38,6 +38,30 @@ describe('bootstrap', () => { } })); + + it('should allow injecting VCRef into the root (bootstrapped) component', + withBody('before||after', async () => { + @Component({selector: 'dynamic-cmp', standalone: true, template: 'dynamic'}) + class DynamicCmp { + } + + @Component({selector: 'test-cmp', standalone: true, template: '(test)'}) + class TestCmp { + constructor(public vcRef: ViewContainerRef) {} + } + + expect(document.body.textContent).toEqual('before||after'); + + const appRef = await bootstrapApplication(TestCmp); + expect(document.body.textContent).toEqual('before|(test)|after'); + + appRef.components[0].instance.vcRef.createComponent(DynamicCmp); + expect(document.body.textContent).toEqual('before|(test)dynamic|after'); + + appRef.destroy(); + expect(document.body.textContent).toEqual('before||after'); + })); + describe('options', () => { function createComponentAndModule( options: diff --git a/packages/core/test/acceptance/view_container_ref_spec.ts b/packages/core/test/acceptance/view_container_ref_spec.ts index c12638115da..fcac7467774 100644 --- a/packages/core/test/acceptance/view_container_ref_spec.ts +++ b/packages/core/test/acceptance/view_container_ref_spec.ts @@ -8,7 +8,7 @@ import {CommonModule, DOCUMENT} from '@angular/common'; import {computeMsgId} from '@angular/compiler'; -import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, InjectionToken, Injector, Input, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, OnDestroy, OnInit, Pipe, PipeTransform, QueryList, Renderer2, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core'; +import {ChangeDetectorRef, Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, InjectionToken, Injector, Input, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, OnDestroy, OnInit, Pipe, PipeTransform, QueryList, Renderer2, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core'; import {isProceduralRenderer} from '@angular/core/src/render3/interfaces/renderer'; import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; import {ComponentFixture, TestBed, TestComponentRenderer} from '@angular/core/testing'; @@ -150,6 +150,31 @@ describe('ViewContainerRef', () => { expect(fixture.debugElement.nativeElement.innerHTML).toContain('Hello'); }); + it('should view queries in dynamically created components', () => { + @Component({ + selector: 'dynamic-cmpt-with-view-queries', + template: `
`, + }) + class DynamicCompWithViewQueries { + @ViewChildren('foo') fooList!: QueryList; + } + + @Component({ + selector: 'test-cmp', + template: ``, + }) + class TestCmp { + constructor(readonly vcRf: ViewContainerRef) {} + } + + const fixture = TestBed.createComponent(TestCmp); + const cmpRef = fixture.componentInstance.vcRf.createComponent(DynamicCompWithViewQueries); + fixture.detectChanges(); + + expect(cmpRef.instance.fooList.length).toBe(1); + expect(cmpRef.instance.fooList.first).toBeAnInstanceOf(ElementRef); + }); + describe('element namespaces', () => { function runTestWithSelectors(svgSelector: string, mathMLSelector: string) { it('should be set correctly for host elements of dynamically created components', () => { @@ -374,6 +399,33 @@ describe('ViewContainerRef', () => { expect(cmpt.c1.indexOf(viewRef)).toBe(-1); expect(cmpt.c2.indexOf(viewRef)).toBe(0); }); + + it('should add embedded views at the right position in the DOM tree (ng-template next to other ng-template)', + () => { + @Component({ + template: `before|AB|after` + }) + class TestCmp { + @ViewChild('a', {static: true}) ta!: TemplateRef<{}>; + @ViewChild('b', {static: true}) tb!: TemplateRef<{}>; + @ViewChild('a', {static: true, read: ViewContainerRef}) ca!: ViewContainerRef; + @ViewChild('b', {static: true, read: ViewContainerRef}) cb!: ViewContainerRef; + } + + const fixture = TestBed.createComponent(TestCmp); + const testCmpInstance = fixture.componentInstance; + + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('before||after'); + + testCmpInstance.cb.createEmbeddedView(testCmpInstance.tb); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('before|B|after'); + + testCmpInstance.ca.createEmbeddedView(testCmpInstance.ta); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('before|AB|after'); + }); }); describe('move', () => { @@ -685,7 +737,7 @@ describe('ViewContainerRef', () => { }); }); - describe('getters', () => { + describe('getters for the anchor node', () => { it('should work on templates', () => { @Component({ template: ` @@ -707,10 +759,56 @@ describe('ViewContainerRef', () => { // that the comment is a placeholder for a container. expect(vcRefDir.vcref.element.nativeElement.textContent).toEqual('container'); - expect(vcRefDir.vcref.injector.get(ElementRef).nativeElement.textContent); + expect(vcRefDir.vcref.injector.get(ElementRef).nativeElement.textContent) + .toEqual('container'); expect(getElementHtml(vcRefDir.vcref.parentInjector.get(ElementRef).nativeElement)) .toBe('
'); }); + + it('should work on elements', () => { + @Component({ + template: ` +
+
+ ` + }) + class TestComponent { + @ViewChild(VCRefDirective, {static: true}) vcRefDir!: VCRefDirective; + } + + TestBed.configureTestingModule({declarations: [VCRefDirective, TestComponent]}); + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + const vcref = fixture.componentInstance.vcRefDir.vcref; + + expect(vcref.element.nativeElement.tagName.toLowerCase()).toEqual('header'); + expect(vcref.injector.get(ElementRef).nativeElement.tagName.toLowerCase()).toEqual('header'); + }); + + it('should work on components', () => { + @Component({selector: 'header-cmp', template: ``}) + class HeaderCmp { + } + + @Component({ + template: ` + +
+ ` + }) + class TestComponent { + @ViewChild(VCRefDirective, {static: true}) vcRefDir!: VCRefDirective; + } + + TestBed.configureTestingModule({declarations: [HeaderCmp, VCRefDirective, TestComponent]}); + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + const vcref = fixture.componentInstance.vcRefDir.vcref; + + expect(vcref.element.nativeElement.tagName.toLowerCase()).toEqual('header-cmp'); + expect(vcref.injector.get(ElementRef).nativeElement.tagName.toLowerCase()) + .toEqual('header-cmp'); + }); }); describe('detach', () => { @@ -771,6 +869,30 @@ describe('ViewContainerRef', () => { expect(viewE.destroyed).toBeFalsy(); expect(ngDevMode!.rendererDestroyNode).toBe(0); }); + + it('should not throw when destroying a detached component view', () => { + @Component({selector: 'dynamic-cmp'}) + class DynamicCmp { + } + + @Component({selector: 'test-cmp'}) + class TestCmp { + constructor(public vcRef: ViewContainerRef) {} + } + + const fixture = TestBed.createComponent(TestCmp); + fixture.detectChanges(); + + const vcRef = fixture.componentInstance.vcRef; + const cmpRef = vcRef.createComponent(DynamicCmp); + fixture.detectChanges(); + + vcRef.detach(vcRef.indexOf(cmpRef.hostView)); + + expect(() => { + cmpRef.destroy(); + }).not.toThrow(); + }); }); describe('remove', () => { @@ -1391,6 +1513,47 @@ describe('ViewContainerRef', () => { expect(fixture.debugElement.nativeElement.innerHTML).toContain('Child Component'); }); + it('should return ComponentRef with ChangeDetectorRef attached to root view', () => { + @Component({selector: 'dynamic-cmp', template: ``}) + class DynamicCmp { + doCheckCount = 0; + + ngDoCheck() { + this.doCheckCount++; + } + } + + @Component({template: ``}) + class TestCmp { + constructor( + public viewContainerRef: ViewContainerRef, + public componentFactoryResolver: ComponentFactoryResolver) {} + } + + const fixture = TestBed.createComponent(TestCmp); + const testCmpInstance = fixture.componentInstance; + const cmpFactory = + testCmpInstance.componentFactoryResolver.resolveComponentFactory(DynamicCmp); + const dynamicCmpRef = testCmpInstance.viewContainerRef.createComponent(cmpFactory); + + // change detection didn't run at all + expect(dynamicCmpRef.instance.doCheckCount).toBe(0); + + // running change detection on the dynamicCmpRef level + dynamicCmpRef.changeDetectorRef.detectChanges(); + expect(dynamicCmpRef.instance.doCheckCount).toBe(1); + + // running change detection on the TestBed fixture level + fixture.changeDetectorRef.detectChanges(); + expect(dynamicCmpRef.instance.doCheckCount).toBe(2); + + // The injector should retrieve the change detector ref for DynamicComp. As such, + // the doCheck hook for DynamicComp should NOT run upon ref.detectChanges(). + const changeDetector = dynamicCmpRef.injector.get(ChangeDetectorRef); + changeDetector.detectChanges(); + expect(dynamicCmpRef.instance.doCheckCount).toBe(2); + }); + describe('createComponent using Type', () => { const TOKEN_A = new InjectionToken('A'); const TOKEN_B = new InjectionToken('B'); diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts deleted file mode 100644 index 9dfd64b2914..00000000000 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ /dev/null @@ -1,451 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {QueryFlags} from '@angular/core/src/render3/interfaces/query'; -import {HEADER_OFFSET} from '@angular/core/src/render3/interfaces/view'; - -import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ElementRef, QueryList, TemplateRef, ViewContainerRef, ViewRef} from '../../src/core'; -import {ViewEncapsulation} from '../../src/metadata'; -import {injectComponentFactoryResolver, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵlistener, ɵɵloadQuery, ɵɵqueryRefresh, ɵɵviewQuery} from '../../src/render3/index'; -import {ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all'; -import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {RElement} from '../../src/render3/interfaces/renderer_dom'; -import {getLView} from '../../src/render3/state'; -import {getNativeByIndex} from '../../src/render3/util/view_utils'; - -import {ComponentFixture, createComponent, TemplateFixture} from './render_util'; - - -const Component: typeof _Component = function(...args: any[]): any { - // In test we use @Component for documentation only so it's safe to mock out the implementation. - return () => undefined; -} as any; - - -describe('ViewContainerRef', () => { - let directiveInstance: DirectiveWithVCRef|null; - - beforeEach(() => directiveInstance = null); - - class DirectiveWithVCRef { - static ɵfac = () => directiveInstance = new DirectiveWithVCRef( - ɵɵdirectiveInject(ViewContainerRef as any), injectComponentFactoryResolver()) - - static ɵdir = ɵɵdefineDirective({ - type: DirectiveWithVCRef, - selectors: [['', 'vcref', '']], - inputs: {tplRef: 'tplRef', name: 'name'} - }); - - // TODO(issue/24571): remove '!'. - tplRef!: TemplateRef<{}>; - - name: string = ''; - - // injecting a ViewContainerRef to create a dynamic container in which embedded views will be - // created - constructor(public vcref: ViewContainerRef, public cfr: ComponentFactoryResolver) {} - } - - describe('API', () => { - describe('createEmbeddedView (incl. insert)', () => { - it('should add embedded views at the right position in the DOM tree (ng-template next to other ng-template)', - () => { - let directiveInstances: TestDirective[] = []; - - class TestDirective { - static ɵfac = - () => { - const instance = new TestDirective( - ɵɵdirectiveInject(ViewContainerRef as any), - ɵɵdirectiveInject(TemplateRef as any)); - - directiveInstances.push(instance); - - return instance; - } - - static ɵdir = ɵɵdefineDirective({ - type: TestDirective, - selectors: [['', 'testdir', '']], - }); - - constructor(private _vcRef: ViewContainerRef, private _tplRef: TemplateRef<{}>) {} - - insertTpl(ctx: {}) { - this._vcRef.createEmbeddedView(this._tplRef, ctx); - } - - remove(index?: number) { - this._vcRef.remove(index); - } - } - - function EmbeddedTemplateA(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'A'); - } - } - - function EmbeddedTemplateB(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'B'); - } - } - - /** - * before| - * A - * B - * |after - */ - class TestComponent { - // TODO(issue/24571): remove '!'. - testDir!: TestDirective; - static ɵfac = () => new TestComponent(); - static ɵcmp = ɵɵdefineComponent({ - type: TestComponent, - encapsulation: ViewEncapsulation.None, - selectors: [['test-cmp']], - decls: 4, - vars: 0, - consts: [['testdir', '']], - template: - (rf: RenderFlags, cmp: TestComponent) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'before|'); - ɵɵtemplate(1, EmbeddedTemplateA, 1, 0, 'ng-template', 0); - ɵɵtemplate(2, EmbeddedTemplateB, 1, 0, 'ng-template', 0); - ɵɵtext(3, '|after'); - } - }, - dependencies: [TestDirective] - }); - } - - const fixture = new ComponentFixture(TestComponent); - expect(fixture.html).toEqual('before||after'); - - directiveInstances![1].insertTpl({}); - expect(fixture.html).toEqual('before|B|after'); - - directiveInstances![0].insertTpl({}); - expect(fixture.html).toEqual('before|AB|after'); - }); - }); - - describe('createComponent', () => { - let templateExecutionCounter = 0; - - describe('ComponentRef', () => { - let dynamicComp!: DynamicComp; - - class AppComp { - constructor(public vcr: ViewContainerRef, public cfr: ComponentFactoryResolver) {} - - static ɵfac = - () => { - return new AppComp( - ɵɵdirectiveInject(ViewContainerRef as any), injectComponentFactoryResolver()); - } - - static ɵcmp = ɵɵdefineComponent({ - type: AppComp, - selectors: [['app-comp']], - decls: 0, - vars: 0, - template: (rf: RenderFlags, cmp: AppComp) => {} - }); - } - - class DynamicComp { - doCheckCount = 0; - - ngDoCheck() { - this.doCheckCount++; - } - - static ɵfac = () => dynamicComp = new DynamicComp(); - - static ɵcmp = ɵɵdefineComponent({ - type: DynamicComp, - selectors: [['dynamic-comp']], - decls: 0, - vars: 0, - template: (rf: RenderFlags, cmp: DynamicComp) => {} - }); - } - - it('should return ComponentRef with ChangeDetectorRef attached to root view', () => { - const fixture = new ComponentFixture(AppComp); - - const dynamicCompFactory = fixture.component.cfr.resolveComponentFactory(DynamicComp); - const ref = fixture.component.vcr.createComponent(dynamicCompFactory); - fixture.update(); - expect(dynamicComp.doCheckCount).toEqual(1); - - // The change detector ref should be attached to the root view that contains - // DynamicComp, so the doCheck hook for DynamicComp should run upon ref.detectChanges(). - ref.changeDetectorRef.detectChanges(); - expect(dynamicComp.doCheckCount).toEqual(2); - expect((ref.changeDetectorRef as any).context).toBeNull(); - }); - - it('should return ComponentRef that can retrieve component ChangeDetectorRef through its injector', - () => { - const fixture = new ComponentFixture(AppComp); - - const dynamicCompFactory = fixture.component.cfr.resolveComponentFactory(DynamicComp); - const ref = fixture.component.vcr.createComponent(dynamicCompFactory); - fixture.update(); - expect(dynamicComp.doCheckCount).toEqual(1); - - // The injector should retrieve the change detector ref for DynamicComp. As such, - // the doCheck hook for DynamicComp should NOT run upon ref.detectChanges(). - const changeDetector = ref.injector.get(ChangeDetectorRef); - changeDetector.detectChanges(); - expect(dynamicComp.doCheckCount).toEqual(1); - expect((changeDetector as any).context).toEqual(dynamicComp); - }); - - it('should not throw when destroying a reattached component', () => { - const fixture = new ComponentFixture(AppComp); - - const dynamicCompFactory = fixture.component.cfr.resolveComponentFactory(DynamicComp); - const ref = fixture.component.vcr.createComponent(dynamicCompFactory); - fixture.update(); - - fixture.component.vcr.detach(fixture.component.vcr.indexOf(ref.hostView)); - - expect(() => { - ref.destroy(); - }).not.toThrow(); - }); - }); - }); - - describe('getters', () => { - it('should work on elements', () => { - function createTemplate() { - ɵɵelement(0, 'header', 0); - ɵɵelement(1, 'footer'); - } - - new TemplateFixture({ - create: createTemplate, - decls: 2, - directives: [DirectiveWithVCRef], - consts: [['vcref', '']] - }); - - expect(directiveInstance!.vcref.element.nativeElement.tagName.toLowerCase()) - .toEqual('header'); - expect( - directiveInstance!.vcref.injector.get(ElementRef).nativeElement.tagName.toLowerCase()) - .toEqual('header'); - expect(() => directiveInstance!.vcref.parentInjector.get(ElementRef)).toThrow(); - }); - - it('should work on components', () => { - const HeaderComponent = - createComponent('header-cmp', function(rf: RenderFlags, ctx: any) {}); - - function createTemplate() { - ɵɵelement(0, 'header-cmp', 0); - ɵɵelement(1, 'footer'); - } - - new TemplateFixture({ - create: createTemplate, - decls: 2, - directives: [HeaderComponent, DirectiveWithVCRef], - consts: [['vcref', '']] - }); - - expect(directiveInstance!.vcref.element.nativeElement.tagName.toLowerCase()) - .toEqual('header-cmp'); - expect( - directiveInstance!.vcref.injector.get(ElementRef).nativeElement.tagName.toLowerCase()) - .toEqual('header-cmp'); - expect(() => directiveInstance!.vcref.parentInjector.get(ElementRef)).toThrow(); - }); - }); - }); - - describe('view engine compatibility', () => { - @Component({selector: 'app', template: ''}) - class AppCmpt { - static ɵfac = () => - new AppCmpt(ɵɵdirectiveInject(ViewContainerRef as any), injectComponentFactoryResolver()) - - static ɵcmp = ɵɵdefineComponent({ - type: AppCmpt, - selectors: [['app']], - decls: 0, - vars: 0, - template: (rf: RenderFlags, cmp: AppCmpt) => {} - }); - - constructor(private _vcRef: ViewContainerRef, private _cfResolver: ComponentFactoryResolver) { - } - - insert(comp: any) { - this._vcRef.createComponent(this._cfResolver.resolveComponentFactory(comp)); - } - - clear() { - this._vcRef.clear(); - } - - getVCRefParentInjector() { - return this._vcRef.parentInjector; - } - } - - // https://stackblitz.com/edit/angular-xxpffd?file=src%2Findex.html - it('should allow injecting VCRef into the root (bootstrapped) component', () => { - const DynamicComponent = - createComponent('dynamic-cmpt', function(rf: RenderFlags, parent: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'inserted dynamically'); - } - }, 1, 0); - - - const fixture = new ComponentFixture(AppCmpt); - expect(fixture.outerHtml).toBe('
'); - - fixture.component.insert(DynamicComponent); - fixture.update(); - expect(fixture.outerHtml) - .toBe('
inserted dynamically'); - - fixture.component.clear(); - fixture.update(); - expect(fixture.outerHtml).toBe('
'); - }); - - it('should allow getting the parentInjector of the VCRef which was injected into the root (bootstrapped) component', - () => { - const fixture = new ComponentFixture(AppCmpt, { - injector: { - get: (token: any) => { - if (token === 'foo') return 'bar'; - } - } - }); - expect(fixture.outerHtml).toBe('
'); - - const parentInjector = fixture.component.getVCRefParentInjector(); - expect(parentInjector.get('foo')).toEqual('bar'); - }); - - it('should support view queries for dynamically created components', () => { - let dynamicComp!: DynamicCompWithViewQueries; - let fooEl!: RElement; - - class DynamicCompWithViewQueries { - // @ViewChildren('foo') - foo!: QueryList; - - static ɵfac = () => dynamicComp = new DynamicCompWithViewQueries(); - static ɵcmp = ɵɵdefineComponent({ - type: DynamicCompWithViewQueries, - selectors: [['dynamic-cmpt-with-view-queries']], - decls: 2, - vars: 0, - consts: [['foo', ''], ['bar', '']], - template: - (rf: RenderFlags, ctx: DynamicCompWithViewQueries) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', 1, 0); - } - // testing only - fooEl = getNativeByIndex(HEADER_OFFSET, getLView()) as RElement; - }, - viewQuery: - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['foo'], QueryFlags.descendants); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadQuery>()) && - (ctx.foo = tmp as QueryList); - } - } - }); - } - - const fixture = new ComponentFixture(AppCmpt); - - fixture.component.insert(DynamicCompWithViewQueries); - fixture.update(); - - expect(dynamicComp.foo.first.nativeElement).toEqual(fooEl as any); - }); - }); - - describe('view destruction', () => { - class CompWithListenerThatDestroysItself { - constructor(private viewRef: ViewRef) {} - - onClick() {} - - ngOnDestroy() { - this.viewRef.destroy(); - } - - // We want the ViewRef, so we rely on the knowledge that `ViewRef` is actually given - // when injecting `ChangeDetectorRef`. - static ɵfac = () => - new CompWithListenerThatDestroysItself(ɵɵdirectiveInject(ChangeDetectorRef as any)) - - static ɵcmp = ɵɵdefineComponent({ - type: CompWithListenerThatDestroysItself, - selectors: [['comp-with-listener-and-on-destroy']], - decls: 2, - vars: 0, - /** */ - template: - function CompTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button'); - { - ɵɵlistener('click', function() { - return ctx.onClick(); - }); - ɵɵtext(1, 'Click me'); - } - ɵɵelementEnd(); - } - }, - }); - } - - - it('should not error when destroying a view with listeners twice', () => { - const CompWithChildListener = createComponent('test-app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp-with-listener-and-on-destroy'); - } - }, 1, 0, [CompWithListenerThatDestroysItself]); - - const fixture = new ComponentFixture(CompWithChildListener); - fixture.update(); - - // Destroying the parent view will also destroy all of its children views and call their - // onDestroy hooks. Here, our child view attempts to destroy itself *again* in its - // onDestroy. This test exists to verify that no errors are thrown when doing this. We want - // the test component to destroy its own view in onDestroy because the destroy hooks happen - // as a *part of* view destruction. We also ensure that the test component has at least one - // listener so that it runs the event listener cleanup code path. - fixture.destroy(); - }); - }); -});