undo3d-run-test
Version:
A collection of utilities for testing Undo3D, and apps built with Undo3D
183 lines (144 loc) • 6.59 kB
JavaScript
//// Level
////
//// The base class for test-levels. These can be:
//// - The top-level test runner, which contains several test suites
//// - A test suite, which contains several test cases
//// - A test case, which contains the actual test code
//// CLASS
export default class Level {
constructor (options={}) {
const { hub, index, title, first, last, skip } = options
const { before, after, beforeEach, afterEach } = options
//// Set the event hub.
if ( invalidHub(hub) ) throw Error( invalidHub(hub) )
this._hub = hub
//// Set the index. Note that a LevelRunner always has index `1`.
if ( invalidIndex(index) ) throw Error( invalidIndex(index) )
this._index = index
//// Set the title, or create it if not in `options.title`.
if ( invalidTitle(title) ) throw Error( invalidTitle(title) )
this._title = (title || 'Untitled ' + this.constructor.name).trim()
//// Set the reason that this level has been skipped.
if ( invalidSkip(skip) ) throw Error( invalidSkip(skip) )
if (null == skip && /^\s*# skip\S*\s+.+/i.test(this._title) )
this._skip = ( this._title.match(/^\s*# skip\S*\s+(.+)/i) )[1]
else
this._skip = skip ? skip.trim() : null
//// Cases, suites and the runner all have a `first` and a `last`.
if ( invalidFirst(first) ) throw Error( invalidFirst(first) )
if ( invalidLast(last) ) throw Error( invalidLast(last) )
this._first = first || 1
this._last = null == last ? Infinity : last // Infinity will be rounded down
if (first > last)
throw Error(`options.first ${first} is > options.last ${last}`)
//// Cases, suites and the runner all have a `status` and a `result`.
this._status = this._skip ? '# skip' : 'pending' // can also be 'running', 'ok' or 'not ok'
this._result = null
//// Set the 4 hooks: before(), after(), beforeEach() and afterEach().
if ( invalidHook(before) ) throw Error( invalidHook(before) )
if ( invalidHook(after) ) throw Error( invalidHook(after) )
if ( invalidHook(beforeEach) ) throw Error( invalidHook(beforeEach) )
if ( invalidHook(afterEach) ) throw Error( invalidHook(afterEach) )
const noop = () => {}
this._before = before || noop
this._after = after || noop
this._beforeEach = beforeEach || noop
this._afterEach = afterEach || noop
}
//// PUBLIC PROPERTIES
//// Xx (read-only).
get hub () { return this._hub }
get index () { return this._index }
get title () { return this._title }
get skip () { return this._skip }
get first () { return this._first }
get last () { return this._last }
get status () { return this._status }
get result () { return this._result }
//// Hooks (read-only).
get before () { return this._before }
get after () { return this._after }
get beforeEach () { return this._beforeEach }
get afterEach () { return this._afterEach }
//// Used by tapline. '=' for parallel suites, '^' for async test cases
get dash () { return '-' }
//// The runner and suite override these.
get first () { return this._first }
get last () { return this._last }
//// Xx.
get tapPlan () {
return `${this.first}..${this.last}`
}
get tapTestLine () {
const { index, title, status, dash } = this
return status + ' '.repeat(7-status.length)
+ ' '.repeat(3-(index+'').length) + index
+ ' ' + dash + ' '
+ title
}
//// PUBLIC METHODS
//// Add an event listener. The behavior of this method depends on the `hub`
//// option passed to the constructor. If no `hub` option was passed, an
//// instance of the Hub class defined in runner/hub.mjs is used.
on (...args) { return this._hub.on.apply(this._hub, args) }
}
//// VALIDATE
function invalidHub (hub) {
if (null == hub)
return 'options.hub must be set in ' + this.constructor.name
if ('object' !== typeof hub)
return `options.hub is type '${typeof hub}' not 'object'`
if ('function' !== typeof hub.on)
return `options.hub.on is type '${typeof hub.on}' not 'function'`
if ('function' !== typeof hub.fire)
return `options.hub.fire is type '${typeof hub.fire}' not 'function'`
}
function invalidIndex (index) {
if (null == index)
return `options.index must be set`
if ('number' !== typeof index)
return `options.index is type '${typeof index}' not 'number'`
if (~~index !== index || 1 > index)
return `options.index is ${index}, which is not a positive integer`
}
function invalidTitle (title) {
if (null == title) return // optional
if ('string' !== typeof title)
return `options.title is type '${typeof title}' not 'string'`
const len = title.trim().length
if (0 === len)
return `options.title must not be an empty string or entirely whitespace`
if (64 < len)
return `options.title is ${len} characters, must be 64 or less`
//@TODO reject control characters, invisibles and other weird unicode
}
function invalidFirst (first) {
if (null == first) return // optional
if ('number' !== typeof first)
return `options.first is type '${typeof first}' not 'number'`
if (~~first !== first || 1 > first)
return `options.first is ${first}, which is not a positive integer`
}
function invalidLast (last) {
if (null == last) return // optional
if ('number' !== typeof last)
return `options.last is type '${typeof last}' not 'number'`
if (~~last !== last || 0 > last) // '1..0' means don’t run anything
return `options.last is ${last}, which is not 0 or a positive integer`
}
function invalidSkip (skip) {
if (null == skip) return // optional
if ('string' !== typeof skip)
return `options.skip is type '${typeof skip}' not 'string'`
const len = skip.trim().length
if (0 === len)
return `options.skip must not be an empty string or entirely whitespace`
if (64 < len)
return `options.skip is ${len} characters, must be 64 or less`
//@TODO reject control characters, invisibles and other weird unicode
}
function invalidHook (hook) {
if (null == hook) return // optional
if ('function' !== typeof hook)
return `An options hook is type '${typeof hook}' not 'function'`
}