2017-05-28 00:38:50 +02:00
'use strict' ;
const isGeneratorFn = require ( 'is-generator-fn' ) ;
const co = require ( 'co-with-promise' ) ;
2017-08-14 05:01:11 +02:00
const concordance = require ( 'concordance' ) ;
2017-05-28 00:38:50 +02:00
const observableToPromise = require ( 'observable-to-promise' ) ;
const isPromise = require ( 'is-promise' ) ;
const isObservable = require ( 'is-observable' ) ;
const plur = require ( 'plur' ) ;
const assert = require ( './assert' ) ;
const globals = require ( './globals' ) ;
2017-08-14 05:01:11 +02:00
const concordanceOptions = require ( './concordance-options' ) . default ;
function formatErrorValue ( label , error ) {
const formatted = concordance . format ( error , concordanceOptions ) ;
return { label , formatted } ;
}
2017-05-28 00:38:50 +02:00
class SkipApi {
constructor ( test ) {
this . _test = test ;
}
}
const captureStack = start => {
const limitBefore = Error . stackTraceLimit ;
Error . stackTraceLimit = 1 ;
const obj = { } ;
Error . captureStackTrace ( obj , start ) ;
Error . stackTraceLimit = limitBefore ;
return obj . stack ;
} ;
class ExecutionContext {
constructor ( test ) {
2017-08-14 05:01:11 +02:00
Object . defineProperties ( this , {
_test : { value : test } ,
skip : { value : new SkipApi ( test ) }
} ) ;
2017-05-28 00:38:50 +02:00
}
plan ( ct ) {
this . _test . plan ( ct , captureStack ( this . plan ) ) ;
}
get end ( ) {
const end = this . _test . bindEndCallback ( ) ;
const endFn = err => end ( err , captureStack ( endFn ) ) ;
return endFn ;
}
get title ( ) {
return this . _test . title ;
}
get context ( ) {
const contextRef = this . _test . contextRef ;
return contextRef && contextRef . context ;
}
set context ( context ) {
const contextRef = this . _test . contextRef ;
if ( ! contextRef ) {
this . _test . saveFirstError ( new Error ( ` \` t.context \` is not available in ${ this . _test . metadata . type } tests ` ) ) ;
return ;
}
contextRef . context = context ;
}
_throwsArgStart ( assertion , file , line ) {
this . _test . trackThrows ( { assertion , file , line } ) ;
}
_throwsArgEnd ( ) {
this . _test . trackThrows ( null ) ;
}
}
{
const assertions = assert . wrapAssertions ( {
pass ( executionContext ) {
executionContext . _test . countPassedAssertion ( ) ;
} ,
pending ( executionContext , promise ) {
executionContext . _test . addPendingAssertion ( promise ) ;
} ,
fail ( executionContext , error ) {
executionContext . _test . addFailedAssertion ( error ) ;
}
} ) ;
Object . assign ( ExecutionContext . prototype , assertions ) ;
function skipFn ( ) {
this . _test . countPassedAssertion ( ) ;
}
Object . keys ( assertions ) . forEach ( el => {
SkipApi . prototype [ el ] = skipFn ;
} ) ;
}
class Test {
constructor ( options ) {
this . contextRef = options . contextRef ;
this . failWithoutAssertions = options . failWithoutAssertions ;
this . fn = isGeneratorFn ( options . fn ) ? co . wrap ( options . fn ) : options . fn ;
this . metadata = options . metadata ;
this . onResult = options . onResult ;
this . title = options . title ;
2017-08-14 05:01:11 +02:00
this . snapshotInvocationCount = 0 ;
this . compareWithSnapshot = assertionOptions => {
const belongsTo = assertionOptions . id || this . title ;
const expected = assertionOptions . expected ;
const index = assertionOptions . id ? 0 : this . snapshotInvocationCount ++ ;
const label = assertionOptions . id ? '' : assertionOptions . message || ` Snapshot ${ this . snapshotInvocationCount } ` ;
return options . compareTestSnapshot ( { belongsTo , expected , index , label } ) ;
} ;
2017-05-28 00:38:50 +02:00
this . assertCount = 0 ;
this . assertError = undefined ;
this . calledEnd = false ;
this . duration = null ;
this . endCallbackFinisher = null ;
this . finishDueToAttributedError = null ;
this . finishDueToInactivity = null ;
this . finishing = false ;
this . pendingAssertionCount = 0 ;
this . pendingThrowsAssertion = null ;
this . planCount = null ;
this . startedAt = 0 ;
}
bindEndCallback ( ) {
if ( this . metadata . callback ) {
return ( err , stack ) => {
this . endCallback ( err , stack ) ;
} ;
}
throw new Error ( '`t.end()`` is not supported in this context. To use `t.end()` as a callback, you must use "callback mode" via `test.cb(testName, fn)`' ) ;
}
endCallback ( err , stack ) {
if ( this . calledEnd ) {
this . saveFirstError ( new Error ( '`t.end()` called more than once' ) ) ;
return ;
}
this . calledEnd = true ;
if ( err ) {
this . saveFirstError ( new assert . AssertionError ( {
actual : err ,
message : 'Callback called with an error' ,
stack ,
2017-08-14 05:01:11 +02:00
values : [ formatErrorValue ( 'Callback called with an error:' , err ) ]
2017-05-28 00:38:50 +02:00
} ) ) ;
}
if ( this . endCallbackFinisher ) {
this . endCallbackFinisher ( ) ;
}
}
createExecutionContext ( ) {
return new ExecutionContext ( this ) ;
}
countPassedAssertion ( ) {
if ( this . finishing ) {
this . saveFirstError ( new Error ( 'Assertion passed, but test has already finished' ) ) ;
}
this . assertCount ++ ;
}
addPendingAssertion ( promise ) {
if ( this . finishing ) {
this . saveFirstError ( new Error ( 'Assertion passed, but test has already finished' ) ) ;
}
this . assertCount ++ ;
this . pendingAssertionCount ++ ;
promise
. catch ( err => this . saveFirstError ( err ) )
. then ( ( ) => this . pendingAssertionCount -- ) ;
}
addFailedAssertion ( error ) {
if ( this . finishing ) {
this . saveFirstError ( new Error ( 'Assertion failed, but test has already finished' ) ) ;
}
this . assertCount ++ ;
this . saveFirstError ( error ) ;
}
saveFirstError ( err ) {
if ( ! this . assertError ) {
this . assertError = err ;
}
}
plan ( count , planStack ) {
if ( typeof count !== 'number' ) {
throw new TypeError ( 'Expected a number' ) ;
}
this . planCount = count ;
// In case the `planCount` doesn't match `assertCount, we need the stack of
// this function to throw with a useful stack.
this . planStack = planStack ;
}
verifyPlan ( ) {
if ( ! this . assertError && this . planCount !== null && this . planCount !== this . assertCount ) {
this . saveFirstError ( new assert . AssertionError ( {
assertion : 'plan' ,
message : ` Planned for ${ this . planCount } ${ plur ( 'assertion' , this . planCount ) } , but got ${ this . assertCount } . ` ,
operator : '===' ,
stack : this . planStack
} ) ) ;
}
}
verifyAssertions ( ) {
if ( ! this . assertError ) {
if ( this . failWithoutAssertions && ! this . calledEnd && this . planCount === null && this . assertCount === 0 ) {
this . saveFirstError ( new Error ( 'Test finished without running any assertions' ) ) ;
} else if ( this . pendingAssertionCount > 0 ) {
this . saveFirstError ( new Error ( 'Test finished, but an assertion is still pending' ) ) ;
}
}
}
trackThrows ( pending ) {
this . pendingThrowsAssertion = pending ;
}
detectImproperThrows ( err ) {
if ( ! this . pendingThrowsAssertion ) {
return false ;
}
const pending = this . pendingThrowsAssertion ;
this . pendingThrowsAssertion = null ;
const values = [ ] ;
if ( err ) {
2017-08-14 05:01:11 +02:00
values . push ( formatErrorValue ( ` The following error was thrown, possibly before \` t. ${ pending . assertion } () \` could be called: ` , err ) ) ;
2017-05-28 00:38:50 +02:00
}
this . saveFirstError ( new assert . AssertionError ( {
assertion : pending . assertion ,
fixedSource : { file : pending . file , line : pending . line } ,
improperUsage : true ,
message : ` Improper usage of \` t. ${ pending . assertion } () \` detected ` ,
stack : err instanceof Error && err . stack ,
values
} ) ) ;
return true ;
}
waitForPendingThrowsAssertion ( ) {
return new Promise ( resolve => {
this . finishDueToAttributedError = ( ) => {
resolve ( this . finishPromised ( ) ) ;
} ;
this . finishDueToInactivity = ( ) => {
this . detectImproperThrows ( ) ;
resolve ( this . finishPromised ( ) ) ;
} ;
// Wait up to a second to see if an error can be attributed to the
// pending assertion.
globals . setTimeout ( ( ) => this . finishDueToInactivity ( ) , 1000 ) . unref ( ) ;
} ) ;
}
attributeLeakedError ( err ) {
if ( ! this . detectImproperThrows ( err ) ) {
return false ;
}
this . finishDueToAttributedError ( ) ;
return true ;
}
callFn ( ) {
try {
return {
ok : true ,
retval : this . fn ( this . createExecutionContext ( ) )
} ;
} catch ( err ) {
return {
ok : false ,
error : err
} ;
}
}
run ( ) {
this . startedAt = globals . now ( ) ;
const result = this . callFn ( ) ;
if ( ! result . ok ) {
if ( ! this . detectImproperThrows ( result . error ) ) {
this . saveFirstError ( new assert . AssertionError ( {
message : 'Error thrown in test' ,
stack : result . error instanceof Error && result . error . stack ,
2017-08-14 05:01:11 +02:00
values : [ formatErrorValue ( 'Error thrown in test:' , result . error ) ]
2017-05-28 00:38:50 +02:00
} ) ) ;
}
return this . finish ( ) ;
}
const returnedObservable = isObservable ( result . retval ) ;
const returnedPromise = isPromise ( result . retval ) ;
let promise ;
if ( returnedObservable ) {
promise = observableToPromise ( result . retval ) ;
} else if ( returnedPromise ) {
// `retval` can be any thenable, so convert to a proper promise.
promise = Promise . resolve ( result . retval ) ;
}
if ( this . metadata . callback ) {
if ( returnedObservable || returnedPromise ) {
const asyncType = returnedObservable ? 'observables' : 'promises' ;
this . saveFirstError ( new Error ( ` Do not return ${ asyncType } from tests declared via \` test.cb(...) \` , if you want to return a promise simply declare the test via \` test(...) \` ` ) ) ;
return this . finish ( ) ;
}
if ( this . calledEnd ) {
return this . finish ( ) ;
}
return new Promise ( resolve => {
this . endCallbackFinisher = ( ) => {
resolve ( this . finishPromised ( ) ) ;
} ;
this . finishDueToAttributedError = ( ) => {
resolve ( this . finishPromised ( ) ) ;
} ;
this . finishDueToInactivity = ( ) => {
this . saveFirstError ( new Error ( '`t.end()` was never called' ) ) ;
resolve ( this . finishPromised ( ) ) ;
} ;
} ) ;
}
if ( promise ) {
return new Promise ( resolve => {
this . finishDueToAttributedError = ( ) => {
resolve ( this . finishPromised ( ) ) ;
} ;
this . finishDueToInactivity = ( ) => {
const err = returnedObservable ?
new Error ( 'Observable returned by test never completed' ) :
new Error ( 'Promise returned by test never resolved' ) ;
this . saveFirstError ( err ) ;
resolve ( this . finishPromised ( ) ) ;
} ;
promise
. catch ( err => {
if ( ! this . detectImproperThrows ( err ) ) {
this . saveFirstError ( new assert . AssertionError ( {
message : 'Rejected promise returned by test' ,
stack : err instanceof Error && err . stack ,
2017-08-14 05:01:11 +02:00
values : [ formatErrorValue ( 'Rejected promise returned by test. Reason:' , err ) ]
2017-05-28 00:38:50 +02:00
} ) ) ;
}
} )
. then ( ( ) => resolve ( this . finishPromised ( ) ) ) ;
} ) ;
}
return this . finish ( ) ;
}
finish ( ) {
this . finishing = true ;
if ( ! this . assertError && this . pendingThrowsAssertion ) {
return this . waitForPendingThrowsAssertion ( ) ;
}
this . verifyPlan ( ) ;
this . verifyAssertions ( ) ;
this . duration = globals . now ( ) - this . startedAt ;
let reason = this . assertError ;
let passed = ! reason ;
if ( this . metadata . failing ) {
passed = ! passed ;
if ( passed ) {
reason = undefined ;
} else {
reason = new Error ( 'Test was expected to fail, but succeeded, you should stop marking the test as failing' ) ;
}
}
this . onResult ( {
passed ,
result : this ,
reason
} ) ;
return passed ;
}
finishPromised ( ) {
return new Promise ( resolve => {
resolve ( this . finish ( ) ) ;
} ) ;
}
}
module . exports = Test ;