import path from 'path' ; import chalk from 'chalk' ; import * as StepName from './StepName' ; import ErrorStackParser from 'error-stack-parser' ; import prettyFormatStep from './prettyFormatStep' ; import { IStep , ActionFunction , StepDefName , ITestLoadLogger , IPrescriptAPI } from './types' ; import { PendingError } from './PendingError' ; type StackTrace = ErrorStackParser . StackFrame [ ] ; export interface ITestLoadOptions { logger ? : ITestLoadLogger | null ; } function loadTest ( testModule , options = { } ) { const { logger : inLogger = createConsoleLogger ( ) } = options ; const implicitRoot = { name : StepName . named `[implicit test]` , children : [ ] } ; const tests = [ ] ; const usedTestNames = { } ; let currentStep ; let currentTest ; const logger = inLogger || createNullLogger ( ) ; const independentContextSet = new Set < IStep > ( ) ; function appendStep < X > ( options , f ) { const { name , creator , definition , cleanup , defer , pending } = options ; if ( ! currentStep ) { throw new Error ( 'Invalid state... This should not happen! currentStep is null.' ) ; } if ( currentStep . action ) { throw new Error ( 'A step may only have an action or sub-steps but not both.' ) ; } const parentStep = currentStep ; if ( ! parentStep . children ) { parentStep . children = [ ] ; } const independent = independentContextSet . has ( parentStep ) ; const number = ( parentStep . number ? parentStep . number + '.' : '' ) + ( parentStep . children . length + 1 ) ; const childStep = { name , independent , creator , definition , cleanup , number , pending , defer } ; parentStep . children . push ( childStep ) ; logger . step ( childStep ) ; try { currentStep = childStep ; const result = f ( ) ; if ( ! childStep . children && ! childStep . action ) { throw new Error ( 'Unexpected empty step. A step must either have children or an action.' ) ; } return result ; } finally { currentStep = parentStep ; } } function setAction ( f , definition ) { if ( ! currentStep ) { throw new Error ( 'Invalid state... This should not happen! currentStep is null.' ) ; } if ( currentStep . action ) { throw new Error ( 'A step may only have one action block.' ) ; } if ( currentStep . children ) { throw new Error ( 'A step may only have an action or sub-steps but not both.' ) ; } currentStep . action = f ; currentStep . actionDefinition = definition ; } function getSource ( stackTrace ) { const stackFrame = stackTrace . filter ( frame => frame . fileName && path . relative ( __dirname , frame . fileName ) . startsWith ( '..' ) ) [ 0 ] ; if ( ! stackFrame ) return '(unknown)' ; return stackFrame . fileName + ':' + stackFrame . lineNumber ; } function appendTest < X > ( name , f ) { if ( currentTest && currentTest . root === implicitRoot ) { if ( implicitRoot . children && implicitRoot . children . length ) { throw new Error ( 'An implicit test has been started.' ) ; } currentTest = null ; currentStep = null ; } if ( currentTest ) { throw new Error ( 'test() calls may not be nested.' ) ; } const nameStr = String ( name ) ; if ( usedTestNames [ nameStr ] ) { throw new Error ( `Test name must be unique. A test named "${ nameStr }" has already been declared.` ) ; } usedTestNames [ nameStr ] = true ; const root = ( currentStep = { name : name , children : [ ] } ) ; logger . test ( name ) ; currentTest = createTest ( root ) ; tests . push ( root ) ; try { const value = f ( ) ; finishTest ( ) ; return value ; } finally { currentStep = null ; currentTest = null ; } } function ensureDeprecatedAPIUsersDoNotUseTaggedTemplate ( strings ) { if ( Array . isArray ( strings ) ) { throw new Error ( 'The old deprecated API does not support tagged template literal syntax.' ) ; } } const DEFAULT_COMPOSITE_STEP = ( ) => { context . pending ( ) ; } ; const DEFAULT_ACTION_STEP = async ( ) => { throw new PendingError ( ) ; } ; const context = { step < X > ( inName , f ) { ensureDeprecatedAPIUsersDoNotUseTaggedTemplate ( inName ) ; const name = StepName . coerce ( inName ) ; const definition = getSource ( ErrorStackParser . parse ( new Error ( `Step: ${ name }` ) ) ) ; return appendStep ( { name , definition } , f ) ; } , test ( ... args ) { if ( isTemplateString ( args [ 0 ] ) ) { const name = StepName . named ( args [ 0 ] , ... args . slice ( 1 ) ) ; return < X > ( f ) => appendTest ( name , f ) ; } else { const name = StepName . coerce ( args [ 0 ] ) ; return appendTest ( name , args [ 1 ] ) ; } } , cleanup < X > ( inName , f ) { ensureDeprecatedAPIUsersDoNotUseTaggedTemplate ( inName ) ; const name = StepName . coerce ( inName ) ; const definition = getSource ( ErrorStackParser . parse ( new Error ( `Cleanup step: ${ name }` ) ) ) ; return appendStep ( { name , definition , cleanup : true } , f ) ; } , onFinish ( f ) { if ( ! currentStep ) { throw new Error ( 'Invalid state... This should not happen! currentStep is null.' ) ; } if ( ! currentTest ) { throw new Error ( 'Invalid state... This should not happen! currentTest is null.' ) ; } const definition = getSource ( ErrorStackParser . parse ( new Error ( `onFinish` ) ) ) ; const cause = currentStep . number ; currentTest . finishActions . push ( { action : f , definition , cause } ) ; } , use < X > ( m ) { return m ( context ) ; } , action < X > ( ... args ) { if ( isTemplateString ( args [ 0 ] ) ) { const name = StepName . named ( args [ 0 ] , ... args . slice ( 1 ) ) ; const definition = getSource ( ErrorStackParser . parse ( new Error ( `Action Step: ${ name }` ) ) ) ; return ( f = DEFAULT_ACTION_STEP ) => appendStep ( { name , definition } , ( ) => { return setAction ( f , definition ) ; } ) ; } else if ( ! args [ 1 ] ) { const definition = getSource ( ErrorStackParser . parse ( new Error ( `Action definition` ) ) ) ; const f = args [ 0 ] as ActionFunction ; return setAction ( f , definition ) ; } else { const name = StepName . coerce ( args [ 0 ] ) ; const f = args [ 1 ] as ActionFunction ; const definition = getSource ( ErrorStackParser . parse ( new Error ( `Action Step: ${ name }` ) ) ) ; return appendStep ( { name , definition } , ( ) => { return setAction ( f , definition ) ; } ) ; } } , defer ( ... args ) { if ( isTemplateString ( args [ 0 ] ) ) { const name = StepName . named ( args [ 0 ] , ... args . slice ( 1 ) ) ; const definition = getSource ( ErrorStackParser . parse ( new Error ( `Deferred Action Step: ${ name }` ) ) ) ; return ( f = DEFAULT_ACTION_STEP ) => appendStep ( { name , definition , defer : true } , ( ) => { return setAction ( f , definition ) ; } ) ; } else { const name = StepName . coerce ( args [ 0 ] ) ; const definition = getSource ( ErrorStackParser . parse ( new Error ( `Deferred Action Step: {$name}` ) ) ) ; const f = args [ 1 ] as ActionFunction ; return appendStep ( { name , definition , defer : true } , ( ) => { return setAction ( f , definition ) ; } ) ; } } , to ( ... args ) { if ( isTemplateString ( args [ 0 ] ) ) { const name = StepName . named ( args [ 0 ] , ... args . slice ( 1 ) ) ; const definition = getSource ( ErrorStackParser . parse ( new Error ( `Composite Step: ${ name }` ) ) ) ; return < X > ( f = DEFAULT_COMPOSITE_STEP as any ) => appendStep ( { name , definition } , f ) ; } else { const name = StepName . coerce ( args [ 0 ] ) ; const definition = getSource ( ErrorStackParser . parse ( new Error ( `Composite Step: ${ name }` ) ) ) ; const f = args [ 1 ] ; return appendStep ( { name , definition } , f ) ; } } , pending ( ) { const error = new PendingError ( ) ; const definition = getSource ( ErrorStackParser . parse ( new Error ( `Pending` ) ) ) ; return appendStep ( { name : StepName . coerce ( 'Pending' ) , definition , pending : true } , ( ) => { context . action ( ( ) => { throw error ; } ) ; } ) ; } , independent ( f ) { if ( ! currentStep ) { throw new Error ( 'Invalid state... This should not happen! currentStep is null.' ) ; } independentContextSet . add ( currentStep ) ; try { return f ( ) ; } finally { independentContextSet . delete ( currentStep ) ; } } } ; function finishTest ( ) { if ( ! currentTest ) { throw new Error ( 'Invalid state... currentTest is null.' ) ; } for ( const entry of currentTest . finishActions ) { appendStep ( { name : StepName . coerce ( 'Post-test actions' ) , creator : entry . cause , definition : entry . definition , cleanup : true } , ( ) => { entry . action ( ) ; } ) ; } } function load ( ) { currentStep = implicitRoot ; currentTest = createTest ( implicitRoot ) ; try { testModule ( context ) ; if ( currentTest !== null ) { finishTest ( ) ; } } finally { currentStep = null ; currentTest = null ; } if ( ! tests . length && implicitRoot . children && implicitRoot . children . length ) { tests . push ( implicitRoot ) ; } return tests ; } return load ( ) ; } export function createConsoleLogger ( ) { return { step ( step ) { console . log ( chalk . dim ( ( step . defer ? 'Deferred ' : '' ) + 'Step' ) , prettyFormatStep ( step ) ) ; } , test ( name ) { console . log ( chalk . yellow ( `### ${ StepName . format ( name ) }` ) ) ; } } ; } export function createNullLogger ( ) { return { step ( step ) { } , test ( name ) { } } ; } interface ITest { root : IStep ; finishActions : { action : ( ) => void ; definition : string ; cause ? : string ; } [ ] ; } function createTest ( root ) { return { root , finishActions : [ ] } ; } export default loadTest ; function isTemplateString ( input ) { return Array . isArray ( input ) ; }