UNPKG

@jhermsmeier/control

Version:

Minimal, speedy test framework

317 lines (255 loc) 7.76 kB
const GeneratorFunction = (function*() {}).constructor const AsyncFunction = (async function() {}).constructor const AsyncGeneratorFunction = (async function*() {}).constructor function isFunction( value ) { return value instanceof Function || value instanceof AsyncFunction || value instanceof GeneratorFunction || value instanceof AsyncGeneratorFunction } class Context { constructor( label ) { // Meta this.label = label || '' this.skip = false // Structure this.parent = null this.parents = [] this.children = [] // Before/after once this.setup = [] this.teardown = [] // Before/after each this.before = [] this.after = [] // Tests this.tasks = [] this.only = [] // Context lifecycle error (setup/teardown) this.error = null // Performance this.timing = { setup: 0, tasks: 0, teardown: 0, total: 0, } } get hasTasks() { return this.tasks.length > 0 || this.only.length > 0 } runTask( task ) { return new Promise( async function runTask( resolve, reject ) { let done = function done( error ) { error ? reject( error ) : resolve() } try { let retval = task.fn( done ) if( retval instanceof Promise ) { await retval; resolve() } else if( task.fn.length == 0 ) { resolve() } } catch( error ) { reject( error ) } }) } async runLifeCycle( type ) { let tasks = Array.isArray( this[ type ] ) ? this[ type ] : [] for( let task of tasks ) { let taskTime = performance.now() try { await this.runTask( task ) } catch( error ) { task.error = error throw error } finally { task.timing.task = performance.now() - taskTime task.timing.total = task.timing.task Control.emit( type, task ) } } } async runTests() { var tasks = this.only.length ? this.only : this.tasks for( let task of tasks ) { task.skip = task.skip || task.context.skip if( task.skip ) { Control.emit( 'test', task ) continue } let beforeTime = performance.now() await this.runLifeCycle( 'before' ) task.timing.before = performance.now() - beforeTime let taskTime = performance.now() try { await this.runTask( task ) } catch( error ) { task.error = error } finally { task.timing.task = performance.now() - taskTime let afterTime = performance.now() await this.runLifeCycle( 'after' ) task.timing.after = performance.now() - afterTime task.timing.total = task.timing.before + task.timing.task + task.timing.after Control.emit( 'test', task ) } } } async run() { Control.emit( 'context', this ) var contextTime = performance.now() if( !this.skip && this.setup.length ) { let setupTime = performance.now() try { await this.runLifeCycle( 'setup' ) } catch( error ) { this.error = error } this.timing.setup = performance.now() - setupTime } if( this.error != null ) { this.timing.total = performance.now() - contextTime Control.emit( 'context:end', this ) return } var taskTime = performance.now() await this.runTests() this.timing.tasks = performance.now() - taskTime if( !this.skip && this.teardown.length ) { let teardownTime = performance.now() try { await this.runLifeCycle( 'teardown' ) } catch( error ) { this.error = error } this.timing.teardown = performance.now() - teardownTime } this.timing.total = performance.now() - contextTime Control.emit( 'context:end', this ) if( this.error != null ) { return } if( this.children.length ) { this.children.sort( Control.sortContexts ) for( let context of this.children ) { await context.run() } } } } class Task { constructor( context, label, fn ) { this.context = context this.label = label || '' this.fn = fn this.skip = false this.error = null this.timing = { before: 0, task: 0, after: 0, total: 0, } } } class Control { static rootContext = new Context() static currentContext = this.rootContext static Context = Context static Task = Task static _listeners = new Map() static on( eventType, handler ) { this._listeners.has( eventType ) ? this._listeners.get( eventType ).push( handler ) : this._listeners.set( eventType, [ handler ] ) } static emit( eventType, ...argv ) { var listeners = this._listeners.get( eventType ) if( listeners ) { for( let i = 0; i < listeners.length; i++ ) { let handler = listeners[i] if( isFunction( handler ) ) { handler.apply( this, argv ) } else if( isFunction( handler.handleEvent ) ) { handler.handleEvent( eventType, ...argv ) } } } } static collator = new Intl.Collator( 'en', { usage: 'sort', numeric: true, }) static sortContexts( a, b ) { return Control.collator.compare( a.label, b.label ) } static context( label, fn ) { if( isFunction( label ) ) { fn = label; label = fn.name } var context = new Context( label ) context.skip = this.currentContext.skip context.parent = this.currentContext context.parents = [].concat( this.currentContext.parents, this.currentContext ) this.currentContext.children.push( context ) this.currentContext = context fn.call( this.currentContext ) this.currentContext = context.parent } static skipContext( label, fn ) { if( isFunction( label ) ) { fn = label; label = fn.name } var context = new Context( label ) context.skip = true context.parent = this.currentContext context.parents = [].concat( this.currentContext.parents, this.currentContext ) this.currentContext.children.push( context ) this.currentContext = context fn.call( this.currentContext ) this.currentContext = context.parent } static setup( label, fn ) { if( isFunction( label ) ) { fn = label; label = fn.name } var task = new Task( this.currentContext, label, fn ) this.currentContext.setup.push( task ) } static before( label, fn ) { if( isFunction( label ) ) { fn = label; label = fn.name } var task = new Task( this.currentContext, label, fn ) this.currentContext.before.push( task ) } static after( label, fn ) { if( isFunction( label ) ) { fn = label; label = fn.name } var task = new Task( this.currentContext, label, fn ) this.currentContext.after.push( task ) } static test( label, fn ) { if( isFunction( label ) ) { fn = label; label = fn.name } var task = new Task( this.currentContext, label, fn ) this.currentContext.tasks.push( task ) } static skip( label, fn ) { if( isFunction( label ) ) { fn = label; label = fn.name } var task = new Task( this.currentContext, label, fn ) task.skip = true this.currentContext.tasks.push( task ) } static only( label, fn ) { if( isFunction( label ) ) { fn = label; label = fn.name } var task = new Task( this.currentContext, label, fn ) this.currentContext.only.push( task ) } static teardown( label, fn ) { if( isFunction( label ) ) { fn = label; label = fn.name } var task = new Task( this.currentContext, label, fn ) this.currentContext.teardown.push( task ) } static async run() { this.emit( 'start' ) await this.rootContext.run() this.emit( 'end' ) } } export default Control