Skip to content

Commit

Permalink
Merge pull request #110 from hartzis/master
Browse files Browse the repository at this point in the history
[semver-minor] add support for object property selector
  • Loading branch information
ljharb committed Jan 20, 2016
2 parents 46f4d36 + 443a73e commit 3feb0ca
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 5 deletions.
6 changes: 5 additions & 1 deletion docs/api/ReactWrapper/find.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ const wrapper = mount(<MyComponent />);
expect(wrapper.find('Foo')).to.have.length(1);
```


Object Property Selector:
```jsx
const wrapper = mount(<MyComponent />);
expect(wrapper.find({prop: 'value'})).to.have.length(1);
```

#### Related Methods

Expand Down
5 changes: 5 additions & 0 deletions docs/api/ShallowWrapper/find.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ const wrapper = shallow(<MyComponent />);
expect(wrapper.find('Foo')).to.have.length(1);
```

Object Property Selector:
```jsx
const wrapper = shallow(<MyComponent />);
expect(wrapper.find({prop: 'value'})).to.have.length(1);
```


#### Related Methods
Expand Down
23 changes: 21 additions & 2 deletions docs/api/selector.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ one of the following three categories:

### 1. A Valid CSS Selector

Enzyme supports a subset of valid CSS selectors to find nodes inside a render tree. Support is as
Enzyme supports a subset of valid CSS selectors to find nodes inside a render tree. Support is as
follows:

- class syntax (`.foo`, `.foo-bar`, etc.)
Expand Down Expand Up @@ -92,6 +92,25 @@ MyComponent.displayName = 'MyComponent';
const myComponents = wrapper.find('MyComponent');
```

NOTE: This will *only* work if the selector (and thus the component's `displayName`) is a string
NOTE: This will *only* work if the selector (and thus the component's `displayName`) is a string
starting with a capital letter. Strings starting with lower case letters will assume it is a CSS
selector using the tag syntax.



### 4. Object Property Selector

Enzyme allows you to find components and nodes based on a subset of their properties:


```jsx
const wrapper = mount(
<div>
<span foo={3} bar={false} title="baz" />
</div>
)

wrapper.find({ foo: 3 })
wrapper.find({ bar: false })
wrapper.find({ title: 'baz'})
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"license": "MIT",
"dependencies": {
"cheerio": "^0.19.0",
"is-subset": "^0.1.1",
"object.assign": "^4.0.3",
"sinon": "^1.15.4",
"underscore": "^1.8.3"
Expand Down
18 changes: 17 additions & 1 deletion src/MountedTraversal.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isEmpty } from 'underscore';
import isSubset from 'is-subset';
import {
coercePropValue,
nodeEqual,
Expand Down Expand Up @@ -177,6 +179,12 @@ export function parentsOfInst(inst, root) {
return pathToNode(inst, root).reverse();
}

export function instMatchesObjectProps(inst, props) {
if (!isDOMComponent(inst)) return false;
const node = getNode(inst);
return isSubset(propsOfNode(node), props);
}

export function buildInstPredicate(selector) {
switch (typeof selector) {
case 'function':
Expand Down Expand Up @@ -207,8 +215,16 @@ export function buildInstPredicate(selector) {
}
break;

case 'object':
if (!Array.isArray(selector) && selector !== null && !isEmpty(selector)) {
return node => instMatchesObjectProps(node, selector);
}
throw new TypeError(
'Enzyme::Selector does not support an array, null, or empty object as a selector'
);

default:
throw new TypeError('Expecting a string or Component Constructor');
throw new TypeError(`Enzyme::Selector expects a string, object, or Component Constructor`);
}
}

Expand Down
15 changes: 14 additions & 1 deletion src/ShallowTraversal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from 'react';
import { isEmpty } from 'underscore';
import isSubset from 'is-subset';
import {
coercePropValue,
propsOfNode,
Expand Down Expand Up @@ -95,6 +97,10 @@ export function nodeHasType(node, type) {
return node.type.name === type || node.type.displayName === type;
}

export function nodeMatchesObjectProps(node, props) {
return isSubset(propsOfNode(node), props);
}

export function buildPredicate(selector) {
switch (typeof selector) {
case 'function':
Expand Down Expand Up @@ -127,9 +133,16 @@ export function buildPredicate(selector) {
}
break;

case 'object':
if (!Array.isArray(selector) && selector !== null && !isEmpty(selector)) {
return node => nodeMatchesObjectProps(node, selector);
}
throw new TypeError(
'Enzyme::Selector does not support an array, null, or empty object as a selector'
);

default:
throw new TypeError('Expecting a string or Component Constructor');
throw new TypeError(`Enzyme::Selector expects a string, object, or Component Constructor`);
}
}

Expand Down
52 changes: 52 additions & 0 deletions src/__tests__/ReactWrapper-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,58 @@ describeWithDOM('mount', () => {
expect(() => wrapper.find('.foo .foo')).to.throw(Error);
});

it('should support object property selectors', () => {
const wrapper = mount(
<div>
<input data-test="ref" className="foo" type="text" />
<input data-test="ref" type="text"/>
<button data-test="ref" prop={undefined} />
<span data-test="ref" prop={null} />
<div data-test="ref" prop={123} />
<input data-test="ref" prop={false} />
<a data-test="ref" prop />
</div>
);
expect(wrapper.find({ a: 1 })).to.have.length(0);
expect(wrapper.find({ 'data-test': 'ref' })).to.have.length(7);
expect(wrapper.find({ className: 'foo' })).to.have.length(1);
expect(wrapper.find({ prop: undefined })).to.have.length(1);
expect(wrapper.find({ prop: null })).to.have.length(1);
expect(wrapper.find({ prop: 123 })).to.have.length(1);
expect(wrapper.find({ prop: false })).to.have.length(1);
expect(wrapper.find({ prop: true })).to.have.length(1);
});

it('should support complex and nested object property selectors', () => {
const testFunction = () => {};
const wrapper = mount(
<div>
<span more={[{ id: 1 }]} data-test="ref" prop onChange={testFunction} />
<a more={[{ id: 1 }]} data-test="ref" />
<div more={{ item: { id: 1 } }} data-test="ref" />
<input style={{ height: 20 }} data-test="ref" />
</div>
);
expect(wrapper.find({ 'data-test': 'ref' })).to.have.length(4);
expect(wrapper.find({ more: { a: 1 } })).to.have.length(0);
expect(wrapper.find({ more: [{ id: 1 }] })).to.have.length(2);
expect(wrapper.find({ more: { item: { id: 1 } } })).to.have.length(1);
expect(wrapper.find({ style: { height: 20 } })).to.have.length(1);
expect(wrapper
.find({ more: [{ id: 1 }], 'data-test': 'ref', prop: true, onChange: testFunction })
).to.have.length(1);
});

it('should throw when given empty object, null, or an array', () => {
const wrapper = mount(
<div>
<input className="foo" type="text" />
</div>
);
expect(() => wrapper.find({})).to.throw(Error);
expect(() => wrapper.find([])).to.throw(Error);
expect(() => wrapper.find(null)).to.throw(Error);
});
});

describe('.findWhere(predicate)', () => {
Expand Down
52 changes: 52 additions & 0 deletions src/__tests__/ShallowWrapper-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,58 @@ describe('shallow', () => {
expect(() => wrapper.find('.foo .foo')).to.throw(Error);
});

it('should support object property selectors', () => {
const wrapper = shallow(
<div>
<input data-test="ref" className="foo" type="text" />
<input data-test="ref" type="text"/>
<button data-test="ref" prop={undefined} />
<span data-test="ref" prop={null} />
<div data-test="ref" prop={123} />
<input data-test="ref" prop={false} />
<a data-test="ref" prop />
</div>
);
expect(wrapper.find({ a: 1 })).to.have.length(0);
expect(wrapper.find({ 'data-test': 'ref' })).to.have.length(7);
expect(wrapper.find({ className: 'foo' })).to.have.length(1);
expect(wrapper.find({ prop: undefined })).to.have.length(1);
expect(wrapper.find({ prop: null })).to.have.length(1);
expect(wrapper.find({ prop: 123 })).to.have.length(1);
expect(wrapper.find({ prop: false })).to.have.length(1);
expect(wrapper.find({ prop: true })).to.have.length(1);
});

it('should support complex and nested object property selectors', () => {
const testFunction = () => {};
const wrapper = shallow(
<div>
<span more={[{ id: 1 }]} data-test="ref" prop onChange={testFunction} />
<a more={[{ id: 1 }]} data-test="ref" />
<div more={{ item: { id: 1 } }} data-test="ref" />
<input style={{ height: 20 }} data-test="ref" />
</div>
);
expect(wrapper.find({ 'data-test': 'ref' })).to.have.length(4);
expect(wrapper.find({ more: { a: 1 } })).to.have.length(0);
expect(wrapper.find({ more: [{ id: 1 }] })).to.have.length(2);
expect(wrapper.find({ more: { item: { id: 1 } } })).to.have.length(1);
expect(wrapper.find({ style: { height: 20 } })).to.have.length(1);
expect(wrapper
.find({ more: [{ id: 1 }], 'data-test': 'ref', prop: true, onChange: testFunction })
).to.have.length(1);
});

it('should throw when given empty object, null, or an array', () => {
const wrapper = shallow(
<div>
<input className="foo" type="text" />
</div>
);
expect(() => wrapper.find({})).to.throw(Error);
expect(() => wrapper.find([])).to.throw(Error);
expect(() => wrapper.find(null)).to.throw(Error);
});
});

describe('.findWhere(predicate)', () => {
Expand Down

0 comments on commit 3feb0ca

Please sign in to comment.