From 5fd39283ec332c60491803fec89b8deac92a1b27 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 9 May 2019 22:08:31 -0400 Subject: [PATCH] test(ivy): move output tests to acceptance (#30372) Moves most of the tests in `output_spec` into `acceptance`. Note that one test is left in `render3`, because it's testing mixing in custom logic with instructions which we can't replicate using `TestBed`. PR Close #30372 --- packages/core/test/acceptance/outputs_spec.ts | 282 ++++++++++++++ packages/core/test/render3/outputs_spec.ts | 355 +----------------- 2 files changed, 285 insertions(+), 352 deletions(-) create mode 100644 packages/core/test/acceptance/outputs_spec.ts diff --git a/packages/core/test/acceptance/outputs_spec.ts b/packages/core/test/acceptance/outputs_spec.ts new file mode 100644 index 0000000000..873b663388 --- /dev/null +++ b/packages/core/test/acceptance/outputs_spec.ts @@ -0,0 +1,282 @@ +/** + * @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 {Component, Directive, EventEmitter, Input, OnDestroy, Output, ViewChild} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; + +describe('outputs', () => { + @Component({selector: 'button-toggle', template: ''}) + class ButtonToggle { + @Output('change') + change = new EventEmitter(); + + @Output('reset') + resetStream = new EventEmitter(); + } + + @Directive({selector: '[otherDir]'}) + class OtherDir { + @Output('change') + changeStream = new EventEmitter(); + } + + @Component({selector: 'destroy-comp', template: ''}) + class DestroyComp implements OnDestroy { + events: string[] = []; + ngOnDestroy() { this.events.push('destroy'); } + } + + @Directive({selector: '[myButton]'}) + class MyButton { + @Output() + click = new EventEmitter(); + } + + it('should call component output function when event is emitted', () => { + let counter = 0; + + @Component({template: ''}) + class App { + @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + onChange() { counter++; } + } + TestBed.configureTestingModule({declarations: [App, ButtonToggle]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + fixture.componentInstance.buttonToggle.change.next(); + expect(counter).toBe(1); + + fixture.componentInstance.buttonToggle.change.next(); + expect(counter).toBe(2); + }); + + it('should support more than 1 output function on the same node', () => { + let counter = 0; + let resetCounter = 0; + + @Component( + {template: ''}) + class App { + @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + onChange() { counter++; } + onReset() { resetCounter++; } + } + TestBed.configureTestingModule({declarations: [App, ButtonToggle]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + fixture.componentInstance.buttonToggle.change.next(); + expect(counter).toBe(1); + + fixture.componentInstance.buttonToggle.resetStream.next(); + expect(resetCounter).toBe(1); + }); + + it('should eval component output expression when event is emitted', () => { + @Component({template: ''}) + class App { + @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + counter = 0; + } + TestBed.configureTestingModule({declarations: [App, ButtonToggle]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + fixture.componentInstance.buttonToggle.change.next(); + expect(fixture.componentInstance.counter).toBe(1); + + fixture.componentInstance.buttonToggle.change.next(); + expect(fixture.componentInstance.counter).toBe(2); + }); + + it('should unsubscribe from output when view is destroyed', () => { + let counter = 0; + + @Component( + {template: ''}) + class App { + @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + condition = true; + + onChange() { counter++; } + } + TestBed.configureTestingModule({imports: [CommonModule], declarations: [App, ButtonToggle]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const buttonToggle = fixture.componentInstance.buttonToggle; + + buttonToggle.change.next(); + expect(counter).toBe(1); + + fixture.componentInstance.condition = false; + fixture.detectChanges(); + + buttonToggle.change.next(); + expect(counter).toBe(1); + }); + + it('should unsubscribe from output in nested view', () => { + let counter = 0; + + @Component({ + template: ` +
+ +
+ ` + }) + class App { + @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + condition = true; + condition2 = true; + + onChange() { counter++; } + } + TestBed.configureTestingModule({imports: [CommonModule], declarations: [App, ButtonToggle]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const buttonToggle = fixture.componentInstance.buttonToggle; + + buttonToggle.change.next(); + expect(counter).toBe(1); + + fixture.componentInstance.condition = false; + fixture.detectChanges(); + + buttonToggle.change.next(); + expect(counter).toBe(1); + }); + + it('should work properly when view also has listeners and destroys', () => { + let clickCounter = 0; + let changeCounter = 0; + + @Component({ + template: ` +
+ + + +
+ ` + }) + class App { + @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + @ViewChild(DestroyComp) destroyComp !: DestroyComp; + condition = true; + + onClick() { clickCounter++; } + onChange() { changeCounter++; } + } + TestBed.configureTestingModule( + {imports: [CommonModule], declarations: [App, ButtonToggle, DestroyComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const {buttonToggle, destroyComp} = fixture.componentInstance; + const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); + + buttonToggle.change.next(); + expect(changeCounter).toBe(1); + expect(clickCounter).toBe(0); + + button.click(); + expect(changeCounter).toBe(1); + expect(clickCounter).toBe(1); + + fixture.componentInstance.condition = false; + fixture.detectChanges(); + + expect(destroyComp.events).toEqual(['destroy']); + + buttonToggle.change.next(); + button.click(); + expect(changeCounter).toBe(1); + expect(clickCounter).toBe(1); + }); + + it('should fire event listeners along with outputs if they match', () => { + let counter = 0; + + @Component({template: ''}) + class App { + @ViewChild(MyButton) buttonDir !: MyButton; + onClick() { counter++; } + } + TestBed.configureTestingModule({declarations: [App, MyButton]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + // To match current Angular behavior, the click listener is still + // set up in addition to any matching outputs. + const button = fixture.nativeElement.querySelector('button'); + button.click(); + expect(counter).toBe(1); + + fixture.componentInstance.buttonDir.click.next(); + expect(counter).toBe(2); + }); + + it('should work with two outputs of the same name', () => { + let counter = 0; + + @Component({template: ''}) + class App { + @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + @ViewChild(OtherDir) otherDir !: OtherDir; + onChange() { counter++; } + } + TestBed.configureTestingModule({declarations: [App, ButtonToggle, OtherDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + fixture.componentInstance.buttonToggle.change.next(); + expect(counter).toBe(1); + + fixture.componentInstance.otherDir.changeStream.next(); + expect(counter).toBe(2); + }); + + it('should work with an input and output of the same name', () => { + let counter = 0; + + @Directive({selector: '[otherChangeDir]'}) + class OtherChangeDir { + @Input() + change !: boolean; + } + + @Component({ + template: + '' + }) + class App { + @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + @ViewChild(OtherChangeDir) otherDir !: OtherChangeDir; + change = true; + + onChange() { counter++; } + } + TestBed.configureTestingModule({declarations: [App, ButtonToggle, OtherChangeDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const {buttonToggle, otherDir} = fixture.componentInstance; + + expect(otherDir.change).toBe(true); + + fixture.componentInstance.change = false; + fixture.detectChanges(); + + expect(otherDir.change).toBe(false); + + buttonToggle.change.next(); + expect(counter).toBe(1); + }); + +}); diff --git a/packages/core/test/render3/outputs_spec.ts b/packages/core/test/render3/outputs_spec.ts index 26970eaefe..58607ea713 100644 --- a/packages/core/test/render3/outputs_spec.ts +++ b/packages/core/test/render3/outputs_spec.ts @@ -9,15 +9,13 @@ import {EventEmitter} from '@angular/core'; import {ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/index'; -import {ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵlistener, ɵɵtext} from '../../src/render3/instructions/all'; +import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵlistener, ɵɵtext} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {containerEl, renderToHtml} from './render_util'; +import {renderToHtml} from './render_util'; describe('outputs', () => { let buttonToggle: ButtonToggle; - let destroyComp: DestroyComp; - let buttonDir: MyButton; class ButtonToggle { change = new EventEmitter(); @@ -47,355 +45,8 @@ describe('outputs', () => { }); } - class DestroyComp { - events: string[] = []; - ngOnDestroy() { this.events.push('destroy'); } - static ngComponentDef = ɵɵdefineComponent({ - type: DestroyComp, - selectors: [['destroy-comp']], - consts: 0, - vars: 0, - template: function(rf: RenderFlags, ctx: any) {}, - factory: () => destroyComp = new DestroyComp() - }); - } - - /** */ - class MyButton { - click = new EventEmitter(); - - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyButton, - selectors: [['', 'myButton', '']], - factory: () => buttonDir = new MyButton, - outputs: {click: 'click'} - }); - } - - - const deps = [ButtonToggle, OtherDir, DestroyComp, MyButton]; - - it('should call component output function when event is emitted', () => { - /** */ - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button-toggle'); - { - ɵɵlistener('change', function() { return ctx.onChange(); }); - } - ɵɵelementEnd(); - } - } - - let counter = 0; - const ctx = {onChange: () => counter++}; - renderToHtml(Template, ctx, 1, 0, deps); - - buttonToggle !.change.next(); - expect(counter).toEqual(1); - - buttonToggle !.change.next(); - expect(counter).toEqual(2); - }); - - it('should support more than 1 output function on the same node', () => { - /** */ - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button-toggle'); - { - ɵɵlistener('change', function() { return ctx.onChange(); }); - ɵɵlistener('reset', function() { return ctx.onReset(); }); - } - ɵɵelementEnd(); - } - } - - let counter = 0; - let resetCounter = 0; - const ctx = {onChange: () => counter++, onReset: () => resetCounter++}; - renderToHtml(Template, ctx, 1, 0, deps); - - buttonToggle !.change.next(); - expect(counter).toEqual(1); - - buttonToggle !.resetStream.next(); - expect(resetCounter).toEqual(1); - }); - - it('should eval component output expression when event is emitted', () => { - /** */ - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button-toggle'); - { - ɵɵlistener('change', function() { return ctx.counter++; }); - } - ɵɵelementEnd(); - } - } - - const ctx = {counter: 0}; - renderToHtml(Template, ctx, 1, 0, deps); - - buttonToggle !.change.next(); - expect(ctx.counter).toEqual(1); - - buttonToggle !.change.next(); - expect(ctx.counter).toEqual(2); - }); - - it('should unsubscribe from output when view is destroyed', () => { - - /** - * % if (condition) { - * - * % } - */ - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'button-toggle'); - { - ɵɵlistener('change', function() { return ctx.onChange(); }); - } - ɵɵelementEnd(); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - } - - let counter = 0; - const ctx = {onChange: () => counter++, condition: true}; - renderToHtml(Template, ctx, 1, 0, deps); - - buttonToggle !.change.next(); - expect(counter).toEqual(1); - - ctx.condition = false; - renderToHtml(Template, ctx, 1, 0, deps); - - buttonToggle !.change.next(); - expect(counter).toEqual(1); - }); - - it('should unsubscribe from output in nested view', () => { - - /** - * % if (condition) { - * % if (condition2) { - * - * % } - * % } - */ - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵcontainer(0); - } - ɵɵcontainerRefreshStart(0); - { - if (ctx.condition2) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'button-toggle'); - { - ɵɵlistener('change', function() { return ctx.onChange(); }); - } - ɵɵelementEnd(); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - } - - let counter = 0; - const ctx = {onChange: () => counter++, condition: true, condition2: true}; - renderToHtml(Template, ctx, 1, 0, deps); - - buttonToggle !.change.next(); - expect(counter).toEqual(1); - - ctx.condition = false; - renderToHtml(Template, ctx, 1, 0, deps); - - buttonToggle !.change.next(); - expect(counter).toEqual(1); - }); - - it('should work properly when view also has listeners and destroys', () => { - /** - * % if (condition) { - * - * - * - * % } - */ - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 4, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'button'); - { - ɵɵlistener('click', function() { return ctx.onClick(); }); - ɵɵtext(1, 'Click me'); - } - ɵɵelementEnd(); - ɵɵelementStart(2, 'button-toggle'); - { - ɵɵlistener('change', function() { return ctx.onChange(); }); - } - ɵɵelementEnd(); - ɵɵelement(3, 'destroy-comp'); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - } - - let clickCounter = 0; - let changeCounter = 0; - const ctx = {condition: true, onChange: () => changeCounter++, onClick: () => clickCounter++}; - renderToHtml(Template, ctx, 1, 0, deps); - - buttonToggle !.change.next(); - expect(changeCounter).toEqual(1); - expect(clickCounter).toEqual(0); - - const button = containerEl.querySelector('button'); - button !.click(); - expect(changeCounter).toEqual(1); - expect(clickCounter).toEqual(1); - - ctx.condition = false; - renderToHtml(Template, ctx, 1, 0, deps); - - expect(destroyComp !.events).toEqual(['destroy']); - - buttonToggle !.change.next(); - button !.click(); - expect(changeCounter).toEqual(1); - expect(clickCounter).toEqual(1); - }); - - it('should fire event listeners along with outputs if they match', () => { - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['myButton', '']); - { - ɵɵlistener('click', function() { return ctx.onClick(); }); - } - ɵɵelementEnd(); - } - } - - let counter = 0; - renderToHtml(Template, {counter, onClick: () => counter++}, 1, 0, deps); - - // To match current Angular behavior, the click listener is still - // set up in addition to any matching outputs. - const button = containerEl.querySelector('button') !; - button.click(); - expect(counter).toEqual(1); - - buttonDir !.click.next(); - expect(counter).toEqual(2); - }); - - it('should work with two outputs of the same name', () => { - /** */ - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button-toggle', ['otherDir', '']); - { - ɵɵlistener('change', function() { return ctx.onChange(); }); - } - ɵɵelementEnd(); - } - } - - let counter = 0; - renderToHtml(Template, {counter, onChange: () => counter++}, 1, 0, deps); - - buttonToggle !.change.next(); - expect(counter).toEqual(1); - - otherDir !.changeStream.next(); - expect(counter).toEqual(2); - }); - - it('should work with an input and output of the same name', () => { - let otherDir: OtherChangeDir; - - class OtherChangeDir { - // TODO(issue/24571): remove '!'. - change !: boolean; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: OtherChangeDir, - selectors: [['', 'otherChangeDir', '']], - factory: () => otherDir = new OtherChangeDir, - inputs: {change: 'change'} - }); - } - - /** */ - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button-toggle', ['otherChangeDir', '']); - { - ɵɵlistener('change', function() { return ctx.onChange(); }); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'change', ɵɵbind(ctx.change)); - } - } - - let counter = 0; - const deps = [ButtonToggle, OtherChangeDir]; - renderToHtml(Template, {counter, onChange: () => counter++, change: true}, 1, 1, deps); - expect(otherDir !.change).toEqual(true); - - renderToHtml(Template, {counter, onChange: () => counter++, change: false}, 1, 1, deps); - expect(otherDir !.change).toEqual(false); - - buttonToggle !.change.next(); - expect(counter).toEqual(1); - }); + const deps = [ButtonToggle, OtherDir]; it('should work with outputs at same index in if block', () => { /**