Skip to content

Commit

Permalink
Support nested/multiple find calls
Browse files Browse the repository at this point in the history
  • Loading branch information
lelandrichardson committed Sep 18, 2017
1 parent 394da88 commit 3b739bd
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 29 deletions.
21 changes: 21 additions & 0 deletions packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,27 @@ describeWithDOM('mount', () => {
expect(wrapper.find('input.foo').length).to.equal(1);
});

it('should work on non-single nodes', () => {
const wrapper = mount(
<div className="a">
<div className="b">
<div className="c">Text</div>
<div className="c">Text</div>
<div className="c">Text</div>
</div>
<div className="b">
<div className="c">Text</div>
<div className="c">Text</div>
<div className="c">Text</div>
</div>
</div>,
);
expect(wrapper.find('.a').length).to.equal(1);
expect(wrapper.find('.b').length).to.equal(2);
expect(wrapper.find('.b').find('.c').length).to.equal(6);
});


it('should find an element based on a tag name and id', () => {
const wrapper = mount(
<div>
Expand Down
20 changes: 20 additions & 0 deletions packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,26 @@ describe('shallow', () => {
expect(wrapper.find('button').length).to.equal(1);
});

it('should work on non-single nodes', () => {
const wrapper = shallow(
<div className="a">
<div className="b">
<div className="c">Text</div>
<div className="c">Text</div>
<div className="c">Text</div>
</div>
<div className="b">
<div className="c">Text</div>
<div className="c">Text</div>
<div className="c">Text</div>
</div>
</div>,
);
expect(wrapper.find('.a').length).to.equal(1);
expect(wrapper.find('.b').length).to.equal(2);
expect(wrapper.find('.b').find('.c').length).to.equal(6);
});

it('should find component based on a react prop', () => {
const wrapper = shallow(
<div>
Expand Down
4 changes: 2 additions & 2 deletions packages/enzyme/src/ReactWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
treeFilter,
} from './RSTTraversal';

import { buildPredicate, reduceTreeBySelector } from './selectors';
import { buildPredicate, reduceTreesBySelector } from './selectors';

const noop = () => {};

Expand Down Expand Up @@ -467,7 +467,7 @@ class ReactWrapper {
* @returns {ReactWrapper}
*/
find(selector) {
return reduceTreeBySelector(selector, this);
return this.wrap(reduceTreesBySelector(selector, this.getNodesInternal()));
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/enzyme/src/ShallowWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
parentsOfNode,
treeFilter,
} from './RSTTraversal';
import { buildPredicate, reduceTreeBySelector } from './selectors';
import { buildPredicate, reduceTreesBySelector } from './selectors';

const NODE = sym('__node__');
const NODES = sym('__nodes__');
Expand Down Expand Up @@ -545,7 +545,7 @@ class ShallowWrapper {
* @returns {ShallowWrapper}
*/
find(selector) {
return reduceTreeBySelector(selector, this);
return this.wrap(reduceTreesBySelector(selector, this.getNodesInternal()));
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/enzyme/src/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ export function nodeHasProperty(node, propKey, propValue) {
}

export function displayNameOfNode(node) {
if (!node) return null;

const { type } = node;

if (!type) return null;
Expand Down
55 changes: 30 additions & 25 deletions packages/enzyme/src/selectors.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createParser } from 'rst-selector-parser';
import isEmpty from 'lodash/isEmpty';
import flatten from 'lodash/flatten';
import unique from 'lodash/uniq';
import {
treeFilter,
Expand Down Expand Up @@ -31,10 +32,10 @@ const PSEUDO_CLASS = 'pseudoClassSelector';
const PSEUDO_ELEMENT = 'pseudoElementSelector';

/**
* Calls reduce on a array of nodes with the passed
* Calls reduce on a array of nodes with the passed
* function, returning only unique results.
* @param {Function} fn
* @param {Array<Node>} nodes
* @param {Function} fn
* @param {Array<Node>} nodes
*/
function uniqueReduce(fn, nodes) {
return unique(nodes.reduce(fn, []));
Expand All @@ -43,7 +44,7 @@ function uniqueReduce(fn, nodes) {
/**
* Takes a CSS selector and returns a set of tokens parsed
* by scalpel.
* @param {String} selector
* @param {String} selector
*/
function safelyGenerateTokens(selector) {
try {
Expand All @@ -56,8 +57,8 @@ function safelyGenerateTokens(selector) {
/**
* Takes a node and a token and determines if the node
* matches the predicate defined by the token.
* @param {Node} node
* @param {Token} token
* @param {Node} node
* @param {Token} token
*/
function nodeMatchesToken(node, token) {
if (node === null || typeof node === 'string') {
Expand Down Expand Up @@ -108,7 +109,7 @@ function nodeMatchesToken(node, token) {
* Returns a predicate function that checks if a
* node matches every token in the body of a selector
* token.
* @param {Token} token
* @param {Token} token
*/
function buildPredicateFromToken(token) {
return node => token.body.every(
Expand All @@ -119,7 +120,7 @@ function buildPredicateFromToken(token) {
/**
* Returns whether a parsed selector is a complex selector, which
* is defined as a selector that contains combinators.
* @param {Array<Token>} tokens
* @param {Array<Token>} tokens
*/
function isComplexSelector(tokens) {
return tokens.some(token => token.type !== SELECTOR);
Expand All @@ -129,8 +130,8 @@ function isComplexSelector(tokens) {
/**
* Takes a component constructor, object, or string representing
* a simple selector and returns a predicate function that can
* be applied to a single node.
* @param {Function|Object|String} selector
* be applied to a single node.
* @param {Function|Object|String} selector
*/
export function buildPredicate(selector) {
// If the selector is a function, check if the node's constructor matches
Expand Down Expand Up @@ -161,8 +162,8 @@ export function buildPredicate(selector) {
/**
* Matches only nodes which are adjacent siblings (direct next sibling)
* against a predicate, returning those that match.
* @param {Array<Node>} nodes
* @param {Function} predicate
* @param {Array<Node>} nodes
* @param {Function} predicate
* @param {Node} root
*/
function matchAdjacentSiblings(nodes, predicate, root) {
Expand All @@ -188,8 +189,8 @@ function matchAdjacentSiblings(nodes, predicate, root) {
/**
* Matches only nodes which are general siblings (any sibling *after*)
* against a predicate, returning those that match.
* @param {Array<Node>} nodes
* @param {Function} predicate
* @param {Array<Node>} nodes
* @param {Function} predicate
* @param {Node} root
*/
function matchGeneralSibling(nodes, predicate, root) {
Expand All @@ -208,8 +209,8 @@ function matchGeneralSibling(nodes, predicate, root) {
/**
* Matches only nodes which are direct children (not grandchildren, etc.)
* against a predicate, returning those that match.
* @param {Array<Node>} nodes
* @param {Function} predicate
* @param {Array<Node>} nodes
* @param {Function} predicate
*/
function matchDirectChild(nodes, predicate) {
return uniqueReduce((matches, node) => {
Expand All @@ -226,8 +227,8 @@ function matchDirectChild(nodes, predicate) {
/**
* Matches all descendant nodes against a predicate,
* returning those that match.
* @param {Array<Node>} nodes
* @param {Function} predicate
* @param {Array<Node>} nodes
* @param {Function} predicate
*/
function matchDescendant(nodes, predicate) {
return uniqueReduce(
Expand All @@ -241,11 +242,10 @@ function matchDescendant(nodes, predicate) {
* the selector. The selector can be a simple selector, which
* is handled by `buildPredicate`, or a complex CSS selector which
* reduceTreeBySelector parses and reduces the tree based on the combinators.
* @param {Function|Object|String} selector
* @param {ReactWrapper|ShallowWrapper} wrapper
* @param {Function|Object|String} selector
* @param {RSTNode} wrapper
*/
export function reduceTreeBySelector(selector, wrapper) {
const root = wrapper.getNodeInternal();
export function reduceTreeBySelector(selector, root) {
let results = [];

if (typeof selector === 'function' || typeof selector === 'object') {
Expand All @@ -258,12 +258,12 @@ export function reduceTreeBySelector(selector, wrapper) {
token = tokens[index];
/**
* There are two types of tokens in a CSS selector:
*
*
* 1. Selector tokens. These target nodes directly, like
* type or attribute selectors. These are easy to apply
* because we can travserse the tree and return only
* the nodes that match the predicate.
*
*
* 2. Combinator tokens. These tokens chain together
* selector nodes. For example > for children, or +
* for adjecent siblings. These are harder to match
Expand Down Expand Up @@ -311,5 +311,10 @@ export function reduceTreeBySelector(selector, wrapper) {
} else {
throw new TypeError('Enzyme::Selector expects a string, object, or Component Constructor');
}
return wrapper.wrap(results);
return results;
}

export function reduceTreesBySelector(selector, roots) {
const results = roots.map(n => reduceTreeBySelector(selector, n));
return unique(flatten(results));
}

0 comments on commit 3b739bd

Please sign in to comment.