From 17d87d4e10c877933e0a78cbe75031cbdba3d486 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 22 May 2019 15:40:41 -0700 Subject: [PATCH] test(ivy): Add acceptance tests for view insertion (#30625) This work is being done ahead of changes to how view insertion is done in Ivy in accordance with [this design document](https://hackmd.io/Ae3W_2pOQlKouu9YNy1t6A?view). The idea is to make sure we have acceptance tests ahead of that change. PR Close #30625 --- .../test/acceptance/view_insertion_spec.ts | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 packages/core/test/acceptance/view_insertion_spec.ts diff --git a/packages/core/test/acceptance/view_insertion_spec.ts b/packages/core/test/acceptance/view_insertion_spec.ts new file mode 100644 index 0000000000..e6cfb544d5 --- /dev/null +++ b/packages/core/test/acceptance/view_insertion_spec.ts @@ -0,0 +1,252 @@ +/** + * @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, EmbeddedViewRef, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; + +describe('view insertion', () => { + describe('of a simple template', () => { + it('should insert into an empty container, at the front, in the middle, and at the end', () => { + let _counter = 0; + + @Component({ + selector: 'increment-comp', + template: `created{{counter}}`, + }) + class IncrementComp { + counter = _counter++; + } + + @Component({ + template: ` + +
+ ` + }) + class App { + @ViewChild('container', {read: ViewContainerRef}) + container: ViewContainerRef = null !; + + @ViewChild('simple', {read: TemplateRef}) + simple: TemplateRef = null !; + + view0: EmbeddedViewRef = null !; + view1: EmbeddedViewRef = null !; + view2: EmbeddedViewRef = null !; + view3: EmbeddedViewRef = null !; + + constructor(public changeDetector: ChangeDetectorRef) {} + + ngAfterViewInit() { + // insert at the front + this.view1 = this.container.createEmbeddedView(this.simple); // "created0" + + // insert at the front again + this.view0 = this.container.createEmbeddedView(this.simple, {}, 0); // "created1" + + // insert at the end + this.view3 = this.container.createEmbeddedView(this.simple); // "created2" + + // insert in the middle + this.view2 = this.container.createEmbeddedView(this.simple, {}, 2); // "created3" + + // We need to run change detection here to avoid + // ExpressionChangedAfterItHasBeenCheckedError because of the value updating in + // increment-comp + this.changeDetector.detectChanges(); + } + } + + TestBed.configureTestingModule({ + declarations: [App, IncrementComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + + expect(app.container.indexOf(app.view0)).toBe(0); + expect(app.container.indexOf(app.view1)).toBe(1); + expect(app.container.indexOf(app.view2)).toBe(2); + expect(app.container.indexOf(app.view3)).toBe(3); + // The text in each component differs based on *when* it was created. + expect(fixture.nativeElement.textContent).toBe('created1created0created3created2'); + }); + }); + + describe('of an empty template', () => { + it('should insert into an empty container, at the front, in the middle, and at the end', () => { + @Component({ + template: ` + +
+ ` + }) + class App { + @ViewChild('container', {read: ViewContainerRef}) + container: ViewContainerRef = null !; + + @ViewChild('empty', {read: TemplateRef}) + empty: TemplateRef = null !; + + view0: EmbeddedViewRef = null !; + view1: EmbeddedViewRef = null !; + view2: EmbeddedViewRef = null !; + view3: EmbeddedViewRef = null !; + + ngAfterViewInit() { + // insert at the front + this.view1 = this.container.createEmbeddedView(this.empty); + + // insert at the front again + this.view0 = this.container.createEmbeddedView(this.empty, {}, 0); + + // insert at the end + this.view3 = this.container.createEmbeddedView(this.empty); + + // insert in the middle + this.view2 = this.container.createEmbeddedView(this.empty, {}, 2); + } + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + + expect(app.container.indexOf(app.view0)).toBe(0); + expect(app.container.indexOf(app.view1)).toBe(1); + expect(app.container.indexOf(app.view2)).toBe(2); + expect(app.container.indexOf(app.view3)).toBe(3); + }); + }); + + describe('of an ng-content projection', () => { + it('should insert into an empty container, at the front, in the middle, and at the end', () => { + @Component({ + selector: 'comp', + template: ` + +
+ ` + }) + class Comp { + @ViewChild('container', {read: ViewContainerRef}) + container: ViewContainerRef = null !; + + @ViewChild('projection', {read: TemplateRef}) + projection: TemplateRef = null !; + + view0: EmbeddedViewRef = null !; + view1: EmbeddedViewRef = null !; + view2: EmbeddedViewRef = null !; + view3: EmbeddedViewRef = null !; + + ngAfterViewInit() { + // insert at the front + this.view1 = this.container.createEmbeddedView(this.projection); + + // insert at the front again + this.view0 = this.container.createEmbeddedView(this.projection, {}, 0); + + // insert at the end + this.view3 = this.container.createEmbeddedView(this.projection); + + // insert in the middle + this.view2 = this.container.createEmbeddedView(this.projection, {}, 2); + } + } + + @Component({ + template: ` + test + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const comp = fixture.debugElement.query(By.directive(Comp)).injector.get(Comp); + + expect(comp.container.indexOf(comp.view0)).toBe(0); + expect(comp.container.indexOf(comp.view1)).toBe(1); + expect(comp.container.indexOf(comp.view2)).toBe(2); + expect(comp.container.indexOf(comp.view3)).toBe(3); + + // Both ViewEngine and Ivy only honor one of the inserted ng-content components, even though + // all are inserted. + expect(fixture.nativeElement.textContent).toBe('test'); + }); + }); + + describe('of another container like ngIf', () => { + it('should insert into an empty container, at the front, in the middle, and at the end', () => { + @Component({ + template: ` +
test
+
+ ` + }) + class App { + @ViewChild('container', {read: ViewContainerRef}) + container: ViewContainerRef = null !; + + @ViewChild('subContainer', {read: TemplateRef}) + subContainer: TemplateRef = null !; + + view0: EmbeddedViewRef = null !; + view1: EmbeddedViewRef = null !; + view2: EmbeddedViewRef = null !; + view3: EmbeddedViewRef = null !; + + constructor(public changeDetectorRef: ChangeDetectorRef) {} + + ngAfterViewInit() { + // insert at the front + this.view1 = this.container.createEmbeddedView(this.subContainer, null, 0); + + // insert at the front again + this.view0 = this.container.createEmbeddedView(this.subContainer, null, 0); + + // insert at the end + this.view3 = this.container.createEmbeddedView(this.subContainer, null, 2); + + // insert in the middle + this.view2 = this.container.createEmbeddedView(this.subContainer, null, 2); + + // We need to run change detection here to avoid + // ExpressionChangedAfterItHasBeenCheckedError because of the value getting passed to ngIf + // in the template. + this.changeDetectorRef.detectChanges(); + } + } + + TestBed.configureTestingModule({ + declarations: [App], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + + expect(app.container.indexOf(app.view0)).toBe(0); + expect(app.container.indexOf(app.view1)).toBe(1); + expect(app.container.indexOf(app.view2)).toBe(2); + expect(app.container.indexOf(app.view3)).toBe(3); + + expect(fixture.debugElement.queryAll(By.css('div.dynamic')).length).toBe(4); + }); + }); +});