Skip to content

Commit

Permalink
accounts for Fragment type nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
madicap authored and ljharb committed Aug 5, 2018
1 parent ff8fde2 commit 54498a0
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 9 deletions.
94 changes: 94 additions & 0 deletions packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,63 @@ describeWithDOM('mount', () => {
expect(elements.filter('i')).to.have.lengthOf(2);
});
});

describeIf(is('>= 16.2'), 'with fragments', () => {
const NestedFragmentComponent = () => (
<div className="container">
<React.Fragment>
<span>A span</span>
<span>B span</span>
<div>A div</div>
<React.Fragment>
<span>C span</span>
</React.Fragment>
</React.Fragment>
<span>D span</span>
</div>
);

it('should find descendant span inside React.Fragment', () => {
const wrapper = mount(<NestedFragmentComponent />);
expect(wrapper.find('.container span')).to.have.lengthOf(4);
});

it('should not find nonexistent p inside React.Fragment', () => {
const wrapper = mount(<NestedFragmentComponent />);
expect(wrapper.find('.container p')).to.have.lengthOf(0);
});

it('should find direct child span inside React.Fragment', () => {
const wrapper = mount(<NestedFragmentComponent />);
expect(wrapper.find('.container > span')).to.have.lengthOf(4);
});

it('should handle adjacent sibling selector inside React.Fragment', () => {
const wrapper = mount(<NestedFragmentComponent />);
expect(wrapper.find('.container span + div')).to.have.lengthOf(1);
});

it('should handle general sibling selector inside React.Fragment', () => {
const wrapper = mount(<NestedFragmentComponent />);
expect(wrapper.find('.container div ~ span')).to.have.lengthOf(2);
});

itIf(is('>= 16.4.1'), 'should handle fragments with no content', () => {
const EmptyFragmentComponent = () => (
<div className="container">
<React.Fragment>
<React.Fragment />
</React.Fragment>
</div>
);

const wrapper = mount(<EmptyFragmentComponent />);

expect(wrapper.find('.container > span')).to.have.lengthOf(0);
expect(wrapper.find('.container span')).to.have.lengthOf(0);
expect(wrapper.children()).to.have.lengthOf(1);
});
});
});

describe('.findWhere(predicate)', () => {
Expand Down Expand Up @@ -1175,6 +1232,43 @@ describeWithDOM('mount', () => {
expect(foundNotSpan.type()).to.equal('i');
});

describeIf(is('>= 16.2'), 'with fragments', () => {
it('finds nodes', () => {
class FragmentFoo extends React.Component {
render() {
return (
<div>
<React.Fragment>
<span data-foo={this.props.selector} />
<i data-foo={this.props.selector} />
<React.Fragment>
<i data-foo={this.props.selector} />
</React.Fragment>
</React.Fragment>
<span data-foo={this.props.selector} />
</div>
);
}
}

const selector = 'blah';
const wrapper = mount(<FragmentFoo selector={selector} />);
const foundSpans = wrapper.findWhere(n => (
n.type() === 'span' && n.props()['data-foo'] === selector
));
expect(foundSpans).to.have.lengthOf(2);
expect(foundSpans.get(0).type).to.equal('span');
expect(foundSpans.get(1).type).to.equal('span');

const foundNotSpans = wrapper.findWhere(n => (
n.type() !== 'span' && n.props()['data-foo'] === selector
));
expect(foundNotSpans).to.have.lengthOf(2);
expect(foundNotSpans.get(0).type).to.equal('i');
expect(foundNotSpans.get(1).type).to.equal('i');
});
});

it('finds nodes when conditionally rendered', () => {
class Foo extends React.Component {
render() {
Expand Down
99 changes: 98 additions & 1 deletion packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import {
import getAdapter from 'enzyme/build/getAdapter';

import './_helpers/setupAdapters';
import { createClass, createContext } from './_helpers/react-compat';
import {
createClass,
createContext,
Fragment,
} from './_helpers/react-compat';
import {
describeIf,
itIf,
Expand Down Expand Up @@ -957,6 +961,62 @@ describe('shallow', () => {
expect(elements.filter('i')).to.have.lengthOf(2);
});
});

describeIf(is('>= 16.2'), 'works with fragments', () => {
const NestedFragmentComponent = () => (
<div className="container">
<Fragment>
<span>A span</span>
<span>B span</span>
<div>A div</div>
<Fragment>
<span>C span</span>
</Fragment>
</Fragment>
<span>D span</span>
</div>
);

it('should find descendant span inside React.Fragment', () => {
const wrapper = shallow(<NestedFragmentComponent />);
expect(wrapper.find('.container span')).to.have.lengthOf(4);
});

it('should not find nonexistent p inside React.Fragment', () => {
const wrapper = shallow(<NestedFragmentComponent />);
expect(wrapper.find('.container p')).to.have.lengthOf(0);
});

it('should find direct child span inside React.Fragment', () => {
const wrapper = shallow(<NestedFragmentComponent />);
expect(wrapper.find('.container > span')).to.have.lengthOf(4);
});

it('should handle adjacent sibling selector inside React.Fragment', () => {
const wrapper = shallow(<NestedFragmentComponent />);
expect(wrapper.find('.container span + div')).to.have.lengthOf(1);
});

it('should handle general sibling selector inside React.Fragment', () => {
const wrapper = shallow(<NestedFragmentComponent />);
expect(wrapper.find('.container div ~ span')).to.have.lengthOf(2);
});

it('should handle fragments with no content', () => {
const EmptyFragmentComponent = () => (
<div className="container">
<Fragment>
<Fragment />
</Fragment>
</div>
);
const wrapper = shallow(<EmptyFragmentComponent />);

expect(wrapper.find('.container > span')).to.have.lengthOf(0);
expect(wrapper.find('.container span')).to.have.lengthOf(0);
expect(wrapper.children()).to.have.lengthOf(0);
});
});
});

describe('.findWhere(predicate)', () => {
Expand Down Expand Up @@ -1028,6 +1088,43 @@ describe('shallow', () => {
expect(foundNotSpan.type()).to.equal('i');
});

describeIf(is('>= 16.2'), 'with fragments', () => {
it('finds nodes', () => {
class FragmentFoo extends React.Component {
render() {
return (
<div>
<Fragment>
<span data-foo={this.props.selector} />
<i data-foo={this.props.selector} />
<Fragment>
<i data-foo={this.props.selector} />
</Fragment>
</Fragment>
<span data-foo={this.props.selector} />
</div>
);
}
}

const selector = 'blah';
const wrapper = shallow(<FragmentFoo selector={selector} />);
const foundSpans = wrapper.findWhere(n => (
n.type() === 'span' && n.props()['data-foo'] === selector
));
expect(foundSpans).to.have.lengthOf(2);
expect(foundSpans.get(0).type).to.equal('span');
expect(foundSpans.get(1).type).to.equal('span');

const foundNotSpans = wrapper.findWhere(n => (
n.type() !== 'span' && n.props()['data-foo'] === selector
));
expect(foundNotSpans).to.have.lengthOf(2);
expect(foundNotSpans.get(0).type).to.equal('i');
expect(foundNotSpans.get(1).type).to.equal('i');
});
});

it('finds nodes when conditionally rendered', () => {
class Foo extends React.Component {
render() {
Expand Down
26 changes: 22 additions & 4 deletions packages/enzyme/src/RSTTraversal.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@ import flat from 'array.prototype.flat';
import entries from 'object.entries';
import isSubset from 'is-subset';
import functionName from 'function.prototype.name';
import getAdapter from './getAdapter';

export function propsOfNode(node) {
return (node && node.props) || {};
}

export function childrenOfNode(node) {
if (!node) return [];
return Array.isArray(node.rendered) ? flat(node.rendered, 1) : [node.rendered];

const adapter = getAdapter();
const adapterHasIsFragment = adapter && adapter.isFragment && (typeof adapter.isFragment === 'function');

const renderedArray = Array.isArray(node.rendered) ? flat(node.rendered, 1) : [node.rendered];

// React adapters before 16 will not have isFragment
if (!adapterHasIsFragment) {
return renderedArray;
}

return flat(renderedArray.map((currentChild) => {
// If the node is a Fragment, we want to return its children, not the fragment itself
if (adapter.isFragment(currentChild)) {
return childrenOfNode(currentChild);
}

return currentChild;
}), 1);
}

export function hasClassName(node, className) {
Expand Down Expand Up @@ -52,9 +71,8 @@ export function findParentNode(root, targetNode) {
if (!node.rendered) {
return false;
}
return Array.isArray(node.rendered)
? node.rendered.indexOf(targetNode) !== -1
: node.rendered === targetNode;

return childrenOfNode(node).indexOf(targetNode) !== -1;
},
);
return results[0] || null;
Expand Down
10 changes: 6 additions & 4 deletions packages/enzyme/src/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,9 @@ function matchAdjacentSiblings(nodes, predicate, root) {
if (!parent) {
return matches;
}
const nodeIndex = parent.rendered.indexOf(node);
const adjacentSibling = parent.rendered[nodeIndex + 1];
const parentChildren = childrenOfNode(parent);
const nodeIndex = parentChildren.indexOf(node);
const adjacentSibling = parentChildren[nodeIndex + 1];
// No sibling
if (!adjacentSibling) {
return matches;
Expand All @@ -313,8 +314,9 @@ function matchGeneralSibling(nodes, predicate, root) {
if (!parent) {
return matches;
}
const nodeIndex = parent.rendered.indexOf(node);
const youngerSiblings = parent.rendered.slice(nodeIndex + 1);
const parentChildren = childrenOfNode(parent);
const nodeIndex = parentChildren.indexOf(node);
const youngerSiblings = parentChildren.slice(nodeIndex + 1);
return matches.concat(youngerSiblings.filter(predicate));
}, nodes);
}
Expand Down

0 comments on commit 54498a0

Please sign in to comment.