From 150f5450b7a3673540e74947643c728643b6d2ec Mon Sep 17 00:00:00 2001 From: Leland Richardson Date: Mon, 1 Feb 2016 10:07:36 -0800 Subject: [PATCH] Allow .contains() to accept array of nodes. --- docs/README.md | 4 ++-- docs/api/ReactWrapper/contains.md | 23 +++++++++++++++++++++-- docs/api/ShallowWrapper/contains.md | 23 +++++++++++++++++++++-- docs/api/mount.md | 4 ++-- docs/api/shallow.md | 4 ++-- package.json | 1 + src/ReactWrapper.js | 15 +++++++++++---- src/ShallowWrapper.js | 11 ++++++++--- src/Utils.js | 26 ++++++++++++++++++++++---- src/__tests__/ReactWrapper-spec.js | 22 ++++++++++++++++++++++ src/__tests__/ShallowWrapper-spec.js | 22 ++++++++++++++++++++++ src/react-compat.js | 23 +++++++++++++++++++++-- 12 files changed, 155 insertions(+), 23 deletions(-) diff --git a/docs/README.md b/docs/README.md index 6ecf88f1b..19d35289a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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) @@ -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) diff --git a/docs/api/ReactWrapper/contains.md b/docs/api/ReactWrapper/contains.md index fed5eab43..4aad9303d 100644 --- a/docs/api/ReactWrapper/contains.md +++ b/docs/api/ReactWrapper/contains.md @@ -1,4 +1,4 @@ -# `.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. @@ -6,7 +6,7 @@ the one passed in. #### Arguments -1. `node` (`ReactElement`): The node whose presence you are detecting in the current instance's +1. `nodeOrNodes` (`ReactElement|Array`): The node or array of nodes whose presence you are detecting in the current instance's render tree. @@ -26,6 +26,25 @@ const wrapper = mount(); expect(wrapper.contains(
)).to.equal(true); ``` +```jsx +const wrapper = mount( +
+ Hello +
Goodbye
+ Again +
+); +const passes = [ + Hello, +
Goodbye
, +]; + +expect(wrapper.contains([ + Hello, +
Goodbye
, +])).to.equal(true); +``` + #### Common Gotchas diff --git a/docs/api/ShallowWrapper/contains.md b/docs/api/ShallowWrapper/contains.md index 18fe29746..1c764748d 100644 --- a/docs/api/ShallowWrapper/contains.md +++ b/docs/api/ShallowWrapper/contains.md @@ -1,4 +1,4 @@ -# `.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. @@ -6,7 +6,7 @@ the one passed in. #### Arguments -1. `node` (`ReactElement`): The node whose presence you are detecting in the current instance's +1. `nodeOrNodes` (`ReactElement|Array`): The node or array of nodes whose presence you are detecting in the current instance's render tree. @@ -26,6 +26,25 @@ const wrapper = shallow(); expect(wrapper.contains(
)).to.equal(true); ``` +```jsx +const wrapper = shallow( +
+ Hello +
Goodbye
+ Again +
+); +const passes = [ + Hello, +
Goodbye
, +]; + +expect(wrapper.contains([ + Hello, +
Goodbye
, +])).to.equal(true); +``` + #### Common Gotchas diff --git a/docs/api/mount.md b/docs/api/mount.md index eaeeeafa3..59c8b241c 100644 --- a/docs/api/mount.md +++ b/docs/api/mount.md @@ -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. diff --git a/docs/api/shallow.md b/docs/api/shallow.md index cd33df670..11b2ba446 100644 --- a/docs/api/shallow.md +++ b/docs/api/shallow.md @@ -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. diff --git a/package.json b/package.json index 45b71820c..a7ae60dd3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/ReactWrapper.js b/src/ReactWrapper.js index 20ee10882..cfd76ac05 100644 --- a/src/ReactWrapper.js +++ b/src/ReactWrapper.js @@ -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 @@ -207,11 +210,15 @@ export default class ReactWrapper { * expect(wrapper.contains(
)).to.equal(true); * ``` * - * @param {ReactElement} node + * @param {ReactElement|Array} 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; } /** diff --git a/src/ShallowWrapper.js b/src/ShallowWrapper.js index 9e800f062..e362bb56c 100644 --- a/src/ShallowWrapper.js +++ b/src/ShallowWrapper.js @@ -3,6 +3,7 @@ import { flatten, unique, compact } from 'underscore'; import cheerio from 'cheerio'; import { nodeEqual, + containsChildrenSubArray, propFromEvent, withSetStateAllowed, propsOfNode, @@ -198,11 +199,15 @@ export default class ShallowWrapper { * expect(wrapper.contains(
)).to.equal(true); * ``` * - * @param {ReactElement} node + * @param {ReactElement|Array} 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; } /** diff --git a/src/Utils.js b/src/Utils.js index f89897240..0f9021c1c 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -3,6 +3,7 @@ import { isEqual } from 'underscore'; import { isDOMComponent, findDOMNode, + childrenToArray, } from './react-compat'; import { REACT013, @@ -10,8 +11,8 @@ import { } 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) || {}; } @@ -64,7 +65,6 @@ 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); @@ -72,7 +72,9 @@ export function nodeEqual(a, b) { 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') { @@ -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) { diff --git a/src/__tests__/ReactWrapper-spec.js b/src/__tests__/ReactWrapper-spec.js index c4e139cd2..ebcc74cca 100644 --- a/src/__tests__/ReactWrapper-spec.js +++ b/src/__tests__/ReactWrapper-spec.js @@ -88,6 +88,28 @@ describeWithDOM('mount', () => { expect(wrapper.contains(b)).to.equal(true); }); + it('should do something with arrays of nodes', () => { + const wrapper = mount( +
+ Hello +
Goodbye
+ More +
+ ); + const fails = [ + wrong, +
Goodbye
, + ]; + + const passes = [ + Hello, +
Goodbye
, + ]; + + expect(wrapper.contains(fails)).to.equal(false); + expect(wrapper.contains(passes)).to.equal(true); + }); + }); describe('.find(selector)', () => { diff --git a/src/__tests__/ShallowWrapper-spec.js b/src/__tests__/ShallowWrapper-spec.js index 718df00c0..68ce26806 100644 --- a/src/__tests__/ShallowWrapper-spec.js +++ b/src/__tests__/ShallowWrapper-spec.js @@ -115,6 +115,28 @@ describe('shallow', () => { expect(wrapper.contains(
{5}
)).to.equal(true); }); + it('should do something with arrays of nodes', () => { + const wrapper = shallow( +
+ Hello +
Goodbye
+ More +
+ ); + const fails = [ + wrong, +
Goodbye
, + ]; + + const passes = [ + Hello, +
Goodbye
, + ]; + + expect(wrapper.contains(fails)).to.equal(false); + expect(wrapper.contains(passes)).to.equal(true); + }); + }); describe('.equals(node)', () => { diff --git a/src/react-compat.js b/src/react-compat.js index 5f8e8fbe7..ca471ee60 100644 --- a/src/react-compat.js +++ b/src/react-compat.js @@ -1,3 +1,4 @@ +/* eslint react/no-deprecated: 0 */ import { REACT013 } from './version'; let TestUtils; @@ -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 */ @@ -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; @@ -74,6 +91,7 @@ if (REACT013) { }; }; renderIntoDocument = TestUtils.renderIntoDocument; + childrenToArray = React.Children.toArray; } const { @@ -102,4 +120,5 @@ export { Simulate, findDOMNode, findAllInRenderedTree, + childrenToArray, };