Skip to content

Commit

Permalink
Merge pull request #104 from astorije/astorije/not-property
Browse files Browse the repository at this point in the history
Adapt `.property` (more specifically `.not.property`) to the Chai v4 changes
  • Loading branch information
astorije authored Nov 25, 2017
2 parents 6f6c3d9 + 37e8506 commit 78fa174
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 61 deletions.
87 changes: 65 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,30 +167,40 @@ expect(new Map({ foo: 1, bar: 2 })).to.have.all.keys('foo', 'bar');
expect(new Map({ foo: 1, bar: 2 })).to.contain.key('foo');
```

### .property(name, [value])
### .property(path[, val])

- **@param** *{ String | Array | Iterable }* name
- **@param** *{ Mixed }* value (optional)
- **@param** *{ String | Array | Iterable }* path
- **@param** *{ Mixed }* val (optional)

Asserts that the target has a property `name`, optionally asserting that
the value of that property is equal to `value`. `value` can be an
Immutable object.
If the `nested` flag is set, you can use dot- and bracket-notation for nested
references into objects and arrays.
Asserts that the target has a property with the given `path`.

<!-- fulky:define maps -->
<!-- fulky:define map -->
```js
// Simple referencing
var map = new Map({ foo: 'bar' });
const map = new Map({ foo: 'bar' });
expect(map).to.have.property('foo');
```

When `val` is provided, `.property` also asserts that the property's value
is equal to the given `val`. `val` can be an Immutable object.

<!-- fulky:use map -->
```js
expect(map).to.have.property('foo', 'bar');
```

// Deep referencing
var nestedMap = new Map({
Add `.nested` earlier in the chain to enable dot- and bracket-notation when
referencing nested properties.

<!-- fulky:define nestedMap -->
```js
// Nested referencing
const nestedMap = new Map({
green: new Map({ tea: 'matcha' }),
teas: new List(['chai', 'matcha', new Map({ tea: 'konacha' })])
});

expect(nestedMap).to.have.nested.property('green.tea');
expect(nestedMap).to.have.nested.property('green.tea', 'matcha');
expect(nestedMap).to.have.nested.property(['green', 'tea'], 'matcha');
expect(nestedMap).to.have.nested.property(new List(['green', 'tea']), 'matcha');
Expand All @@ -206,7 +216,7 @@ You can also use a `List` as the starting point of a `nested.property`
assertion, or traverse nested `List`s.

```js
var list = new List([
const list = new List([
new List(['chai', 'matcha', 'konacha']),
new List([
new Map({ tea: 'chai' }),
Expand All @@ -223,11 +233,44 @@ expect(list).to.have.nested.property([1, 2, 'tea'], 'konacha');
expect(list).to.have.nested.property(new List([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.
Add `.not` earlier in the chain to negate `.property`.

<!-- fulky:use map -->
```js
expect(map).to.not.have.property('baz');
```

However, it's dangerous to negate `.property` when providing `val`. The
problem is that it creates uncertain expectations by asserting that the
target either doesn't have a property with the given `path`, or that it
does have a property with the given `path` but its value isn't equal to
the given `val`. It's often best to identify the exact output that's
expected, and then write an assertion that only accepts that exact output.

When the target isn't expected to have a property with the given `name`,
it's often best to assert exactly that.

<!-- fulky:use map -->
```js
expect(map).to.not.have.property('baz'); // Recommended
expect(map).to.not.have.property('baz', 42); // Not recommended
```

When the target is expected to have a property with the given `path`,
it's often best to assert that the property has its expected value, rather
than asserting that it doesn't have one of many unexpected values.

<!-- fulky:use map -->
```js
expect(map).to.have.property('foo', 'bar'); // Recommended
expect(map).to.not.have.property('baz', 42); // Not recommended
```

`.property` changes the target of any assertions that follow in the chain
to be the value of the property from the original target object.

<!-- fulky:use maps -->
<!-- fulky:use map -->
<!-- fulky:use nestedMap -->
```js
expect(map).to.have.property('foo')
.that.is.a('string');
Expand All @@ -241,16 +284,16 @@ expect(nestedMap).to.have.property('teas')
```

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.
the `nested` flag is set, while they must NOT be escaped when the
`nested` flag is not set.

```js
// Simple referencing
var css = new Map({ '.link[target]': 42 });
const css = new Map({ '.link[target]': 42 });
expect(css).to.have.property('.link[target]', 42);

// Deep referencing
var nestedCss = new Map({ '.link': new Map({ '[target]': 42 }) });
// Nested referencing
const nestedCss = new Map({ '.link': new Map({ '[target]': 42 }) });
expect(nestedCss).to.have.nested.property('\\.link.\\[target\\]', 42);
```

Expand Down
103 changes: 69 additions & 34 deletions chai-immutable.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,26 +332,34 @@
}

/**
* ### .property(name, [value])
* ### .property(path[, val])
*
* Asserts that the target has a property `name`, optionally asserting that
* the value of that property is equal to `value`. `value` can be an
* Immutable object.
* If the `nested` flag is set, you can use dot- and bracket-notation for
* nested references into objects and arrays.
* Asserts that the target has a property with the given `path`.
*
* ```js
* // Simple referencing
* const map = new Map({ foo: 'bar' });
* expect(map).to.have.property('foo');
* ```
*
* When `val` is provided, `.property` also asserts that the property's value
* is equal to the given `val`. `val` can be an Immutable object.
*
* ```js
* expect(map).to.have.property('foo', 'bar');
* ```
*
* Add `.nested` earlier in the chain to enable dot- and bracket-notation when
* referencing nested properties.
*
* // Deep referencing
* ```js
* // Nested referencing
* const nestedMap = new Map({
* green: new Map({ tea: 'matcha' }),
* teas: new List(['chai', 'matcha', new Map({ tea: 'konacha' })])
* green: new Map({ tea: 'matcha' }),
* teas: new List(['chai', 'matcha', new Map({ tea: 'konacha' })])
* });
*
* expect(nestedMap).to.have.nested.property('green.tea');
* expect(nestedMap).to.have.nested.property('green.tea', 'matcha');
* expect(nestedMap).to.have.nested.property(['green', 'tea'], 'matcha');
* expect(nestedMap).to.have.nested.property(new List(['green', 'tea']), 'matcha');
Expand Down Expand Up @@ -384,9 +392,38 @@
* expect(list).to.have.nested.property(new List([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.
* Add `.not` earlier in the chain to negate `.property`.
*
* ```js
* expect(map).to.not.have.property('baz');
* ```
*
* However, it's dangerous to negate `.property` when providing `val`. The
* problem is that it creates uncertain expectations by asserting that the
* target either doesn't have a property with the given `path`, or that it
* does have a property with the given `path` but its value isn't equal to
* the given `val`. It's often best to identify the exact output that's
* expected, and then write an assertion that only accepts that exact output.
*
* When the target isn't expected to have a property with the given `name`,
* it's often best to assert exactly that.
*
* ```js
* expect(map).to.not.have.property('baz'); // Recommended
* expect(map).to.not.have.property('baz', 42); // Not recommended
* ```
*
* When the target is expected to have a property with the given `path`,
* it's often best to assert that the property has its expected value, rather
* than asserting that it doesn't have one of many unexpected values.
*
* ```js
* expect(map).to.have.property('foo', 'bar'); // Recommended
* expect(map).to.not.have.property('baz', 42); // Not recommended
* ```
*
* `.property` changes the target of any assertions that follow in the chain
* to be the value of the property from the original target object.
*
* ```js
* expect(map).to.have.property('foo')
Expand All @@ -409,14 +446,15 @@
* const css = new Map({ '.link[target]': 42 });
* expect(css).to.have.property('.link[target]', 42);
*
* // Deep referencing
* // Nested referencing
* const nestedCss = new Map({ '.link': new Map({ '[target]': 42 }) });
* expect(nestedCss).to.have.nested.property('\\.link.\\[target\\]', 42);
* ```
*
* @name property
* @param {String|Array|Iterable} name
* @param {Mixed} value (optional)
* @param {String|Array|Iterable} path
* @param {Mixed} val (optional)
* @returns value of property for chaining
* @namespace BDD
* @api public
*/
Expand All @@ -425,40 +463,37 @@
const obj = this._obj;

if (Immutable.Iterable.isIterable(obj)) {
const isNested = Boolean(utils.flag(this, 'nested'));
const negate = Boolean(utils.flag(this, 'negate'));
const isNested = utils.flag(this, 'nested');
const negate = utils.flag(this, 'negate');

let descriptor;
let hasProperty;
let value;

if (isNested) {
descriptor = 'nested property ';
descriptor = 'nested ';
if (typeof path === 'string') {
path = parsePath(path);
}
value = obj.getIn(path);
hasProperty = obj.hasIn(path);
} else {
descriptor = 'property ';
value = obj.get(path);
hasProperty = obj.has(path);
}

// In the negate case, we only throw if property is missing so we can
// check the value later.
if (negate && arguments.length > 1) {
if (!hasProperty) {
throw new chai.AssertionError(
`expected ${utils.inspect(obj)} to have a ${descriptor}` +
`${utils.inspect(path)}`
);
}
} else {
// 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 fail. It must both have a property with the given name,
// and the value of that property must equal the given val. Therefore,
// skip this assertion in favor of the next.
if (!negate || arguments.length === 1) {
this.assert(
hasProperty,
`expected #{this} to have a ${descriptor}${utils.inspect(path)}`,
`expected #{this} not to have ${descriptor}${utils.inspect(path)}`
`expected #{this} to have ${descriptor}property ` +
`${utils.inspect(path)}`,
`expected #{this} to not have ${descriptor}property ` +
`${utils.inspect(path)}`
);
}

Expand All @@ -471,10 +506,10 @@
}

this.assert(
isEqual,
`expected #{this} to have a ${descriptor}` +
hasProperty && isEqual,
`expected #{this} to have ${descriptor}property ` +
`${utils.inspect(path)} of #{exp}, but got #{act}`,
`expected #{this} not to have a ${descriptor}` +
`expected #{this} to not have ${descriptor}property ` +
`${utils.inspect(path)} of #{act}`,
val,
value
Expand Down
25 changes: 20 additions & 5 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,11 @@ describe('chai-immutable', function () { // eslint-disable-line prefer-arrow-cal
expect({ x: 1 }).to.have.property('x', 1);
});

it('should not affect the original assertion using `not`', function () { // eslint-disable-line prefer-arrow-callback
expect({ x: 1 }).not.to.have.property('z');
expect({ x: 1 }).not.to.have.property('z', 42);
});

it('should fail given an inexisting property', function () { // eslint-disable-line prefer-arrow-callback
const obj = Immutable.fromJS({ x: 1 });
fail(() => expect(obj).to.have.property('z'));
Expand All @@ -407,6 +412,11 @@ describe('chai-immutable', function () { // eslint-disable-line prefer-arrow-cal
expect(obj).not.to.have.property('z');
});

it('should pass using `not` given an inexisting property and value', function () { // eslint-disable-line prefer-arrow-callback
const obj = Immutable.fromJS({ x: 1 });
expect(obj).not.to.have.property('z', 42);
});

it('should pass given an existing property', function () { // eslint-disable-line prefer-arrow-callback
const obj = Immutable.fromJS({ x: 1 });
expect(obj).to.have.property('x');
Expand Down Expand Up @@ -443,6 +453,11 @@ describe('chai-immutable', function () { // eslint-disable-line prefer-arrow-cal
expect({ x: 1, y: { x: 2, y: 3 } }).to.have.nested.property('y.x', 2);
});

it('should not affect the original assertion using `not`', function () { // eslint-disable-line prefer-arrow-callback
expect({ x: 1, y: { x: 2 } }).not.to.have.nested.property('z.z');
expect({ x: 1, y: { x: 2 } }).not.to.have.nested.property('z.z', 42);
});

it('should fail given an inexisting property', function () { // eslint-disable-line prefer-arrow-callback
const obj = Immutable.fromJS({ x: 1, y: { x: 2, y: 3 } });
fail(() => expect(obj).to.have.nested.property(['y', 'z']));
Expand All @@ -453,6 +468,11 @@ describe('chai-immutable', function () { // eslint-disable-line prefer-arrow-cal
expect(obj).not.to.have.nested.property(['y', 'z']);
});

it('should pass using `not` given an inexisting property and value', function () { // eslint-disable-line prefer-arrow-callback
const obj = Immutable.fromJS({ x: 1 });
expect(obj).not.to.have.nested.property(['y', 'x'], 'different');
});

it('should pass given an existing property', function () { // eslint-disable-line prefer-arrow-callback
const obj = Immutable.fromJS({ x: 1, y: { x: 2, y: 3 } });
expect(obj).to.have.nested.property(['y', 'x']);
Expand All @@ -478,11 +498,6 @@ describe('chai-immutable', function () { // eslint-disable-line prefer-arrow-cal
expect(obj).to.have.nested.property(['y', 'x'], 2);
});

it('should fail using `not` given an inexisting property', function () { // eslint-disable-line prefer-arrow-callback
const obj = Immutable.fromJS({ x: 1 });
fail(() => expect(obj).not.to.have.nested.property(['y', 'x'], 'different'));
});

it('should fail using `not` given a property with good value', function () { // eslint-disable-line prefer-arrow-callback
const obj = Immutable.fromJS({ x: 1, y: { x: 2 } });
fail(() => expect(obj).not.to.have.nested.property(['y', 'x'], 2));
Expand Down

0 comments on commit 78fa174

Please sign in to comment.