/** * Tests for conforming Thenable 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 . */ let Class = require( 'easejs' ).Class, Sut = require( '../../' ).then.Thenable, expect = require( 'chai' ).expect; module.exports = ( ctor, accept, reject ) => { describe( 'conforming to Thenable', () => { it( 'implements Thenable', () => expect( Class.isA( Sut, ctor() ) ).to.be.true ); describe( '#then', () => _thenTests( ctor, accept, reject ) ); /** * Promises/A+ 2.2.4. * * For detailed rationale on why this is imposed on a simple * thenable, see the Thenable interface. */ describe( 'call stack', () => describe( 'is allowed to clear', () => { let stack_ctor = ( type, done ) => { let cleared = ''; // rm Error (2 lines); onTimeout callback (1); strip // remaining file and line numbers, since they may vary // with onTimeout call location; this should hopefully // be flexible enough to accomodate various environments // (provided that Error().stack is available) let getStack = () => Error().stack .replace( /^.*\n.*\n.*\n/, '' ) .replace( /\(.*?:[0-9]+:[0-9]+\)/g, '' ); let chk = () => { expect( cleared ).to.equal( getStack().replace( /^.*\n/, '' ) ); }; // ensure the stack clears setTimeout( () => cleared = getStack(), 0 ); // if the callback is invoked asynchronously, then it // will run after the above timeout callback let sut = ctor(), n = 3; while ( n-- ) { sut.then( type === accept && chk, type === reject && chk ); } // finish after all callbacks have been applied setTimeout( () => done() ); type( sut ); }; it( 'before invoking fulfillment callback', done => stack_ctor( accept, done ) ); it( 'before invoking rejection callback', done => stack_ctor( reject, done ) ); } ) ); } ); }; function _thenTests( ctor, accept, reject ) { let sut; // these methods just make it easier to work into a lisp-style // procedure let accept_sut = () => accept( sut ), reject_sut = () => reject( sut ); beforeEach( () => { sut = ctor(); } ); it( 'returns another Thenable', () => expect( Class.isA( Sut, ctor().then( null, null ) ) ) .to.be.true ); describe( 'callbacks', () => { /** * The Promises/A+ specification states that non-function * arguments must be "ignored"; this is a bit ambiguous, since * ignoring can involve any number of things, including throwing * an exception (and ignoring the values as a consequence of * aborting the call). * * We will allow the implementation to do whatever it chooses, * provided that the most definitive indications of intentional * omission are permitted without error. * * See Promises/A+ §2.2.1. */ describe( 'params', () => { it( 'permit omitted callbacks', () => expect( () => ctor().then() ) .to.not.throw( Error ) ); it( 'permit undefined callbacks', () => expect( () => ctor().then( undefined, undefined ) ) .to.not.throw( Error ) ); it( 'permit null callbacks', () => expect( () => ctor().then( null, null ) ) .to.not.throw( Error ) ); } ); it( 'trigger fulfillment on accept', done => accept_sut( sut.then( ()=>done() ) ) ); it( 'trigger rejection on reject', done => reject_sut( sut.then( null, ()=>done() ) ) ); /** * An implementation that invokes both callbacks for a given * value/reason pair (that is---either an accept or reject) is * implying that the request both succeeded and failed; since we * have no intent on supporting superimposed states---and it's * rational to assume that the consumer of the implementation * has no such intent either---such an implementation should be * considered to be in error. * * This will obviously not catch cases that may require certain * types of input (e.g. a bug in specific portions of the * logic), but will hopefully help to point out fatal design * flaws, and prevent the need to duplicate such obvious tests * for every `Thenable` implementation. */ describe( 'are not both called for a value/reason pair', () => { /** * This implementation attempts to determine, broadly, * whether both callbacks were invoked. Doing so is not * fool-proof, since `Thenable` mandates asynchrony, but we * have no idea when the asynchronous event will be * scheduled. Here, we assume that the callbacks are * scheduled immediately after the invocation of `accept` or * `reject`, and that scheduling our own callback would * enqueue it after any `Thenable` callbacks. This may or * may not be true. */ let postchk = ( f, done ) => { let ok = true, cf = () => ok = false; f( ctor().then( ( f === reject ) && cf, ( f === accept ) && cf ) ); setTimeout( () => { expect( ok ).to.be.true; done(); }, 0 ); }; it( 'when fulfilled', done => postchk( accept, done ) ); it( 'when rejected', done => postchk( reject, done ) ); } ); /** * Promises/A+ §2.2.5. */ describe( 'calling context', () => { let no_context = ( () => this )(); let expect_no_c = done => () => expect( this ).to.equal( no_context ) && done(); it( 'invokes fulfilled callback without context', done => accept_sut( sut .then( expect_no_c( ()=>done() ), null ) ) ); it( 'invokes reject callback without context', done => reject_sut( sut .then( null, expect_no_c( ()=>done() ) ) ) ); } ); } ); describe( 'invoked multiple times', () => /** * There may be more than one observer interested in the results * of a computation; Promises/A+ §2.2.6 states that each * callback should be invoked in the order of their respective * `then` calls. */ describe( 'will invoke in order of respective #then', () => { let c = ( chk, i ) => { expect( chk[ i-1 ] ).to.be.undefined; chk[ i ] = undefined; }; let multi_ctor = done => { let chk = [ 0, 1, 2 ]; chk.forEach( i => { let f = c( chk, i ); sut.then( f, f ); } ); sut.then( ()=>done(), ()=>done() ); return sut; }; it( 'all fulfillment callbacks upon accept', done => accept_sut( multi_ctor( done ) ) ); it( 'all rejection callbacks upon reject', done => reject_sut( multi_ctor( done ) ) ); } ) ); }