/** * @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 {AsyncTestCompleter, beforeEach, beforeEachProviders, ddescribe, describe, iit, inject, it, xit,} from '@angular/core/testing/testing_internal'; import {TestComponentBuilder} from '@angular/core/testing'; import {ListWrapper} from '../../src/facade/collection'; import {Component, TemplateRef, ContentChild} from '@angular/core'; import {NgFor, NgIf} from '@angular/common'; import {expect} from '@angular/platform-browser/testing/matchers'; import {By} from '@angular/platform-browser/src/dom/debug/by'; export function main() { describe('ngFor', () => { const TEMPLATE = '
{{item.toString()}};
'; it('should reflect initial elements', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { tcb.overrideTemplate(TestComponent, TEMPLATE) .createAsync(TestComponent) .then((fixture) => { fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('1;2;'); async.done(); }); })); it('should reflect added elements', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { tcb.overrideTemplate(TestComponent, TEMPLATE) .createAsync(TestComponent) .then((fixture) => { fixture.detectChanges(); (fixture.debugElement.componentInstance.items).push(3); fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('1;2;3;'); async.done(); }); })); it('should reflect removed elements', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { tcb.overrideTemplate(TestComponent, TEMPLATE) .createAsync(TestComponent) .then((fixture) => { fixture.detectChanges(); ListWrapper.removeAt(fixture.debugElement.componentInstance.items, 1); fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('1;'); async.done(); }); })); it('should reflect moved elements', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { tcb.overrideTemplate(TestComponent, TEMPLATE) .createAsync(TestComponent) .then((fixture) => { fixture.detectChanges(); ListWrapper.removeAt(fixture.debugElement.componentInstance.items, 0); (fixture.debugElement.componentInstance.items).push(1); fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('2;1;'); async.done(); }); })); it('should reflect a mix of all changes (additions/removals/moves)', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { tcb.overrideTemplate(TestComponent, TEMPLATE) .createAsync(TestComponent) .then((fixture) => { fixture.debugElement.componentInstance.items = [0, 1, 2, 3, 4, 5]; fixture.detectChanges(); fixture.debugElement.componentInstance.items = [6, 2, 7, 0, 4, 8]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('6;2;7;0;4;8;'); async.done(); }); })); it('should iterate over an array of objects', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = ''; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { // INIT fixture.debugElement.componentInstance.items = [{'name': 'misko'}, {'name': 'shyam'}]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('misko;shyam;'); // GROW (fixture.debugElement.componentInstance.items).push({'name': 'adam'}); fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('misko;shyam;adam;'); // SHRINK ListWrapper.removeAt(fixture.debugElement.componentInstance.items, 2); ListWrapper.removeAt(fixture.debugElement.componentInstance.items, 0); fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('shyam;'); async.done(); }); })); it('should gracefully handle nulls', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = '
  • {{item}};
'; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText(''); async.done(); }); })); it('should gracefully handle ref changing to null and back', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { tcb.overrideTemplate(TestComponent, TEMPLATE) .createAsync(TestComponent) .then((fixture) => { fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('1;2;'); fixture.debugElement.componentInstance.items = null; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText(''); fixture.debugElement.componentInstance.items = [1, 2, 3]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('1;2;3;'); async.done(); }); })); it('should throw on non-iterable ref and suggest using an array', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { tcb.overrideTemplate(TestComponent, TEMPLATE).createAsync(TestComponent).then((fixture) => { fixture.debugElement.componentInstance.items = 'whaaa'; expect(() => fixture.detectChanges()) .toThrowError( /Cannot find a differ supporting object 'whaaa' of type 'string'. NgFor only supports binding to Iterables such as Arrays/); async.done(); }); })); it('should throw on ref changing to string', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { tcb.overrideTemplate(TestComponent, TEMPLATE) .createAsync(TestComponent) .then((fixture) => { fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('1;2;'); fixture.debugElement.componentInstance.items = 'whaaa'; expect(() => fixture.detectChanges()).toThrowError(); async.done(); }); })); it('should works with duplicates', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { tcb.overrideTemplate(TestComponent, TEMPLATE) .createAsync(TestComponent) .then((fixture) => { var a = new Foo(); fixture.debugElement.componentInstance.items = [a, a]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('foo;foo;'); async.done(); }); })); it('should repeat over nested arrays', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = '
' + '
' + '
' + '{{subitem}}-{{item.length}};' + '
|' + '
' + '
'; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { fixture.debugElement.componentInstance.items = [['a', 'b'], ['c']]; fixture.detectChanges(); fixture.detectChanges(); fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('a-2;b-2;|c-1;|'); fixture.debugElement.componentInstance.items = [['e'], ['f', 'g']]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('e-1;|f-2;g-2;|'); async.done(); }); })); it('should repeat over nested arrays with no intermediate element', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = '
'; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { fixture.debugElement.componentInstance.items = [['a', 'b'], ['c']]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('a-2;b-2;c-1;'); fixture.debugElement.componentInstance.items = [['e'], ['f', 'g']]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('e-1;f-2;g-2;'); async.done(); }); })); it('should repeat over nested ngIf that are the last node in the ngFor temlate', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = `
`; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { const el = fixture.debugElement.nativeElement; const items = [1]; fixture.debugElement.componentInstance.items = items; fixture.detectChanges(); expect(el).toHaveText('0|even|'); items.push(1); fixture.detectChanges(); expect(el).toHaveText('0|even|1|'); items.push(1); fixture.detectChanges(); expect(el).toHaveText('0|even|1|2|even|'); async.done(); }); })); it('should display indices correctly', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = '
{{i.toString()}}
'; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { fixture.debugElement.componentInstance.items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('0123456789'); fixture.debugElement.componentInstance.items = [1, 2, 6, 7, 4, 3, 5, 8, 9, 0]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('0123456789'); async.done(); }); })); it('should display first item correctly', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = '
{{isFirst.toString()}}
'; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { fixture.debugElement.componentInstance.items = [0, 1, 2]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('truefalsefalse'); fixture.debugElement.componentInstance.items = [2, 1]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('truefalse'); async.done(); }); })); it('should display last item correctly', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = '
{{isLast.toString()}}
'; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { fixture.debugElement.componentInstance.items = [0, 1, 2]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('falsefalsetrue'); fixture.debugElement.componentInstance.items = [2, 1]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('falsetrue'); async.done(); }); })); it('should display even items correctly', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = '
{{isEven.toString()}}
'; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { fixture.debugElement.componentInstance.items = [0, 1, 2]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('truefalsetrue'); fixture.debugElement.componentInstance.items = [2, 1]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('truefalse'); async.done(); }); })); it('should display odd items correctly', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = '
{{isOdd.toString()}}
'; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { fixture.debugElement.componentInstance.items = [0, 1, 2, 3]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('falsetruefalsetrue'); fixture.debugElement.componentInstance.items = [2, 1]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('falsetrue'); async.done(); }); })); it('should allow to use a custom template', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { tcb.overrideTemplate( TestComponent, '
') .overrideTemplate( ComponentUsingTestComponent, '
  • {{i}}: {{item}};
  • ') .createAsync(ComponentUsingTestComponent) .then((fixture) => { const testComponent = fixture.debugElement.children[0]; testComponent.componentInstance.items = ['a', 'b', 'c']; fixture.detectChanges(); expect(testComponent.nativeElement).toHaveText('0: a;1: b;2: c;'); async.done(); }); })); it('should use a default template if a custom one is null', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { tcb.overrideTemplate(TestComponent, `
    `) .overrideTemplate(ComponentUsingTestComponent, '') .createAsync(ComponentUsingTestComponent) .then((fixture) => { const testComponent = fixture.debugElement.children[0]; testComponent.componentInstance.items = ['a', 'b', 'c']; fixture.detectChanges(); expect(testComponent.nativeElement).toHaveText('0: a;1: b;2: c;'); async.done(); }); })); it('should use a custom template when both default and a custom one are present', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { tcb.overrideTemplate(TestComponent, `
    `) .overrideTemplate( ComponentUsingTestComponent, '
  • {{i}}: {{item}};
  • ') .createAsync(ComponentUsingTestComponent) .then((fixture) => { const testComponent = fixture.debugElement.children[0]; testComponent.componentInstance.items = ['a', 'b', 'c']; fixture.detectChanges(); expect(testComponent.nativeElement).toHaveText('0: a;1: b;2: c;'); async.done(); }); })); describe('track by', () => { it('should not replace tracked items', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = ``; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { var buildItemList = () => { fixture.debugElement.componentInstance.items = [{'id': 'a'}]; fixture.detectChanges(); return fixture.debugElement.queryAll(By.css('p'))[0]; }; var firstP = buildItemList(); var finalP = buildItemList(); expect(finalP.nativeElement).toBe(firstP.nativeElement); async.done(); }); })); it('should update implicit local variable on view', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = `
    `; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { fixture.debugElement.componentInstance.items = [{'id': 'a', 'color': 'blue'}]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('blue'); fixture.debugElement.componentInstance.items = [{'id': 'a', 'color': 'red'}]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('red'); async.done(); }); })); it('should move items around and keep them updated ', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = `
    `; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { fixture.debugElement.componentInstance.items = [{'id': 'a', 'color': 'blue'}, {'id': 'b', 'color': 'yellow'}]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('blueyellow'); fixture.debugElement.componentInstance.items = [{'id': 'b', 'color': 'orange'}, {'id': 'a', 'color': 'red'}]; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('orangered'); async.done(); }); })); it('should handle added and removed items properly when tracking by index', inject( [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const template = `
    `; tcb.overrideTemplate(TestComponent, template) .createAsync(TestComponent) .then((fixture) => { fixture.debugElement.componentInstance.items = ['a', 'b', 'c', 'd']; fixture.detectChanges(); fixture.debugElement.componentInstance.items = ['e', 'f', 'g', 'h']; fixture.detectChanges(); fixture.debugElement.componentInstance.items = ['e', 'f', 'h']; fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('efh'); async.done(); }); })); }); }); } class Foo { toString() { return 'foo'; } } @Component({selector: 'test-cmp', directives: [NgFor, NgIf], template: ''}) class TestComponent { @ContentChild(TemplateRef) contentTpl: TemplateRef; items: any; constructor() { this.items = [1, 2]; } trackById(index: number, item: any): string { return item['id']; } trackByIndex(index: number, item: any): number { return index; } } @Component({selector: 'outer-cmp', directives: [TestComponent], template: ''}) class ComponentUsingTestComponent { items: any; constructor() { this.items = [1, 2]; } }