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
This commit is contained in:
Pawel Kozlowski 2022-06-28 15:40:23 +02:00 committed by Dylan Hunn
parent 879ac84862
commit 2ea991633a
3 changed files with 192 additions and 456 deletions

View file

@ -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|<test-cmp></test-cmp>|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:

View file

@ -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: `<div #foo></div>`,
})
class DynamicCompWithViewQueries {
@ViewChildren('foo') fooList!: QueryList<ElementRef>;
}
@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|<ng-template #a>A</ng-template><ng-template #b>B</ng-template>|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('<footer></footer>');
});
it('should work on elements', () => {
@Component({
template: `
<header vcref></header>
<footer></footer>
`
})
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: `
<header-cmp vcref></header-cmp>
<footer></footer>
`
})
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');

View file

@ -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|
* <ng-template directive>A<ng-template>
* <ng-template directive>B<ng-template>
* |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('<div host="mark"></div>');
fixture.component.insert(DynamicComponent);
fixture.update();
expect(fixture.outerHtml)
.toBe('<div host="mark"></div><dynamic-cmpt>inserted dynamically</dynamic-cmpt>');
fixture.component.clear();
fixture.update();
expect(fixture.outerHtml).toBe('<div host="mark"></div>');
});
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('<div host="mark"></div>');
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<any>;
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<QueryList<any>>()) &&
(ctx.foo = tmp as QueryList<any>);
}
}
});
}
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,
/** <button (click)="onClick()"> Click me </button> */
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();
});
});
});