diff --git a/component.json b/component.json index 3d9029bfe..162639ef8 100644 --- a/component.json +++ b/component.json @@ -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" diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js index 97a85d124..055f6b697 100644 --- a/lib/chai/core/assertions.js +++ b/lib/chai/core/assertions.js @@ -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 @@ -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) { @@ -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)); } diff --git a/lib/chai/utils/getPathInfo.js b/lib/chai/utils/getPathInfo.js new file mode 100644 index 000000000..538b2a1c9 --- /dev/null +++ b/lib/chai/utils/getPathInfo.js @@ -0,0 +1,109 @@ +/*! + * Chai - getPathInfo utility + * Copyright(c) 2012-2014 Jake Luer + * 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; +} diff --git a/lib/chai/utils/getPathValue.js b/lib/chai/utils/getPathValue.js index 23b31df06..eb94f99e4 100644 --- a/lib/chai/utils/getPathValue.js +++ b/lib/chai/utils/getPathValue.js @@ -5,6 +5,8 @@ * MIT Licensed */ +var getPathInfo = require('./getPathInfo'); + /** * ### .getPathValue(path, object) * @@ -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; +}; diff --git a/lib/chai/utils/hasProperty.js b/lib/chai/utils/hasProperty.js new file mode 100644 index 000000000..6ebea84b5 --- /dev/null +++ b/lib/chai/utils/hasProperty.js @@ -0,0 +1,63 @@ +/*! + * Chai - hasProperty utility + * Copyright(c) 2012-2014 Jake Luer + * 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; +}; diff --git a/lib/chai/utils/index.js b/lib/chai/utils/index.js index 974c7afea..4ed87ed46 100644 --- a/lib/chai/utils/index.js +++ b/lib/chai/utils/index.js @@ -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 */ diff --git a/test/expect.js b/test/expect.js index bd03a50c9..6282027c4 100644 --- a/test/expect.js +++ b/test/expect.js @@ -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'"); @@ -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'); @@ -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"); diff --git a/test/utilities.js b/test/utilities.js index eeedc0c12..f1783c462 100644 --- a/test/utilities.js +++ b/test/utilities.js @@ -82,6 +82,121 @@ describe('utilities', function () { }); }); + describe('getPathInfo', function() { + var gpi, + obj = { + id: '10702S300W', + primes: [2, 3, 5, 7, 11], + dimensions: { + units: 'mm', + lengths: [[1.2, 3.5], [2.2, 1.5], [5, 7]] + } + }; + + beforeEach(function() { + chai.use(function (_chai, utils) { + gpi = utils.getPathInfo; + }); + }); + + it('should handle simple property', function() { + var info = gpi('dimensions.units', obj); + + info.parent.should.equal(obj.dimensions); + info.value.should.equal(obj.dimensions.units); + info.name.should.equal('units'); + info.exists.should.be.true; + }); + + it('should handle non-existent property', function() { + var info = gpi('dimensions.size', obj); + + info.parent.should.equal(obj.dimensions); + expect(info.value).to.be.undefined; + info.name.should.equal('size'); + info.exists.should.be.false; + }); + + it('should handle array index', function() { + var info = gpi('primes[2]', obj); + + info.parent.should.equal(obj.primes); + info.value.should.equal(obj.primes[2]); + info.name.should.equal(2); + info.exists.should.be.true; + }); + + it('should handle dimensional array', function() { + var info = gpi('dimensions.lengths[2][1]', obj); + + info.parent.should.equal(obj.dimensions.lengths[2]); + info.value.should.equal(obj.dimensions.lengths[2][1]); + info.name.should.equal(1); + info.exists.should.be.true; + }); + + it('should handle out of bounds array index', function() { + var info = gpi('dimensions.lengths[3]', obj); + + info.parent.should.equal(obj.dimensions.lengths); + expect(info.value).to.be.undefined; + info.name.should.equal(3); + info.exists.should.be.false; + }); + + it('should handle out of bounds dimensional array index', function() { + var info = gpi('dimensions.lengths[2][5]', obj); + + info.parent.should.equal(obj.dimensions.lengths[2]); + expect(info.value).to.be.undefined; + info.name.should.equal(5); + info.exists.should.be.false; + }); + }); + + describe('hasProperty', function() { + var hp; + beforeEach(function() { + chai.use(function (_chai, utils) { + hp = utils.hasProperty; + }); + }); + + it('should handle array index', function() { + var arr = [1, 2, 'cheeseburger']; + + hp(1, arr).should.be.true; + hp(3, arr).should.be.false; + }); + + it('should handle literal types', function() { + var s = 'string literal'; + hp('length', s).should.be.true; + hp(3, s).should.be.true; + hp(14, s).should.be.false; + + hp('foo', 1).should.be.false; + }); + + it('should handle undefined', function() { + var o = { + foo: 'bar' + }; + + hp('foo', o).should.be.true; + hp('baz', o).should.be.false; + hp(0, o).should.be.false; + }); + + it('should handle undefined', function() { + hp('foo', undefined).should.be.false; + }); + + it('should handle null', function() { + hp('foo', null).should.be.false; + }); + }); + it('addMethod', function () { chai.use(function(_chai, utils) { expect(_chai.Assertion).to.not.respondTo('eqqqual');