Skip to content

Commit

Permalink
Merge pull request #154 from airbnb/lmr--contains-array-of-nodes
Browse files Browse the repository at this point in the history
Allow .contains() to accept array of nodes.
  • Loading branch information
lelandrichardson committed Feb 2, 2016
2 parents f648aee + 150f545 commit b4cc71d
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 23 deletions.
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* [findWhere(predicate)](/docs/api/ShallowWrapper/findWhere.md)
* [filter(selector)](/docs/api/ShallowWrapper/filter.md)
* [filterWhere(predicate)](/docs/api/ShallowWrapper/filterWhere.md)
* [contains(node)](/docs/api/ShallowWrapper/contains.md)
* [contains(nodeOrNodes)](/docs/api/ShallowWrapper/contains.md)
* [equals(node)](/docs/api/ShallowWrapper/equals.md)
* [hasClass(className)](/docs/api/ShallowWrapper/hasClass.md)
* [is(selector)](/docs/api/ShallowWrapper/is.md)
Expand Down Expand Up @@ -53,7 +53,7 @@
* [findWhere(predicate)](/docs/api/ReactWrapper/findWhere.md)
* [filter(selector)](/docs/api/ReactWrapper/filter.md)
* [filterWhere(predicate)](/docs/api/ReactWrapper/filterWhere.md)
* [contains(node)](/docs/api/ReactWrapper/contains.md)
* [contains(nodeOrNodes)](/docs/api/ReactWrapper/contains.md)
* [hasClass(className)](/docs/api/ReactWrapper/hasClass.md)
* [is(selector)](/docs/api/ReactWrapper/is.md)
* [not(selector)](/docs/api/ReactWrapper/not.md)
Expand Down
23 changes: 21 additions & 2 deletions docs/api/ReactWrapper/contains.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# `.contains(node)`
# `.contains(nodeOrNodes) => Boolean`

Returns whether or not the current wrapper has a node anywhere in it's render tree that looks like
the one passed in.


#### Arguments

1. `node` (`ReactElement`): The node whose presence you are detecting in the current instance's
1. `nodeOrNodes` (`ReactElement|Array<ReactElement>`): The node or array of nodes whose presence you are detecting in the current instance's
render tree.


Expand All @@ -26,6 +26,25 @@ const wrapper = mount(<MyComponent />);
expect(wrapper.contains(<div className="foo bar" />)).to.equal(true);
```

```jsx
const wrapper = mount(
<div>
<span>Hello</span>
<div>Goodbye</div>
<span>Again</span>
</div>
);
const passes = [
<span>Hello</span>,
<div>Goodbye</div>,
];

expect(wrapper.contains([
<span>Hello</span>,
<div>Goodbye</div>,
])).to.equal(true);
```


#### Common Gotchas

Expand Down
23 changes: 21 additions & 2 deletions docs/api/ShallowWrapper/contains.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# `.contains(node) => Boolean`
# `.contains(nodeOrNodes) => Boolean`

Returns whether or not the current wrapper has a node anywhere in it's render tree that looks like
the one passed in.


#### Arguments

1. `node` (`ReactElement`): The node whose presence you are detecting in the current instance's
1. `nodeOrNodes` (`ReactElement|Array<ReactElement>`): The node or array of nodes whose presence you are detecting in the current instance's
render tree.


Expand All @@ -26,6 +26,25 @@ const wrapper = shallow(<MyComponent />);
expect(wrapper.contains(<div className="foo bar" />)).to.equal(true);
```

```jsx
const wrapper = shallow(
<div>
<span>Hello</span>
<div>Goodbye</div>
<span>Again</span>
</div>
);
const passes = [
<span>Hello</span>,
<div>Goodbye</div>,
];

expect(wrapper.contains([
<span>Hello</span>,
<div>Goodbye</div>,
])).to.equal(true);
```


#### Common Gotchas

Expand Down
4 changes: 2 additions & 2 deletions docs/api/mount.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ Remove nodes in the current wrapper that do not match the provided selector.
#### [`.filterWhere(predicate) => ReactWrapper`](ReactWrapper/filterWhere.md)
Remove nodes in the current wrapper that do not return true for the provided predicate function.

#### [`.contains(node) => Boolean`](ReactWrapper/contains.md)
Returns whether or not a given node is somewhere in the render tree.
#### [`.contains(nodeOrNodes) => Boolean`](ReactWrapper/contains.md)
Returns whether or not a given node or array of nodes is somewhere in the render tree.

#### [`.hasClass(className) => Boolean`](ReactWrapper/hasClass.md)
Returns whether or not the current root node has the given class name or not.
Expand Down
4 changes: 2 additions & 2 deletions docs/api/shallow.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ Remove nodes in the current wrapper that do not match the provided selector.
#### [`.filterWhere(predicate) => ShallowWrapper`](ShallowWrapper/filterWhere.md)
Remove nodes in the current wrapper that do not return true for the provided predicate function.

#### [`.contains(node) => Boolean`](ShallowWrapper/contains.md)
Returns whether or not a given node is somewhere in the render tree.
#### [`.contains(nodeOrNodes) => Boolean`](ShallowWrapper/contains.md)
Returns whether or not a given node or array of nodes is somewhere in the render tree.

#### [`.equals(node) => Boolean`](ShallowWrapper/equals.md)
Returns whether or not the current render tree is equal to the given node.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"check": "npm run lint && npm run test:all",
"build": "babel src --out-dir build",
"test:watch": "mocha --compilers js:babel-core/register --recursive src/**/__tests__/*.js --watch",
"test:only": "mocha --compilers js:babel-core/register --watch",
"test:describeWithDOMOnly": "mocha --compilers js:babel-core/register --recursive src/**/__tests__/describeWithDOM/describeWithDOMOnly-spec.js",
"test:describeWithDOMSkip": "mocha --compilers js:babel-core/register --recursive src/**/__tests__/describeWithDOM/describeWithDOMSkip-spec.js",
"test:all": "npm run react:13 && npm test && npm run test:describeWithDOMOnly && npm run test:describeWithDOMSkip && npm run react:14 && npm test && npm run test:describeWithDOMOnly && npm run test:describeWithDOMSkip",
Expand Down
15 changes: 11 additions & 4 deletions src/ReactWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
Simulate,
findDOMNode,
} from './react-compat';
import { mapNativeEventNames } from './Utils';
import {
mapNativeEventNames,
containsChildrenSubArray,
} from './Utils';

/**
* Finds all nodes in the current wrapper nodes' render trees that match the provided predicate
Expand Down Expand Up @@ -207,11 +210,15 @@ export default class ReactWrapper {
* expect(wrapper.contains(<div className="foo bar" />)).to.equal(true);
* ```
*
* @param {ReactElement} node
* @param {ReactElement|Array<ReactElement>} nodeOrNodes
* @returns {Boolean}
*/
contains(node) {
return findWhereUnwrapped(this, other => instEqual(node, other)).length > 0;
contains(nodeOrNodes) {
const predicate = Array.isArray(nodeOrNodes)
? other => containsChildrenSubArray(instEqual, other, nodeOrNodes)
: other => instEqual(nodeOrNodes, other);

return findWhereUnwrapped(this, predicate).length > 0;
}

/**
Expand Down
11 changes: 8 additions & 3 deletions src/ShallowWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { flatten, unique, compact } from 'underscore';
import cheerio from 'cheerio';
import {
nodeEqual,
containsChildrenSubArray,
propFromEvent,
withSetStateAllowed,
propsOfNode,
Expand Down Expand Up @@ -198,11 +199,15 @@ export default class ShallowWrapper {
* expect(wrapper.contains(<div className="foo bar" />)).to.equal(true);
* ```
*
* @param {ReactElement} node
* @param {ReactElement|Array<ReactElement>} nodeOrNodes
* @returns {Boolean}
*/
contains(node) {
return findWhereUnwrapped(this, other => nodeEqual(node, other)).length > 0;
contains(nodeOrNodes) {
const predicate = Array.isArray(nodeOrNodes)
? other => containsChildrenSubArray(nodeEqual, other, nodeOrNodes)
: other => nodeEqual(nodeOrNodes, other);

return findWhereUnwrapped(this, predicate).length > 0;
}

/**
Expand Down
26 changes: 22 additions & 4 deletions src/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import { isEqual } from 'underscore';
import {
isDOMComponent,
findDOMNode,
childrenToArray,
} from './react-compat';
import {
REACT013,
REACT014,
} from './version';

export function propsOfNode(node) {
if (REACT013) {
return (node && node._store && node._store.props) || {};
if (REACT013 && node && node._store) {
return (node._store.props) || {};
}
return (node && node.props) || {};
}
Expand Down Expand Up @@ -64,15 +65,16 @@ export function nodeEqual(a, b) {
if (a === b) return true;
if (!a || !b) return false;
if (a.type !== b.type) return false;

const left = propsOfNode(a);
const leftKeys = Object.keys(left);
const right = propsOfNode(b);
for (let i = 0; i < leftKeys.length; i++) {
const prop = leftKeys[i];
if (!(prop in right)) return false;
if (prop === 'children') {
if (!childrenEqual(left.children, right.children)) return false;
if (!childrenEqual(childrenToArray(left.children), childrenToArray(right.children))) {
return false;
}
} else if (right[prop] === left[prop]) {
// continue;
} else if (typeof right[prop] === typeof left[prop] && typeof left[prop] === 'object') {
Expand All @@ -89,6 +91,22 @@ export function nodeEqual(a, b) {
return false;
}

export function containsChildrenSubArray(match, node, subArray) {
const children = childrenOfNode(node);
return children.some((_, i) => arraysEqual(match, children.slice(i, subArray.length), subArray));
}

function arraysEqual(match, left, right) {
return left.length === right.length && left.every((el, i) => match(el, right[i]));
}

function childrenOfNode(node) {
const props = propsOfNode(node);
const { children } = props;
return childrenToArray(children);
}


// 'click' => 'onClick'
// 'mouseEnter' => 'onMouseEnter'
export function propFromEvent(event) {
Expand Down
22 changes: 22 additions & 0 deletions src/__tests__/ReactWrapper-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,28 @@ describeWithDOM('mount', () => {
expect(wrapper.contains(b)).to.equal(true);
});

it('should do something with arrays of nodes', () => {
const wrapper = mount(
<div>
<span>Hello</span>
<div>Goodbye</div>
<span>More</span>
</div>
);
const fails = [
<span>wrong</span>,
<div>Goodbye</div>,
];

const passes = [
<span>Hello</span>,
<div>Goodbye</div>,
];

expect(wrapper.contains(fails)).to.equal(false);
expect(wrapper.contains(passes)).to.equal(true);
});

});

describe('.find(selector)', () => {
Expand Down
22 changes: 22 additions & 0 deletions src/__tests__/ShallowWrapper-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,28 @@ describe('shallow', () => {
expect(wrapper.contains(<div>{5}</div>)).to.equal(true);
});

it('should do something with arrays of nodes', () => {
const wrapper = shallow(
<div>
<span>Hello</span>
<div>Goodbye</div>
<span>More</span>
</div>
);
const fails = [
<span>wrong</span>,
<div>Goodbye</div>,
];

const passes = [
<span>Hello</span>,
<div>Goodbye</div>,
];

expect(wrapper.contains(fails)).to.equal(false);
expect(wrapper.contains(passes)).to.equal(true);
});

});

describe('.equals(node)', () => {
Expand Down
23 changes: 21 additions & 2 deletions src/react-compat.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint react/no-deprecated: 0 */
import { REACT013 } from './version';

let TestUtils;
Expand All @@ -7,9 +8,12 @@ let renderIntoDocument;
let findDOMNode;
let React;
let ReactContext;
let childrenToArray;

React = require('react');

if (REACT013) {
renderToStaticMarkup = require('react').renderToStaticMarkup;
React = require('react');
renderToStaticMarkup = React.renderToStaticMarkup;
/* eslint-disable react/no-deprecated */
findDOMNode = React.findDOMNode;
/* eslint-enable react/no-deprecated */
Expand All @@ -33,6 +37,19 @@ if (REACT013) {
// this fixes some issues in React 0.13 with setState and jsdom...
// see issue: https://github.com/airbnb/enzyme/issues/27
require('react/lib/ExecutionEnvironment').canUseDOM = true;

// in 0.13, a Children.toArray function was not exported. Make our own instead.
childrenToArray = (children) => {
const results = [];
if (children !== undefined && children !== null && children !== false) {
React.Children.forEach(children, (el) => {
if (el !== undefined && el !== null && el !== false) {
results.push(el);
}
});
}
return results;
};
} else {
renderToStaticMarkup = require('react-dom/server').renderToStaticMarkup;
findDOMNode = require('react-dom').findDOMNode;
Expand Down Expand Up @@ -74,6 +91,7 @@ if (REACT013) {
};
};
renderIntoDocument = TestUtils.renderIntoDocument;
childrenToArray = React.Children.toArray;
}

const {
Expand Down Expand Up @@ -102,4 +120,5 @@ export {
Simulate,
findDOMNode,
findAllInRenderedTree,
childrenToArray,
};

0 comments on commit b4cc71d

Please sign in to comment.