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