cloudboost-tv
Version:
Database Service that does Storage, Search, Real-time and a whole lot more.
372 lines (318 loc) • 11.5 kB
JavaScript
/*!
* Should
* Copyright(c) 2010-2014 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
var util = require('../util');
var eql = require('should-equal');
var aSlice = Array.prototype.slice;
module.exports = function(should, Assertion) {
var i = should.format;
/**
* Asserts given object has some descriptor. **On success it change given object to be value of property**.
*
* @name propertyWithDescriptor
* @memberOf Assertion
* @category assertion property
* @param {string} name Name of property
* @param {Object} desc Descriptor like used in Object.defineProperty (not required to add all properties)
* @example
*
* ({ a: 10 }).should.have.propertyWithDescriptor('a', { enumerable: true });
*/
Assertion.add('propertyWithDescriptor', function(name, desc) {
this.params = {actual: this.obj, operator: 'to have own property with descriptor ' + i(desc)};
var obj = this.obj;
this.have.ownProperty(name);
should(Object.getOwnPropertyDescriptor(Object(obj), name)).have.properties(desc);
});
function processPropsArgs() {
var args = {};
if(arguments.length > 1) {
args.names = aSlice.call(arguments);
} else {
var arg = arguments[0];
var t = should.type(arg);
if(t == should.type.STRING) {
args.names = [arg];
} else if(util.isIndexable(arg)) {
args.names = arg;
} else {
args.names = Object.keys(arg);
args.values = arg;
}
}
return args;
}
/**
* Asserts given object has enumerable property with optionally value. **On success it change given object to be value of property**.
*
* @name enumerable
* @memberOf Assertion
* @category assertion property
* @param {string} name Name of property
* @param {*} [val] Optional property value to check
* @example
*
* ({ a: 10 }).should.have.enumerable('a');
*/
Assertion.add('enumerable', function(name, val) {
name = util.convertPropertyName(name);
this.params = {
operator: "to have enumerable property " + util.formatProp(name) + (arguments.length > 1 ? " equal to " + i(val): "")
};
var desc = { enumerable: true };
if(arguments.length > 1) desc.value = val;
this.have.propertyWithDescriptor(name, desc);
});
/**
* Asserts given object has enumerable properties
*
* @name enumerables
* @memberOf Assertion
* @category assertion property
* @param {Array|...string|Object} names Names of property
* @example
*
* ({ a: 10, b: 10 }).should.have.enumerables('a');
*/
Assertion.add('enumerables', function(names) {
var args = processPropsArgs.apply(null, arguments);
this.params = {
operator: "to have enumerables " + args.names.map(util.formatProp)
};
var obj = this.obj;
args.names.forEach(function(name) {
obj.should.have.enumerable(name);
});
});
/**
* Asserts given object has property with optionally value. **On success it change given object to be value of property**.
*
* @name property
* @memberOf Assertion
* @category assertion property
* @param {string} name Name of property
* @param {*} [val] Optional property value to check
* @example
*
* ({ a: 10 }).should.have.property('a');
*/
Assertion.add('property', function(name, val) {
name = util.convertPropertyName(name);
if(arguments.length > 1) {
var p = {};
p[name] = val;
this.have.properties(p);
} else {
this.have.properties(name);
}
this.obj = this.obj[name];
});
/**
* Asserts given object has properties. On this method affect .any modifier, which allow to check not all properties.
*
* @name properties
* @memberOf Assertion
* @category assertion property
* @param {Array|...string|Object} names Names of property
* @example
*
* ({ a: 10 }).should.have.properties('a');
* ({ a: 10, b: 20 }).should.have.properties([ 'a' ]);
* ({ a: 10, b: 20 }).should.have.properties({ b: 20 });
*/
Assertion.add('properties', function(names) {
var values = {};
if(arguments.length > 1) {
names = aSlice.call(arguments);
} else if(!Array.isArray(names)) {
if(typeof names == 'string' || typeof names == 'symbol') {
names = [names];
} else {
values = names;
names = Object.keys(names);
}
}
var obj = Object(this.obj), missingProperties = [];
//just enumerate properties and check if they all present
names.forEach(function(name) {
if(!(name in obj)) missingProperties.push(util.formatProp(name));
});
var props = missingProperties;
if(props.length === 0) {
props = names.map(util.formatProp);
} else if(this.anyOne) {
props = names.filter(function(name) {
return missingProperties.indexOf(util.formatProp(name)) < 0;
}).map(util.formatProp);
}
var operator = (props.length === 1 ?
'to have property ' : 'to have ' + (this.anyOne ? 'any of ' : '') + 'properties ') + props.join(', ');
this.params = {obj: this.obj, operator: operator};
//check that all properties presented
//or if we request one of them that at least one them presented
this.assert(missingProperties.length === 0 || (this.anyOne && missingProperties.length != names.length));
// check if values in object matched expected
var valueCheckNames = Object.keys(values);
if(valueCheckNames.length) {
var wrongValues = [];
props = [];
// now check values, as there we have all properties
valueCheckNames.forEach(function(name) {
var value = values[name];
if(!eql(obj[name], value).result) {
wrongValues.push(util.formatProp(name) + ' of ' + i(value) + ' (got ' + i(obj[name]) + ')');
} else {
props.push(util.formatProp(name) + ' of ' + i(value));
}
});
if((wrongValues.length !== 0 && !this.anyOne) || (this.anyOne && props.length === 0)) {
props = wrongValues;
}
operator = (props.length === 1 ?
'to have property ' : 'to have ' + (this.anyOne ? 'any of ' : '') + 'properties ') + props.join(', ');
this.params = {obj: this.obj, operator: operator};
//if there is no not matched values
//or there is at least one matched
this.assert(wrongValues.length === 0 || (this.anyOne && wrongValues.length != valueCheckNames.length));
}
});
/**
* Asserts given object has property `length` with given value `n`
*
* @name length
* @alias Assertion#lengthOf
* @memberOf Assertion
* @category assertion property
* @param {number} n Expected length
* @param {string} [description] Optional message
* @example
*
* [1, 2].should.have.length(2);
*/
Assertion.add('length', function(n, description) {
this.have.property('length', n, description);
});
Assertion.alias('length', 'lengthOf');
var hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* Asserts given object has own property. **On success it change given object to be value of property**.
*
* @name ownProperty
* @alias Assertion#hasOwnProperty
* @memberOf Assertion
* @category assertion property
* @param {string} name Name of property
* @param {string} [description] Optional message
* @example
*
* ({ a: 10 }).should.have.ownProperty('a');
*/
Assertion.add('ownProperty', function(name, description) {
name = util.convertPropertyName(name);
this.params = {
actual: this.obj,
operator: 'to have own property ' + util.formatProp(name),
message: description
};
this.assert(hasOwnProperty.call(this.obj, name));
this.obj = this.obj[name];
});
Assertion.alias('ownProperty', 'hasOwnProperty');
/**
* Asserts given object is empty. For strings, arrays and arguments it checks .length property, for objects it checks keys.
*
* @name empty
* @memberOf Assertion
* @category assertion property
* @example
*
* ''.should.be.empty;
* [].should.be.empty;
* ({}).should.be.empty;
*/
Assertion.add('empty', function() {
this.params = {operator: 'to be empty'};
if(util.length(this.obj) !== void 0) {
this.obj.should.have.property('length', 0);
} else {
var obj = Object(this.obj); // wrap to reference for booleans and numbers
for(var prop in obj) {
this.obj.should.not.have.ownProperty(prop);
}
}
}, true);
/**
* Asserts given object has exact keys. Compared to `properties`, `keys` does not accept Object as a argument.
*
* @name keys
* @alias Assertion#key
* @memberOf Assertion
* @category assertion property
* @param {Array|...string} [keys] Keys to check
* @example
*
* ({ a: 10 }).should.have.keys('a');
* ({ a: 10, b: 20 }).should.have.keys('a', 'b');
* ({ a: 10, b: 20 }).should.have.keys([ 'a', 'b' ]);
* ({}).should.have.keys();
*/
Assertion.add('keys', function(keys) {
if(arguments.length > 1) keys = aSlice.call(arguments);
else if(arguments.length === 1 && should.type(keys) == should.type.STRING) keys = [keys];
else if(arguments.length === 0) keys = [];
keys = keys.map(String);
var obj = Object(this.obj);
// first check if some keys are missing
var missingKeys = [];
keys.forEach(function(key) {
if(!hasOwnProperty.call(this.obj, key))
missingKeys.push(util.formatProp(key));
}, this);
// second check for extra keys
var extraKeys = [];
Object.keys(obj).forEach(function(key) {
if(keys.indexOf(key) < 0) {
extraKeys.push(util.formatProp(key));
}
});
var verb = keys.length === 0 ? 'to be empty' :
'to have ' + (keys.length === 1 ? 'key ' : 'keys ');
this.params = {operator: verb + keys.map(util.formatProp).join(', ')};
if(missingKeys.length > 0)
this.params.operator += '\n\tmissing keys: ' + missingKeys.join(', ');
if(extraKeys.length > 0)
this.params.operator += '\n\textra keys: ' + extraKeys.join(', ');
this.assert(missingKeys.length === 0 && extraKeys.length === 0);
});
Assertion.alias("keys", "key");
/**
* Asserts given object has nested property in depth by path. **On success it change given object to be value of final property**.
*
* @name propertyByPath
* @memberOf Assertion
* @category assertion property
* @param {Array|...string} properties Properties path to search
* @example
*
* ({ a: {b: 10}}).should.have.propertyByPath('a', 'b').eql(10);
*/
Assertion.add('propertyByPath', function(properties) {
if(arguments.length > 1) properties = aSlice.call(arguments);
else if(arguments.length === 1 && typeof properties == 'string') properties = [properties];
else if(arguments.length === 0) properties = [];
var allProps = properties.map(util.formatProp);
properties = properties.map(String);
var obj = should(Object(this.obj));
var foundProperties = [];
var currentProperty;
while(currentProperty = properties.shift()) {
this.params = {operator: 'to have property by path ' + allProps.join(', ') + ' - failed on ' + util.formatProp(currentProperty)};
obj = obj.have.property(currentProperty);
foundProperties.push(currentProperty);
}
this.params = {obj: this.obj, operator: 'to have property by path ' + allProps.join(', ')};
this.obj = obj.obj;
});
};