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

Adapt .property (more specifically .not.property) to the Chai v4 changes #104

Merged
merged 1 commit into from
Nov 25, 2017
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
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