Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix properties with undefined value pass property assertion #308

Merged
merged 7 commits into from
Dec 2, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions component.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
, "lib/chai/utils/getMessage.js"
, "lib/chai/utils/getName.js"
, "lib/chai/utils/getPathValue.js"
, "lib/chai/utils/getPathInfo.js"
, "lib/chai/utils/hasProperty.js"
, "lib/chai/utils/getProperties.js"
, "lib/chai/utils/index.js"
, "lib/chai/utils/inspect.js"
Expand Down
19 changes: 12 additions & 7 deletions lib/chai/core/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,11 @@ module.exports = function (chai, _) {
for (var k in val) new Assertion(obj).property(k, val[k]);
return;
}
var subset = {}
for (var k in val) subset[k] = obj[k]
var subset = {};
for (var k in val) subset[k] = obj[k];
expected = _.eql(subset, val);
} else {
expected = obj && ~obj.indexOf(val)
expected = obj && ~obj.indexOf(val);
}
this.assert(
expected
Expand Down Expand Up @@ -765,11 +765,16 @@ module.exports = function (chai, _) {
Assertion.addMethod('property', function (name, val, msg) {
if (msg) flag(this, 'message', msg);

var descriptor = flag(this, 'deep') ? 'deep property ' : 'property '
var isDeep = !!flag(this, 'deep')
, descriptor = isDeep ? 'deep property ' : 'property '
, negate = flag(this, 'negate')
, obj = flag(this, 'object')
, value = flag(this, 'deep')
? _.getPathValue(name, obj)
, pathInfo = isDeep ? _.getPathInfo(name, obj) : null
, hasProperty = isDeep
? pathInfo.exists
: _.hasProperty(name, obj)
, value = isDeep
? pathInfo.value
: obj[name];

if (negate && undefined !== val) {
Expand All @@ -779,7 +784,7 @@ module.exports = function (chai, _) {
}
} else {
this.assert(
undefined !== value
hasProperty
, 'expected #{this} to have a ' + descriptor + _.inspect(name)
, 'expected #{this} to not have ' + descriptor + _.inspect(name));
}
Expand Down
109 changes: 109 additions & 0 deletions lib/chai/utils/getPathInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*!
* Chai - getPathInfo utility
* Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
* MIT Licensed
*/

var hasProperty = require('./hasProperty');

/**
* ### .getPathInfo(path, object)
*
* This allows the retrieval of property info in an
* object given a string path.
*
* The path info consists of an object with the
* following properties:
*
* * parent - The parent object of the property referenced by `path`
* * name - The name of the final property, a number if it was an array indexer
* * value - The value of the property, if it exists, otherwise `undefined`
* * exists - Whether the property exists or not
*
* @param {String} path
* @param {Object} object
* @returns {Object} info
* @name getPathInfo
* @api public
*/

module.exports = function getPathInfo(path, obj) {
var parsed = parsePath(path),
last = parsed[parsed.length - 1];

var info = {
parent: _getPathValue(parsed, obj, parsed.length - 1),
name: last.p || last.i,
value: _getPathValue(parsed, obj),
};
info.exists = hasProperty(info.name, info.parent);

return info;
};


/*!
* ## parsePath(path)
*
* Helper function used to parse string object
* paths. Use in conjunction with `_getPathValue`.
*
* var parsed = parsePath('myobject.property.subprop');
*
* ### Paths:
*
* * Can be as near infinitely deep and nested
* * Arrays are also valid using the formal `myobject.document[3].property`.
*
* @param {String} path
* @returns {Object} parsed
* @api private
*/

function parsePath (path) {
var str = path.replace(/\[/g, '.[')
, parts = str.match(/(\\\.|[^.]+?)+/g);
return parts.map(function (value) {
var re = /\[(\d+)\]$/
, mArr = re.exec(value);
if (mArr) return { i: parseFloat(mArr[1]) };
else return { p: value };
});
}


/*!
* ## _getPathValue(parsed, obj)
*
* Helper companion function for `.parsePath` that returns
* the value located at the parsed address.
*
* var value = getPathValue(parsed, obj);
*
* @param {Object} parsed definition from `parsePath`.
* @param {Object} object to search against
* @param {Number} object to search against
* @returns {Object|Undefined} value
* @api private
*/

function _getPathValue (parsed, obj, index) {
var tmp = obj
, res;

index = (index === undefined ? parsed.length : index);

for (var i = 0, l = index; i < l; i++) {
var part = parsed[i];
if (tmp) {
if ('undefined' !== typeof part.p)
tmp = tmp[part.p];
else if ('undefined' !== typeof part.i)
tmp = tmp[part.i];
if (i == (l - 1)) res = tmp;
} else {
res = undefined;
}
}
return res;
}
72 changes: 6 additions & 66 deletions lib/chai/utils/getPathValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* MIT Licensed
*/

var getPathInfo = require('./getPathInfo');

/**
* ### .getPathValue(path, object)
*
Expand Down Expand Up @@ -34,69 +36,7 @@
* @name getPathValue
* @api public
*/

var getPathValue = module.exports = function (path, obj) {
var parsed = parsePath(path);
return _getPathValue(parsed, obj);
};

/*!
* ## parsePath(path)
*
* Helper function used to parse string object
* paths. Use in conjunction with `_getPathValue`.
*
* var parsed = parsePath('myobject.property.subprop');
*
* ### Paths:
*
* * Can be as near infinitely deep and nested
* * Arrays are also valid using the formal `myobject.document[3].property`.
*
* @param {String} path
* @returns {Object} parsed
* @api private
*/

function parsePath (path) {
var str = path.replace(/\[/g, '.[')
, parts = str.match(/(\\\.|[^.]+?)+/g);
return parts.map(function (value) {
var re = /\[(\d+)\]$/
, mArr = re.exec(value)
if (mArr) return { i: parseFloat(mArr[1]) };
else return { p: value };
});
};

/*!
* ## _getPathValue(parsed, obj)
*
* Helper companion function for `.parsePath` that returns
* the value located at the parsed address.
*
* var value = getPathValue(parsed, obj);
*
* @param {Object} parsed definition from `parsePath`.
* @param {Object} object to search against
* @returns {Object|Undefined} value
* @api private
*/

function _getPathValue (parsed, obj) {
var tmp = obj
, res;
for (var i = 0, l = parsed.length; i < l; i++) {
var part = parsed[i];
if (tmp) {
if ('undefined' !== typeof part.p)
tmp = tmp[part.p];
else if ('undefined' !== typeof part.i)
tmp = tmp[part.i];
if (i == (l - 1)) res = tmp;
} else {
res = undefined;
}
}
return res;
};
module.exports = function(path, obj) {
var info = getPathInfo(path, obj);
return info.value;
};
63 changes: 63 additions & 0 deletions lib/chai/utils/hasProperty.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*!
* Chai - hasProperty utility
* Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
* MIT Licensed
*/

var type = require('./type');

/**
* ### .hasProperty(object, name)
*
* This allows checking whether an object has
* named property or numeric array index.
*
* Basically does the same thing as the `in`
* operator but works properly with natives
* and null/undefined values.
*
* var obj = {
* arr: ['a', 'b', 'c']
* , str: 'Hello'
* }
*
* The following would be the results.
*
* hasProperty('str', obj); // true
* hasProperty('constructor', obj); // true
* hasProperty('bar', obj); // false
*
* hasProperty('length', obj.str); // true
* hasProperty(1, obj.str); // true
* hasProperty(5, obj.str); // false
*
* hasProperty('length', obj.arr); // true
* hasProperty(2, obj.arr); // true
* hasProperty(3, obj.arr); // false
*
* @param {Objuect} object
* @param {String|Number} name
* @returns {Boolean} whether it exists
* @name getPathInfo
* @api public
*/

var literals = {
'number': Number
, 'string': String
};

module.exports = function hasProperty(name, obj) {
var ot = type(obj);

// Bad Object, obviously no props at all
if(ot === 'null' || ot === 'undefined')
return false;

// The `in` operator does not work with certain literals
// box these before the check
if(literals[ot] && typeof obj !== 'object')
obj = new literals[ot](obj);

return name in obj;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, this looks much better!

12 changes: 12 additions & 0 deletions lib/chai/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ exports.eql = require('deep-eql');

exports.getPathValue = require('./getPathValue');

/*!
* Deep path info
*/

exports.getPathInfo = require('./getPathInfo');

/*!
* Check if a property exists
*/

exports.hasProperty = require('./hasProperty');

/*!
* Function name
*/
Expand Down
46 changes: 46 additions & 0 deletions test/expect.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,14 @@ describe('expect', function () {
expect({ foo: { bar: 'baz' } })
.to.not.have.property('foo.bar');

// Properties with the value 'undefined' are still properties
var obj = { foo: undefined };
Object.defineProperty(obj, 'bar', {
get: function() { }
});
expect(obj).to.have.property('foo');
expect(obj).to.have.property('bar');

err(function(){
expect('asd').to.have.property('foo');
}, "expected 'asd' to have a property 'foo'");
Expand All @@ -412,6 +420,9 @@ describe('expect', function () {
expect({ foo: { bar: 'baz' } })
.to.have.deep.property('foo.bar');

expect({ 'foo': [1, 2, 3] })
.to.have.deep.property('foo[1]');

err(function(){
expect({ 'foo.bar': 'baz' })
.to.have.deep.property('foo.bar');
Expand All @@ -422,6 +433,41 @@ describe('expect', function () {
expect('test').to.have.property('length', 4);
expect('asd').to.have.property('constructor', String);

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');
err(function(){
expect(deepObj).to.have.deep.property('teas[3]');
}, "expected { Object (green, teas) } to have a deep property 'teas[3]'");
err(function(){
expect(deepObj).to.have.deep.property('teas[3]', 'bar');
}, "expected { Object (green, teas) } to have a deep property 'teas[3]'");
err(function(){
expect(deepObj).to.have.deep.property('teas[3].tea', 'bar');
}, "expected { Object (green, teas) } to have a deep property 'teas[3].tea'");

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');
err(function(){
expect(arr).to.have.deep.property('[2][1]');
}, "expected [ Array(2) ] to have a deep property '[2][1]'");
err(function(){
expect(arr).to.have.deep.property('[2][1]', 'none');
}, "expected [ Array(2) ] to have a deep property '[2][1]'");
err(function(){
expect(arr).to.have.deep.property('[0][3]', 'none');
}, "expected [ Array(2) ] to have a deep property '[0][3]'");

err(function(){
expect('asd').to.have.property('length', 4, 'blah');
}, "blah: expected 'asd' to have a property 'length' of 4, but got 3");
Expand Down
Loading