/** * 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 ); } ); } ); } ); /** * `addListener` is an alias for `on` (consistent with Node.js' API) */ [ 'on', 'addListener' ].forEach( on => { 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 listener 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 ); } ); /** * It is highly suspect if the exact same listener is registered * for the same event---it is either a bug, or a questionable * design decision; we assume that the former is far more likely * to be true, and that the latter should be addressed and * resolved. * * Note that this does not prevent a caller from wrapping a * listener in a function that is instead registered as the * listener---this is perfectly valid and has many use cases. */ it( 'does not permit same event listener multiple times', () => { var ev = 'foo', f = () => {}; expect( () => { stub.evDefineEvents( [ ev ] ) [ on ]( ev, f ) [ on ]( ev, f ); } ).to.throw( Error ); } ); } ); } ); describe( '#emit', () => { it( 'cannot emit undefined events', () => { var ev = 'unknown'; expect( () => stub.evEmit( ev ) ) .to.throw( Error, ev ); } ); it( 'invokes all listeners 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 listener 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 listeners', ( 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 listeners * 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 listeners synchronously', () => { var ev = 'foo', called = [], n = 2; stub.evDefineEvents( [ ev ] ); // add N listeners for EV for ( let i = 0; i < n; i++ ) { stub.on( ev, () => called[ i ] = true ); } // trigger listeners stub.evEmit( ev ); // by now, we care about two things: that all listeners 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 listeners for that specific event * should be invoked. */ it( 'does not mix listeners 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 listeners 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 listeners 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 ); } ); } ); /** * Permits additional actions on hook and overriding event hooking * storage behavior */ describe( '#hookEvent', function() { var ev = 'foo', f = ()=>{}; it( 'can be overridden by subtypes', ( done ) => { common.checkOverride( EvStub, 'hookEvent', ( given_ev, given_callback ) => { expect( given_ev ).to.equal( ev ); expect( given_callback ).to.equal( f ); done(); } )().evDefineEvents( [ ev ] ).on( ev, f ); } ); /** * Should provide full control over how the listeners are handled. */ it( 'can prevent listeners from being added', () => { var ev = 'foo'; // override does nothing expect( () => { EvStub.extend( { 'override hookEvent': ( _, __ ) => {} } )() .evDefineEvents( [ ev ] ) .on( ev, () => { throw Error( "Event called!" ); } ).evEmit( ev ); } ).to.not.throw( Error ); } ); } ); describe( '#removeListener', () => { it( 'rejects unknown event ids', () => { var ev = 'foo'; expect( () => stub.removeListener( ev , ()=>{} ) ) .to.throw( Error, ev ); } ); /** * The purpose of removing a listener is to prevent it from being * called when an event is emitted. We don't really care how this is * done; just don't call it. */ it( 'prevents removed listener from being called', () => { var ev = 'foo', called = false, listener = ( () => called = true ); stub.evDefineEvents( [ ev ] ); // we have already proved that adding a listener works; // immediately remove it and ensure that it's not called stub.on( ev, listener ) .removeListener( ev, listener ); stub.evEmit( ev ); expect( called ).to.equal( false ); } ); /** * If the given listener to remove was never registered (or no * longer is), then we will shall act as though it was successfully * removed (since, according to our initial requirement, we * succeeded in ensuring that the listener will not be called on * subsequent emits). */ it( 'does not fail and returns self on missing listener', () => { let ev = 'foo'; expect( stub.evDefineEvents( [ ev ] ) .removeListener( ev, () => {} ) ).to.equal( stub ); } ); it( 'returns self on success', () => { let ev = 'foo', f = () => {}; expect( stub.evDefineEvents( [ ev ] ) .on( ev, f ) .removeListener( ev, f ) ).to.equal( stub ); } ); /** * These are tests that are not design per se, but are intended to * comprehensively guard against future design changes, likely or * not. */ describe( 'sanity check', () => { /** * Removing a single listener for an event should not touch * others for that same event */ it( 'does not remove all listeners for event', () => { var ev = 'foo', f1c = false, f2c = false, f1 = () => f1c = true, f2 = () => f2c = true; stub.evDefineEvents( [ ev ] ) .on( ev, f1 ) .on( ev, f2 ) .removeListener( ev, f1 ) .evEmit( ev ); expect( f1c ).to.be.false; expect( f2c ).to.be.true; } ); /** * The same listener may be added to multiple events---we should * only ever remove it from the one that was requested. We have * already proven in previous tests that event listeners are * actually removed as expected, so we need only ensure that it * is *not* removed from another event that it is attached to. */ it( 'does not remove same listener on other events', () => { var evs = [ 'foo', 'bar' ], called = false, f = () => called = true; stub.evDefineEvents( evs ) .on( evs[ 0 ], f ) .on( evs[ 1 ], f ) .removeListener( evs[ 0 ], f ) .evEmit( evs[ 1 ] ); expect( called ).to.be.true; } ); /** * Removing a listener may introduce, depending on * implementation details that we do not care about here, gaps * in the data structure holding the listeners. The idea here is * to ensure that this gap, if immediately filled, will not * cause problems. */ it( 'can add listener after removal', () => { var ev = 'foo', called = 0, frm = () => {}, fok = () => called++, fok2 = () => called += 2; stub.evDefineEvents( [ ev ] ) .on( ev, frm ) .removeListener( ev, frm ) .on( ev, fok ) .on( ev, fok2 ) .evEmit( ev ); expect( called ).to.equal( 3 ); } ); /** * Similar to the above test, but ensures that the system is not * marking a listener for deletion in such a way that it won't * be recognized later. This would be an odd implementation, but * people do odd things sometimes. */ it( 'can re-add removed listener', () => { var ev = 'foo', called = false, f = () => called = true; stub.evDefineEvents( [ ev ] ) .on( ev, f ) .removeListener( ev, f ) .on( ev, f ) .evEmit( ev ); expect( called ).to.be.true; } ); } ); } ); describe( '#hooksEvent', () => { var ev = 'foo', f; beforeEach( () => { // ensure fresh listener f = () => {}; } ); it( 'recognizes listener for a hooked event', () => { stub.evDefineEvents( [ ev ] ) .on( ev, f ); expect( stub.hooksEvent( ev, f ) ).to.be.true; } ); it( 'does not recognize listener for non-hooked event', () => { expect( stub.hooksEvent( ev, f ) ).to.be.false; } ); /** * Technically this test is sufficient to make the previous two * redundant, but they are retained for clairty in debugging. */ it( 'does not recognize listeners of other events', () => { var ev2 = 'bar', f2 = () => {}; stub.evDefineEvents( [ ev, ev2 ] ) .on( ev, f ) .on( ev2, f2 ); expect( stub.hooksEvent( ev, f ) ).to.be.true; expect( stub.hooksEvent( ev2, f ) ).to.be.false; expect( stub.hooksEvent( ev, f2 ) ).to.be.false; expect( stub.hooksEvent( ev2, f2 ) ).to.be.true; } ); } ); describe( 'listener metadata', () => { /** * It is important that each Evented instance store its listener * metadata in a distinct field; otherwise, they would query * each-other, which would produce conflicts if the events happen to * have the same name (which would not be uncommon). */ it( 'is not accessible to other Evented instances', () => { var ev = 'foo', f = () => {}; // this would fail if data were shared because the system would // consider f to have already been registered with event `foo' [ stub, EvStub() ].forEach( s => s.evDefineEvents( [ ev ] ) .on( ev, f ) ); } ); } ); } );