diff --git a/packages/core/test/acceptance/lifecycle_spec.ts b/packages/core/test/acceptance/lifecycle_spec.ts index f5ce6fd7d8..55330ec033 100644 --- a/packages/core/test/acceptance/lifecycle_spec.ts +++ b/packages/core/test/acceptance/lifecycle_spec.ts @@ -6,8 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, Input, OnChanges, SimpleChanges} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Component, ComponentFactoryResolver, Directive, Input, NgModule, OnChanges, SimpleChanges, ViewChild, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; describe('ngOnChanges', () => { it('should correctly support updating one Input among many', () => { @@ -169,3 +171,494 @@ it('should call hooks after setting directives inputs', () => { fixture.detectChanges(); expect(log).toEqual(['doCheckB1', 'doCheckC1', 'doCheckB1', 'doCheckC1']); }); + +describe('onInit', () => { + it('should call onInit after inputs are the first time', () => { + const input1Values: string[] = []; + const input2Values: string[] = []; + + @Component({ + selector: 'my-comp', + template: `

test

`, + }) + class MyComponent { + @Input() + input1 = ''; + + @Input() + input2 = ''; + + ngOnInit() { + input1Values.push(this.input1); + input2Values.push(this.input2); + } + } + + @Component({ + template: ` + + `, + }) + class App { + value1 = 'a'; + value2 = 'b'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(input1Values).toEqual(['a']); + expect(input2Values).toEqual(['b']); + + fixture.componentInstance.value1 = 'c'; + fixture.componentInstance.value2 = 'd'; + fixture.detectChanges(); + + // Shouldn't be called again just because change detection ran. + expect(input1Values).toEqual(['a']); + expect(input2Values).toEqual(['b']); + }); + + it('should be called on root component', () => { + let onInitCalled = 0; + + @Component({template: ``}) + class App { + ngOnInit() { onInitCalled++; } + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(onInitCalled).toBe(1); + }); + + it('should call parent onInit before it calls child onInit', () => { + const initCalls: string[] = []; + + @Component({ + selector: `child-comp`, + template: `

child

`, + }) + class ChildComp { + ngOnInit() { initCalls.push('child'); } + } + + @Component({ + template: ``, + }) + class ParentComp { + ngOnInit() { initCalls.push('parent'); } + } + + TestBed.configureTestingModule({ + declarations: [ParentComp, ChildComp], + }); + const fixture = TestBed.createComponent(ParentComp); + fixture.detectChanges(); + + expect(initCalls).toEqual(['parent', 'child']); + }); + + it('should call all parent onInits across view before calling children onInits', () => { + const initCalls: string[] = []; + + @Component({ + selector: `child-comp`, + template: `

child

`, + }) + class ChildComp { + @Input() + name = ''; + + ngOnInit() { initCalls.push(`child of parent ${this.name}`); } + } + + @Component({ + selector: 'parent-comp', + template: ``, + }) + class ParentComp { + @Input() + name = ''; + + ngOnInit() { initCalls.push(`parent ${this.name}`); } + } + + @Component({ + template: ` + + + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, ParentComp, ChildComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initCalls).toEqual(['parent 1', 'parent 2', 'child of parent 1', 'child of parent 2']); + }); + + it('should call onInit every time a new view is created (if block)', () => { + let onInitCalls = 0; + + @Component({selector: 'my-comp', template: '

test

'}) + class MyComp { + ngOnInit() { onInitCalls++; } + } + + @Component({ + template: ` +
+ ` + }) + class App { + show = true; + } + TestBed.configureTestingModule({ + declarations: [App, MyComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(onInitCalls).toBe(1); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(onInitCalls).toBe(1); + + fixture.componentInstance.show = true; + fixture.detectChanges(); + + expect(onInitCalls).toBe(2); + }); + + it('should call onInit for children of dynamically created components', () => { + @Component({selector: 'my-comp', template: '

test

'}) + class MyComp { + onInitCalled = false; + + ngOnInit() { this.onInitCalled = true; } + } + + @Component({ + selector: 'dynamic-comp', + template: ` + + `, + }) + class DynamicComp { + } + + @Component({ + template: ` +
+ `, + }) + class App { + @ViewChild('container', {read: ViewContainerRef}) + viewContainerRef !: ViewContainerRef; + + constructor(public compFactoryResolver: ComponentFactoryResolver) {} + + createDynamicView() { + const dynamicCompFactory = this.compFactoryResolver.resolveComponentFactory(DynamicComp); + this.viewContainerRef.createComponent(dynamicCompFactory); + } + } + + // View Engine requires that DynamicComp be in entryComponents. + @NgModule({ + declarations: [App, MyComp, DynamicComp], + entryComponents: [DynamicComp, App], + }) + class AppModule { + } + + TestBed.configureTestingModule({imports: [AppModule]}); + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + fixture.componentInstance.createDynamicView(); + fixture.detectChanges(); + + const myComp = fixture.debugElement.query(By.directive(MyComp)).componentInstance; + expect(myComp.onInitCalled).toBe(true); + }); + + it('should call onInit in hosts before their content children', () => { + const initialized: string[] = []; + + @Component({ + selector: 'projected', + template: '', + }) + class Projected { + ngOnInit() { initialized.push('projected'); } + } + + @Component({ + selector: 'comp', + template: ``, + }) + class Comp { + ngOnInit() { initialized.push('comp'); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + ngOnInit() { initialized.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Projected], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initialized).toEqual(['app', 'comp', 'projected']); + }); + + + it('should call onInit in host and its content children before next host', () => { + const initialized: string[] = []; + + @Component({ + selector: 'projected', + template: '', + }) + class Projected { + @Input() + name = ''; + + ngOnInit() { initialized.push('projected ' + this.name); } + } + + @Component({ + selector: 'comp', + template: ``, + }) + class Comp { + @Input() + name = ''; + + ngOnInit() { initialized.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + + + + + ` + }) + class App { + ngOnInit() { initialized.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Projected], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initialized).toEqual(['app', 'comp 1', 'projected 1', 'comp 2', 'projected 2']); + }); + + it('should be called on directives after component', () => { + const initialized: string[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input('dir-name') + name = ''; + + ngOnInit() { initialized.push('dir ' + this.name); } + } + + @Component({ + selector: 'comp', + template: `

`, + }) + class Comp { + @Input() + name = ''; + + ngOnInit() { initialized.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + ` + }) + class App { + ngOnInit() { initialized.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initialized).toEqual(['app', 'comp 1', 'dir 1', 'comp 2', 'dir 2']); + }); + + it('should be called on directives on an element', () => { + const initialized: string[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input('dir-name') + name = ''; + + ngOnInit() { initialized.push('dir ' + this.name); } + } + + @Component({ + template: ` +

+

+ ` + }) + class App { + ngOnInit() { initialized.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initialized).toEqual(['app', 'dir 1', 'dir 2']); + }); + + + it('should call onInit properly in for loop', () => { + const initialized: string[] = []; + + @Component({ + selector: 'comp', + template: `

`, + }) + class Comp { + @Input() + name = ''; + + ngOnInit() { initialized.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + numbers = [2, 3, 4, 5, 6]; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initialized).toEqual([ + 'comp 0', 'comp 1', 'comp 2', 'comp 3', 'comp 4', 'comp 5', 'comp 6' + ]); + }); + + it('should call onInit properly in for loop with children', () => { + const initialized: string[] = []; + + @Component({ + selector: 'child', + template: `

`, + }) + class Child { + @Input() + name = ''; + + ngOnInit() { initialized.push('child of parent ' + this.name); } + } + + @Component({selector: 'parent', template: ''}) + class Parent { + @Input() + name = ''; + + ngOnInit() { initialized.push('parent ' + this.name); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + numbers = [2, 3, 4, 5, 6]; + } + + TestBed.configureTestingModule({ + declarations: [App, Child, Parent], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initialized).toEqual([ + // First the two root level components + 'parent 0', + 'parent 1', + + // Then our 5 embedded views + 'parent 2', + 'child of parent 2', + 'parent 3', + 'child of parent 3', + 'parent 4', + 'child of parent 4', + 'parent 5', + 'child of parent 5', + 'parent 6', + 'child of parent 6', + + // Then the children of the root level components + 'child of parent 0', + 'child of parent 1', + ]); + }); +}); diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index c7c9c465c4..868f185bb0 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -78,75 +78,6 @@ describe('lifecycles', () => { const directives = [Comp, Parent, ProjectedComp, Directive, NgIf]; - it('should call onInit method after inputs are set in creation mode (and not in update mode)', - () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'comp'); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'val', Δbind(ctx.val)); - } - }, 1, 1, directives); - - const fixture = new ComponentFixture(App); - fixture.update(); - expect(events).toEqual(['comp']); - - fixture.component.val = '2'; - fixture.update(); - expect(events).toEqual(['comp']); - }); - - it('should be called on root component in creation mode', () => { - const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]}); - expect(events).toEqual(['comp']); - - markDirty(comp); - requestAnimationFrame.flush(); - expect(events).toEqual(['comp']); - }); - - it('should call parent onInit before child onInit', () => { - /** - * - * parent temp: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'parent'); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['parent', 'comp']); - }); - - it('should call all parent onInits across view before calling children onInits', () => { - /** - * - * - * - * parent temp: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'parent'); - Δelement(1, 'parent'); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'val', 1); - Δselect(1); - ΔelementProperty(1, 'val', 2); - } - }, 2, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['parent1', 'parent2', 'comp1', 'comp2']); - }); - - it('should call onInit every time a new view is created (if block)', () => { /** * % if (!skip) { @@ -183,239 +114,6 @@ describe('lifecycles', () => { fixture.update(); expect(events).toEqual(['comp', 'comp']); }); - - - it('should call onInit every time a new view is created (ngIf)', () => { - - function IfTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'comp'); - } - } - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δtemplate(0, IfTemplate, 1, 0, 'comp', [AttributeMarker.Template, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'ngIf', Δbind(ctx.showing)); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - - fixture.component.showing = true; - fixture.update(); - expect(events).toEqual(['comp']); - - fixture.component.showing = false; - fixture.update(); - expect(events).toEqual(['comp']); - - fixture.component.showing = true; - fixture.update(); - expect(events).toEqual(['comp', 'comp']); - }); - - it('should call onInit for children of dynamically created components', () => { - let viewContainerComp !: ViewContainerComp; - - class ViewContainerComp { - constructor(public vcr: ViewContainerRef, public cfr: ComponentFactoryResolver) {} - - static ngComponentDef = ΔdefineComponent({ - type: ViewContainerComp, - selectors: [['view-container-comp']], - factory: () => viewContainerComp = new ViewContainerComp( - ΔdirectiveInject(ViewContainerRef as any), injectComponentFactoryResolver()), - consts: 0, - vars: 0, - template: (rf: RenderFlags, ctx: ViewContainerComp) => {} - }); - } - - const DynamicComp = createComponent('dynamic-comp', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - Δelement(0, 'comp'); - } - }, 1, 0, [Comp]); - - const fixture = new ComponentFixture(ViewContainerComp); - expect(events).toEqual([]); - - viewContainerComp.vcr.createComponent( - viewContainerComp.cfr.resolveComponentFactory(DynamicComp)); - fixture.update(); - expect(events).toEqual(['comp']); - }); - - it('should call onInit in hosts before their content children', () => { - /** - * - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'comp'); - { ΔelementStart(1, 'projected'); } - ΔelementEnd(); - } - }, 2, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp', 'projected']); - }); - - it('should call onInit in host and its content children before next host', () => { - /** - * - * - * - * - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'comp'); - { ΔelementStart(1, 'projected'); } - ΔelementEnd(); - ΔelementStart(2, 'comp'); - { ΔelementStart(3, 'projected'); } - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'val', 1); - Δselect(1); - ΔelementProperty(1, 'val', 1); - Δselect(2); - ΔelementProperty(2, 'val', 2); - Δselect(3); - ΔelementProperty(3, 'val', 2); - } - }, 4, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp1', 'projected1', 'comp2', 'projected2']); - }); - - it('should be called on directives after component', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'comp', ['dir', '']); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp', 'dir']); - - fixture.update(); - expect(events).toEqual(['comp', 'dir']); - }); - - it('should be called on directives on an element', () => { - /**
*/ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'div', ['dir', '']); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['dir']); - - fixture.update(); - expect(events).toEqual(['dir']); - }); - - it('should call onInit properly in for loop', () => { - /** - * - * % for (let j = 2; j < 5; j++) { - * - * % } - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'comp'); - Δcontainer(1); - Δelement(2, 'comp'); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'val', 1); - Δselect(2); - ΔelementProperty(2, 'val', 5); - ΔcontainerRefreshStart(1); - { - for (let j = 2; j < 5; j++) { - let rf1 = ΔembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - Δelement(0, 'comp'); - } - if (rf1 & RenderFlags.Update) { - ΔelementProperty(0, 'val', j); - } - ΔembeddedViewEnd(); - } - } - ΔcontainerRefreshEnd(); - } - }, 3, 0, directives); - - const fixture = new ComponentFixture(App); - // onInit is called top to bottom, so top level comps (1 and 5) are called - // before the comps inside the for loop's embedded view (2, 3, and 4) - expect(events).toEqual(['comp1', 'comp5', 'comp2', 'comp3', 'comp4']); - }); - - it('should call onInit properly in for loop with children', () => { - /** - * - * % for (let j = 2; j < 5; j++) { - * - * % } - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'parent'); - Δcontainer(1); - Δelement(2, 'parent'); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'val', 1); - Δselect(2); - ΔelementProperty(2, 'val', 5); - ΔcontainerRefreshStart(1); - { - for (let j = 2; j < 5; j++) { - let rf1 = ΔembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - Δelement(0, 'parent'); - } - if (rf1 & RenderFlags.Update) { - ΔelementProperty(0, 'val', j); - } - ΔembeddedViewEnd(); - } - } - ΔcontainerRefreshEnd(); - } - }, 3, 0, directives); - - const fixture = new ComponentFixture(App); - // onInit is called top to bottom, so top level comps (1 and 5) are called - // before the comps inside the for loop's embedded view (2, 3, and 4) - expect(events).toEqual([ - 'parent1', 'parent5', 'parent2', 'comp2', 'parent3', 'comp3', 'parent4', 'comp4', 'comp1', - 'comp5' - ]); - }); - }); describe('doCheck', () => {