/** * @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 {CommonModule} from '@angular/common'; import {Component, ContentChildren, Directive, Injectable, NO_ERRORS_SCHEMA, OnDestroy, QueryList, TemplateRef} from '@angular/core'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; describe('NgTemplateOutlet', () => { let fixture: ComponentFixture; function setTplRef(value: any): void { fixture.componentInstance.currentTplRef = value; } function detectChangesAndExpectText(text: string): void { fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText(text); } afterEach(() => { fixture = null as any; }); beforeEach(() => { TestBed.configureTestingModule({ declarations: [TestComponent, CaptureTplRefs, DestroyableCmpt], imports: [CommonModule], providers: [DestroyedSpyService] }); }); // https://github.com/angular/angular/issues/14778 it('should accept the component as the context', async(() => { const template = `` + `{{context.foo}}`; fixture = createTestComponent(template); detectChangesAndExpectText('bar'); })); it('should do nothing if templateRef is `null`', async(() => { const template = ``; fixture = createTestComponent(template); detectChangesAndExpectText(''); })); it('should insert content specified by TemplateRef', async(() => { const template = `foo` + ``; fixture = createTestComponent(template); detectChangesAndExpectText('foo'); })); it('should clear content if TemplateRef becomes `null`', async(() => { const template = `foo` + ``; fixture = createTestComponent(template); fixture.detectChanges(); const refs = fixture.debugElement.children[0].references!['refs']; setTplRef(refs.tplRefs.first); detectChangesAndExpectText('foo'); setTplRef(null); detectChangesAndExpectText(''); })); it('should swap content if TemplateRef changes', async(() => { const template = `foobar` + ``; fixture = createTestComponent(template); fixture.detectChanges(); const refs = fixture.debugElement.children[0].references!['refs']; setTplRef(refs.tplRefs.first); detectChangesAndExpectText('foo'); setTplRef(refs.tplRefs.last); detectChangesAndExpectText('bar'); })); it('should display template if context is `null`', async(() => { const template = `foo` + ``; fixture = createTestComponent(template); detectChangesAndExpectText('foo'); })); it('should reflect initial context and changes', async(() => { const template = `{{foo}}` + ``; fixture = createTestComponent(template); fixture.detectChanges(); detectChangesAndExpectText('bar'); fixture.componentInstance.context.foo = 'alter-bar'; detectChangesAndExpectText('alter-bar'); })); it('should reflect user defined `$implicit` property in the context', async(() => { const template = `{{ctx.foo}}` + ``; fixture = createTestComponent(template); fixture.componentInstance.context = {$implicit: {foo: 'bra'}}; detectChangesAndExpectText('bra'); })); it('should reflect context re-binding', async(() => { const template = `{{shawshank}}` + ``; fixture = createTestComponent(template); fixture.componentInstance.context = {shawshank: 'brooks'}; detectChangesAndExpectText('brooks'); fixture.componentInstance.context = {shawshank: 'was here'}; detectChangesAndExpectText('was here'); })); it('should update but not destroy embedded view when context values change', () => { const template = `:{{foo}}` + ``; fixture = createTestComponent(template); const spyService = fixture.debugElement.injector.get(DestroyedSpyService); detectChangesAndExpectText('Content to destroy:bar'); expect(spyService.destroyed).toBeFalsy(); fixture.componentInstance.value = 'baz'; detectChangesAndExpectText('Content to destroy:baz'); expect(spyService.destroyed).toBeFalsy(); }); it('should recreate embedded view when context shape changes', () => { const template = `:{{foo}}` + ``; fixture = createTestComponent(template); const spyService = fixture.debugElement.injector.get(DestroyedSpyService); detectChangesAndExpectText('Content to destroy:bar'); expect(spyService.destroyed).toBeFalsy(); fixture.componentInstance.context = {foo: 'baz', other: true}; detectChangesAndExpectText('Content to destroy:baz'); expect(spyService.destroyed).toBeTruthy(); }); it('should destroy embedded view when context value changes and templateRef becomes undefined', () => { const template = `:{{foo}}` + ``; fixture = createTestComponent(template); const spyService = fixture.debugElement.injector.get(DestroyedSpyService); detectChangesAndExpectText('Content to destroy:bar'); expect(spyService.destroyed).toBeFalsy(); fixture.componentInstance.value = 'baz'; detectChangesAndExpectText(''); expect(spyService.destroyed).toBeTruthy(); }); it('should not try to update null / undefined context when context changes but template stays the same', () => { const template = `{{foo}}` + ``; fixture = createTestComponent(template); detectChangesAndExpectText(''); fixture.componentInstance.value = 'baz'; detectChangesAndExpectText(''); }); it('should not try to update null / undefined context when template changes', () => { const template = `{{foo}}` + `{{foo}}` + ``; fixture = createTestComponent(template); detectChangesAndExpectText(''); fixture.componentInstance.value = 'baz'; detectChangesAndExpectText(''); }); it('should not try to update context on undefined view', () => { const template = `{{foo}}` + ``; fixture = createTestComponent(template); detectChangesAndExpectText(''); fixture.componentInstance.value = 'baz'; detectChangesAndExpectText(''); }); // https://github.com/angular/angular/issues/30801 it('should not throw if the context is left blank', () => { const template = ` test `; expect(() => { fixture = createTestComponent(template); detectChangesAndExpectText('test'); }).not.toThrow(); }); it('should not throw when switching from template to null and back to template', async(() => { const template = `foo` + ``; fixture = createTestComponent(template); fixture.detectChanges(); const refs = fixture.debugElement.children[0].references!['refs']; setTplRef(refs.tplRefs.first); detectChangesAndExpectText('foo'); setTplRef(null); detectChangesAndExpectText(''); expect(() => { setTplRef(refs.tplRefs.first); detectChangesAndExpectText('foo'); }).not.toThrow(); })); }); @Injectable() class DestroyedSpyService { destroyed = false; } @Component({selector: 'destroyable-cmpt', template: 'Content to destroy'}) class DestroyableCmpt implements OnDestroy { constructor(private _spyService: DestroyedSpyService) {} ngOnDestroy(): void { this._spyService.destroyed = true; } } @Directive({selector: 'tpl-refs', exportAs: 'tplRefs'}) class CaptureTplRefs { // TODO(issue/24571): remove '!'. @ContentChildren(TemplateRef) tplRefs!: QueryList>; } @Component({selector: 'test-cmp', template: ''}) class TestComponent { // TODO(issue/24571): remove '!'. currentTplRef!: TemplateRef; context: any = {foo: 'bar'}; value = 'bar'; } function createTestComponent(template: string): ComponentFixture { return TestBed.overrideComponent(TestComponent, {set: {template: template}}) .configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}) .createComponent(TestComponent); }