/** * Tests trait providing 1-N message dispatch * * Copyright (C) 2014 Mike Gerwitz * * This file is part of jsTonic. * * jstonic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ var Sut = require( '../../' ).event.Evented, Class = require( 'easejs' ).Class, expect = require( 'chai' ).expect, common = require( '../lib' ), EvStub = Class.use( Sut ).extend( { // defineEvents is protected evDefineEvents( ids ) { return this.defineEvents( ids ); }, // emit is protected evEmit( ev ) { return this.emit.apply( this, arguments ); } } ); describe( 'event.Evented', () => { var stub; beforeEach( () => { stub = EvStub(); } ); describe( '#defineEvents', () => { /** * We aren't going to bother testing every possibly value; let's * just see if the most common case (probably) is caught. */ it( 'rejects non-array argument', () => { expect( () => stub.evDefineEvents( 'string' ) ) .to.throw( TypeError, "array" ); } ); it( 'accepts array argument', () => { expect( () => stub.evDefineEvents( [] ) ) .to.not.throw( TypeError ); } ); it( 'returns self', () => { expect( stub.evDefineEvents( [] ) ).to.equal( stub ); } ); /** * This relies on functionality proved below */ it( 'defines each event in provided list', () => { var events = [ 'a', 'b' ]; stub.evDefineEvents( events ); var i = events.length; while ( i-- ) { // will fail if event did not register expect( () => stub.on( events[ i ], ()=>{} ) ) .to.not.throw( Error ); } } ); /** * While at first glance this may seem like an odd thing to want to * do, this will come in useful when exposed via an API that defines * events individually. */ it( 'merges definitions in multiple calls', () => { var evs = [ 'a', 'b' ]; // define each separately for ( var i = 0; i < evs.length; i++ ) { stub.evDefineEvents( [ evs[ i ] ] ); // will fail if event did not register expect( () => stub.on( evs[ i ], ()=>{} ) ) .to.not.throw( Error ); } } ); /** * We do not want someone to declare an event thinking that the id * is free, which risks mixing handlers from what is supposed to be * two separate events. */ describe( 'throws exception on duplicate events', () => { it( 'in separate calls', () => { var name = 'ev'; stub.evDefineEvents( [ name ] ); expect( () => stub.evDefineEvents( [ name ] ) ) .to.throw( Error, name ); } ); it( 'in a single call', () => { var dup = 'foo'; expect( () => stub.evDefineEvents( [ dup, 'a', dup ] ) ) .to.throw( Error, dup ); } ); } ); } ); describe( '#on', () => { it( 'does not allow hooking undeclared events', () => { var badevent = 'bazbar'; expect( () => stub.on( badevent, ()=>{} ) ) .to.throw( Error, badevent ); } ); it( 'allows hooking declared events', () => { var name = 'testev'; stub.evDefineEvents( [ name ] ) .on( name, ()=>{} ); } ); it( 'returns self', () => { var name = 'testev'; var ret = stub.evDefineEvents( [ name ] ) .on( name, ()=>{} ); expect( ret ).to.equal( stub ); } ); it( 'requires that callback is a function', () => { var ev = 'foo'; stub.evDefineEvents( [ ev ] ); // OK expect( () => stub.on( ev, ()=>{} ) ) .to.not.throw( TypeError ); // bad expect( () => stub.on( ev, "kittens" ) ) .to.throw( TypeError ); } ); } ); describe( '#emit', () => { it( 'cannot emit undefined events', () => { var ev = 'unknown'; expect( () => stub.evEmit( ev ) ) .to.throw( Error, ev ); } ); it( 'invokes all callbacks attached with #on', () => { var ev = 'foo', called = 0; stub.evDefineEvents( [ ev ] ) .on( ev, () => called++ ); expect( () => stub.evEmit( ev ) ) .to.not.throw( Error ); // make sure the callback was invoked only once expect( called ).to.equal( 1 ); } ); /** * It is courteous for event handler to *not* modify the values they * are given. Otherwise, the emitter must take care to clone or * encapsulate the values if this is a concern. */ it( 'passes all emit args by reference to callbacks', ( done ) => { var ev = 'foo', a = {}, b = []; stub.evDefineEvents( [ ev ] ) .on( ev, ( givena, givenb ) => { expect( givena ).to.equal( a ); expect( givenb ).to.equal( b ); done(); } ) .evEmit( ev, a, b ); } ); /** * The default event scheduler synchronously invokes all callbacks * in an undefined order (that is: the order may change in the * future). This is not desirable for all cases, but that's not the * point here. */ it( 'invokes multiple callbacks synchronously', () => { var ev = 'foo', called = [], n = 2; stub.evDefineEvents( [ ev ] ); // add N callbacks for EV for ( let i = 0; i < n; i++ ) { stub.on( ev, () => called[ i ] = true ); } // trigger callbacks stub.evEmit( ev ); // by now, we care about two things: that all callbacks were // invoked and that they have been invoked by this point, // meaning that they were done synchronously while ( n-- ) { expect( called[ n ] ).to.equal( true ); } } ); /** * When we emit an event, only the callbacks for that specific event * should be invoked. */ it( 'does not mix callbacks from other events', () => { var ev = 'foo', wrongev = 'bar'; stub.evDefineEvents( [ ev, wrongev ] ); var called = false; stub.on( ev, () => called = true ); stub.on( wrongev, () => { throw Error( "Thar be demons" ) } ); // wrongev callback should *not* be called stub.evEmit( ev ); expect( called ).to.equal( true ); } ); /** * It is common to leave events unhooked. */ it( 'does not fail when emitting unhooked events', () => { var ev = 'foo'; expect( () => stub.evDefineEvents( [ ev ] ).evEmit( ev ) ) .to.not.throw( Error ); } ); it( 'returns self', () => { var ev = 'foo'; expect( stub.evDefineEvents( [ ev ] ).evEmit( ev ) ) .to.equal( stub ); } ); } ); /** * This is important to support stackable traits for custom schedulers. */ describe( '#scheduleCallbacks', () => { var ev = 'foo', a = 'bar', b = 'baz'; it( 'can be overridden by subtypes', ( done ) => { common.checkOverride( EvStub, 'scheduleCallbacks', ( given_ev, _, given_args ) => { expect( given_ev ).to.equal( ev ); expect( given_args ).to.deep.equal( [ a, b ] ); done(); } )().evDefineEvents( [ ev ] ).evEmit( ev, a, b ); } ); /** * The listeners are encapsulated within the trait, so they must be * passed to us for processing. */ it( 'is provided with callbacks to invoke', ( done ) => { var a = function() {}, b = function() {}; common.checkOverride( EvStub, 'scheduleCallbacks', ( _, evc, __ ) => { expect( evc ).to.include( a ); expect( evc ).to.include( b ); done(); } )() .evDefineEvents( [ ev ] ) .on( ev, a ) .on( ev, b ) .evEmit( ev ); } ); } ); } );