diff --git a/packages/core/test/acceptance/lifecycle_spec.ts b/packages/core/test/acceptance/lifecycle_spec.ts index 611876f0fd..5175d3dd7d 100644 --- a/packages/core/test/acceptance/lifecycle_spec.ts +++ b/packages/core/test/acceptance/lifecycle_spec.ts @@ -3376,3 +3376,335 @@ describe('onDestroy', () => { expect(events).toEqual(['dir']); }); }); + +describe('hook order', () => { + let events: string[] = []; + + beforeEach(() => events = []); + + @Component({ + selector: 'comp', + template: `{{value}}
`, + }) + class Comp { + @Input() + value = ''; + + @Input() + name = ''; + + ngOnInit() { events.push(`${this.name} onInit`); } + + ngDoCheck() { events.push(`${this.name} doCheck`); } + + ngOnChanges() { events.push(`${this.name} onChanges`); } + + ngAfterContentInit() { events.push(`${this.name} afterContentInit`); } + + ngAfterContentChecked() { events.push(`${this.name} afterContentChecked`); } + + ngAfterViewInit() { events.push(`${this.name} afterViewInit`); } + + ngAfterViewChecked() { events.push(`${this.name} afterViewChecked`); } + + ngOnDestroy() { events.push(`${this.name} onDestroy`); } + } + + @Component({ + selector: 'parent', + template: + ``, + }) + class Parent extends Comp { + } + + it('should call all hooks in correct order', () => { + @Component({template: ``}) + class App { + value = 'a'; + + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'comp onChanges', + 'comp onInit', + 'comp doCheck', + 'comp afterContentInit', + 'comp afterContentChecked', + 'comp afterViewInit', + 'comp afterViewChecked', + ]); + + events.length = 0; + fixture.detectChanges(); + expect(events).toEqual([ + 'comp doCheck', + 'comp afterContentChecked', + 'comp afterViewChecked', + ]); + + events.length = 0; + fixture.componentInstance.value = 'b'; + fixture.detectChanges(); + expect(events).toEqual([ + 'comp onChanges', + 'comp doCheck', + 'comp afterContentChecked', + 'comp afterViewChecked', + ]); + + events.length = 0; + fixture.componentInstance.show = false; + fixture.detectChanges(); + expect(events).toEqual([ + 'comp onDestroy', + ]); + }); + + it('should call all hooks in correct order with children', () => { + @Component({ + template: ` +
+ + +
+ ` + }) + class App { + value = 'a'; + + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Parent, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'parent1 onChanges', + 'parent1 onInit', + 'parent1 doCheck', + 'parent2 onChanges', + 'parent2 onInit', + 'parent2 doCheck', + 'parent1 afterContentInit', + 'parent1 afterContentChecked', + 'parent2 afterContentInit', + 'parent2 afterContentChecked', + 'child of parent1 onChanges', + 'child of parent1 onInit', + 'child of parent1 doCheck', + 'child of parent1 afterContentInit', + 'child of parent1 afterContentChecked', + 'child of parent1 afterViewInit', + 'child of parent1 afterViewChecked', + 'child of parent2 onChanges', + 'child of parent2 onInit', + 'child of parent2 doCheck', + 'child of parent2 afterContentInit', + 'child of parent2 afterContentChecked', + 'child of parent2 afterViewInit', + 'child of parent2 afterViewChecked', + 'parent1 afterViewInit', + 'parent1 afterViewChecked', + 'parent2 afterViewInit', + 'parent2 afterViewChecked', + ]); + + events.length = 0; + fixture.componentInstance.value = 'b'; + fixture.detectChanges(); + + expect(events).toEqual([ + 'parent1 onChanges', + 'parent1 doCheck', + 'parent2 onChanges', + 'parent2 doCheck', + 'parent1 afterContentChecked', + 'parent2 afterContentChecked', + 'child of parent1 onChanges', + 'child of parent1 doCheck', + 'child of parent1 afterContentChecked', + 'child of parent1 afterViewChecked', + 'child of parent2 onChanges', + 'child of parent2 doCheck', + 'child of parent2 afterContentChecked', + 'child of parent2 afterViewChecked', + 'parent1 afterViewChecked', + 'parent2 afterViewChecked', + ]); + + events.length = 0; + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual([ + 'child of parent1 onDestroy', + 'child of parent2 onDestroy', + 'parent1 onDestroy', + 'parent2 onDestroy', + ]); + }); + + // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-ng + it('should call all hooks in correct order with view and content', () => { + @Component({ + template: ` +
+ + + + + + +
+ ` + }) + class App { + value = 'a'; + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Parent, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'parent1 onChanges', + 'parent1 onInit', + 'parent1 doCheck', + 'projected1 onChanges', + 'projected1 onInit', + 'projected1 doCheck', + 'parent2 onChanges', + 'parent2 onInit', + 'parent2 doCheck', + 'projected2 onChanges', + 'projected2 onInit', + 'projected2 doCheck', + 'projected1 afterContentInit', + 'projected1 afterContentChecked', + 'parent1 afterContentInit', + 'parent1 afterContentChecked', + 'projected2 afterContentInit', + 'projected2 afterContentChecked', + 'parent2 afterContentInit', + 'parent2 afterContentChecked', + 'child of parent1 onChanges', + 'child of parent1 onInit', + 'child of parent1 doCheck', + 'child of parent1 afterContentInit', + 'child of parent1 afterContentChecked', + 'child of parent1 afterViewInit', + 'child of parent1 afterViewChecked', + 'child of parent2 onChanges', + 'child of parent2 onInit', + 'child of parent2 doCheck', + 'child of parent2 afterContentInit', + 'child of parent2 afterContentChecked', + 'child of parent2 afterViewInit', + 'child of parent2 afterViewChecked', + 'projected1 afterViewInit', + 'projected1 afterViewChecked', + 'parent1 afterViewInit', + 'parent1 afterViewChecked', + 'projected2 afterViewInit', + 'projected2 afterViewChecked', + 'parent2 afterViewInit', + 'parent2 afterViewChecked', + ]); + + events.length = 0; + fixture.componentInstance.value = 'b'; + fixture.detectChanges(); + + expect(events).toEqual([ + 'parent1 onChanges', + 'parent1 doCheck', + 'projected1 onChanges', + 'projected1 doCheck', + 'parent2 onChanges', + 'parent2 doCheck', + 'projected2 onChanges', + 'projected2 doCheck', + 'projected1 afterContentChecked', + 'parent1 afterContentChecked', + 'projected2 afterContentChecked', + 'parent2 afterContentChecked', + 'child of parent1 onChanges', + 'child of parent1 doCheck', + 'child of parent1 afterContentChecked', + 'child of parent1 afterViewChecked', + 'child of parent2 onChanges', + 'child of parent2 doCheck', + 'child of parent2 afterContentChecked', + 'child of parent2 afterViewChecked', + 'projected1 afterViewChecked', + 'parent1 afterViewChecked', + 'projected2 afterViewChecked', + 'parent2 afterViewChecked', + ]); + + events.length = 0; + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual([ + 'child of parent1 onDestroy', + 'child of parent2 onDestroy', + 'projected1 onDestroy', + 'parent1 onDestroy', + 'projected2 onDestroy', + 'parent2 onDestroy', + ]); + }); +}); + +describe('non-regression', () => { + it('should call lifecycle hooks for directives active on ', () => { + let destroyed = false; + + @Directive({ + selector: '[onDestroyDir]', + }) + class OnDestroyDir { + ngOnDestroy() { destroyed = true; } + } + + @Component({ + template: ` + content + ` + }) + class App { + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, OnDestroyDir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(destroyed).toBeFalsy(); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(destroyed).toBeTruthy(); + }); +}); diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index 0c3c35edca..b0883302c2 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentFactoryResolver, OnDestroy, SimpleChange, SimpleChanges, ViewContainerRef} from '../../src/core'; -import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, injectComponentFactoryResolver, ΔNgOnChangesFeature, ΔdefineComponent, ΔdefineDirective} from '../../src/render3/index'; -import {markDirty, Δbind, Δcontainer, ΔcontainerRefreshEnd, ΔcontainerRefreshStart, ΔdirectiveInject, Δelement, ΔelementEnd, ΔelementProperty, ΔelementStart, ΔembeddedViewEnd, ΔembeddedViewStart, Δlistener, Δprojection, ΔprojectionDef, Δselect, Δtemplate, Δtext} from '../../src/render3/instructions/all'; +import {OnDestroy} from '../../src/core'; +import {AttributeMarker, ComponentTemplate, ΔNgOnChangesFeature, ΔdefineComponent, ΔdefineDirective} from '../../src/render3/index'; +import {Δbind, Δcontainer, ΔcontainerRefreshEnd, ΔcontainerRefreshStart, Δelement, ΔelementEnd, ΔelementProperty, ΔelementStart, ΔembeddedViewEnd, ΔembeddedViewStart, Δprojection, ΔprojectionDef, Δselect, Δtemplate, Δtext} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {NgIf} from './common_with_def'; -import {ComponentFixture, containerEl, createComponent, renderComponent, renderToHtml, requestAnimationFrame} from './render_util'; +import {ComponentFixture, createComponent} from './render_util'; describe('lifecycles', () => { @@ -32,7 +32,7 @@ describe('lifecycles', () => { beforeEach(() => { events = []; }); - let Comp = createOnInitComponent('comp', (rf: RenderFlags, ctx: any) => { + let Comp = createOnInitComponent('comp', (rf: RenderFlags) => { if (rf & RenderFlags.Create) { ΔprojectionDef(); ΔelementStart(0, 'div'); @@ -41,7 +41,7 @@ describe('lifecycles', () => { } }, 2); let Parent = createOnInitComponent('parent', getParentTemplate('comp'), 1, 1, [Comp]); - let ProjectedComp = createOnInitComponent('projected', (rf: RenderFlags, ctx: any) => { + let ProjectedComp = createOnInitComponent('projected', (rf: RenderFlags) => { if (rf & RenderFlags.Create) { Δtext(0, 'content'); } @@ -115,277 +115,4 @@ describe('lifecycles', () => { expect(events).toEqual(['comp', 'comp']); }); }); - - describe('hook order', () => { - let events: string[]; - - beforeEach(() => { events = []; }); - - function createAllHooksComponent( - name: string, template: ComponentTemplate, consts: number = 0, vars: number = 0, - directives: any[] = []) { - return class Component { - val: string = ''; - - ngOnChanges() { events.push(`changes ${name}${this.val}`); } - - ngOnInit() { events.push(`init ${name}${this.val}`); } - ngDoCheck() { events.push(`check ${name}${this.val}`); } - - ngAfterContentInit() { events.push(`contentInit ${name}${this.val}`); } - ngAfterContentChecked() { events.push(`contentCheck ${name}${this.val}`); } - - ngAfterViewInit() { events.push(`viewInit ${name}${this.val}`); } - ngAfterViewChecked() { events.push(`viewCheck ${name}${this.val}`); } - - static ngComponentDef = ΔdefineComponent({ - type: Component, - selectors: [[name]], - factory: () => new Component(), - consts: consts, - vars: vars, - inputs: {val: 'val'}, template, - directives: directives, - features: [ΔNgOnChangesFeature()], - }); - }; - } - - it('should call all hooks in correct order', () => { - const Comp = createAllHooksComponent('comp', (rf: RenderFlags, ctx: any) => {}); - - /** - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'comp'); - Δelement(1, 'comp'); - } - // This template function is a little weird in that the `elementProperty` calls - // below are directly setting values `1` and `2`, where normally there would be - // a call to `bind()` that would do the work of seeing if something changed. - // This means when `fixture.update()` is called below, ngOnChanges should fire, - // even though the *value* itself never changed. - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'val', 1); - Δselect(1); - ΔelementProperty(1, 'val', 2); - } - }, 2, 0, [Comp]); - - const fixture = new ComponentFixture(App); - expect(events).toEqual([ - 'changes comp1', 'init comp1', 'check comp1', 'changes comp2', 'init comp2', 'check comp2', - 'contentInit comp1', 'contentCheck comp1', 'contentInit comp2', 'contentCheck comp2', - 'viewInit comp1', 'viewCheck comp1', 'viewInit comp2', 'viewCheck comp2' - ]); - - events = []; - fixture.update(); // Changes are made due to lack of `bind()` call in template fn. - expect(events).toEqual([ - 'changes comp1', 'check comp1', 'changes comp2', 'check comp2', 'contentCheck comp1', - 'contentCheck comp2', 'viewCheck comp1', 'viewCheck comp2' - ]); - }); - - it('should call all hooks in correct order with children', () => { - const Comp = createAllHooksComponent('comp', (rf: RenderFlags, ctx: any) => {}); - - /** */ - const Parent = createAllHooksComponent('parent', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - Δelement(0, 'comp'); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'val', Δbind(ctx.val)); - } - }, 1, 1, [Comp]); - - /** - * - * - */ - 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, [Parent]); - - const fixture = new ComponentFixture(App); - expect(events).toEqual([ - 'changes parent1', 'init parent1', 'check parent1', - 'changes parent2', 'init parent2', 'check parent2', - 'contentInit parent1', 'contentCheck parent1', 'contentInit parent2', - 'contentCheck parent2', 'changes comp1', 'init comp1', - 'check comp1', 'contentInit comp1', 'contentCheck comp1', - 'viewInit comp1', 'viewCheck comp1', 'changes comp2', - 'init comp2', 'check comp2', 'contentInit comp2', - 'contentCheck comp2', 'viewInit comp2', 'viewCheck comp2', - 'viewInit parent1', 'viewCheck parent1', 'viewInit parent2', - 'viewCheck parent2' - ]); - - events = []; - fixture.update(); - expect(events).toEqual([ - 'changes parent1', 'check parent1', 'changes parent2', 'check parent2', - 'contentCheck parent1', 'contentCheck parent2', 'check comp1', 'contentCheck comp1', - 'viewCheck comp1', 'check comp2', 'contentCheck comp2', 'viewCheck comp2', - 'viewCheck parent1', 'viewCheck parent2' - ]); - - }); - - // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-ng - it('should call all hooks in correct order with view and content', () => { - const Content = createAllHooksComponent('content', (rf: RenderFlags, ctx: any) => {}); - - const View = createAllHooksComponent('view', (rf: RenderFlags, ctx: any) => {}); - - /** */ - const Parent = createAllHooksComponent('parent', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ΔprojectionDef(); - Δprojection(0); - Δelement(1, 'view'); - } - if (rf & RenderFlags.Update) { - Δselect(1); - ΔelementProperty(1, 'val', Δbind(ctx.val)); - } - }, 2, 1, [View]); - - /** - * - * - * - * - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'parent'); - { Δelement(1, 'content'); } - ΔelementEnd(); - ΔelementStart(2, 'parent'); - { Δelement(3, 'content'); } - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'val', Δbind(1)); - Δselect(1); - ΔelementProperty(1, 'val', Δbind(1)); - Δselect(2); - ΔelementProperty(2, 'val', Δbind(2)); - Δselect(3); - ΔelementProperty(3, 'val', Δbind(2)); - } - }, 4, 4, [Parent, Content]); - - const fixture = new ComponentFixture(App); - expect(events).toEqual([ - 'changes parent1', 'init parent1', - 'check parent1', 'changes content1', - 'init content1', 'check content1', - 'changes parent2', 'init parent2', - 'check parent2', 'changes content2', - 'init content2', 'check content2', - 'contentInit content1', 'contentCheck content1', - 'contentInit parent1', 'contentCheck parent1', - 'contentInit content2', 'contentCheck content2', - 'contentInit parent2', 'contentCheck parent2', - 'changes view1', 'init view1', - 'check view1', 'contentInit view1', - 'contentCheck view1', 'viewInit view1', - 'viewCheck view1', 'changes view2', - 'init view2', 'check view2', - 'contentInit view2', 'contentCheck view2', - 'viewInit view2', 'viewCheck view2', - 'viewInit content1', 'viewCheck content1', - 'viewInit parent1', 'viewCheck parent1', - 'viewInit content2', 'viewCheck content2', - 'viewInit parent2', 'viewCheck parent2' - ]); - - events = []; - fixture.update(); - expect(events).toEqual([ - 'check parent1', 'check content1', 'check parent2', 'check content2', - 'contentCheck content1', 'contentCheck parent1', 'contentCheck content2', - 'contentCheck parent2', 'check view1', 'contentCheck view1', 'viewCheck view1', - 'check view2', 'contentCheck view2', 'viewCheck view2', 'viewCheck content1', - 'viewCheck parent1', 'viewCheck content2', 'viewCheck parent2' - ]); - - }); - - }); - - describe('non-regression', () => { - - it('should call lifecycle hooks for directives active on ', () => { - let destroyed = false; - - class OnDestroyDirective implements OnDestroy { - ngOnDestroy() { destroyed = true; } - - static ngDirectiveDef = ΔdefineDirective({ - type: OnDestroyDirective, - selectors: [['', 'onDestroyDirective', '']], - factory: () => new OnDestroyDirective() - }); - } - - - function conditionTpl(rf: RenderFlags, ctx: Cmpt) { - if (rf & RenderFlags.Create) { - Δtemplate(0, null, 0, 1, 'ng-template', [AttributeMarker.Bindings, 'onDestroyDirective']); - } - } - - /** - * - * - * - */ - function cmptTpl(rf: RenderFlags, cmpt: Cmpt) { - if (rf & RenderFlags.Create) { - Δtemplate(0, conditionTpl, 1, 1, 'ng-template', [AttributeMarker.Bindings, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'ngIf', Δbind(cmpt.showing)); - } - } - - class Cmpt { - showing = true; - static ngComponentDef = ΔdefineComponent({ - type: Cmpt, - factory: () => new Cmpt(), - selectors: [['cmpt']], - consts: 1, - vars: 1, - template: cmptTpl, - directives: [NgIf, OnDestroyDirective] - }); - } - - const fixture = new ComponentFixture(Cmpt); - expect(destroyed).toBeFalsy(); - - fixture.component.showing = false; - fixture.update(); - expect(destroyed).toBeTruthy(); - }); - }); - });