Skip to content

Commit

Permalink
Merge pull request #810 from meeber/refactor-own-property
Browse files Browse the repository at this point in the history
Refactor `ownProperty`
  • Loading branch information
meeber committed Sep 29, 2016
2 parents 29e7d87 + edfe064 commit 5dd96a0
Show file tree
Hide file tree
Showing 6 changed files with 675 additions and 411 deletions.
176 changes: 86 additions & 90 deletions lib/chai/core/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ module.exports = function (chai, _) {
* var deepCss = { '.link': { '[target]': 42 }};
* expect(deepCss).to.have.nested.property('\\.link.\\[target\\]', 42);
*
* The `nested` flag cannot be combined with the `own` flag.
*
* @name nested
* @namespace BDD
* @api public
Expand All @@ -113,6 +115,25 @@ module.exports = function (chai, _) {
flag(this, 'nested', true);
});

/**
* ### .own
*
* Sets the `own` flag, later used by the `property` assertion.
*
* expect({a: 1}).to.have.own.property('a');
* expect({a: 1}).to.have.own.property('a', 1);
*
* The `own` flag cannot be combined with the `nested` flag.
*
* @name own
* @namespace BDD
* @api public
*/

Assertion.addProperty('own', function () {
flag(this, 'own', true);
});

/**
* ### .ordered
*
Expand Down Expand Up @@ -954,12 +975,14 @@ module.exports = function (chai, _) {
/**
* ### .property(name, [value])
*
* Asserts that the target has a property `name`, optionally asserting that
* the value of that property is strictly equal to `value`.
* Asserts that the target has a direct or inherited property `name`,
* optionally asserting that the value of that property is strictly equal to
* `value`.
*
* var obj = { foo: 'bar' };
* expect(obj).to.have.property('foo');
* expect(obj).to.have.property('foo', 'bar');
* expect(obj).to.have.property('toString');
* expect(obj).to.not.have.property('baz');
* expect(obj).to.not.have.property('foo', 'baz');
* expect(obj).to.not.have.property('baz', 'bar');
Expand All @@ -971,6 +994,14 @@ module.exports = function (chai, _) {
* expect(obj).to.have.deep.property('foo', { bar: 'baz' });
* expect(obj).to.not.have.deep.property('foo', { bar: 'quux' });
*
* If the `own` flag is set, the property must exist directly on the object.
* Inherited properties aren't checked.
*
* var obj = { foo: 'bar' };
* expect(obj).to.have.own.property('foo');
* expect(obj).to.have.own.property('foo', 'bar');
* expect(obj).to.not.have.own.property('toString');
*
* If the `nested` flag is set, you can use dot- and bracket-notation for
* nested references into objects and arrays.
*
Expand All @@ -982,11 +1013,6 @@ module.exports = function (chai, _) {
* expect(deepObj).to.have.nested.property('teas[1]', 'matcha');
* expect(deepObj).to.have.nested.property('teas[2].tea', 'konacha');
*
* The `deep` and `nested` flags can be combined.
*
* expect({ foo: { bar: { baz: 'quux' } } })
* .to.have.deep.nested.property('foo.bar', { baz: 'quux' });
*
* You can also use an array as the starting point of a `nested.property`
* assertion, or traverse nested arrays.
*
Expand All @@ -999,20 +1025,6 @@ module.exports = function (chai, _) {
* expect(arr).to.have.nested.property('[0][1]', 'matcha');
* expect(arr).to.have.nested.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.nested.property('[2]')
* .that.deep.equals({ tea: 'konacha' });
*
* Note that dots and brackets in `name` must be backslash-escaped when
* the `nested` flag is set, while they must NOT be escaped when the `nested`
* flag is not set.
Expand All @@ -1025,7 +1037,32 @@ module.exports = function (chai, _) {
* var deepCss = { '.link': { '[target]': 42 }};
* expect(deepCss).to.have.nested.property('\\.link.\\[target\\]', 42);
*
* The `deep` and `own` flags can be combined, and the `deep` and `nested`
* flags can be combined, but the `own` and `nested` flags cannot be combined.
*
* expect({ foo: { bar: 'baz' } })
* .to.have.deep.own.property('foo', { bar: 'baz' });
* expect({ foo: { bar: { baz: 'quux' } } })
* .to.have.deep.nested.property('foo.bar', { baz: 'quux' });
*
* Note that `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.own.property('green')
* .that.is.an('object')
* .that.deep.equals({ tea: 'matcha' });
* expect(deepObj).to.have.property('teas')
* .that.is.an('array')
* .with.nested.property('[2]')
* .that.deep.equals({ tea: 'konacha' });
*
* @name property
* @alias own.property
* @alias ownProperty
* @alias haveOwnProperty
* @alias deep.property
* @alias nested.property
* @param {String} name
Expand All @@ -1036,23 +1073,32 @@ module.exports = function (chai, _) {
* @api public
*/

Assertion.addMethod('property', function (name, val, msg) {
function assertProperty (name, val, msg) {
if (msg) flag(this, 'message', msg);

var isNested = !!flag(this, 'nested')
, isDeep = !!flag(this, 'deep')
, descriptor = (isDeep ? 'deep ' : '')
+ (isNested ? 'nested ' : '')
+ 'property '
var isNested = flag(this, 'nested')
, isOwn = flag(this, 'own');

if (isNested && isOwn) {
throw new Error('The "nested" and "own" flags cannot be combined.');
}

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

var descriptor = '';
if (isDeep) descriptor += 'deep ';
if (isOwn) descriptor += 'own ';
if (isNested) descriptor += 'nested ';
descriptor += 'property ';

var hasProperty;
if (isOwn) hasProperty = Object.prototype.hasOwnProperty.call(obj, name);
else if (isNested) hasProperty = pathInfo.exists;
else hasProperty = _.hasProperty(name, obj);

// When performing a negated assertion for both name and val, merely having
// a property with the given name isn't enough to cause the assertion to
Expand All @@ -1062,78 +1108,28 @@ module.exports = function (chai, _) {
if (!negate || arguments.length === 1) {
this.assert(
hasProperty
, 'expected #{this} to have a ' + descriptor + _.inspect(name)
, 'expected #{this} to have ' + descriptor + _.inspect(name)
, 'expected #{this} to not have ' + descriptor + _.inspect(name));
}

if (arguments.length > 1) {
this.assert(
hasProperty && (isDeep ? _.eql(val, value) : 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}'
, 'expected #{this} to have ' + descriptor + _.inspect(name) + ' of #{exp}, but got #{act}'
, 'expected #{this} to not have ' + descriptor + _.inspect(name) + ' of #{act}'
, val
, value
);
}

flag(this, 'object', value);
});

}

/**
* ### .ownProperty(name, [value])
*
* Asserts that the target has an own property `name` and, optionally, if it has
* (or not, if using `.not`) the desired `value`.
*
* expect('test').to.have.ownProperty('length');
* expect('test').to.haveOwnProperty('length');
* expect('test').to.not.have.ownProperty('foo');
* expect('test').to.not.haveOwnProperty('foo');
* expect({ length: 12 }).to.have.ownProperty('length', 12);
* expect({ length: 1337 }).to.not.have.ownProperty('length', 20);
* expect({ length: 12 }).to.haveOwnProperty('length', 12);
* expect({ length: 1337 }).to.not.haveOwnProperty('length', 20);
*
* @name ownProperty
* @alias haveOwnProperty
* @param {String} name
* @param {Mixed} value (optional)
* @param {String} message _optional_
* @namespace BDD
* @api public
*/
Assertion.addMethod('property', assertProperty);

function assertOwnProperty (name, value, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
var negate = flag(this, 'negate');
var objHasProperty = Object.prototype.hasOwnProperty.call(obj, name)
var actualValue = obj[name];

if (negate && value !== undefined) {
if (actualValue === undefined) {
throw new Error(_.inspect(obj) + ' does not have own property ' + _.inspect(name));
}
} else {
this.assert(
objHasProperty
, 'expected #{this} to have own property ' + _.inspect(name)
, 'expected #{this} to not have own property ' + _.inspect(name)
);
}

if (value !== undefined) {
this.assert(
actualValue === value
, 'expected #{this} to have own property ' + _.inspect(name) + ' of #{exp}, but got #{act}'
, 'expected #{this} to not have own property ' + _.inspect(name) + ' of #{act}'
, value
, actualValue
);
}

flag(this, 'object', actualValue);
flag(this, 'own', true);
assertProperty.apply(this, arguments);
}

Assertion.addMethod('ownProperty', assertOwnProperty);
Expand Down

0 comments on commit 5dd96a0

Please sign in to comment.