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

Allow .contains() to accept array of nodes. #154

Merged
merged 1 commit into from
Feb 2, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -52,7 +52,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 @@ -88,6 +88,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 @@ -115,6 +115,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,
};