/** * @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); }); });