Skip to content

Commit

Permalink
Added support for descendent, >, ~, and + (#217)
Browse files Browse the repository at this point in the history
  • Loading branch information
dustinsanders authored and lelandrichardson committed May 7, 2016
1 parent 80f02d4 commit 77eb751
Show file tree
Hide file tree
Showing 9 changed files with 339 additions and 48 deletions.
7 changes: 3 additions & 4 deletions docs/api/selector.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,16 @@ input#input-name
label[foo=true]
```

Are all valid selectors in enzyme. At this time, however, any contextual CSS selector syntax that
requires knowledge of a node's ancestors or siblings is not yet supported. For instance:
Enzyme also gives support for the following contextual selectors

```
.foo .bar
.foo > .bar
.foo + .bar
.foo ~ .bar
.foo input
```

Are all unsupported selectors in enzyme.


**Want more CSS support?**

Expand Down
125 changes: 125 additions & 0 deletions src/ComplexSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import split from 'lodash/split';

export default class ComplexSelector {
constructor(buildPredicate, findWhereUnwrapped, childrenOfNode) {
this.buildPredicate = buildPredicate;
this.findWhereUnwrapped = findWhereUnwrapped;
this.childrenOfNode = childrenOfNode;
}

getSelectors(selector) {
const cleaned = selector.replace(/\s{2,}/g, ' ');
const selectors = split(cleaned, ' ');
return selectors.reduce((list, sel) => {
if (sel === '+' || sel === '~') {
const temp = list.pop();
list.push(sel, temp);
return list;
}

list.push(sel);
return list;
}, []);
}

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) => {
const results = [];
this.childrenOfNode(tree).forEach(child => {
if (fn(child)) {
results.push(child);
}
});

return results;
};
}

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;
};
}

}
5 changes: 0 additions & 5 deletions src/MountedTraversal.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import {
nodeEqual,
propsOfNode,
isFunctionalComponent,
isSimpleSelector,
splitSelector,
selectorError,
selectorType,
isCompoundSelector,
AND,
Expand Down Expand Up @@ -208,9 +206,6 @@ export function buildInstPredicate(selector) {
return inst => instHasType(inst, selector);

case 'string':
if (!isSimpleSelector(selector)) {
throw selectorError(selector);
}
if (isCompoundSelector.test(selector)) {
return AND(splitSelector(selector).map(buildInstPredicate));
}
Expand Down
17 changes: 12 additions & 5 deletions src/ReactWrapper.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import ComplexSelector from './ComplexSelector';
import cheerio from 'cheerio';
import flatten from 'lodash/flatten';
import unique from 'lodash/uniq';
Expand Down Expand Up @@ -35,10 +36,11 @@ import {
*
* @param {ReactWrapper} wrapper
* @param {Function} predicate
* @param {Function} filter
* @returns {ReactWrapper}
*/
function findWhereUnwrapped(wrapper, predicate) {
return wrapper.flatMap(n => treeFilter(n.node, predicate));
function findWhereUnwrapped(wrapper, predicate, filter = treeFilter) {
return wrapper.flatMap(n => filter(n.node, predicate));
}

/**
Expand Down Expand Up @@ -91,6 +93,11 @@ export default class ReactWrapper {
this.length = this.nodes.length;
}
this.options = options;
this.complexSelector = new ComplexSelector(
buildInstPredicate,
findWhereUnwrapped,
childrenOfInst
);
}

/**
Expand Down Expand Up @@ -270,8 +277,7 @@ export default class ReactWrapper {
* @returns {ReactWrapper}
*/
find(selector) {
const predicate = buildInstPredicate(selector);
return findWhereUnwrapped(this, predicate);
return this.complexSelector.find(selector, this);
}

/**
Expand Down Expand Up @@ -641,7 +647,8 @@ export default class ReactWrapper {
const nodes = this.nodes.map((n, i) => fn.call(this, this.wrap(n), i));
const flattened = flatten(nodes, true);
const uniques = unique(flattened);
return this.wrap(uniques);
const compacted = compact(uniques);
return this.wrap(compacted);
}

/**
Expand Down
5 changes: 0 additions & 5 deletions src/ShallowTraversal.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import isSubset from 'is-subset';
import {
coercePropValue,
propsOfNode,
isSimpleSelector,
splitSelector,
selectorError,
isCompoundSelector,
selectorType,
AND,
Expand Down Expand Up @@ -117,9 +115,6 @@ export function buildPredicate(selector) {
return node => node && node.type === selector;

case 'string':
if (!isSimpleSelector(selector)) {
throw selectorError(selector);
}
if (isCompoundSelector.test(selector)) {
return AND(splitSelector(selector).map(buildPredicate));
}
Expand Down
13 changes: 8 additions & 5 deletions src/ShallowWrapper.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import ComplexSelector from './ComplexSelector';
import flatten from 'lodash/flatten';
import unique from 'lodash/uniq';
import compact from 'lodash/compact';
Expand Down Expand Up @@ -33,10 +34,11 @@ import {
*
* @param {ShallowWrapper} wrapper
* @param {Function} predicate
* @param {Function} filter
* @returns {ShallowWrapper}
*/
function findWhereUnwrapped(wrapper, predicate) {
return wrapper.flatMap(n => treeFilter(n.node, predicate));
function findWhereUnwrapped(wrapper, predicate, filter = treeFilter) {
return wrapper.flatMap(n => filter(n.node, predicate));
}

/**
Expand Down Expand Up @@ -79,6 +81,7 @@ export default class ShallowWrapper {
this.length = this.nodes.length;
}
this.options = options;
this.complexSelector = new ComplexSelector(buildPredicate, findWhereUnwrapped, childrenOfNode);
}

/**
Expand Down Expand Up @@ -236,8 +239,7 @@ export default class ShallowWrapper {
* @returns {ShallowWrapper}
*/
find(selector) {
const predicate = buildPredicate(selector);
return findWhereUnwrapped(this, predicate);
return this.complexSelector.find(selector, this);
}

/**
Expand Down Expand Up @@ -629,7 +631,8 @@ export default class ShallowWrapper {
const nodes = this.nodes.map((n, i) => fn.call(this, this.wrap(n), i));
const flattened = flatten(nodes, true);
const uniques = unique(flattened);
return this.wrap(uniques);
const compacted = compact(uniques);
return this.wrap(compacted);
}

/**
Expand Down
Loading

0 comments on commit 77eb751

Please sign in to comment.