unexpected
Version:
Minimalistic BDD assertion toolkit inspired by [expect.js](https://github.com/LearnBoost/expect.js)
1,419 lines (1,261 loc) • 134 kB
JavaScript
/*!
* Copyright (c) 2013 Sune Simonsen <sune@we-knowhow.dk>
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the 'Software'), to deal in the Software without
* restriction, including without limitation the rights to use, copy,
* modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var o;"undefined"!=typeof window?o=window:"undefined"!=typeof global?o=global:"undefined"!=typeof self&&(o=self),(o.weknowhow||(o.weknowhow={})).expect=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var shim = require(5);
var bind = shim.bind;
var forEach = shim.forEach;
function Assertion(expect, subject, testDescription, flags, args) {
this.expect = expect;
this.obj = subject; // deprecated
this.equal = bind(expect.equal, expect); // deprecated
this.eql = this.equal; // deprecated
this.inspect = bind(expect.inspect, expect); // deprecated
this.subject = subject;
this.testDescription = testDescription;
this.flags = flags;
this.args = args;
this.errorMode = 'default';
}
Assertion.prototype.standardErrorMessage = function () {
var expect = this.expect;
var output = expect.output.clone();
output.error('expected ')
.append(expect.inspect(this.subject))
.sp().error(this.testDescription);
if (this.args.length > 0) {
output.sp();
forEach(this.args, function (arg, index) {
output.append(expect.inspect(arg));
if (index < this.args.length - 1) {
output.text(', ');
}
}, this);
}
return output;
};
Assertion.prototype.throwStandardError = function () {
var err = new Error();
err.output = this.standardErrorMessage();
err._isUnexpected = true;
throw err;
};
Assertion.prototype.assert = function (condition) {
var not = !!this.flags.not;
condition = !!condition;
if (condition === not) {
this.throwStandardError();
}
};
module.exports = Assertion;
},{}],2:[function(require,module,exports){
var Assertion = require(1);
var shim = require(5);
var utils = require(7);
var magicpen = require(15);
var bind = shim.bind;
var forEach = shim.forEach;
var filter = shim.filter;
var map = shim.map;
var trim = shim.trim;
var reduce = shim.reduce;
var getKeys = shim.getKeys;
var indexOf = shim.indexOf;
var truncateStack = utils.truncateStack;
var extend = utils.extend;
var levenshteinDistance = utils.levenshteinDistance;
var isArray = utils.isArray;
var anyType = {
name: 'any',
identify: function (value) {
return true;
},
equal: function (a, b) {
return a === b;
},
inspect: function (output, value) {
return output.text(value);
},
toJSON: function (value) {
return value;
}
};
function Unexpected(options) {
options = options || {};
this.assertions = options.assertions || {};
this.types = options.types || [anyType];
this.output = options.output || magicpen();
this._outputFormat = options.format || magicpen.defaultFormat;
}
Unexpected.prototype.equal = function (actual, expected, depth, seen) {
var that = this;
depth = depth || 0;
if (depth > 500) {
// detect recursive loops in the structure
seen = seen || [];
if (seen.indexOf(actual) !== -1) {
throw new Error('Cannot compare circular structures');
}
seen.push(actual);
}
var matchingCustomType = utils.findFirst(this.types || [], function (type) {
return type.identify(actual) && type.identify(expected);
});
if (matchingCustomType) {
return matchingCustomType.equal(actual, expected, function (a, b) {
return that.equal(a, b, depth + 1, seen);
});
}
return false; // we should never get there
};
Unexpected.prototype.inspect = function (obj, depth) {
var types = this.types;
var seen = [];
var printOutput = function (output, obj, depth) {
if (depth === 0) {
return output.text('...');
}
seen = seen || [];
if (indexOf(seen, obj) !== -1) {
return output.text('[Circular]');
}
var matchingCustomType = utils.findFirst(types || [], function (type) {
return type.identify(obj);
});
if (matchingCustomType) {
return matchingCustomType.inspect(output, obj, function (output, v) {
seen.push(obj);
return printOutput(output, v, depth - 1);
}, depth);
} else {
return output;
}
};
return printOutput(this.output.clone(), obj, depth || 3);
};
var placeholderSplitRegexp = /(\{(?:\d+)\})/g;
var placeholderRegexp = /\{(\d+)\}/;
Unexpected.prototype.fail = function (arg) {
var output = this.output.clone();
if (typeof arg === 'function') {
arg.call(this, output);
} else {
var message = arg || "explicit failure";
var args = Array.prototype.slice.call(arguments, 1);
var tokens = message.split(placeholderSplitRegexp);
forEach(tokens, function (token) {
var match = placeholderRegexp.exec(token);
if (match) {
var index = match[1];
var placeholderArg = index in args ? args[index] : match[0];
if (placeholderArg.isMagicPen) {
output.append(placeholderArg);
} else {
output.text(placeholderArg);
}
} else {
output.text(token);
}
});
}
var error = new Error();
error._isUnexpected = true;
error.output = output;
this.serializeOutputToMessage(error);
throw error;
};
Unexpected.prototype.findAssertionSimilarTo = function (text) {
var editDistrances = [];
forEach(getKeys(this.assertions), function (assertion) {
var distance = levenshteinDistance(text, assertion);
editDistrances.push({
assertion: assertion,
distance: distance
});
});
editDistrances.sort(function (x, y) {
return x.distance - y.distance;
});
return map(editDistrances.slice(0, 5), function (editDistrance) {
return editDistrance.assertion;
});
};
Unexpected.prototype.addAssertion = function () {
var assertions = this.assertions;
var patterns = Array.prototype.slice.call(arguments, 0, -1);
var handler = Array.prototype.slice.call(arguments, -1)[0];
var isSeenByExpandedPattern = {};
forEach(patterns, function (pattern) {
ensureValidPattern(pattern);
forEach(expandPattern(pattern), function (expandedPattern) {
if (expandedPattern.text in assertions) {
if (!isSeenByExpandedPattern[expandedPattern.text]) {
throw new Error('Cannot redefine assertion: ' + expandedPattern.text);
}
} else {
isSeenByExpandedPattern[expandedPattern.text] = true;
assertions[expandedPattern.text] = {
handler: handler,
flags: expandedPattern.flags
};
}
});
});
return this.expect; // for chaining
};
Unexpected.prototype.getType = function (typeName) {
return utils.findFirst(this.types, function (type) {
return type.name === typeName;
});
};
Unexpected.prototype.addType = function (type) {
var baseType;
if (typeof type.name !== 'string' || trim(type.name) === '') {
throw new Error('A custom type must be given a non-empty name');
}
if (type.base) {
baseType = utils.findFirst(this.types, function (t) {
return t.name === type.base;
});
if (!baseType) {
throw new Error('Unknown base type: ' + type.base);
}
} else {
baseType = anyType;
}
this.types.unshift(extend({}, baseType, type, { baseType: baseType }));
return this.expect;
};
Unexpected.prototype.installPlugin = function (plugin) {
if (typeof plugin !== 'function') {
throw new Error('Expected first argument given to installPlugin to be a function');
}
plugin(this.expect);
return this.expect; // for chaining
};
Unexpected.prototype.sanitize = function (obj, stack) {
var that = this;
stack = stack || [];
var i;
for (i = 0 ; i < stack.length ; i += 1) {
if (stack[i] === obj) {
return obj;
}
}
stack.push(obj);
var sanitized,
matchingCustomType = utils.findFirst(this.types || [], function (type) {
return type.identify(obj);
});
if (matchingCustomType) {
sanitized = matchingCustomType.toJSON(obj, function (v) {
return that.sanitize(v, stack);
});
} else if (isArray(obj)) {
sanitized = map(obj, function (item) {
return this.sanitize(item, stack);
}, this);
} else if (typeof obj === 'object' && obj) {
sanitized = {};
forEach(getKeys(obj).sort(), function (key) {
sanitized[key] = this.sanitize(obj[key], stack);
}, this);
} else {
sanitized = obj;
}
stack.pop();
return sanitized;
};
var errorMethodBlacklist = reduce(['message', 'line', 'sourceId', 'sourceURL', 'stack', 'stackArray'], function (result, prop) {
result[prop] = true;
return result;
}, {});
function errorWithMessage(e, message) {
var newError = new Error();
forEach(getKeys(e), function (key) {
if (!errorMethodBlacklist[key]) {
newError[key] = e[key];
}
});
newError.output = message;
return newError;
}
function handleNestedExpects(e, assertion) {
switch (assertion.errorMode) {
case 'nested':
return errorWithMessage(e, assertion.standardErrorMessage().nl()
.indentLines()
.i().block(e.output));
case 'default':
return errorWithMessage(e, assertion.standardErrorMessage());
case 'bubble':
return errorWithMessage(e, e.output);
default:
throw new Error("Unknown error mode: '" + assertion.errorMode + "'");
}
}
function installExpectMethods(unexpected, expectFunction) {
var expect = bind(expectFunction, unexpected);
expect.equal = bind(unexpected.equal, unexpected);
expect.sanitize = bind(unexpected.sanitize, unexpected);
expect.inspect = bind(unexpected.inspect, unexpected);
expect.fail = bind(unexpected.fail, unexpected);
expect.addAssertion = bind(unexpected.addAssertion, unexpected);
expect.addType = bind(unexpected.addType, unexpected);
expect.clone = bind(unexpected.clone, unexpected);
expect.toString = bind(unexpected.toString, unexpected);
expect.assertions = unexpected.assertions;
expect.installPlugin = bind(unexpected.installPlugin, unexpected);
expect.output = unexpected.output;
expect.outputFormat = bind(unexpected.outputFormat, unexpected);
return expect;
}
function makeExpectFunction(unexpected) {
var expect = installExpectMethods(unexpected, unexpected.expect);
unexpected.expect = expect;
return expect;
}
Unexpected.prototype.serializeOutputToMessage = function (err) {
var outputFormat = this.outputFormat();
if (outputFormat === 'html') {
outputFormat = 'text';
err.htmlMessage = err.output.toString('html');
}
err.message = err.output.toString(outputFormat);
};
Unexpected.prototype.expect = function expect(subject, testDescriptionString) {
var that = this;
if (arguments.length < 2) {
throw new Error('The expect functions requires at least two parameters.');
}
if (typeof testDescriptionString !== 'string') {
throw new Error('The expect functions requires second parameter to be a string.');
}
var assertionRule = this.assertions[testDescriptionString];
if (assertionRule) {
var flags = extend({}, assertionRule.flags);
var nestingLevel = 0;
var callInNestedContext = function (callback) {
nestingLevel += 1;
try {
callback();
nestingLevel -= 1;
} catch (e) {
nestingLevel -= 1;
if (e._isUnexpected) {
truncateStack(e, wrappedExpect);
if (nestingLevel === 0) {
var wrappedError = handleNestedExpects(e, assertion);
that.serializeOutputToMessage(wrappedError);
throw wrappedError;
}
}
throw e;
}
};
var wrappedExpect = function wrappedExpect(subject, testDescriptionString) {
testDescriptionString = trim(testDescriptionString.replace(/\[(!?)([^\]]+)\] ?/g, function (match, negate, flag) {
return Boolean(flags[flag]) !== Boolean(negate) ? flag + ' ' : '';
}));
var args = Array.prototype.slice.call(arguments, 2);
callInNestedContext(function () {
that.expect.apply(that, [subject, testDescriptionString].concat(args));
});
};
// Not sure this is the right way to go about this:
wrappedExpect.equal = this.equal;
wrappedExpect.types = this.types;
wrappedExpect.sanitize = this.sanitize;
wrappedExpect.inspect = this.inspect;
wrappedExpect.output = this.output;
wrappedExpect.outputFormat = this.outputFormat;
wrappedExpect.fail = function () {
var args = arguments;
callInNestedContext(function () {
that.fail.apply(that, args);
});
};
wrappedExpect.format = this.format;
var args = Array.prototype.slice.call(arguments, 2);
args.unshift(wrappedExpect, subject);
var assertion = new Assertion(wrappedExpect, subject, testDescriptionString, flags, args.slice(2));
var handler = assertionRule.handler;
try {
handler.apply(assertion, args);
} catch (e) {
var err = e;
if (err._isUnexpected) {
err = errorWithMessage(err, err.output);
that.serializeOutputToMessage(err);
truncateStack(err, this.expect);
}
throw err;
}
} else {
var similarAssertions = this.findAssertionSimilarTo(testDescriptionString);
var message =
'Unknown assertion "' + testDescriptionString + '", ' +
'did you mean: "' + similarAssertions[0] + '"';
var err = new Error(message);
truncateStack(err, this.expect);
throw err;
}
};
Unexpected.prototype.toString = function () {
return getKeys(this.assertions).sort().join('\n');
};
Unexpected.prototype.clone = function () {
var unexpected = new Unexpected({
assertions: extend({}, this.assertions),
types: [].concat(this.types),
output: this.output.clone(),
format: this.outputFormat()
});
return makeExpectFunction(unexpected);
};
Unexpected.prototype.outputFormat = function (format) {
if (typeof format === 'undefined') {
return this._outputFormat;
} else {
this._outputFormat = format;
return this;
}
};
Unexpected.create = function () {
var unexpected = new Unexpected();
return makeExpectFunction(unexpected);
};
var expandPattern = (function () {
function isFlag(token) {
return token.slice(0, 1) === '[' && token.slice(-1) === ']';
}
function isAlternation(token) {
return token.slice(0, 1) === '(' && token.slice(-1) === ')';
}
function removeEmptyStrings(texts) {
return filter(texts, function (text) {
return text !== '';
});
}
function createPermutations(tokens, index) {
if (index === tokens.length) {
return [{ text: '', flags: {}}];
}
var token = tokens[index];
var tail = createPermutations(tokens, index + 1);
if (isFlag(token)) {
var flag = token.slice(1, -1);
return map(tail, function (pattern) {
var flags = {};
flags[flag] = true;
return {
text: flag + ' ' + pattern.text,
flags: extend(flags, pattern.flags)
};
}).concat(map(tail, function (pattern) {
var flags = {};
flags[flag] = false;
return {
text: pattern.text,
flags: extend(flags, pattern.flags)
};
}));
} else if (isAlternation(token)) {
var alternations = token.split(/\(|\)|\|/);
alternations = removeEmptyStrings(alternations);
return reduce(alternations, function (result, alternation) {
return result.concat(map(tail, function (pattern) {
return {
text: alternation + pattern.text,
flags: pattern.flags
};
}));
}, []);
} else {
return map(tail, function (pattern) {
return {
text: token + pattern.text,
flags: pattern.flags
};
});
}
}
return function (pattern) {
pattern = pattern.replace(/(\[[^\]]+\]) ?/g, '$1');
var splitRegex = /\[[^\]]+\]|\([^\)]+\)/g;
var tokens = [];
var m;
var lastIndex = 0;
while ((m = splitRegex.exec(pattern))) {
tokens.push(pattern.slice(lastIndex, m.index));
tokens.push(pattern.slice(m.index, splitRegex.lastIndex));
lastIndex = splitRegex.lastIndex;
}
tokens.push(pattern.slice(lastIndex));
tokens = removeEmptyStrings(tokens);
var permutations = createPermutations(tokens, 0);
forEach(permutations, function (permutation) {
permutation.text = trim(permutation.text);
if (permutation.text === '') {
// This can only happen if the pattern only contains flags
throw new Error("Assertion patterns must not only contain flags");
}
});
return permutations;
};
}());
function ensureValidUseOfParenthesesOrBrackets(pattern) {
var counts = {
'[': 0,
']': 0,
'(': 0,
')': 0
};
for (var i = 0; i < pattern.length; i += 1) {
var c = pattern.charAt(i);
if (c in counts) {
counts[c] += 1;
}
if (c === ']' && counts['['] >= counts[']']) {
if (counts['['] === counts[']'] + 1) {
throw new Error("Assertion patterns must not contain flags with brackets: '" + pattern + "'");
}
if (counts['('] !== counts[')']) {
throw new Error("Assertion patterns must not contain flags with parentheses: '" + pattern + "'");
}
if (pattern.charAt(i - 1) === '[') {
throw new Error("Assertion patterns must not contain empty flags: '" + pattern + "'");
}
} else if (c === ')' && counts['('] >= counts[')']) {
if (counts['('] === counts[')'] + 1) {
throw new Error("Assertion patterns must not contain alternations with parentheses: '" + pattern + "'");
}
if (counts['['] !== counts[']']) {
throw new Error("Assertion patterns must not contain alternations with brackets: '" + pattern + "'");
}
}
if ((c === ')' || c === '|') && counts['('] >= counts[')']) {
if (pattern.charAt(i - 1) === '(' || pattern.charAt(i - 1) === '|') {
throw new Error("Assertion patterns must not contain empty alternations: '" + pattern + "'");
}
}
}
if (counts['['] !== counts[']']) {
throw new Error("Assertion patterns must not contain unbalanced brackets: '" + pattern + "'");
}
if (counts['('] !== counts[')']) {
throw new Error("Assertion patterns must not contain unbalanced parentheses: '" + pattern + "'");
}
}
function ensureValidPattern(pattern) {
if (typeof pattern !== 'string' || pattern === '') {
throw new Error("Assertion patterns must be a non empty string");
}
if (pattern.match(/^\s|\s$/)) {
throw new Error("Assertion patterns can't start or end with whitespace");
}
ensureValidUseOfParenthesesOrBrackets(pattern);
}
module.exports = Unexpected;
},{}],3:[function(require,module,exports){
var shim = require(5);
var forEach = shim.forEach;
var getKeys = shim.getKeys;
var every = shim.every;
var indexOf = shim.indexOf;
var utils = require(7);
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);
}
});
};
},{}],4:[function(require,module,exports){
module.exports = function (expect) {
expect.output.addStyle('error', function (content) {
this.text(content, 'red, bold');
});
expect.output.addStyle('strings', function (content) {
this.text(content, 'cyan');
});
expect.output.addStyle('key', function (content) {
this.text(content);
});
// Intended to be redefined by a plugin that offers syntax highlighting:
expect.output.addStyle('code', function (content, language) {
this.text(content);
});
};
},{}],5:[function(require,module,exports){
var shim;
try {
shim = require('./shim-es4');
} catch (e) {
// Probably excluded via browserify -u lib/shim-es4.js
shim = {};
}
var prototypes = {
bind: Function.prototype.bind,
every: Array.prototype.every,
some: Array.prototype.some,
indexOf: Array.prototype.indexOf,
forEach: Array.prototype.forEach,
map: Array.prototype.map,
filter: Array.prototype.filter,
reduce: Array.prototype.reduce,
trim: String.prototype.trim
};
function createShimMethod(key) {
shim[key] = function (obj) {
var args = Array.prototype.slice.call(arguments, 1);
return prototypes[key].apply(obj, args);
};
}
for (var key in prototypes) {
if (prototypes.hasOwnProperty(key) && prototypes[key]) {
createShimMethod(key);
}
}
if (!shim.bind) {
shim.bind = function (fn, scope) {
return function () {
return fn.apply(scope, arguments);
};
};
}
if (Object.keys) {
shim['getKeys'] = Object.keys;
}
if ('object' === typeof JSON && JSON.parse && JSON.stringify) {
shim['JSON'] = JSON;
}
module.exports = shim;
},{}],6:[function(require,module,exports){
(function (Buffer){
/*global Uint8Array, Uint16Array*/
var utils = require(7);
var isRegExp = utils.isRegExp;
var leftPad = utils.leftPad;
var shim = require(5);
var json = shim.JSON;
var every = shim.every;
var some = shim.some;
var forEach = shim.forEach;
var map = shim.map;
var getKeys = shim.getKeys;
var reduce = shim.reduce;
var extend = utils.extend;
module.exports = function (expect) {
expect.addType({
name: 'object',
identify: function (obj) {
return obj && typeof obj === 'object';
},
equal: function (a, b, equal) {
if (a === b) {
return true;
}
// an identical "prototype" property.
if (a.prototype !== b.prototype) {
return false;
}
//~~~I've managed to break Object.keys through screwy arguments passing.
// Converting to array solves the problem.
if (utils.isArguments(a)) {
if (!utils.isArguments(b)) {
return false;
}
return equal(Array.prototype.slice.call(a), Array.prototype.slice.call(b));
}
var actualKeys = utils.getKeysOfDefinedProperties(a),
expectedKeys = utils.getKeysOfDefinedProperties(b),
key,
i;
// having the same number of owned properties (keys incorporates hasOwnProperty)
if (actualKeys.length !== expectedKeys.length) {
return false;
}
//the same set of keys (although not necessarily the same order),
actualKeys.sort();
expectedKeys.sort();
//~~~cheap key test
for (i = actualKeys.length - 1; i >= 0; i -= 1) {
if (actualKeys[i] !== expectedKeys[i]) {
return false;
}
}
//equivalent values for every corresponding key, and
//~~~possibly expensive deep test
for (i = actualKeys.length - 1; i >= 0; i -= 1) {
key = actualKeys[i];
if (!equal(a[key], b[key])) {
return false;
}
}
return true;
},
inspect: function (output, obj, inspect) {
var keys = getKeys(obj);
if (keys.length === 0) {
return output.text('{}');
}
var inspectedItems = map(keys, function (key) {
var propertyOutput = output.clone();
if (key.match(/["' ]/)) {
propertyOutput.append(expect.inspect(key));
} else {
propertyOutput.key(key);
}
propertyOutput.text(':');
var hasGetter = obj.__lookupGetter__ && obj.__lookupGetter__(key);
var hasSetter = obj.__lookupGetter__ && obj.__lookupSetter__(key);
if (hasGetter || !hasSetter) {
propertyOutput.sp().append(inspect(propertyOutput.clone(), obj[key]));
}
if (hasGetter && hasSetter) {
propertyOutput.sp().text('[Getter/Setter]');
} else if (hasGetter) {
propertyOutput.sp().text('[Getter]');
} else if (hasSetter) {
propertyOutput.sp().text('[Setter]');
}
return propertyOutput;
});
var width = 0;
var multipleLines = some(inspectedItems, function (o) {
var size = o.size();
width += size.width;
return width > 50 || size.height > 1;
});
forEach(inspectedItems, function (inspectedItem, index) {
var lastIndex = index === inspectedItems.length - 1;
if (!lastIndex) {
inspectedItem.text(',');
}
});
if (multipleLines) {
output.text('{').nl().indentLines();
forEach(inspectedItems, function (inspectedItem, index) {
output.i().block(inspectedItem).nl();
});
output.outdentLines().text('}');
} else {
output.text('{ ');
forEach(inspectedItems, function (inspectedItem, index) {
output.append(inspectedItem);
var lastIndex = index === inspectedItems.length - 1;
if (!lastIndex) {
output.sp();
}
});
output.text(' }');
}
return output;
},
toJSON: function (obj, toJSON) {
return reduce(getKeys(obj), function (result, key) {
result[key] = toJSON(obj[key]);
return result;
}, {});
}
});
expect.addType({
name: 'array',
identify: function (arr) {
return utils.isArray(arr) || utils.isArguments(arr);
},
equal: function (a, b, equal) {
return a === b || (a.length === b.length && every(a, function (v, index) {
return equal(v, b[index]);
}));
},
inspect: function (output, arr, inspect, depth) {
if (arr.length === 0) {
return output.text('[]');
}
if (depth === 1) {
return output.text('[...]');
}
var inspectedItems = map(arr, function (v) {
return inspect(output.clone(), v);
});
var width = 0;
var multipleLines = some(inspectedItems, function (o) {
var size = o.size();
width += size.width;
return width > 50 || o.height > 1;
});
forEach(inspectedItems, function (inspectedItem, index) {
var lastIndex = index === inspectedItems.length - 1;
if (!lastIndex) {
inspectedItem.text(',');
}
});
if (multipleLines) {
output.text('[').nl().indentLines();
forEach(inspectedItems, function (inspectedItem, index) {
output.i().block(inspectedItem).nl();
});
output.outdentLines().text(']');
} else {
output.text('[ ');
forEach(inspectedItems, function (inspectedItem, index) {
output.append(inspectedItem);
var lastIndex = index === inspectedItems.length - 1;
if (!lastIndex) {
output.sp();
}
});
output.text(' ]');
}
return output;
},
toJSON: function (arr, toJSON) {
return map(arr, toJSON);
}
});
expect.addType({
base: 'object',
name: 'Error',
identify: function (v