UNPKG

unexpected

Version:

Minimalistic BDD assertion toolkit inspired by [expect.js](https://github.com/LearnBoost/expect.js)

488 lines (433 loc) 18.8 kB
var shim = require('./shim'); var forEach = shim.forEach; var getKeys = shim.getKeys; var every = shim.every; var indexOf = shim.indexOf; var utils = require('./utils'); var isRegExp = utils.isRegExp; var isArray = utils.isArray; module.exports = function (expect) { expect.addAssertion('[not] to be (ok|truthy)', function (expect, subject) { this.assert(subject); }); expect.addAssertion('[not] to be', function (expect, subject, value) { if (typeof subject === 'string' && typeof value === 'string') { expect(subject, '[not] to equal', value); } else { expect(subject === value, '[not] to be truthy'); } }); expect.addAssertion('[not] to be true', function (expect, subject) { expect(subject, '[not] to be', true); }); expect.addAssertion('[not] to be false', function (expect, subject) { expect(subject, '[not] to be', false); }); expect.addAssertion('[not] to be falsy', function (expect, subject) { expect(subject, '[!not] to be truthy'); }); expect.addAssertion('[not] to be null', function (expect, subject) { expect(subject, '[not] to be', null); }); expect.addAssertion('[not] to be undefined', function (expect, subject) { expect(typeof subject, '[not] to be', 'undefined'); }); expect.addAssertion('[not] to be NaN', function (expect, subject) { expect(isNaN(subject), '[not] to be true'); }); expect.addAssertion('[not] to be close to', function (expect, subject, value, epsilon) { this.errorMode = 'bubble'; if (typeof epsilon !== 'number') { epsilon = 1e-9; } try { expect(Math.abs(subject - value), '[not] to be less than or equal to', epsilon); } catch (e) { expect.fail('expected {0} {1} {2} (epsilon: {3})', expect.inspect(subject), this.testDescription, expect.inspect(value), epsilon.toExponential()); } }); expect.addAssertion('[not] to be (a|an)', function (expect, subject, type) { if ('string' === typeof type) { // typeof with support for 'array' expect('array' === type ? isArray(subject) : 'object' === type ? 'object' === typeof subject && null !== subject : /^reg(?:exp?|ular expression)$/.test(type) ? isRegExp(subject) : type === typeof subject, '[not] to be true'); } else { expect(subject instanceof type, '[not] to be true'); } return this; }); // Alias for common '[not] to be (a|an)' assertions expect.addAssertion('[not] to be (a|an) (boolean|number|string|function|object|array|regexp|regex|regular expression)', function (expect, subject) { var matches = /(.* be (?:a|an)) ([\w\s]+)/.exec(this.testDescription); expect(subject, matches[1], matches[2]); }); forEach(['string', 'array', 'object'], function (type) { expect.addAssertion('to be (the|an) empty ' + type, function (expect, subject) { expect(subject, 'to be a', type); expect(subject, 'to be empty'); }); expect.addAssertion('to be a non-empty ' + type, function (expect, subject) { expect(subject, 'to be a', type); expect(subject, 'not to be empty'); }); }); expect.addAssertion('[not] to match', function (expect, subject, regexp) { expect(regexp.exec(subject), '[not] to be truthy'); }); expect.addAssertion('[not] to have [own] property', function (expect, subject, key, value) { if (arguments.length === 4) { expect(subject, 'to have [own] property', key); expect(subject[key], '[not] to equal', value); } else { expect(this.flags.own ? subject && subject.hasOwnProperty(key) : subject && subject[key] !== undefined, '[not] to be truthy'); } }); expect.addAssertion('[not] to have [own] properties', function (expect, subject, properties) { if (properties && isArray(properties)) { forEach(properties, function (property) { expect(subject, '[not] to have [own] property', property); }); } else if (properties && typeof properties === 'object') { // TODO the not flag does not make a lot of sense in this case if (this.flags.not) { forEach(getKeys(properties), function (property) { expect(subject, 'not to have [own] property', property); }); } else { try { forEach(getKeys(properties), function (property) { var value = properties[property]; if (typeof value === 'undefined') { expect(subject, 'not to have [own] property', property); } else { expect(subject, 'to have [own] property', property, value); } }); } catch (e) { e.actual = expect.sanitize(subject); e.expected = expect.sanitize(properties); for (var propertyName in subject) { if ((!this.flags.own || subject.hasOwnProperty(propertyName)) && !(propertyName in properties)) { e.expected[propertyName] = expect.sanitize(subject[propertyName]); } if (!this.flags.own && !(propertyName in e.actual)) { e.actual[propertyName] = expect.sanitize(subject[propertyName]); } } e.showDiff = true; throw e; } } } else { throw new Error("Assertion '" + this.testDescription + "' only supports " + "input in the form of an Array or an Object."); } }); expect.addAssertion('[not] to have length', function (expect, subject, length) { if (!subject || typeof subject.length !== 'number') { throw new Error("Assertion '" + this.testDescription + "' only supports array like objects"); } expect(subject.length, '[not] to be', length); }); expect.addAssertion('[not] to be empty', function (expect, subject) { var length; if (subject && 'number' === typeof subject.length) { length = subject.length; } else if (isArray(subject) || typeof subject === 'string') { length = subject.length; } else if (subject && typeof subject === 'object') { length = getKeys(subject).length; } else { throw new Error("Assertion '" + this.testDescription + "' only supports strings, arrays and objects"); } expect(length, '[not] to be', 0); }); expect.addAssertion('to be non-empty', function (expect, subject) { expect(subject, 'not to be empty'); }); expect.addAssertion('to [not] [only] have (key|keys)', '[not] to have (key|keys)', function (expect, subject, keys) { keys = isArray(keys) ? keys : Array.prototype.slice.call(arguments, 2); var hasKeys = subject && every(keys, function (key) { return subject.hasOwnProperty(key); }); if (this.flags.only) { expect(hasKeys, 'to be truthy'); expect(getKeys(subject).length === keys.length, '[not] to be truthy'); } else { expect(hasKeys, '[not] to be truthy'); } }); expect.addAssertion('[not] to contain', function (expect, subject, arg) { var args = Array.prototype.slice.call(arguments, 2); var that = this; if ('string' === typeof subject) { forEach(args, function (arg) { expect(subject.indexOf(arg) !== -1, '[not] to be truthy'); }); } else if (isArray(subject)) { forEach(args, function (arg) { expect(subject && indexOf(subject, arg) !== -1, '[not] to be truthy'); }); } else if (subject === null) { expect(that.flags.not, '[not] to be falsy'); } else { throw new Error("Assertion '" + this.testDescription + "' only supports strings and arrays"); } }); expect.addAssertion('[not] to be finite', function (expect, subject) { expect(typeof subject === 'number' && isFinite(subject), '[not] to be truthy'); }); expect.addAssertion('[not] to be infinite', function (expect, subject) { expect(typeof subject === 'number' && !isNaN(subject) && !isFinite(subject), '[not] to be truthy'); }); expect.addAssertion('[not] to be within', function (expect, subject, start, finish) { this.args = [start + '..' + finish]; expect(subject, 'to be a number'); expect(subject >= start && subject <= finish, '[not] to be true'); }); expect.addAssertion('<', '[not] to be (<|less than|below)', function (expect, subject, value) { expect(subject < value, '[not] to be true'); }); expect.addAssertion('<=', '[not] to be (<=|less than or equal to)', function (expect, subject, value) { expect(subject <= value, '[not] to be true'); }); expect.addAssertion('>', '[not] to be (>|greater than|above)', function (expect, subject, value) { expect(subject > value, '[not] to be true'); }); expect.addAssertion('>=', '[not] to be (>=|greater than or equal to)', function (expect, subject, value) { expect(subject >= value, '[not] to be true'); }); expect.addAssertion('[not] to be positive', function (expect, subject) { expect(subject, '[not] to be >', 0); }); expect.addAssertion('[not] to be negative', function (expect, subject) { expect(subject, '[not] to be <', 0); }); expect.addAssertion('[not] to equal', function (expect, subject, value) { try { expect(expect.equal(value, subject), '[not] to be true'); } catch (e) { if (!this.flags.not) { e.expected = expect.sanitize(value); e.actual = expect.sanitize(subject); // Explicitly tell mocha to stringify and diff arrays // and objects, but only when the types are identical // and non-primitive: if (e.actual && e.expected && typeof e.actual === 'object' && typeof e.expected === 'object' && isArray(e.actual) === isArray(e.expected)) { e.showDiff = true; } } throw e; } }); expect.addAssertion('[not] to (throw|throw error|throw exception)', function (expect, subject, arg) { this.errorMode = 'nested'; if (typeof subject !== 'function') { throw new Error("Assertion '" + this.testDescription + "' only supports functions"); } var thrown = false; var argType = typeof arg; try { subject(); } catch (e) { var subject; if (e._isUnexpected) { subject = e.output.toString(); } else if (typeof e === 'string') { subject = e; } else { subject = e.message; } if ('function' === argType) { arg(e); } else if ('string' === argType) { expect(subject, '[not] to equal', arg); } else if (isRegExp(arg)) { expect(subject, '[not] to match', arg); } else if (this.flags.not) { expect.fail('threw: {0}', e._isUnexpected ? e.output : subject); } thrown = true; } this.errorMode = 'default'; if ('string' === argType || isRegExp(arg)) { // in the presence of a matcher, ensure the `not` only applies to // the matching. expect(thrown, 'to be true'); } else { expect(thrown, '[not] to be true'); } }); expect.addAssertion('to be (a|an) [non-empty] (map|hash|object) whose values satisfy', function (expect, subject, callbackOrString) { var callback; if ('function' === typeof callbackOrString) { callback = callbackOrString; } else if ('string' === typeof callbackOrString) { var args = Array.prototype.slice.call(arguments, 2); callback = function (value) { expect.apply(expect, [value].concat(args)); }; } else { throw new Error('Assertion "' + this.testDescription + '" expects a function as argument'); } this.errorMode = 'nested'; expect(subject, 'to be an object'); if (this.flags['non-empty']) { expect(subject, 'to be non-empty'); } this.errorMode = 'bubble'; var errors = {}; forEach(getKeys(subject), function (key, index) { try { callback(subject[key], index); } catch (e) { errors[key] = e; } }); var errorKeys = getKeys(errors); if (errorKeys.length > 0) { expect.fail(function (output) { var subjectOutput = expect.inspect(subject); output.error('failed expectation in'); if (subjectOutput.size().height > 1) { output.nl(); } else { output.sp(); } subjectOutput.error(':'); output.block(subjectOutput).nl(); output.indentLines(); forEach(errorKeys, function (key, index) { var error = errors[key]; output.i().text(key).text(': '); if (error._isUnexpected) { output.block(error.output); } else { output.block(output.clone().text(error.message)); } if (index < errorKeys.length - 1) { output.nl(); } }); }); } }); expect.addAssertion('to be (a|an) [non-empty] array whose items satisfy', function (expect, subject, callbackOrString) { var callback; if ('function' === typeof callbackOrString) { callback = callbackOrString; } else if ('string' === typeof callbackOrString) { var args = Array.prototype.slice.call(arguments, 2); callback = function (item) { expect.apply(expect, [item].concat(args)); }; } else { throw new Error('Assertion "' + this.testDescription + '" expects a function as argument'); } this.errorMode = 'nested'; expect(subject, 'to be an array'); if (this.flags['non-empty']) { expect(subject, 'to be non-empty'); } this.errorMode = 'bubble'; expect(subject, 'to be a map whose values satisfy', callback); }); forEach(['string', 'number', 'boolean', 'array', 'object', 'function', 'regexp', 'regex', 'regular expression'], function (type) { expect.addAssertion('to be (a|an) [non-empty] array of ' + type + 's', function (expect, subject) { expect(subject, 'to be an array whose items satisfy', function (item) { expect(item, 'to be a', type); }); if (this.flags['non-empty']) { expect(subject, 'to be non-empty'); } }); }); expect.addAssertion('to be (a|an) [non-empty] (map|hash|object) whose keys satisfy', function (expect, subject, callbackOrString) { var callback; if ('function' === typeof callbackOrString) { this.errorMode = 'nested'; callback = callbackOrString; } else if ('string' === typeof callbackOrString) { var args = Array.prototype.slice.call(arguments, 2); callback = function (key) { expect.apply(expect, [key].concat(args)); }; } else { throw new Error('Assertion "' + this.testDescription + '" expects a function as argument'); } this.errorMode = 'nested'; expect(subject, 'to be an object'); if (this.flags['non-empty']) { expect(subject, 'to be non-empty'); } this.errorMode = 'bubble'; var errors = {}; forEach(getKeys(subject), function (key, index) { try { callback(key); } catch (e) { errors[key] = e; } }); var errorKeys = getKeys(errors); if (errorKeys.length > 0) { expect.fail(function (output) { output.error('failed expectation on keys ') .text(getKeys(subject).join(', ')) .error(':').nl() .indentLines(); forEach(errorKeys, function (key, index) { var error = errors[key]; output.i().text(key).text(': '); if (error._isUnexpected) { output.block(error.output); } else { output.block(output.clone().text(error.message)); } if (index < errorKeys.length - 1) { output.nl(); } }); }); } }); expect.addAssertion('to be canonical', function (expect, subject, stack) { stack = stack || []; var i; for (i = 0 ; i < stack.length ; i += 1) { if (stack[i] === subject) { return; } } if (subject && typeof subject === 'object') { var keys = getKeys(subject); for (i = 0 ; i < keys.length - 1 ; i += 1) { expect(keys[i], 'to be less than', keys[i + 1]); } stack.push(subject); forEach(keys, function (key) { expect(subject[key], 'to be canonical', stack); }); stack.pop(subject); } }); };