,
+ );
+ expect(wrapper.find('a[href="https://www.foo.com"]')).to.have.lengthOf(1);
+ expect(wrapper.find('a[href="foo.com"]')).to.have.lengthOf(1);
+ });
+ });
+ });
+});
diff --git a/packages/enzyme/package.json b/packages/enzyme/package.json
index 7f7b444b3..f1b519151 100644
--- a/packages/enzyme/package.json
+++ b/packages/enzyme/package.json
@@ -39,7 +39,7 @@
"object.assign": "^4.0.4",
"object.entries": "^1.0.4",
"raf": "^3.3.2",
- "uuid": "^3.1.0"
+ "rst-selector-parser": "^2.2.0"
},
"devDependencies": {
"babel-cli": "^6.24.1",
diff --git a/packages/enzyme/src/ComplexSelector.js b/packages/enzyme/src/ComplexSelector.js
deleted file mode 100644
index 9bd31e55f..000000000
--- a/packages/enzyme/src/ComplexSelector.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import split from 'lodash/split';
-
-export default class ComplexSelector {
- constructor(buildPredicate, findWhereUnwrapped, childrenOfNode) {
- this.buildPredicate = buildPredicate;
- this.findWhereUnwrapped = findWhereUnwrapped;
- this.childrenOfNode = childrenOfNode;
- }
-
- getSelectors(selector) { // eslint-disable-line class-methods-use-this
- const selectors = split(selector, / (?=(?:(?:[^"]*"){2})*[^"]*$)/);
- return selectors.reduce((list, sel) => {
- if (sel === '+' || sel === '~') {
- const temp = list.pop();
- return list.concat(sel, temp);
- }
-
- return list.concat(sel);
- }, []);
- }
-
- handleSelectors(selectors, wrapper) {
- const recurseSelector = (offset, fn, pre) => {
- const predicate = pre || this.buildPredicate(selectors[offset]);
- const nextWrapper = this.findWhereUnwrapped(wrapper, predicate, fn);
- const nextSelectors = selectors.slice(offset + 1);
- return this.handleSelectors(nextSelectors, nextWrapper);
- };
-
- const buildSiblingPredicate = (first, second) => {
- const firstPredicate = this.buildPredicate(first);
- const secondPredicate = this.buildPredicate(second);
-
- return (child) => {
- if (firstPredicate(child)) {
- return sibling => secondPredicate(sibling);
- }
-
- return false;
- };
- };
-
- let predicate;
- let selectSiblings;
-
- if (selectors.length) {
- switch (selectors[0]) {
- case '>':
- return recurseSelector(1, this.treeFilterDirect());
- case '+':
- predicate = buildSiblingPredicate(selectors[1], selectors[2]);
- selectSiblings = (children, pre, results, idx) => {
- const adjacent = children[idx + 1];
- if (pre(adjacent)) { results.push(adjacent); }
- };
-
- return recurseSelector(2, this.treeFindSiblings(selectSiblings), predicate);
- case '~':
- predicate = buildSiblingPredicate(selectors[1], selectors[2]);
- selectSiblings = (children, pre, results, idx) =>
- children.slice(idx + 1).map(child =>
- (pre(child) ? results.push(child) : null),
- );
-
- return recurseSelector(2, this.treeFindSiblings(selectSiblings), predicate);
- default:
- return recurseSelector(0);
- }
- }
-
- return wrapper;
- }
-
- find(selector, wrapper) {
- if (typeof selector === 'string') {
- const selectors = this.getSelectors(selector);
-
- return this.handleSelectors(selectors, wrapper);
- }
-
- const predicate = this.buildPredicate(selector);
- return this.findWhereUnwrapped(wrapper, predicate);
- }
-
- treeFilterDirect() {
- return (tree, fn) => this.childrenOfNode(tree).filter(child => fn(child));
- }
-
- treeFindSiblings(selectSiblings) {
- return (tree, fn) => {
- const results = [];
- const list = [this.childrenOfNode(tree)];
-
- const traverseChildren = children => children.forEach((child, i) => {
- const secondPredicate = fn(child);
-
- list.push(this.childrenOfNode(child));
-
- if (secondPredicate) {
- selectSiblings(children, secondPredicate, results, i);
- }
- });
-
- while (list.length) {
- traverseChildren(list.shift());
- }
-
- return results;
- };
- }
-}
diff --git a/packages/enzyme/src/RSTTraversal.js b/packages/enzyme/src/RSTTraversal.js
index 730be555c..9b55cc434 100644
--- a/packages/enzyme/src/RSTTraversal.js
+++ b/packages/enzyme/src/RSTTraversal.js
@@ -1,16 +1,7 @@
-import isEmpty from 'lodash/isEmpty';
import flatten from 'lodash/flatten';
import isSubset from 'is-subset';
import functionName from 'function.prototype.name';
-import {
- splitSelector,
- isCompoundSelector,
- selectorType,
- AND,
- SELECTOR,
- nodeHasType,
- nodeHasProperty,
-} from './Utils';
+import { nodeHasProperty } from './Utils';
export function propsOfNode(node) {
return (node && node.props) || {};
@@ -44,6 +35,31 @@ export function treeFilter(tree, fn) {
return results;
}
+/**
+ * To support sibling selectors we need to be able to find
+ * the siblings of a node. The easiest way to do that is find
+ * the parent of the node and access its children.
+ *
+ * This would be unneeded if the RST spec included sibling pointers
+ * such as node.nextSibling and node.prevSibling
+ * @param {*} root
+ * @param {*} targetNode
+ */
+export function findParentNode(root, targetNode) {
+ const results = treeFilter(
+ root,
+ (node) => {
+ if (!node.rendered) {
+ return false;
+ }
+ return Array.isArray(node.rendered)
+ ? node.rendered.indexOf(targetNode) !== -1
+ : node.rendered === targetNode;
+ },
+ );
+ return results[0] || null;
+}
+
function pathFilter(path, fn) {
return path.filter(tree => treeFilter(tree, fn).length !== 0);
}
@@ -86,49 +102,6 @@ export function nodeMatchesObjectProps(node, props) {
return isSubset(propsOfNode(node), props);
}
-export function buildPredicate(selector) {
- switch (typeof selector) {
- case 'function':
- // selector is a component constructor
- return node => node && node.type === selector;
-
- case 'string':
- if (isCompoundSelector.test(selector)) {
- return AND(splitSelector(selector).map(buildPredicate));
- }
-
- switch (selectorType(selector)) {
- case SELECTOR.CLASS_TYPE:
- return node => hasClassName(node, selector.slice(1));
-
- case SELECTOR.ID_TYPE:
- return node => nodeHasId(node, selector.slice(1));
-
- case SELECTOR.PROP_TYPE: {
- const propKey = selector.split(/\[([a-zA-Z][a-zA-Z_\d\-:]*?)(=|])/)[1];
- const propValue = selector.split(/=(.*?)]/)[1];
-
- return node => nodeHasProperty(node, propKey, propValue);
- }
- default:
- // selector is a string. match to DOM tag or constructor displayName
- return node => nodeHasType(node, selector);
- }
-
- 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('Enzyme::Selector expects a string, object, or Component Constructor');
- }
-}
-
-
export function getTextFromNode(node) {
if (node === null || node === undefined) {
return '';
diff --git a/packages/enzyme/src/ReactWrapper.js b/packages/enzyme/src/ReactWrapper.js
index b4fd2fb53..818b44e33 100644
--- a/packages/enzyme/src/ReactWrapper.js
+++ b/packages/enzyme/src/ReactWrapper.js
@@ -3,7 +3,6 @@ import flatten from 'lodash/flatten';
import unique from 'lodash/uniq';
import compact from 'lodash/compact';
-import ComplexSelector from './ComplexSelector';
import {
containsChildrenSubArray,
typeOfNode,
@@ -25,9 +24,10 @@ import {
childrenOfNode,
parentsOfNode,
treeFilter,
- buildPredicate,
} from './RSTTraversal';
+import { buildPredicate, reduceTreeBySelector } from './selectors';
+
const noop = () => {};
const NODE = sym('__node__');
@@ -36,7 +36,6 @@ const RENDERER = sym('__renderer__');
const UNRENDERED = sym('__unrendered__');
const ROOT = sym('__root__');
const OPTIONS = sym('__options__');
-const COMPLEX_SELECTOR = sym('__complexSelector__');
/**
* Finds all nodes in the current wrapper nodes' render trees that match the provided predicate
@@ -101,11 +100,6 @@ class ReactWrapper {
this.length = this[NODES].length;
}
privateSet(this, OPTIONS, root ? root[OPTIONS] : options);
- privateSet(this, COMPLEX_SELECTOR, new ComplexSelector(
- buildPredicate,
- findWhereUnwrapped,
- childrenOfNode,
- ));
}
/**
@@ -464,7 +458,7 @@ class ReactWrapper {
* @returns {ReactWrapper}
*/
find(selector) {
- return this[COMPLEX_SELECTOR].find(selector, this);
+ return reduceTreeBySelector(selector, this);
}
/**
diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js
index 3d89215af..af1c3e068 100644
--- a/packages/enzyme/src/ShallowWrapper.js
+++ b/packages/enzyme/src/ShallowWrapper.js
@@ -3,7 +3,6 @@ import unique from 'lodash/uniq';
import compact from 'lodash/compact';
import cheerio from 'cheerio';
-import ComplexSelector from './ComplexSelector';
import {
nodeEqual,
nodeMatches,
@@ -29,8 +28,8 @@ import {
childrenOfNode,
parentsOfNode,
treeFilter,
- buildPredicate,
} from './RSTTraversal';
+import { buildPredicate, reduceTreeBySelector } from './selectors';
const NODE = sym('__node__');
const NODES = sym('__nodes__');
@@ -38,7 +37,6 @@ const RENDERER = sym('__renderer__');
const UNRENDERED = sym('__unrendered__');
const ROOT = sym('__root__');
const OPTIONS = sym('__options__');
-const COMPLEX_SELECTOR = sym('__complexSelector__');
/**
* Finds all nodes in the current wrapper nodes' render trees that match the provided predicate
* function.
@@ -145,11 +143,6 @@ class ShallowWrapper {
this.length = this[NODES].length;
}
privateSet(this, OPTIONS, root ? root[OPTIONS] : options);
- privateSet(this, COMPLEX_SELECTOR, new ComplexSelector(
- buildPredicate,
- findWhereUnwrapped,
- childrenOfNode,
- ));
}
getNodeInternal() {
@@ -541,7 +534,7 @@ class ShallowWrapper {
* @returns {ShallowWrapper}
*/
find(selector) {
- return this[COMPLEX_SELECTOR].find(selector, this);
+ return reduceTreeBySelector(selector, this);
}
/**
diff --git a/packages/enzyme/src/Utils.js b/packages/enzyme/src/Utils.js
index 12f36de51..84c9de011 100644
--- a/packages/enzyme/src/Utils.js
+++ b/packages/enzyme/src/Utils.js
@@ -1,7 +1,6 @@
/* eslint no-use-before-define:0 */
import isEqual from 'lodash/isEqual';
import is from 'object-is';
-import uuidv4 from 'uuid/v4';
import entries from 'object.entries';
import functionName from 'function.prototype.name';
import configuration from './configuration';
@@ -206,135 +205,12 @@ export function withSetStateAllowed(fn) {
}
}
-export function splitSelector(selector) {
- // step 1: make a map of all quoted strings with a uuid
- const quotedSegments = selector.split(/[^" ]+|("[^"]*")|.*/g)
- .filter(Boolean)
- .reduce((obj, match) => ({ ...obj, [match]: uuidv4() }), {});
-
- const splits = selector
- // step 2: replace all quoted strings with the uuid, so we don't have to properly parse them
- .replace(/[^" ]+|("[^"]*")|.*/g, x => quotedSegments[x] || x)
- // step 3: split as best we can without a proper parser
- .split(/(?=\.|\[.*])|(?=#|\[#.*])/)
- // step 4: restore the quoted strings by swapping back the uuid's for the original segments
- .map((selectorSegment) => {
- let restoredSegment = selectorSegment;
- entries(quotedSegments).forEach(([k, v]) => {
- restoredSegment = restoredSegment.replace(v, k);
- });
- return restoredSegment;
- });
-
- if (splits.length === 1 && splits[0] === selector) {
- // splitSelector expects selector to be "splittable"
- throw new TypeError('Enzyme::Selector received what appears to be a malformed string selector');
- }
-
- return splits;
-}
-
-
-const containsQuotes = /'|"/;
-const containsColon = /:/;
-
-
-export function isPseudoClassSelector(selector) {
- if (containsColon.test(selector)) {
- if (!containsQuotes.test(selector)) {
- return true;
- }
- const tokens = selector.split(containsQuotes);
- return tokens.some((token, i) =>
- containsColon.test(token) && i % 2 === 0,
- );
- }
- return false;
-}
-
-function selectorError(selector, type = '') {
- return new TypeError(
- `Enzyme received a ${type} CSS selector ('${selector}') that it does not currently support`,
- );
-}
-
-export const isCompoundSelector = /^[.#]?-?[_a-z]+[_a-z0-9-]*[.[#]/i;
-
-const isPropSelector = /^\[.*]$/;
-
-export const SELECTOR = {
- CLASS_TYPE: 0,
- ID_TYPE: 1,
- PROP_TYPE: 2,
-};
-
-export function selectorType(selector) {
- if (isPseudoClassSelector(selector)) {
- throw selectorError(selector, 'pseudo-class');
- }
- if (selector[0] === '.') {
- return SELECTOR.CLASS_TYPE;
- } else if (selector[0] === '#') {
- return SELECTOR.ID_TYPE;
- } else if (isPropSelector.test(selector)) {
- return SELECTOR.PROP_TYPE;
- }
- return undefined;
-}
-
export function AND(fns) {
const fnsReversed = fns.slice().reverse();
return x => fnsReversed.every(fn => fn(x));
}
-export function coercePropValue(propName, propValue) {
- // can be undefined
- if (propValue === undefined) {
- return propValue;
- }
-
- // can be the empty string
- if (propValue === '') {
- return propValue;
- }
-
- if (propValue === 'NaN') {
- return NaN;
- }
-
- if (propValue === 'null') {
- return null;
- }
-
- const trimmedValue = propValue.trim();
-
- // if propValue includes quotes, it should be
- // treated as a string
- // eslint override pending https://github.com/eslint/eslint/issues/7472
- // eslint-disable-next-line no-useless-escape
- if (/^(['"]).*\1$/.test(trimmedValue)) {
- return trimmedValue.slice(1, -1);
- }
-
- const numericPropValue = +trimmedValue;
-
- // if parseInt is not NaN, then we've wanted a number
- if (!is(NaN, numericPropValue)) {
- return numericPropValue;
- }
-
- // coerce to boolean
- if (trimmedValue === 'true') return true;
- if (trimmedValue === 'false') return false;
-
- // user provided an unquoted string value
- throw new TypeError(
- `Enzyme::Unable to parse selector '[${propName}=${propValue}]'. ` +
- `Perhaps you forgot to escape a string? Try '[${propName}="${trimmedValue}"]' instead.`,
- );
-}
-
-export function nodeHasProperty(node, propKey, stringifiedPropValue) {
+export function nodeHasProperty(node, propKey, propValue) {
const nodeProps = propsOfNode(node);
const descriptor = Object.getOwnPropertyDescriptor(nodeProps, propKey);
if (descriptor && descriptor.get) {
@@ -342,8 +218,6 @@ export function nodeHasProperty(node, propKey, stringifiedPropValue) {
}
const nodePropValue = nodeProps[propKey];
- const propValue = coercePropValue(propKey, stringifiedPropValue);
-
if (nodePropValue === undefined) {
return false;
}
diff --git a/packages/enzyme/src/selectors.js b/packages/enzyme/src/selectors.js
new file mode 100644
index 000000000..fb096f741
--- /dev/null
+++ b/packages/enzyme/src/selectors.js
@@ -0,0 +1,315 @@
+import { createParser } from 'rst-selector-parser';
+import isEmpty from 'lodash/isEmpty';
+import unique from 'lodash/uniq';
+import {
+ treeFilter,
+ nodeHasId,
+ findParentNode,
+ nodeMatchesObjectProps,
+ childrenOfNode,
+ hasClassName,
+} from './RSTTraversal';
+import { nodeHasType, nodeHasProperty } from './Utils';
+// our CSS selector parser instance
+const parser = createParser();
+
+// Combinators that allow you to chance selectors
+const CHILD = 'childCombinator';
+const ADJACENT_SIBLING = 'adjacentSiblingCombinator';
+const GENERAL_SIBLING = 'generalSiblingCombinator';
+const DESCENDANT = 'descendantCombinator';
+
+// Selectors for targeting elements
+const SELECTOR = 'selector';
+const TYPE_SELECTOR = 'typeSelector';
+const CLASS_SELECTOR = 'classSelector';
+const ID_SELECTOR = 'idSelector';
+const ATTRIBUTE_PRESENCE = 'attributePresenceSelector';
+const ATTRIBUTE_VALUE = 'attributeValueSelector';
+// @TODO we dont support these, throw if they are used
+const PSEUDO_CLASS = 'pseudoClassSelector';
+const PSEUDO_ELEMENT = 'pseudoElementSelector';
+
+/**
+ * Calls reduce on a array of nodes with the passed
+ * function, returning only unique results.
+ * @param {Function} fn
+ * @param {Array} nodes
+ */
+function uniqueReduce(fn, nodes) {
+ return unique(nodes.reduce(fn, []));
+}
+
+/**
+ * Takes a CSS selector and returns a set of tokens parsed
+ * by scalpel.
+ * @param {String} selector
+ */
+function safelyGenerateTokens(selector) {
+ try {
+ return parser.parse(selector);
+ } catch (err) {
+ throw new Error(`Failed to parse selector: ${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
+ */
+function nodeMatchesToken(node, token) {
+ if (node === null || typeof node === 'string') {
+ return false;
+ }
+ switch (token.type) {
+ /**
+ * Match against the className prop
+ * @example '.active' matches
+ */
+ case CLASS_SELECTOR:
+ return hasClassName(node, token.name);
+ /**
+ * Simple type matching
+ * @example 'div' matches
+ */
+ case TYPE_SELECTOR:
+ return nodeHasType(node, token.name);
+ /**
+ * Match against the `id` prop
+ * @example '#nav' matches
+ */
+ case ID_SELECTOR:
+ return nodeHasId(node, token.name);
+ /**
+ * Matches if an attribute is present, regardless
+ * of its value
+ * @example '[disabled]' matches
+ */
+ case ATTRIBUTE_PRESENCE:
+ return nodeHasProperty(node, token.name);
+ /**
+ * Matches if an attribute is present with the
+ * provided value
+ * @example '[data-foo=foo]' matches
+ */
+ case ATTRIBUTE_VALUE:
+ return nodeHasProperty(node, token.name, token.value);
+ case PSEUDO_ELEMENT:
+ case PSEUDO_CLASS:
+ throw new Error('Enzyme::Selector does not support psuedo-element or psuedo-class selectors.');
+ default:
+ throw new Error(`Unknown token type: ${token.type}`);
+ }
+}
+
+/**
+ * Returns a predicate function that checks if a
+ * node matches every token in the body of a selector
+ * token.
+ * @param {Token} token
+ */
+function buildPredicateFromToken(token) {
+ return node => token.body.every(
+ bodyToken => nodeMatchesToken(node, bodyToken),
+ );
+}
+
+/**
+ * Returns whether a parsed selector is a complex selector, which
+ * is defined as a selector that contains combinators.
+ * @param {Array} tokens
+ */
+function isComplexSelector(tokens) {
+ return tokens.some(token => token.type !== SELECTOR);
+}
+
+
+/**
+ * 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
+ */
+export function buildPredicate(selector) {
+ // If the selector is a function, check if the node's constructor matches
+ if (typeof selector === 'function') {
+ return node => node && node.type === selector;
+ }
+ // If the selector is an non-empty object, treat the keys/values as props
+ if (typeof selector === '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',
+ );
+ }
+ // If the selector is a string, parse it as a simple CSS selector
+ if (typeof selector === 'string') {
+ const tokens = safelyGenerateTokens(selector);
+ if (isComplexSelector(tokens)) {
+ throw new TypeError('This method does not support complex CSS selectors');
+ }
+ // Simple selectors only have a single selector token
+ return buildPredicateFromToken(tokens[0]);
+ }
+ throw new TypeError('Enzyme::Selector expects a string, object, or Component Constructor');
+}
+
+/**
+ * Matches only nodes which are adjacent siblings (direct next sibling)
+ * against a predicate, returning those that match.
+ * @param {Array} nodes
+ * @param {Function} predicate
+ * @param {Node} root
+ */
+function matchAdjacentSiblings(nodes, predicate, root) {
+ return nodes.reduce((matches, node) => {
+ const parent = findParentNode(root, node);
+ // If there's no parent, there's no siblings
+ if (!parent) {
+ return matches;
+ }
+ const nodeIndex = parent.rendered.indexOf(node);
+ const adjacentSibling = parent.rendered[nodeIndex + 1];
+ // No sibling
+ if (!adjacentSibling) {
+ return matches;
+ }
+ if (predicate(adjacentSibling)) {
+ matches.push(adjacentSibling);
+ }
+ return matches;
+ }, []);
+}
+
+/**
+ * Matches only nodes which are general siblings (any sibling *after*)
+ * against a predicate, returning those that match.
+ * @param {Array} nodes
+ * @param {Function} predicate
+ * @param {Node} root
+ */
+function matchGeneralSibling(nodes, predicate, root) {
+ return uniqueReduce((matches, node) => {
+ const parent = findParentNode(root, node);
+ const nodeIndex = parent.rendered.indexOf(node);
+ parent.rendered.forEach((sibling, i) => {
+ if (i > nodeIndex && predicate(sibling)) {
+ matches.push(sibling);
+ }
+ });
+ return matches;
+ }, nodes);
+}
+
+/**
+ * Matches only nodes which are direct children (not grandchildren, etc.)
+ * against a predicate, returning those that match.
+ * @param {Array} nodes
+ * @param {Function} predicate
+ */
+function matchDirectChild(nodes, predicate) {
+ return uniqueReduce((matches, node) => {
+ const children = childrenOfNode(node);
+ children.forEach((child) => {
+ if (predicate(child)) {
+ matches.push(child);
+ }
+ });
+ return matches;
+ }, nodes);
+}
+
+/**
+ * Matches all descendant nodes against a predicate,
+ * returning those that match.
+ * @param {Array} nodes
+ * @param {Function} predicate
+ */
+function matchDescendant(nodes, predicate) {
+ return uniqueReduce(
+ (matches, node) => matches.concat(treeFilter(node, predicate)),
+ nodes,
+ );
+}
+
+/**
+ * Takes an RST and reduces it to a set of nodes matching
+ * 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
+ */
+export function reduceTreeBySelector(selector, wrapper) {
+ const root = wrapper.getNodeInternal();
+ let results = [];
+
+ if (typeof selector === 'function' || typeof selector === 'object') {
+ results = treeFilter(root, buildPredicate(selector));
+ } else if (typeof selector === 'string') {
+ const tokens = safelyGenerateTokens(selector);
+ let index = 0;
+ let token = null;
+ while (index < tokens.length) {
+ 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
+ * as we have to track where in the tree we are
+ * to determine if a selector node applies or not.
+ */
+ if (token.type === SELECTOR) {
+ const predicate = buildPredicateFromToken(token);
+ results = results.concat(treeFilter(root, predicate));
+ } else {
+ // We can assume there always all previously matched tokens since selectors
+ // cannot start with combinators.
+ const type = token.type;
+ // We assume the next token is a selector, so move the index
+ // forward and build the predicate.
+ index += 1;
+ token = tokens[index];
+ const predicate = buildPredicateFromToken(token);
+ // We match against only the nodes which have already been matched,
+ // since a combinator is meant to refine a previous selector.
+ switch (type) {
+ // The + combinator
+ case ADJACENT_SIBLING:
+ results = matchAdjacentSiblings(results, predicate, root);
+ break;
+ // The ~ combinator
+ case GENERAL_SIBLING:
+ results = matchGeneralSibling(results, predicate, root);
+ break;
+ // The > combinator
+ case CHILD:
+ results = matchDirectChild(results, predicate);
+ break;
+ // The ' ' (whitespace) combinator
+ case DESCENDANT: {
+ results = matchDescendant(results, predicate);
+ break;
+ }
+ default:
+ throw new Error(`Unkown combinator selector: ${type}`);
+ }
+ }
+ index += 1;
+ }
+ } else {
+ throw new TypeError('Enzyme::Selector expects a string, object, or Component Constructor');
+ }
+ return wrapper.wrap(results);
+}