const STUB_EXHAUSTED_ERROR = `stub exhausted, call not expected` ; interface CallList < T > { calls : T [ ] ; callCount : number ; called : boolean ; } type StubbedFn = ( ... args : unknown [ ] ) => unknown ; type Stub < T extends StubbedFn > = T & CallList < Parameters < T > > ; type LazyExtension < T extends StubbedFn > = { return < K = ReturnType < T > > ( value ) : LazyStub < T > ; resolve < K > ( value ) : LazyStub < T > ; reject < K extends any = Error > ( reason ) : LazyStub < T > ; throw < K extends any = Error > ( val ) : LazyStub < T > ; } ; type LazyStub < T extends StubbedFn > = Stub < T > & LazyExtension < T > ; const descriptor = ( calls ) => ( { calls : { value : calls } , callCount : { get ( ) { return calls . length ; } } , called : { get ( ) { return calls . length > 0 ; } } } ) ; const fromAsyncGenerator = < T extends ( ... args : unknown [ ] ) => Promise < unknown > > ( generator ) : Stub < T > => { const calls = [ ] ; return Object . defineProperties ( fn , descriptor ( calls ) ) ; async function fn ( ... args ) { calls . push ( args ) ; const { value , done } = await generator . next ( ) ; if ( done ) { throw new Error ( STUB_EXHAUSTED_ERROR ) ; } return value ; } } ; const fromGenerator = < T extends StubbedFn > ( generator ) : Stub < T > => { const calls = [ ] ; return Object . defineProperties ( fn , descriptor ( calls ) ) ; function fn ( ... args ) { calls . push ( args ) ; const { value , done } = generator . next ( ) ; if ( done ) { throw new Error ( STUB_EXHAUSTED_ERROR ) ; } return value ; } } ; enum CallType { ERROR = 'error' , VALUE = 'value' } interface CallDefinition < T > { type : CallType ; value : T ; } const fromVoid = < T extends StubbedFn > ( ) : LazyStub < T > => { const queue = [ ] ; const generator = ( function * ( ) { while ( true ) { const next = queue . shift ( ) ; if ( ! next ) { break ; } const { type , value } = next ; if ( type === CallType . ERROR ) { throw value ; } yield value ; } } ) ( ) ; const fn = fromGenerator < T > ( generator ) ; const queueCall = < K > ( value , type = CallType . VALUE ) => { queue . push ( { value , type } ) ; return fn as LazyStub < T > ; } ; const extension = { return : queueCall , resolve : < K > ( val ) => queueCall < Promise < K > > ( Promise . resolve ( val ) ) , reject : < K extends any = Error > ( val ) => queueCall < Promise < K > > ( Promise . reject ( val ) ) , throw : < K extends any = Error > ( val ) => queueCall < K > ( val , CallType . ERROR ) } ; return Object . assign ( fn , extension ) ; } ; function stub < T extends StubbedFn > ( generator ) ; function stub < T extends StubbedFn > ( generator ) ; function stub < T extends StubbedFn > ( ) ; function stub < T extends StubbedFn > ( ... args ) ; function stub < T extends ( ... arg : unknown [ ] ) => unknown > ( ... args ) { const [ input ] = args ; if ( typeof input === 'function' ) { const generatorObject = input ( ) ; return generatorObject [ Symbol . asyncIterator ] ? fromAsyncGenerator ( generatorObject ) : fromGenerator ( generatorObject ) ; } return args . length === 0 ? fromVoid ( ) : fromGenerator ( function * ( ) { yield * args ; } ( ) ) ; } export default stub ;