Skip to content

Commit

Permalink
initial implementation of "functional" interface
Browse files Browse the repository at this point in the history
  • Loading branch information
boneskull committed May 30, 2018
1 parent 0d95e3f commit b96abc9
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 111 deletions.
170 changes: 170 additions & 0 deletions lib/behavior.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
'use strict';

/**
* Built-in behaviors.
* These provide hooks into modifying Mocha's behavior for different use cases.
*/

var utils = require('./utils');

/**
* Default Mocha behavior
*/
exports.Default = {
Suite: {
/**
* Runs the suite; called by `Suite.create()`. Calls its callback fn
* with the Suite as its context.
* @this {Suite}
*/
run: function() {
// FUTURE: async suites
this.fn.apply(this);
}
},
Runnable: {
/**
* Determines whether or not we should provide a nodeback parameter and
* expect it to be called
* @this {Runnable}
* @returns {boolean} `true` if we should run the test fn with a callback
*/
shouldRunWithCallback: function() {
return Boolean(this.fn && this.fn.length);
},
/**
* Runs the Runnable synchronously, or potentially returning a Promise
* @param {Function} done - Callback
*/
run: function(done) {
var result = this.fn.call(this.ctx);
if (result && typeof result.then === 'function') {
this.resetTimeout();
result.then(
function() {
done();
// Return null so libraries like bluebird do not warn about
// subsequently constructed Promises.
return null;
},
function(reason) {
done(
reason || new Error('Promise rejected with no or falsy reason')
);
}
);
} else {
if (this.asyncOnly) {
return done(
new Error(
'--async-only option in use without declaring `done()` or returning a promise'
)
);
}
done();
}
},
/**
* Runs the Runnable, passing a nodeback function, which must be called to
* complete the test
* @param {Function} done - Callback
*/
runWithCallback: function(done) {
var result = this.fn.call(this.ctx, function(err) {
if (
err instanceof Error ||
Object.prototype.toString.call(err) === '[object Error]'
) {
return done(err);
}
if (err) {
if (Object.prototype.toString.call(err) === '[object Object]') {
return done(
new Error('done() invoked with non-Error: ' + JSON.stringify(err))
);
}
return done(new Error('done() invoked with non-Error: ' + err));
}
if (result && utils.isPromise(result)) {
return done(
new Error(
'Resolution method is overspecified. Specify a callback *or* return a Promise; not both.'
)
);
}
done();
});
}
}
};

/**
* Provides a test context in a functional manner for use with
* lambdas/arrow functions.
* All Runnables must either return a promise or call `runnable.done()`, where
* `runnable` is the first parameter to the Runnable's callback (`fn`)
*/
exports.Functional = {
Suite: {
/**
* Runs the Suite. Calls its `fn` with NO context and the suite itself
* as the first parameter.
* @this {Suite}
*/
run: function(opts) {
this.fn.call(null, this);
}
},
Runnable: {
/**
* Determines whether or not we should provide a nodeback parameter and
* expect it to be called; always false
* @this {Runnable}
* @returns false
*/
shouldRunWithCallback: function() {
return false;
},

/**
* Runs the Runnable expecting a call to ctx.done() or a Promise
* @param {Function} done - Callback
*/
run: function(done) {
this.ctx.done = function(err) {
if (
err instanceof Error ||
Object.prototype.toString.call(err) === '[object Error]'
) {
return done(err);
}
if (err) {
if (Object.prototype.toString.call(err) === '[object Object]') {
return done(
new Error('done() invoked with non-Error: ' + JSON.stringify(err))
);
}
return done(new Error('done() invoked with non-Error: ' + err));
}
done();
};
var result = this.fn.call(null, this.ctx);
if (result && typeof result.then === 'function') {
this.resetTimeout();
result.then(
function() {
done();
// Return null so libraries like bluebird do not warn about
// subsequently constructed Promises.
return null;
},
function(reason) {
done(
reason || new Error('Promise rejected with no or falsy reason')
);
}
);
}
}
}
};
7 changes: 4 additions & 3 deletions lib/interfaces/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ module.exports = function(suites, context, mocha) {
*/
runWithSuite: function runWithSuite(suite) {
return function run() {
suite.run();
suite.runIfRoot();
};
},

Expand Down Expand Up @@ -104,12 +104,13 @@ module.exports = function(suites, context, mocha) {
var suite = Suite.create(suites[0], opts.title);
suite.pending = Boolean(opts.pending);
suite.file = opts.file;
suite.fn = opts.fn;
suites.unshift(suite);
if (opts.isOnly) {
suite.parent._onlySuites = suite.parent._onlySuites.concat(suite);
suite.appendExclusiveToParent();
}
if (typeof opts.fn === 'function') {
opts.fn.call(suite);
suite.callBehavior('run', opts);
suites.shift();
} else if (typeof opts.fn === 'undefined' && !suite.pending) {
throw new Error(
Expand Down
24 changes: 24 additions & 0 deletions lib/interfaces/func.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

var bddInterface = require('./bdd');
var behavior = require('../behavior');

/**
* Functional BDD-style interface:
*
* describe('Array', () => {
* describe('#indexOf()', () => {
* it('should return -1 when not present', () => {
* // ...
* });
* it('passes ctx as param', test => {})
* it('uses async as second param , )
* });
* });
*
* @param {Suite} suite Root suite.
*/
module.exports = function funcInterface(suite) {
suite.behavior(behavior.Functional);
bddInterface(suite);
};
1 change: 1 addition & 0 deletions lib/interfaces/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ exports.bdd = require('./bdd');
exports.tdd = require('./tdd');
exports.qunit = require('./qunit');
exports.exports = require('./exports');
exports.func = require('./func');
Loading

0 comments on commit b96abc9

Please sign in to comment.