code
Version:
assertion library
542 lines (342 loc) • 14.5 kB
JavaScript
'use strict';
// Load modules
const NodeUtil = require('util');
const Hoek = require('hoek');
// Declare internals
const internals = {
flags: ['not', 'once', 'only', 'part', 'shallow'],
grammar: ['a', 'an', 'and', 'at', 'be', 'have', 'in', 'to'],
locations: {},
count: 0
};
exports.settings = {
truncateMessages: false,
comparePrototypes: false
};
exports.expect = function (value, prefix) {
const at = exports.thrownAt();
const location = at.filename + ':' + at.line + '.' + at.column;
internals.locations[location] = true;
++internals.count;
return new internals.Assertion(value, prefix, location, at);
};
exports.fail = function (message) {
throw new Error(message);
};
exports.incomplete = function () {
const locations = Object.keys(internals.locations);
return locations.length ? locations : null;
};
exports.count = function () {
return internals.count;
};
internals.Assertion = function (ref, prefix, location, at) {
this._ref = ref;
this._prefix = prefix || '';
this._location = location;
this._at = at;
this._flags = {};
};
internals.filterLocal = function (line) {
return line.indexOf(__dirname) === -1;
};
internals.Assertion.prototype.assert = function (result, verb, actual, expected) {
delete internals.locations[this._location];
if (this._flags.not ? !result : result) {
this._flags = {};
return this;
}
if (verb === 'exist' &&
this._flags.not &&
this._ref instanceof Error) {
const original = this._ref;
original.at = exports.thrownAt();
throw original;
}
let message = '';
if (this._prefix) {
message += this._prefix + ': ';
}
message += 'Expected ' + internals.display(this._ref) + ' to ';
if (this._flags.not) {
message += 'not ';
}
message += verb;
if (this._flags.once) {
message += ' once';
}
if (arguments.length === 3) { // 'actual' without 'expected'
message += ' but got ' + internals.display(actual);
}
const error = new Error(message);
Error.captureStackTrace(error, this.assert);
error.actual = actual;
error.expected = expected;
error.at = exports.thrownAt(error) || this._at;
throw error;
};
internals.flags.forEach((word) => {
Object.defineProperty(internals.Assertion.prototype, word, {
get: function () {
this._flags[word] = !this._flags[word];
return this;
},
configurable: true
});
});
internals.grammar.forEach((word) => {
Object.defineProperty(internals.Assertion.prototype, word, {
get: function () {
return this;
},
configurable: true
});
});
internals.addMethod = function (names, fn) {
const method = function (name) {
internals.Assertion.prototype[name] = fn;
};
names = [].concat(names);
names.forEach(method);
};
['arguments', 'array', 'boolean', 'buffer', 'date', 'function', 'number', 'regexp', 'string', 'object'].forEach((word) => {
const article = ['a', 'e', 'i', 'o', 'u'].indexOf(word[0]) !== -1 ? 'an ' : 'a ';
const method = function () {
const type = internals.type(this._ref);
return this.assert(type === word, 'be ' + article + word, type);
};
internals.addMethod(word, method);
});
internals.addMethod('error', function (...args /*type, message*/) {
const type = args.length && typeof args[0] !== 'string' && !(args[0] instanceof RegExp) ? args[0] : Error;
const lastArg = args[1] || args[0];
const message = typeof lastArg === 'string' || lastArg instanceof RegExp ? lastArg : null;
const err = this._ref;
if (!this._flags.not || message === null) {
this.assert(err instanceof type, 'be an error with ' + (type.name || 'provided') + ' type');
}
if (message !== null) {
const error = err.message || '';
this.assert(typeof message === 'string' ? error === message : error.match(message), 'be an error with specified message', error, message);
}
});
[true, false, null, undefined].forEach((value) => {
const name = NodeUtil.inspect(value);
const method = function () {
return this.assert(this._ref === value, 'be ' + name);
};
internals.addMethod(name, method);
});
internals.nan = function () {
return this.assert(Number.isNaN(this._ref), 'be NaN');
};
internals.addMethod('NaN', internals.nan);
internals.include = function (value) {
internals.assert(this, arguments.length === 1, 'Can only assert include with a single parameter');
this._flags.deep = !this._flags.shallow;
this._flags.part = this._flags.hasOwnProperty('part') ? this._flags.part : false;
return this.assert(Hoek.contain(this._ref, value, this._flags), 'include ' + internals.display(value));
};
internals.addMethod(['include', 'includes', 'contain', 'contains'], internals.include);
internals.endWith = function (value) {
internals.assert(this, typeof this._ref === 'string' && typeof value === 'string', 'Can only assert endsWith on a string, with a string');
const comparator = this._ref.slice(-value.length);
return this.assert(comparator === value, 'endWith ' + internals.display(value));
};
internals.addMethod(['endWith', 'endsWith'], internals.endWith);
internals.startWith = function (value) {
internals.assert(this, typeof this._ref === 'string' && typeof value === 'string', 'Can only assert startsWith on a string, with a string');
const comparator = this._ref.slice(0, value.length);
return this.assert(comparator === value, 'startWith ' + internals.display(value));
};
internals.addMethod(['startWith', 'startsWith'], internals.startWith);
internals.exist = function () {
return this.assert(this._ref !== null && this._ref !== undefined, 'exist');
};
internals.addMethod(['exist', 'exists'], internals.exist);
internals.empty = function () {
internals.assert(this, typeof this._ref === 'object' || typeof this._ref === 'string', 'Can only assert empty on object, array or string');
const length = this._ref.length !== undefined ? this._ref.length : Object.keys(this._ref).length;
return this.assert(!length, 'be empty');
};
internals.addMethod('empty', internals.empty);
internals.length = function (size) {
internals.assert(this, (typeof this._ref === 'object' && this._ref !== null) || typeof this._ref === 'string', 'Can only assert length on object, array or string');
const length = this._ref.length !== undefined ? this._ref.length : Object.keys(this._ref).length;
return this.assert(length === size, 'have a length of ' + size, length);
};
internals.addMethod('length', internals.length);
internals.equal = function (value, options) {
options = options || {};
const settings = Hoek.applyToDefaults({ prototype: exports.settings.comparePrototypes }, options);
const compare = this._flags.shallow ? (a, b) => a === b
: (a, b) => Hoek.deepEqual(a, b, settings);
return this.assert(compare(this._ref, value), `equal specified value: ${internals.display(value)}`, this._ref, value);
};
internals.addMethod(['equal', 'equals'], internals.equal);
internals.above = function (value) {
return this.assert(this._ref > value, 'be above ' + value);
};
internals.addMethod(['above', 'greaterThan'], internals.above);
internals.least = function (value) {
return this.assert(this._ref >= value, 'be at least ' + value);
};
internals.addMethod(['least', 'min'], internals.least);
internals.below = function (value) {
return this.assert(this._ref < value, 'be below ' + value);
};
internals.addMethod(['below', 'lessThan'], internals.below);
internals.most = function (value) {
return this.assert(this._ref <= value, 'be at most ' + value);
};
internals.addMethod(['most', 'max'], internals.most);
internals.within = function (from, to) {
return this.assert(this._ref >= from && this._ref <= to, 'be within ' + from + '..' + to);
};
internals.addMethod(['within', 'range'], internals.within);
internals.between = function (from, to) {
return this.assert(this._ref > from && this._ref < to, 'be between ' + from + '..' + to);
};
internals.addMethod('between', internals.between);
internals.above = function (value, delta) {
internals.assert(this, internals.type(this._ref) === 'number', 'Can only assert about on numbers');
internals.assert(this, internals.type(value) === 'number' && internals.type(delta) === 'number', 'About assertion requires two number arguments');
return this.assert(Math.abs(this._ref - value) <= delta, 'be about ' + value + ' \u00b1' + delta);
};
internals.addMethod('about', internals.above);
internals.instanceof = function (type) {
return this.assert(this._ref instanceof type, 'be an instance of ' + (type.name || 'provided type'));
};
internals.addMethod(['instanceof', 'instanceOf'], internals.instanceof);
internals.match = function (regex) {
return this.assert(regex.exec(this._ref), 'match ' + regex);
};
internals.addMethod(['match', 'matches'], internals.match);
internals.satisfy = function (validator) {
return this.assert(validator(this._ref), 'satisfy rule');
};
internals.addMethod(['satisfy', 'satisfies'], internals.satisfy);
internals.throw = function (...args /* type, message */) {
internals.assert(this, typeof this._ref === 'function', 'Can only assert throw on functions');
internals.assert(this, !this._flags.not || !args.length, 'Cannot specify arguments when expecting not to throw');
const type = args.length && typeof args[0] !== 'string' && !(args[0] instanceof RegExp) ? args[0] : null;
const lastArg = args[1] || args[0];
const message = typeof lastArg === 'string' || lastArg instanceof RegExp ? lastArg : null;
let thrown = false;
try {
this._ref();
}
catch (err) {
thrown = true;
if (type) {
this.assert(err instanceof type, 'throw ' + (type.name || 'provided type'));
}
if (message !== null) {
const error = err.message || '';
this.assert(typeof message === 'string' ? error === message : error.match(message), 'throw an error with specified message', error, message);
}
this.assert(thrown, 'throw an error', err);
return err;
}
return this.assert(thrown, 'throw an error');
};
internals.addMethod(['throw', 'throws'], internals.throw);
internals.reject = async function (...args/* type, message */) {
try {
internals.assert(this, internals.isPromise(this._ref), 'Can only assert reject on promises');
const type = args.length && typeof args[0] !== 'string' && !(args[0] instanceof RegExp) ? args[0] : null;
const lastArg = args[1] || args[0];
const message = typeof lastArg === 'string' || lastArg instanceof RegExp ? lastArg : null;
let thrown = null;
try {
await this._ref;
}
catch (err) {
thrown = err;
}
internals.assert(this, !this._flags.not || !arguments.length, 'Cannot specify arguments when expecting not to reject');
if (thrown) {
internals.assert(this, arguments.length < 2 || message, 'Can not assert with invalid message argument type');
internals.assert(this, arguments.length < 1 || message !== null || typeof type === 'function', 'Can not assert with invalid type argument');
if (type) {
this.assert(thrown instanceof type, 'reject with ' + (type.name || 'provided type'));
}
if (message !== null) {
const error = thrown.message || '';
this.assert(typeof message === 'string' ? error === message : error.match(message), 'reject with an error with specified message', error, message);
}
this.assert(thrown, 'reject with an error', thrown);
}
this.assert(thrown, 'reject with an error');
return thrown;
}
catch (err) {
return new Promise((resolve, reject) => {
reject(err);
});
}
};
internals.addMethod(['reject', 'rejects'], internals.reject);
internals.isPromise = function (promise) {
return promise && typeof promise.then === 'function';
};
internals.display = function (value) {
const string = value instanceof Error ? `[${value.toString()}]` : (internals.isPromise(value) ? '[Promise]' : NodeUtil.inspect(value));
if (!exports.settings.truncateMessages || string.length <= 40) {
return string;
}
if (Array.isArray(value)) {
return '[Array(' + value.length + ')]';
}
if (typeof value === 'object') {
const keys = Object.keys(value);
return '{ Object (' + (keys.length > 2 ? (keys.splice(0, 2).join(', ') + ', ...') : keys.join(', ')) + ') }';
}
return string.slice(0, 40) + '...\'';
};
internals.natives = {
'[object Arguments]': 'arguments',
'[object Array]': 'array',
'[object AsyncFunction]': 'function',
'[object Date]': 'date',
'[object Function]': 'function',
'[object Number]': 'number',
'[object RegExp]': 'regexp',
'[object String]': 'string'
};
internals.type = function (value) {
if (value === null) {
return 'null';
}
if (value === undefined) {
return 'undefined';
}
if (Buffer.isBuffer(value)) {
return 'buffer';
}
const name = Object.prototype.toString.call(value);
if (internals.natives[name]) {
return internals.natives[name];
}
if (value === Object(value)) {
return 'object';
}
return typeof value;
};
exports.thrownAt = function (error) {
error = error || new Error();
const stack = typeof error.stack === 'string' ? error.stack : '';
const frame = stack.replace(error.toString(), '').split('\n').slice(1).filter(internals.filterLocal)[0] || '';
const at = frame.match(/^\s*at [^(/]*\(?(.+)\:(\d+)\:(\d+)\)?$/);
return Array.isArray(at) ? {
filename: at[1],
line: at[2],
column: at[3]
} : undefined;
};
internals.assert = function (assertion, condition, error) {
if (!condition) {
delete internals.locations[assertion._location];
Hoek.assert(condition, error);
}
};