UNPKG

undo3d-run-test

Version:

A collection of utilities for testing Undo3D, and apps built with Undo3D

183 lines (144 loc) 6.59 kB
//// 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'` }