/** * @license * Copyright Google Inc. 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 {ChangeDetectorRef, Component, Directive, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; import {Input} from '@angular/core/src/metadata'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; describe('projection', () => { function getElementHtml(element: HTMLElement) { return element.innerHTML.replace(//g, '') .replace(/\sng-reflect-\S*="[^"]*"/g, ''); } it('should project content', () => { @Component({selector: 'child', template: `
`}) class Child { } @Component({selector: 'parent', template: 'content'}) class Parent { } TestBed.configureTestingModule({declarations: [Parent, Child]}); const fixture = TestBed.createComponent(Parent); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toBe(`
content
`); }); it('should project content when is at a template root', () => { @Component({ selector: 'child', template: '', }) class Child { } @Component({ selector: 'parent', template: 'content', }) class Parent { } TestBed.configureTestingModule({declarations: [Parent, Child]}); const fixture = TestBed.createComponent(Parent); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toBe(`content`); }); it('should project content with siblings', () => { @Component({selector: 'child', template: ''}) class Child { } @Component({selector: 'parent', template: `before
content
after
`}) class Parent { } TestBed.configureTestingModule({declarations: [Parent, Child]}); const fixture = TestBed.createComponent(Parent); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toBe(`before
content
after
`); }); it('should be able to re-project content', () => { @Component({selector: 'grand-child', template: `
`}) class GrandChild { } @Component( {selector: 'child', template: ``}) class Child { } @Component({ selector: 'parent', template: `HelloWorld!`, }) class Parent { } TestBed.configureTestingModule({declarations: [Parent, Child, GrandChild]}); const fixture = TestBed.createComponent(Parent); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML) .toBe('
HelloWorld!
'); }); it('should project components', () => { @Component({ selector: 'child', template: `
`, }) class Child { } @Component({ selector: 'projected-comp', template: 'content', }) class ProjectedComp { } @Component({selector: 'parent', template: ``}) class Parent { } TestBed.configureTestingModule({declarations: [Parent, Child, ProjectedComp]}); const fixture = TestBed.createComponent(Parent); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML) .toBe('
content
'); }); it('should project components that have their own projection', () => { @Component({selector: 'child', template: `
`}) class Child { } @Component({selector: 'projected-comp', template: `

`}) class ProjectedComp { } @Component({ selector: 'parent', template: `
Some content
Other content
`, }) class Parent { } TestBed.configureTestingModule({declarations: [Parent, Child, ProjectedComp]}); const fixture = TestBed.createComponent(Parent); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML) .toBe( `

Some content
Other content

`); }); it('should project into dynamic views (with createEmbeddedView)', () => { @Component({ selector: 'child', template: `Before--After` }) class Child { showing = false; } @Component({selector: 'parent', template: `
A
Some text
`}) class Parent { } TestBed.configureTestingModule({declarations: [Parent, Child], imports: [CommonModule]}); const fixture = TestBed.createComponent(Parent); const childDebugEl = fixture.debugElement.query(By.directive(Child)); const childInstance = childDebugEl.injector.get(Child); const childElement = childDebugEl.nativeElement as HTMLElement; childInstance.showing = true; fixture.detectChanges(); expect(getElementHtml(childElement)).toBe(`Before-
A
Some text-After`); childInstance.showing = false; fixture.detectChanges(); expect(getElementHtml(childElement)).toBe(`Before--After`); childInstance.showing = true; fixture.detectChanges(); expect(getElementHtml(childElement)).toBe(`Before-
A
Some text-After`); }); it('should project into dynamic views with specific selectors', () => { @Component({ selector: 'child', template: ` Before- -After` }) class Child { showing = false; } @Component({ selector: 'parent', template: `
A
B
` }) class Parent { } TestBed.configureTestingModule({declarations: [Parent, Child], imports: [CommonModule]}); const fixture = TestBed.createComponent(Parent); const childDebugEl = fixture.debugElement.query(By.directive(Child)); const childInstance = childDebugEl.injector.get(Child); childInstance.showing = true; fixture.detectChanges(); expect(getElementHtml(fixture.nativeElement)) .toBe('B Before-
A
-After
'); childInstance.showing = false; fixture.detectChanges(); expect(getElementHtml(fixture.nativeElement)) .toBe('B Before- -After'); childInstance.showing = true; fixture.detectChanges(); expect(getElementHtml(fixture.nativeElement)) .toBe('B Before-
A
-After
'); }); it('should project if is in a template that has different declaration/insertion points', () => { @Component( {selector: 'comp', template: ``}) class Comp { @ViewChild(TemplateRef, {static: true}) template !: TemplateRef; } @Directive({selector: '[trigger]'}) class Trigger { @Input() trigger !: Comp; constructor(public vcr: ViewContainerRef) {} open() { this.vcr.createEmbeddedView(this.trigger.template); } } @Component({ selector: 'parent', template: ` Some content ` }) class Parent { } TestBed.configureTestingModule({declarations: [Parent, Trigger, Comp]}); const fixture = TestBed.createComponent(Parent); const trigger = fixture.debugElement.query(By.directive(Trigger)).injector.get(Trigger); fixture.detectChanges(); expect(getElementHtml(fixture.nativeElement)).toBe(``); trigger.open(); expect(getElementHtml(fixture.nativeElement)) .toBe(`Some content`); }); // https://stackblitz.com/edit/angular-ceqmnw?file=src%2Fapp%2Fapp.component.ts it('should project nodes into the last ng-content unrolled by ngFor', () => { @Component({ selector: 'child', template: `
({{i}}):
` }) class Child { } @Component({selector: 'parent', template: `content`}) class Parent { } TestBed.configureTestingModule({declarations: [Parent, Child], imports: [CommonModule]}); const fixture = TestBed.createComponent(Parent); fixture.detectChanges(); expect(getElementHtml(fixture.nativeElement)) .toBe('
(0):
(1):content
'); }); it('should handle projected containers inside other containers', () => { @Component({selector: 'nested-comp', template: `
Child content
`}) class NestedComp { } @Component({ selector: 'root-comp', template: ``, }) class RootComp { } @Component({ selector: 'my-app', template: ` ` }) class MyApp { items = [1, 2]; } TestBed.configureTestingModule( {declarations: [MyApp, RootComp, NestedComp], imports: [CommonModule]}); const fixture = TestBed.createComponent(MyApp); fixture.detectChanges(); // expecting # of divs to be (items.length - 1), since last element is filtered out by *ngIf, // this applies to all other assertions below expect(fixture.nativeElement.querySelectorAll('div').length).toBe(1); fixture.componentInstance.items = [3, 4, 5]; fixture.detectChanges(); expect(fixture.nativeElement.querySelectorAll('div').length).toBe(2); fixture.componentInstance.items = [6, 7, 8, 9]; fixture.detectChanges(); expect(fixture.nativeElement.querySelectorAll('div').length).toBe(3); }); it('should handle projection into element containers at the view root', () => { @Component({ selector: 'root-comp', template: ` `, }) class RootComp { @Input() show: boolean = true; } @Component({ selector: 'my-app', template: `
` }) class MyApp { show = true; } TestBed.configureTestingModule({declarations: [MyApp, RootComp]}); const fixture = TestBed.createComponent(MyApp); fixture.detectChanges(); expect(fixture.nativeElement.querySelectorAll('div').length).toBe(1); fixture.componentInstance.show = false; fixture.detectChanges(); expect(fixture.nativeElement.querySelectorAll('div').length).toBe(0); }); it('should handle projection of views with element containers at the root', () => { @Component({ selector: 'root-comp', template: ``, }) class RootComp { @Input() show: boolean = true; } @Component({ selector: 'my-app', template: `
` }) class MyApp { show = true; } TestBed.configureTestingModule({declarations: [MyApp, RootComp]}); const fixture = TestBed.createComponent(MyApp); fixture.detectChanges(); expect(fixture.nativeElement.querySelectorAll('div').length).toBe(1); fixture.componentInstance.show = false; fixture.detectChanges(); expect(fixture.nativeElement.querySelectorAll('div').length).toBe(0); }); it('should handle re-projection at the root of an embedded view', () => { @Component({ selector: 'child-comp', template: ``, }) class ChildComp { @Input() show: boolean = true; } @Component({ selector: 'parent-comp', template: `` }) class ParentComp { @Input() show: boolean = true; } @Component( {selector: 'my-app', template: `
`}) class MyApp { show = true; } TestBed.configureTestingModule({declarations: [MyApp, ParentComp, ChildComp]}); const fixture = TestBed.createComponent(MyApp); fixture.detectChanges(); expect(fixture.nativeElement.querySelectorAll('div').length).toBe(1); fixture.componentInstance.show = false; fixture.detectChanges(); expect(fixture.nativeElement.querySelectorAll('div').length).toBe(0); }); describe('with selectors', () => { // https://stackblitz.com/edit/angular-psokum?file=src%2Fapp%2Fapp.module.ts it('should project nodes where attribute selector matches a binding', () => { @Component({ selector: 'child', template: ``, }) class Child { } @Component({ selector: 'parent', template: `Has title` }) class Parent { } TestBed.configureTestingModule({declarations: [Child, Parent]}); const fixture = TestBed.createComponent(Parent); fixture.detectChanges(); expect(getElementHtml(fixture.nativeElement)) .toEqual('Has title'); }); it('should match selectors against projected containers', () => { @Component( {selector: 'child', template: ``}) class Child { } @Component({template: `
content
`}) class Parent { value = false; } TestBed.configureTestingModule({declarations: [Child, Parent]}); const fixture = TestBed.createComponent(Parent); fixture.componentInstance.value = true; fixture.detectChanges(); expect(getElementHtml(fixture.nativeElement)) .toEqual('
content
'); }); }); it('should handle projected containers inside other containers', () => { @Component({ selector: 'child-comp', // template: '' }) class ChildComp { } @Component({ selector: 'root-comp', // template: '' }) class RootComp { } @Component({ selector: 'my-app', template: ` {{ item }}| ` }) class MyApp { items: number[] = [1, 2, 3]; } TestBed.configureTestingModule({declarations: [ChildComp, RootComp, MyApp]}); const fixture = TestBed.createComponent(MyApp); fixture.detectChanges(); // expecting # of elements to be (items.length - 1), since last element is filtered out by // *ngIf, this applies to all other assertions below expect(fixture.nativeElement).toHaveText('1|2|'); fixture.componentInstance.items = [4, 5]; fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('4|'); fixture.componentInstance.items = [6, 7, 8, 9]; fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('6|7|8|'); }); it('should project content if the change detector has been detached', () => { @Component({selector: 'my-comp', template: ''}) class MyComp { constructor(changeDetectorRef: ChangeDetectorRef) { changeDetectorRef.detach(); } } @Component({ selector: 'my-app', template: `

hello

` }) class MyApp { } TestBed.configureTestingModule({declarations: [MyComp, MyApp]}); const fixture = TestBed.createComponent(MyApp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('hello'); }); it('should support ngProjectAs on elements (including )', () => { @Component({ selector: 'card', template: ` --- ` }) class Card { } @Component({ selector: 'card-with-title', template: `

Title

` }) class CardWithTitle { } @Component({ selector: 'app', template: ` content ` }) class App { } TestBed.configureTestingModule({declarations: [Card, CardWithTitle, App]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); // Compare the text output, because Ivy and ViewEngine produce slightly different HTML. expect(fixture.nativeElement.textContent).toContain('Title --- content'); }); it('should not match multiple selectors in ngProjectAs', () => { @Component({ selector: 'card', template: ` content ` }) class Card { } @Component({ template: `

Title

` }) class App { } TestBed.configureTestingModule({declarations: [Card, App]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); // Compare the text output, because Ivy and ViewEngine produce slightly different HTML. expect(fixture.nativeElement.textContent).not.toContain('Title content'); }); describe('on inline templates (e.g. *ngIf)', () => { it('should work when matching the element name', () => { let divDirectives = 0; @Component({selector: 'selector-proj', template: ''}) class SelectedNgContentComp { } @Directive({selector: 'div'}) class DivDirective { constructor() { divDirectives++; } } @Component({ selector: 'main-selector', template: '
Hello world!
' }) class SelectorMainComp { } TestBed.configureTestingModule( {declarations: [DivDirective, SelectedNgContentComp, SelectorMainComp]}); const fixture = TestBed.createComponent(SelectorMainComp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('Hello world!'); expect(divDirectives).toEqual(1); }); it('should work when matching attributes', () => { let xDirectives = 0; @Component({selector: 'selector-proj', template: ''}) class SelectedNgContentComp { } @Directive({selector: '[x]'}) class XDirective { constructor() { xDirectives++; } } @Component({ selector: 'main-selector', template: '
Hello world!
' }) class SelectorMainComp { } TestBed.configureTestingModule( {declarations: [XDirective, SelectedNgContentComp, SelectorMainComp]}); const fixture = TestBed.createComponent(SelectorMainComp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('Hello world!'); expect(xDirectives).toEqual(1); }); it('should work when matching classes', () => { let xDirectives = 0; @Component({selector: 'selector-proj', template: ''}) class SelectedNgContentComp { } @Directive({selector: '.x'}) class XDirective { constructor() { xDirectives++; } } @Component({ selector: 'main-selector', template: '
Hello world!
' }) class SelectorMainComp { } TestBed.configureTestingModule( {declarations: [XDirective, SelectedNgContentComp, SelectorMainComp]}); const fixture = TestBed.createComponent(SelectorMainComp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('Hello world!'); expect(xDirectives).toEqual(1); }); it('should ignore synthesized attributes (e.g. ngTrackBy)', () => { @Component( {selector: 'selector-proj', template: ''}) class SelectedNgContentComp { } @Component({ selector: 'main-selector', template: 'inline(
{{item.name}}
)' + 'ng-template(
{{item.name}}
)' }) class SelectorMainComp { items = [ {id: 1, name: 'one'}, {id: 2, name: 'two'}, {id: 3, name: 'three'}, ]; getItemId(item: {id: number}) { return item.id; } } TestBed.configureTestingModule({declarations: [SelectedNgContentComp, SelectorMainComp]}); const fixture = TestBed.createComponent(SelectorMainComp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('inline()ng-template(onetwothree)'); }); describe('on containers', () => { it('should work when matching attributes', () => { let xDirectives = 0; @Component({selector: 'selector-proj', template: ''}) class SelectedNgContentComp { } @Directive({selector: '[x]'}) class XDirective { constructor() { xDirectives++; } } @Component({ selector: 'main-selector', template: 'Hello world!' }) class SelectorMainComp { } TestBed.configureTestingModule( {declarations: [XDirective, SelectedNgContentComp, SelectorMainComp]}); const fixture = TestBed.createComponent(SelectorMainComp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('Hello world!'); expect(xDirectives).toEqual(1); }); it('should work when matching classes', () => { let xDirectives = 0; @Component({selector: 'selector-proj', template: ''}) class SelectedNgContentComp { } @Directive({selector: '.x'}) class XDirective { constructor() { xDirectives++; } } @Component({ selector: 'main-selector', template: 'Hello world!' }) class SelectorMainComp { } TestBed.configureTestingModule( {declarations: [XDirective, SelectedNgContentComp, SelectorMainComp]}); const fixture = TestBed.createComponent(SelectorMainComp); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('Hello world!'); expect(xDirectives).toEqual(1); }); }); }); });