264 lines
8.8 KiB
JavaScript
264 lines
8.8 KiB
JavaScript
/**
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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 ) ) );
|
|
} ) );
|
|
}
|
|
|