diff --git a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
index 79ede478f..b603a2cfb 100644
--- a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
+++ b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
@@ -21,6 +21,7 @@ import {
Portal,
} from 'react-is';
import { EnzymeAdapter } from 'enzyme';
+import { typeOfNode } from 'enzyme/build/Utils';
import {
displayNameOfNode,
elementToTree,
@@ -388,6 +389,10 @@ class ReactSixteenAdapter extends EnzymeAdapter {
return isValidElementType(object);
}
+ isFragment(fragment) {
+ return typeOfNode(fragment) === Fragment;
+ }
+
createElement(...args) {
return React.createElement(...args);
}
diff --git a/packages/enzyme-test-suite/test/Adapter-spec.jsx b/packages/enzyme-test-suite/test/Adapter-spec.jsx
index dacd6f051..c6da4c920 100644
--- a/packages/enzyme-test-suite/test/Adapter-spec.jsx
+++ b/packages/enzyme-test-suite/test/Adapter-spec.jsx
@@ -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();
@@ -906,4 +906,14 @@ describe('Adapter', () => {
expect(getDisplayName()).to.equal('Profiler');
});
});
+
+ describeIf(is('>= 16.2'), 'determines if node isFragment', () => {
+ it('correctly identifies Fragment', () => {
+ expect(adapter.isFragment()).to.equal(true);
+ });
+
+ it('correctly identifies a non-Fragment', () => {
+ expect(adapter.isFragment(
)).to.equal(false);
+ });
+ });
});
diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
index c30203034..4592068bb 100644
--- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
@@ -1104,6 +1104,63 @@ describeWithDOM('mount', () => {
expect(elements.filter('i')).to.have.lengthOf(2);
});
});
+
+ describeIf(is('>= 16.2'), 'with fragments', () => {
+ const NestedFragmentComponent = () => (
+
+
+ A span
+ B span
+ A div
+
+ C span
+
+
+
D span
+
+ );
+
+ it('should find descendant span inside React.Fragment', () => {
+ const wrapper = mount();
+ expect(wrapper.find('.container span')).to.have.lengthOf(4);
+ });
+
+ it('should not find nonexistent p inside React.Fragment', () => {
+ const wrapper = mount();
+ expect(wrapper.find('.container p')).to.have.lengthOf(0);
+ });
+
+ it('should find direct child span inside React.Fragment', () => {
+ const wrapper = mount();
+ expect(wrapper.find('.container > span')).to.have.lengthOf(4);
+ });
+
+ it('should handle adjacent sibling selector inside React.Fragment', () => {
+ const wrapper = mount();
+ expect(wrapper.find('.container span + div')).to.have.lengthOf(1);
+ });
+
+ it('should handle general sibling selector inside React.Fragment', () => {
+ const wrapper = mount();
+ expect(wrapper.find('.container div ~ span')).to.have.lengthOf(2);
+ });
+
+ itIf(is('>= 16.4.1'), 'should handle fragments with no content', () => {
+ const EmptyFragmentComponent = () => (
+
+
+
+
+
+ );
+
+ const wrapper = mount();
+
+ 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)', () => {
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ }
+
+ const selector = 'blah';
+ const wrapper = mount();
+ 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() {
diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
index 8e5786a1e..edff45b3f 100644
--- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
@@ -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,
@@ -957,6 +961,62 @@ describe('shallow', () => {
expect(elements.filter('i')).to.have.lengthOf(2);
});
});
+
+ describeIf(is('>= 16.2'), 'works with fragments', () => {
+ const NestedFragmentComponent = () => (
+
+
+ A span
+ B span
+ A div
+
+ C span
+
+
+
D span
+
+ );
+
+ it('should find descendant span inside React.Fragment', () => {
+ const wrapper = shallow();
+ expect(wrapper.find('.container span')).to.have.lengthOf(4);
+ });
+
+ it('should not find nonexistent p inside React.Fragment', () => {
+ const wrapper = shallow();
+ expect(wrapper.find('.container p')).to.have.lengthOf(0);
+ });
+
+ it('should find direct child span inside React.Fragment', () => {
+ const wrapper = shallow();
+ expect(wrapper.find('.container > span')).to.have.lengthOf(4);
+ });
+
+ it('should handle adjacent sibling selector inside React.Fragment', () => {
+ const wrapper = shallow();
+ expect(wrapper.find('.container span + div')).to.have.lengthOf(1);
+ });
+
+ it('should handle general sibling selector inside React.Fragment', () => {
+ const wrapper = shallow();
+ expect(wrapper.find('.container div ~ span')).to.have.lengthOf(2);
+ });
+
+ it('should handle fragments with no content', () => {
+ const EmptyFragmentComponent = () => (
+
+
+
+
+
+ );
+ const wrapper = shallow();
+
+ 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)', () => {
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ }
+
+ const selector = 'blah';
+ const wrapper = shallow();
+ 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() {
diff --git a/packages/enzyme/src/RSTTraversal.js b/packages/enzyme/src/RSTTraversal.js
index f4206e8d3..7b1837346 100644
--- a/packages/enzyme/src/RSTTraversal.js
+++ b/packages/enzyme/src/RSTTraversal.js
@@ -2,6 +2,7 @@ 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) || {};
@@ -9,7 +10,25 @@ export function propsOfNode(node) {
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) {
@@ -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;
diff --git a/packages/enzyme/src/selectors.js b/packages/enzyme/src/selectors.js
index 8a34bfdaa..9f590e80c 100644
--- a/packages/enzyme/src/selectors.js
+++ b/packages/enzyme/src/selectors.js
@@ -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;
@@ -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);
}