Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Fragment support for shallow .find and .findWhere #1733

Merged
merged 2 commits into from
Aug 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Portal,
} from 'react-is';
import { EnzymeAdapter } from 'enzyme';
import { typeOfNode } from 'enzyme/build/Utils';
import {
displayNameOfNode,
elementToTree,
Expand Down Expand Up @@ -388,6 +389,10 @@ class ReactSixteenAdapter extends EnzymeAdapter {
return isValidElementType(object);
}

isFragment(fragment) {
return typeOfNode(fragment) === Fragment;
}

createElement(...args) {
return React.createElement(...args);
}
Expand Down
12 changes: 11 additions & 1 deletion packages/enzyme-test-suite/test/Adapter-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
Profiler,
} from './_helpers/react-compat';
import { is } from './_helpers/version';
import { itIf, describeWithDOM } from './_helpers';
import { itIf, describeWithDOM, describeIf } from './_helpers';

const { adapter } = get();

Expand Down Expand Up @@ -906,4 +906,14 @@ describe('Adapter', () => {
expect(getDisplayName(<Profiler />)).to.equal('Profiler');
});
});

describeIf(is('>= 16.2'), 'determines if node isFragment', () => {
it('correctly identifies Fragment', () => {
expect(adapter.isFragment(<Fragment />)).to.equal(true);
});

it('correctly identifies a non-Fragment', () => {
expect(adapter.isFragment(<div />)).to.equal(false);
});
});
});
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