You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
693 lines
15 KiB
693 lines
15 KiB
/*! |
|
* Should |
|
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca> |
|
* MIT Licensed |
|
*/ |
|
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var util = require('util') |
|
, http = require('http') |
|
, assert = require('assert') |
|
, AssertionError = assert.AssertionError |
|
, statusCodes = http.STATUS_CODES |
|
, eql = require('./eql') |
|
, i = util.inspect; |
|
|
|
/** |
|
* Expose assert as should. |
|
* |
|
* This allows you to do things like below |
|
* without require()ing the assert module. |
|
* |
|
* should.equal(foo.bar, undefined); |
|
* |
|
*/ |
|
|
|
exports = module.exports = assert; |
|
|
|
/** |
|
* Library version. |
|
*/ |
|
|
|
exports.version = '0.5.1'; |
|
|
|
/** |
|
* Assert _obj_ exists, with optional message. |
|
* |
|
* @param {Mixed} obj |
|
* @param {String} msg |
|
* @api public |
|
*/ |
|
|
|
exports.exist = function(obj, msg){ |
|
if (null == obj) { |
|
throw new AssertionError({ |
|
message: msg || ('expected ' + i(obj) + ' to exist') |
|
, stackStartFunction: should.exist |
|
}); |
|
} |
|
}; |
|
|
|
/** |
|
* Asserts _obj_ does not exist, with optional message. |
|
* |
|
* @param {Mixed} obj |
|
* @param {String} msg |
|
* @api public |
|
*/ |
|
|
|
exports.not = {}; |
|
exports.not.exist = function(obj, msg){ |
|
if (null != obj) { |
|
throw new AssertionError({ |
|
message: msg || ('expected ' + i(obj) + ' to not exist') |
|
, stackStartFunction: should.not.exist |
|
}); |
|
} |
|
}; |
|
|
|
/** |
|
* Expose api via `Object#should`. |
|
* |
|
* @api public |
|
*/ |
|
|
|
Object.defineProperty(Object.prototype, 'should', { |
|
set: function(){}, |
|
get: function(){ |
|
return new Assertion(this); |
|
}, |
|
configurable: true |
|
}); |
|
|
|
/** |
|
* Initialize a new `Assertion` with the given _obj_. |
|
* |
|
* @param {Mixed} obj |
|
* @api private |
|
*/ |
|
|
|
var Assertion = exports.Assertion = function Assertion(obj) { |
|
this.obj = obj; |
|
}; |
|
|
|
/** |
|
* Prototype. |
|
*/ |
|
|
|
Assertion.prototype = { |
|
|
|
/** |
|
* HACK: prevents double require() from failing. |
|
*/ |
|
|
|
exports: exports, |
|
|
|
/** |
|
* Assert _expr_ with the given _msg_ and _negatedMsg_. |
|
* |
|
* @param {Boolean} expr |
|
* @param {String} msg |
|
* @param {String} negatedMsg |
|
* @api private |
|
*/ |
|
|
|
assert: function(expr, msg, negatedMsg){ |
|
var msg = this.negate ? negatedMsg : msg |
|
, ok = this.negate ? !expr : expr; |
|
if (!ok) { |
|
throw new AssertionError({ |
|
message: msg |
|
, stackStartFunction: this.assert |
|
}); |
|
} |
|
}, |
|
|
|
/** |
|
* Dummy getter. |
|
* |
|
* @api public |
|
*/ |
|
|
|
get an() { |
|
return this; |
|
}, |
|
|
|
/** |
|
* Dummy getter. |
|
* |
|
* @api public |
|
*/ |
|
|
|
get and() { |
|
return this; |
|
}, |
|
|
|
/** |
|
* Dummy getter. |
|
* |
|
* @api public |
|
*/ |
|
|
|
get be() { |
|
return this; |
|
}, |
|
|
|
/** |
|
* Dummy getter. |
|
* |
|
* @api public |
|
*/ |
|
|
|
get have() { |
|
return this; |
|
}, |
|
|
|
/** |
|
* Dummy getter. |
|
* |
|
* @api public |
|
*/ |
|
|
|
get with() { |
|
return this; |
|
}, |
|
|
|
/** |
|
* Negation modifier. |
|
* |
|
* @api public |
|
*/ |
|
|
|
get not() { |
|
this.negate = true; |
|
return this; |
|
}, |
|
|
|
/** |
|
* Get object inspection string. |
|
* |
|
* @return {String} |
|
* @api private |
|
*/ |
|
|
|
get inspect() { |
|
return i(this.obj); |
|
}, |
|
|
|
/** |
|
* Assert instanceof `Arguments`. |
|
* |
|
* @api public |
|
*/ |
|
|
|
get arguments() { |
|
this.assert( |
|
'[object Arguments]' == Object.prototype.toString.call(this.obj) |
|
, 'expected ' + this.inspect + ' to be arguments' |
|
, 'expected ' + this.inspect + ' to not be arguments'); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert that an object is empty aka length of 0. |
|
* |
|
* @api public |
|
*/ |
|
|
|
get empty() { |
|
this.obj.should.have.property('length'); |
|
this.assert( |
|
0 === this.obj.length |
|
, 'expected ' + this.inspect + ' to be empty' |
|
, 'expected ' + this.inspect + ' not to be empty'); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert ok. |
|
* |
|
* @api public |
|
*/ |
|
|
|
get ok() { |
|
this.assert( |
|
this.obj |
|
, 'expected ' + this.inspect + ' to be truthy' |
|
, 'expected ' + this.inspect + ' to be falsey'); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert true. |
|
* |
|
* @api public |
|
*/ |
|
|
|
get true() { |
|
this.assert( |
|
true === this.obj |
|
, 'expected ' + this.inspect + ' to be true' |
|
, 'expected ' + this.inspect + ' not to be true'); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert false. |
|
* |
|
* @api public |
|
*/ |
|
|
|
get false() { |
|
this.assert( |
|
false === this.obj |
|
, 'expected ' + this.inspect + ' to be false' |
|
, 'expected ' + this.inspect + ' not to be false'); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert equal. |
|
* |
|
* @param {Mixed} val |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
eql: function(val, desc){ |
|
this.assert( |
|
eql(val, this.obj) |
|
, 'expected ' + this.inspect + ' to equal ' + i(val) + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' to not equal ' + i(val) + (desc ? " | " + desc : "")); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert strict equal. |
|
* |
|
* @param {Mixed} val |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
equal: function(val, desc){ |
|
this.assert( |
|
val === this.obj |
|
, 'expected ' + this.inspect + ' to equal ' + i(val) + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' to not equal ' + i(val) + (desc ? " | " + desc : "")); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert within start to finish (inclusive). |
|
* |
|
* @param {Number} start |
|
* @param {Number} finish |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
within: function(start, finish, desc){ |
|
var range = start + '..' + finish; |
|
this.assert( |
|
this.obj >= start && this.obj <= finish |
|
, 'expected ' + this.inspect + ' to be within ' + range + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' to not be within ' + range + (desc ? " | " + desc : "")); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert typeof. |
|
* |
|
* @param {Mixed} type |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
a: function(type, desc){ |
|
this.assert( |
|
type == typeof this.obj |
|
, 'expected ' + this.inspect + ' to be a ' + type + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' not to be a ' + type + (desc ? " | " + desc : "")); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert instanceof. |
|
* |
|
* @param {Function} constructor |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
instanceof: function(constructor, desc){ |
|
var name = constructor.name; |
|
this.assert( |
|
this.obj instanceof constructor |
|
, 'expected ' + this.inspect + ' to be an instance of ' + name + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' not to be an instance of ' + name + (desc ? " | " + desc : "")); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert numeric value above _n_. |
|
* |
|
* @param {Number} n |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
above: function(n, desc){ |
|
this.assert( |
|
this.obj > n |
|
, 'expected ' + this.inspect + ' to be above ' + n + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' to be below ' + n + (desc ? " | " + desc : "")); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert numeric value below _n_. |
|
* |
|
* @param {Number} n |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
below: function(n, desc){ |
|
this.assert( |
|
this.obj < n |
|
, 'expected ' + this.inspect + ' to be below ' + n + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' to be above ' + n + (desc ? " | " + desc : "")); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert string value matches _regexp_. |
|
* |
|
* @param {RegExp} regexp |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
match: function(regexp, desc){ |
|
this.assert( |
|
regexp.exec(this.obj) |
|
, 'expected ' + this.inspect + ' to match ' + regexp + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' not to match ' + regexp + (desc ? " | " + desc : "")); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert property "length" exists and has value of _n_. |
|
* |
|
* @param {Number} n |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
length: function(n, desc){ |
|
this.obj.should.have.property('length'); |
|
var len = this.obj.length; |
|
this.assert( |
|
n == len |
|
, 'expected ' + this.inspect + ' to have a length of ' + n + ' but got ' + len + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' to not have a length of ' + len + (desc ? " | " + desc : "")); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert property _name_ exists, with optional _val_. |
|
* |
|
* @param {String} name |
|
* @param {Mixed} val |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
property: function(name, val, desc){ |
|
if (this.negate && undefined !== val) { |
|
if (undefined === this.obj[name]) { |
|
throw new Error(this.inspect + ' has no property ' + i(name) + (desc ? " | " + desc : "")); |
|
} |
|
} else { |
|
this.assert( |
|
undefined !== this.obj[name] |
|
, 'expected ' + this.inspect + ' to have a property ' + i(name) + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' to not have a property ' + i(name) + (desc ? " | " + desc : "")); |
|
} |
|
|
|
if (undefined !== val) { |
|
this.assert( |
|
val === this.obj[name] |
|
, 'expected ' + this.inspect + ' to have a property ' + i(name) |
|
+ ' of ' + i(val) + ', but got ' + i(this.obj[name]) + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' to not have a property ' + i(name) + ' of ' + i(val) + (desc ? " | " + desc : "")); |
|
} |
|
|
|
this.obj = this.obj[name]; |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert own property _name_ exists. |
|
* |
|
* @param {String} name |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
ownProperty: function(name, desc){ |
|
this.assert( |
|
this.obj.hasOwnProperty(name) |
|
, 'expected ' + this.inspect + ' to have own property ' + i(name) + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' to not have own property ' + i(name) + (desc ? " | " + desc : "")); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert that `obj` is present via `.indexOf()`. |
|
* |
|
* @param {Mixed} obj |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
include: function(obj, desc){ |
|
this.assert( |
|
~this.obj.indexOf(obj) |
|
, 'expected ' + this.inspect + ' to include ' + i(obj) + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' to not include ' + i(obj) + (desc ? " | " + desc : "")); |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert that an object equal to `obj` is present. |
|
* |
|
* @param {Array} obj |
|
* @param {String} description |
|
* @api public |
|
*/ |
|
|
|
includeEql: function(obj, desc){ |
|
this.assert( |
|
this.obj.some(function(item) { return eql(obj, item); }) |
|
, 'expected ' + this.inspect + ' to include an object equal to ' + i(obj) + (desc ? " | " + desc : "") |
|
, 'expected ' + this.inspect + ' to not include an object equal to ' + i(obj) + (desc ? " | " + desc : "")); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert that the array contains _obj_. |
|
* |
|
* @param {Mixed} obj |
|
* @api public |
|
*/ |
|
|
|
contain: function(obj){ |
|
console.warn('should.contain() is deprecated, use should.include()'); |
|
this.obj.should.be.an.instanceof(Array); |
|
this.assert( |
|
~this.obj.indexOf(obj) |
|
, 'expected ' + this.inspect + ' to contain ' + i(obj) |
|
, 'expected ' + this.inspect + ' to not contain ' + i(obj)); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert exact keys or inclusion of keys by using |
|
* the `.include` modifier. |
|
* |
|
* @param {Array|String ...} keys |
|
* @api public |
|
*/ |
|
|
|
keys: function(keys){ |
|
var str |
|
, ok = true; |
|
|
|
keys = keys instanceof Array |
|
? keys |
|
: Array.prototype.slice.call(arguments); |
|
|
|
if (!keys.length) throw new Error('keys required'); |
|
|
|
var actual = Object.keys(this.obj) |
|
, len = keys.length; |
|
|
|
// make sure they're all present |
|
ok = keys.every(function(key){ |
|
return ~actual.indexOf(key); |
|
}); |
|
|
|
// matching length |
|
ok = ok && keys.length == actual.length; |
|
|
|
// key string |
|
if (len > 1) { |
|
keys = keys.map(function(key){ |
|
return i(key); |
|
}); |
|
var last = keys.pop(); |
|
str = keys.join(', ') + ', and ' + last; |
|
} else { |
|
str = i(keys[0]); |
|
} |
|
|
|
// message |
|
str = 'have ' + (len > 1 ? 'keys ' : 'key ') + str; |
|
|
|
this.assert( |
|
ok |
|
, 'expected ' + this.inspect + ' to ' + str |
|
, 'expected ' + this.inspect + ' to not ' + str); |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert that header `field` has the given `val`. |
|
* |
|
* @param {String} field |
|
* @param {String} val |
|
* @return {Assertion} for chaining |
|
* @api public |
|
*/ |
|
|
|
header: function(field, val){ |
|
this.obj.should |
|
.have.property('headers').and |
|
.have.property(field.toLowerCase(), val); |
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert `.statusCode` of `code`. |
|
* |
|
* @param {Number} code |
|
* @return {Assertion} for chaining |
|
* @api public |
|
*/ |
|
|
|
status: function(code){ |
|
this.obj.should.have.property('statusCode'); |
|
var status = this.obj.statusCode; |
|
|
|
this.assert( |
|
code == status |
|
, 'expected response code of ' + code + ' ' + i(statusCodes[code]) |
|
+ ', but got ' + status + ' ' + i(statusCodes[status]) |
|
, 'expected to not respond with ' + code + ' ' + i(statusCodes[code])); |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
* Assert that this response has content-type: application/json. |
|
* |
|
* @return {Assertion} for chaining |
|
* @api public |
|
*/ |
|
|
|
get json() { |
|
this.obj.should.have.property('headers'); |
|
this.obj.headers.should.have.property('content-type'); |
|
this.obj.headers['content-type'].should.include('application/json'); |
|
}, |
|
|
|
/** |
|
* Assert that this response has content-type: text/html. |
|
* |
|
* @return {Assertion} for chaining |
|
* @api public |
|
*/ |
|
|
|
get html() { |
|
this.obj.should.have.property('headers'); |
|
this.obj.headers.should.have.property('content-type'); |
|
this.obj.headers['content-type'].should.include('text/html'); |
|
}, |
|
|
|
/** |
|
* Assert that this function will or will not |
|
* throw an exception. |
|
* |
|
* @return {Assertion} for chaining |
|
* @api public |
|
*/ |
|
|
|
throw: function(message){ |
|
var fn = this.obj |
|
, err = {} |
|
, errorInfo = '' |
|
, ok = true; |
|
|
|
try { |
|
fn(); |
|
ok = false; |
|
} catch (e) { |
|
err = e; |
|
} |
|
|
|
if (ok) { |
|
if ('string' == typeof message) { |
|
ok = message == err.message; |
|
} else if (message instanceof RegExp) { |
|
ok = message.test(err.message); |
|
} |
|
|
|
if (message && !ok) { |
|
if ('string' == typeof message) { |
|
errorInfo = " with a message matching '" + message + "', but got '" + err.message + "'"; |
|
} else { |
|
errorInfo = " with a message matching " + message + ", but got '" + err.message + "'"; |
|
} |
|
} |
|
} |
|
|
|
this.assert( |
|
ok |
|
, 'expected an exception to be thrown' + errorInfo |
|
, 'expected no exception to be thrown, got "' + err.message + '"'); |
|
|
|
return this; |
|
} |
|
}; |
|
|
|
/** |
|
* Aliases. |
|
*/ |
|
|
|
(function alias(name, as){ |
|
Assertion.prototype[as] = Assertion.prototype[name]; |
|
return alias; |
|
}) |
|
('length', 'lengthOf') |
|
('keys', 'key') |
|
('ownProperty', 'haveOwnProperty') |
|
('above', 'greaterThan') |
|
('below', 'lessThan'); |
|
|
|
|