jstonic/test/event/EventedTest.js

746 lines
22 KiB
JavaScript
Raw Normal View History

/**
* 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 <http://www.gnu.org/licenses/>.
*/
var Sut = require( '../../' ).event.Evented,
Eventable = require( '../../' ).event.Eventable,
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();
} );
/**
* As a general-purpose library, we would not want to restrict
* developers to a specific event implementation (for example, maybe the
* user would prefer to use Node.js' event system, or transparently
* integrate with libraries/systems that use it). Together with ease.js'
* interop support, this makes such a case trivial.
*/
describe( 'interface', () =>
{
it( 'is Eventable', () =>
expect( Class.isA( Eventable, stub ) ).to.be.true
);
} );
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( ReferenceError, 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;
} );
} );
} );
/**
* Intended to clean up after #hookEvent
*/
describe( '#unhookEvent', function()
{
var ev = 'foo',
f = ()=>{};
it( 'can be overridden by subtypes', ( done ) =>
{
common.checkOverride( EvStub, 'unhookEvent',
( given_ev, given_listener ) =>
{
expect( given_ev ).to.equal( ev );
expect( given_listener ).to.equal( f );
done();
}
)().evDefineEvents( [ ev ] )
.on( ev, f )
.removeListener( ev, f );
} );
/**
* Since #hookEvent can do whatever it pleases with listeners
* (including preventing them from being hooked to begin with, or
* replacing them with another hook), we need to ensure that the
* hook is called regardless of whether the listener is known to be
* attached to a particular event; otherwise, certain types of
* cleanup would not be possible.
*/
it( 'will be called even if listener is not attached', ( done ) =>
{
common.checkOverride( EvStub, 'unhookEvent', ( _, __ ) =>
{
done();
} )().evDefineEvents( [ ev ] )
.removeListener( ev, f );
} );
/**
* This should never be done---since it violates the Eventable
* specification---but it is useful for determining the scope of the
* hook's ability, and is important for proving that, when we modify
* the behavior, we know that the default behavior is suppressed
* (unless we invoke it ourselves).
*/
it( 'can prevent default implementation', () =>
{
let norm = () => called = true,
called = false;
EvStub.extend(
{
'override unhookEvent': ( _, __ ) => {},
} )().evDefineEvents( [ ev ] )
.on( ev, norm )
.removeListener( ev, norm )
.evEmit( ev );
// should have been called since we suppressed unhooking
expect( called ).to.be.true;
} );
/**
* Listeners must ultimately be added/removed by invoking the super
* method (to add listeners to the internal data structures needed
* for dispatch on emit). If we are doing any sort of meaningful
* manipulation, then we must be able to wholly override the
* arguments provided to us.
*
* As an example, if #hookEvent wrapped the given listener $L$ as
* $L'$, which was then hooked for dispatch on some event, then we'd
* need to similarily remove $L'$---not $L$---when invoking
* #removeListener with $L$.
*/
it( 'can override provided event id and listener', () =>
{
let rmf = () => called = true,
called = false,
ev2 = 'snazzlepuss';
EvStub.extend(
{
'override unhookEvent'( _, __ )
{
// use these regardless of what we're given
this.__super( ev, rmf );
},
} )().evDefineEvents( [ ev, ev2 ] )
.on( ev, rmf )
.removeListener( ev2, f )
.evEmit( ev );
// even though we requested to remove F from some arbitrary
// event, our hard-coded removal of RMF should take place
expect( called ).to.be.false;
} );
} );
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 )
);
} );
} );
} );