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

Refactor ownProperty #810

Merged
merged 2 commits into from
Sep 29, 2016
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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