/** * 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: function( ids ) { return this.defineEvents( ids ); }, // emit is protected evEmit: function( ev ) { return this.emit.apply( this, arguments ); } } ); describe( 'event.Evented', function() { var stub; beforeEach( function() { stub = EvStub(); } ); describe( '#defineEvents', function() { /** * 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', function() { expect( function() { stub.evDefineEvents( 'string' ); } ).to.throw( TypeError, "array" ); } ); it( 'accepts array argument', function() { expect( function() { stub.evDefineEvents( [] ); } ).to.not.throw( TypeError ); } ); it( 'returns self', function() { expect( stub.evDefineEvents( [] ) ).to.equal( stub ); } ); /** * This relies on functionality proven below */ it( 'defines each event in provided list', function() { var events = [ 'a', 'b' ]; stub.evDefineEvents( events ); var i = events.length; while ( i-- ) { expect( function() { // will fail if event did not register stub.on( events[ i ], function() {} ); } ).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', function() { var evs = [ 'a', 'b' ]; // define each separately for ( var i = 0; i < evs.length; i++ ) { stub.evDefineEvents( [ evs[ i ] ] ); expect( function() { // will fail if event did not register stub.on( evs[ i ], function() {} ); } ).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', function() { it( 'in separate calls', function() { var name = 'ev'; stub.evDefineEvents( [ name ] ); expect( function() { stub.evDefineEvents( [ name ] ); } ).to.throw( Error, name ); } ); it( 'in a single call', function() { var dup = 'foo'; expect( function() { stub.evDefineEvents( [ 'a', dup, 'b', dup ] ); } ).to.throw( Error, dup ); } ); } ); } ); describe( '#on', function() { it( 'does not allow hooking undeclared events', function() { var badevent = 'bazbar'; expect( function() { stub.on( badevent, function() {} ); } ).to.throw( Error, badevent ); } ); it( 'allows hooking declared events', function() { var name = 'testev'; stub.evDefineEvents( [ name ] ) .on( name, function() {} ); } ); it( 'returns self', function() { var name = 'testev'; var ret = stub.evDefineEvents( [ name ] ) .on( name, function() {} ); expect( ret ).to.equal( stub ); } ); it( 'requires that callback is a function', function() { var ev = 'foo'; stub.evDefineEvents( [ ev ] ); // OK expect( function() { stub.on( ev, function() {} ); } ).to.not.throw( TypeError ); // bad expect( function() { stub.on( ev, "kittens" ); } ).to.throw( TypeError ); } ); } ); describe( '#emit', function() { it( 'cannot emit undefined events', function() { var ev = 'unknown'; expect( function() { stub.evEmit( ev ); } ).to.throw( Error, ev ); } ); it( 'invokes all callbacks attached with #on', function() { var ev = 'foo', called = 0; stub.evDefineEvents( [ ev ] ) .on( ev, function() { called++; } ); expect( function() { 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', function( done ) { var ev = 'foo', a = {}, b = []; stub.evDefineEvents( [ ev ] ) .on( ev, function( 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', function() { var ev = 'foo', called = [], n = 2; stub.evDefineEvents( [ ev ] ); // add N callbacks for EV for ( var i = 0; i < n; i++ ) { ( function( i ) { stub.on( ev, function() { called[ i ] = true; } ); } )( i ); } // 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', function() { var ev = 'foo', wrongev = 'bar'; stub.evDefineEvents( [ ev, wrongev ] ); var called = false; stub.on( ev, function() { called = true; } ); stub.on( wrongev, function() { 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', function() { var ev = 'foo'; expect( function() { // no hooks, but emit stub.evDefineEvents( [ ev ] ).evEmit( ev ); } ).to.not.throw( Error ); } ); it( 'returns self', function() { var ev = 'foo'; expect( stub.evDefineEvents( [ ev ] ).evEmit( ev ) ) .to.equal( stub ); } ); } ); /** * This is important to support stackable traits for custom schedulers. */ describe( '#scheduleCallbacks', function() { var ev = 'foo', a = 'bar', b = 'baz'; it( 'can be overridden by subtypes', function( done ) { common.checkOverride( EvStub, 'scheduleCallbacks', function( 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', function( done ) { var a = function() {}, b = function() {}; common.checkOverride( EvStub, 'scheduleCallbacks', function( _, evc, __ ) { expect( evc ).to.include( a ); expect( evc ).to.include( b ); done(); } )() .evDefineEvents( [ ev ] ) .on( ev, a ) .on( ev, b ) .evEmit( ev ); } ); } ); } );