diff --git a/src/event/Eventable.js b/src/event/Eventable.js index d2d42cc..c64a971 100644 --- a/src/event/Eventable.js +++ b/src/event/Eventable.js @@ -74,7 +74,10 @@ module.exports = Interface( 'Eventable', * synchronous; it is also undefined whether LISTENER will receive * messages emitted by event EV before having hooked the event. * - * The validity of the event identifier EV is undefined. + * The validity of the event identifier EV is undefined, except that it + * must be a string and an empty string shall throw a TypeError (as this + * may very well indicate a bug in the calling code). LISTENER shall not + * be required to declare any number of parameters. * * An implementation shall not automatically unhook LISTENER (i.e. * without use of #removeListener) from event EV unless the diff --git a/test/event/EventableTestConform.js b/test/event/EventableTestConform.js new file mode 100644 index 0000000..656f941 --- /dev/null +++ b/test/event/EventableTestConform.js @@ -0,0 +1,162 @@ +/** + * Tests for conforming Eventable implementation + * + * 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 . + * + * A conforming implementation must pass each of these tests. See the + * Eventable interface itself for additional requirements and documentation. + * + * To use these tests, invoke the exported function with a constructor + * function returning an instance of the SUT. + */ + +let expect = require( 'chai' ).expect, + types = require( '../../' ).util.types; + +let _fvoid = () => {}, + _ev = 'ev', + + _nonstr = types.typevals.nonstr, + _nonf = types.typevals.nonfunc; + +// data passed to ctor to prepare for test +let _events = [ _ev ]; + + +/** + * Invoke conformance tests + * + * CTOR will be passed an array of events that the conformance tests will + * make use of, allowing them to be registered if needed (for validation + * assertions). + * + * This test case will ideally be invoked after the top-level `describe` of + * the SUT test case so that the output reads as "SUT, conforming to + * Eventable, ...". + * + * @param {function(Array.)} ctor SUT constructor function + * + * @return {undefined} + */ +module.exports = ctor => +{ + // shorthand for SUT construction + let meta_ctor = () => ctor( _events ); + + /** + * #on and #addListener are one and the same, but detecting that they + * alias each other is not reliable for various reasons; instead, we'll + * run the tests on both methods. + */ + describe( 'conforming to Eventable', () => + { + [ 'on', 'addListener' ].forEach( x => + _onTests( meta_ctor, x ) ); + } ); +}; + + +function _onTests( ctor, on ) +{ + /** + * Note that by using _fvoid, we are implicitly testing the requirement + * that the listener shall not be required to declare any number of + * parameters (because we defined no parameters). + */ + describe( `#${on}`, () => + { + /** + * We must test both inputs at once since we cannot otherwise say + * with confidence which parameter is non-conforming. + */ + it( 'accepts string event id with listener function', () => + { + expect( () => + { + ctor()[ on ]( _ev, _fvoid ); + } ).to.not.throw( Error ); + } ); + + + /** + * A TypeError should be thrown when the event id is empty. The + * rationale behind this is that the event id may be dynamically + * determined at runtime, or may otherwise be stored in a variable, + * which probably is not supposed to be empty; if it is, this could + * represent a logic error (such as an incomplete conditional or + * table lookup failure). More likely, this would result in + * `undefined`, which is covered in below tests. + * + * Implementations wishing to use an empty string to indicate "all + * events" could instead use, for example, `*` (motivated by shell + * globbing). + */ + it( 'does not accept an empty event id', () => + { + expect( () => ctor()[ on ]( '', _fvoid ) ) + .to.throw( TypeError ); + } ); + + + /** + * All event identifiers should be strings; this provides + * consistency between all implementations and helps to weed out + * runtime bugs (see the "empty event id" test for examples). + */ + it( 'does not accept non-string event ids', () => + { + _nonstr.forEach( badev => + { + expect( () => ctor()[ on ]( badev, _fvoid ) ) + .to.throw( TypeError ); + } ); + } ); + + + /** + * Same rationale as the event string argument. + */ + it( 'does not accept non-function listeners', () => + { + _nonf.forEach( badf => + { + expect( () => ctor()[ on ]( _ev, badf ) ) + .to.throw( TypeError ); + } ); + } ); + + + /** + * When hooking multiple events on a single object, it is convenient + * to be able to do so concisely without having to repeat the name + * of the object reference. + * + * Since exceptions are used to indicate error conditions, there is + * also no other useful value to return that would not break + * encapsulation. + */ + it( 'returns self for method chaining', () => + { + let inst = ctor(); + + expect( inst[ on ]( _ev, _fvoid ) ) + .to.equal( inst ); + } ); + } ); +} +