chai
Version:
BDD/TDD assertion library for node.js and the browser. Test framework agnostic.
1,762 lines (1,589 loc) • 49.5 kB
JavaScript
/*!
* chai
* http://chaijs.com
* Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
* MIT Licensed
*/
module.exports = function (chai, _) {
var Assertion = chai.Assertion
, toString = Object.prototype.toString
, flag = _.flag;
/**
* ### Language Chains
*
* The following are provided as chainable getters to
* improve the readability of your assertions. They
* do not provide testing capabilities unless they
* have been overwritten by a plugin.
*
* **Chains**
*
* - to
* - be
* - been
* - is
* - that
* - which
* - and
* - has
* - have
* - with
* - at
* - of
* - same
*
* @name language chains
* @api public
*/
[ 'to', 'be', 'been'
, 'is', 'and', 'has', 'have'
, 'with', 'that', 'which', 'at'
, 'of', 'same' ].forEach(function (chain) {
Assertion.addProperty(chain, function () {
return this;
});
});
/**
* ### .not
*
* Negates any of assertions following in the chain.
*
* expect(foo).to.not.equal('bar');
* expect(goodFn).to.not.throw(Error);
* expect({ foo: 'baz' }).to.have.property('foo')
* .and.not.equal('bar');
*
* @name not
* @api public
*/
Assertion.addProperty('not', function () {
flag(this, 'negate', true);
});
/**
* ### .deep
*
* Sets the `deep` flag, later used by the `equal` and
* `property` assertions.
*
* expect(foo).to.deep.equal({ bar: 'baz' });
* expect({ foo: { bar: { baz: 'quux' } } })
* .to.have.deep.property('foo.bar.baz', 'quux');
*
* `.deep.property` special characters can be escaped
* by adding two slashes before the `.` or `[]`.
*
* var deepCss = { '.link': { '[target]': 42 }};
* expect(deepCss).to.have.deep.property('\\.link.\\[target\\]', 42);
*
* @name deep
* @api public
*/
Assertion.addProperty('deep', function () {
flag(this, 'deep', true);
});
/**
* ### .any
*
* Sets the `any` flag, (opposite of the `all` flag)
* later used in the `keys` assertion.
*
* expect(foo).to.have.any.keys('bar', 'baz');
*
* @name any
* @api public
*/
Assertion.addProperty('any', function () {
flag(this, 'any', true);
flag(this, 'all', false)
});
/**
* ### .all
*
* Sets the `all` flag (opposite of the `any` flag)
* later used by the `keys` assertion.
*
* expect(foo).to.have.all.keys('bar', 'baz');
*
* @name all
* @api public
*/
Assertion.addProperty('all', function () {
flag(this, 'all', true);
flag(this, 'any', false);
});
/**
* ### .a(type)
*
* The `a` and `an` assertions are aliases that can be
* used either as language chains or to assert a value's
* type.
*
* // typeof
* expect('test').to.be.a('string');
* expect({ foo: 'bar' }).to.be.an('object');
* expect(null).to.be.a('null');
* expect(undefined).to.be.an('undefined');
* expect(new Promise).to.be.a('promise');
* expect(new Float32Array()).to.be.a('float32array');
* expect(Symbol()).to.be.a('symbol');
*
* // es6 overrides
* expect({[Symbol.toStringTag]:()=>'foo'}).to.be.a('foo');
*
* // language chain
* expect(foo).to.be.an.instanceof(Foo);
*
* @name a
* @alias an
* @param {String} type
* @param {String} message _optional_
* @api public
*/
function an (type, msg) {
if (msg) flag(this, 'message', msg);
type = type.toLowerCase();
var obj = flag(this, 'object')
, article = ~[ 'a', 'e', 'i', 'o', 'u' ].indexOf(type.charAt(0)) ? 'an ' : 'a ';
this.assert(
type === _.type(obj)
, 'expected #{this} to be ' + article + type
, 'expected #{this} not to be ' + article + type
);
}
Assertion.addChainableMethod('an', an);
Assertion.addChainableMethod('a', an);
/**
* ### .include(value)
*
* The `include` and `contain` assertions can be used as either property
* based language chains or as methods to assert the inclusion of an object
* in an array or a substring in a string. When used as language chains,
* they toggle the `contains` flag for the `keys` assertion.
*
* expect([1,2,3]).to.include(2);
* expect('foobar').to.contain('foo');
* expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo');
*
* @name include
* @alias contain
* @alias includes
* @alias contains
* @param {Object|String|Number} obj
* @param {String} message _optional_
* @api public
*/
function includeChainingBehavior () {
flag(this, 'contains', true);
}
function include (val, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
var expected = false;
if (_.type(obj) === 'array' && _.type(val) === 'object') {
for (var i in obj) {
if (_.eql(obj[i], val)) {
expected = true;
break;
}
}
} else if (_.type(val) === 'object') {
if (!flag(this, 'negate')) {
for (var k in val) new Assertion(obj).property(k, val[k]);
return;
}
var subset = {};
for (var k in val) subset[k] = obj[k];
expected = _.eql(subset, val);
} else {
expected = (obj != undefined) && ~obj.indexOf(val);
}
this.assert(
expected
, 'expected #{this} to include ' + _.inspect(val)
, 'expected #{this} to not include ' + _.inspect(val));
}
Assertion.addChainableMethod('include', include, includeChainingBehavior);
Assertion.addChainableMethod('contain', include, includeChainingBehavior);
Assertion.addChainableMethod('contains', include, includeChainingBehavior);
Assertion.addChainableMethod('includes', include, includeChainingBehavior);
/**
* ### .ok
*
* Asserts that the target is truthy.
*
* expect('everthing').to.be.ok;
* expect(1).to.be.ok;
* expect(false).to.not.be.ok;
* expect(undefined).to.not.be.ok;
* expect(null).to.not.be.ok;
*
* @name ok
* @api public
*/
Assertion.addProperty('ok', function () {
this.assert(
flag(this, 'object')
, 'expected #{this} to be truthy'
, 'expected #{this} to be falsy');
});
/**
* ### .true
*
* Asserts that the target is `true`.
*
* expect(true).to.be.true;
* expect(1).to.not.be.true;
*
* @name true
* @api public
*/
Assertion.addProperty('true', function () {
this.assert(
true === flag(this, 'object')
, 'expected #{this} to be true'
, 'expected #{this} to be false'
, this.negate ? false : true
);
});
/**
* ### .false
*
* Asserts that the target is `false`.
*
* expect(false).to.be.false;
* expect(0).to.not.be.false;
*
* @name false
* @api public
*/
Assertion.addProperty('false', function () {
this.assert(
false === flag(this, 'object')
, 'expected #{this} to be false'
, 'expected #{this} to be true'
, this.negate ? true : false
);
});
/**
* ### .null
*
* Asserts that the target is `null`.
*
* expect(null).to.be.null;
* expect(undefined).to.not.be.null;
*
* @name null
* @api public
*/
Assertion.addProperty('null', function () {
this.assert(
null === flag(this, 'object')
, 'expected #{this} to be null'
, 'expected #{this} not to be null'
);
});
/**
* ### .undefined
*
* Asserts that the target is `undefined`.
*
* expect(undefined).to.be.undefined;
* expect(null).to.not.be.undefined;
*
* @name undefined
* @api public
*/
Assertion.addProperty('undefined', function () {
this.assert(
undefined === flag(this, 'object')
, 'expected #{this} to be undefined'
, 'expected #{this} not to be undefined'
);
});
/**
* ### .NaN
* Asserts that the target is `NaN`.
*
* expect('foo').to.be.NaN;
* expect(4).not.to.be.NaN;
*
* @name NaN
* @api public
*/
Assertion.addProperty('NaN', function () {
this.assert(
isNaN(flag(this, 'object'))
, 'expected #{this} to be NaN'
, 'expected #{this} not to be NaN'
);
});
/**
* ### .exist
*
* Asserts that the target is neither `null` nor `undefined`.
*
* var foo = 'hi'
* , bar = null
* , baz;
*
* expect(foo).to.exist;
* expect(bar).to.not.exist;
* expect(baz).to.not.exist;
*
* @name exist
* @api public
*/
Assertion.addProperty('exist', function () {
this.assert(
null != flag(this, 'object')
, 'expected #{this} to exist'
, 'expected #{this} to not exist'
);
});
/**
* ### .empty
*
* Asserts that the target's length is `0`. For arrays and strings, it checks
* the `length` property. For objects, it gets the count of
* enumerable keys.
*
* expect([]).to.be.empty;
* expect('').to.be.empty;
* expect({}).to.be.empty;
*
* @name empty
* @api public
*/
Assertion.addProperty('empty', function () {
this.assert(
Object.keys(Object(flag(this, 'object'))).length === 0
, 'expected #{this} to be empty'
, 'expected #{this} not to be empty'
);
});
/**
* ### .arguments
*
* Asserts that the target is an arguments object.
*
* function test () {
* expect(arguments).to.be.arguments;
* }
*
* @name arguments
* @alias Arguments
* @api public
*/
function checkArguments () {
var obj = flag(this, 'object')
, type = Object.prototype.toString.call(obj);
this.assert(
'[object Arguments]' === type
, 'expected #{this} to be arguments but got ' + type
, 'expected #{this} to not be arguments'
);
}
Assertion.addProperty('arguments', checkArguments);
Assertion.addProperty('Arguments', checkArguments);
/**
* ### .equal(value)
*
* Asserts that the target is strictly equal (`===`) to `value`.
* Alternately, if the `deep` flag is set, asserts that
* the target is deeply equal to `value`.
*
* expect('hello').to.equal('hello');
* expect(42).to.equal(42);
* expect(1).to.not.equal(true);
* expect({ foo: 'bar' }).to.not.equal({ foo: 'bar' });
* expect({ foo: 'bar' }).to.deep.equal({ foo: 'bar' });
*
* @name equal
* @alias equals
* @alias eq
* @alias deep.equal
* @param {Mixed} value
* @param {String} message _optional_
* @api public
*/
function assertEqual (val, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
if (flag(this, 'deep')) {
return this.eql(val);
} else {
this.assert(
val === obj
, 'expected #{this} to equal #{exp}'
, 'expected #{this} to not equal #{exp}'
, val
, this._obj
, true
);
}
}
Assertion.addMethod('equal', assertEqual);
Assertion.addMethod('equals', assertEqual);
Assertion.addMethod('eq', assertEqual);
/**
* ### .eql(value)
*
* Asserts that the target is deeply equal to `value`.
*
* expect({ foo: 'bar' }).to.eql({ foo: 'bar' });
* expect([ 1, 2, 3 ]).to.eql([ 1, 2, 3 ]);
*
* @name eql
* @alias eqls
* @param {Mixed} value
* @param {String} message _optional_
* @api public
*/
function assertEql(obj, msg) {
if (msg) flag(this, 'message', msg);
this.assert(
_.eql(obj, flag(this, 'object'))
, 'expected #{this} to deeply equal #{exp}'
, 'expected #{this} to not deeply equal #{exp}'
, obj
, this._obj
, true
);
}
Assertion.addMethod('eql', assertEql);
Assertion.addMethod('eqls', assertEql);
/**
* ### .above(value)
*
* Asserts that the target is greater than `value`.
*
* expect(10).to.be.above(5);
*
* Can also be used in conjunction with `length` to
* assert a minimum length. The benefit being a
* more informative error message than if the length
* was supplied directly.
*
* expect('foo').to.have.length.above(2);
* expect([ 1, 2, 3 ]).to.have.length.above(2);
*
* @name above
* @alias gt
* @alias greaterThan
* @param {Number} value
* @param {String} message _optional_
* @api public
*/
function assertAbove (n, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
if (flag(this, 'doLength')) {
new Assertion(obj, msg).to.have.property('length');
var len = obj.length;
this.assert(
len > n
, 'expected #{this} to have a length above #{exp} but got #{act}'
, 'expected #{this} to not have a length above #{exp}'
, n
, len
);
} else {
this.assert(
obj > n
, 'expected #{this} to be above ' + n
, 'expected #{this} to be at most ' + n
);
}
}
Assertion.addMethod('above', assertAbove);
Assertion.addMethod('gt', assertAbove);
Assertion.addMethod('greaterThan', assertAbove);
/**
* ### .least(value)
*
* Asserts that the target is greater than or equal to `value`.
*
* expect(10).to.be.at.least(10);
*
* Can also be used in conjunction with `length` to
* assert a minimum length. The benefit being a
* more informative error message than if the length
* was supplied directly.
*
* expect('foo').to.have.length.of.at.least(2);
* expect([ 1, 2, 3 ]).to.have.length.of.at.least(3);
*
* @name least
* @alias gte
* @param {Number} value
* @param {String} message _optional_
* @api public
*/
function assertLeast (n, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
if (flag(this, 'doLength')) {
new Assertion(obj, msg).to.have.property('length');
var len = obj.length;
this.assert(
len >= n
, 'expected #{this} to have a length at least #{exp} but got #{act}'
, 'expected #{this} to have a length below #{exp}'
, n
, len
);
} else {
this.assert(
obj >= n
, 'expected #{this} to be at least ' + n
, 'expected #{this} to be below ' + n
);
}
}
Assertion.addMethod('least', assertLeast);
Assertion.addMethod('gte', assertLeast);
/**
* ### .below(value)
*
* Asserts that the target is less than `value`.
*
* expect(5).to.be.below(10);
*
* Can also be used in conjunction with `length` to
* assert a maximum length. The benefit being a
* more informative error message than if the length
* was supplied directly.
*
* expect('foo').to.have.length.below(4);
* expect([ 1, 2, 3 ]).to.have.length.below(4);
*
* @name below
* @alias lt
* @alias lessThan
* @param {Number} value
* @param {String} message _optional_
* @api public
*/
function assertBelow (n, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
if (flag(this, 'doLength')) {
new Assertion(obj, msg).to.have.property('length');
var len = obj.length;
this.assert(
len < n
, 'expected #{this} to have a length below #{exp} but got #{act}'
, 'expected #{this} to not have a length below #{exp}'
, n
, len
);
} else {
this.assert(
obj < n
, 'expected #{this} to be below ' + n
, 'expected #{this} to be at least ' + n
);
}
}
Assertion.addMethod('below', assertBelow);
Assertion.addMethod('lt', assertBelow);
Assertion.addMethod('lessThan', assertBelow);
/**
* ### .most(value)
*
* Asserts that the target is less than or equal to `value`.
*
* expect(5).to.be.at.most(5);
*
* Can also be used in conjunction with `length` to
* assert a maximum length. The benefit being a
* more informative error message than if the length
* was supplied directly.
*
* expect('foo').to.have.length.of.at.most(4);
* expect([ 1, 2, 3 ]).to.have.length.of.at.most(3);
*
* @name most
* @alias lte
* @param {Number} value
* @param {String} message _optional_
* @api public
*/
function assertMost (n, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
if (flag(this, 'doLength')) {
new Assertion(obj, msg).to.have.property('length');
var len = obj.length;
this.assert(
len <= n
, 'expected #{this} to have a length at most #{exp} but got #{act}'
, 'expected #{this} to have a length above #{exp}'
, n
, len
);
} else {
this.assert(
obj <= n
, 'expected #{this} to be at most ' + n
, 'expected #{this} to be above ' + n
);
}
}
Assertion.addMethod('most', assertMost);
Assertion.addMethod('lte', assertMost);
/**
* ### .within(start, finish)
*
* Asserts that the target is within a range.
*
* expect(7).to.be.within(5,10);
*
* Can also be used in conjunction with `length` to
* assert a length range. The benefit being a
* more informative error message than if the length
* was supplied directly.
*
* expect('foo').to.have.length.within(2,4);
* expect([ 1, 2, 3 ]).to.have.length.within(2,4);
*
* @name within
* @param {Number} start lowerbound inclusive
* @param {Number} finish upperbound inclusive
* @param {String} message _optional_
* @api public
*/
Assertion.addMethod('within', function (start, finish, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object')
, range = start + '..' + finish;
if (flag(this, 'doLength')) {
new Assertion(obj, msg).to.have.property('length');
var len = obj.length;
this.assert(
len >= start && len <= finish
, 'expected #{this} to have a length within ' + range
, 'expected #{this} to not have a length within ' + range
);
} else {
this.assert(
obj >= start && obj <= finish
, 'expected #{this} to be within ' + range
, 'expected #{this} to not be within ' + range
);
}
});
/**
* ### .instanceof(constructor)
*
* Asserts that the target is an instance of `constructor`.
*
* var Tea = function (name) { this.name = name; }
* , Chai = new Tea('chai');
*
* expect(Chai).to.be.an.instanceof(Tea);
* expect([ 1, 2, 3 ]).to.be.instanceof(Array);
*
* @name instanceof
* @param {Constructor} constructor
* @param {String} message _optional_
* @alias instanceOf
* @api public
*/
function assertInstanceOf (constructor, msg) {
if (msg) flag(this, 'message', msg);
var name = _.getName(constructor);
this.assert(
flag(this, 'object') instanceof constructor
, 'expected #{this} to be an instance of ' + name
, 'expected #{this} to not be an instance of ' + name
);
};
Assertion.addMethod('instanceof', assertInstanceOf);
Assertion.addMethod('instanceOf', assertInstanceOf);
/**
* ### .property(name, [value])
*
* Asserts that the target has a property `name`, optionally asserting that
* the value of that property is strictly equal to `value`.
* If the `deep` flag is set, you can use dot- and bracket-notation for deep
* references into objects and arrays.
*
* // simple referencing
* var obj = { foo: 'bar' };
* expect(obj).to.have.property('foo');
* expect(obj).to.have.property('foo', 'bar');
*
* // deep referencing
* var deepObj = {
* green: { tea: 'matcha' }
* , teas: [ 'chai', 'matcha', { tea: 'konacha' } ]
* };
*
* expect(deepObj).to.have.deep.property('green.tea', 'matcha');
* expect(deepObj).to.have.deep.property('teas[1]', 'matcha');
* expect(deepObj).to.have.deep.property('teas[2].tea', 'konacha');
*
* You can also use an array as the starting point of a `deep.property`
* assertion, or traverse nested arrays.
*
* var arr = [
* [ 'chai', 'matcha', 'konacha' ]
* , [ { tea: 'chai' }
* , { tea: 'matcha' }
* , { tea: 'konacha' } ]
* ];
*
* expect(arr).to.have.deep.property('[0][1]', 'matcha');
* expect(arr).to.have.deep.property('[1][2].tea', 'konacha');
*
* Furthermore, `property` changes the subject of the assertion
* to be the value of that property from the original object. This
* permits for further chainable assertions on that property.
*
* expect(obj).to.have.property('foo')
* .that.is.a('string');
* expect(deepObj).to.have.property('green')
* .that.is.an('object')
* .that.deep.equals({ tea: 'matcha' });
* expect(deepObj).to.have.property('teas')
* .that.is.an('array')
* .with.deep.property('[2]')
* .that.deep.equals({ tea: 'konacha' });
*
* Note that dots and bracket in `name` must be backslash-escaped when
* the `deep` flag is set, while they must NOT be escaped when the `deep`
* flag is not set.
*
* // simple referencing
* var css = { '.link[target]': 42 };
* expect(css).to.have.property('.link[target]', 42);
*
* // deep referencing
* var deepCss = { '.link': { '[target]': 42 }};
* expect(deepCss).to.have.deep.property('\\.link.\\[target\\]', 42);
*
* @name property
* @alias deep.property
* @param {String} name
* @param {Mixed} value (optional)
* @param {String} message _optional_
* @returns value of property for chaining
* @api public
*/
Assertion.addMethod('property', function (name, val, msg) {
if (msg) flag(this, 'message', msg);
var isDeep = !!flag(this, 'deep')
, descriptor = isDeep ? 'deep property ' : 'property '
, negate = flag(this, 'negate')
, obj = flag(this, 'object')
, pathInfo = isDeep ? _.getPathInfo(name, obj) : null
, hasProperty = isDeep
? pathInfo.exists
: _.hasProperty(name, obj)
, value = isDeep
? pathInfo.value
: obj[name];
if (negate && arguments.length > 1) {
if (undefined === value) {
msg = (msg != null) ? msg + ': ' : '';
throw new Error(msg + _.inspect(obj) + ' has no ' + descriptor + _.inspect(name));
}
} else {
this.assert(
hasProperty
, 'expected #{this} to have a ' + descriptor + _.inspect(name)
, 'expected #{this} to not have ' + descriptor + _.inspect(name));
}
if (arguments.length > 1) {
this.assert(
val === value
, 'expected #{this} to have a ' + descriptor + _.inspect(name) + ' of #{exp}, but got #{act}'
, 'expected #{this} to not have a ' + descriptor + _.inspect(name) + ' of #{act}'
, val
, value
);
}
flag(this, 'object', value);
});
/**
* ### .ownProperty(name)
*
* Asserts that the target has an own property `name`.
*
* expect('test').to.have.ownProperty('length');
*
* @name ownProperty
* @alias haveOwnProperty
* @param {String} name
* @param {String} message _optional_
* @api public
*/
function assertOwnProperty (name, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
this.assert(
obj.hasOwnProperty(name)
, 'expected #{this} to have own property ' + _.inspect(name)
, 'expected #{this} to not have own property ' + _.inspect(name)
);
}
Assertion.addMethod('ownProperty', assertOwnProperty);
Assertion.addMethod('haveOwnProperty', assertOwnProperty);
/**
* ### .ownPropertyDescriptor(name[, descriptor[, message]])
*
* Asserts that the target has an own property descriptor `name`, that optionally matches `descriptor`.
*
* expect('test').to.have.ownPropertyDescriptor('length');
* expect('test').to.have.ownPropertyDescriptor('length', { enumerable: false, configurable: false, writable: false, value: 4 });
* expect('test').not.to.have.ownPropertyDescriptor('length', { enumerable: false, configurable: false, writable: false, value: 3 });
* expect('test').ownPropertyDescriptor('length').to.have.property('enumerable', false);
* expect('test').ownPropertyDescriptor('length').to.have.keys('value');
*
* @name ownPropertyDescriptor
* @alias haveOwnPropertyDescriptor
* @param {String} name
* @param {Object} descriptor _optional_
* @param {String} message _optional_
* @api public
*/
function assertOwnPropertyDescriptor (name, descriptor, msg) {
if (typeof descriptor === 'string') {
msg = descriptor;
descriptor = null;
}
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
var actualDescriptor = Object.getOwnPropertyDescriptor(Object(obj), name);
if (actualDescriptor && descriptor) {
this.assert(
_.eql(descriptor, actualDescriptor)
, 'expected the own property descriptor for ' + _.inspect(name) + ' on #{this} to match ' + _.inspect(descriptor) + ', got ' + _.inspect(actualDescriptor)
, 'expected the own property descriptor for ' + _.inspect(name) + ' on #{this} to not match ' + _.inspect(descriptor)
, descriptor
, actualDescriptor
, true
);
} else {
this.assert(
actualDescriptor
, 'expected #{this} to have an own property descriptor for ' + _.inspect(name)
, 'expected #{this} to not have an own property descriptor for ' + _.inspect(name)
);
}
flag(this, 'object', actualDescriptor);
}
Assertion.addMethod('ownPropertyDescriptor', assertOwnPropertyDescriptor);
Assertion.addMethod('haveOwnPropertyDescriptor', assertOwnPropertyDescriptor);
/**
* ### .length
*
* Sets the `doLength` flag later used as a chain precursor to a value
* comparison for the `length` property.
*
* expect('foo').to.have.length.above(2);
* expect([ 1, 2, 3 ]).to.have.length.above(2);
* expect('foo').to.have.length.below(4);
* expect([ 1, 2, 3 ]).to.have.length.below(4);
* expect('foo').to.have.length.within(2,4);
* expect([ 1, 2, 3 ]).to.have.length.within(2,4);
*
* *Deprecation notice:* Using `length` as an assertion will be deprecated
* in version 2.4.0 and removed in 3.0.0. Code using the old style of
* asserting for `length` property value using `length(value)` should be
* switched to use `lengthOf(value)` instead.
*
* @name length
* @api public
*/
/**
* ### .lengthOf(value[, message])
*
* Asserts that the target's `length` property has
* the expected value.
*
* expect([ 1, 2, 3]).to.have.lengthOf(3);
* expect('foobar').to.have.lengthOf(6);
*
* @name lengthOf
* @param {Number} length
* @param {String} message _optional_
* @api public
*/
function assertLengthChain () {
flag(this, 'doLength', true);
}
function assertLength (n, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
new Assertion(obj, msg).to.have.property('length');
var len = obj.length;
this.assert(
len == n
, 'expected #{this} to have a length of #{exp} but got #{act}'
, 'expected #{this} to not have a length of #{act}'
, n
, len
);
}
Assertion.addChainableMethod('length', assertLength, assertLengthChain);
Assertion.addMethod('lengthOf', assertLength);
/**
* ### .match(regexp)
*
* Asserts that the target matches a regular expression.
*
* expect('foobar').to.match(/^foo/);
*
* @name match
* @alias matches
* @param {RegExp} RegularExpression
* @param {String} message _optional_
* @api public
*/
function assertMatch(re, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
this.assert(
re.exec(obj)
, 'expected #{this} to match ' + re
, 'expected #{this} not to match ' + re
);
}
Assertion.addMethod('match', assertMatch);
Assertion.addMethod('matches', assertMatch);
/**
* ### .string(string)
*
* Asserts that the string target contains another string.
*
* expect('foobar').to.have.string('bar');
*
* @name string
* @param {String} string
* @param {String} message _optional_
* @api public
*/
Assertion.addMethod('string', function (str, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
new Assertion(obj, msg).is.a('string');
this.assert(
~obj.indexOf(str)
, 'expected #{this} to contain ' + _.inspect(str)
, 'expected #{this} to not contain ' + _.inspect(str)
);
});
/**
* ### .keys(key1, [key2], [...])
*
* Asserts that the target contains any or all of the passed-in keys.
* Use in combination with `any`, `all`, `contains`, or `have` will affect
* what will pass.
*
* When used in conjunction with `any`, at least one key that is passed
* in must exist in the target object. This is regardless whether or not
* the `have` or `contain` qualifiers are used. Note, either `any` or `all`
* should be used in the assertion. If neither are used, the assertion is
* defaulted to `all`.
*
* When both `all` and `contain` are used, the target object must have at
* least all of the passed-in keys but may have more keys not listed.
*
* When both `all` and `have` are used, the target object must both contain
* all of the passed-in keys AND the number of keys in the target object must
* match the number of keys passed in (in other words, a target object must
* have all and only all of the passed-in keys).
*
* expect({ foo: 1, bar: 2 }).to.have.any.keys('foo', 'baz');
* expect({ foo: 1, bar: 2 }).to.have.any.keys('foo');
* expect({ foo: 1, bar: 2 }).to.contain.any.keys('bar', 'baz');
* expect({ foo: 1, bar: 2 }).to.contain.any.keys(['foo']);
* expect({ foo: 1, bar: 2 }).to.contain.any.keys({'foo': 6});
* expect({ foo: 1, bar: 2 }).to.have.all.keys(['bar', 'foo']);
* expect({ foo: 1, bar: 2 }).to.have.all.keys({'bar': 6, 'foo': 7});
* expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys(['bar', 'foo']);
* expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys({'bar': 6});
*
*
* @name keys
* @alias key
* @param {String...|Array|Object} keys
* @api public
*/
function assertKeys (keys) {
var obj = flag(this, 'object')
, str
, ok = true
, mixedArgsMsg = 'keys must be given single argument of Array|Object|String, or multiple String arguments';
switch (_.type(keys)) {
case "array":
if (arguments.length > 1) throw (new Error(mixedArgsMsg));
break;
case "object":
if (arguments.length > 1) throw (new Error(mixedArgsMsg));
keys = Object.keys(keys);
break;
default:
keys = Array.prototype.slice.call(arguments);
}
if (!keys.length) throw new Error('keys required');
var actual = Object.keys(obj)
, expected = keys
, len = keys.length
, any = flag(this, 'any')
, all = flag(this, 'all');
if (!any && !all) {
all = true;
}
// Has any
if (any) {
var intersection = expected.filter(function(key) {
return ~actual.indexOf(key);
});
ok = intersection.length > 0;
}
// Has all
if (all) {
ok = keys.every(function(key){
return ~actual.indexOf(key);
});
if (!flag(this, 'negate') && !flag(this, 'contains')) {
ok = ok && keys.length == actual.length;
}
}
// Key string
if (len > 1) {
keys = keys.map(function(key){
return _.inspect(key);
});
var last = keys.pop();
if (all) {
str = keys.join(', ') + ', and ' + last;
}
if (any) {
str = keys.join(', ') + ', or ' + last;
}
} else {
str = _.inspect(keys[0]);
}
// Form
str = (len > 1 ? 'keys ' : 'key ') + str;
// Have / include
str = (flag(this, 'contains') ? 'contain ' : 'have ') + str;
// Assertion
this.assert(
ok
, 'expected #{this} to ' + str
, 'expected #{this} to not ' + str
, expected.slice(0).sort()
, actual.sort()
, true
);
}
Assertion.addMethod('keys', assertKeys);
Assertion.addMethod('key', assertKeys);
/**
* ### .throw(constructor)
*
* Asserts that the function target will throw a specific error, or specific type of error
* (as determined using `instanceof`), optionally with a RegExp or string inclusion test
* for the error's message.
*
* var err = new ReferenceError('This is a bad function.');
* var fn = function () { throw err; }
* expect(fn).to.throw(ReferenceError);
* expect(fn).to.throw(Error);
* expect(fn).to.throw(/bad function/);
* expect(fn).to.not.throw('good function');
* expect(fn).to.throw(ReferenceError, /bad function/);
* expect(fn).to.throw(err);
* expect(fn).to.not.throw(new RangeError('Out of range.'));
*
* Please note that when a throw expectation is negated, it will check each
* parameter independently, starting with error constructor type. The appropriate way
* to check for the existence of a type of error but for a message that does not match
* is to use `and`.
*
* expect(fn).to.throw(ReferenceError)
* .and.not.throw(/good function/);
*
* @name throw
* @alias throws
* @alias Throw
* @param {ErrorConstructor} constructor
* @param {String|RegExp} expected error message
* @param {String} message _optional_
* @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types
* @returns error for chaining (null if no error)
* @api public
*/
function assertThrows (constructor, errMsg, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
new Assertion(obj, msg).is.a('function');
var thrown = false
, desiredError = null
, name = null
, thrownError = null;
if (arguments.length === 0) {
errMsg = null;
constructor = null;
} else if (constructor && (constructor instanceof RegExp || 'string' === typeof constructor)) {
errMsg = constructor;
constructor = null;
} else if (constructor && constructor instanceof Error) {
desiredError = constructor;
constructor = null;
errMsg = null;
} else if (typeof constructor === 'function') {
name = constructor.prototype.name || constructor.name;
if (name === 'Error' && constructor !== Error) {
name = (new constructor()).name;
}
} else {
constructor = null;
}
try {
obj();
} catch (err) {
// first, check desired error
if (desiredError) {
this.assert(
err === desiredError
, 'expected #{this} to throw #{exp} but #{act} was thrown'
, 'expected #{this} to not throw #{exp}'
, (desiredError instanceof Error ? desiredError.toString() : desiredError)
, (err instanceof Error ? err.toString() : err)
);
flag(this, 'object', err);
return this;
}
// next, check constructor
if (constructor) {
this.assert(
err instanceof constructor
, 'expected #{this} to throw #{exp} but #{act} was thrown'
, 'expected #{this} to not throw #{exp} but #{act} was thrown'
, name
, (err instanceof Error ? err.toString() : err)
);
if (!errMsg) {
flag(this, 'object', err);
return this;
}
}
// next, check message
var message = 'error' === _.type(err) && "message" in err
? err.message
: '' + err;
if ((message != null) && errMsg && errMsg instanceof RegExp) {
this.assert(
errMsg.exec(message)
, 'expected #{this} to throw error matching #{exp} but got #{act}'
, 'expected #{this} to throw error not matching #{exp}'
, errMsg
, message
);
flag(this, 'object', err);
return this;
} else if ((message != null) && errMsg && 'string' === typeof errMsg) {
this.assert(
~message.indexOf(errMsg)
, 'expected #{this} to throw error including #{exp} but got #{act}'
, 'expected #{this} to throw error not including #{act}'
, errMsg
, message
);
flag(this, 'object', err);
return this;
} else {
thrown = true;
thrownError = err;
}
}
var actuallyGot = ''
, expectedThrown = name !== null
? name
: desiredError
? '#{exp}' //_.inspect(desiredError)
: 'an error';
if (thrown) {
actuallyGot = ' but #{act} was thrown'
}
this.assert(
thrown === true
, 'expected #{this} to throw ' + expectedThrown + actuallyGot
, 'expected #{this} to not throw ' + expectedThrown + actuallyGot
, (desiredError instanceof Error ? desiredError.toString() : desiredError)
, (thrownError instanceof Error ? thrownError.toString() : thrownError)
);
flag(this, 'object', thrownError);
};
Assertion.addMethod('throw', assertThrows);
Assertion.addMethod('throws', assertThrows);
Assertion.addMethod('Throw', assertThrows);
/**
* ### .respondTo(method)
*
* Asserts that the object or class target will respond to a method.
*
* Klass.prototype.bar = function(){};
* expect(Klass).to.respondTo('bar');
* expect(obj).to.respondTo('bar');
*
* To check if a constructor will respond to a static function,
* set the `itself` flag.
*
* Klass.baz = function(){};
* expect(Klass).itself.to.respondTo('baz');
*
* @name respondTo
* @alias respondsTo
* @param {String} method
* @param {String} message _optional_
* @api public
*/
function respondTo (method, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object')
, itself = flag(this, 'itself')
, context = ('function' === _.type(obj) && !itself)
? obj.prototype[method]
: obj[method];
this.assert(
'function' === typeof context
, 'expected #{this} to respond to ' + _.inspect(method)
, 'expected #{this} to not respond to ' + _.inspect(method)
);
}
Assertion.addMethod('respondTo', respondTo);
Assertion.addMethod('respondsTo', respondTo);
/**
* ### .itself
*
* Sets the `itself` flag, later used by the `respondTo` assertion.
*
* function Foo() {}
* Foo.bar = function() {}
* Foo.prototype.baz = function() {}
*
* expect(Foo).itself.to.respondTo('bar');
* expect(Foo).itself.not.to.respondTo('baz');
*
* @name itself
* @api public
*/
Assertion.addProperty('itself', function () {
flag(this, 'itself', true);
});
/**
* ### .satisfy(method)
*
* Asserts that the target passes a given truth test.
*
* expect(1).to.satisfy(function(num) { return num > 0; });
*
* @name satisfy
* @alias satisfies
* @param {Function} matcher
* @param {String} message _optional_
* @api public
*/
function satisfy (matcher, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
var result = matcher(obj);
this.assert(
result
, 'expected #{this} to satisfy ' + _.objDisplay(matcher)
, 'expected #{this} to not satisfy' + _.objDisplay(matcher)
, this.negate ? false : true
, result
);
}
Assertion.addMethod('satisfy', satisfy);
Assertion.addMethod('satisfies', satisfy);
/**
* ### .closeTo(expected, delta)
*
* Asserts that the target is equal `expected`, to within a +/- `delta` range.
*
* expect(1.5).to.be.closeTo(1, 0.5);
*
* @name closeTo
* @param {Number} expected
* @param {Number} delta
* @param {String} message _optional_
* @api public
*/
Assertion.addMethod('closeTo', function (expected, delta, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
new Assertion(obj, msg).is.a('number');
if (_.type(expected) !== 'number' || _.type(delta) !== 'number') {
throw new Error('the arguments to closeTo must be numbers');
}
this.assert(
Math.abs(obj - expected) <= delta
, 'expected #{this} to be close to ' + expected + ' +/- ' + delta
, 'expected #{this} not to be close to ' + expected + ' +/- ' + delta
);
});
function isSubsetOf(subset, superset, cmp) {
return subset.every(function(elem) {
if (!cmp) return superset.indexOf(elem) !== -1;
return superset.some(function(elem2) {
return cmp(elem, elem2);
});
})
}
/**
* ### .members(set)
*
* Asserts that the target is a superset of `set`,
* or that the target and `set` have the same strictly-equal (===) members.
* Alternately, if the `deep` flag is set, set members are compared for deep
* equality.
*
* expect([1, 2, 3]).to.include.members([3, 2]);
* expect([1, 2, 3]).to.not.include.members([3, 2, 8]);
*
* expect([4, 2]).to.have.members([2, 4]);
* expect([5, 2]).to.not.have.members([5, 2, 1]);
*
* expect([{ id: 1 }]).to.deep.include.members([{ id: 1 }]);
*
* @name members
* @param {Array} set
* @param {String} message _optional_
* @api public
*/
Assertion.addMethod('members', function (subset, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
new Assertion(obj).to.be.an('array');
new Assertion(subset).to.be.an('array');
var cmp = flag(this, 'deep') ? _.eql : undefined;
if (flag(this, 'contains')) {
return this.assert(
isSubsetOf(subset, obj, cmp)
, 'expected #{this} to be a superset of #{act}'
, 'expected #{this} to not be a superset of #{act}'
, obj
, subset
);
}
this.assert(
isSubsetOf(obj, subset, cmp) && isSubsetOf(subset, obj, cmp)
, 'expected #{this} to have the same members as #{act}'
, 'expected #{this} to not have the same members as #{act}'
, obj
, subset
);
});
/**
* ### .change(function)
*
* Asserts that a function changes an object property
*
* var obj = { val: 10 };
* var fn = function() { obj.val += 3 };
* var noChangeFn = function() { return 'foo' + 'bar'; }
* expect(fn).to.change(obj, 'val');
* expect(noChangFn).to.not.change(obj, 'val')
*
* @name change
* @alias changes
* @alias Change
* @param {String} object
* @param {String} property name
* @param {String} message _optional_
* @api public
*/
function assertChanges (object, prop, msg) {
if (msg) flag(this, 'message', msg);
var fn = flag(this, 'object');
new Assertion(object, msg).to.have.property(prop);
new Assertion(fn).is.a('function');
var initial = object[prop];
fn();
this.assert(
initial !== object[prop]
, 'expected .' + prop + ' to change'
, 'expected .' + prop + ' to not change'
);
}
Assertion.addChainableMethod('change', assertChanges);
Assertion.addChainableMethod('changes', assertChanges);
/**
* ### .increase(function)
*
* Asserts that a function increases an object property
*
* var obj = { val: 10 };
* var fn = function() { obj.val = 15 };
* expect(fn).to.increase(obj, 'val');
*
* @name increase
* @alias increases
* @alias Increase
* @param {String} object
* @param {String} property name
* @param {String} message _optional_
* @api public
*/
function assertIncreases (object, prop, msg) {
if (msg) flag(this, 'message', msg);
var fn = flag(this, 'object');
new Assertion(object, msg).to.have.property(prop);
new Assertion(fn).is.a('function');
var initial = object[prop];
fn();
this.assert(
object[prop] - initial > 0
, 'expected .' + prop + ' to increase'
, 'expected .' + prop + ' to not increase'
);
}
Assertion.addChainableMethod('increase', assertIncreases);
Assertion.addChainableMethod('increases', assertIncreases);
/**
* ### .decrease(function)
*
* Asserts that a function decreases an object property
*
* var obj = { val: 10 };
* var fn = function() { obj.val = 5 };
* expect(fn).to.decrease(obj, 'val');
*
* @name decrease
* @alias decreases
* @alias Decrease
* @param {String} object
* @param {String} property name
* @param {String} message _optional_
* @api public
*/
function assertDecreases (object, prop, msg) {
if (msg) flag(this, 'message', msg);
var fn = flag(this, 'object');
new Assertion(object, msg).to.have.property(prop);
new Assertion(fn).is.a('function');
var initial = object[prop];
fn();
this.assert(
object[prop] - initial < 0
, 'expected .' + prop + ' to decrease'
, 'expected .' + prop + ' to not decrease'
);
}
Assertion.addChainableMethod('decrease', assertDecreases);
Assertion.addChainableMethod('decreases', assertDecreases);
/**
* ### .extensible
*
* Asserts that the target is extensible (can have new properties added to
* it).
*
* var nonExtensibleObject = Object.preventExtensions({});
* var sealedObject = Object.seal({});
* var frozenObject = Object.freeze({});
*
* expect({}).to.be.extensible;
* expect(nonExtensibleObject).to.not.be.extensible;
* expect(sealedObject).to.not.be.extensible;
* expect(frozenObject).to.not.be.extensible;
*
* @name extensible
* @api public
*/
Assertion.addProperty('extensible', function() {
var obj = flag(this, 'object');
// In ES5, if the argument to this method is not an object (a primitive), then it will cause a TypeError.
// In ES6, a non-object argument will be treated as if it was a non-extensible ordinary object, simply return false.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible
// The following provides ES6 behavior when a TypeError is thrown under ES5.
var isExtensible;
try {
isExtensible = Object.isExtensible(obj);
} catch (err) {
if (err instanceof TypeError) isExtensible = false;
else throw err;
}
this.assert(
isExtensible
, 'expected #{this} to be extensible'
, 'expected #{this} to not be extensible'
);
});
/**
* ### .sealed
*
* Asserts that the target is sealed (cannot have new properties added to it
* and its existing properties cannot be removed).
*
* var sealedObject = Object.seal({});
* var frozenObject = Object.freeze({});
*
* expect(sealedObject).to.be.sealed;
* expect(frozenObject).to.be.sealed;
* expect({}).to.not.be.sealed;
*
* @name sealed
* @api public
*/
Assertion.addProperty('sealed', function() {
var obj = flag(this, 'object');
// In ES5, if the argument to this method is not an object (a primitive), then it will cause a TypeError.
// In ES6, a non-object argument will be treated as if it was a sealed ordinary object, simply return true.
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isSealed
// The following provides ES6 behavior when a TypeError is thrown under ES5.
var isSealed;
try {
isSealed = Object.isSealed(obj);
} catch (err) {
if (err instanceof TypeError) isSealed = true;
else throw err;
}
this.assert(
isSealed
, 'expected #{this} to be sealed'
, 'expected #{this} to not be sealed'
);
});
/**
* ### .frozen
*
* Asserts that the target is frozen (cannot have new properties added to it
* and its existing properties cannot be modified).
*
* var frozenObject = Object.freeze({});
*
* expect(frozenObject).to.be.frozen;
* expect({}).to.not.be.frozen;
*
* @name frozen
* @api public
*/
Assertion.addProperty('frozen', function() {
var obj = flag(this, 'object');
// In ES5, if the argument to this method is not an object (a primitive), then it will cause a TypeError.
// In ES6, a non-object argument will be treated as if it was a frozen ordinary object, simply return true.
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isFrozen
// The following provides ES6 behavior when a TypeError is thrown under ES5.
var isFrozen;
try {
isFrozen = Object.isFrozen(obj);
} catch (err) {
if (err instanceof TypeError) isFrozen = true;
else throw err;
}
this.assert(
isFrozen
, 'expected #{this} to be frozen'
, 'expected #{this} to not be frozen'
);
});
};